kimyu0218
  • [spring] JPA/Hibernate, Spring Data JPA, 영속성 컨텍스트
    2024년 12월 16일 15시 00분 51초에 업로드 된 글입니다.
    작성자: @kimyu0218

    JPA/Hibernate

    JPA는 Java Persistence API로, 자바 어플리케이션이 데이터를 영속할 수 있도록 API를 제공한다. JPA는 ORM 표준 스펙이므로 실제로 동작하기 위해서는 구현체가 필요한데 Hibernate가 대표적인 JPA 구현 프레임워크다.
     
    모든 자바 어플리케이션은 데이터베이스에 데이터를 저장하기 위해 JDBC API를 사용한다. JDBC는 Java Database Connectivity의 약어로, 자바의 데이터 접근 표준이다. Hibernate 같은 프레임워크를 이용하면 내부에서 JDBC를 사용하여 데이터를 처리한다.

    # JPA/Hibernate 관련 설정
    spring:
      jpa:
        show-sql: false # (default=false) JPA/Hibernate가 생성한 쿼리를 로그로 남기지 않는다
        hibernate:
          database-platform: org.hibernate.dialect.MySQL8Dialect # Hibernate는 MySQL8 데이터베이스를 사용한다 (대부분 데이터베이스 드라이버를 참고하여 자동으로 dialect를 선택한다)
          ddl-auto: none # (default=none) Hibernate가 @Entity 어노테이션이 붙은 엔티티를 스캔하여 DDL 쿼리를 만들고 실행할 수 없다
    
    💡 Hibernate의 데이터베이스 초기화 설정
    • `none` : DDL을 실행하지 않는다 (default)
    • `validate` : 엔티티 클래스와 데이터베이스 스키마를 서로 비교하고, 다른 점이 있다면 예외를 발생시키고 어플리케이션을 종료한다
    • `update` : 엔티티 클래스와 데이터베이스를 비교하고, 다른 경우 엔티티 변경 사항을 데이터베이스에 업데이트 한다
    • `create` : 어플리케이션이 시작하면 기존에 생성된 테이블을 drop하고 새롭게 생성한다
    • `create-drop` : `create`와 비슷하나 어플리케이션 종료 시 생성한 테이블을 drop한다

     

    Spring Data JPA

    Spring Data는 영속성 계층을 보다 쉽게 구현할 수 있도록 도와주는 스프링 생태계 프로젝트다. 다양한 데이터베이스에 일관된 방법으로 데이터를 처리할 수 있는 기능을 제공한다. 그중 Spring Data JPA는 JPA를 사용하는 프로젝트로, JPA를 더 쉽게 사용할 수 있도록 돕는다.

    • CRUD 기본 기능 : 엔티티를 처리할 수 있는 몇 가지 CRUD 기본 메서드를 제공하여 매번 반복해서 기본 메서드를 구현하지 않아도 된다.
    • 쿼리 생성 기능 : Repository 인터페이스에 선언된 메서드 이름으로 쿼리를 자동으로 생성한다.
    • 감사(audit) 기능 : 데이터를 생성하거나 수정할 때 누가 언제 했는지 추적할 수 있도록 애너테이션을 제공한다.
    • 페이징/정렬 기능

    Spring Data JPA는 내부적으로 JPA 구현체를 사용한다. `spring-boot-starter-data-jpa`를 의존성으로 추가하면 `spring-boot-starter-jdbc`, `hibernate-core`, `spring-data-jpa` 등 필요한 라이브러리들을 추가하고 자동으로 구성한다. 데이터베이스의 데이터를 처리하기 위해서는 다음 컴포넌트들이 필요하다.

    • `DataSource`: `Connection` 객체를 제공하는 인터페이스 (구현체로는 HikariCP, Tomcat pool, DBCP2 등이 있다)
    • `EntityManager` : 엔티티 클래스를 영속할 수 있는 메서드를 제공하는 클래스
    • `TransactionManager` : 트랜잭션을 처리하는 인터페이스
    # Hikari DataSource 설정
    spring:
      datasource:
        hikari:
          jdbc-url: jdpc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE} # DataSource가 Connection 객체를 생성할 때 사용할 URL
          driver-class-name: com.mysql.cj.jdbc.Driver # DataSource가 Connection 객체를 생성할 때 사용할 드라이버 클래스
          username: ${DB_USERNAME}
          password: ${DB_PASSWORD}
    💡 데이터 소스는 DBMS에 대한 커넥션을 관리하는 객체로, 커넥션을 효율적으로 요청하여 작업 성능을 높인다. 데이터 소스는 JDBC 드라이버를 사용하여 DBMS에 연결하는 데, JDBC 드라이버는 특정 관계형 데이터베이스에 연결할 수 있는 구현을 제공한다.
    🚨 스프링 부트는 별도로 데이터 소스를 지정하지 않으면 HikariCP를 사용한다. (구성보다 관례)


    지금부터 Spring Data JPA를 이용해서 레포지토리를 만드는 방법을 알아보자. 먼저, 특정 엔티티 클래스를 위한 레포지토리 인터페이스를 정의한다. (클래스가 아닌 인터페이스를 정의하는 이유는 쿼리 메서드 기능을 통해 쿼리를 자동으로 생성하기 때문이다!)

    public interface MemberRepository extends JpaRepository<Member, Long> {
        ...
    }

    레포지토리 인터페이스는 `JpaRepository` 인터페이스를 상속 받는다. Spring Data JPA는 `JpaRepository`를 구현한 `SimpleJpaRepository` 구현체를 제공하는데, 이 덕분에 CRUD 기본 메서드를 정의하지 않아도 된다. (`SimpleJpaRepository`는 Hibernate에서 제공하는 클래스와 메서드를 이용하여 CRUD 메서드를 제공한다!)

    💡 스프링 데이터는 `Repository`, `CrudRepository`, `PagingAndSortingRepository` 인터페이스를 제공한다. 하지만 특정 기술을 사용하는 경우, 해당 기술에 특화된 연산을 제공하는 인터페이스를 사용한다. (ex. `JpaRepository`, `MongoRepository`)
    🔗 [spring-data-jpa] SimpleJpaRepository.java
    🔗 [spring-data-common] CrudRepository.java
    @Repository
    @Transactional(readOnly = true)
    public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
        ...
    }
    @NoRepositoryBean
    public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
        ...
        /**
        * CrudRepository<T, ID>
        * (ListCrudRepository<T, ID> extends CrudRepository<T, ID>)
        * <S extends T> S save(S entity) : 인자로 전달한 엔티티 객체 저장
        * <S extends T> Iterable<S> saveAll(Iterable<S> entities) : 인자로 전달한 엔티티 객체들 모두 저장
        * Optional<T> findById(ID id) : 기본 키와 일치하는 엔티티 객체 조회
        * boolean existsById(ID id) : 기본 키와 일치하는 엔티티 객체 존재 여부 조ㅈ회
        * Iterable<T> findAllById(Iterable<ID> ids) : 기본 키들과 일치하는 엔티티 객체 모두 조회
        * void deleteById(ID id) : 기본 키와 일치하는 엔티티 객체 삭제
        * void delete(T entity) : 인자로 전달한 엔티티 객체를 데이터베이스에서 삭제
        * void deleteAllById(Iterable<? extends ID> ids) : 기본 키들과 일치하는 엔티티 객체 모두 삭제
        * void deleteAll(Iterable<? extends T> entities) : 인자로 전달한 엔티티 객체들 모두 삭제
        * void deleteAll() : 모든 엔티티 객체 삭제
        */
    }

     
    만약 `JpaRepository`에서 제공하지 않는 기능을 사용해야 한다면 개발자가 직접 메서드를 정의해야 한다. 사용자 정의 쿼리는 1) 메서드 이름으로 자동으로 생성하거나 (= 쿼리 메서드)  2) `@Query` 어노테이션으로 직접 작성할 수 있다.

    쿼리 메서드 작성 방법
    🔗 [docs.spring.io] Defining Query Methods
    🔗 [docs.spring.io] JPA Query Methods
    🚨 쿼리 메서드가 자동으로 쿼리를 생성해주기 때문에 더 좋은 방법 같지만 이는 단점도 가진다. 복잡한 쿼리가 필요한 경우 메서드 이름이 길어져 가독성이 떨어지며, 메서드를 쿼리로 변환하기 때문에 앱 초기화가 느려져 성능에 영향을 미친다.


    `@Query` 어노테이션을 이용하여 쿼리를 작성해보자. 다음은 어노테이션 코드의 일부다.

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
    @QueryAnnotation
    @Documented
    public @interface Query {
        
        String value() default ""; // 어노테이션이 붙은 메서드가 호출되었을 때 실행할 JPA 쿼리
        
        boolean nativeQuery() default false; // 쿼리가 native query인지 여부
        ...
    }

    어노테이션의 `value` 값으로 JPQL(Java Persistence Query Language)이나 SQL을 전달할 수 있다. 하지만 SQL문은 데이터베이스 종류에 따라 변할 수 있느니 JPQL을 권장한다.

    public interface MemberRepository extends JpaRepository<Member, Long> {
        ...
        // 1. 쿼리 메서드 사용하기
        Optional<Member> findByNickname(final String nickname);
        
        // 2. @Query 어노테이션 사용하기
        @Query("SELECT m FROM Member m where m.nickname =: nickname")
        Optional<Member> findByNicknameUsingQueryAnnotation(final String nickname);
    }

     

    영속성 컨텍스트

    `EntityManager`는 엔티티 객체를 영속성 컨텍스트로 조회하거나 영속성 컨텍스트에 저장/삭제하는 기능을 제공한다. 영속성 컨텍스트는 스프링 컨텍스트가 빈을 관리하는 것처럼 엔티티 객체를 저장하고 관리하는 환경이다. 엔터티 객체를 보관하고 상태를 관리하며, 엔티티의 상태에 따라 데이터베이스에 적절한 쿼리를 실행한다.

    💡 Hibernate는 내부적으로 `EntityManager`를 사용하므로 직접 쿼리를 실행하는 대신 영속성 컨텍스트를 이용하여 데이터를 처리한다.

     
    영속성 컨텍스트는 엔티티 객체를 다음 네 가지 상태로 정의한다.

    1. 비영속 상태 : 엔티티 객체가 영속성 컨텍스트에 포함되지 않은 상태
    2. 영속 상태 : 엔티티 객체가 영속성 컨텍스트에 포함되어 관리되는 상태
      • 새롭게 생성한 엔티티 객체를 `persist()`를 사용하여 영속 상태로 변경
      • 데이터베이스의 데이터를 `find()`를 통해 조회하고 영속 상태로 변경
    3. 준영속 상태 : 엔티티 객체가 영속성 컨텍스트에 포함되었다가 분리된 상태
      • 준영속 상태의 객체를 `merge()`를 사용하여 영속 상태로 변경
    4. 삭제 상태 : 엔티티 객체가 영속성 컨텍스트에서 삭제된 상태

    트랜잭션이 정상적으로 종료되면 Hibernate는 `EntityManager`의 `flush()` 메서드를 실행하여 객체들을 데이터베이스에 반영한다. 새로 만들어진 객체는 INSERT, 속성이 변경된 객체는 UPDATE, 삭제된 객체는 DELETE를 사용한다. (변경 감지, 쓰기 지연) 
     
    영속성 컨텍스트는 성능 향상을 위해 다음 기능을 제공한다.

    • 1차 캐시 : 한 트랜잭션 내에서 같은 데이터를 여러 번 조회할 때 첫번째 요청 시엔 데이터베이스에서, 두 번째 요청부터는 영속성 컨텍스트에 저장된 엔티티 객체를 반환한다. 마찬가지로 여러 번 수정할 때도 매번 쿼리를 수행하는 대신 변경 사항을 조합하여 최종 결과만 데이터베이스에 동기화한다.
    • 쓰기 지연변경 감지 : 영속성 컨텍스트는 엔티티의 변경/신규 생성 여부를 관리하고, `flush()`를 통해 데이터베이스에 엔티티 객체를 동기화한다.
    • 지연 로딩 : 연관 관계에 있는 엔티티 객체들을 참조될 때 로딩한다.

    참고자료

    댓글