아래는 "JPA 성능 최적화"에 대해 설명하는 글입니다. 실무에서 자주 발생하는 문제와 해결 방법을 중심으로, 예시와 함께 구체적으로 작성했습니다. --- ### JPA 성능 최적화 JPA는 객체와 관계형 데이터베이스를 매핑하며 개발 생산성을 높여주지만, 잘못 사용하면 성능 저하를 초래할 수 있습니다. 성능 최적화는 불필요한 데이터베이스 호출을 줄이고, 쿼리 실행을 효율화하며, 메모리 사용을 최적화하는 데 초점을 맞춥니다. 아래는 JPA 성능 최적화의 주요 기법과 실무 적용 방법을 다룹니다. #### 1. N+1 문제와 해결 방법 **N+1 문제**는 연관 관계를 조회할 때 발생하는 대표적인 성능 문제입니다. 예를 들어, `Team`과 `Member`의 1:N 관계에서 모든 팀을 조회한 뒤 각 팀의 회원을 개별적으로 조회하면, 초기 쿼리 1번(N개의 `Team` 조회) + N번(`Member` 조회)이 발생합니다. - **해결 방법 1: 페치 조인(Fetch Join)** 연관된 엔티티를 한 번의 쿼리로 함께 조회합니다. ```java @Entity public class Team { @Id private Long id; private String name; @OneToMany(mappedBy = "team") private List members = new ArrayList<>(); // getter } @Entity public class Member { @Id private Long id; private String name; @ManyToOne @JoinColumn(name = "team_id") private Team team; // getter } ``` ```java // JPQL로 페치 조인 String jpql = "SELECT t FROM Team t JOIN FETCH t.members"; List teams = entityManager.createQuery(jpql, Team.class).getResultList(); ``` - 결과: 단일 쿼리로 `Team`과 `Member`를 함께 조회(`SELECT t.*, m.* FROM team t INNER JOIN member m ON t.id = m.team_id`). - **해결 방법 2: 배치 크기 설정 (@BatchSize)** 지연 로딩(Lazy Loading) 시 한 번에 여러 엔티티를 조회합니다. ```java @OneToMany(mappedBy = "team") @BatchSize(size = 100) private List members; ``` - `size=100`으로 설정하면 100개의 `Team`에 대한 `Member`를 한 번의 IN 쿼리로 조회합니다. #### 2. 지연 로딩(Lazy Loading) 활용 기본적으로 연관 관계는 지연 로딩으로 설정해 불필요한 데이터 조회를 방지합니다. - `@ManyToOne`, `@OneToOne`: 기본값은 즉시 로딩(EAGER)이므로 `fetch = FetchType.LAZY`로 변경. - `@OneToMany`, `@ManyToMany`: 기본값이 지연 로딩(LAZY)이므로 유지. ```java @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; ``` - **주의**: 지연 로딩 사용 시 프록시 객체가 반환되며, 트랜잭션 밖에서 접근하면 `LazyInitializationException`이 발생할 수 있음. #### 3. 페치 조인과 일반 조인의 적절한 사용 - **페치 조인**: 연관 데이터를 즉시 로드할 때 사용. 데이터 중복이 발생할 수 있으니 페이징(`setFirstResult`, `setMaxResults`)과 함께 사용 금지. - **일반 조인**: 필터링이나 조건에만 사용하며, 엔티티 로드는 지연 로딩에 의존. #### 4. 읽기 전용 쿼리 설정 수정할 필요 없는 조회 쿼리는 영속성 컨텍스트의 관리 부담을 줄이기 위해 읽기 전용으로 설정합니다. - **JPQL**: ```java entityManager.createQuery("SELECT m FROM Member m", Member.class) .setHint("org.hibernate.readOnly", true) .getResultList(); ``` - **효과**: 변경 감지(Dirty Checking)와 스냅샷 생성이 생략되어 메모리와 성능 개선. #### 5. 배치 처리와 대량 데이터 관리 대량 데이터를 삽입/수정/삭제할 때는 JPA의 기본 동작(영속성 컨텍스트 사용)이 비효율적일 수 있습니다. - **방법**: JDBC 배치나 벌크 연산 사용. ```java // 벌크 업데이트 String jpql = "UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId"; entityManager.createQuery(jpql) .setParameter("teamId", teamId) .executeUpdate(); entityManager.clear(); // 영속성 컨텍스트 초기화 필수 ``` - **주의**: 벌크 연산은 영속성 컨텍스트를 무시하므로, 이후 작업 전에 `clear()`나 `refresh()`로 동기화 필요. #### 6. 캐시 활용 JPA는 1차 캐시와 2차 캐시를 제공하며, 이를 활용해 성능을 높일 수 있습니다. - **1차 캐시**: 트랜잭션 내에서 동일 엔티티를 재조회할 때 데이터베이스 접근을 줄임(기본 제공). - **2차 캐시**: 애플리케이션 범위에서 캐시를 공유. Hibernate 사용 시 `@Cacheable`과 캐시 제공자(Ehcache, Redis 등)를 설정. ```java @Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Product { @Id private Long id; private String name; // getter } ``` - **적용 시점**: 자주 조회되고 변경이 드문 데이터(예: 카테고리 목록). #### 7. 쿼리 최적화 - **필요한 필드만 조회**: 엔티티 전체 대신 DTO로 필요한 데이터만 조회. ```java public class MemberDto { private Long id; private String name; public MemberDto(Long id, String name) { this.id = id; this.name = name; } // getter } List result = entityManager.createQuery( "SELECT new com.example.MemberDto(m.id, m.name) FROM Member m", MemberDto.class) .getResultList(); ``` - **인덱스 활용**: 자주 검색되는 컬럼에 인덱스 추가(데이터베이스 레벨 또는 `@Index`). #### 8. 트랜잭션 범위 최소화 영속성 컨텍스트는 트랜잭션 범위에서 동작하므로, 불필요하게 긴 트랜잭션은 메모리 사용과 성능 저하를 유발합니다. - **방법**: 비즈니스 로직을 간결히 하고, 읽기 전용 작업은 트랜잭션 없이 처리(`@Transactional(readOnly = true)`). #### 실무 예시 ```java @Repository public class MemberRepository { @PersistenceContext private EntityManager em; @Transactional(readOnly = true) public List findMembersWithTeam() { return em.createQuery("SELECT m FROM Member m JOIN FETCH m.team", Member.class) .setHint("org.hibernate.readOnly", true) .getResultList(); } @Transactional public void bulkUpdateAge(Long teamId) { em.createQuery("UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId") .setParameter("teamId", teamId) .executeUpdate(); em.clear(); } } ``` #### 주의사항 - **N+1 디버깅**: 쿼리 로그를 활성화해(`spring.jpa.show-sql=true`) 발생 여부 확인. - **캐시 동기화**: 2차 캐시 사용 시 데이터 정합성을 유지하기 위해 적절한 전략 선택. - **테스트**: 성능 개선 후 부작용(예: 데이터 불일치)을 검증. --- JPA 성능 최적화는 애플리케이션 규모와 요구사항에 따라 달라지며, 문제 원인을 분석하고 적절한 기법을 적용하는 것이 중요합니다. 다음 장에서는 트랜잭션 관리와 실무 활용을 다뤄보겠습니다. --- 책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!