기록

@Builder 사용 시 주의사항 – Dto에 기본 생성자를 추가해야 한다 본문

Web/Spring

@Builder 사용 시 주의사항 – Dto에 기본 생성자를 추가해야 한다

youngyin 2025. 2. 7. 00:00

 

1. 문제 상황

얼마 전, Kafka Consumer가 특정 메시지를 받아 DB에 저장하는 기능을 담당하는 서비스를 운영하던 중 Dto를 수정하는 일이 생겼습니다. 테스트 코드를 위해 생성자가 필요했고, 이에 DTO 클래스에 @Builder 어노테이션을 추가했습니다. 이를 통해 테스트 코드에서 MyDto.builder().dataId("testId").name("testName").build()와 같이 편리하게 객체를 생성할 수 있었습니다.

수정된 코드는 다음과 같습니다:

@Getter
public class MyDto {
    private String dataId = "11111";
    private String name = "22222";
	
    // 추가 시작
    @Builder
    public MyDto(String dataId, String name) {
        this.dataId = dataId;
        this.name = name;
    }
    // 추가 종료
}

 

로컬 테스트에서는 아무 문제가 없었습니다. 테스트 코드를 실행했을 때도 정상적으로 동작했기에, 아무런 의심 없이 서버에 배포했습니다.


2. 배포 후 발생한 문제

배포 후 몇 분 지나지 않아 서버에서 심각한 문제가 발생했습니다.

  • Kafka 메시지가 처리되지 않는 상황이 발생했습니다.
  • 동시에 서버 로그에 수많은 오류 메시지가 찍히고 있었습니다.

로그에 나타난 핵심 오류 메시지는 다음과 같았습니다:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `com.example.kafkademo.MyDto`
(no Creators, like default constructor, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)

이 메시지를 보고 역직렬화 문제가 발생했음을 알 수 있었습니다. Kafka Consumer가 메시지를 수신하고 JSON 데이터를 DTO로 변환하는 과정에서 오류가 발생한 것입니다.

Kafka 메시지 예시

{
  "dataId": "12345",
  "name": "sampleName"
}

위 JSON 메시지를 Kafka가 수신한 후 Jackson 라이브러리를 사용해 MyDto 객체로 역직렬화하는 과정에서 기본 생성자가 없기 때문에 오류가 발생했습니다.


3. 왜 이런 문제가 발생했을까?

문제의 핵심은 Jackson 라이브러리@Builder 어노테이션의 동작 방식에 있었습니다.

  • Kafka Consumer는 JSON 형태의 메시지를 받아 Java 객체로 변환해야 합니다.
  • 이 과정에서 Jackson은 기본적으로 기본 생성자(no-arg constructor)를 사용하여 객체를 생성한 뒤, JSON 데이터를 해당 객체의 필드에 매핑합니다.
  • 하지만 우리가 @Builder를 추가하면서 모든 필드를 받는 명시적인 생성자를 정의했고, 이로 인해 자바가 자동으로 생성하던 기본 생성자가 사라지게 되었습니다.
  • 결과적으로 Jackson이 객체를 생성할 때 사용할 기본 생성자가 없었기 때문에 역직렬화에 실패한 것입니다.

자바의 기본 생성자 규칙
클래스에 명시적인 생성자가 하나라도 정의되면, 자바는 자동으로 기본 생성자를 생성하지 않습니다.
따라서 Jackson이 객체를 생성하려 할 때 기본 생성자가 없어서 문제가 발생한 것입니다.

 


4. 해결 방법

이 문제를 해결하기 위해서는 기본 생성자를 추가해야 했습니다. 다만 기본 생성자를 외부에서 함부로 호출하지 못하도록 protected 접근 제어자로 설정했습니다. 수정한 코드는 다음과 같습니다:

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // 기본 생성자 추가
public class MyDto {
    private String dataId = "11111";
    private String name = "22222";

    @Builder
    public MyDto(String dataId, String name) {
        this.dataId = dataId;
        this.name = name;
    }
}

이렇게 수정한 후 서버를 재배포했더니, Kafka 메시지를 정상적으로 처리하고, 데이터가 정상적으로 저장되는 것을 확인할 수 있었습니다.


5. 배운 점

이번 경험을 통해 몇 가지 중요한 교훈을 얻었습니다:

  1. Jackson의 동작 방식을 정확히 이해하자
    • Jackson은 기본 생성자를 사용하여 객체를 생성한다는 점을 꼭 기억해야 합니다.
    • Builder 패턴을 사용할 때 기본 생성자가 누락되면 역직렬화 오류가 발생할 수 있으므로, 이를 방지하기 위해 @NoArgsConstructor와 같은 어노테이션을 적절히 사용해야 합니다.
  2. 테스트 환경과 실제 배포 환경은 다를 수 있다
    • 이번 장애는 단순히 코드 수정으로 인한 것이 아니라, 테스트의 부실 때문이기도 했습니다. 로컬에서는 직접 DTO 객체를 생성하여 테스트했기 때문에 문제가 드러나지 않았습니다. 그러나 실제 배포 환경에서는 Kafka 메시지를 통해 JSON 데이터를 수신하고 이를 역직렬화하는 과정에서 문제가 발생했던 것입니다. 이런 문제를 방지하려면 임베디드 Kafka와 같은 도구를 활용하여 실제 환경과 유사한 흐름으로 테스트를 진행해야 합니다.
    • 따라서 Mock 객체임베디드 Kafka를 활용하여 실제 환경과 유사한 통합 테스트를 진행하는 것이 중요합니다.
  3. 작은 수정도 신중하게 검토하자
    • 단순히 DTO 클래스에 @Builder를 추가하는 작은 수정이었지만, 이는 실제로 심각한 장애로 이어졌습니다.
    • 사소한 수정이라도 테스트, 검토, 배포 후 모니터링을 철저히 해야 한다는 점을 배웠습니다.

6. 마무리

이번 장애를 통해 단순한 코드 수정이 예상치 못한 문제를 초래할 수 있다는 점을 깊이 깨달았습니다. 작은 수정이라도 배포 전에 반드시 실제 환경과 동일한 조건에서 테스트를 진행해야 하며, 테스트 코드 작성 시에도 실제 동작 흐름을 최대한 반영하는 것이 중요합니다.

Comments