본문 바로가기
JPA

JPA Entity 설계시 베스트 프랙티스

by 이상한나라의개발자 2024. 2. 22.

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

  • 불변성 유지 : 엔티티의 상태를 변경할 수 있는 setter 메소드가 많으면 엔티티의 상태가 예측하기 어려워지고, 불변성을 유지하기 어려워집지다. 불변성이 유지되면, 버그 발생 가능성이 줄어들고 코드를 이해하기 쉬워집니다.
  • 엔티티의 일관성 보장 : 생성 시점에 필요한 모든 값을 생성자를 통해 전달 받으면, 객체가 일관된 상태로 시작할 수 있습니다. 이는 엔티티가 항상 유효한 상태로 유지되도록 돕습니다.
  • 변경 추적 용이 : JPA 변경 감지 기능 (Dirty Checking)은 트랜잭션 내에서 엔티티의 상태 변화를 감지하여 변경 사항을 데이터베이스에 자동으로 반영합니다. setter 대신 명확한 의도를 나타내는 메소드 (예:changeUsername, increasePrice)를 사용하면, 이러한 변경 사항이 코드 상에서 명확하게 드러나고 추적하기 쉬워집니다.

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

  • 성능 최적화 : 즉시로딩(EAGER)은 연관된 엔티티를 항상 함께 로드하기 때문에, 필요하지 않은 데이터 까지 불러오게 되어 성능 문제를 야기할 수 있습니다. 반면 지연로딩은 실제로 엔티티의 관계가 접근될 때까지 로드를 지연시키므로, 필요할 때만 데이터를 로드하여 성능을 최적화 할 수 있습니다.
  • 리소스 사용 최소화 : 지연로딩은 어플리케이션의 메모리 사용량을 최소화 하는데 도움이 됩니다. 즉, 어플리케이션이 필요로 하는 시점에만 데이터를 로드하여 리소스를 효율적으로 사용할 수 있습니다.

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

  • NullPointException 방지 : 컬렉션 필드를 클래스 내에서 바로 초기화 하면, 해당 컬렉션을 사용하는 코드가 실행될 때 NullPointException의 위험이 없습니다. 이는 컬렉션이 항상 비어 있거나, 실제 값이 있을 수 있지만, null이 아니라는 것을 보장합니다.
  • 코드의 간결성 : 필드에서 컬렉션을 초기화하면 생성자나 다른 메소드에서 별도의 초기화 코드를 작성할 필요가 없어져 코드가 간결해 집니다.
  • 안전한 컬렉션 조작 : 컬렉션이 미리 초기화 되어 있으면, 엔티티의 상태를 변경하는 로직에서 해당 컬렉션을 안전하게 조작할 수 있습니다. 예를 들어, 컬렉션에 요소를 추가하거나 제가하는 등의 작업을 할 때, 컬렉션의 null 을 피할 수 있습니다.

4. 엔티티 설계시 생성자는 protected로 하자

  • JPA 스펙에 따라 엔티티 클래스는 비어 있는 기본 생성자를 가질 수 있어야 합니다. 이 생성자를 protected로 설정함으로써 불필요한 인스턴스화를 방지하고, 클래스의 인스턴스 생성을 내부 또는 상속 받은 클래스로 제한하여 객체의 일관성을 유지할 수 있습니다.

5. N+1 문제 인식 및 해결

JPA를 사용하면 발생할 수 있는 일반적인 성능 문제 중 하나는 N+1 쿼리 문제 입니다. 이 문제는 엔티티를 로드할 때 연관된 엔티티를 로딩하기 위해 추가적인 쿼리가 발생하는 현상입니다. 이를 방지하기 위해 fetch join, 엔티티 그래프, 배치 사이즈 등을 적절히 사용해야 합니다.

 

N+1 방지 방법 예시

아래는 N+1 문제에 대한 해결 방법을 설명합니다. 한명의 회원은 한 팀에 속할 수 있습니다.

@Entity
@Getter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
@Getter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

}

 

 

1. Fetch Join 사용

String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();

 

위와 같이 fetch join을 사용하게 되면 쿼리가 실행될 때 연관된 team을 함께 로드 하기 때문에 member에서 추가적으로 team 로드를 위한 추가적인 쿼리가 실행되지 않습니다.

예를들어, members로 값을 리턴 받고 members.getTeam() 을 하게 되면 이미 로드 되어 있기 때문에 추가적으로 쿼리가 발생하지 않습니다.

 

2. 엔티티 그래프

EntityGraph<Member> graph = em.createEntityGraph(Member.class);
graph.addAttributeNodes("team");

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.loadgraph", graph);

List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
                          .setHint("javax.persistence.loadgraph", graph)
                          .getResultList();

 

엔티티 그래프 기능을 사용하여 특정 엔티티를 조회할 때 어떤 연관 엔티티를 함께 로드할지 명시적으로 지정합니다.

이 방법은 member 조회시 team 엔티티를 명시적으로 함께 로드하도록 합니다.

 

3. Batch Size 설정

@Entity
@Getter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    @BatchSize(size = 10)
    private Team team;
}

-- application.yml 에서의 설정 
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        default_batch_fetch_size: 1000
        format_sql: true

 

이 설정은 team 엔티티를 로드할때 ( member.getTeam()) 한번에 최대 10개 까지 함께 로드할 수 있게 됩니다. 이는 여러 member의 team을 로드할 때 N+1 문제를 완화하는데 도움을 줍니다.

 

배치 사이즈를 적용하게 되면 아래와 같은 결과를 얻을 수 있습니다.

-- 예를 들어 10명의 member을 저장한다고 가정 했을 경우 
-- 미적용
INSERT INTO member (name, team_id) VALUES ('Member1', 1);
INSERT INTO member (name, team_id) VALUES ('Member2', 1);
...
INSERT INTO member (name, team_id) VALUES ('Member10', 1);


-- 적용
INSERT INTO member (name, team_id) VALUES ('Member1', 1), ('Member2', 1), ..., ('Member10', 1);

 

조회 시 배치 사이즈를 적용하는 경우와 적용하지 않는 경우의 차이를 설명하기 위해, member 엔티티가 team 엔티티에 대한 지연로딩 관계를 가지고 있어야 합니다. 배치 사이즈는 지연 로딩된 연관 엔티티를 얼마나 많은 수를 한번에 로드할지 결정합니다.

-- 10명의 member을 조회한다고 가정 했을 경우
-- 미적용
SELECT * FROM Team WHERE id = ?; -- 첫 번째 Member의 Team 정보 조회
SELECT * FROM Team WHERE id = ?; -- 두 번째 Member의 Team 정보 조회
...
SELECT * FROM Team WHERE id = ?; -- 열 번째 Member의 Team 정보 조회

-- 적용
SELECT * FROM Team WHERE id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); -- 10개 Member의 Team 정보 한 번에 조회

'JPA' 카테고리의 다른 글

Spring Data JPA  (0) 2024.03.08
JPA 조회 전략  (0) 2024.03.07
JPA OSIV  (0) 2024.01.31
JPA Auditing  (0) 2024.01.30
JPA  (0) 2023.12.03