13.1 트랜잭션 범위의 영속성 컨텍스트
13.1.1 스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 이 전략은 이름 그대로 트랜잭션의 범위와 영속성 컨택스트의 생존범위가 같다는 뜻이다. 좀 더 풀어서 이야기하자면 이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텐스트에 접근한다.
스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜젝션을 시작한다. 외부에서는 단순히 서비스 계층의 메소드를 호출하는 것처럼 보이지만 이 어노테이션이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜젝션 AOP가 먼저 동작한다.
- 스프링 트랜잭션 AOP
- 대상 메소드를 호출하기 직전에 트랜젝션을 실행하고, 대상메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다.
- 이때 중요한일이 일어나는 데, 프랜젝션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋한다.
- 만약 예외가 발생하면 트랜젝션을 롤백하고 종료하는데, 이때는 플러시를 호출하지 않는다.
@Controller
class HelloController {
@Autowired HelloService helloservice;
public void hello() {
//반환된 member엔티티는 준영속 상태다.
Member member = helloService.logic();
}
}
@Service
class HelloService {
@PersistenceContext //엔티티 매니저 주입
EntityManager em;
@Autowired Repository1 repository1;
@Autowired Repository2 repository2;
//트랜잭션 시작
@Transactional
public void logic() {
repository1.hello();
//Member는 영속 상태다.
Member member = repository2.findMember();
return member;
}
//트랜잭션 종료
}
@Repository
class Repository1 {
@persistenceContext
EntityManager em;
public void hello(){
em.xxx(); //A. 영속성 컨텍스트 접근
}
}
@Repository
class REpository2 {
@PersistenceContext
EntityManager em;
public Member findMember() {
return em.find(Member.class, "id1"); //B. 영속성 컨텍스트 접근
}
}
1. HelloService.logic() 메소드에 @Transactional을 선언해서 메소드를 호출할 때 트랜잭션을 먼저 시작한다.
2. repository2.findMember()를 통해 조회한 member 엔티티는 트랜잭션 범위 안에 있으므로 영속성 컨텍스트의 관리를 받는다. 따라서 지금은 영속 상태다.
3. @Transactional을 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데, 이때 영속성 컨텍스트를 종료한다. 영속성 컨텍스트가 사라졌으므로 조회한 엔티티는 이제부터 준영속 상태가 된다.
4. 서비스 메소드가 끝나면서 트랜젝션과 영속성 컨텍스트가 종료되었다. 따라서 컨트롤러에 반환된 member엔티티는 준영속상태다.
- 트랜잭션이 같으면 영속성 컨텍스트를 사용한다.
- 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜젝션이 같으면 항상 같은 영속성 컨텍스트를 사용한다. 예제에서 엔티티 매니저를 사용하는 A, B 코드는 모구 같은 트랜잭션 범위에 있다. 따라서 엔티티 매니저는 달라도 같은 영속성 컨텍스트를 사용한다.
- 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
- 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
- 스프링 컨테이너는 스레드마다 각각 다른 트랜젝션을 할당한다.
13.2 준영속 상태와 지연로딩
스프링이나 J2EE 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 그리고 트랜잭션을 보통 서비스 계증에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다. 따라서 조회한 엔티티가 서비스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속상태를 유지하지만 컨트롤러나 뷰같은 프레젠테이션 계층에서는 준영속 상태가 된다.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) //지연로딩전략
private Memember member; //주문회원
}
컨테이너 환경의 기본전략인 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프리젠테이션 계층에서 엔티티는 준영속 상태다. 따라서 변경감지와 지연로딩이 동작하지 않는다.
class OrderController {
public String view(Long orderId) {
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); //지연 로딩 시 예외발생
}
}
-> 지연로딩 시점에 예외가 발생한다.
- 준영속 상태와 변경감지
변경감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다. 보통 변경감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다.
- 준영속 상태와 지연로딩
뷰를 렌더링할 때 연관된 엔티티도 함께 사용해야 하는데 연관된 엔티티를 지연 로딩으로 설정해서 프록시 객체로 조회할 때, 영속성 컨텍스트가 없으므로 지연로딩을 할 수 없다. -> org.hibernate.LazyInitializationException 예외가 발생한다.
이를 해결하는 방법
-> 뷰가 필요한 엔티티를 미리 로딩하는 방법
-> OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법