JPA는 연관된 엔티티를 조회할 때, 실제 엔티티 객체 대신 프록시(Proxy) 객체를 사용하여 성능을 최적화합니다.
프록시 객체:
getName())가 최초로 호출되는 시점에, JPA는 비로소 데이터베이스에 SELECT 쿼리를 보내 실제 데이터를 조회하고 프록시 객체를 초기화합니다.지연 로딩 (Lazy Loading):
@ManyToOne, @OneToMany 등의 연관관계 어노테이션에서 fetch 속성을 FetchType.LAZY로 설정합니다. (Spring Boot에서는 @ManyToOne은 LAZY가, @OneToMany는 LAZY가 기본값)즉시 로딩 (Eager Loading):
fetch = FetchType.EAGER)N+1 문제: JPA를 사용할 때 발생하는 가장 대표적인 성능 문제. 연관관계가 설정된 엔티티를 조회할 때, 한 번의 쿼리로 N개의 데이터를 가져왔는데, 이 N개의 데이터 각각에 대해 연관된 데이터를 조회하기 위해 N번의 추가 쿼리가 발생하는 현상입니다. (총 1 + N번의 쿼리 실행)
발생 시나리오:
postRepository.findAll()을 호출하여 모든 게시글(Post)을 조회합니다. (쿼리 1번)post.getUser().getName()을 호출합니다.Post와 User가 지연 로딩(LAZY)으로 설정되어 있다면, 이 시점에 각 Post마다 User를 조회하기 위한 SELECT 쿼리가 N번 추가로 실행됩니다.페치 조인 (Fetch Join):
JOIN FETCH 키워드를 사용하여, 조회하는 주 엔티티와 연관된 엔티티를 처음부터 함께 조회하도록 명시하는 방법입니다.JOIN FETCH를 보고, 외부 조인(OUTER JOIN)을 사용하여 단 한 번의 SQL 쿼리로 모든 연관 데이터를 가져옵니다.@Query 어노테이션과 함께 사용됩니다.@Query("SELECT p FROM Post p JOIN FETCH p.user")
List<Post> findAllWithUser();@EntityGraph:
attributePaths 속성에 함께 조회할 연관 필드의 이름을 지정하면, JPA가 페치 조인과 유사한 쿼리를 생성해줍니다.@EntityGraph(attributePaths = {"user"})
List<Post> findAll();배치 사이즈 (Batch Size):
@BatchSize 어노테이션이나 default_batch_fetch_size 설정을 통해, 지연 로딩 시 한 번에 조회할 엔티티의 개수를 지정합니다.WHERE id IN (?, ?, ...) 과 같이 100개씩 묶어서 조회하는 쿼리가 실행되어 전체 쿼리 수를 크게 줄여줍니다.문제점: 여러 사용자가 동시에 동일한 데이터를 수정하려고 할 때, 데이터의 일관성이 깨지는 문제가 발생할 수 있습니다. (e.g., 동시에 상품 재고를 감소시키는 경우)
동시성 제어: 이러한 문제를 해결하여 데이터의 무결성을 보장하는 메커니즘입니다.
비관적 락 (Pessimistic Lock):
@Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션을 사용합니다.낙관적 락 (Optimistic Lock):
@Version 어노테이션이 붙은 버전 관리용 필드(e.g., long version)를 추가합니다.version=1)을 함께 가져옵니다.version=1)과 현재 DB의 버전이 동일한가?"를 확인합니다.version=2) 업데이트합니다. 만약 그 사이에 다른 트랜잭션 B가 먼저 수정하여 버전이 변경되었다면(version=2), 예외(OptimisticLockException)를 발생시켜 업데이트를 실패시킵니다.@Version 어노테이션을 필드에 추가하기만 하면 됩니다.@EntityGraph를 사용하여, 단 한 번의 쿼리로 연관된 데이터를 함께 조회함으로써 해결하는 것이 가장 효과적입니다.@Version)을 사용하는 것이 일반적인 전략입니다.