개발이군고구마

@NoArgsConstructor & @AllArgsConstructor: 무작위 생성자 어노테이션의 진짜 의미 본문

SERVER/Spring

@NoArgsConstructor & @AllArgsConstructor: 무작위 생성자 어노테이션의 진짜 의미

김구황 2026. 1. 6. 07:27
728x90

레거시 코드를 보다 보면 @NoArgsConstructor, @AllArgsConstructor가 아무 생각 없이 붙어 있는 경우를 자주 만난다.

@NoArgsConstructor
@AllArgsConstructor
public class Response extends DefaultResponse {
	....
}

 

처음엔 “이렇게 쓰라고 있는 거겠지” 하고 넘겼지만, 정말 이 생성자들이 왜 필요한지, 그리고 지금 이 위치에 맞는 설계인지에 대한 의문이 들었다.

 

 

 

1. 레거시 코드의 Lombok 생성자 어노테이션

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class SampleResponse {
    private String name;
    private int age;
}
  • 정말 모든 필드를 받는 생성자가 필요한가?
  • 이 객체는 어디에서, 어떤 책임으로 생성되는가?
  • 프레임워크(Jackson, JPA) 때문에 필요한 생성자인가, 아니면 그냥 붙여둔 것인가?

생성자는 객체의 생명주기 진입점인데, 그 의미를 고려하지 않고 어노테이션을 붙이면 설계가 흐려진다.

 

 

2. 생성자 기본 원리 (상속에서 핵심)

  • 자식 객체를 만들면, 내부적으로 부모 부분부터 먼저 생성돼야 한다.
  • 그래서 자식 생성자는 결국 아래 중 하나를 “첫 줄”에 해야한다. 
    • super(...) : 부모 생성자 호출
    • this(...) : 같은 클래스의 다른 생성자 호출(결국 마지막엔 super(...)로 연결됨)
public class Parent {
    public Parent(String value) {
        System.out.println(value);
    }
}

public class Child extends Parent {
    public Child() {
        // 컴파일 에러
    }
}

※ 즉, 부모에 “인자 없는 생성자(super())”가 없으면 자식 생성자가 super()를 호출할 수 없어서 컴파일 에러

  • BUT! 아무것도 안 쓰면 Java가 자동으로 super()를 삽입한다. 

 

 

3. 생성자 어노테이션

 

1️⃣ @NoArgsConstructor (기본 생성자)

👉 응답 DTO에는 거의 필수

@NoArgsConstructor
public class Example {
    private String a;
    private String b;
}
new Example();

의미

  • 필드 값은 전부 null / 기본값
  • 프레임워크(Jackson, Nexacro, JPA, 직렬화)가 객체를 먼저 만들고 나중에 필드를 채우는 방식에 필요 (set...)
Response r = new Response();
r.setGrpcoId("A");
r.setGrpcoNm("B");

 

▶ 언제 좋을까 

  • 응답 DTO
  • 직렬화/역직렬화 대상
  • 상속 구조에서 상위 생성자 자동 호출을 기대할 때

 

 

 2️⃣ @AllArgsConstructor (모든 필드를 받는 생성자)

@AllArgsConstructor
public class Example {
    private String a;
    private String b;
}
public Example(String a, String b) {
    this.a = a;
    this.b = b;
}

생성되는 코드

new Example("x", "y");

의미

▶ 언제 좋을까? 

  • 불변 객체
  • 값 객체(Value Object)
  • 상속 없는 단순 DTO
  • “이 생성자를 호출하면 객체가 완성된다”가 보장될 때

 

 

3️⃣ 상속클래스와 @AllArgsConstructor 👉 헤깔리는 부분

 

3-1. @AllArgsConstructor가 “있을 때” 

ㄴ Lombok의 @AllArgsConstructor는 기본적으로 “해당 클래스(자식)에 선언된 필드만” 받는 생성자를 만들어 줌 
(부모 필드까지 자동 포함 X — 기본 동작)

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Response extends DefaultResponse {
    String grpcoId;
    String grpcoNm;

    public Response(DspCmmnBnrTgtBoDTO dto) {
        this.grpcoId = dto.getGrpcoId();
        this.grpcoNm = dto.getGrpcoNm();
    }
}
public Response(String grpcoId, String grpcoNm) {
    super();              // 부모의 no-arg 생성자 호출 (없으면 에러)
    this.grpcoId = grpcoId;
    this.grpcoNm = grpcoNm;
}

생성되는 코드 

 

✅ 결과적으로 가능한 생성자 조합

  • new DspTgtGrpResponse() (NoArgsConstructor)
  • new DspTgtGrpResponse("A", "B") (AllArgsConstructor로 추가)
  • new DspTgtGrpResponse(dto) (직접 작성한 생성자)

⚠️ 중요한 포인트

  • 위 AllArgs 생성자는 부모의 _CHK, _STATUS는 초기화하지 않음.
    • 부모 필드는 super() 호출로 인해 기본값(false / null)  (부모의 생성자 세팅 방법으로) 상태로 남게 됨
  • 테스트/임시 객체 만들 땐 편하지만, 부모 필드까지 “꼭 세팅돼야 한다”면 의도치 않은 누락이 생길 수 있음.

 

3-2. @AllArgsConstructor가 “없을 때”

@AllArgsConstructor가 없으면 Lombok이 만들어주는 “필드 2개짜리 생성자”가 사라져서

 

  생성자 선택지는 보통 아래와 같이 만들어진다. 

new DspTgtGrpResponse();      // 가능 (NoArgsConstructor가 있다면)
new DspTgtGrpResponse(dto);   // 가능 (직접 만든 생성자)
new DspTgtGrpResponse("A","B");// ❌ 불가능 (해당 시그니처 생성자가 없음)

 

대신 문자열 두 필드를 넣고 싶다면:

DspTgtGrpResponse r = new DspTgtGrpResponse();
r.setGrpcoId("A");
r.setGrpcoNm("B");

 

혹은 DTO 생성자를 강제하고 싶다면(“DTO로만 만들게” 설계):

// @NoArgsConstructor도 빼버리면 new DspTgtGrpResponse()도 막을 수 있어요.
public DspTgtGrpResponse(DspCmmnBnrTgtBoDTO dto) { ... }

 

 

3-3. (참고) “부모 필드까지 포함한 AllArgs 생성자”가 필요하면?

▶ 부모 필드까지 한 번에 받고 싶으면, 보통 부모에도 AllArgs 생성자가 있어야 하고, 자식은 callSuper = true를 키면된다. 

@Data
@NoArgsConstructor
@AllArgsConstructor ✅
public class DefaultResponse {
    private boolean _CHK;
    private String _STATUS;
}
@Data
@NoArgsConstructor
@AllArgsConstructor(callSuper = true) ✅
public class DspTgtGrpResponse extends DefaultResponse {
    private String grpcoId;
    private String grpcoNm;
}

 

이러면 생성자가 이런 형태로 생성된다

public DspTgtGrpResponse(boolean _CHK, String _STATUS, String grpcoId, String grpcoNm) {
    super(_CHK, _STATUS);
    this.grpcoId = grpcoId;
    this.grpcoNm = grpcoNm;
}

 

 

 

4. 생성자와 정적 팩토리 메서드 

ㄴ생성자 vs from() vs fromList() 분리의 장점 

// ✅ 생성자: 기본적인 객체 생성
public DspCmmnBrndFindResponse(List<DspCmmnBrndFindResponse> convertedList, 
                                Pageable pageable, int totalCnt) { 3️⃣ 실 생성자
    super(convertedList, pageable, totalCnt);
    this.dspCmmnBrndFindResponseList = convertedList;
}

// from: 단일 DTO → Response 변환 (정적 팩토리 메서드)
public static DspCmmnBrndFindResponse from(DspCmmnBrResultBoDTO dto) {
    return new DspCmmnBrndFindResponse(dto); 2️⃣ 생성자를 호출 
}

// fromList: 여러 DTO → Response 리스트 변환
public static List<DspCmmnBrndFindResponse> fromList(List<DspCmmnBrResultBoDTO> results) {
    return results.stream()
        .map(DspCmmnBrndFindResponse::from) 1️⃣ 단일 객체로 변환 (DTO를 던져서 생성자를 만들겠다)
        .collect(Collectors.toList());
}

 

▶ 언제 사용될까?

JPA Entity → DTO 변환, API Response 생성 등

 

  왜 static으로 선언할까?

❇정적 팩토리 메서드 패턴 (Static Factory Method Pattern)

// static이 아니면? → 이상한 코드
DspCmmnBrndFindResponse temp = new DspCmmnBrndFindResponse();
DspCmmnBrndFindResponse result = temp.from(dto); // ❌ 어색함

// static이면? → 자연스러운 코드  
DspCmmnBrndFindResponse result = DspCmmnBrndFindResponse.from(dto); // ✅ 명확함

 

어떤 장점이 있을까? 

(1) 인스턴스 없이 호출 가능

// 객체를 먼저 만들 필요가 없음
List<DspCmmnBrndFindResponse> responses = 
    DspCmmnBrndFindResponse.fromList(dtoList); // 바로 사용

// 만약 static이 아니라면?
DspCmmnBrndFindResponse dummy = new DspCmmnBrndFindResponse(...); // 불필요한 객체 생성
List<DspCmmnBrndFindResponse> responses = dummy.fromList(dtoList); // 이상함

 

(2) 의미론적 명확성

  • Controller는 흐름 제어만 담당
  • 객체 생성 책임을 Response 클래스에 집중
  • from(): "DTO로부터 Response를 만든다"는 의미
  • 특정 인스턴스의 메서드가 아닌, 클래스 레벨의 변환 기능임을 명시, 마치 Integer.valueOf(), LocalDate.of() 처럼 사용
  • 변환 로직 변경 시 한 곳에서 관리 가능
  • 테스트가 쉬워짐

 

(참고) stream map의 원리 

return results.stream()
        .map(DspCmmnBrndFindResponse::from) 1️⃣ 단일 객체로 변환 (DTO를 던져서 생성자를 만들겠다)
        .collect(Collectors.toList());

::{생성자} 자체는 “그냥 new”가 아니라, **어떤 함수 타입 자리(타겟 타입)**에 놓이면서 의미가 결정된다.

map(...) 안에 들어가면 컴파일러는:

  1. map이 요구하는 타입이 Function<T,R> 임을 알고
  2. 현재 스트림 요소 타입이 DspCmmnBrResultBoDTO라는 걸 알고
  3. 결과 타입이 DspCmmnBrndFindResponse이어야 함을 알고
  4. 그러면 생성자 DspCmmnBrndFindResponse(DspCmmnBrResultBoDTO) 형태의 생성자가 있는지 찾고
  5. 있으면 그 생성자를 호출하는 함수로 :: from 를 연결한다.

그래서 DspTgtGrpResponse에 이런 생성자가 있어야하며, 없다면 컴파일 에러가 발생한다. 

public static DspCmmnBrndFindResponse from(DspCmmnBrResultBoDTO dto) {
    return new DspCmmnBrndFindResponse(dto); 2️⃣ 생성자를 호출 
}

 

 

 

5. 그래서 Lombok 생성자 어노테이션은 언제 쓰는 게 좋을까?

 

  • @NoArgsConstructor
    • Jackson, JPA 등 프레임워크 요구사항이 있을 때만 사용
  • @AllArgsConstructor
    • 테스트용, 단순 VO 등 명확한 목적이 있을 때만 사용
  • Response / DTO
    • 명시적 생성자 + 정적 팩토리 메서드가 더 의도를 잘 드러냄