스프링 환경에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성을 관리해준다.
컨테이너 환경에서 동작하는 JPA의 내부 동작 방식을 이해하지 못하면 문제가 발생했을 때 해결이 쉽지 않다.
1. 트랜잭션 범위의 영속성 컨텍스트
1.1. 스프링 컨테이너의 기본 전략
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
- 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다
- 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
- 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다
@Transactional
어노테이션이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션AOP
가 먼저 동작한다.- 스프링 트랜잭션
AOP
는 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다.- 트랜잭션 커밋 → 영속성 컨텍스트 플러시 (변경내용 DB 반영) → DB트랜잭션 커밋
- if 예외가 발생하면 트랜잭션을 롤백하고 종료 (플러시를 호출하지 않음)
코드 예제
더보기
@Contorller
class HelloController {
@Autowired HelloService helloService;
public void hello() {
//반환된 member 엔티티는 준영속 상태 -- 4
Member member = helloService.logic();
}
}
@Service
class HelloService {
@PersistenceContext //엔티티 매니저 주입
EntityManager em;
@Autowired Repository1 repository1;
@Autowired Repository2 repository2;
//트랜잭션 시작 -- 1
@Transactional
public void logic() {
repository1.hello();
//member는 영속 상태 -- 2
Member member = repository2.findMember();
return member;
}
//트랜잭션 종료 -- 3
}
@Repository
class Repository1 {
@PersistenceContext
EntityManager em;
public void hello() {
em.xxx(); //A. 영속성 컨텍스트 접근
}
}
@Repository
class Repository2 {
@PersistenceContext
EntityManager em;
public Member member() {
return em.find(Member.class, "id1"); //B. 영속성 컨텍스트 접근
}
}
트랜잭션이 같으면 같은 영속성 컨테스트를 사용한다
- 엔티티 매니저는 달라도 같은 트랜잭션 범위안에 있다면 같은 영속성 컨텍스트를 사용한다.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다
- 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
- 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당
- 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티스레드 상황에서 안전하다.
2. 준영속 상태와 지연 로딩
- 트랜잭션은 보통 서비스 계층에서 시작되므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료
- 서비스와 리포지토리 계층에서는 영속 상태를 유지
- 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태
- 프리젠테이션 계층에서 준영속 상태이기 때문에 변경감지와 지연로딩이 동작하지 않는다.
@Entity
public class Order {
@Id
@GeneratedValue
private Long Id;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}
// 컨트롤러 로직
class OrderController {
public String view(Long orderId) {
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); //지연 로딩 시 예외 발생
}
}
준영속 상태와 변경 감지
- 변경 감지 기능은 영속성 컨텍스트가 살아있는 서비스 계층까지만 동작한다.
- 변경 감지 기능이 프리젠테이션 계층에서도 동작하면 어플리케이션 계층이 가지는 책임이 모호해지고, 유지보수가 어렵다.
준영속상태와 지연로딩
- 프리젠테이션 계층에서 연관된 엔티티를 사용하려고 할 때 준영속 상태라 지연 로딩을 할 수 없는 문제가 발생
- 해결 방법 2가지
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
- OSIV를 사용해서 엔티티를 항상 영속상태로 유지하는 방법
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
2.1 글로벌 페치 전략 수정
- 가장 간단한 방법 - 글로벌 페치 전략을 지연로딩에서 즉시로딩으로 변경
- 즉시 로딩 전략으로 설정하면 연관된 엔티티도 항상 함께 로딩한다.
@Entity
public class Order {
@Id
@GeneratedValue
private Long Id;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 전략
private Member member;
}
글로벌 페치 전략에 즉시로딩 사용시 단점
- 사용하지 않는 엔티티를 로딩한다
- N+1 문제가 발생한다 - JPQL 페치 조인으로 해결 가능
2.2 JPQL 페치 조인
- 페치 조인을 사용하면 N+1 문제를 해결하면서 연관된 엔티티를 함께 로딩한다.
- 페치 조인은 조인 명령어 마지막에
fetch
를 넣어주면 된다 select o from Order o join fetch o.member
JPQL 페치 조인의 단점
- 페치 조인을 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있다. 결국 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범하는 것이다.
- 화면 A를 위해 order만 조회하는
repository.findOrder()
메소드 - 화면 B를 위해 order와 연관된 member를 페치 조인으로 조회하는
repository.findOrderWithMember()
메소드
- 화면 A를 위해 order만 조회하는
- 무분별한 최적화로 프리젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것 보다는 적절한 선에서 타협점을 찾는 것이 합리적이다.
2.3 강제로 초기화
- 영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법
class OrderService {
@Transactional
public Order findOrder(Long Id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체를 강제로 초기화
return order;
}
- 프록시 객체는 실제 사용하는 시점에 초기화
- 하이버네이트를 사용하면
initialize()
메소드 사용해서 강제로 초기화 가능
- 하이버네이트를 사용하면
- 손 쉽게 뷰에서 필요한 연관 관계를 넣어서 반환해 줄 수 있지만, 프레젠테이션 계층이 서비스 계층을 침범하고 있다.
- 따라서 비즈니스 로직을 담당하는 서비스 계층과 프레젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. 이때 FACADE 계층이 사용된다.
2.4 FACADE 계층 추가
- 프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법
FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적인 의존성을 분리해준다.
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.
- 서비스 계층을 호출해서 비즈니스 로직을 실행한다
- 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
class OrderFacade {
@Autowired
private OrderSerivce orderService;
public Order findOrder(Long id) {
Order order = orderService.findOrder(id);
order.getMember().getName(); // 프록시 객체 강제로 초기화
return order;
}
}
class OrderService {
public Order findOrder(Long Id) {
Order order = orderRepository.findOrder(id);
return order;
}
- 실용적인 관점에서 중간에 계층이 하나 더 끼어든다는 단점
- 단순히 서비스 계층을 호출만 하는 위임코드가 상당히 많은 문제
2.5 준영속 상태와 지연 로딩의 문제점
- 뷰를 개발할 때 필요한 엔티티를 미리 초기화 하는 방법은 생각보다 오류가 발생할 가능성이 높다. 초기화여부를 확인하기 위해 FACADE나 서비스 클래스를 열어보는 일은 상당히 번거롭다.
- 어플리케이션 로직과 뷰가 물리적으로는 나누어져 있지만 논리적으로는 서로 의존한다는 문제가 있다.
- 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 그러므로 영속성 컨텍스트를 뷰까지 살아있게 열어둘 수 있는데, 이것이 OSIV이다.
3. OSIV
- OSIV (Open Session In View)는 영속성 컨텍스트를 뷰까지 열어 둔다는 뜻이다. 뷰에서도 엔티티가 영속상태이므로 지연로딩을 사용할 수 있다.
- OSIV는 하이버네이트에서 부르는 용어고, JPA에서는 OEIV라고 부른다. 하지만 둘 다 관례상 OSIV라고 부른다.
3.1 과거 OSIV : 요청 당 트랜잭션
- 요청 당 트랜잭션 : 클라이언트 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션을 끝내는 것
- 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 프리젠테이션 계층에서도 지연로딩 사용이 가능
요청 당 트랜잭션 방식의 OSIV 문제점
- 프리젠테이션 계층이 엔티티를 변경할 수 있으므로, 프레젠테이션 계층에서 변경한 사항이 데이터베이스에도 적용이 되는 심각한 문제가 발생한다.
- 해결 방법은 아래 3가지
엔티티를 읽기 전용 인터페이스로 제공
- 엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법
interface MemberView {
public String getName();
}
@Entity
class Member implements MemberView {
...
}
class MemberService {
public MemberView getMember(id) {
return memberRepository.findById(id);
}
}
엔티티 래핑
- 엔티티의 읽기 전용 메소드만 가지고 있는 엔티티를 감싼 객체를 만들고 이것을 프리젠테이션 계층에 반환
class MemberWarpper {
private Member member;
public MemberWrapper(member) {
this.member = member;
}
//읽기 전용 메소드만 제공
public String getName() {
return member.getName();
}
}
DTO만 반환
- 프리젠테이션 계층에 DTO를 생성해서 반환
- 하지만 OSIV의 장점을 살릴 수 없고 엔티티를 거의 복사한듯한 DTO 클래스도 하나 더 만들어야 한다.
요청 당 트랜잭션 방식의 OSIV는 최근에는 거의 사용하지 않고 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.
3.2 스프링 OSIV : 비즈니스 계층 트랜잭션
- 스프링 프레임워크는 다양한 OSIV 클래스를 제공한다.
- 하이버네이트 OSIV 서블릿 필터:
org.springframework.orm.hibernate4.support.OpenSessionInViewFilter
- 하이버네이트 OSIV 스프링 인터셉터:
org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor
- JPA OEIV 서블릿 필터:
org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
- JPA OEIV 스프링 인터셉터
org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor
- 하이버네이트 OSIV 서블릿 필터:
스프링 OSIV 분석
- 스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이다.
- 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단, 이때 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때, 1번에서 미리 생성해 둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝나지만 영속성 컨텍스트는 그대로 유지된다.
- 컨트롤러와 View까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.
트랜잭션 없이 읽기
- 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기라 한다.
- 프록시를 초기화하는 지연로딩도 조회기능이므로 트랜잭션 없이 읽기가 가능하다.
- 정리하면, 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티 조회, 수정 가능 / 트랜잭션 범위 밖에서 엔티티 조회 가능
컨트롤러에서는 플러시가 동작하지 않는 이유?
- 서비스 게층이 끝날 때 트랜잭션이 커밋되면서 이미 플러시해버렸다.
- 스프링이 제공하는 OSIV서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고
em.close()
로 영속성 컨텍스트만 종료해 버리므로 플러시가 일어나지 않는다 . - 프리젠테이션 계층에서
em.flush()
를 호출해서 강제로 플러시해도 트랜잭션 범위 밖이이므로 데이터를 수정할 수 없다는 예외를 만난다. - 따라서 프리젠테이션 계층에서 엔티티를 수정해도 수정 내용을 DB에 반영하지 않는다.
스프링 OSIV 주의사항
- 프리젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생
class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경했다.
memberService.biz(); // 비즈니스 로직
return "view";
}
}
biz()
메소드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 변경 감지가 동작하면서 회원 엔티티의 수정 사항을 DB에 반영한다- 해결 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.
4. 너무 엄격한 계층
- OSIV를 사용하게 되면 영속성 컨텍스트가 프레젠테이션 계층까지 살아있으므로 미리 초기화 할 필요가 없다.
- 따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 상관 없다.
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 14장 - 컬렉션과 부가 기능 (0) | 2023.07.27 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 12장 - 스프링 데이터 JPA (0) | 2023.07.26 |
[자바 ORM 표준 JPA 프로그래밍] 10.4 - QueryDSL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 10.2 - JPQL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입 (0) | 2023.07.22 |