[JPA] JPA 맛보기

 

JPA 맛보기

발표 날짜: 2022/08/13 작성자: Junseok Yoon 키워드: Spring

김영한님의 “자바 ORM 표준 JPA 프로그래밍”을 참조하여 작성한 글입니다.

JPA란?

  • JPA(Java Persistent API), JAVA 진영의 ORM 기술 표준
  • 어플리케이션과 JDBC 사이에서 동작함

![<도서> 자바 ORM 표준 JPA 프로그래밍](https://user-images.githubusercontent.com/39883609/216023483-e79b1cbf-e8ab-4e61-9f3a-32e7e09b5758.png)

<도서> 자바 ORM 표준 JPA 프로그래밍 ## JDBC - Java DataBase Connectivity - 자바 진영에서 DB와 연결되어 데이터를 주고 받을 수 있게 해주는 API ```kotlin val conn = ... val sql = "select id, email, user_role from account" val psmt = conn.prepareStatement(sql) val result = psmt.executeQuery() ``` ## JPA 등장 배경 - EJB(Enterprise Java Bean)에서 제공하는 ORM인 Entity Bean ![https://kimseunghyun76.tistory.com/327](https://user-images.githubusercontent.com/39883609/216023550-09eda004-3d89-4bcb-8c89-e8e789c4e7c4.png) [https://kimseunghyun76.tistory.com/327](https://kimseunghyun76.tistory.com/327) - 너무 복잡하고 기술이 성숙하지 않았음. → Hibernate의 등장 - EJB 3.0에서 Entity Bean 대신 JPA를 표준으로 사용하게 됨 ![https://stackoverflow.com/questions/4477082/what-is-a-jpa-implementation](https://user-images.githubusercontent.com/39883609/216023588-67f82d1b-58ce-40f3-b766-c3f13c9508a7.png) [https://stackoverflow.com/questions/4477082/what-is-a-jpa-implementation](https://stackoverflow.com/questions/4477082/what-is-a-jpa-implementation) - JPA는 단순히 JAVA ORM 기술에 대한 API 표준 명세임 → 인터페이스 - JPA를 사용하려면 JPA를 구현한 ORM 프레임워크(JPA 구현체)를 선택해야 함 ## JPA를 왜 사용하는가? - **생산성 - Raw SQL을 작성하지 않아도 됨** ```kotlin val account = ... entityManager.persist(account) // 저장 val findAccount = entityManager.find(accountId) // 조회 ``` - **유지보수** SQL을 직접 작성하는 경우 엔티티에 필드를 하나만 추가해도 관련된 CRUD SQL과 JDBC API 코드를 모두 수정해야 함 JPA를 사용하면 이 과정을 알아서 처리해줌, 또한 패러다임 불일치 문제도 해결해줌 > 패러다임 불일치 문제 객체지향 패러다임과 관계형 패러다임의 불일치 문제- 상속, 연관관계, 객체 그래프 탐색, 비교 > - **성능** 어플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 **기회**를 제공함 ```kotlin val email = "test@test.com" val member1 = entityManager.findByEmail(email) val member2 = entityManager.findByEmail(email) ``` JDBC를 사용하는 경우 회원을 조회할 때 마다 SQL을 보내므로 DB와 두 번 통신함 JPA의 경우 한번 SQL을 전달하고 두 번째는 조회한 회원 객체를 재사용함 - 영속성 컨텍스트로 인해 가능 - **데이터 접근 추상화와 벤더 독립성** 관계형 데이터베이스는 같은 기능이라도 벤더마다 사용법이 다른 경우가 많음 JDBC를 사용하는 경우 SQL을 작성하기 때문에 처음 선택한 DBMS에 종속되고 다른 DBMS로 변경하는 데는 비용이 매우 많이 듬 JPA는 다양한 Dialect를 지원하기 때문에 어플리케이션이 특정 DBMS에 종속되지 않도록 함 ![https://jamong-icetea.tistory.com/395](https://user-images.githubusercontent.com/39883609/216023637-8ea20d4c-8371-4cde-b18b-ac0be90b36c3.png) [https://jamong-icetea.tistory.com/395](https://jamong-icetea.tistory.com/395) ### JPA 단점 - **N+1 문제로 인한 성능 저하 이슈** N : 1 관계에서 하나의 데이터를 조회할 때 1번 조회해야할 것을 N개의 데이터 각각을 추가로 조회해서 총 N + 1번 조회를 하게 되는 문제 JPA를 사용할 경우 N+1 문제를 고려해야 함 → 안할 경우 성능 저하 이슈 - **높은 러닝 커브** 방대한 내용으로 인해 러닝 커브가 높음 --- ## JPA 맛보기 ```kotlin @Entity @Table(name = "account") class Account( @Id @Column(name = "member_id") @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, @Column(name = "email", unique = true) val email: String, @Column(name = "password") val password: String, @Column(name = "user_role") val userRole: UserRole? = UserRole.ROLE_USER, @Column(name = "created_at") val createdAt: ZonedDateTime? = ZonedDateTime.now() ) { ``` ### @Entity - 클래스를 테이블과 매핑, JPA가 관리 - `@Entity`가 붙은 클래스를 엔티티 클래스라고 함 - Kotlin에서 Entity 클래스를 사용할 때 Data class를 사용하는 것은 주의해야 함 [](https://www.baeldung.com/kotlin/jpa#data-classes) ### @Table - 엔티티 클래스에 매핑할 테이블 정보를 알려줌 - `name` 속성을 사용해 테이블 이름을 지정할 수 있음 - `name` 속성 생략 시 해당 엔티티 이름을 사용해 테이블을 지정함 ### @Id - 엔티티 클래스의 필드를 테이블의 기본키(PK)로 매핑함 - 기본키 매핑의 경우 직접 할당과 자동 생성이 있음 ### @GeneratedValue - 기본기 매핑 시 자동 생성을 위한 어노테이션 - `strategy` 속성을 통해 생성 방법을 지정해줄 수 있음 - `GenerationType.IDENTITY`: 기본 키 생성을 DBMS에 위임, AUTO_INCREMENT 속성 - `GenerationType.SEQUENCE`: DBMS의 Sequence Object를 사용 - `GenerationType.TABLE`: 키 생성 전용 테이블을 통해 시퀀스처럼 사용 - `GenerationType.AUTO`: Dialect에 따라 위 세 가지 방법 중 하나를 자동으로 지정함, Default ### @Column - 필드를 칼럼에 매핑함 - `name` 속성을 사용해 칼럼 이름을 지정하여 매핑할 수 있음 --- ## EntityManager - JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있음 - 엔티티 매니저는 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리함 ### EntityManagerFactory ```kotlin val emf: EntityManagerFactory = Persistence.createEntityManagerFactory("") ``` - EntityManagerFactory를 통해 필요할 때 마다 엔티티 매니저를 생성할 수 있음 ```kotlin val em: EntityManager = emf.createEntityManager() ``` - EntityManagerFactory는 생성 비용이 크기 때문에 한 개만 생성하여 어플리케이션 전체에서 공유하도록 설계되어 있음 - 반면 EntityManager를 생성하는 비용은 매우 적음 ![https://velog.io/@kcwthing1210/W8D2-JPA](https://user-images.githubusercontent.com/39883609/216023709-2c881dd4-9cb6-47bb-8caa-9e8fde19bd90.png) [https://velog.io/@kcwthing1210/W8D2-JPA](https://velog.io/@kcwthing1210/W8D2-JPA) - EntityManagerFactory는 여러 스레드가 동시에 접근해도 안전하므로 다른 스레드 간 공유 가능 - EntityManager는 여러 스레드가 동시에 접근하면 동시성 문제 발생 → 스레드 간 공유 불가능 --- ## 영속성 컨텍스트 - JPA에서 가장 중요한 개념 → 영속성 컨텍스트(persistent context) - 엔티티를 영구 저장하는 환경, 엔티티 매니저가 영속성 컨텍스트에 엔티티를 보관하고 관리함 ```kotlin em.persist(account) ``` - 위의 계정 엔티티를 저장하는 코드에서 `persist()` 메서드는 엔티티 매니저를 사용해서 계정 엔티티를 영속성 컨텍스트에 저장하는 역할을 함 - 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지고 엔티티 매니저를 통해 영속성 컨텍스트에 접근 및 관리가 가능함 ![https://www.baeldung.com/jpa-hibernate-persistence-context](https://user-images.githubusercontent.com/39883609/216023771-129785e4-618d-4216-a289-c29df165a44d.png) [https://www.baeldung.com/jpa-hibernate-persistence-context](https://www.baeldung.com/jpa-hibernate-persistence-context) ## 엔티티의 생명주기 - 엔티티의 상태에는 4가지가 존재함 - 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태 - 영속(managed): 영속성 컨텍스트에 저장된 상태 - 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태 - 삭제(removed): 삭제된 상태 ![https://blog.woniper.net/266](https://user-images.githubusercontent.com/39883609/216023827-7be977bc-ca83-46d9-be49-adc7b1a3c767.png) [https://blog.woniper.net/266](https://blog.woniper.net/266) ### 비영속(new) ```kotlin val account = Account("email", "password") ``` - 엔티티 객체를 생성하기만 하고 아직 저장하지 않은 상태 - 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없음 ### 영속(managed) ```kotlin em.persist(account) ``` - 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태 - 영속성 컨텍스트가 관리하는 엔티티를 영속 상태에 있다고 함 - `em.find()`나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태임 ### 준영속(detached) ```kotlin em.detach(account) ``` - 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 관리하지 않게 되면 준영속 상태가 됨 - 엔티티 매니저의 `detach()` 메서드를 호출하여 준영속 상태로 만들 수 있음 ### 삭제(removed) ```kotlin em.remove(account) ``` - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한 상태 ## 영속성 컨텍스트의 이점 - 1차 캐시 - 영속성 컨텍스트가 관리하는 모든 객체는 캐싱됨 - 동일성 보장 - 값이 아닌 인스턴스의 동일성을 보장함 - 트랜잭션을 지원하는 쓰기 지연 - 엔티티의 변경사항을 바로 적용하지 않고 쿼리형태로 저장되어 `flush()`나 `commit`을 통해 변경됨 - 변경 감지(Dirty Checking) - 엔티티의 수정을 영속성 컨텍스트가 알아서 체크해줌 - 지연 로딩 - 프록시 객체를 통해 실제로 사용할 때 조회함 ### 엔티티 조회 - 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 함 - 영속 상태의 엔티티들은 모두 이곳에 저장됨 → 키가 @Id, 값이 엔티티인 Map ```kotlin val member = Member(id = "member1", username = "회원1") em.persist(member) ``` ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216023905-b59b6569-5577-4dce-aa34-9802e59f25c2.png) <도서> 자바 ORM 표준 프로그래밍 - 아직 데이터베이스에는 저장되지 않은 상태 - 1차 캐시의 키는 식별자 값, 식별자 값은 데이터베이스의 기본키와 매핑되어 있음 ```kotlin val member = em.find(Member, "member1") ``` - 엔티티 조회 시 1차 캐시에서 찾고 없으면 데이터베이스에서 조회함 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216023959-eb0401d2-abe3-44aa-abe4-4f95a8d061e1.png) <도서> 자바 ORM 표준 프로그래밍 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216023994-91efdedb-b43a-4658-84e5-299bfbcdedc9.png) <도서> 자바 ORM 표준 프로그래밍 - 영속 상태의 엔티티는 동일성을 보장할까? ```kotlin val member1 = em.find(Member, "member1") val member2 = em.find(Member, "member2") // member1 == member2 ``` - `find()` 메서드 호출 시 1차 캐시에 있는 엔티티 인스턴스를 반환하기 때문에 같은 인스턴스임 ### 엔티티 등록 ```kotlin val transaction = em.getTransaction() transaction.begin() // 트랜잭션 시작 em.persist(member1) em.persist(member2) transaction.commit() // 트랜잭션 커밋 ``` - 트랜잭션을 커밋하기 전까지 데이터베이스에 엔티티를 저장하지 않고 쿼리 형태로 모아둠 - 트랜잭션 커밋 시 모아둔 쿼리를 데이터베이스에 보내는데 이를 트랜잭션을 지원하는 쓰기 지연이라 함 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216024051-5444f39b-0fc5-46ee-933b-73cb5c78b1f0.png) <도서> 자바 ORM 표준 프로그래밍 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216024116-db5aa7a9-617e-405c-afeb-e5357a6ab763.png) <도서> 자바 ORM 표준 프로그래밍 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216024171-f3f9a26c-4569-4446-aa7d-fee406c92222.png) <도서> 자바 ORM 표준 프로그래밍 - 트랜잭션을 커밋할 때 엔티티 매니저는 영속성 컨텍스트를 `flush` 함 - `flush()`는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업 ### 엔티티 수정 - JPA에서 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 됨 - 이를 변경 감지(dirty checking)이라고 함 ![<도서> 자바 ORM 표준 프로그래밍](https://user-images.githubusercontent.com/39883609/216024232-fc7c5185-6b42-44a5-bcf4-e59aca20aab7.png) <도서> 자바 ORM 표준 프로그래밍 - JPA에서 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이를 스냅샷이라 함 - 그 후 `flush()` 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾음 - 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됨 ### 엔티티 삭제 ```kotlin val member = em.find(Member, "member") em.remove(member) ``` - `em.remove()`에 삭제 대상 엔티티를 넘겨주면 엔티티를 삭제함 - 위에서 말한 쓰기 지연이 적용되어 삭제 쿼리를 쿼리 형태로 저장하고 트랜잭션 커밋 시 삭제 쿼리를 전달함 - 하지만 `em.remove()`를 호출하는 순간 넘겨준 엔티티는 영속성 컨텍스트에서 제거됨 ## Spring Data JPA - Spring Framework에서 JPA를 편리하게 사용할 수 있도록 지원 - JPA를 이용한 구현체를 더욱 추상화한 레포지토리를 제공함 → 쉽게 Spring 개발을 가능하게 함 ```java @NoRepositoryBean public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor { @Override List findAll(); @Override List findAll(Sort sort); @Override List findAllById(Iterable ids); @Override List saveAll(Iterable entities); void flush(); S saveAndFlush(S entity); List saveAllAndFlush(Iterable entities); @Deprecated default void deleteInBatch(Iterable entities) { deleteAllInBatch(entities); } void deleteAllInBatch(Iterable entities); void deleteAllByIdInBatch(Iterable ids); void deleteAllInBatch(); @Deprecated T getOne(ID id); @Deprecated T getById(ID id); T getReferenceById(ID id); @Override List findAll(Example example); @Override List findAll(Example example, Sort sort); } ``` ![스크린샷 2022-08-11 오전 11.06.10.png](https://user-images.githubusercontent.com/39883609/216024292-88dd1960-8240-4aba-bbe3-2d682686eef9.png) - `JpaRepository`를 상속받아 인터페이스만 작성하면 실행 시점에 구현 객체를 동적으로 생성하여 주입함 - 구현된 공통 메서드들을 사용해 CRUD 처리를 쉽게 할 수 있음 ### Query Methods - 메서드 이름만으로 쿼리를 생성하게 해주는 방법 - `JpaRepository`를 상속받아 구현한 레포지토리에 메서드만 선언하면 해당 메서드의 이름으로 적절한 쿼리를 생성하여 실행해줌 ![스크린샷 2022-08-11 오전 11.12.08.png](https://user-images.githubusercontent.com/39883609/216024381-871cb642-54da-4697-b4a0-b9d86efdb9ed.png) - 예를 들어 특정 시간 이후에 생성된 계정을 조회하는 쿼리가 필요한 경우 다음과 같이 선언할 수 있음 ```kotlin fun findAccountByCreatedAtAfter(createdAt: ZonedDateTime): Account? ``` - 쿼리를 직접 작성해야 하는 경우 `@Query` 어노테이션을 통해 레포지토리에 메소드 쿼리를 정의할 수 있음 - `@Param` 어노테이션을 통해 파라미터를 바인딩할 수 있음, 이름 기반 파라미터 바인딩이 권장됨 ```kotlin @Query("select a from Account a where a.created_at > :created_at") fun findAccount(@Param("created_at") createdAt: ZonedDateTime): Account? ``` ### Paging, Sorting - 특정 시간 이후에 생성되었고, 생성 시간으로 내림차순, 한 페이지당 10개의 데이터를 조회하고 첫 페이지만 보여주는 페이징 함수를 선언할 때 - 순수 JPA로 이를 구현한다면 ```kotlin fun findByCreatedAtPaging(createdAt: ZonedDateTime, offset: Int, limit: Int) { return em.createQuery("select a from Account a where a.created_at > :created_at by a.created_at desc", Account) .setParameter("created_at", createdAt) .setFirstResult(offset) // 몇번째부터? .setMaxResults(limit) // 몇개까지? .getResultList() } // 페이징 구현 과정에서 필요한 정보와 과정들을 전부 신경써줘야 함 ``` - Spring Data Jpa에서는 `Pageable` 인터페이스를 통해 페이징과 정렬 기능을 제공함 ![스크린샷 2022-08-11 오전 11.33.37.png](https://user-images.githubusercontent.com/39883609/216024417-416b1b46-2747-4237-8dca-7798f256fc8a.png) - 위에서 순수 JPA로 구현한 내용을 `Pageable` 인터페이스를 통해 단순하게 정의 가능함 ```kotlin fun findAccountByCreatedAtAfter( createdAt: ZonedDateTime, pageable: Pageable): Page fun findAccountByCreatedAtAfter( createdAt: ZonedDateTime, pageable: Pageable): List // 반환 타입으로 List를 사용하는 경우 count 쿼리를 호출하지 않음 ``` - `Pageable` 인터페이스는 페이징과 관련된 다양한 메서드들을 제공함 ```kotlin @Transactional fun findAccountByCreatedAt(createdAt: ZonedDateTime) { val pageRequest = PageRequest.of(0, 10, Sort.Direction.DESC) val pageResult = accountRepository .findAccountByCreatedAtAfter(createdAt, pageRequest) val accounts = pageResult.get() val totalPages = pageResult.totalPages val hasNextPage = pageResult.hasNext() } ``` --- ## More to learn - 프록시와 연관관계 매핑, 지연로딩과 즉시로딩 - N+1 문제 해결방법 - JPQL(Java Persistent Query Language)와 JPA Criteria, QueryDSL - JPA 성능 최적화 - Spring Webflux ![[https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html)](https://user-images.githubusercontent.com/39883609/216024463-aef6dffb-1619-4411-8328-2367ccf423dc.png) [https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) - Non-Blocking한 DB Connection을 위해선? → WebFlux + R2DBC - 참조 <도서> 자바 ORM 표준 JPA 프로그래밍 [https://docs.spring.io/spring-data/jpa/docs/current/reference/html/](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/) [https://www.baeldung.com/learn-jpa-hibernate](https://www.baeldung.com/learn-jpa-hibernate) [https://engineering.linecorp.com/ko/blog/kotlinjdsl-jpa-criteria-api-with-kotlin](https://engineering.linecorp.com/ko/blog/kotlinjdsl-jpa-criteria-api-with-kotlin)