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

5. 자주 하는 실수 (Tip)

❌ 실수 1: GET 에 본문을 넣는다

java
// ❌ GET 요청에 JSON 본문으로 검색 조건을 실어 보낸다
HttpRequest.newBuilder(uri).method("GET", BodyPublishers.ofString(filterJson)).build();

HTTP 표준(RFC 9110)은 GET 본문의 의미를 정의하지 않습니다. 프록시·CDN·일부 HTTP 라이브러리가 GET 본문을 통째로 버려서, 서버는 필터를 못 받고 전체 목록을 돌려줍니다.

java
// ✅ 조건은 쿼리 파라미터로
HttpRequest.newBuilder(URI.create(base + "/members?name=홍&page=0&size=10")).GET().build();

❌ 실수 2: 에러도 200 으로 응답한다

java
// ❌ 상태 코드는 항상 200, 성공/실패를 본문의 "success" 필드로만 구분
Router.writeJson(ex, 200, Map.of("success", false, "message", "이메일 형식 오류"));

이러면 axios/fetch 는 이 요청을 "성공"으로 처리해 .catch() 가 안 걸립니다. 매 호출마다 if (!body.success) 를 손으로 검사해야 하고, 캐시·재시도 미들웨어(상태 코드 기준으로 동작)가 전부 무력화됩니다.

java
// ✅ 상태 코드로 성공/실패를 표현하고, 본문은 상세 정보만 담당
ApiError.send(ex, 400, "VALIDATION_ERROR", "이메일 형식 오류", errors);

❌ 실수 3: 예외 메시지를 그대로 노출한다

java
// ❌ NullPointerException, SQLException 메시지를 그대로 응답에
} catch (Exception e) {
    ApiError.send(ex, 500, "ERROR", e.getMessage(), null);   // "Duplicate entry '[email protected]' for key ..." 노출
}

DB 스키마, 테이블명, 내부 클래스 경로가 그대로 새 나가 공격자에게 정보를 줍니다. 이 레슨의 Router.dispatch 는 서버 콘솔에만 상세를 찍고 클라이언트에는 고정 문구만 보냅니다.

java
// ✅ 상세는 서버 로그, 클라이언트에는 코드 + traceId 만
System.err.println("  [UNHANDLED] " + method + " " + path + " -> " + e);
ApiError.send(exchange, 500, "INTERNAL_ERROR", "서버 내부 오류가 발생했습니다", null);

❌ 실수 4: long id 를 숫자 그대로 내려준다

java
// ❌ id 를 Long 그대로 JSON 숫자로 직렬화
m.put("id", id);   // 9007199254740993L → JS 에서 9007199254740992 로 반올림돼 다른 행을 가리킬 수 있다

JS 의 Number 는 IEEE754 배정도 부동소수점이라 정수 정밀도가 2^53(약 9천조)까지입니다. 그 이상은 반올림되고, 회원가입 트래픽이 많은 서비스는 시퀀스 값이 실제로 이 근처까지 갑니다.

java
// ✅ 문자열로 내보낸다(이 레슨의 Member.toJson())
m.put("id", String.valueOf(id));

❌ 실수 5: 날짜에 시간대를 안 붙인다

java
// ❌ LocalDateTime.now().toString() → "2026-09-09T18:00:00" (시간대 정보 없음)
m.put("createdAt", LocalDateTime.now().toString());

서버가 한국(UTC+9)이고 프런트가 브라우저 로컬(UTC)로 파싱하면 9시간이 어긋납니다. 여러 서버·타임존이 섞인 시스템에서는 재현이 어려운 버그가 됩니다.

java
// ✅ Instant(항상 UTC) + ISO-8601, 표시는 프런트에서 로컬로 변환
m.put("createdAt", Instant.now().toString());   // "2026-09-09T09:00:00.123456Z"

❌ 실수 6: Content-Type 검사를 생략한다

java
// ❌ Content-Type 을 안 보고 바로 JSON 으로 파싱 시도
Map<String, Object> body = MiniJson.read(bodyAsString(ex));   // form-urlencoded 가 오면 파싱 예외 → 500

폼 전송(application/x-www-form-urlencoded)이나 빈 본문이 오면 JSON 파서가 예외를 던지고, 그게 잡히지 않으면 400 이어야 할 상황이 500 으로 나갑니다.

java
// ✅ 먼저 Content-Type 을 확인해 415 로 명확히 응답
if (ct == null || !ct.toLowerCase(Locale.ROOT).contains("application/json"))
    throw new Router.ApiException(415, "UNSUPPORTED_MEDIA_TYPE", "Content-Type: application/json 이 필요합니다");

❌ 실수 7: 요청 본문 스트림을 소비하지 않는다

java
// ❌ Content-Type 만 보고 바로 리턴, 본문 스트림을 안 읽고 안 닫음
if (!"application/json".equals(ct)) { ApiError.send(ex, 415, ...); return; }   // getRequestBody() 를 아예 안 건드림

JDK HttpServer(및 대부분의 커넥션 재사용 서버)는 다음 요청을 같은 커넥션에서 처리하려면 이전 요청의 본문을 끝까지 읽어야 합니다. 안 읽으면 커넥션이 재사용되지 못하거나 다음 요청이 밀려 대기합니다.

java
// ✅ 분기와 무관하게 항상 끝까지 읽고 닫는다(이 레슨의 bodyAsString)
try (InputStream is = ex.getRequestBody()) { return new String(is.readAllBytes(), StandardCharsets.UTF_8); }
자주 하는 실수 (Tip)
  • ❌ 실수 1: GET 에 본문을 넣는다
  • ❌ 실수 2: 에러도 200 으로 응답한다
  • ❌ 실수 3: 예외 메시지를 그대로 노출한다
  • ❌ 실수 4: long id 를 숫자 그대로 내려준다
  • ❌ 실수 5: 날짜에 시간대를 안 붙인다
  • ❌ 실수 6: Content-Type 검사를 생략한다
  • ❌ 실수 7: 요청 본문 스트림을 소비하지 않는다
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제