13.1 트랜잭션 범위의 영속성 컨텍스트
- J2SE 환경에서 JPA 사용 시 개발자가 직접 엔티티 매니저를 생성하고 트랜잭션도 관리해야함.
- 스플이 또는 J2SE 컨테이너 환경 → JPA 사용 시, 컨테이너가 제공하는 전략 따라야함.
(1) 스프링 컨테이너의 기본 전략
트랜잭션 범위의 영속성 컨텍스트 전략:
트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 동일함.
즉, 트랜잭션 시작 시, 영속성 컨텍스트를 생성하고 트랜잭션 종료 시 영속성 컨텍스트를 종료함. 또한, 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근함.
- 스프링 프레임워크의 경우 비즈니스 로직을 시작하는 서비스 계층에
@Transactional 어노테이션을 선언해 트랜잭션을 시작
- 메소드 실행 직전 스프링의 트랜잭션 AOP가 먼저 동작함
- 대상 메소드 정상 종료 시에는 트랜잭션을 커밋하면서 종료
- 트랜잭션 커밋 시:
- JPA는 영속성 컨텍스트를 플러시해 변경 내용을 데이터베이스에 반영한 후, 트랜잭션을 커밋.
- 즉, 영속성 컨텍스트의 변경 내용이 정상 반영됨
- 예외 발생 시:
- 트랜잭션을 롤백하고 종료. 이때는 플러시를 호출하지 않음
예제 분석
@Controller
class HelloController {
@Autowired HelloService helloService;
public void hello() {
Member member = helloService.logic();
}
}
@Service
class HelloService {
@PersistenceContext
EntityManager em;
@Autowired Repository1 repository1;
@Autowired Repository2 repository2;
**@Transactional**
public void logic() {
repository.hello();
Member member = repository2.findMember();
return member;
}
}
@Repository
class Repository1 {
@PersistenceContext
EntityManager em;
public void hello() {
em.xxx();
}
}
@Repository
class Repository2 {
@PersistenceContext
EntityManager em;
public Member findMember() {
return em.find(Member.class, "id1");
}
}
@PersistenceContext 어노테이션: 스프링 컨테이너가 엔티티 매니저를 주입
HelloService.logic()
@Transactional을 선언해 메소드 호출 시, 트랜잭션을 우선 시작
repository2.findMember()를 통해 조회한 member 엔티티는 트랜잭션의 범위 내에 존재, 따라서 영속성 컨텍스트의 관리를 받음 → 즉, 현재 영속 상태.
@Transactional 선언 메소드 정상 종료 시, 트랜잭션을 커밋. 이때 영속성 컨텍스트 역시 종료. 따라서 조회한 엔티티는 준영속 상태가 됨.
트랜잭션이 같으면 같은 영속성 컨텍스트 사용
- 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용함
- 위 코드의 두 번의 영속성 컨텍스트 접근 모두 동일한 영속성 컨텍스트를 사용함.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용
- 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름
- 즉, 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당해 같은 엔티티 매니절ㄹ 호출해도 접근하는 영속성 컨텍스트가 달라, 멀티 스레드 상황에서도 안전함.
13.2 준영속 상태와 지연 로딩
- 조회한 엔티티가 서비스와 레포지토리 계층에서는 영속성 컨텍스트에 관리되며 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 됨.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne (fetch = FetchType.LAZY)
private Member member;
}
- 프레젠테이션 계층에서 엔티티가 준영속 상태 ⇒ 변경 감지와 지연로딩이 동작하지 않음.
- 즉, 지연 로딩 시점에서 예외 발생 가능
준영속 상태와 변경 감지
- 변경감지기능의 경우 데터를 주로 보여주는 역할인 프레젠테이션 계층에서는 거의 발생하지 않음.
- 책임의 모호성이 발생하지 않게하기 위해서라도, 프레젠테이션 계층에서 데이터를 변경하지 안않고, 서비스 계층에서 비즈니스 로직을 처리하는 것이 좋음
준영속 상태와 지연 로딩
- 뷰를 렌더링 할 때, 연관된 엔티티 역시 함께 사용해야하는데 이때, 연관 엔티티를 지연로딩으로 설정해 프록시 객체로 조회했을 때 ⇒ 아직 초기화하지 않은 프록시 객체 사용 시, 실제 데이터를 불러오기 위해 초기화를 시도
- 하지만 준영속 상태는 영속성 컨텍스트가 없기에 지연 로딩이 불가 → 예외가 발생함
- 해결 방법:
- 뷰가 필요한 엔티티를 미리 로딩하기
- OSIV를 사용해 엔티티를 항상 영속 상태로 유지하기
해결 방법1: 뷰가 필요한 엔티티를 미리 로딩
: 엔티티가 준영속 상태여도, 미리 로딩하거나 초기화해 반환하기에 지연 로딩이 발생하지 않음.
어디에서 미리 로딩하는지에 따라 3가지 방법이 존재
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제 초기화
(1) 글로벌 페치 전략 수정
글로벌 페치 전략을 아래처러 지연 로딩에서 즉시로딩으로 변경
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
private Member member;
}
- fetch 타입을 변경하면 애플리케이션 전체에서 해당 엔티티를 로딩할 때마다 해당 전략을 사용 ⇒ 그렇기에 글로벌 페치 전략이라고 부름
- 단점:
- 사용하지 않는 엔티티를 로딩해야함
- N+1 문제가 발생함
(2) JPQL 페치 조인
- JPQL 호출 시점에 함께 로딩할 엔티티를 선택하는 방식
- 글로벌 페치 전략을 즉시 로딩으로 설정할 시 애플리케이션 전체에 영향을 줘 비효율적.
페치 조인 사용 전
JPQL: select o from Order o
SQL: select * from Order
페치 조인 사용 후
JPQL:
select o
from Order o
**join fetch** o.member
- 페치 조인 사용 시, SQL JOIN을 사용해 페치 조인 대상까지 함께 조회 → N+1 문제가 발생하지 않음. (연관 엔티티를 이미 로딩했기에 글로벌 페치 전략은 무의미.)
- 단점
- 무분별하게 사용 시, 화면에 맞춘 레포지토리 메소드가 증가 → 프레젠테이션 계층이 데이터 접근 계층을 침법하는 것
(3) 강제 초기화
- 영속성 컨텍스트가 살아있을 때, 프레젠테이션 계층이 필요한 엔티티를 강제 초기화해 반환하는 방법
class OrderService {
@Transactional
public Order findOrder (Long id) {
Order order = orderRepository.findOrder(id);
**order.getMember().getName();
return order;
}
}
- 프레젠테이션 계층에서 필요한 프록시 객체를 영속성 컨텍스트가 살아 있을 때, 강제로 초기화해서 반환하면 이미 초기화했기에 준영속 상태에서도 사용 가능
- 하이버네이트 사용 시,
initialize() 메소드를 사용해 프록시를 강제로 초기화 가능
- JPA 표준에는 프록시 초기화 메소드가 없으며, 초기화 여부만 확인 가능
(4) FACADE 계층 추가
- 프레젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법
- 뷰를 위한 프록시 초기화는 서비스 계층이 아니라 FACADE 계층에서 담당
FACADE 계층의 역할과 특징
-
프레젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리
-
프레젠테이션 계층에서 필요한 프록시 객체를 초기화
-
서비스 계층을 호출해 비즈니스 로직을 실행
-
레포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾음
예제코드
class OrderFacade {
@Autowired OrderService orderService;
public Order findOrder(Long id) {
Order order = orderService.findOrder(id);
order.getMember().getName();
return order;
}
}
class OrderService {
public Order findOrder(id) {
return orderRepository.findOrder(id);
}
}
- 이전에 OrderService에 있던 프록시 초기화 코드가 OrderFacade로 이동함
준영속 상태와 지연 로딩 문제점
- 뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류 발생 가능성이 높음
- 이유: 뷰 개발 시에는 엔티티 클래스를 보고 개발을 하는 것이지, 초기화 여부 확인을 위해 FACADE나 서비스 클래스까지 열어보는 것이 번거롭고 놓치기 쉬워서
- 이런 단점을 해결하는 방법이 OSIV.
13.3 OSIV
OSIV: 영속성 컨텍스트를 뷰까지 열어둔다는 의미.
- 영속성 컨텍스트가 살아있으면 엔티티가 영속 상태로 유지되기 때문에, 뷰에서 지연로딩을 사용할 수 있음
(1) 과거 OSIV: 요청 당 트랜잭션
- 단순한 구현방법: 클라이언트의 요청이 들어오자마자, 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션 역시 종료하는 것 ⇒ 이것을 요청 당 트랜잭션 방식의 OSIV라함.
- 영속성 컨텍스트가 처음부터 끝까지 살아있기에 조회한 엔티티도 영속 상태를 유지함
- 뷰에서도 지연로딩이 가능하기에 엔티티를 미리 초기화할 필요가 없음
문제점
- 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있음 예) 보안상의 이유로 고객의 이름을 XXX로 변경해 뷰에 전달 → 뷰 렌더링 이후, 트랜잭션을 커밋해 고객의 이름이 변경된 고객의 이름 XXX로 변경되는 문제가 발생함.
- 프레젠테이션 계층에서 데이터 수정을 막는 방법:
- 엔티티를 읽기 전용 인터페이스로 제공:
- 엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프레젠테이션 계층에 제공
- 엔티티 래핑
- 엔티티의 읽기 전용 메소드만 가진 엔티티를 감싼 객체를 만들고 이것을 프레젠테이션 계층에 반환하는 방법
- DTO만 반환
- 가장 전통적인 방법으로 프레젠테이션 계층에 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 것
- 단점: OSIV를 사용하는 장점을 살릴 수 없고, 엔티티를 거의 복사하는 듯한 DTO 클래스를 만들어야함
(2) 스프링 OSIV: 비즈니스 계층 트랜잭션
스프링 프레임워크가 제공하는 OSIV 라이브러리
OSIV를 서블릿 필터에서 적용할지, 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택해 사용하면 됨.
- 하이버네이트 OSIV 서블릿 필터
- 하이버네이트 OSIV 스프링 인터셉터
- JPA OEIV 서블릿 필터
- JPA OEIV 스프링 인터셉터
스프링 OSIV 분석
- 스프링 프레임워크의 OSIV = 비즈니스 계층에서 트랜잭션을 사용하는 OSIV. ⇒ 즉, OSIV는 사용하지만, 트랜잭션은 비즈니스 계층에서만 사용함
동작 원리
- 클라이언트 요청이 들어오면 영속성 컨텍스트를 생성 (이때 트랜잭션은 시작X)
- 서비스 계층에서 트랜잭션 시작 → 생성해둔 영속성 컨텍스트에 트랜잭션을 시작
- 비즈니스 로직을 실행하고 서비스 계층 종료 후, 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시
- 이때 트랜잭션만 종료하고, 영속성 컨텍스트는 살려둠
- 클라이언트의 요청이 끝날 때, 영속성 컨텍스트를 종료
트랜잭션 없이 읽기
- 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야함
- 만약, 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 예외 발생
- 트랜잭션 없이 읽기: 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이를 이르는 말.
- 프레젠테이션 계층에서는 트랜잭션이 없기에, 엔티티를 수정할 수 없음. → 이전의 단점 보완.
- 트랜잭션 없이 읽기를 사용해 프레젠테이션 계층에서 지연된 로딩 사용 가능
스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV 정리
- 영속성 컨텍스트를 프레젠테이션 계층까지 유지
- 프레젠테이션 계층에는 트랜잭션이 없으므로 엔티티 수정 불가
- 프레젠테이션 계층에는 트랜잭션이 없지만, 트랜잭션 없이 읽기를 사용해 지연 로딩 가능
스프링 OSIV 주의사항
- 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층 호출 시 문제가 발생
13.4 너무 엄격한 계층
아래 예제는 상품 구매 후 구매 결과 엔티티를 조회하기 위해 컨트롤러에서 레포지토리를 직접 접근
class OrderController {
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
public String orderRequest (Order order, Model model) {
long Id = orderService.order(order);
Order orderResult = orderRepository.findOne(id);
model.addAttribute("order", orderResult);
...
}
}
@Transactional
class OrderService {
@Autowired OrderRepository orderRepository;
public Long order(order) {
return orderRepository.save(order);
}
}
class OrderRepository {
@PersistenceContext EntityManager em;
public Order findOne(Long id) {
return em.find(Order.class, id);
}
}
- OSIV 사용 이전: 프레젠테이션 계층에서 사용할 지연 로딩 엔티티를 미리 초기화 해야했음
- 초기화: 서비스 계층/FACADE 계층이 담당 (영속성 컨텍스트가 살아있는 계층)
⇒ OSIV 사용: 영속성 컨텍스트가 프레젠테이션 계층까지 살아있기에 미리 초기화할 필요X
- 단순한 엔티티 조회의 경우, 컨트롤러에서 레포지토리 직접 호출해도 문제 없음
- OSIV를 사용하면 과거 EJB 시절 DTO를 만들어 반환하고, 엔티티가 계층을 뛰어넘기 힘들다는 불편함을 줄이고 더 유연하고 실용적인 관점으로 접근할 수 있음.