Spring

JPA 실무 용어 정리 + 프록시 + OSIV

sehunbang 2024. 3. 6. 19:26

 

EntityManger

엔티티 매니저 란 영속성 컨텍스트 를 관리한다.

 

엔티티 매니저를 통하여 영속성 컨텍스트에 접근할 수 있다. 영속성 컨텍스트는 엔티티에 대한 조회, 수정, 삭제와 같은 API를 제공한다.

 

 

 

PersistentceContext

영속성 컨텍스트는 엔티티를 보관하고 관리한다. 영속성 컨텍스트는

1차 캐시, 쓰기 지연, 더티 체킹과 같은 이점들을 제공

 

 

EntityManagerFactory

엔티티 매니저 팩토리는 애플리케이션 전역으로 하나만 만들고, 요청이 오면 엔티티 매니저를 만들어서 제공하는 역할을 한다.

 

Session vs EntityManager

EntityManager 는 JPA 스펙이고 Session 은 HIbernate에서 제공해주는 API다. 같다고 생각하면 된다.

 

 

 

영속성 컨텍스트 특징

그렇다면 JPA에서는 왜 영속성 컨텍스트라는 개념을 도입했을까?

 

1차 캐시

영속성 컨텍스트는 내부적으로 Map을이용해서 엔티티들을 보관한다

Key는 엔티티에 사용한 @Id 로 매핑한 식별자고, 값은 엔티티다.

 

동일성 보장

앞서 1차 캐시 개념으로 생각해보면 당연한 얘기다.

1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문에 동일성을 보장해줄 수 있다.

 

쓰기 지연

엔티티를 저장하면 엔티티 매니저는 영속성 컨텍스트에만 저장해놓고 실제 데이터베이스에 sql을 날리지 않는다. 트랜잭션이 정상적으로 수행되어 커밋하는 순간 모아둔 insert sql들을 데이터베이스에 날린다. 한 번에 sql을 날리기 때문에 성능상 이점을 가져갈 수 있다. 이것을 쓰기 지연이라한다.

 

 

더티 체킹

기본적으로 데이터에 변경이 있을 경우 `

조회 쿼리 → 수정 → 수정 쿼리`

이런식으로 작업을 진행해야한다.

데이터 수정 쿼리에서도 실수가 발생할 수 있고, 비즈니스 로직이 sql에 의존하게 된다.

JPA에서는 이를 개선하고 더티 체킹이라는 개념을 도입한다.

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태에 대한 `스냅샷`을 갖고있는다.

그리고 flush가 발생하는 순간 스냅샷 시점과, 현재 엔티티를 비교하여 변경된 것이 있는지 확인한다.

 

변경된 엔티티가 있다면, sql을 저장해두고 트랜잭션을 커밋하는 순간 sql을 날린다.

이러한 과정 덕에 JPA에서는 update를 사용하지 않고, 엔티티 데이터만 변경해주기만 하면 된다.

 

@Transactional
public void foo(final Long id) {
  final Member mem = memberRepository.findById(id).orElseThrow(EntityNotFoundException::new);
  member.changeName("foo");
  // memberRepository.save(mem); 이런 코드가 필요 없다.
}

 

 

 

 

 

프록시

jpa에서는 연관 관계 객체를 데이터 조회 시점에 처음부터 조회회는 것이 아니라,

해당 객체가 사용될 시점에 데이터베이스에서 조회할 수 있도록 프록시라는 개념을 사용한다.

 

(FetchType.LAZY랑 FetchType.EAGER)

 JPA에서는 company를 실제 사용되는 시점까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연로딩라 한다.

 

 

하지만 생각해보면 해당 연관관계를 조회하지 않는다고해서 null로 사용할 수는 없다. 따라서 프록시라는 가짜 객체를 넣어주는것이다. 클라이언트 입장에서는 진짜 객체인지 프록시인지 구분하지 않고 사용하기만 하면 된다.

 

@Transactional(readOnly = true)
public void foo(final String name) {
  final User user = userRepository.findByName(name).orElseThrow(EntityExistsException::new);
  log.info("=============================================");
  final Company company = user.getCompany();
  log.info("company location = {}", company.getLocation());
}

 

 

주의 사항

  • JPA 지연로딩 방식은 JPA 구현체에 위임했다. 이는 하이버네이트 구현에 대한 설명이다.
  • 프록시가 초기화 되었다고해서 프록시가 실제 엔티티로 바뀌는 것은 아니다. 프록시를 통해 엔티티에 접근한다.
  • 프록시 초기화는 처음 사용할때 한 번만 진행한다.
  • 프록시 객체는 엔티티를 상속받아서 만든거다. equals 같은 타입 체크가 필요할때 주의해야한다.
  • 준영속 상태의 프록시를 조회할 경우 Lazyinitializationexception이 발생한다.

 

EntityManager & Thread-Safe

우리는 웹 애플리케이션을 EntityManager를 빈으로 주입 받아서 사용한다. 따라서 멀티 스레드 환경에서 동시성 문제가 발생할 수 있다. 스프링에서는 어떻게 스레드 세이프를 보장해줄까?

 

빈으로 주입을 받아서 사용한 적이 없다고 할 수 있다. 그러나 JpaRepository를 사용하면 내부적으로 구현체인 SimpleJpaRepository에서 EntityManager를 주입받아서 사용한다.

 

결론말 말하지만 프록시다. 스프링에서는 엔티티매니저를 프록시로 감싼다. 그 후 EntityManager를 호출할 때 필요에 의해 내부적으로 EntityManager를 생성하는 방식으로 동작한다.

 

스프링에서 이러한 방식으로 동시성 문제를 해결해주기 때문에, 개발자는 동시성에 대한 이슈 없이 EntityManager를 사용할 수 있다.

 

 

OSIV

스프링에서 기본적으로 영속성 컨텍스트의 생존 범위는 트랜잭션 범위와 같다. FLUSH 랑 CLEAR 한다는 뜻

{

앞서 얘기한 ** LazyinitializationException은 위 그림에서 Controller와 같이 영속성 컨텍스트가 끝나는 시점에서 프록시를 초기화할 경우 발생하게 된다.

}

 

트랜잭션이 시작될 때 영속성 컨텍스트를 생성하고 트낼잭션이 끝날 때 영속성 컨텍스트를 종료한다.

 

 

OSIV(Open-Session-In-View) 설정은 영속성 컨텍스트를 뷰까지 열어둔다. OSIV 설정은 Filter, Interceptor 원하는 것을 선택해서 적용할 수 있다.

1. 클라이언트 요청이 들어오면 영속성 컨텍스트를 생성한다. ->

2. 트랜잭션이 정상적으로 끝나면 커밋후 영속성 컨텍스트를 이전과 같이 flush를 한다.

3. 그리고 영속성 컨텍스트를 종료하지 않고 서블릿이나 필터에 요청이 돌아오면 영속성 컨텍스트를 종료한다.

 

트랜잭션이 끝난 프렌젠테이션 레이어 같은곳에서도 영속성 컨텍스트가 살아있다.

하지만 영속성 컨텍스트를 통한 변경은 트랜잭션 안에서 발생해야하기 때문에, 수정은 불가능하다.

트랜잭션은 없지만 트랜잭션 없이 읽기를 사용하여 초기화를 할 수 있다.

 

open session inview 키는법 :

 

 

 

OSIV를 이용하면 Lazy로딩을 적극적으로 활용해서 좋아보일 수 있다. 하지만 단점들이 존재한다.

 

1. 예측 못한 조회 쿼리들이 실행될 수 있다

 

트랜잭션 밖에서 언제든 조회가 생길 수 있기 때문에, 예측 불가능한 쿼리들이 많이 생길 수 있다.

오히려 처음부터 필요한 데이터들을 조회해서 Dto로 반환해서 사용하는 것이 더 효과적이다.

public class UserController {
  public void foo() {
    final User user = userService.getUser();
    log.info("companyLocation = {}", user.getCompany.getLocation()); // sql 발생
  }
}

 

 

2. 여러 트랜잭션에 공유 할 수 있다.

 

영속성 컨텍스트가 트랜잭션 밖에서 수정된 두 다시 트랜잭션에서 다시 들어가면 영속성 컨텍스트를 플러쉬 하기 때문에 예상하지 못한 데이터베이스 변경이 발생할 수 있다.

 

public class UserController {
  public void foo() {
    final User user = userService.getUser();
    user.setName("*****"); //
    userService.doSomething(); // 트랜잭션을 실행하는 다른 비즈니스 로직을 실행
  }
}

 

 

3. 성능 저하

 

데이터베이스 커넥션을 물고 있으므로 처리량이 제한되고 성능 저하고 발생할 수 있다.

 

 

결론

OSIV는 많은 단점들이 존재하여, 안티패턴으로 소개되기도한다.

https://vladmihalcea.com/the-open-session-in-view-anti-pattern/

데이터 조회는 한번에 필요한 것들을 미리 가져올 수 있는것이 좋고,

엔티티를 트랜잭션 범위 밖으로 노출하지 말고

 

Dto로 반환해서 응답하는 것이 좋다.