TIL - 생성자에 private과 protected, 언제 어떻게 쓸까

2026. 4. 24. 12:23·내배캠

DDD에서 도메인과 엔티티를 분리했을 때, 생성자의 접근 제한자

DDD 프로젝트에서 도메인 객체와 JPA 엔티티를 분리했을 때, 생성자에 어떤 접근 제한자를 붙여야 하는지 직접 정리해 보았다.

기존 방식: 엔티티에 도메인 로직을 함께 담을 때

Spring 프로젝트에서 엔티티 하나가 도메인 역할까지 같이 맡는 경우가 많다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CompetitionEntity {
    // 필드, 비즈니스 로직 ...
}

여기서 기본 생성자에 protected를 붙이는 이유는 단순하다.JPA 스펙이 그렇게 요구하기 때문이다.

JPA는 DB에서 데이터를 읽어와 객체를 복원할 때, 우리가 정의한 생성자를 직접 호출하지 않는다.

리플렉션으로 기본 생성자를 호출해 빈 객체를 만든 뒤, 필드에 값을 하나씩 주입한다.

DB 조회 → 기본 생성자로 빈 객체 생성 → 필드에 값 주입

이때 기본 생성자가 private이면 JPA가 접근하지 못해 런타임 에러가 발생한다.
JPA 스펙은 기본 생성자를 public 또는 protected로 요구한다.

그래서 외부의 무분별한 생성을 막으면서 JPA는 접근할 수 있도록, 둘 중 더 닫혀 있는 protected를 쓰는 것이다.

그런데 도메인을 따로 분리하면?

DDD에서는 도메인 모델의 순수성을 지키기 위해 도메인 객체를 JPA에서 분리하기도 한다.

domain.model       ← 순수 도메인 객체 (JPA 의존 X)
infrastructure     ← JPA 엔티티 (기술 구현)

이 구조에서는 도메인 객체가 더 이상 JPA에 의존하지 않는다. 즉, JPA에 양보할 이유 자체가 사라진다.

생성자에 protected를 붙여야 할 근거가 없어진 것이다.

순수 도메인 객체: private 생성자 + 정적 팩토리

먼저 순수 도메인 VO인 ParticipantCount를 보자.

public class ParticipantCount {

    private final int min;
    private final int max;
    private final int current;

    private ParticipantCount(int min, int max, int current) {
        if (min > max) {
            throw new IllegalArgumentException("최소 인원은 최대 인원보다 클 수 없습니다.");
        }
        if (min < 0 || current < 0) {
            throw new IllegalArgumentException("참가 인원은 0 이상이어야 합니다.");
        }
        this.min = min;
        this.max = max;
        this.current = current;
    }

    public static ParticipantCount of(int min, int max) {
        return new ParticipantCount(min, max, 0);
    }

    public static ParticipantCount of(int min, int max, int current) {
        return new ParticipantCount(min, max, current);
    }
    // ...
}

생성자가 private이다. JPA가 손대지 않으니 리플렉션 제약에서 자유롭고,

외부에서는 오직 of() 팩토리 메서드를 통해서만 객체를 만들 수 있다.

잘못된 값으로는 애초에 인스턴스가 만들어지지 않는다. min > max이거나 음수가 들어오면 생성 단계에서 막힌다.

애그리거트 루트인 Competition도 같은 원칙을 따른다.

@Builder(access = AccessLevel.PRIVATE)
private Competition(...) {
    // 신규 생성용
    validate();
}

private Competition(UUID competitionId, ...) {
    // 영속 데이터 복원용
    validate();
}

public static Competition createCompetition(...) { ... }
public static Competition from(UUID competitionId, ...) { ... }

생성자 두 개 다 private, 빌더도 AccessLevel.PRIVATE로 닫혀 있다.

외부에서는 createCompetition()(신규 생성)이나 from()(영속 데이터 복원) 두 개의 정적 팩토리 메서드만 보인다.

도메인 객체가 자기 생성 규칙을 완전히 통제하게 된 것이다.

JPA 엔티티: 여전히 protected가 필요하다

반대로 CompetitionEntity는 JPA 영역이니 옛날 방식 그대로다.

@Entity
@Table(name = "p_competitions")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CompetitionEntity extends BaseEntity implements Persistable<UUID> {

    @Builder(access = AccessLevel.PRIVATE)
    private CompetitionEntity(...) {
        // ...
        validate();
    }

    public static CompetitionEntity from(Competition domain) { ... }
    public Competition toDomain() { ... }
}

기본 생성자는 protected. JPA가 리플렉션으로 호출해야 하기 때문이다.

빌더 생성자는 private이고, 외부에서는 도메인 객체를 받는 from(Competition domain) 팩토리만 노출한다.

 

엔티티는 도메인을 DB에 저장할 형태로 변환하는 책임만 가진다.

정리

제한자 접근 이유
protected JPA 엔티티 기본 생성자 JPA가 리플렉션으로 호출해야 함
private JPA 엔티티 빌더/지정 생성자 외부 생성은 from() 같은 팩토리로만
private 순수 도메인 객체 생성자 JPA 제약 없음, 팩토리로 완전 캡슐화

protected는 JPA에 양보하기 위한 타협이고, private은 도메인이 스스로의 생성 규칙을 통제하기 위한 선택이다.

마무리

도메인과 엔티티를 분리해서 DDD를 적용할 때, 생성자가 어디에서 호출되고 사용되는 지를 생각하면서 개발해야하는 것 같다.
나아가서 생성자뿐만이아니라 메서드의 사용 위치를 고려해야하는 이유를 느꼈다.

 

'내배캠' 카테고리의 다른 글

전자기기로 이해하는 헥사고날 아키텍처  (1) 2026.04.30
TIL - 공통 모듈의 ErrorCode, 어떻게 관리해야 할까  (0) 2026.04.26
TIL - 실시간 랭킹 구현 방법  (0) 2026.04.23
TIL - 개발 키워드 정리  (0) 2026.04.15
TIL - 회복성, SPOF, 분산 트랜잭션과 결과적 일관성  (1) 2026.04.13
'내배캠' 카테고리의 다른 글
  • 전자기기로 이해하는 헥사고날 아키텍처
  • TIL - 공통 모듈의 ErrorCode, 어떻게 관리해야 할까
  • TIL - 실시간 랭킹 구현 방법
  • TIL - 개발 키워드 정리
MvA
MvA
백엔드 개발자 김재현입니다. 주로 공부하면서 느낀점을 기록합니다.
  • MvA
    Man vs Ai
    MvA
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • Java (6)
      • Python (8)
        • 딥러닝 (1)
        • 머신러닝 (7)
      • JavaScript (2)
      • 내배캠 (60)
      • 개인 프로젝트 (11)
      • 책 후기 (5)
      • 기타 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    머신러닝
    내일배움캠프
    아키텍처
    TiL
    배포
    딥러닝
    Riot API
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
MvA
TIL - 생성자에 private과 protected, 언제 어떻게 쓸까
상단으로

티스토리툴바