웹 애플리케이션과 영속성 관리
트랜잭션 범위의 영속성 컨텍스트
순수하게 J2SE 환경에서 JPA를 사용하면 개발자가 직접 EntityManager
를 생성하고 트랜잭션도 관리해야 한다. 하지만 스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 한다.
스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
@Transactional
애너테이션이 있으면 호출한 메서드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다. 스프링 트랜잭션 AOP는 대상 메서드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메서드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다.
-
트랜잭션이 같으면 같은 영속성 컨텍스트를 사용하지만 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
-
여러 스레드에서 동시에 요청이 와서 같은
EntityManager
를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다. -
스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다.
준영속 상태와 지연 로딩
스프링이나 J2EE 컨테이너는 일반적으로 트랜잭션이 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다. 조회한 엔티티가 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다.
class OrderController {
public String view(Long orderId) {
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); // 지연 로딩 시 예외 발생
...
}
}
만약 프레젠테이션 계층에서도 영속성 컨텍스트가 동작하게 되면 예외는 발생하지 않겠지만 애플리케이션 계층이 가지는 책임이 모호해지고 무엇보다 데이터를 어디서 어떻게 변경했는지 프레젠테이션 계층까지 찾아야하므로 애플리케이션 유지보수가 어려워진다.
뷰가 필요한 엔티티를 미리 로딩해두는 방법
준영속 상태의 지연 로딩 해결 전략으로 뷰가 필요한 엔티티를 미리 로딩해두는 방법이 있다.
1. 글로벌 페치 전략 수정
글로벌 로딩 전략을 지연 로딩에서 즉시 로딩으로 변경하면 된다. 그러나 이 때 아래와 같은 문제가 일어날 수 있다.
-
사용하지 않는 엔티티를 로딩한다.
-
N+1 문제가 발생한다.
2. JPQL 페치 조인
즉시 로딩으로 N+1 문제가 발생하게 된다면 JPQL로 페치 조인을 사용하자.
페치 조인이 현실적인 대안이지만 무분별하게 사용하면 아래와 같이 화면에 맞춘 레포지토리 메서드가 증가 할 수 있다. 그래서 프레젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범하게 된다.
- 화면 A를 위해 order만 조회하는
repository.findOrder()
메서드 - 화면 B를 위해 order와 연관된 member를 페치 조인으로 조회하는
repository.findOrderWithMember()
메서드
3. 강제로 초기화
영속성 컨텍스트가 살아있을 때 프레젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.
class OrderService {
@Transactional
public Order findOrder(Long id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체를 강제로 초기화
return order;
}
}
위 해결법도 프레젠테이션 계층이 서비스 계층을 침범하는 상황이다. 따라서 비즈니스 로직을 담당하는 서비스 계층에서 프레젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. 해결법으로는 계층 사이에 FACADE 계층을 두어 그 역할을 담당하게 할 수 있다.
4. 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(Long id) {
return orderRepository.findOrder(id);
}
}
FACADE 계층이 추가되면 서비스 계층은 비즈니스 로직에 집중하고 프레젠테이션 계층을 위한 초기화 코드는 모두 FACADE가 담당하면 된다. 하지만 실용적인 관점에서 볼 때 중간에 계층이 하나 더 끼어든다는 단점이 생긴다.
OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
준영속 상태의 지연 로딩 해결 전략으로 OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법이 있다.
OSIV(Open Session In View)
는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻으로 영속성 컨텍스트가 살아있으므로 엔티티는 영속상태로 유지된다. 따라서 뷰에서 지연 로딩을 사용할 수 있다.
과거 OSIV : 요청 당 트랜잭션
요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다.
요청 당 트랜잭션 방식의 OSIV는 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다는 문제점이 있다. 그래서 프레젠테이션 계층에서 엔티티를 수정하지 못 하게 막으려면 엔티티를 읽기 전용 인터페이스로 제공하거나 엔티티 래핑, DTO 반환 방식을 사용해야 한다.
1. 엔티티를 읽기 전용 인터페이스로 제공
엔티티를 직접 노출하는 대신 읽기 전용 메서드만 제공하는 인터페이스를 프레젠테이션 계층에 제공하는 방법.
interface MemberView {
public String getName();
}
@Entity
class Member implements MemberView {
...
}
class MemberService {
public MemberView getMember(Long id) {
return memberRepository.findById(id);
}
}
2. 엔티티 래핑
엔티티의 읽기 전용 메서드만 가지고 있는 엔티티를 감싼 객체를 만들고 이 객체를 프레젠테이션 계층에 반환하는 방법.
class MemberWrapper {
private Member member;
public MemberWrapper(Member member) {
this.member = member;
}
// 읽기 전용 메서드
public String getName() {
return member.getName();
}
}
3. DTO 반환
가장 전통적인 방법으로 프레젠테이션 계층에 엔티티 대신 단순 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 방법. 하지만 이 방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 한다.
스프링 OSIV : 비즈니스 계층 트랜잭션
OSIV를 서블릿 필터에서 적용할 지 스프링 인터셉터에서 적용할 지에 따라 원하는 클래스를 선택해서 사용하면 된다. 예를 들어 JPA 하이버네이트를 사용하면서 서블릿 필터에 OSIV를 적용하려면 OpenSessionInViewFilter
를 서블릿 필터에 등록하면 되고 스프링 인터셉터에 OSIV를 적용하려면 OpenSessionInViewInterceptor
를 스프링 인터셉터에 등록하면 된다.
스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV다.
- 클라이언트의 요청이 들어오면 영속성 컨텍스트를 생성한다. 이 때 트랜잭션은 시작하지 않는다.
- 서비스 계층에서
@Transactional
로 트랜잭션을 시작할 때 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다. - 비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시한다. 이 때 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.
- 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다. 이 후 클라이언트의 요청이 끝날 때 플러시를 호출하지 않고 영속성 컨텍스트를 바로 종료한다.
트랜잭션 없이 읽기
엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기라 한다. 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.
OSIV는 영속성 컨텍스트를 프레젠테이션 계층까지 유지한다. 그리고 프레젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없다. 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩은 할 수 있다.
컨트롤러에서 플러시가 동작하지 않는 이유
스프링이 제공하는 OSIV 서블릿 필터나 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고 em.close()
로 영속성 컨텍스트만 종료하기 때문에 플러시가 일어나지 않는다. 그래서 프레젠테이션 계층에서 em.flush()
를 호출하여 강제로 플러시해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외가 발생한다.
스프링 OSIV 주의사항
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX");
memberService.biz(); // 비즈니스 로직
return "view";
}
}
위 예제에서 memberService.biz()
메서드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이 때 변경 감지가 동작하면서 회원 엔티티의 수정 사항을 DB에 반영한다. 이런 문제를 해결하는 단순한 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하는 것이다.
OSIV 정리
스프링 OSIV 특징
- 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 유지한다. 그래서 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태가 유지된다.
- 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다. 트랜잭션이 없는 프레젠테이션 계층은 지연 로딩을 포함하여 조회만 가능하다.
스프링 OSIV 단점
- OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다. 그래서 특히 트랜잭션 롤백 시 주의가 필요하다.
- 프레젠테이션 계층에서 엔티티를 수정하고 비즈니스 로직을 수행하면 엔티티가 수정된다.
- 프레젠테이션 계층에서 지연 로딩에 의한 SQL이 실행되기 때문에 성능 튜닝시에 확인 부분이 확장된다.
여러 엔티티를 조회하는 경우
OSIV를 사용하는 방법이 만능은 아니다. OSIV를 사용하게 되면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다. 하지만 복잡한 화면을 구성할 때는 이 방법이 효과적이지 않은 경우가 많다. 그래서 엔티티를 직접 조회하기보다는 JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나은 해결책일 수 있다.
JVM을 벗어난 원격 상황
OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다. JSON이나 XML을 생성할 때는 지연 로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다.
JPA를 사용하면 트랜잭션이라는 단위로 영속성 컨텍스트를 관리하므로 트랜잭션을 커밋하거나 롤백할 때 문제가 없다. 유일한 단점은 프레젠테이션 계층에서 엔티티가 준영속 상태가 되므로 지연 로딩을 할 수 없다는 점이다.
기존 OSIV는 프레젠테이션 계층에서도 엔티티를 수정할 수 있다는 단점이 있었다. 스프링 프레임워크가 제공하는 OSIV는 기존 OSIV의 단점들을 해결하여 프레젠테이션 계층에서 엔티티를 수정하지 않는다.
Comments