[JPA] 2. 도메인 분석 설계 - 엔티티 설계시 주의점

2025. 5. 11. 22:58·BE/SpringBoot
728x90

1. 엔티티에는 가급적 Setter를 사용하지 말자

JPA에서 엔티티는 단순한 데이터 그릇이 아니다.
비즈니스 규칙과 상태를 표현하는 도메인 객체이기 때문에 값을 아무 때나 바꿀 수 있는 @Setter는 유지보수와 안정성을 해친다.

Setter가 모두 열려있을 경우 변경 포인트가 너~무 많아서 유지보수하기 어렵다!(Getter는 열어놔도 됨)


2. 모든 연관관계는 지연로딩(Lazy)으로 설정하자(⭐️)

- 즉시로딩(EAGER)은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.

JPA는 크게 2가지 방식으로 데이터를 조회한다.

JPQL (객체 중심) "SELECT o FROM Order o" 엔티티 기준 쿼리. SQL 아님
JPA 메서드 직접 조회 em.find(Order.class, id) PK로 한 건만 조회할 때

그 중에서 N+1 문제가 자주 터지는 곳은 JPQL로 리스트를 조회할 때 !

즉, 연관관계로 매핑된 엔티티를 조회할 때 첫 번째 쿼리로 날린 결과만큼 n번의 쿼리가 나가는 것을 말한다.


Q) 그럼 N+1문제는 무엇일까?

N+1 문제란 원래는 쿼리 1번이면 충분할 것 같았는데, 실제로는 추가적인 N번의 쿼리가 실행되는 현상을 말한다.

(1+N이라고 생각하면 더 이해하기 편하다.)

 

JPA는 @ManyToOne, @OneToOne, @OneToMany, @ManyToMany 등 여러 연관관계 매핑을 자동으로 지원해준다.

그리고 JPA는 객체 중심(Entity 중심) 으로 개발할 수 있도록 SQL 대신 객체를 다루는 쿼리 언어인 JPQL도 제공한다.

하지만 바로 이 연관관계 즉시 로딩(EAGER) 과 JPQL이 만나면 개발자가 모르게 추가 쿼리(N) 가 발생하게 되는 것이다.

  • 즉시로딩(EAGER): 연관된 엔티티를 함께 한 번에 조회하려고 JPA가 자동으로 JOIN 쿼리를 만들어 실행하는 방식
@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}
@Entity
@Table(name="orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER) // 또는 기본 설정
    @JoinColumn(name = "member_id")
    private Member member;
}

예를 들어 하나의 Member는 여러 개의 Order를 가질 수 있는 엔티티 구조가 있을 때(일대다), Order와 Member의 관계는 다대일의 관계다. 이 경우 우리는 Order를 1번 가져오면 추가 쿼리 발생 없이 Order에 필요한 Member 데이터도 자동으로 가져올 수 있을 거라 생각한다.

List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class).getResultList();

그러나 위 코드를 보면 알 수 있듯 Order는 쿼리로 1번 가져오지만, Order 마다 연결된 Member를 즉시로딩하기 때문에 100번의 Member select가 추가로 발생한다. (Order를 가져오는 시점에 Member를 가져오는 것)

만약 100만명의 회원이 있다면? .. Order 쿼리 한번이 발생할 때 Member 100만번이 실행되어야 한다. 즉, 쿼리 1번 + 연관된 엔티티 N번 = 총 N+1번의 쿼리가 발생한다. 한 번의 서비스 로직 실행에 DB 쿼리가 100만번 실행된다는 것은 엄청난 비효율적인 방식이다.


Q) 그럼 어떻게 해결해야할까?

- 실무에서 모든 연관관계는 지연로딩(LAZY)으로 설정해야 한다.

- @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩(LAZY)으로 설정해야 한다.

@ManyToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY)
  • @XtoMany(OneToMany, ManyToMany) 관계는 기본이 지연로딩(LAZY)이므로 따로 설정해줄 필요 없다.

- 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.

fetch join: 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능 (연관된 엔티티까지 영속성 컨텍스트에 전부 올림)

@Query("select o from Order o join fetch o.member")
List<Order> findOrdersWithMembers();
  • 대표적인 fetch join 문제 상황: OneToMany 관계에서 페이징 처리할 때
    • fetch join과 페이징을 함께 처리하면, fetch join한 데이터를 전부 가져와서(full scan) 인메모리(RAM, Heap Area)에 넣어놓고 가공하는 작업을 거치게 된다. 근데, 만약 데이터가 100만건이라면 이 100만건의 데이터를 전부 가져와서 메모리에서 관리하게 되면 메모리 부하가 일어나게 된다. (페이징은 JPA 내부 메모리에서 수행하게 된다. 이걸 in-memory라고 함)
    • OneToMany, ManyToMany (1:N or N:M)의 연관관계에서 문제가 발생한다.

이걸 해결하기 위한 것이 바로!

@BatchSize()

@Entity
public class Member {

    @OneToMany(mappedBy = "member")
    @BatchSize(size = 100) // member 100명 기준으로 한 번에 orders 조회
    private List<Order> orders = new ArrayList<>();
}

List<Member>를 LAZY 로딩으로 가져온 다음, getOrders()를 호출할 때 100명 단위로 IN 쿼리 한 번에 가져옴

SELECT * FROM orders WHERE member_id IN (?, ?, ..., ?) -- 최대 100개

100개의 SELECT 쿼리가 아니라, 1개의 IN 쿼리로 최적화할 수 있다!


3. 컬렉션은 필드에서 초기화 하자

@OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.

Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());

//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
  • `null` 문제에서 안전하다.
public List<Order> getOrders() {
    if (orders == null) {
        orders = new ArrayList<>();
    }
    return orders;
}
  • 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
  • 이건 게으른 초기화(lazy init) 방식이지만, Hibernate가 proxy로 감싸둔 컬렉션을 개발자가 직접 new로 덮어쓰기 때문에 위험
  • 개발자가 임의로 new를 하면 프록시가 깨지고 Hibernate가 더 이상 추적하거나 조작할 수 없게 된다.

객체를 영속화 한다는 것은 DB에 저장할 수 있는 상태로 객체를 관리 한다는 의미.
  1. EntityManager.persist(entity) 호출
    • 이 시점에서 entity는 영속 상태가 되어 영속성 컨텍스트(1차 캐시)에 등록된다. (DB에 바로 저장X)
  2. 트랜잭션 안에서 flush() 또는 commit() 발생
    • 이때 영속성 컨텍스트에 쌓인 변경사항이 DB로 반영된다 (INSERT, UPDATE 등 SQL 날아감).
    • 즉, commit 시점에 DB에 저장됨
  3. 그 전까지는 1차 캐시에만 존재
    • 트랜잭션 중 find()를 하면 DB가 아니라 영속성 컨텍스트(1차 캐시)에서 먼저 찾는다.
    • 이를 통해 성능 최적화도 이뤄지고, 같은 엔티티는 항상 동일성을 유지할 수 있음

4. 테이블, 컬럼명 생성 전략

스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다름

하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용(SpringPhysicalNamingStrategy)

스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))

1. 카멜 케이스 ➡️ 언더스코어(memberPoint member_point)
2. .(점) ➡️ _(언더스코어)
3. 대문자 ➡️ 소문자
728x90

'BE > SpringBoot' 카테고리의 다른 글

[JPA] 4. 회원 도메인 개발  (0) 2025.05.13
[JPA] 3. 애플리케이션 구현 준비  (0) 2025.05.12
[JPA] @Embedded @Embedable 임베디드 타입이란?  (0) 2025.05.07
[JPA] 2. 도메인 분석 설계 - 요구사항 분석 / 도메인 모델과 테이블 설계 / 엔티티 클래스 개발  (1) 2025.05.04
[JPA] 1. 프로젝트 환경설정  (0) 2025.05.04
'BE/SpringBoot' 카테고리의 다른 글
  • [JPA] 4. 회원 도메인 개발
  • [JPA] 3. 애플리케이션 구현 준비
  • [JPA] @Embedded @Embedable 임베디드 타입이란?
  • [JPA] 2. 도메인 분석 설계 - 요구사항 분석 / 도메인 모델과 테이블 설계 / 엔티티 클래스 개발
DROPDEW
DROPDEW
💻 Developer | 기록하지 않으면 존재하지 않는다
  • DROPDEW
    제 2장 1막
    DROPDEW
  • 전체
    오늘
    어제
    • Dev (444)
      • App·Android (1)
      • AI (10)
      • BE (50)
        • HTTP 웹 기본 지식 (8)
        • SpringBoot (23)
        • 스프링부트와 JPA 활용 (0)
        • JAVA (1)
        • PHP (11)
      • FE·Client (23)
        • HTML (1)
        • React (19)
        • Unity (1)
      • Data (28)
        • Bigdata (6)
        • Database (1)
        • Python (0)
        • 빅데이터분석기사 (13)
      • Infra (1)
      • Activity (7)
        • Intern (0)
        • SK AI Dream Camp (0)
        • 구름톤 유니브 4기 (1)
        • 리모트 인턴십 6기 (3)
        • 봉사활동 (0)
        • 부스트캠프 AI Tech 8기 (3)
      • CS (8)
      • 취준 (12)
        • 자격증 (4)
        • 인적성·NCS (6)
        • 코테·필기·면접 후기 (2)
      • 코테 (270)
        • Algorithm (222)
        • SQL (35)
        • 정리 (13)
      • 인사이트 (27)
        • 금융경제뉴스 (7)
        • 금융용어·지식 (2)
        • 북마크 (7)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    너비우선탐색
    투포인터
    이분탐색
    구현
    백준
    누적합
    매개변수탐색
    오블완
    그래프탐색
    티스토리챌린지
    다이나믹프로그래밍
    정렬
    시뮬레이션
    그래프이론
    문자열
    브루트포스 알고리즘
    최단경로
    수학
    그리디알고리즘
    자료구조
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
DROPDEW
[JPA] 2. 도메인 분석 설계 - 엔티티 설계시 주의점
상단으로

티스토리툴바