자바 백엔드 입문 28편. @RestController가 JSON을 어떻게 자동 변환하고 Jackson이 어떤 역할을 하는지, ResponseEntity로 응답 코드·헤더 제어하는 법을 통역사 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 28편이에요. 27편에서 컨트롤러 매핑 패턴을 잡았다면, 이번 28편은 @RestController 가 어떻게 자바 객체를 JSON으로 자동 변환해주는지의 내부 동작을 풀어 가요. REST API의 90% 흐름이 여기 들어 있어요.
JSON 응답이 헷갈리는 이유
처음 @RestController 의 메서드에서 Order 객체를 반환하는 코드를 보면 — "그냥 자바 객체인데 어떻게 브라우저에선 JSON으로 보일까?" 가 안 잡혀요. 마법처럼 동작하는 그 사이에 Jackson 이라는 라이브러리가 있어요.
이 글에서는 통역사 비유로 풀어요. 컨트롤러 = 한국말로 보고하는 직원, 브라우저 = 영어만 이해하는 외국 클라이언트, Jackson = 가운데서 자동 통역하는 통역사. 끝까지 따라오시면 JSON 응답의 전체 그림이 한 번에 들어와요.
@RestController = @Controller + @ResponseBody
먼저 @RestController 의 정체부터. 8편에서 짧게 다뤘는데, 정확히는 두 어노테이션의 합이에요.
@Controller
@ResponseBody // 메서드 반환값을 응답 본문으로 직접
public class OrderController { ... }
// 위 두 줄을 한 줄로
@RestController
public class OrderController { ... }
@Controller 단독 = 뷰 템플릿 렌더(HTML), @RestController = JSON·문자열 등을 응답 본문으로 직접 반환. 현대 백엔드의 95%가 @RestController.
자바 객체 → JSON 자동 변환 흐름
컨트롤러가 Order 객체를 반환하면 — 다음 흐름으로 JSON이 만들어져요.
[1] 컨트롤러 메서드 반환 → return new Order(...)
[2] DispatcherServlet이 받음
[3] HttpMessageConverter 후보들 중 적절한 것 찾기
[4] Jackson MappingJackson2HttpMessageConverter 선택
[5] Order 객체를 JSON 문자열로 직렬화
[6] HTTP 응답 본문에 박음 (Content-Type: application/json)
[7] 브라우저에 응답
핵심 부품 = HttpMessageConverter. Spring이 가지고 있는 "변환기 풀" 에서 "이 객체와 클라이언트의 Accept 헤더를 보고 어느 변환기를 쓸지" 자동 선택해줘요. 기본 변환기 = Jackson(MappingJackson2HttpMessageConverter).
Jackson — JSON 직렬화의 표준
Jackson은 자바 진영의 거의 표준 JSON 라이브러리예요. Spring Boot가 spring-boot-starter-web 의존성에 자동 포함시켜요. 우리가 별도 설정 안 해도 JSON 변환이 동작.
기본 동작 — "객체의 모든 public getter 메서드를 호출해 JSON 필드로 변환". 예를 들어:
public class Order {
private Long id;
private int amount;
private LocalDateTime createdAt;
public Long getId() { return id; }
public int getAmount() { return amount; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
// 반환 시 JSON:
// {"id": 123, "amount": 10000, "createdAt": "2026-05-16T12:00:00"}
Order 의 3개 getter가 자동으로 JSON 필드 3개로. Lombok @Getter 박으면 getter 자동 생성이라 — @Data·@Getter 한 줄만 박아도 JSON 변환 동작.
Jackson 직렬화 제어 — @JsonProperty·@JsonIgnore
기본 동작으로 부족하면 어노테이션으로 미세 제어 가능.
@JsonProperty — 필드 이름 변경
public class Order {
@JsonProperty("order_id") // JSON에서는 order_id로
private Long id;
}
// {"order_id": 123}
자바는 camelCase, JSON은 snake_case 컨벤션일 때 자주 쓰여요. 또는 "외부 API와 키 이름 맞춰야 할 때".
@JsonIgnore — 직렬화 제외
public class User {
private Long id;
private String name;
@JsonIgnore // 응답에 절대 안 들어감
private String password;
}
비밀번호·내부 토큰 같이 "클라이언트에 절대 노출하면 안 되는 필드" 차단. 보안의 첫 방어선.
@JsonFormat — 날짜·시간 포맷
public class Order {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
// "createdAt": "2026-05-16 12:30:45"
기본 LocalDateTime 직렬화 형태(2026-05-16T12:30:45)가 마음에 안 들 때.
@JsonInclude — null 필드 제외
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Order {
private Long id;
private String description; // null이면 응답에 안 들어감
}
전역 설정도 가능 — application.yml 의 spring.jackson.default-property-inclusion: non_null.
ResponseEntity — 응답 코드·헤더까지 제어
기본 패턴 — "객체 반환하면 200 OK + JSON". 하지만 응답 상태 코드나 헤더를 제어하려면 ResponseEntity<T> 를 반환해요.
@PostMapping
public ResponseEntity<Order> create(@RequestBody OrderRequest req) {
Order saved = orderService.create(req);
return ResponseEntity
.status(HttpStatus.CREATED) // 201
.header("Location", "/orders/" + saved.getId())
.body(saved);
}
@GetMapping("/{id}")
public ResponseEntity<Order> get(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok) // 200 + body
.orElse(ResponseEntity.notFound().build()); // 404
}
ResponseEntity 가 응답의 모든 것을 제어. HTTP 상태 코드·헤더·본문 셋 다 한 객체로 표현. REST API에서 자주 등장하는 패턴이에요.
자주 쓰이는 빌더:
| 메서드 | 효과 |
|---|---|
ResponseEntity.ok(body) |
200 OK + 본문 |
ResponseEntity.status(HttpStatus.CREATED).body(...) |
201 Created + 본문 |
ResponseEntity.notFound().build() |
404 Not Found |
ResponseEntity.badRequest().body(error) |
400 + 오류 본문 |
ResponseEntity.noContent().build() |
204 No Content (응답 본문 없음) |
@ResponseStatus — 간단한 상태 코드 변경
ResponseEntity 까지 안 가도 상태 코드만 바꾸고 싶을 때 @ResponseStatus 한 줄.
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Order create(@RequestBody OrderRequest req) {
return orderService.create(req);
}
// 응답 = 201 Created + JSON
예외 클래스에 박으면 더 강력해요. 23편 예외 처리에서 자세히.
Jackson 글로벌 설정 — application.yml
매번 어노테이션 박기 번거로우면 application.yml 또는 application.properties 에 글로벌 설정.
spring:
jackson:
property-naming-strategy: SNAKE_CASE # 모든 필드 snake_case
default-property-inclusion: non_null # null 필드 자동 제외
date-format: yyyy-MM-dd HH:mm:ss # 날짜 포맷
time-zone: Asia/Seoul # 시간대
이걸 한 번 박아두면 — 모든 컨트롤러의 JSON 응답이 일관된 포맷으로 자동 정렬돼요. 실무 표준 패턴.
JPA @Entity 객체를 컨트롤러에서 직접 반환하지 마세요. 지연 로딩(LazyLoading) 함정이 발생해요. 트랜잭션이 닫힌 후 Jackson이 getter를 호출하면 LazyInitializationException 폭발. DTO를 따로 만들어 변환 후 반환이 표준 패턴. 30·32편 JPA에서 자세히.
한국 회사 백엔드 표준 — DTO 패턴
위 함정 때문에 한국 회사 백엔드는 거의 다음 패턴이에요.
// 1. JPA Entity (DB 매핑 전용)
@Entity
public class Order { ... }
// 2. Response DTO (API 응답 전용)
@Getter
@AllArgsConstructor
public class OrderResponse {
private Long id;
private int amount;
private String status;
public static OrderResponse from(Order entity) {
return new OrderResponse(entity.getId(), entity.getAmount(), entity.getStatus());
}
}
// 3. 컨트롤러 — DTO 반환
@GetMapping("/{id}")
public OrderResponse get(@PathVariable Long id) {
return OrderResponse.from(orderService.findById(id));
}
Entity와 DTO 분리. DB 모델과 API 응답 모델이 독립적이라 — 한쪽이 바뀌어도 다른 쪽이 안 깨져요. 코드 양이 많아 보이지만 한국 백엔드의 표준 패턴.
한 줄 정리 — @RestController = @Controller + @ResponseBody. Jackson이 자바 객체를 JSON으로 자동 변환. ResponseEntity 로 상태 코드·헤더 제어. Entity 직접 반환 X, DTO 패턴 표준.
시험 직전 한 번 더 — JSON 응답 입문자가 매번 헷갈리는 것
@RestController=@Controller+@ResponseBody합친 어노테이션@Controller단독 = HTML 뷰 렌더,@RestController= JSON 직접 반환- 현대 백엔드 95% =
@RestController - 자바 객체 → JSON 자동 변환 = HttpMessageConverter 가 처리
- 기본 변환기 = Jackson (Spring Boot 자동 포함)
- Jackson 기본 = public getter 메서드를 JSON 필드로
- Lombok
@Getter·@Data박으면 자동 동작 @JsonProperty("name")= JSON 필드 이름 변경@JsonIgnore= 직렬화 제외 (비밀번호 등 보안)@JsonFormat= 날짜·시간 포맷 지정@JsonInclude(NON_NULL)= null 필드 제외ResponseEntity<T>= 응답 상태 코드·헤더·본문 모두 제어ResponseEntity.ok(body)= 200,.status(CREATED).body(...)= 201ResponseEntity.notFound().build()= 404,.noContent().build()= 204@ResponseStatus= 상태 코드만 간단히 변경application.yml의spring.jackson.*= 글로벌 설정- JPA Entity 직접 반환 X — LazyLoading 함정
- DTO 패턴 = Entity와 분리된 응답 전용 객체 (한국 백엔드 표준)
- 정적 메서드
OrderResponse.from(entity)= DTO 변환 표준 패턴 - MapStruct 같은 라이브러리로 DTO 변환 자동화 가능
- JSON 응답 =
Content-Type: application/json; charset=utf-8자동
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 23편 — AOP 횡단 관심사가 뭔가
- 24편 — @Aspect로 첫 AOP 박기
- 25편 — DispatcherServlet 요청 처리 흐름
- 26편 — Filter vs Interceptor 비교
- 27편 — @Controller @RequestMapping
다음 글: