일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- visitor proxy pattern
- annotation
- line messaging api
- NullPointerException
- 챗봇
- reflection
- Modelmapper
- java
- enum
- ngrok
- webhook
- springboot
- Controller
- Optional
- DtoToEntity
- getOrCreate
- mapping
- 토비의 봄
- Dynamic dispatch
- static dispatch
- Visitor pattern
- spring
- double dispatch
- EntityToDto
- linebot
- Today
- Total
database by narae :p
JPA 정리하기 본문
영속성 컨텍스트
=> 엔티티를 영구 저장하는 환경
=> 엔티티 매니저로 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 인티티를 보관하고 관리한다.
영속성 컨텍스트의 특징
⁃ 식별자 값 : 영속성 컨텍스트는 엔티티를 식별자 값(@Id)으로 구분한다. 따라서, 영속 상태는 식별자 값이 반드시 있어야 한다.
⁃ 데이터베이스 저장 : JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다. 이를 flush라고 한다.
⁃ 장점 : 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩
엔티티 생명주기
- 비영속 (new/transient) : 영속성 컨텍스트와 관계 없는 상태
- 영속 (managed) : 영속성 컨텍스트에 저장된 상태
- 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 (removed) : 삭제된 상태
엔티티 조회
⁃ 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 ‘1차 캐시’라고 한다. 영속 상태의 엔티티는 모두 이곳에 저장된다.
⁃ 영속성 컨텍스트 내부에 Map이 하나 있는데, key는 @Id로 매핑한 식별자고, 값은 엔티티 인스턴스이다.
⁃ 식별자 값은 데이터베이스 기본 키와 매핑되어 있다. 따라서, 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스 기본키 값이다.
⁃ em.find()를 하면, 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.
⁃ 1) 1차 캐시에서 조회 : 영속성 컨텍스트의 1차 캐시에 엔티티가 존재하면 데이터베이스를 조회하지 않고, 메모리에 있는 1차 캐시에서 엔티티를 조회한다.
⁃ 2) 데이터베이스에서 조회 : 엔티티가 1차 캐시에 없으면, 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다. (DB 조회 => 1차 캐시에 저장 => 1차 캐시에 있는 영속 상태의 엔티티 반환)
⁃ 식별자가 같은 엔티티 인스턴스는 같은 엔티티 인스턴스이다. 영속성 컨텍스트는 엔티티의 동일성을 보장한다.
⁃ JPA는 1차 캐시를 통해 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 애플리케이션 차원에서 제공한다.
엔티티 등록
⁃ 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고, 내부 쿼리 저장소에 SQL을 차곡차곡 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보낸다. 이것을 트랜잭션을 지원하는 쓰기 지연이라고 한다.
⁃ persist(Entity) : 1차 캐시에 엔티티를 영속화하고, 쓰기 지연 SQL 저장소에 INSERT SQL을 저장 => 트랜잭션이 커밋되면 그 때 데이터베이스에 보내서 INSERT를 수행
⁃ 트랜잭션을 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이 때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다. (쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.) 이렇게 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋한다.
엔티티 수정
• 변경 감지 : JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. 엔티티의 변경 사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지라고 한다.
• JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해둔다. (스냅샷)
• 그리고, 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
• 트랜잭션 커밋
• 1) 엔티티 매니저 내부에서 flush() 호출
• 2) 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
• 3) 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
• 4) 데이터베이스 트랜잭션 커밋
• 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다. 비영속, 준영속처럼 영속성 컨텍스트의 관리를 받지 못하는 엔티티는 값을 변경해도 데이터베이스에 반영되지 않는다.
• JPA의 엔티티 수정 전략 (정적 수정 쿼리) : 바뀐 필드만 업데이트 하지 않고, 엔티티의 모든 필드를 업데이트 한다.
• 데이터베이스에 보내는 데이터 전송량이 증가하는 단점이 있지만, 두가지 장점이 있다.
• 1) 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
• 2) 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
• 대략 30개 이상의 컬럼을 가지고 있으면 기본 전략인 정적 수정 쿼리보다, @DynamicUpdate를 사용한 동적 수정 쿼리가 빠르다고 한다.
엔티티 삭제
⁃ 엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회하고, em.remove()에 삭제 대상 엔티티를 넘겨주어 엔티티를 삭제한다.
⁃ 엔티티를 즉시 삭제하는 것이 아니라 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록한다. 이후 트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달한다. remove()를 호출하면 영속성 컨텍스트의 1차캐시에서는 제거된다. 이렇게 삭제된 엔티티는 재사용하지 말고 자연스럽게 가비지 컬렉션의 대상이 되도록 두는 것이 좋다.
플러시 flush
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
⁃ 1) 변경 감지 동작 : 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
⁃ 2) 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
⁃ em.flush() 직접 호출 => 거의 사용하지 않음
⁃ 트랜잭션 커밋 시 플러시 자동 호출
⁃ JPQL 쿼리 실행 시 플러시 자동 호출 => JPQL은 SQL로 변환되어 (영속성 컨텍스트가 아닌) 실제 데이터베이스에서 엔티티를 조회하기 때문
⁃ 데이터베이스와 동기화를 최대한 늦추는 것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문이다. 트랜잭션 커밋 직전에만 변경 내용을 데이터베이스에 보내 동기화하면 된다.
준영속
영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태. 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
⁃ em.detach(entity) : 특정 엔티티만 준영속 상태로 전환한다.
⁃ 영속성 컨텍스트에게 해당 엔티티를 더는 관리하지 말라는 것.
⁃ 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.
⁃ em.clear() : 영속성 컨텍스트 초기화
⁃ 모든 엔티티를 준영속 상태로 만든다.
⁃ em.close() : 영속성 컨텍스트 종료
⁃ 특징
⁃ 비영속 상태에 가깝다. 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
⁃ 식별자 값을 가지고 있다. 비영속 상태는 식별자 값이 없을 수도 있지만, 준영속 상태는 한번 영속 상태였으므로 식별자 값을 가지고 있다.
⁃ 지연 로딩을 할 수 없다. 지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.
병합 (merge) : 준영속 및 비영속(식별자 값이 있는) 상태의 엔티티를 다시 영속 상태로 변경
⁃ 1) 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
⁃ 2) 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
⁃ 3) 조회한 영속 엔티티에 엔티티의 값을 채워넣고, 반환
프록시 proxy
⁃ 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
⁃ 지연 로딩 : 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법 => 지연 로딩 기능을 사용하려면 실제 엔티티 개체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라고 한다.
⁃ em.find() : 엔티티를 실제 사용하든 사용하지 않든 데이터베이스에서 조회한다.
⁃ em.getReference() : 데이터베이스를 조회하지 않고, 시제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
프록시 클래스
실제 클래스를 상속받아서 만들어지므로 실제 클래스와 겉 모양이 같다. 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
⁃ 프록시 객체는 실제 객체에 대한 참조(target)를 보관한다.
⁃ 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
⁃ 프록시 객체 초기화 : 프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다.
⁃ 1) 프록시 객체에 실제 메소드를 호출해서 실제 데이터를 조회하도록 한다.
⁃ 2) 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다 (프록시 초기화)
⁃ 3) 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
⁃ 4) 프록시 객체는 생성된 실제 엔티티 객체의 참조를 프록시 멤버 변수에 보관한다.
⁃ 5) 프록시 객체는 실제 엔티티 객체의 메소드를 호출해서 결과를 반환한다.
프록시의 특징
⁃ 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
⁃ 프록시 객체를 초기화한다고 프록시객체가 실제 엔티티로 바뀌는 것은 아니다. 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
⁃ 프록시 객체는 원본 엔티티를 상속 받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
⁃ 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
⁃ 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제(LazyInitializationException)가 발생한다.
프록시와 식별자
⁃ 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
⁃ 엔티티 접근 방식이 PROPERTY로 설정한 경우, 프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 getId()를 호출해도 프록시를 초기화하지 않는다.
⁃ 엔티티 접근 방식이 FIELD로 설정한 경우, JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다.
⁃ 연관 관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있다. 연관 관계를 설정할 때는 엔티티 접근 방식이 FIELD여도 프록시를 초기화하지 않는다.
프록시 확인
PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다. 아직 초기화되지 않은 프록시 인스턴스는 false를 반환하고, 이미 초기화되었거나 프록시 인스턴스가 아니면 true를 반환한다.
프록시 강제 초기화 : initialize()
⁃ 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. FetchType.EAGER
⁃ 즉시 로딩을 최적화하기 위해 조인 쿼리를 사용한다. 내부 조인이 외부 조인보다 성능과 최적화에서 더 유리하지만, JPA는 기본적으로 조인 전략으로 외부 조인을 사용한다.
⁃ 기본 조인 전략 : 외부 조인 => nullable, optional인 경우 내부 조인을 하면 누락되는 데이터가 존재할 수 있기 때문
⁃ Nullable = false, optional = false이면 내부 조인
⁃ 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다. FetchType.LAZY
⁃ 해당 엔티티만 조회하고 연관된 엔티티는 조회하지 않는다. 그리고 연관된 엔티티 멤버 변수에 프록시 객체를 넣어둔다,
⁃ 따라서 연관 엔티티 객체는 프록시 객체이며, 이 프록시 객체는 실제 사용될 떄까지 데이터 로딩을 미룬다.
⁃ 연관 엔티티를 조회할 때는 연관 엔티티의 테이블에서 조회해온다. (조인 x)
프록시와 컬렉션 래퍼
⁃ 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고 한다.
⁃ 엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만, 컬렉션은 컬랙션 래퍼가 지연 로딩을 처리한다. 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 하므로 따로 구분하지 않고 프록시라고 생각한다.
JPA 기본 패치 전략
⁃ @ManyToOne, @OneToOne : 연관된 엔티티가 하나면 즉시 로딩
⁃ @OneToMany, @ManyToMany : 컬렉션이면 지연 로딩(컬렉션을 로딩하는 것은 비용이 많이 들기 떄문)
컬렉션에 FetchType.EAGER 사용 시 주의점
⁃ 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다. 컬렉션과 조인한다는 것은 데이터베이스 테이블로 보면 일대다 조인이다. 일대다 조인은 결과 데이터가 다 쪽에 있는 수만큼 증가하게 된다.
⁃ 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
영속성 전이 : CASCADE
⁃ 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다.
⁃ JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.
⁃ 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.
⁃ JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 따라서
영속성 전이 : 저장 CascadeType.PERSIST
⁃ 부모를 영속화할 때 연관된 자식들도 함께 영속화하라는 옵션
⁃ 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다. 단지 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.
영속성 전이 : 삭제 CascadeType.REMOVE
⁃ 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제된다.
Cascade 종류
⁃ ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH
고아 객체 orphanRemoval = true
⁃ JPA는 부모엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라고 한다.
⁃ 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
⁃ 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서, 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 쉽게 이야기해서 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 한다. @OneToOne과 @OneToMany에서만 쓸 수 있다.
스프링 데이터 JPA
스프링 프레임워크 + JPA
스프링 컨테이너 위에서 JPA를 사용하기 때문에, 스프링 컨테이너가 제공하는 데이터베이스 커넥션과 트랜잭션 처리 기능을 사용할 수 있다.
@Transactional : 스프링 프레임워크는 이 어노테이션이 붙어있는 클래스나 메소드에 트랜잭션을 적용한다. 외부에서 이 클래스의 메소드를 호출할 때 트랜잭션을 시작하고 메소드를 종료할때 트랜잭션을 커밋한다. 만약 예외가 발생하면 트랜잭션을 롤백한다.
=> RuntimeException과 그 자식들인 Unchecked 예외만 롤백한다. 만약 체크 예외가 발생해도 롤백하고 싶다면 @Transactional(rollbackFor = Exception.class)처럼 롤백할 예외를 지정해야 한다.
검증 로직이 있어도 멀티 스레드 상황을 고려해서 Unique 제약 조건을 추가하는것이 안전
⁃ CRUD를 처리하기 위한 공통 인터페이스 제공
⁃ 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서, 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발 완료
스프링 데이터 프로젝트는 JPA, mongo DB, NEO4J, REDIS,..다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터접근 코드를 줄여준다.
Repository - CrudRepository - PagingAndSortingRepository — JpaRepository
save(S) : 엔티티에 식별자 값이 없으면(null) 새로운 엔티티로 판단해서 em.persist 호출, 식별자 값이 있으면 이미 있는 엔티티로 판단해서 em.merge() 호출
delete(T) : em.remove()
findOne(id) : 엔티티 조회. Em.find()
getOne(id) : 엔티티를 프록시로 조회. em.getReference()
findAll() : 모든 엔티티 조회, sort/pageable 조건을 파라미터로 넘길 수 있다.
쿼리 메소드
⁃ 스프링 데이터 JPA가 제공하는 기능
⁃ 메소드 이름만으로 쿼리를 생성
⁃ 메소드 이름으로 JPA NamedQuery 호출
⁃ @Query 어노테이션을 사용해 리포지토리 인터페이스에 쿼리 직접 정의
JPA NamedQuery : 쿼리에 이름을 부여해서 사용. 어노테이션이나 XML에 쿼리를 정의할 수 있다.
OSIV를 사용하지 않으면 : 조회한 엔티티는 준영속 상태. 변경 감지 기능이 동작하지 않는다. 만약 수정한 내용을 데이터베이스에 반영하고 싶으면 병합(merge)을 사용해야 함
OSIV를 사용하면 : 조회한 에닡티는 영속 상태. 하지만, OSIV의 특성 상 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않는다. 따라서 수정한 내용을 데이터베이스에 반영하지 않는다. 만약 수정한 내용을 데이터베이스에서 반영하고 싶으면 트랜잭션을 시작하는 서비스 계층을 호출해야 한다. 해당 서비스 계층이 종료될 때 플러시와 트랜잭션 커밋이 일어나서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해줄 것이다.
스프링 데이터 JPA가 사용하는 구현체
⁃ @Repository : JPA 예외를 스프링이 추상화한 예외로 변환한다.
⁃ @Transactional 트랜잭션 적용 : JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경(등록, 수정, 삭제)하는 메소드에 @Transactional로 트랜잭션 처리가 되어 있다. 따라서 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다. 물론 서비스 계층에서 트랜잭션을 시작했으면 리포지토리도 해당 트랜잭션을 전파받아서 그대로 사용한다.
⁃ @Transactional(readonly=true) : 데이터를 조회하는 메소드에는 readOnly=true 옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜잭션에서 이 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
스프링 컨테이너 위에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해주므로 애플리케이션을 손쉽게 개발할 수 있다.
- 컨테이너 환경에서 동작하는 JPA의 내부 동작 방식
- 컨테이너 환경에서 웹 애플리케이션을 개발할 때 발생할 수 있는 다양한 문제점과 해결 방안
스프링 컨테이너
- 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
- 트랜잭션의 생존 범위 = 영속성 컨텍스트 생존 범위
- 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
- 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
- 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다. 이 어노테이션이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.
스프링 트랜잭션 AOP
- 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다.
- 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋한다.
- 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이 때는 플러시를 호출하지 않는다.
트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
- 트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
- 엔티티 매니저는 달라도 같은 영속성 컨텍스트를 사용한다.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
- 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용하더라도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
- 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다. 따라서, 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티 스레드 상황에 안전하다.
- 스프링 컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 처리해준다는 점이다. 따라서 개발자는 싱글 스레드 애플리케이션처럼 단순하게 개발할 수 있고 결과적으로 비즈니스 로직 개발에 집중할 수 있다.
준영속 상태와 지연 로딩
- 조회된 엔티티가 서비스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 된다.
컨테이너 환경의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프리젠테이션 계층에서 엔티티는 준영속 상태이다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다.
=> 준영속 상태와 변경 감지
- 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다.
- 프리젠테이션 계층에서 데이터를 수정할 일은 거의 없기 때문에 변경 감지가 일어나지 않는다.
- 만약, 변경 감지 기능이 프리젠테이션 계층에서도 동작하면 애플리케이션 계층이 가지는 책임이 모호해지고 무엇보다 데이터를 어디서 어떻게 변경했는지 프리젠테이션 계층까지 다 찾아야 하므로 애플리케이션을 유지보수하기 어렵다.
=> 준영속 상태와 지연 로딩
- 준영속 상태에서는 영속성 컨텍스트가 없으므로 지연 로딩을 할 수 없다. 지연 로딩을 시도하면 LazyInitializationException이라는 문제가 발생한다.
- 준영속 상태인 프리젠테이션 계층에서 지연로딩을 시도하려고 하면 문제 발생
- 해결 방법
- 1) 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- 2) OSIV를 사용해서 엔티티를 항상 영속 상태로 유지
뷰 영역(준영속상태)에서 지연 로딩 문제 해결
=> 1) 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- 영속성 컨텍스트가 살아있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법
- 따라서, 엔티티가 준영속 상태로 변해도 연관된 엔티티들을 미리 다 로딩해두었기 떄문에 지연 로딩 문제가 발생하지 않는다.
- 1) 글로벌 페치 전략을 즉시 로딩으로 설정
- 그냥 지연 로딩 대신 즉시 로딩으로 사용한다.
- 애플리케이션 전체에 영향을 주므로 매우 비효율적
- 단점 1. 사용하지 않는 엔티티를 로딩한다.
- 필요없는 엔티티들도 같이 조회(로딩)하기 때문에 좋지 않음
- 단점 2. N+1 문제가 발생한다.
- 보통 em.find()를 하면 조인 쿼리를 사용해 연관 엔티티까지 불러오지만, JPQL을 사용할 때 문제가 발생한다.
- JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다. 따라서, JPQL이 조인 쿼리가 아니라면 해당 엔티티를 조회하는 SQL과 해당 엔티티의 연관 엔티티를 조회하는 SQL을 각자 호출해야 한다.
- 이처럼 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1문제라고 한다.
- N+1이 발생하면 SQL이 아주 많이 호출되므로 조회 성능에 치명적이다.
- => JPQL 페치 조인으로 해결한다.
- 2) JPQL 페치 조인
- 위에 설명한 N+1문제에 JPQL만 페치 조인을 사용
- JPQL에 ‘join fetch 연관 엔티티’라는 페치 조인을 넣어주면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다.
- 따라서 N+1 문제는 해결한다. 하지만, 연관된 엔티티는 이미 로딩 했기 때문에 글로벌 페치 전략은 무의미하다. (그냥 즉시 로딩)
- 페치 조인은 N+1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현실적인 방법이다.
- 하지만 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있다. 결국 프리젠테이션 계층이 알게모르게 데이터 접근 계층을 침범하는 것이다.
- 무분별한 최적화로 프리젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는 적절한 선에서 타협점을 찾는 것이 합리적.
- 3) 강제로 초기화
- 영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화시켜 반환하는 방법
- 프리젠테이션 계층에서 필요한 프록시 객체를 영속성 컨텍스트가 살아있을 때 강제로 초기화해서 반환하면 이미 초기화 되었기 떄문에 준영속 상태에서도 쓸 수 있다.
- 하이버네이트를 사용하면 Initialize() 메소드 이용
- 문제점 : 프록시를 초기화하는 역할을 서비스 계층에서 담당하게 되면, 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 한다.
- 은근 슬쩍 프리젠테이션 계층이 서비스 계층을 침범하는 상황이다.
- 서비스 계층은 비즈니스 로직만을 담당해야지, 이렇게 프리젠테이션 계층을 위한 일까지 하는 것은 좋지않다.
- 따라서, 비즈니스 로직을 담당하는 서비스 계층에서 프리젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 한다.
- => FACADE 계층 추가
- 4) FACADE 계층 추가
- 뷰를 위해 강제로 프록시 객체를 초기화하는 역할을 FACADE 계층에 맡긴다.
- 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다.
- 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로, FACADE 계층까지는 트랜잭션을 시작해야 한다. (트랜잭션 범위)
- 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성 분리
- 프리젠테이션 계층에서 필요한 프록시 객체 초기화
- 서비스 계층을 호출해서 비즈니스 로직 실행
- 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
- 서비스 계층은 비즈니스 로직에 집중하고, 프리젠테이션 계층을 위한 초기화 코드는 모두 FACADE 계층이 담당한다.
- 단점 : 중간에 계층이 하나 더 끼어들어, 결국 더 많은 코드를 작성해야 한다. 그리고 FACADE에서는 단순히 서비스 계층을 호출만 하는 위임 코드가 상당히 많다.
뷰 영역(준영속상태)에서 지연 로딩 문제 해결
=> 2) OSIV
- 위에서 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한 문제이다. 따라서, 영속성 컨텍스트를 뷰까지 살아있게 열어둔다. 그럼 뷰에서도 지연 로딩을 사용할 수 있다. 이것이 바로 OSIV이다.
- 영속성 컨텍스트를 뷰까지 열어두는 것. 따라서, 엔티티가 영속 상태로 유지되므로 뷰에서도 지연 로딩을 사용할 수 있다.
OSIV : Open Session In View
과거 OSIV - 요청 당 트랜잭션 방식의 OSIV
- 뷰에서도 지연 로딩이 가능한 것
- 가장 단순한 구현 : 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요창이 끝날 때 트랜잭션도 끝내는 것 => 요청 당 트랜잭션 방식
- 이 방식은 영속성 컨텍스트가 처음부터 끝까지 살아있으므로, 조회한 엔티티도 영속 상태를 유지한다.
- 문제점 : 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점
- 요청 당 트랜잭션 방식의 OSIV는 뷰를 렌더링한 후에 트랜잭션을 커밋한다. 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시한다. 이 때 영속성 컨텍스트의 변경 감지 기능이 작동한다.
- 따라서, 뷰 영역에서 의도하지 않은 변경이 실제 데이터베이스에 반영되는 문제점이 발생한다.
- 이런 문제를 해결하려면 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막으면 된다.
- 1) 엔티티를 읽기 전용 인터페이스로 제공
- 실제 엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법
- 2) 엔티티 레핑
- 엔티티의 읽기 전용 메소드만 가지고 있는 엔티티를 감싼 객체를 반환 (읽기 메소드만 제공하는 엔티티)
- 3) DTO만 반환
- 프리젠테이션 계층에 엔티티 대신 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 것
- OSIV를 사용하는 장점을 살릴 수 없다.
스프링 OSIV - 비즈니스 계층 트랜잭션
- 스프링 프레임워크가 제공하는 OSIV 라이브러리
- 하이버네이트 OSIV 서블릿 필터
- 하이버네이트 OSIV 스프링 인터셉터
- JPA OEIV 서블릿 필터
- JPA OEIV 스프링 인터셉터
- 스프링 OSIV는 ‘비즈니스 계층에서 트랜잭션을 사용하는 OSIV’
- OSIV를 사용하긴 하지만 트랜잭션은 비즈니스 계층에서만 사용한다.
- 클라이언트의 요청이 들어오면 영속성 컨텍스트 생성. 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 트랜잭션을 시작하면 앞에 생성해둔 영속성 컨텍스트에 트랜잭션을 시작한다.
- 비즈니스 로직을 실행하고 서비스 곛으이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시한다. 이 때 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.
- 클라이언트 요청이 끝날 때 영속성 컨텍스트를 반환한다.
스프링 OSIV의 동작
- 클라이언트의 요청이 들어옴
- 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트 생성
- 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작
- 미리 생성해둔 영속성 컨텍스트에 트랜잭션을 시작
- 서비스 계층이 끝남
- 트랜잭션 커밋, 영속성 컨텍스트 플러시
- 트랜잭션은 끝나지만, 영속성 컨텍스트는 종료하지 않는다.
- 컨트롤러, 뷰
- 영속성 컨텍스트가 유지되므로, 조회한 엔티티는 영속 상태를 유지한다. (지연 로딩도 가능)
- 요청 처리 완료
- 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이 때 플러시를 호출하지 않고 바로 종료한다.
트랜잭션 없이 읽기
- 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에 이루어져야 한다.
- 만약, 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 TransactionRequiredException이 발생한다.
- 엔티티를 변경하지 않고, 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 ‘트랜잭션 없이 읽기’라고 한다.
- 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.
- 정리
- 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
- 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. (트랜잭션 없이 읽기)
- 따라서, 프리젠테이션 계층에서 엔티티를 수정할 수 있었던 기존 OSIV 단점을 보완했다. 또한, 트랜잭션 없이 읽기를 사용해서 프리젠테이션 계층에서도 지연 로딩 기능을 사용할 수 있다.
스프링 OSIV - 비즈니스 계층 트랜잭션
- 영속성 컨텍스트는 프리젠테이션 계층까지 유지한다.
- 프리젠테이션 계층에는 트랜잭션이 없기 때문에 엔티티를 수정할 수 없고, 조회만 가능하다.
- 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩을 할 수 있다.
트랜잭셤 범위 밖에서 플러시가 동작하지 않음 (수정 불가)
- 스프링 OSIV 서블릿 필터나 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고, em.close()로 영속성 컨텍스트만 종료해버리므로 플러시가 일어나지않는다.
- 강제로 em.flush()를 호출해도 트랜잭션 범위밖이므로 데이터를 수정할 수 없다는 예외를 만난다.
스프링 OSIV 주의사항
- 프리젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생
- 수정이 실제 DB에 반영되게 할거면
- 1) 엔티티 수정 후
- 2) 비즈니스 로직(서비스) 호출
- 수정이 실제 DB에 반영되지 않게 할거면
- 1) 비즈니스 로직 호출 후
- 2) 엔티티 수정
- 스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다.
- OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션 생명 주기와 영속성 컨텍스트 생명주기가 같으므로 이런 문제가 발생하지 않는다.
엄격한 계층
- OSIV를 사용하기 전에는 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화했다.
- 하지만 OSIV를 사용하면서 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없어졌다. 따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무 문제가 없다.
JPA 표준 예외처리
PersistenceException - RuntimeException
- 트랜잭션 롤백을 표시하는 예외
: 심각한 예외이므로 복구하면 안된다. 트랜잭션을 강제로 커밋해도 트랜잭션이 커밋되지 않고 대신에 RollbackException 예외가 발생한다.
- 트랜잭션 롤백을 표시하지 않는 예외
: 심각한 예외가 아니므로 트랜잭션을 커밋할지 롤백한지 정한다.
트랜잭션 롤백
- 데이터베이스의 반영사항만 롤백하고, 수정한 자바 객체까지 원상태로 복구해주지 않는다.
- 데이터베이스의 데이터는 원래대로 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아있다.
- => 이 상태의 영속성 컨텍스트를 그대로 사용하는 것은 위험하므로, 새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용한다.
- 트랜잭션당 영속성 컨텍스트 전략 : 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료
- OSIV : 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용할 수 있다. 이럴 때는, 트랜잭션 롤백 시 영속성 컨텍스트를 초기화해서 사용한다.
엔티티 비교
- 영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다. 이 1차 캐시는 영속성 컨텍스트와 생명주기를 함께 한다.
- 애플리케이션 수준의 반복 가능한 읽기 : 1차 캐시의 가장 큰 장점으로, 같은 영속성 컨텍스트에서 엔티티를 조회하면 항상 같은 엔티티 인스턴스를 반환한다.
- 영속성 컨텍스트가 같으면 엔티티를 비교할 때 동일성, 동등성, 데이터베이스 동등성 조건을 모두 만족한다.
- @Transactional의 기본전략은 먼저 시작된 트랜잭션이 있으면 그 트랜잭션을 그대로 이어받아 사용하고, 없으면 새로 시작한다. 만약 다른 전략을 사용하고 싶으면 propagation 속성을 변경하면 된다.
- OSIV처럼 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 사용할 때는 동일성 비교가 성공하지만, 영속성 컨텍스트가 달라지면 동일성 비교는 실패한다.
- 데이터베이스 동등성 비교 : 엔티티를 영속화해야 식별자를 얻을 수 있다는 문제가 있다. 엔티티를 영속화하기 전에는 식별자 값이 null이므로 정확한 비교를 할 수 없다.
- 동등성 비교(equals) : 엔티티를 비교할 떄는 비즈니스 키를 활용한 동등성 비교를 권장한다. 비즈니스 키가 되는 필드들은 선택. 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본키후보들이 좋은 대상이다.
프록시
- 원본 엔티티를 상속받아서 만들어지므로, 엔티티를 사용하는 클라이언트는 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있다.
- 하지만 프록시를 사용하는 방식의 기술적인 한계로 문제가 발생한다.
- 1) 영속성 컨텍스트와 프록시
- 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장한다.
- 프록시 먼저 조회하고 원본 엔티티를 조회한 경우 : 원본 엔티티를 찾으면 영속성 컨텍스트는 원본이 아닌 프록시를 반환한다. (동일성 보장)
- 원본을 먼저 조회하고 프록시를 조회한 경우 : 프록시를 조회하면 영속성 컨텍스트는 프록시가 아닌 원본을 반환한다(동일성 보장)
- 2) 프록시 타입 비교
- 프록시는 원본 엔티티를 상속 받아서 만들어지므로, 프록시로 조회한 엔티티의 타입을 비교할 때는 == 비교 대신 instanceof를 사용한다.
- 다른 클래스에서 만들어졌으므로 == 비교는 안됨
- 프록시는 원본 엔티티의 자식 타입이므로 instanceof는 true를 반환
- 3) 프록시 동등성 비교
- 엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 equals() 메소드를 오버라이딩 하고 비교하면 된다. 그런데 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티라면 문제가 없지만 프록시라면 문제가 발생할 수 잇다.
- 1) 타입 비교는 Instanceof 사용
- 2) 멤버 변수 비교는 직접 접근이 아니라, 접근자 getter를 이용한다. 프록시는 실제 데이터를 가지고 있지 않기 떄문에 프록시의 멤버 변수에 직접 접근하면 아무 값도 조회할 수 없다.
- 4) 상속관계와 프록시
- 1) 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성된다.
- Instanceof 연산을 사용할 수 없다.
- 하위 타입으로 다운 캐스팅을 할 수 없다.
- JPQL로 대상 직접 조회 : 처음부터 자식 타입을 직접 조회해서 필요한 연산을 한다. 다형성을 활용할 수 없다.
- 프록시 벗기기 : 프록시에서 원본 엔티티를 가져와서 사용한다. 영속성 컨텍스트는 한 번 프록시로 노출한 엔티티는 계속 프록시로 노출한다. 그래야 영속성 컨텍스트가 영속 엔티티의 동일성을 보장할 수 있고 클라이언트는 조회한 엔티티가 프록시인지 아닌지 구분하지 않고 사용할 수 있다. 따라서, 이 방법을 사용할 떄는 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용되지 않도록 해야 한다.
- 기능을 위한 별도의 인터페이스 제공
- 비지터 패턴 사용 : Visitor 클래스 - Visitor를 받아들이는 대상 클래스 => 프록시는 단순히 Visitory를 받아들이기만 하고 실제 로직은 Visitor가 처리한다. => 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근하고, instanceof와 타입캐스팅이 필요 없다. 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가할 수 있다.
N+1 문제
@BatchSize : 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해서 조회한다.
@Fetch(FetchMode.SUBSELECT) : 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1 문제를 해결한다.
읽기 전용 쿼리의 성능 최적화
- 엔티티가 영속성 컨텍스트에 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있는 혜택이 많다. 하지만, 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다.
- 읽기 전용 쿼리 힌트 org.hibernate.readOnly : 엔티티를 읽기 전용으로 조회한다. 읽기 전용이므로 영속성 컨텍스트는 스냅샷을 보관하지 않아 메모리 사용량을 최적화할 수 있다. 단, 스냅샷이 없으므로 엔티티를 수정해도 데이터베이스에 반영되지 않는다.
- 읽기 전용 트랜잭션 @Transactional(readOnly=true) : 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL로 설정한다. 이렇게하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다. 따라서 트랜잭션을 커밋해도 영속성 컨텍스트를 플러시하지 않는다. 물론 트랜잭션을 시작했으므로 트랜잭션 시작, 로직 수행, 트랜잭션 커밋의 과정은 이루어진다. 단지 영속성 컨텍스트를 플러시하지 않아 스냅샷 비교와 같은 무거운 로직을 수행하지 않는다.
- 트랜잭션 밖에서 읽기 : 트랜잭션 없이 엔티티를 조회한다. Propagation = Propagation.NOT_SUPPORTED => 기본적으로 플러시모드는 AUTO이지만, 트랜잭션 자체가 존재하지 않으므로 트랜잭션을 커밋할 일이 없다. 그리고 JPQL 쿼리도 트랜잭션 없이 실행하면 플러시를 호출하지 않는다.
배치 처리
- 수백만 건의 데이터를 배치 처리해야 하는 상황에 일반적인 방식으로 엔티티를 계속 조회하면 영속성 컨텍스트에 아주 많은 엔티티가 쌓이면서 메모리 부족 오류가 발생한다.
- 따라서, 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야한다.
- 또한, 2차 캐시를 사용한다면 2차 캐시에 엔티티를 보관하지 않도록 주의해야 한다.
- 수백만 건의 엔티티를 한 번에 등록할 때 주의할 점은 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터베이스에 플러시하고 영속성 컨텍스트를 초기화하는 것이다.
- JPA 등록 배치 : 엔티티를 몇 건 저장할 때마다 플러시를 호출하고 영속성 컨텍스트를 초기화할지 정한다.
- JPA 수정 배치 : 아주 많은 데이터를 조회해서 수정할 때, 수많은 데이터를 한번에 메모리에 올려둘 수 없기 때문에 ‘페이징 처리’와 ‘커서’를 이용한다.
- 페이징 배치 처리
- 커서 : 하이버네이트 scroll
- 하이버네이트 무상태 세션 : 영속성 컨텍스트를 만들지 않고, 2차 캐시도 사용하지 않는다.
트랜잭션을 지원하는 쓰기 지연과 성능 최적화
- 네트워크 호출 한 번은 단순한 메소드 수만번 호출하는 것보다 더 큰 비용이 든다. JDBC가 제공하는 SQL 배치 기능을 이용하면 SQL을 모아서 데이터베이스에 한번에 보낼 수 있다,
- JPA는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다.
- 데이터베이스 테이블 로우에 락이 걸리는 시간을 최소화한다는 점이 진짜 장점이다. 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지 데이터베이스에 데이터 등록, 수정, 삭제를 하지 않는다. 따라서 커밋 직전까지 데이터베이스 로우에 락을 걸지 않는다
- JPA는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보내 데이터베이스에 락이 걸리는 시간을 최소화한다.
- JPA의 쓰기 지연 기능은 데이터베이스에 락이 걸리는 시간을 최소화해서 동시에 더 많은 트랜잭션을 처리할 수 있는 장점이 있다.
트랜잭션 격리 수준
READ UNCOMMITTED
- 커밋하지 않는 데이터를 읽을 수 있다.
- DIRTY READ : 데이터를 수정하고 있는데 커밋하지 않아도 다른 트랜잭션이 수정 중인 데이터를 조회할 수 있다.
- 트랜잭션 2가 DIRTY READ한 데이터를 사용하는데 트랜잭션 1을 롤백하면 데이터 정합성에 심각한 문제가 발생할 수 있다.
READ COMMITTED
- 커밋한 데이터만 읽을 수 있다.
- NON-REPEATABLE READ : 트랜잭션 1이 조회 중인데, 트랜잭션 2가 수정을 하고 커밋하면 트랜잭션 1이 다시 조회했을 때 수정된 데이터가 조회된다. => 반복해서 같은 데이터를 읽을 수 없는 상태
REPEATABLE READ
- 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.
- PHANTOM READ : 반복 조회 시 결과 집합이 달라지는 것
SERIALIZABLE
- 가장 엄격한 트랜잭션 격리 수준
- 동시성 처리 성능이 급격히 떨어질 수 있다.
락
낙관적 락
- 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정
- JPA가 제공하는 버전 관리 기능 사용 (애플리케이션이 제공하는 락)
- 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.
비관적 락
- 트랜잭션의 충돌이 발생한다고 가정하고 락을 거는 방법
- 데이터베이스가 제공하는 락 기능 사용
- ex) select for update 구문
- 데이터베이스 트랜잭션 범위를 넘어서는 문제 => 두 번의 갱신 분실 문제
- 마지막 커밋만 인정하기
- 최초 커밋만 인정하기
- 충돌하는 갱신 내용 병합하기
@Version
- JPA가 제공하는 낙관적 락을 사용하기 위해 버전 관리 기능 추가
- 엔티티에 버전 관리용 필드 추가 후 @Version 어노테이션 추가 => 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가한다.
- 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다.
- 최초 커밋만 인정하기
JPA 락 사용
- READ COMMITTED 트랜잭션 격리 수준 + 낙관적 버전 관리(두 번의 갱신 내역 분실 문제 예방)
- em.lock()
- em.find()
- em.refresh()
- Query.setLockMode()
- @NamedQue
NONE
- 엔티티를 수정해야 버전 체크
- 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야 한다. 조회 시점부터 수정 시점까지를 보장한다.
- 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다. 이 때 데이터베이스의 버전 값이 현재 버전이 아니면 예외가 발생한다.
- 두 번의 갱신 분실 문제 예방
OPTIMISTIC
- 엔티티를 조회만 해도 버전 체크
- 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음 (조회 시점부터 트랜잭션이 끝날 때까지 엔티티가 변경되지 않음을 보장)
- 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티의 버전과 같은지 검증한다. 같지 않으면 예외 발생
- DIRTY READ, NON-REPEATABLE READ 방지
OPTIMISTIC_FORCE_INCREMENT
- 버전 정보 강제 증가
- 논리적 단위의 엔티티 묶음을 관리할 수 있다. 연관 엔티티가 수정될 때 버전 정보를 강제로 증가시킬 수 있다.
- 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다. 이떄 데이터베이스의 버전이 다르면 예외가 발생한다. 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다.
- 강제로 버전을 증가해서 논리적 단위의 엔티티 묶음을 버전 관리할 수 있다.
JPA 비관적 락
- 데이터베이스 트랜잭션 락 매커니즘에 의존
- SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
- 주로 PESSIMISTIC_WRITE 모드 사용
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
PESSIMISTIC_WRITE
- 데이터베이스에 쓰기 락을 건다.
- 데이터베이스 select for update를 사용해서 락을 건다.
- NON-REPEATABLE READ 방지. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.
타임아웃
- 비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기한다.
- 무한정 대기할 수 없으므로 타임아웃 시간을 줄 수 있다.
2차 캐시
기존의 1차 캐시
- 영속성 컨텍스트 내부에 있는 엔티티를 보관하는 저장소
- 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다. OSIV를 사용해도 클라이언트의 요청이 시작하고 끝날때까지만 유효하다.
- 따라서 애플리케이션 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다.
2차 캐시
- 조회한 데이터를 메모리에 캐시해서 데이터베이스 접근 횟수를 줄이면 애플리케이션 성능을 획기적으로 개선할 수 있다.
- 애플리케이션 범위의 캐시인 2차 캐시(공유 캐시)를 지원
1차 캐시
- 영속성 컨텍스트 내부에 존재
- 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장
- 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화한다.
- 스프링 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할 때 영속성 컨텍스트도 종료된다. OSIV를 사용하면 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지한다.
2차 캐시
- 애플리케이션에서 공유하는 캐시 (애플리케이션 범위)
- 애플리케이션을 종료할 때까지 캐시가 유지된다. 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다.
- 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾는다. 2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있다.
- 동작
- 1) 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시를 조회
- 2) 2차 캐시에 엔티티가 없으면 데이터베이스를 조회
- 3) 결과를 2차 캐시에 보관
- 4) 2차 캐시는 자신이 보관하는 엔티티를 복사해서 반환
- 5) 2차 캐시에 저장되어 있는 엔티티를 조회하면 복사본을 만들어 반환
- 복사본을 반환하는 이유
- 캐시한 객체를 직접 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 동시에 수정하는 문제가 발생할 수 있다.
- 객체에도 락을 걸면 동시성이 떨어진다.
- 락에 비하면 객체를 복사하는 비용이 아주 저렴하다.
- 특징
- 영속성 유닛 범위의 캐시
- 조회한 객체의 복사본을 만들어 반환
- 데이터베이스 기본 키를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않는다.
'개발 노트' 카테고리의 다른 글
더블 디스패치 double dispatch (0) | 2020.01.07 |
---|---|
user level lock (0) | 2019.06.12 |
Annotation과 Reflection을 이용한 챗봇 컨트롤러 만들기 (0) | 2019.06.07 |
Entity to DTO, DTO to Entity 그리고 ModelMapper (10) | 2019.05.16 |
Optional 클래스 사용하기 (1) | 2019.05.16 |