스프링을 통해 REST API를 구축하다보면 객체(DTO)를 반환하는 일은 매우 자주있다.
// UserResponse.java
public class UserResponse {
private String name;
private int age;
public UserResponse() {
name = "Chan";
age = 27;
}
}
// MainController.java
@RestController
public class MainController {
@GetMapping("/api/v1/users")
public UserResponse getUsers() {
UserResponse response = new UserResponse();
return response;
}
}
다음의 코드를 보고 잘못된 부분을 찾아 보자.
분명 틀린 부분이 없어보이지만, 심지어 실행에도 문제가 없지만 실행을 하면, 다음과 같은 문제가 발생한다.
이 상태 코드는 클라이언트가 요청할 때 지정한 Accept 헤더에 서버가 응답 가능한 타입이 없다는 뜻입니다.
즉, 서버가 JSON을 제공하지 못했다는 것!
무엇이 문제일까? 좀 더 알아보자.
@Controller + @ResponseBody
해당 클래스의 모든 메서드는 반환값을 View가 아닌 HTTP 응답 본문(body)에 직접 쓴다.
즉, 객체를 반환에 직접 사용!
기본적으로 Spring은 반환 시 HttpMessageConverter를 사용.
객체를 반환하면 Spring은 자동으로 객체를 JSON으로 변환한다.
이때, MappingJackson2HttpMessageConverter를 사용!
객체를 JSON으로 변환을 한다!
MappingJackson2HttpMessageConverter은 Jackson 라이브러리를 사용한다.
Jackson은 객체를 JSON으로 직렬화함!
Jackson의 직렬화에 대한 문제
Jackson은 직렬화 시, 다음 중 하나를 필요로 함
- public 필드
- getter 메서드
결국 private 필드인데 getter가 없으면 JSON 직렬화 실패
// UserResponse.java
public class UserResponse {
private String name;
private int age;
public UserResponse() {
name = "Chan";
age = 27;
}
public String getName() {
return "HaeChan";
}
}
다음과 같이 getter가 있어야 작동한다.
다음을 통해 Spring에서 RestController에서 객체 반환 시, Getter의 내용을 사용한다는 것을 알 수 있다.
@JsonIgnore
@JsonProperty
@JsonManagedReference
@JsonBackReference
다음과 같은 어노테이션을 사용할 수 있음
@JsonIgnore: 해당 필드를 JSON 직렬화/역직렬화에서 제외
@JsonProperty: JSON의 필드 이름을 Java 필드와 다르게 지정
@JsonManagedReference & @JsonBackReference: 두 어노테이션은 양방향 관계에서 순환 참조로 인해 StackOverflow가 발생하는 것을 방지
@JsonManagedReference → 직렬화에 포함됨 (보통 부모 → 자식 방향)
@JsonBackReference → 직렬화에서 제외됨 (보통 자식 → 부모 방향)
@JsonIdentityInfo: Jackson이 객체의 ID 기반으로 참조를 추적하게 하여, 순환 참조 구조에서도 무한 루프 없이 직렬화가 가능
ObjectMapper는 Jackson 라이브러리의 핵심 클래스
ObjectMapper objectMapper = new ObjectMapper();
// 직렬화: Java 객체 → JSON
User user = new User("haechan", 25);
String json = objectMapper.writeValueAsString(user);
// 역직렬화: JSON → Java 객체
String jsonString = "{\"name\":\"haechan\",\"age\":25}";
User user2 = objectMapper.readValue(jsonString, User.class);
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 예: snake_case → camelCase 자동 매핑
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// null 값 무시
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
}
다음과 같이 커스텀해서 사용할 수 있다.