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

2. 핵심 원리

2.1 HTTP 요청/응답 해부

REST 통신은 요청 한 줄과 응답 한 줄로 요약됩니다. 각 부분이 하는 역할이 다릅니다.

부분 요청 예 역할
메서드 POST 하려는 동작(조회/생성/치환/부분수정/삭제)
경로 /members/42 어떤 자원인지(무엇을 하는지가 아니라)
쿼리 ?page=0&size=10 필터·페이징·정렬 같은 부가 조건
헤더 Content-Type: application/json 본문 형식, 인증 토큰, 캐시 조건 등 메타데이터
본문 {"name":"..."} 실제 데이터(GET/DELETE 에는 보통 없음)

응답도 구조가 같습니다. 상태 코드(성공/실패의 종류), 헤더(Content-Type, Location, ETag), 본문(데이터 또는 에러) 세 가지입니다. Content-Type 은 "내가 보내는 형식"을, Accept 는 "내가 받고 싶은 형식"을 뜻하며 둘은 반대 방향입니다.

text
요청  POST /members HTTP/1.1
      Content-Type: application/json; charset=UTF-8
      Accept: application/json

      {"name":"홍길동","email":"hong@test.com","age":30}

응답  HTTP/1.1 201 Created
      Location: /members/1
      Content-Type: application/json; charset=UTF-8

      {"id":"1","name":"홍길동", ...}

2.2 REST 자원 설계 — 명사, 복수형, 계층, 동사 금지

경로는 "무엇"을 나타내고, 메서드가 "무엇을 한다"를 나타냅니다. /getMember, /createMember 처럼 경로에 동사를 넣으면 메서드와 의미가 겹칩니다.

나쁜 예 좋은 예 이유
GET /getMember?id=1 GET /members/1 동사 중복. 메서드가 이미 "조회"를 말한다
POST /deleteMember DELETE /members/1 삭제는 DELETE 메서드로
GET /member GET /members 컬렉션은 복수형. 단건은 /members/{id}
GET /members/1/getOrders GET /members/1/orders 하위 자원은 경로 계층으로

멱등성은 "같은 요청을 여러 번 보내도 서버 상태가 한 번 보낸 것과 같은가"입니다. 재시도 설계에서 결정적입니다.

메서드 멱등 설명
GET O 조회는 몇 번을 해도 상태가 안 바뀐다
PUT O 같은 값으로 덮어쓰기를 반복해도 결과가 같다
DELETE O 이미 지운 것을 또 지워도 "없음" 상태는 같다(응답 코드는 다를 수 있음)
POST X 같은 요청을 두 번 보내면 자원이 두 번 생긴다(중복 생성)
PATCH 보통 X "age": "+1" 같은 상대적 수정이면 X, 절대값 대입이면 O

네트워크가 끊겨 응답을 못 받았을 때, 멱등 메서드는 재시도가 안전하지만 POST 재시도는 중복 주문 같은 사고로 이어집니다. 그래서 결제 POST 에는 멱등키(idempotency key)를 따로 둡니다.

2.3 상태 코드 선택표

상태 코드는 클라이언트가 재시도할지, 사용자에게 폼 오류를 보여줄지, 로그인 화면으로 보낼지를 가르는 신호입니다.

코드 이름 언제
200 OK 조회·수정 성공, 본문 있음
201 Created 생성 성공. Location 헤더에 새 자원 경로
204 No Content 성공했지만 돌려줄 본문이 없음(삭제 등)
400 Bad Request 요청 형식·필수값이 잘못됨(클라이언트 책임)
401 Unauthorized 누구인지 모름(로그인 안 됨)
403 Forbidden 누군지는 알지만 권한 없음
404 Not Found 자원이 없음(경로의 id 가 없거나 경로 자체가 없음)
405 Method Not Allowed 경로는 있지만 그 메서드는 안 받음
409 Conflict 상태 충돌(중복 생성, 낙관적 잠금 버전 불일치)
415 Unsupported Media Type Content-Type 이 기대와 다름(JSON 기대인데 form)
422 Unprocessable Entity 형식은 맞지만 업무 규칙 위반(잔액 부족 등)
500 Internal Server Error 서버 버그·예외. 메시지를 그대로 노출하면 안 됨

400 과 422 를 실무에서 섞어 쓰는 팀이 많은데, 이 레슨은 "필수값 누락·형식 오류(이메일 아님, 길이 초과)"는 400, "형식은 맞지만 업무 규칙 위반(잔액 부족, 이미 마감된 주문 수정)"은 422 로 나눕니다. 409 는 "지금 상태와 충돌"(버전 불일치, 중복 키)에만 씁니다.

2.4 JSON 직렬화 규칙

자바 객체와 JSON 은 타입 체계가 다릅니다. Jackson(Spring Boot 기본 JSON 라이브러리)이 이 변환을 자동으로 하지만, 표를 모르면 프런트와 타입 문제로 다툽니다.

자바 타입 JSON 함정
null null Jackson 기본은 null 필드도 그대로 내려보낸다(@JsonInclude 로 생략 가능)
int, Integer 숫자 문제 없음
long, Long 숫자 JS Number 는 2^53 이상에서 정밀도를 잃는다. id·계좌번호는 문자열로 내보낸다
double, BigDecimal 숫자 금액은 double 대신 BigDecimal. JSON 도 문자열로 내보내는 편이 안전
LocalDate, Instant 문자열 ISO-8601(2026-09-09, 2026-09-09T09:00:00Z). 시간대 없는 문자열은 재앙
enum 문자열(기본) @JsonValue 로 다른 표현(코드값)으로 바꿀 수 있다
List, 배열 배열 [...] 순서 보장
중첩 객체 객체 {...} 깊은 중첩은 화면에 안 쓰는 필드까지 끌고 온다(DTO 로 평탄화 권장)

이름 표기도 언어마다 관례가 다릅니다. 자바는 memberName(카멜케이스), DB 컬럼은 member_name(스네이크케이스)이 흔합니다. Spring Boot 는 application.yml 에 아래처럼 설정하면 Jackson 이 자동 변환합니다.

yaml
spring:
  jackson:
    property-naming-strategy: SNAKE_CASE   # 자바 memberName ↔ JSON member_name

이 레슨의 MiniJson 은 Map<String,Object> 를 그대로 직렬화하는 최소 구현이라 이런 변환이 없습니다. "Jackson 이 리플렉션으로 getter 를 찾아 필드 이름을 JSON 키로 바꾸는 그 작업"을 우리는 Member.toJson() 에서 수동으로 하는 셈입니다.

Accept 헤더로 XML 도 요청할 수 있다는 점(콘텐츠 협상)도 알아두면 좋습니다. 이 레슨은 JSON 만 지원하므로 Accept 를 따로 검사하지 않지만, 실무 API 가 여러 형식을 지원한다면 Accept 값에 따라 Content-Type 을 다르게 응답합니다.

2.5 에러 응답 규격

에러도 데이터입니다. 클라이언트가 코드로 분기하고, 폼에 필드별 오류를 표시하고, 로그를 추적할 수 있어야 합니다. 이 레슨은 다음 규격을 씁니다.

text
{
  "code": "VALIDATION_ERROR",       // 클라이언트가 분기할 안정적인 문자열 코드
  "message": "입력 값을 확인하세요",  // 사람이 읽을 요약(로그·토스트에 표시)
  "errors": [                       // 필드별 상세(폼 오류 표시용). 없으면 빈 배열
    { "field": "email", "reason": "이메일 형식이 아닙니다" }
  ],
  "traceId": "b93714dc",            // 서버 로그와 대조할 추적 ID
  "timestamp": "2026-09-09T09:02:20Z"
}

표준화된 대안으로 RFC 9457 application/problem+json 이 있습니다. {type, title, status, detail, instance} 필드를 쓰고 Content-Type: application/problem+json 을 붙입니다. Spring Boot 3.x 는 ProblemDetail 클래스로 이를 기본 지원합니다.

필드 이름만 다를 뿐 목적은 이 레슨의 규격과 같습니다. 코드로 분기하고, 사람이 읽을 설명을 담고, 상세를 붙이고, 추적 ID 로 로그를 찾는 4가지 역할입니다.

2.6 입력 검증 흐름 — 형식 → 필수 → 업무 규칙

검증은 순서가 있습니다. 싼 검사를 먼저 해서 비싼 검사(DB 조회 등)를 아끼기 때문입니다.

text
① 형식 검증   "age" 가 숫자인가?                      → 아니면 400
② 필수 검증   "name" 이 비어있지 않은가?                → 아니면 400
③ 업무 규칙   나이가 0~150 인가? 이메일이 중복 아닌가?    → 아니면 400/409/422

①②는 요청 자체의 문제(400), ③ 중 "이미 존재하는 상태와 충돌"은 409, "형식은 맞지만 처리할 수 없는 값"(예: 탈퇴 회원 재가입 제한, 정책상 불가)은 422 로 나눕니다. 이 레슨의 Validation 은 ①②를 한 번에 모아 errors 배열로 반환하고, ③(버전 충돌)은 MemberStore 가 별도로 409 로 던집니다.

DB 유니크 제약(이메일 중복) 위반도 업무 규칙 검증의 일종입니다. 애플리케이션에서 미리 조회해 409 로 막거나, DB 예외를 잡아 409 로 변환합니다. 어느 쪽이든 예외 메시지 원문은 그대로 노출하지 않습니다(5절 실수 3).

2.7 페이징 응답

목록 API 는 전체를 한 번에 안 돌려주고 페이지 단위로 나눕니다. 응답에 "몇 번째 페이지인지", "총 몇 건인지"를 함께 담아야 프런트가 페이지 버튼을 그릴 수 있습니다.

필드 의미
page 요청한 페이지 번호(0부터)
size 페이지당 개수
totalElements 전체 개수
totalPages 전체 페이지 수
content 이 페이지의 실제 데이터 배열

이 오프셋 페이징(OFFSET size*page LIMIT size)은 뒤 페이지로 갈수록 DB 가 앞부분을 건너뛰느라 느려집니다. 대용량·무한 스크롤에는 키셋(seek) 페이징을 씁니다. "이전 페이지 마지막 id 보다 큰 것 중 상위 N개"(WHERE id > :lastId LIMIT :size)로 항상 일정한 속도를 냅니다.

2.8 동시 수정 — ETag/If-Match 또는 version 으로 409

두 사용자가 같은 회원을 동시에 수정하면 나중에 저장한 쪽이 먼저 저장한 쪽을 덮어씁니다(lost update). 막는 방법은 두 가지입니다.

방식 흐름
버전 필드 자원에 version 정수 보관, 요청에 읽은 버전 동봉, 불일치면 409
ETag/If-Match 응답 ETag: "3", 요청 If-Match: "3", HTTP 표준이라 캐시·프록시와 호환

이 레슨은 PUT 에 version 필드를, GET 에 ETag/If-None-Match(캐시 재검증, 304)를 함께 씁니다. 실무에서는 보통 둘 중 하나로 통일합니다.

2.9 CORS 원리 — 프리플라이트와 허용 헤더

브라우저가 자신과 다른 출처(도메인·포트·프로토콜)로 요청을 보내면, 브라우저는 먼저 OPTIONS 프리플라이트 요청을 보내 "이 실제 요청을 보내도 되는지" 서버에 묻습니다. 서버가 Access-Control-Allow-* 헤더로 허락해야 실제 요청이 나갑니다.

text
프리플라이트가 필요한 경우: PUT/PATCH/DELETE, 커스텀 헤더(Authorization 등), Content-Type: application/json
프리플라이트가 필요 없는 경우: 단순 GET, Content-Type: text/plain 인 단순 POST

서버는 실제 요청과 프리플라이트 응답 모두에 Access-Control-Allow-Origin(허용 출처), Access-Control-Allow-Methods(허용 메서드), Access-Control-Allow-Headers(허용 헤더)를 붙여야 합니다. CORS 는 서버가 브라우저에게 주는 허가일 뿐이라, curl 이나 서버 간 호출에는 애초에 적용되지 않습니다.

text
데모 [10] 실제 결과
OPTIONS /members → 204 Allow-Methods=GET,POST,PUT,PATCH,DELETE,OPTIONS

이 레슨의 CorsFilter 는 OPTIONS 를 라우터까지 보내지 않고 필터 단계에서 204 로 끝냅니다. 실제 GET/POST 요청은 이 필터를 그대로 통과해 라우터로 넘어가되, 같은 CORS 헤더가 응답에도 붙습니다.

2.10 라우팅 구현 원리 — 패턴 매칭과 경로 변수 추출

Spring 의 @GetMapping("/members/{id}") 는 내부적으로 경로 패턴을 정규식으로 컴파일해 두고, 요청이 오면 등록된 패턴들과 순서대로 매칭해 첫 일치를 찾습니다. 이 레슨의 Router 가 이 과정을 그대로 구현합니다.

text
"/members/{id}"  →  정규식 "/members/([^/]+)"  →  "id" 라는 이름을 그룹 순서와 매핑
요청 "/members/42" 가 이 정규식에 매칭 → group(1) = "42" → pathVars.put("id", "42")

메서드가 다르면 경로가 매칭돼도 넘어가고(405 Method Not Allowed 후보), 경로 자체가 안 맞으면 404 후보로 남습니다. 순서대로 다 돌아본 뒤에도 후보가 있으면 405, 아예 없으면 404 를 최종 응답합니다.

2.11 Spring Boot 3.5 대응표

이 레슨 Spring Boot 3.5
Router.add("GET", "/members/{id}", ...) @RestController + @GetMapping("/members/{id}")
pathVars.get("id") @PathVariable Long id
MiniJson.read(body) @RequestBody MemberRequest req(Jackson 이 자동 역직렬화)
Validation.validateCreate(body) @Valid @RequestBody + @NotBlank, @Email, @Min/@Max
Router.writeJson(ex, 201, ...) ResponseEntity.status(201).location(uri).body(dto)
Router 의 catch(RuntimeException) @RestControllerAdvice + @ExceptionHandler
Page.of(all, page, size) 메서드 인자 Pageable pageable → repository.findAll(pageable)
CorsFilter @CrossOrigin 또는 WebMvcConfigurer.addCorsMappings
MemberStore.VersionConflict → 409 JPA @Version → OptimisticLockingFailureException → 409

Vue(axios) 클라이언트는 응답 인터셉터에서 이 레슨의 에러 규격을 한곳에서 처리합니다.

javascript
axios.interceptors.response.use(
  res => res,
  err => {
    const body = err.response?.data;               // {code, message, errors, traceId}
    if (body?.errors?.length) showFieldErrors(body.errors);   // 폼에 필드별 표시
    else toast.error(body?.message ?? '알 수 없는 오류');
    console.error(`traceId=${body?.traceId}`);      // 문의 시 이 값으로 서버 로그 검색
    return Promise.reject(err);
  }
);
핵심 원리
  • 2.1 HTTP 요청/응답 해부
  • 2.2 REST 자원 설계 — 명사, 복수형, 계층, 동사 금지
  • 2.3 상태 코드 선택표
  • 2.4 JSON 직렬화 규칙
  • 2.5 에러 응답 규격
  • 2.6 입력 검증 흐름 — 형식 → 필수 → 업무 규칙
  • 2.7 페이징 응답
  • 2.8 동시 수정 — ETag/If-Match 또는 version 으로 409
  • 2.9 CORS 원리 — 프리플라이트와 허용 헤더
  • 2.10 라우팅 구현 원리 — 패턴 매칭과 경로 변수 추출
  • 2.11 Spring Boot 3.5 대응표
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제