티스토리 뷰
오늘은 Spring Boot에서 자주 사용하는 @RequestBody와 Jackson 라이브러리의 내부 동작에 대해 이야기해보려고 합니다. 특히 DTO에서 Setter가 필요한지, 아닌지에 대한 의문을 파헤쳐보겠습니다.
(사실 면접에서 나온 질문인데 제대로 대답을 못했습니다..) 구글링 검색해보니 관련글이 이미 여러개 있길래 좀 읽었습니다.
궁금증: Setter가 없어도 값이 들어오는 이유
Request DTO 예시
@Getter
@Setter // 주목! Setter가 있습니다
public class LoginRequestDto {
@NotBlank
private String username;
@NotBlank
private String password;
}
Controller
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDto loginRequest) {
// 비즈니스 로직 처리
return ResponseEntity.ok().body("로그인 성공");
}
여기서 LoginRequestDto에 Setter가 없어도 JSON 데이터가 객체에 잘 매핑된다. 왜그럴까?
Jackson이 객체를 생성하는 방법
Jackson 라이브러리는 JSON 데이터를 자바 객체로 변환(역직렬화)할 때 다음과 같은 전략을 사용합니다
- 객체 인스턴스 생성: 기본 생성자를 통해 객체 인스턴스를 만듭니다.
- 프로퍼티 설정: 다음 중 하나의 방법으로 프로퍼티 값을 설정합니다:
- Setter 메서드 호출
- 리플렉션을 통한 필드 직접 접근
자바에서는 다른 생성자가 없을 때 컴파일러가 자동으로 기본 생성자를 생성해주기 때문에, 명시적인 생성자가 없는 클래스라도 Jackson이 객체를 생성할 수 있습니다.
Jackson 내부 동작 원리
Jackson이 어떻게 동작하는지 내부적으로 깊이 들여다보겠습니다.
역직렬화 과정의 핵심 클래스들
- MappingJackson2HttpMessageConverter: Spring에서 HTTP 요청을 자바 객체로 변환할 때 사용하는 클래스
// Spring Framework 내부 코드 (간소화)
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException {
// ObjectMapper를 사용하여 JSON을 자바 객체로 변환
return this.objectMapper.readValue(inputMessage.getBody(), clazz);
}
}
- ObjectMapper: Jackson의 핵심 클래스로, 실제 변환 작업을 처리합니다.
// ObjectMapper 클래스 (간소화)
public <T> T readValue(InputStream src, Class<T> valueType) throws IOException {
return readValue(src, _typeFactory.constructType(valueType));
}
- BeanDeserializer: 특정 자바 빈 객체에 대한 역직렬화를 처리하는 클래스
// BeanDeserializer 클래스 (간소화)
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 객체 생성
Object bean = _valueInstantiator.createUsingDefault(ctxt);
// 2. 필드 채우기
for (JsonToken t = p.currentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) {
String propName = p.currentName();
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) {
try {
// 필드에 값 설정 (setter 또는 필드 직접 접근)
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
// 예외 처리
}
}
}
return bean;
}
- SettableBeanProperty: 객체의 필드에 값을 설정하는 역할을 담당하는 추상 클래스
- MethodProperty: Setter 메서드를 통해 값을 설정하는 구현체
- FieldProperty: 리플렉션을 통해 필드에 직접 접근하는 구현체
// FieldProperty 클래스 (간소화)
@Override
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException {
Object value = deserialize(p, ctxt);
try {
// 리플렉션을 통한 필드 직접 접근
_field.set(instance, value);
} catch (Exception e) {
// 예외 처리
}
}
Spring Boot의 기본 설정
Spring Boot는 자동 설정을 통해 Jackson의 동작 방식을 다음과 같이 구성합니다
// JacksonAutoConfiguration 클래스 (간소화)
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// private 필드에도 접근 가능하도록 설정
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return objectMapper;
}
이 설정 덕분에 @Setter 없이도 Jackson이 리플렉션을 통해 private 필드에 직접 접근할 수 있습니다.
직접 확인해보기: 테스트 코드
이제 실제로 Setter가 없어도 Jackson이 JSON을 객체로 변환할 수 있는지 테스트해보겠습니다.
1. DTO 클래스 작성
public class NoSetterDto {
private String name;
private int age;
// getter만 있고 setter는 없음
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
2. 단위 테스트
@SpringBootTest
public class JacksonDeserializationTest {
@Autowired
private ObjectMapper objectMapper;
@Test
public void testDeserializeWithoutSetter() throws Exception {
// JSON 문자열
String json = "{\"name\":\"홍길동\",\"age\":30}";
// 역직렬화
NoSetterDto dto = objectMapper.readValue(json, NoSetterDto.class);
// 결과 검증
assertEquals("홍길동", dto.getName());
assertEquals(30, dto.getAge());
}
}
3. MockMvc를 사용한 통합 테스트
@SpringBootTest
@AutoConfigureMockMvc
public class ControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testRequestBodyWithoutSetter() throws Exception {
// 요청 데이터 준비
String json = "{\"name\":\"홍길동\",\"age\":30}";
// API 호출 및 결과 검증
mockMvc.perform(post("/api/test")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("홍길동"))
.andExpect(jsonPath("$.age").value(30));
}
}
그래서, @Setter는 필요한가?
이제 왜 Request DTO에 @Setter 없어도 된다. 그러나. 왜 Setter를 사용했냐라고 묻는다면?
@Setter 없이도 동작하는 이유
- Jackson의 기본 설정이 리플렉션을 통한 필드 접근을 허용함
- 자바 컴파일러가 기본 생성자를 자동으로 생성해줌
그럼에도 @Setter를 사용하는 이유라고 한다면?
- 명시적 의도 표현: 필드의 변경 가능성을 코드에서 명확히 보여줌
- 안정성: Jackson 설정이 변경되어도 동작이 보장됨
- 유효성 검증: setter에서 값 검증 로직을 추가할 수 있음
- 디버깅 용이성: 직접 메서드 호출 추적이 가능
더 나은 대안: 불변 객체 패턴
현대 자바에서는 불변 객체를 만드는 더 좋은 방법
Java 14+ Record 사용
public record LoginRequestDto(
@NotBlank String username,
@NotBlank String password
) {}
마무리
오늘은 Spring Boot에서 Jackson 라이브러리가 JSON을 자바 객체로 변환하는 내부 메커니즘에 대해 자세히 알아보았습니다. @Setter가 없어도 값이 할당되는 이유를 이해하고, 각 상황에 맞는 최적의 패턴을 선택할 수 있게 되었습니다.
Jackson의 유연한 설계 덕분에 다양한 방식으로 객체를 구성할 수 있지만, 코드의 의도를 명확히 하고 팀의 컨벤션을 일관되게 유지하는 것이 중요합니다.
참고
https://jojoldu.tistory.com/407
@Request Body에서는 Setter가 필요없다?
회사에서 근무하던중 새로오신 신입 개발자분이 저에게 하나의 질문을 했습니다. POST 요청시에 Setter가 필요없는것 같다고. 여태 제가 알던것과는 달라서 어떻게 된 일인지 궁금했습니다. 정말 P
jojoldu.tistory.com
https://blogshine.tistory.com/446
[Spring] @RequestBody에 기본생성자만 필요하고 Setter는 필요없는 이유 - 2
그간 밀어오고 밀어왔던 내용에 대해 정리하고 넘어가야 겠다 싶어 정리하는 글 이다. 지난 글에서 @RequestBody에서 어떤 방식으로 객체를 생성하는 지 파악한 후, 이번 글에서는 객체에 값을 어떤
blogshine.tistory.com
https://docs.spring.io/spring-boot/reference/features/json.html#features.json
JSON :: Spring Boot
Auto-configuration for JSON-B is provided. When the JSON-B API and an implementation are on the classpath a Jsonb bean will be automatically configured. The preferred JSON-B implementation is Eclipse Yasson for which dependency management is provided.
docs.spring.io
도움
Claude Ai
- Total
- Today
- Yesterday
- Mac
- k8s
- Kotlin
- Bash tab
- intellij
- localtime
- oracle
- window
- rocky
- Java
- svn
- config-location
- mybatis
- LocalDate
- 베리 심플
- 오라클
- Spring
- springboot
- claude
- input
- 북리뷰
- maven
- LocalDateTime
- docker
- mybatis config
- jQuery
- Linux
- JavaScript
- Spring Security
- elasticsearch
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |