공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

3. 코드 예제

실행 방법입니다. 외부 jar 는 필요 없습니다.

powershell
cd java-src\extension\07_rest_json
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 Main --demo   # 11개 시나리오 자동 실행
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 Main         # 8080 에 서버만. curl 로 직접 호출

예제 1: 라우터 — 경로 패턴과 변수 추출

java
// "/members/{id}" 를 "/members/([^/]+)" 정규식으로 컴파일하고 {id} 위치를 기억한다
public Router add(String method, String pathPattern, Handler handler) {
    List<String> names = new ArrayList<>();
    StringBuilder regex = new StringBuilder();
    for (String seg : pathPattern.split("/")) {
        if (seg.isEmpty()) continue;
        regex.append('/');
        if (seg.startsWith("{") && seg.endsWith("}")) {
            names.add(seg.substring(1, seg.length() - 1));
            regex.append("([^/]+)");                  // '/' 를 뺀 한 구간이 변수 값
        } else {
            regex.append(Pattern.quote(seg));
        }
    }
    routes.add(new Route(method, Pattern.compile(regex.toString()), names, handler));
    return this;
}
java
router.add("GET", "/members/{id}", (ex, vars) -> {
    System.out.println("경로 변수: " + vars);
});
// 요청 GET /members/42
// 출력: 경로 변수: {id=42}

패턴이 매칭돼도 메서드가 다르면 405 후보로 남기고, 아예 매칭이 없으면 404 후보로 남깁니다. 이 두 상태를 구분해야 "경로는 맞는데 메서드가 틀렸다"는 정확한 신호를 클라이언트에 줄 수 있습니다.

예제 2: CRUD 핸들러 — 생성과 조회

java
private void create(HttpExchange ex, Map<String, String> vars) throws IOException {
    Map<String, Object> body = readJson(ex);
    var errors = Validation.validateCreate(body);
    if (!errors.isEmpty()) { ApiError.send(ex, 400, "VALIDATION_ERROR", "입력 값을 확인하세요", errors); return; }
    Member m = store.create((String) body.get("name"), (String) body.get("email"), toInteger(body.get("age")));
    ex.getResponseHeaders().set("Location", "/members/" + m.id());   // 201 은 새 자원 위치를 Location 에
    ex.getResponseHeaders().set("ETag", "\"" + m.version() + "\"");
    Router.writeJson(ex, 201, m.toJson());
}
java
// 데모 [1] 실행 결과
// 출력: 201 Location=/members/1 {"id":"1","name":"홍길동","email":"hong@test.com","age":30,"version":1,"createdAt":"2026-09-09T09:02:20.292485300Z"}

id 가 문자열 "1" 로 나가는 이유는 Member.toJson() 이 String.valueOf(id) 를 쓰기 때문입니다(2.4절 long 정밀도 문제). createdAt 은 Instant.toString() 이 만드는 ISO-8601 UTC 문자열입니다.

예제 3: 검증과 에러 응답

java
public static List<FieldError> validateCreate(Map<String, Object> body) {
    List<FieldError> errors = new ArrayList<>();
    checkName(body.get("name"), errors);     // 필수, 2~30자
    checkEmail(body.get("email"), errors);   // 필수, 정규식
    checkAge(body.get("age"), errors);       // 선택, 0~150
    return errors;
}
java
// 데모 [2]: name 없음, email 형식 오류, age 범위 초과를 한 번에
// 출력: 400 {"code":"VALIDATION_ERROR","message":"입력 값을 확인하세요","errors":[
//   {"reason":"필수 항목입니다","field":"name"},
//   {"reason":"이메일 형식이 아닙니다","field":"email"},
//   {"reason":"0~150 사이여야 합니다","field":"age"}],"traceId":"312691f1", ...}

세 검증기가 각각 독립적으로 오류를 모으므로 한 번의 요청으로 세 개의 필드 오류를 동시에 돌려줍니다. 첫 오류에서 멈추면 사용자는 폼을 세 번 고쳐 세 번 재요청해야 합니다.

예제 4: 페이징과 정렬

java
Page<Member> p = Page.of(all, page, size);
Map<String, Object> body = new LinkedHashMap<>();
body.put("page", p.page());
body.put("size", p.size());
body.put("totalElements", p.totalElements());
body.put("totalPages", p.totalPages());
body.put("content", p.content().stream().map(Member::toJson).toList());
Router.writeJson(ex, 200, body);
java
// 데모 [8]: 회원 5명 중 GET /members?page=0&size=2&sort=name,asc
// 출력: 200 {"page":0,"size":2,"totalElements":5,"totalPages":3,"content":[
//   {"id":"2","name":"김영희", ...},{"id":"4","name":"박지훈", ...}]}

size 를 100 으로 상한을 두는 이유는(MemberApi.list) 클라이언트가 size=1000000 을 보내 전체 데이터를 한 번에 덤프하는 것을 막기 위해서입니다. 정렬 키는 화이트리스트(switch 문)로만 받아 임의 필드로 SQL 을 조작하는 경로를 막습니다.

예제 5: HttpClient 로 호출하는 클라이언트

java
static HttpResponse<String> call(HttpClient client, String method, String url, String jsonBody) throws Exception {
    var b = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(5));
    if (jsonBody == null) b.method(method, HttpRequest.BodyPublishers.noBody());
    else b.header("Content-Type", "application/json; charset=UTF-8")
            .method(method, HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8));
    return client.send(b.build(), HttpResponse.BodyHandlers.ofString());
}
java
var r = call(client, "PUT", base + "/members/1", "{\"name\":\"홍길동\",\"email\":\"hong2@test.com\",\"age\":31,\"version\":1}");
// 출력: 200 {"id":"1","name":"홍길동","email":"hong2@test.com","age":31,"version":2, ...}

Content-Type 은 본문이 있을 때만 붙입니다. GET/DELETE 처럼 본문이 없는 요청에 억지로 빈 JSON 을 붙이면 서버가 "본문이 있는데 형식이 이상하다"고 오해할 수 있습니다.

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다. 외부 jar 를 쓰는 레슨은 java-src/lib 를 클래스패스에 넣습니다.

cd java-src\extension\07_rest_json
javac -encoding UTF-8 *.java && java Main

:: 외부 jar 가 필요한 레슨
javac -encoding UTF-8 -cp "..\..\lib\*;." *.java && java -cp "..\..\lib\*;." Main
코드 예제
  • 예제 1: 라우터 — 경로 패턴과 변수 추출
  • 예제 2: CRUD 핸들러 — 생성과 조회
  • 예제 3: 검증과 에러 응답
  • 예제 4: 페이징과 정렬
  • 예제 5: HttpClient 로 호출하는 클라이언트
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제