공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 07 / 22

REST API 서버와 JSON

CRUD, 상태 코드, 에러 규격, 검증, 페이징
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

4. 응용 변형 예제

변형 1: PATCH 부분 수정

java
private void patch(HttpExchange ex, Map<String, String> vars) throws IOException {
    long id = Long.parseLong(vars.get("id"));
    var found = store.find(id);
    if (found.isEmpty()) { ApiError.send(ex, 404, "NOT_FOUND", "회원 없음: id=" + id, null); return; }
    Map<String, Object> body = readJson(ex);
    var errors = Validation.validatePatch(body);           // 들어온 필드만 검증
    if (!errors.isEmpty()) { ApiError.send(ex, 400, "VALIDATION_ERROR", "입력 값을 확인하세요", errors); return; }
    Member cur = found.get();
    Member updated = store.update(id, cur.version(), c -> new Member(
            c.id(),
            body.containsKey("name") ? (String) body.get("name") : c.name(),   // 없으면 기존 값 유지
            body.containsKey("email") ? (String) body.get("email") : c.email(),
            body.containsKey("age") ? toInteger(body.get("age")) : c.age(),
            c.version(), c.createdAt()));
    Router.writeJson(ex, 200, updated.toJson());
}
java
// 데모 [7]: PATCH /members/1 {"age":33}  (name, email 은 그대로)
// 출력: 200 {"id":"1","name":"홍길동","email":"hong2@test.com","age":33,"version":3, ...}

PUT 은 전체 필드가 필수(validateCreate)이고, PATCH 는 온 필드만 검증합니다(validatePatch). containsKey 로 "필드가 왔는지"와 "필드 값이 null 인지"를 구분해야, {"age": null}(나이를 지운다)과 필드 자체를 안 보낸 경우(그대로 둔다)를 구별할 수 있습니다.

변형 2: 벌크 등록 부분 실패 — 207 보고서

java
for (int i = 0; i < rawItems.size(); i++) {
    Map<String, Object> item = ...;
    var errors = Validation.validateCreate(item);
    if (!errors.isEmpty()) {
        result.put("status", 400);
        result.put("errors", errors.stream().map(e -> Map.of("field", e.field(), "reason", e.reason())).toList());
        fail++;
    } else {
        Member m = store.create((String) item.get("name"), (String) item.get("email"), toInteger(item.get("age")));
        result.put("status", 201);
        result.put("id", String.valueOf(m.id()));
        ok++;
    }
}
Router.writeJson(ex, 207, res);   // 207 Multi-Status: 항목마다 결과가 다르다
java
// 데모 [9]: 3건 중 2번째(이름 1자, 이메일 형식 오류) 실패
// 출력: 207 {"successCount":2,"failCount":1,"results":[
//   {"index":0,"status":201,"id":"6"},
//   {"index":1,"status":400,"errors":[{"reason":"2~30자여야 합니다","field":"name"}, ...]},
//   {"index":2,"status":201,"id":"7"}]}

전체를 하나의 트랜잭션으로 묶어 "1건이라도 실패하면 전부 롤백"할 수도 있지만, 대량 등록(엑셀 업로드 등)에서는 "되는 것은 처리하고 안 되는 것만 보고"가 더 실용적입니다. index 를 남겨야 클라이언트가 원본 목록에서 몇 번째가 실패했는지 짚을 수 있습니다.

변형 3: ETag 조건부 요청

java
private void get(HttpExchange ex, Map<String, String> vars) throws IOException {
    long id = Long.parseLong(vars.get("id"));
    Member m = store.find(id).orElseThrow(...);
    String etag = "\"" + m.version() + "\"";
    ex.getResponseHeaders().set("ETag", etag);
    if (etag.equals(ex.getRequestHeaders().getFirst("If-None-Match"))) {   // 캐시가 최신 → 본문 재전송 생략
        ex.sendResponseHeaders(304, -1);
        return;
    }
    Router.writeJson(ex, 200, m.toJson());
}
java
// 데모 [4]: 1차 GET → ETag="1" 수신, 2차 GET (If-None-Match: "1")
// 출력: 1차 200 ETag="1" {...}
// 출력: 2차 If-None-Match 동일 → 304 (본문 0자)

304 는 본문을 아예 안 보내므로 대역폭을 아낍니다. PUT 에서는 같은 헤더를 If-Match 로 바꿔 쓰면 "내가 읽은 버전과 지금 서버 버전이 다르면 거부"(2.8절)가 되어, 이 레슨의 version 필드 방식과 같은 효과를 HTTP 표준 헤더로 냅니다.

변형 4: 요청 로깅·traceId 필터

java
final class LoggingFilter extends Filter {
    @Override public void doFilter(HttpExchange ex, Chain chain) throws IOException {
        String traceId = UUID.randomUUID().toString().substring(0, 8);
        ex.setAttribute("traceId", traceId);          // 핸들러·ApiError 가 이 속성을 읽는다
        long t0 = System.nanoTime();
        chain.doFilter(ex);                            // 다음 필터 → 실제 핸들러까지 실행되고 돌아옴
        long ms = (System.nanoTime() - t0) / 1_000_000;
        System.out.printf("  [%s] %s %s -> %d (%dms)%n",
                traceId, ex.getRequestMethod(), ex.getRequestURI().getPath(), ex.getResponseCode(), ms);
    }
}
java
// 데모 로그 한 줄 (Main 콘솔에 실제로 찍힘)
// 출력: [b93714dc] POST /members -> 201 (144ms)

이 traceId 를 에러 응답의 traceId 필드(2.5절)에도 그대로 실었습니다. 사용자가 "에러 코드 b93714dc 가 떴어요"라고 보고하면, 서버 로그에서 같은 문자열로 정확히 그 요청을 찾을 수 있습니다. Spring Boot 는 MDC(Mapped Diagnostic Context)와 OncePerRequestFilter 로 같은 일을 합니다.

응용 변형 예제
  • 변형 1: PATCH 부분 수정
  • 변형 2: 벌크 등록 부분 실패 — 207 보고서
  • 변형 3: ETag 조건부 요청
  • 변형 4: 요청 로깅·traceId 필터
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)