본문 바로가기

Back-End/JPA

13장. 웹애플리케이션과 영속성 관리

JPA를 이용하여 Spring 개발을 하다보면, LazyInitializationException 같은 예외를 겪어 봤을 것이다.

 

나도 JPA의 내부동작방식을 이해하지 못하고 있었기에 어떤 문제인지 파악을 하지 못하였다.

 

그래서 13장. 웹 어플리케이션과 영속성 관리를 읽게 되었다.

 

트랜잭션 범위의 영속성 컨텍스트 ( persistence context in transaction scope )

스프링 컨테이너의 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용 한다.

 

즉, 트랜잭션이 시작될 때 영속성 컨텍스트를 생성하고 

 

트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 

 

그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.

@Transactional 어노테이션을 선언하면 트랜잭션이 시작된다.

 

1) 스프링 트랜잭션 AOP가 대상 method를 호출하기 직전에 트랜잭션 시작

2) 트랜잭션을 커밋하면, flush() 호출을 통해 변경된 내용 DB에 반영 -> 트랜잭션 커밋

3) 문제가 생기면, flush()를 호출하지 않음

 

위의 과정을 나타내주는 예제이다.

준영속 상태의 지연 로딩 ( lazy loading of detached state )

컨트롤러 계층에서는 트랜잭션이 없기 때문에 준영속 상태이다.

 

그래서 영속성 상태에서 가능한 기능을 사용할 수 없다

  • 변경 감지 ( dirty checking )
    보통 presentation 계층에서 데이터를 수정하는 일이 없기 때문에 특별히 문제가 되지 않는다.
  • 지연 로딩 ( lazy loading )
    아래와 같이 지연로딩으로 프록시 객체를 조회하면 org.hibernate.LazyInitializationException이 발생한다.

그렇다면 Controller 단에서 지연로딩을 사용하기 위해서는 어떻게 해야 할까?

 

1. 글로벌 패치 전략 수정

즉시 모든 것을 로딩 하는 방법이다.

하지만 해당 방법은 2가지 문제점이 있다.

  • 사용하지 않는 Entity까지 로딩한다.
  • N+1문제

    JPA를 사용하면서 가장 조심해야 하는 문제이다.
    JPQL을 사용하는 경우 다음과 같이 조회된다.
    select * from Order;
    select * from Member where id =?
    select * from Member where id =?
    .....

    SQL을 N+!회 호출하게 되어 성능상의 문제가 발생한다.

2. JPQL 패치 조인

위의 N+1문제를 해결 할 수 있는 방법이다.

 

join fetch o.member

와 같이 fetch를 넣어주면 된다.

 

하지만 이 또한 문제가 있다.

  • 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범함
    - findOrder(), findOrderWithMember().... 메소드 계속 생성해야 함
    - 다른 대안 : findOrder()에서 order, member를 함께 로딩하도록 수정 
       로딩 시간이 조금은 길어지겠지만 무분별한 최적화로 인한 의존관계 발생보다는 이게 낫다 ( 타협 )

3. 강제로 초기화

영속성 컨텍스트가 살아있는 Service 단에서 강제로 초기화하여 반환을 하는 방법이다.

  • 문제 1 : 프록시 초기화 코드가 서비스에 생김 ( 프리젠테이션 계층이 침범하는 상황 )
  • 문제 2 : FACADE 계층을 추가한다고 해도 쓸데없는 코드(계층)이 생성 됨

이렇게 하면 문제가 해결 되는가?

이 방법들은 억지로 해결하는 것이다.

 

Controller ( 프리잰테이션 계층)을 개발하면서 Entity가 초기화 되어 있는지 아닌지 확인을 하지 않는다. 결국 실수를 하게 된다.

 

그리고 쓸데 없는 코드도 생기는 문제가 발생한다.

 

그렇다면 영속성 컨텍스트 ( persistence context )를 뷰까지 살아있게 열어두자. 이것이 OSIV이다.

 

OSIV ( Open Session In View )

영속성 컨텍스트를 뷰까지 열어둔다. 영속성 컨텍스트가 살아있으면 엔티티는 영속상태로 유지된다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.

 

과거 OSIV : 요청 당 트랜잭션

아래와 같이 처음부터 요청당 트랜잭션을 만들면 위에서 생겼던 문제가 해결이 된다. 하지만 치명적인 문제가 있다.

  • 문제점 : 프리젠테이션 계층에서 보안상의 이유로만 데이터를 수정한다면? 의도치 않게 dirty check가 되어 데이터베이스의 값까지 바뀌게 될것이다.... 이렇게 되면 도대체 어떻게 코딩하라는거야....

  • 해결책 : DTO만 반환 -> 하지만 코드 증가...
스프링 OSIV : 비즈니스 계층 트랜잭션

 

  1. 클라이언트 요청이 들어옴 -> 영속성 컨텍스트 생성 / 트랜잭션 시작 x
  2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 생성된 영속성 컨텍스트를 가져옴
  3. 서비스 계층 끝 -> 트랜잭션 commit(), 영속성 컨텍스트 flush(), 하지만 영속성 컨텍스트 종료는 안함
  4. 영속성 컨텍스트는 살아있으므로 컨트롤러에서도 엔티티는 영속 상태 유지
  5. 서블릿 필터나 스프링 인터셉터로 돌아오면 영속성 컨텍스트 종료, flush() 호출하지 않고 종료

트랜잭션 없이 읽기 ( Nontransactional reads )

그럼 이제 컨트롤러 단에서도 lazy loading이 가능하다. 그리고 만약 여기서 setName()과 같이 값을 변경한다면?

 

당연히 저장되지 않는다.

  • 위에서 살펴본 5번 과정에 나온 것 처럼 flush()를 호출하지 않기 때문이다.
  • 만약 강제로 flush() 코드를 넣는다면? javax.persistence.TransactionRequiredException이 발생한다.