| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |
- 공통기술
- DDD
- JPA
- 디스크i/o
- Ajax
- AOP
- MVC요청플로우
- 사면초가
- 공통규약
- 단위테스트
- 전전긍긍
- string
- Transaction
- 설정세팅
- 로그백
- 냄새라도
- SpringLegacy
- JDBC
- 이벤트핸들러this
- MVC
- 이벤트핸들러등록
- 문제선택근거
- springsecurity
- 커뮤니티서버
- TDD
- 클린아키텍처
- index
- OOP
- SEQUENCE
- model
- Today
- Total
개발이군고구마
@NoArgsConstructor & @AllArgsConstructor: 무작위 생성자 어노테이션의 진짜 의미 본문
레거시 코드를 보다 보면 @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(...) 안에 들어가면 컴파일러는:
- map이 요구하는 타입이 Function<T,R> 임을 알고
- 현재 스트림 요소 타입이 DspCmmnBrResultBoDTO라는 걸 알고
- 결과 타입이 DspCmmnBrndFindResponse이어야 함을 알고
- 그러면 생성자 DspCmmnBrndFindResponse(DspCmmnBrResultBoDTO) 형태의 생성자가 있는지 찾고
- 있으면 그 생성자를 호출하는 함수로 :: from 를 연결한다.
그래서 DspTgtGrpResponse에 이런 생성자가 있어야하며, 없다면 컴파일 에러가 발생한다.
public static DspCmmnBrndFindResponse from(DspCmmnBrResultBoDTO dto) {
return new DspCmmnBrndFindResponse(dto); 2️⃣ 생성자를 호출
}
5. 그래서 Lombok 생성자 어노테이션은 언제 쓰는 게 좋을까?
- @NoArgsConstructor
- Jackson, JPA 등 프레임워크 요구사항이 있을 때만 사용
- @AllArgsConstructor
- 테스트용, 단순 VO 등 명확한 목적이 있을 때만 사용
- Response / DTO
- 명시적 생성자 + 정적 팩토리 메서드가 더 의도를 잘 드러냄
'SERVER > Spring' 카테고리의 다른 글
| JDBC 트랜잭션의 원리 (0) | 2026.05.17 |
|---|---|
| JVM 클래스 로딩 vs Spring 컴포넌트 스캔: 같은 ‘로딩’이 아니다 (0) | 2025.12.31 |
| [HandlerMethodArgumentResolver] 레거시코드, 고쳐도 될까? (0) | 2025.12.31 |
| [Spring Batch] 수기 업무의 늪에서 탈출하기: 조건부 흐름 제어와 대용량 처리 (1) | 2025.12.02 |
| [Spring Security] redis/DB로 자동로그인 구현 (0) | 2025.11.28 |