Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

장미정원

✨ JPA 이해하기 본문

Back-end

✨ JPA 이해하기

신희성 2024. 9. 2. 18:05

JPA

Java Persistence API, JPA는 자바 진영의 ORM 표준 API(인터페이스) 입니다. RDB와 객체 간의 매핑을 지원해주는 ORM 기술의 자바 표준의 API로 대표적인 구현체로는 Hibernate입니다.

 

ORM

Object-Relational Mapping, ORM은 뜻 그대로 객체와 RDB 테이블을 매핑해주는 기술입니다. 덕분에 SQL 중심적으로 개발하는 방식에서 객체 중심적으로 개발을 할 수 있도록 패러다임 불일치를 해결해줍니다.

 

JPA를 사용하는 이유

JPA 기술을 사용한다면 직접 SQL문을 작성하지 않고 객체의 메서드로 DB를 조작할 수 있어 객체지향적으로 개발할 수 있습니다. DB의 종류를 변경해도 각 DB에 맞는 쿼리를 다시 짜야할 필요가 없는 장점이 있습니다.

 

반복적인 CRUD SQL을 작성하고 쿼리의 결과를 객체에 매핑하는 작업은 힘들고 시간이 많이 드는 작업입니다. 반복적인 작업은 JPA에게 맡기고 비즈니스 로직에만 집중할 수 있게 해줍니다.

 

또 객체 그래프 탐색으로 연관된 엔티티를 탐색할 수 있으며 동일한 트랜잭션 내에서 조회한 엔티티의 동일성을 보장해주기도 합니다.

 

하지만 프로젝트 규모가 커지고 복잡하다면 예상치 못한 쿼리가 발생할 가능성이 많아 별도의 튜닝이 필요할 수 있습니다.

 

또 JPA는 성능 최적화를 위해 아래의 4가지 특징을 가지고 있습니다.

 

1차 캐시, 동일성 보장

  • 같은 트랜잭션 내에서 한번 조회한 엔티티를 다시 조회한다면 1차 캐시에 저장해둔 엔티티를 반환하기 때문에 두 엔티티의 동일성을 보장해줍니다.

쓰기지연

  • 트랜잭션 내에서 발생한 CUD 쿼리드를 한 곳에 모아두었다가 트랜잭션이 커밋될 때 한번에 쿼리하여 네트워크 성능을 향상시킵니다.
  • 영속성 컨텍스트에는 1차 캐시 저장소 이외에도 쓰기지연 SQL 저장소가 있어 해당 저장소에 쿼리를 모아두었다가 커밋되는 순간에 SQL을 날립니다.

변경감지

  • 트랜잭션 내에서 영속 엔티티의 수정이 발생했을 때 트랜잭션이 커밋된다면 1차 캐시의 엔티티의 스냅샷과 비교하여 변경된 부분이 있다면 해당 부분에 대한 쿼리를 생성하여 쓰기지연 저장소에 쿼리를 저장한 후 함께 SQL을 날립니다.

지연로딩, 즉시로딩

  • 엔티티를 조회할때 연관되어있는 엔티티를 한번에 조회하는 방식인 즉시로딩과 연관되어있는 엔티티를 DB에 쿼리하지 않았다가 실제 해당 엔티티를 참조할 때 DB에 쿼리하는 지연로딩을 지원하여 성능을 최적화할 수 있는 기능을 제공합니다.

 

영속성 컨텍스트

영속성 컨텍스트는 데이터를 영구 저장하는 환경입니다.

엔티티 매니저 팩토리를 통해 엔티티 매니저를 생성한다면 해당 엔티티 매니저가 생성될 때 영속성 컨텍스트가 생성되어 매핑됩니다. 영속성 컨텍스트는 application과 DB의 사이에 위치한 공간이라고 생각할 수 있습니다. 일반적으로 영속성 컨텍스트는 트랜잭션이 시작되기 전 생성되고 트랜잭션이 커밋된 후 삭제됩니다.

 

엔티티는 영속성 컨텍스트에 의해 4가지 상태 중 하나를 가집니다.

 

비영속

영속성 컨텍스트와 전혀 상관없는 엔티티입니다. new operation으로 생성하기만 한 엔티티가 해당 상태에 포함됩니다.

 

영속

영속성 컨텍스트에 관리되는 상태인 엔티티입니다. 엔티티 매니저의 persist 메서드로 엔티티를 영속 상태로 만들 수 있고, DB에서 조회한 엔티티는 자동으로 영속성 컨텍스트에서 관리됩니다.

 

준영속

영속성 컨텍스트에 저장되었다가 분리된 상태의 엔티티입니다. 엔티티 매니저의 detach 메서드로 영속 엔티티를 준영속 상태로 만들 수 있습니다. detach 메서드 이외에도 영속성 컨텍스트를 지운다면 해당 영속성 컨텍스트에서 관리되는 엔티티들은 준영속 상태가 됩니다. clear - 영속성 컨텍스트 초기화, close - 영속성 컨텍스트 종료

 

삭제

영속 상태인 엔티티를 삭제시킨 상태입니다.

 

영속성 컨텍스트에서 엔티티가 관리되어야 위에서 말한 여러 이점들을 누릴 수 있습니다. (1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩)

 

JPQL

JPA에서 지원하는 SQL을 추상화한 객체지향 쿼리 언어입니다. SQL은 검색 대상이 테이블이지만 JPQL은 엔티티 객체를 대상으로 쿼리를 작성할 수 있습니다. (모든 테이블을 객체로 변환하는것은 한계가 있습니다.) 애플리케이션에서 물리적인 테이블을 대상으로 SQL 쿼리를 작성한다면 특정 DB에 종속적일 수 있습니다. 하지만 엔티티 객체를 대상으로 쿼리를 작성할 수 있으므로 이런 문제도 해결해줍니다.

 

위의 SQL과 JPQL은 동일한 동작을 합니다.

 

Flush

영속성 컨텍스트의 변경사항들을 DB에 반영하는 것입니다. 영속 엔티티의 변경사항들과 쓰기지연 SQL 저장소에 저장해둔 쿼리들을 DB에 반영하는 것을 말합니다.

Flush의 발생 시점은 엔티티 매니저의 flush 메서드를 직접 호출하거나, 트랜잭션을 커밋하거나, JPQL 쿼리 실행시 flush가 호출됩니다.

 

추가로 벌크성 쿼리를 날리고 나서 flush를 호출해야합니다.

 

JPQL 쿼리 실행시 flush가 호출되는 이유

JPQL 쿼리 실행시 flush가 호출되는 이유는 JPQL의 쿼리를 SQL로 변환하여 DB에 바로 날리기 때문에 해당 트랜잭션에서 JPQL을 실행하기 전 CUD 작업을 진행하여 쓰지기연 SQL 저장소에 쿼리가 저장되어 있다면 JPQL의 조회 결과로 반영된 내용이 조회되지 않을 수 있기 때문에, JPQL을 실행하기 전에 변경사항들을 모두 DB에 반영한 후 JPQL을 실행합니다.

 

벌크성 쿼리를 날린 후 flush를 호출해야하는 이유

벌크쿼리는 많은 양의 튜플에 대한 수정, 삭제 작업을 처리할때 각각 튜플에 대해 쿼리하는 것이 아닌 한번의 쿼리로 많은 양의 데이터에 대한 처리를 할 수 있는 쿼리입니다.

 

JPQL으로 쉽게 작성할 수 있고 JPQL 빌더 역할을 하는 QueryDSL을 사용해서도 작성할 수 있습니다. 하지만 이렇게 편리한 벌크쿼리를 사용할 때는 한가지 유의할 점이 있습니다. 벌크쿼리는 영속성 컨텍스트를 무시하고 DB에 직접적으로 쿼리가 나갑니다. 벌크쿼리 이후 DB의 상태는 변경되었지만 영속성 컨텍스트의 내용은 변경되지 않는 상황이 벌어질 수 있다는 말입니다. 그렇기 때문에 벌크쿼리를 한 이휴 영속성 컨텍스트를 초기화해주어 DB와의 일관성을 맞춰야합니다.

 

영속성 컨텍스트를 엔티티 매니저를 이용해 직접 초기화해주거나, Spring Data JPA의 쿼리 메서드를 사용하여 JPQL을 작성한다면 @Modifying 어노테이션 속성으로 clearAutomatically를 true로 주어 쿼리 이후 영속성 컨텍스트를 초기화하도록 합니다.

 

Spring Data JPA vs JPA

Spring Data JPA와 JPA의 개념에 대해 혼용하여 사용할 여지가 있습니다. Spring Data JPA는 JPA를 조금 더 쉽게 사용할 수 있도록 추상화한 라이브러리입니다. 대표적인 기능으로는 JpaRepository 인터페이스를 만들 수 있도록 지원해주어 쿼리 메서드 기능으로 메서드 시그니처만으로도 DB를 조작할 수 있도록 도와주는 라이브러리입니다.

 

결국 Spring Data JPA도 내부적으로 JPA, 더 깊게 들어가면 JDBC 기술을 사용하고 있으므로 하위 기술에 대한 이해 없이 상위 기술만 사용한다면 나중에 닥쳐올 재앙을 견뎌내기 힘들것 입니다.

 

OSIV

Open Session In View, OSIV는 직역하자면 세션을 view까지 열어둔다. 정도로 해석할 수 있습니다. 여기서 세션은 영속성 컨텍스트를 의미합니다.

 

JPA에서는 엔티티를 저장하기 위해 엔티티 객체를 생성하고 영속성 컨텍스트에서 영속화 시킵니다. (save) 그리고 트랜잭션이 커밋되는 시점에 영속성 컨텍스트에 flush가 호출되어 영속성 컨텍스트의 변경사항들을 실제 데이터베이스에 쿼리를 날립니다.

 

여기서 이 영속성 컨텍스트는 트랜잭션과 생명주기가 동일하다고 생각하실거라 생각됩니다. 하지만 정확히는 아닙니다! OSIV는 영속성 컨텍스트를 요청이 들어오는 순간부터, 요청이 끝날때 까지 영속성 컨텍스트를 유지하는 것입니다. 실제로 OSIV 설정은 기본적으로 true로 설정되어있습니다. 즉 OSIV가 default로 켜져있죠. (여러분들이 스프링을 실행한다면 WARN 로깅이 찍힐것 입니다. OSIV가 켜져있다는 경고이죠)

 

 

그렇기 때문에 실제로 영속성 컨텍스트는 요청이 인터셉터를 통과할 때부터 생성되어 있고, 트랜잭션이 시작된다면 열려있는 영속성 컨텍스트를 가져와 실행하게 되죠, 그리고 트랜잭션이 끝나도 영속성 컨텍스트가 유지되기 때문에 지연로딩을 하거나 영속성 컨텍스트에서 엔티티 조회가 가능하죠.

 

하지만 OSIV가 켜져있다고 마냥 좋은것은 아닙니다. 영속성 컨텍스트가 유지된다는것은 데이터베이스 커넥션을 물고있다는 것입니다. 영속성 컨텍스트가 요청을 시작부터 끝까지 유지되니 그만큼 데이터베이스 커넥션을 유지하게 됩니다.

 

하지만 커넥션 풀의 커넥션은 한정되어있고 결국 커넥션이 부족해지는 현상이 발생할 수 있습니다. 그렇기 때문에 yml 설정으로 OSIV를 끌 수 있습니다.

 

 

OSIV를 끄게된다면 트랜잭션을 시작할때 영속성 컨텍스트를 생성하게 되고 트랜잭션이 커밋되는 시점에 영속성 컨텍스트를 날리고 데이터베이스 커넥션도 반환하게 됩니다. 하지만 트랜잭션 범위 밖에서는 영속성 컨텍스트의 엔티티를 조회하거나 지연로딩은 불가능하겠죠. 이런 복잡성을 관리하기 위해서는 CQRS 패턴을 고려할 수 있습니다. 복잡한 애플리케이션이라면 핵심비즈니스 로직인 CUD 작업 Cammand와 API 스펙 관련 복잡한 조회 작업 Query의 관심사를 분리하여 해결할 수 있습니다.

 

ref

https://www.inflearn.com/course/ORM-JPA-Basic

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94