티스토리 뷰

728x90
반응형

오늘은 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 데이터를 자바 객체로 변환(역직렬화)할 때 다음과 같은 전략을 사용합니다

  1. 객체 인스턴스 생성: 기본 생성자를 통해 객체 인스턴스를 만듭니다.
  2. 프로퍼티 설정: 다음 중 하나의 방법으로 프로퍼티 값을 설정합니다:
    • Setter 메서드 호출
    • 리플렉션을 통한 필드 직접 접근

자바에서는 다른 생성자가 없을 때 컴파일러가 자동으로 기본 생성자를 생성해주기 때문에, 명시적인 생성자가 없는 클래스라도 Jackson이 객체를 생성할 수 있습니다.

Jackson 내부 동작 원리

Jackson이 어떻게 동작하는지 내부적으로 깊이 들여다보겠습니다.

역직렬화 과정의 핵심 클래스들

  1. 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);
    }
}
  1. ObjectMapper: Jackson의 핵심 클래스로, 실제 변환 작업을 처리합니다.
// ObjectMapper 클래스 (간소화)
public <T> T readValue(InputStream src, Class<T> valueType) throws IOException {
    return readValue(src, _typeFactory.constructType(valueType));
}
  1. 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;
}
  1. 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 없이도 동작하는 이유

  1. Jackson의 기본 설정이 리플렉션을 통한 필드 접근을 허용함
  2. 자바 컴파일러가 기본 생성자를 자동으로 생성해줌

그럼에도 @Setter를 사용하는 이유라고 한다면?

  1. 명시적 의도 표현: 필드의 변경 가능성을 코드에서 명확히 보여줌
  2. 안정성: Jackson 설정이 변경되어도 동작이 보장됨
  3. 유효성 검증: setter에서 값 검증 로직을 추가할 수 있음
  4. 디버깅 용이성: 직접 메서드 호출 추적이 가능

더 나은 대안: 불변 객체 패턴

현대 자바에서는 불변 객체를 만드는 더 좋은 방법

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

728x90
반응형
댓글
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/06   »
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
글 보관함
반응형