[JPA] JPA와 Hibernate 동작 원리

슈퍼대디·2024년 12월 24일

CS면접대비

목록 보기
12/13

JPA와 Hibernate 동작 원리

1. JPA와 Hibernate의 관계

JPA (Java Persistence API)

  • 자바 ORM 기술의 표준 명세
  • 인터페이스의 모음으로, 실제 구현체가 필요
  • 자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스

Hibernate

  • JPA의 구현체 중 가장 널리 사용됨
  • JPA 인터페이스를 구현하고 추가적인 기능 제공:
    • 더욱 강력한 쿼리 기능 (HQL)
    • 2차 캐시 기능
    • 다양한 매핑 지원

관계 예시

// JPA 표준 인터페이스 사용
@PersistenceContext
private EntityManager entityManager;  // 실제론 Hibernate의 구현체가 주입됨

// Hibernate 직접 사용
Session session = entityManager.unwrap(Session.class);

2. 영속성 컨텍스트의 특징

영속성 컨텍스트의 정의와 생명주기

  • 엔티티를 영구 저장하는 환경
  • EntityManager를 통해 접근
  • 트랜잭션 범위와 생명주기를 같이함

주요 특징과 이점

  1. 1차 캐시

// 최초 조회: DB에서 조회
User user1 = entityManager.find(User.class, 1L);  // SELECT 쿼리 실행

// 동일 트랜잭션 내 재조회: 캐시에서 조회
User user2 = entityManager.find(User.class, 1L);  // 쿼리 실행 없음

// 1차 캐시 내용 확인
boolean isCached = entityManager.contains(user1);  // true
  1. 쓰기 지연 (Write-Behind)
EntityTransaction tx = entityManager.getTransaction();
tx.begin();

// 쓰기 지연 저장소에 SQL 저장
entityManager.persist(new User("user1"));  // INSERT SQL 생성
entityManager.persist(new User("user2"));  // INSERT SQL 생성

// 실제 DB에 SQL 실행
tx.commit();  // 한 번에 모아서 실행
  1. 변경 감지 (Dirty Checking)
tx.begin();

// 영속 엔티티 조회
User user = entityManager.find(User.class, 1L);

// 엔티티 데이터만 수정
user.setName("newName");

// 별도의 persist() 호출 필요 없음
tx.commit();  // 변경 감지가 자동으로 UPDATE SQL 생성 및 실행

3. 엔티티 생명주기

엔티티의 4가지 상태

  1. 비영속 (new/transient)
User user = new User();
user.setName("user1");  // 영속성 컨텍스트와 관련 없음
  1. 영속 (managed)
entityManager.persist(user);  // 영속성 컨텍스트에서 관리됨
  1. 준영속 (detached)
entityManager.detach(user);  // 영속성 컨텍스트에서 분리
  1. 삭제 (removed)
entityManager.remove(user);  // 삭제 상태

엔티티 매핑

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();
}

4. 지연 로딩과 즉시 로딩

지연 로딩 (LAZY)

  • 연관된 엔티티를 실제 사용하는 시점에 로딩
@Entity
public class User {
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;  // orders 접근 시점에 SQL 실행
}

User user = entityManager.find(User.class, 1L);  // SELECT user만 실행
user.getOrders();  // 이 시점에 SELECT orders 실행

즉시 로딩 (EAGER)

  • 엔티티를 조회할 때 연관된 엔티티도 함께 조회
@Entity
public class User {
    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders;  // user 조회 시 orders도 함께 조회
}

User user = entityManager.find(User.class, 1L);  // SELECT user + JOIN orders

N+1 문제

  • 발생 원인:
    • JPQL을 사용할 때 즉시 로딩이나 지연 로딩에서 발생
    • 연관된 엔티티를 조회할 때 추가 쿼리가 실행되는 현상
// N+1 문제 발생 예시
List<User> users = em.createQuery("select u from User u", User.class)
    .getResultList();  // SELECT * FROM user

// users 순회하면서 orders 접근 시 각각 추가 쿼리 발생
for (User user : users) {
    user.getOrders().size();  // SELECT * FROM orders WHERE user_id = ?
}

N+1 문제 해결 방법

  1. 패치 조인 (Fetch Join)
// JOIN FETCH를 사용하여 한 번에 데이터 조회
List<User> users = em.createQuery(
    "SELECT u FROM User u JOIN FETCH u.orders", User.class)
    .getResultList();
  1. EntityGraph 사용
@EntityGraph(attributePaths = {"orders"})
@Query("SELECT u FROM User u")
List<User> findAllWithOrders();
  1. BatchSize 설정
@Entity
public class User {
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}
  1. QueryPlan 최적화
// 필요한 데이터만 조회하여 DTO로 변환
List<UserDTO> users = em.createQuery(
    "SELECT new com.example.UserDTO(u.id, u.name) FROM User u", 
    UserDTO.class)
    .getResultList();

5. 캐시와 성능 최적화

1차 캐시

  • 영속성 컨텍스트 내부의 캐시
  • 트랜잭션 범위의 캐시
User user1 = entityManager.find(User.class, 1L);  // DB 조회
User user2 = entityManager.find(User.class, 1L);  // 1차 캐시에서 조회

2차 캐시

  • 애플리케이션 범위의 캐시
  • 다른 영속성 컨텍스트간 공유
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class User {
    // 엔티티 정의
}

성능 최적화 전략

  1. 벌크 연산
// 단건 처리
for (User user : users) {
    user.setActive(true);
    entityManager.persist(user);
}

// 벌크 연산
entityManager.createQuery(
    "update User u set u.active = true where u.lastLoginDate < :date")
    .setParameter("date", date)
    .executeUpdate();
  1. 읽기 전용 쿼리
// 읽기 전용 세션으로 조회
Session session = entityManager.unwrap(Session.class);
session.setDefaultReadOnly(true);
List<User> users = session.createQuery("from User", User.class).list();

6. 면접 예상 질문

Q: JPA와 MyBatis의 차이점을 설명하고, 어떤 상황에서 JPA를 선택해야 하는지 설명해주세요.
A: JPA와 MyBatis는 다음과 같은 차이점이 있습니다:

  1. 패러다임

    • JPA: 객체 중심의 개발이 가능하며, 객체와 관계형 데이터베이스 간의 패러다임 불일치 해결
    • MyBatis: SQL 중심의 개발로, 세밀한 쿼리 튜닝이 가능
  2. 생산성

    • JPA: 반복적인 CRUD SQL을 자동으로 생성하여 생산성이 높음
    • MyBatis: SQL을 직접 작성해야 하므로 상대적으로 생산성이 낮음

JPA 선택이 적절한 상황:

  • 도메인 주도 설계(DDD)를 적용하는 프로젝트
  • 반복적인 CRUD 작업이 많은 경우
  • 복잡한 객체 관계를 다루는 경우

Q: 영속성 컨텍스트의 이점을 실무에서의 예시와 함께 설명해주세요.
A: 영속성 컨텍스트는 다음과 같은 이점을 제공합니다:

  1. 1차 캐시
// 동일 트랜잭션에서 같은 엔티티를 여러 번 조회할 때 성능 향상
User user1 = userRepository.findById(1L);  // DB 조회
User user2 = userRepository.findById(1L);  // 캐시에서 조회
  1. 변경 감지 (Dirty Checking)
@Transactional
public void updateUser(Long id, String newName) {
    User user = userRepository.findById(id);
    user.setName(newName);  // 명시적인 save() 호출 불필요
}  // 트랜잭션 종료 시 자동으로 UPDATE SQL 실행
  1. 지연 로딩을 통한 성능 최적화
@Transactional(readOnly = true)
public String getUserName(Long id) {
    User user = userRepository.findById(id);
    return user.getName();  // orders는 로딩되지 않음
}

Q: N+1 문제가 발생하는 상황과 해결 방법을 실제 예시와 함께 설명해주세요.
A: N+1 문제는 연관 관계에서 발생하는 성능 문제입니다.

발생 상황:

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.EAGER)
    private User user;
}

// N+1 문제 발생
List<Order> orders = orderRepository.findAll();  // SELECT * FROM orders
// 각 order마다 user를 조회하는 추가 쿼리 발생
// SELECT * FROM users WHERE id = ?  (N번 실행)

해결 방법:
1. 페치 조인

@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
  1. EntityGraph
@EntityGraph(attributePaths = {"user"})
List<Order> findAll();
  1. BatchSize 설정
# application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=100

Q: JPA의 영속성 전이(Cascade)와 고아 객체 제거에 대해 설명하고, 주의할 점을 말씀해주세요.
A: 영속성 전이와 고아 객체 제거는 엔티티의 라이프사이클을 관리하는 방법입니다.

  1. 영속성 전이 (Cascade)
@Entity
public class User {
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders;  // User 저장 시 연관된 Order도 함께 저장
}
  1. 고아 객체 제거 (orphanRemoval)
@Entity
public class User {
    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private List<Order> orders;  // 연관관계가 끊어진 Order 자동 삭제
}

주의사항:

  • Cascade는 단일 소유자를 가진 private한 연관관계에만 사용
  • orphanRemoval은 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하므로 신중하게 사용

Q: JPA에서 성능 최적화를 위해 사용할 수 있는 방법들을 설명해주세요.
A: JPA에서는 다음과 같은 성능 최적화 방법들을 제공합니다:

  1. 읽기 전용 쿼리 최적화
@Transactional(readOnly = true)
@Query("SELECT u FROM User u")
List<User> findAllUsers();  // 스냅샷 생성 및 변경 감지 비활성화
  1. 벌크 연산
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.lastLoginDate < :date")
int updateUserStatus(@Param("status") String status, @Param("date") LocalDateTime date);
  1. DTO 직접 조회
@Query("SELECT new com.example.UserDTO(u.id, u.name) FROM User u")
List<UserDTO> findAllUserDtos();  // 필요한 컬럼만 조회
  1. 페이징 처리
Pageable pageable = PageRequest.of(0, 10);
Page<User> users = userRepository.findAll(pageable);

이러한 최적화는 상황에 따라 적절히 선택하여 사용해야 하며, 실제 성능 측정을 통해 효과를 확인해야 합니다.

참고 자료

profile
성장하고싶은 Backend 개발자

0개의 댓글