개발/백엔드

[JPA/Spring] 엔티티 설계의 정석: @NoArgsConstructor(access = AccessLevel.PROTECTED)와 @Builder의 조화

taecostudy76 2026. 3. 11. 15:51

최근 프로젝트를 진행하며 JPA 엔티티를 설계하는 과정에서 왜 특정 어노테이션을 조합해 사용하는지, 그리고 객체 지향적인 설계란 무엇인지 깊게 파고들어 보았습니다. 공부한 내용을 잊지 않기 위해 정리합니다.


1. @NoArgsConstructor(access = AccessLevel.PROTECTED)

JPA 엔티티를 만들 때 가장 먼저 마주친 의문은 "왜 기본 생성자를 굳이 protected로 막는가?"였습니다.

  • JPA의 요구사항: JPA는 프록시 객체를 생성하기 위해 기본 생성자가 반드시 필요합니다. 하지만 접근 제어자가 private이면 접근할 수 없으므로 최소 protected 이상이어야 합니다.
  • 객체 오염 방지: 개발자가 new Member()와 같이 빈 객체를 생성하는 것을 막기 위함입니다. 필수 데이터가 없는 불완전한 객체가 시스템을 돌아다니는 것을 원천 차단합니다.
  • 결론: JPA는 일하게 하되, 개발자의 실수는 막는 절묘한 방어막입니다.

2. @Builder와 생성자의 조합

빈 객체 생성을 막았다면, 객체는 어떻게 만들어야 할까요? 정답은 빌더 패턴입니다.

  • 가독성: new Member("홍길동", 20, ...) 처럼 순서에 의존하는 대신 .name("홍길동").age(20)처럼 명확한 코드를 작성할 수 있습니다.
  • 안전성: 클래스 레벨이 아닌 생성자 레벨에 @Builder를 붙여, 내가 허용하는 필드만 빌더에 포함하는 정교한 설계가 가능해집니다.

3. @Embeddable & @Embedded (데이터의 부품화)

모든 필드를 하나의 클래스에 다 넣으면 클래스가 너무 비대해집니다. 이때 사용하는 것이 임베디드 타입입니다.

  • @Embeddable: 독립적인 테이블이 아닌, 다른 엔티티의 부품이 될 클래스에 사용합니다.
  • @AttributeOverrides: 같은 부품을 한 테이블에서 다른 컬럼명으로 쓰고 싶을 때 재정의하여 사용합니다.
  • 이점: 코드의 재사용성이 높아지고, 실력 점수 계산 같은 특정 로직을 해당 클래스에 응집시킬 수 있어 객체 지향적입니다.

4. 양방향 연관관계와 데이터 일치

MatchRecord(게임 기록)와 MatchParticipant(참가자)의 관계를 공부하며 가장 헷갈렸던 부분입니다.

  • @ManyToOne vs @OneToMany: '다(Many)' 쪽이 외래 키(FK)를 가지는 주인입니다.
  • 양방향 데이터 일치의 필요성: DB는 FK 하나로 양쪽을 알지만, 자바 객체는 그렇지 않습니다.
  • 연관관계 편의 메서드: matchRecord.getParticipants().add(this);
  • 생성자에서 이 코드를 호출함으로써, 내 수첩뿐만 아니라 상대방의 리스트에도 나를 추가해줘야 메모리상에서 데이터 불일치가 일어나지 않습니다.

💡 마치며

단순히 코드를 작동시키는 것을 넘어, "왜 이 어노테이션을 쓰는가?"와 "어떻게 하면 더 안전한 객체를 만들 것인가?"를 고민해 본 시간이었습니다. JPA는 알수록 객체 지향 프로그래밍에 대한 깊은 이해를 요구하는 것 같습니다.