[싸피셜이 알려드림: 기술편] @Builder의 역직렬화 동작 원리 파헤치기

2025. 7. 22. 22:43·SSAFY

< 2025.03.29 업로드 >

안녕하세요, 여러분! 👋 싸피 12기 싸피셜 기자단 안수진입니다! 저는 백엔드 개발자를 희망하고 있다는거 기억하고 계시나요?

백엔드 개발을 공부하다 보면 종종 "어? 이게 왜 되지? 🤔" 하는 순간을 마주치게 되죠. 최근 저도 그런 순간을 경험했는데요, Spring Boot 프로젝트에서 다음 코드가 어떻게 정상적으로 작동하는지에 대한 의문이었습니다.

 

장바구니 수량 요청을 보내는데 사용되는 DTO 클래스

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@Schema(description = "장바구니 상품 수량 수정 요청 DTO")
public class CartQuantityRequestDto {
    @Schema(description = "상품 수량", example = "10")
    private Integer quantity;
}

 

swagger로 API 테스트 시, 정상 응답이 오는 모습

 

일반적으로 Lombok의 `@Builder`를 사용할 때는`@NoArgsConstructor`와 `@AllArgsConstructor`도 함께 추가해야 한다고 알고 있었습니다. 특히 Spring Web에서 HTTP 요청을 역직렬화할 때 기본 생성자가 필요하다고 배웠는데, 이 DTO는 그런 어노테이션 없이도 정상적으로 동작하는 거예요! 😲

 

그런데 잠깐, 사실 요청 DTO에는 `@Builder`가 필요하지 않다는 점도 언급해야겠네요.
요청 DTO는 주로 클라이언트에서 서버로 데이터를 전송할 때 사용되며, 서버에서는 이 데이터를 받아 객체로 변환(역직렬화)하는 과정만 필요합니다. 이 과정에는 기본 생성자와 getter 메소드만 있으면 충분하죠.

그런데도 많은 프로젝트에서 습관적으로 모든 DTO에 `@Builder`를 붙이곤 합니다. 제 코드도 그랬네요! 그런데 "아니, 기본 생성자 없이 어떻게 Jackson이 이걸 역직렬화한 거지?" 이 의문을 함께 해결해볼까요?

 


 

🤯 의문의 시작: @Builder만으로 왜 작동할까?

Spring MVC에서는 HTTP 요청을 Java 객체로 변환하기 위해 Jackson 라이브러리를 사용합니다. 그리고 일반적으로 Jackson은 역직렬화를 위해 기본 생성자(no-args constructor)가 필요합니다. 그런데 위의 코드에는 기본 생성자가 없는데도 문제없이 작동했습니다.

이 의문점을 해결하기 위해 먼저 @Builder 어노테이션이 어떻게 동작하는지, 그리고 이것이 왜 Jackson 역직렬화와 충돌하지 않는지 살펴보겠습니다.

 

🔍 Lombok @Builder의 동작 원리

Lombok의 `@Builder` 어노테이션을 클래스에 적용하면 컴파일 시점에 다음과 같은 코드가 생성됩니다.

  1. 정적 빌더 클래스 생성
    • 원본 클래스 내부에 `[클래스명]Builder` 라는 정적 내부 클래스가 생성됩니다.
    • 이 빌더 클래스는 원본 클래스의 모든 필드에 대한 setter 메소드와 build() 메소드를 포함합니다.
  2. 모든 필드를 받는 생성자 생성
    • 모든 필드를 매개변수로 받는 private 생성자가 생성됩니다.
    • 이 생성자는 빌더 클래스의 build() 메소드에서만 호출됩니다.
  3. 정적 builder() 메소드 생성
    • 원본 클래스에 정적 `builder()` 메소드가 추가됩니다.
    • 이 메소드는 빌더 인스턴스를 생성하여 반환합니다.

예를 들어, 위의 CartQuantityRequestDto 클래스는 실제로 컴파일 후 다음과 같은 코드로 변환됩니다.

import io.swagger.v3.oas.annotations.media.Schema;

public class CartQuantityRequestDto {
    @Schema(description = "상품 수량", example = "10")
    private Integer quantity;
    
    // private 생성자 (모든 필드를 매개변수로 받음)
    private CartQuantityRequestDto(Integer quantity) {
        this.quantity = quantity;
    }
    
    // Getter (Lombok의 @Getter에 의해 생성됨)
    public Integer getQuantity() {
        return this.quantity;
    }
    
    // 정적 builder() 메소드
    public static CartQuantityRequestDtoBuilder builder() {
        return new CartQuantityRequestDtoBuilder();
    }
    
    // 빌더 클래스
    public static class CartQuantityRequestDtoBuilder {
        private Integer quantity;
        
        // private 생성자
        private CartQuantityRequestDtoBuilder() {
        }
        
        // 필드에 대한 setter 메소드
        public CartQuantityRequestDtoBuilder quantity(Integer quantity) {
            this.quantity = quantity;
            return this;
        }
        
        // build() 메소드
        public CartQuantityRequestDto build() {
            return new CartQuantityRequestDto(quantity);
        }
        
        // toString() 메소드
        public String toString() {
            return "CartQuantityRequestDto.CartQuantityRequestDtoBuilder(quantity=" + this.quantity + ")";
        }
    }
}

 

하지만 여기서 눈여겨봐야 할 점은

원본 클래스는 매개변수 없는 기본 생성자가 없다는 것입니다.

그렇다면 왜 Jackson은 역직렬화에 문제가 없었을까요?

 

📡 웹 통신에서 역직렬화가 필요한 이유

이 의문을 해결하기 위해, 먼저 왜 웹 요청에서 역직렬화가 필요한지 살펴보겠습니다.

네트워크 통신에서 직렬화와 역직렬화가 필요한 이유는 네트워크가 기본적으로 바이트 스트림만 전송할 수 있기 때문입니다. Java 객체는 메모리에 복잡한 구조로 저장되어 있어 직접 전송이 불가능합니다.

 

네트워크 통신의 근본적 제약

네트워크는 기본적으로 바이트(0과 1로 이루어진 데이터)만 전송할 수 있습니다. 마치 우편 시스템이 종이에 쓰인 글자만 전송할 수 있는 것과 비슷하죠! 편지를 보낼 때 사람을 우체통에 넣어 보낼 수 없는 것처럼요. 😅

 

바이트 스트림이란?

바이트 스트림은 연속된 바이트(8비트) 단위의 데이터 흐름입니다. 컴퓨터가 네트워크로 데이터를 보낼 때 사용하는 가장 기본적인 형태예요. 마치 모스 부호처럼 모든 것을 단순한 신호로 변환해야 하는 거죠!

예시: "Hello" 라는 문자열의 바이트 표현
[72, 101, 108, 108, 111] (ASCII 코드값)

 

직렬화와 역직렬화의 역할 

직렬화(Serialization)는 복잡한 객체를 "편지"처럼 일렬로 작성하는 과정입니다.
쉽게 말하면 Java 객체를 "평평하게 펴서" 텍스트로 만드는 거예요. 마치 3D 모형을 사진으로 찍어 2D로 만드는 것처럼요!

User 객체 → 직렬화 → {"name":"홍길동","age":30}

 

역직렬화(Deserialization)는 받는 쪽에서 이 "편지"를 읽고 다시 객체로 만드는 과정입니다.

이건 마치 종이 접기 설명서를 보고 다시 종이학을 접는 것과 같죠! 📄 ➡️ 🦢

Spring에서는 Jackson 라이브러리가 이 작업을 자동으로 처리합니다. 일반적으로 Jackson은 역직렬화를 위해 기본 생성자가 필요하다고 알려져 있습니다. 그런데 어떻게 @Builder만 있는 클래스가 정상적으로 역직렬화될 수 있었을까요?

{"name":"홍길동","age":30} → 역직렬화 → User 객체

 

🔄 Jackson 역직렬화와 생성자의 관계

Jackson이 JSON을 Java 객체로 역직렬화하는 방법은 여러 가지가 있습니다.

  1. 기본 생성자를 사용한 역직렬화
    • 기본 생성자를 사용해 객체를 생성한 후
    • JSON의 각 필드 값을 찾아 setter 메소드나 필드에 직접 값을 설정
  2. 생성자 기반 역직렬화
    • @JsonCreator와 @JsonProperty를 사용한 명시적 생성자 매핑
    • 또는 매개변수 이름을 기반으로 한 자동 매핑

 

실제로 Spring Boot 3.x 버전에서는 Jackson의 ParameterNamesModule이 기본적으로 활성화되어 있어, 생성자 매개변수 이름을 사용하여 JSON 속성을 매핑할 수 있습니다. 따라서 다음과 같은 시나리오가 가능합니다.

  1. @Builder가 모든 필드를 매개변수로 받는 생성자를 생성
  2. Spring Boot가 구성한 Jackson이 이 생성자의 매개변수 이름을 인식
  3. JSON 속성을 생성자 매개변수에 매핑하여 객체를 생성

프로젝트의 의존성을 살펴보면

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.7'
}

// ... 중략 ...

dependencies {
    // 다양한 의존성들
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // lombok 관련
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    
    // ... 기타 의존성들
}

최신 버전의 Spring Boot와 Lombok을 사용하고 있어 이러한 동작이 가능했던 것입니다.

 

🤔 그렇다면 왜 일반적으로 @NoArgsConstructor가 필요할까?

그렇다면 왜 많은 개발자들이 DTO 클래스에 `@NoArgsConstructor`를 추가하는 것을 권장할까요?

  1. 호환성과 안정성
    • Jackson의 설정이나 버전에 따라 동작이 달라질 수 있습니다.
    • 모든 환경에서 일관된 동작을 보장하기 위해 기본 생성자를 명시적으로 추가합니다.
  2. 유지보수성
    • 코드의 의도를 명확히 표현하여 다른 개발자가 이해하기 쉽게 만듭니다.
    • 잠재적인 오류를 방지하고 디버깅을 용이하게 합니다.
  3. 다른 라이브러리와의 호환성
    • JPA, MyBatis 등 다른 프레임워크에서도 기본 생성자를 필요로 하는 경우가 많습니다.

 

👨‍💻 모범 사례: 기본 생성자와 전체 생성자 명시하기

결론적으로, Spring Boot와 Lombok을 사용한 DTO 클래스는 다음과 같이 작성하는 것이 가장 안전합니다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "장바구니 상품 수량 수정 요청 DTO")
public class CartQuantityRequestDto {
    @Schema(description = "상품 수량", example = "10")
    private Integer quantity;
}

 

이렇게 하면
1. @NoArgsConstructor: Jackson 역직렬화를 위한 기본 생성자 제공
2. @AllArgsConstructor: 모든 필드를 초기화하는 생성자 (Builder 패턴에서 사용)
3. @Builder: 빌더 패턴 구현
4. @Getter: Jackson이 필드 값을 읽을 수 있도록 getter 메소드 제공

 

💬 요청과 응답: 직렬화와 역직렬화의 차이점

지금까지 우리는 요청 DTO의 역직렬화 과정에 대해 알아봤습니다. 반대로 서버에서 클라이언트로 응답을 보낼 때는 어떨까요?

 

응답을 보낼 때는 Java 객체를 JSON으로 직렬화하는 과정이 발생합니다. 이때 Jackson은 객체의 getter 메소드를 사용하여 필드 값에 접근하고, 이를 JSON 형태로 변환합니다. 따라서 응답 DTO에는 @Getter가 필수적입니다.

 

또한 응답 DTO는 서버에서 생성되므로, 객체 생성의 편의성을 위해 @Builder가 유용하게 사용됩니다. 불변 객체로 만들기 위해 setter 없이 생성자나 빌더로만 값을 설정하는 것이 좋은 관행이에요.

// 응답 DTO의 일반적인 형태
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)  // Builder에서만 사용
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 프록시 등을 위해
public class ItemResponseDto {
    private Long id;
    private String name;
    private Integer price;
}

이처럼 요청 DTO와 응답 DTO는 그 목적과 사용 패턴이 다르므로, 각각에 맞는 적절한 어노테이션 조합을 사용하는 것이 중요합니다.

 

🎯 결론

이제 미스터리가 풀렸네요! 😄 Spring Boot와 Lombok을 사용할 때 @Builder만으로도 Jackson 역직렬화가 정상 작동할 수 있습니다. 이는 최신 버전의 Spring Boot가 Jackson을 구성하는 방식과 Lombok이 생성하는 코드의 특성 덕분이랍니다.

 

하지만! 코드의 명확성과 안정성을 위해서는 @NoArgsConstructor와 @AllArgsConstructor를 명시적으로 추가하는 것이 좋은 습관이에요. 특히 팀 프로젝트에서는 동료 개발자들이 "이게 왜 되지?" 하고 헤맬 필요 없도록 명확하게 작성하는 것이 중요합니다.

 

또한, 요청 DTO에는 사실 @Builder가 필요하지 않다는 점도 기억해주세요!
요청 DTO는 클라이언트에서 서버로 데이터를 받는 용도이므로, 역직렬화에 필요한 기본 생성자와 getter 메소드만 있으면 충분합니다. 습관적으로 모든 DTO에 @Builder를 붙이는 패턴을 피하고, 각 클래스의 용도에 맞는 적절한 어노테이션을 사용하는 것이 좋습니다.

 

웹 개발에서 직렬화와 역직렬화는 마치 편지를 쓰고 읽는 것처럼 필수적인 과정이에요. 이 과정을 정확히 이해하고 적절히 구현하면 더 안정적이고 유지보수하기 좋은 애플리케이션을 만들 수 있답니다! 🚀

여러분의 코드에도 이런 작은 미스터리가 숨어 있진 않나요? 한 번 찾아보세요!

 

🗂️ References

RequestBody는 기본생성자가 필요없다.(feat: ParameterNamesModule)
@RequestBody에 기본생성자만 필요하고 Setter는 필요없는 이유 - 1


👀 SSAFY의 다양한 소식과 이야기를 더 알고 싶다면
📌 SSAFYcial 인스타그램
📌 SSAFY 홈페이지

'SSAFY' 카테고리의 다른 글

[싸피셜이 알려드림: SSAFY편] 전공자가 SSAFY를 선택한 이유  (2) 2025.07.22
[싸피셜이 알려드림: 기술편] 핫하다 핫해 AI 에이전트  (3) 2025.07.22
[싸피셜이 알려드림: SSAFY편] 싸피에서 IT 개발자로 성장하기  (1) 2025.06.19
[싸피셜이 알려드림: SSAFY편] SSAFY를 만나고 달라진 일상  (0) 2025.02.27
[싸피셜이 알려드림: 기술편] localhost와 127.0.0.1의 차이  (0) 2025.02.25
'SSAFY' 카테고리의 다른 글
  • [싸피셜이 알려드림: SSAFY편] 전공자가 SSAFY를 선택한 이유
  • [싸피셜이 알려드림: 기술편] 핫하다 핫해 AI 에이전트
  • [싸피셜이 알려드림: SSAFY편] 싸피에서 IT 개발자로 성장하기
  • [싸피셜이 알려드림: SSAFY편] SSAFY를 만나고 달라진 일상
ssuzyn
ssuzyn
  • ssuzyn
    멋쟁이 개발자
    ssuzyn
  • 링크

    • github
    • velog
  • 전체
    오늘
    어제
    • 분류 전체보기 (71)
      • 프로젝트 (9)
        • 짠모아 (6)
        • 피노키오 (0)
      • 코딩 테스트 (39)
        • Baekjoon (27)
        • SWEA (11)
        • Programmers (1)
      • Study (3)
        • Spring (3)
        • Algorithm (0)
      • SSAFY (18)
      • 이모저모 (2)
  • 인기 글

  • 블로그 메뉴

    • 홈
    • 방명록
  • hELLO· Designed By정상우.v4.10.0
ssuzyn
[싸피셜이 알려드림: 기술편] @Builder의 역직렬화 동작 원리 파헤치기
상단으로

티스토리툴바