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());
}// 데모 [7]: PATCH /members/1 {"age":33} (name, email 은 그대로)
// 출력: 200 {"id":"1","name":"홍길동","email":"[email protected]","age":33,"version":3, ...}PUT 은 전체 필드가 필수(validateCreate)이고, PATCH 는 온 필드만 검증합니다(validatePatch). containsKey 로 "필드가 왔는지"와 "필드 값이 null 인지"를 구분해야, {"age": null}(나이를 지운다)과 필드 자체를 안 보낸 경우(그대로 둔다)를 구별할 수 있습니다.
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: 항목마다 결과가 다르다// 데모 [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 를 남겨야 클라이언트가 원본 목록에서 몇 번째가 실패했는지 짚을 수 있습니다.
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());
}// 데모 [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 표준 헤더로 냅니다.
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);
}
}// 데모 로그 한 줄 (Main 콘솔에 실제로 찍힘)
// 출력: [b93714dc] POST /members -> 201 (144ms)이 traceId 를 에러 응답의 traceId 필드(2.5절)에도 그대로 실었습니다. 사용자가 "에러 코드 b93714dc 가 떴어요"라고 보고하면, 서버 로그에서 같은 문자열로 정확히 그 요청을 찾을 수 있습니다. Spring Boot 는 MDC(Mapped Diagnostic Context)와 OncePerRequestFilter 로 같은 일을 합니다.