웹 애플리케이션과 영속성관리
스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해준다.
J2EE: Java 2 Enterprise Edition 자바 기술로 기업환경의 어플리케이션을 만드는데 필요한 스펙들을 모아둔 스펙 집합
J2EE 애플리케이션 서버는 웹컨테이너와 EJB 컨테이너 둘다 있어야 한다. 톰캣은 웹 컨테이너일 뿐이다.
보통 애플리케이션 서버에는 웹 컨테이너, EJB 컨테이너가 있다.
EJB 컨테이너 : Enterprise Bean(EJB 구성 요소)은 비즈니스 논리를 포함하는 Java 프로그래밍 언어 서버 구성 요소
- Enterprise Bean에는 Session Bean, Entity Bean 및 Message-Driven Bean 등 세 가지 유형이 있습니다.
- Session Bean : 임시 객체와 프로세스를 나타내며 대개 단일 클라이언트에서 사용
- Entity Bean: 대개 데이터베이스에서 유지 관리되는 지속성 필드를 나타낸다
- Message-Driven Bean은 응용 프로그램 모듈과 서비스에 비동기적으로 메시지를 전달하기 위해 사용
컨테이너는 Enterprise Bean을 작성하고, Enterprise Bean을 이름 지정 서비스에 바인딩하여 다른 응용 프로그램 구성 요소가 Enterprise Bean에 액세스할 수 있게 하고, 인증된 클라이언트만 Enterprise Bean의 메소드에 액세스하고, Bean의 상태를 영구 저장소에 저장하며, Bean의 상태를 캐시하고, 필요한 경우 Bean을 활성화 또는 비활성화한다.
트랜잭션 범위의 영속성 컨텍스트
스프링이나, J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 한다.
- 스프링이나 J2EE 컨테이너 환경이 아니라면, 개발자가 직접 엔티티 매니저도 생성하고 트랜잭션도 직접 관리해야 한다
스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략
을 기본으로 사용한다
- 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다
- 이말은 -> 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 종료되면 영속성 컨텍스트를 종료한다.
서비스 계층에서 트랜잭션이 시작되고 종료되니 영속성 컨텍스트의 생존 범위도 서비스 계층에서 생성되고 종료된다
비즈니스 로직을 사용하는 서비스 계층에서 @Transatctional 어노테이션을 선언해서 트랜잭션 시작
- 이 어노테이션이 있으면 메소드를 실행하기 직전에
스프링의 트랜잭션 AOP가 먼저 동작
한다
- 이 어노테이션이 있으면 메소드를 실행하기 직전에
스프링 트랜잭션 AOP
- @Transatctional 어노테이션이 선언된 메소드가 호출되기 직전에 트랜잭션 시작
- 대상 메소드가 정상 종료되면 트랜잭션 커밋, 비정상 종료되면 롤백
트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를
플러시(flush)
해서 변경 내용을 데이터베이스에 반영한 후에 트랜잭션을 커밋한다.만약 예외가 발생하여 비정상 종료되면 트랜잭션을 롤백하는데, 이때는
플러시(flush)
를 호출하지 않는다.
@Controller
class HelloController {
@Autowired
private HelloService helloService;
public void hello() {
Member member = helloService.logic(); // 이때 반환된 엔티티는 준영속 상태이다.
}
}
// 서비스클래스
@Service
class HelloService {
@PersistenceContext
private EntityManager em;
@Autowired
private Repository1 repository1;
@Autowired
private Repository2 repository2;
@Transtaictional // 트랜잭션 시작 - 메소드 호출 직전
public void logic() {
repository1.hello();
Member member = repository.findMember(); // 멤버 엔티티는 영속상태
return member; // 트랜잭션 종료
}
}
// Repository
@Repository
class Repository1 {
@PersistenceContext
private EntityManager em;
public void hello() {
em.xxx(); // 영속성 컨텍스트 접근
}
}
//
@Repository
class Repository2 {
@PersistenceContext
private EntityManager em;
public Member findMember() {
return em.find(Member.class, "id");
}
}
- HelloService.logic() 메소드에 @Transactional을 선언하였으므로 메소드를 호출할 때 트랜잭션을 먼저 시작한다.
- repository2.findMember()를 통해 조회한 member 엔티티는 트랜잭션 범위 (영속성 컨텍스트 범위) 안에 있으므로 영속성 컨텍스트의 관리를 받는다 -> 영속 상태
- @Transactional을 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데, 이때 영속성 컨텍스트도 같이 종료된다. 영속성 컨텍스트가 사라졌으므로 조회했던 member 엔티티는 이때부터 준영속 상태가 된다.
- 서비스 메소드가 끝나면서 트랜잭션과 영속성 컨텍스트도 종료되었으므로 컨트롤러에 반환된 member 엔티티는 준영속 상태이다.
트랜잭션이 같으면 같은 영속성 컨텍스틀 사용한다.
트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
HelloSerivce.logic() 메소드에서 @Transational이 적용되었는데 이 안에서 2개의 다른 Repository가 사용된다.
- 둘의 엔티티 매니저는 달라도 같은 트랜잭션 안에 있으므로 같은 영속성 컨텍스트를 사용한다.
같은 쓰레드를 사용한다.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
여러 스레드에서 동시에 요청이 와서
같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다. 따라서 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스트가 다르다. ->
멀티스레드 환경에서 안전하다
스프링이나 J2EE 컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 직접 처리해준다.
개발자는 싱글 스레드 애플리케이션처럼 단순하게 개발할 수 있고, 결과적으로 비즈니스 로직에 집중할 수 있다.
준영속 상태와 지연 로딩
앞에 예제를 보면, 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료되고 멤버 엔티티가 반환된다.
서비스를 이용하여 조회한 엔티티는 트랜잭션이 종료되는 서비스 계층의 상위계층으로 넘어가면 준영속 상태이다.
준영속 상태에서는 변경 감지와 지연 로딩이 동작하지 않는다!
- 다음 예제를 보자
@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 = orderSerivce.findOne(orderId);
Member member = order.getMember();
member.getName(); // 이때 예외 발생.
}
}
- 서비스 계층에서 반환받은 Order 엔티티는 준영속 상태인데, 그 상태로 Order 엔티티와 관계를 가진 member 엔티티를 호출해서 접근하면 예외가 발생한다.
준영속 상태와 변경 감지
- 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작하고 종료된 계층에서는 동작하지 않는다.
- 보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다
- 단순히 데이터를 리턴하거나 보여주는 프리젠테이션 계층에서 데이터를 수정할일은 없다 -> 이미 서비스 계층에서 수정이 끝남
- 만약 수정이 가능하게 되어 변경감지가 동작하면 애플리케이션 계층이 각각 가지는 책임이 모호해지고, 무엇보다 데이터를 어디서 변경했는지 모든 계층을 찾아야 하므로 애플리케이션을 유지보수하기 힘들다.
- 따라서, 수정과 같은 비즈니스 로직은 서비스 계층에서 끝내고, 보여주거나 돌려주는 계층에서 동작하지 않는것은 문제가 되지 않는다.
준영속 상태와 지연 로딩
준영속 상태에서는
지연로딩 (Lazy)
가 동작하지 않는다.만약 엔티티를 돌려준다고 했을때, 연관된 엔티티도 함께 돌려줘야 한다면 문제가 된다
연관된 엔티티가 지연로딩으로 설정되어 프록시 객체를 조회했다고 가정.
- 아직 초기화하지 않은 프록시 객체를 사용하면 실제 데이터를 불려오려고 초기화를 시도한다
- 하지만 준영속 상태는 영속성 컨텍스트가 없으므로 지연로딩을 할 수 없으니 문제가 발생한다
- 하이버네이트 구현체를 기준으로
org.hibernate.LazylnitializationException
이 발생한다
준영속 상태에서 지연로딩을 시도하면 문제가 발생한다. 하지만 JPA 표준에 어떤 문제가 발생하는지 정의하지 않아서 구현체마다 다르게 동작한다.
이 때, 준영속 상태의 지연로딩 문제를 해결하는 방법은 크게 2가지가 있다.
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
OSIV
를 사용해서 항상 엔티티를 영속상태로 유지하는 방법
뷰가 필요한 엔티티를 미리 로딩해두는 방법
- 영속성 컨텍스트가 살아있을 때 (In Transtaiction) 뷰에서 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법
- 엔티티가 준영속 상태로 변해도, 프록시 객체가 아닌 실제로 초기화를 다해뒀으므로 지연로딩이 발생하지 않는다
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법 3가지 - 어디서 로딩하느냐에 따라 3가지 방법이 있다
- 글로벌 페치전략 수정
- JPQL 페치조인 (fetch join)
- 강제로 초기화
1. 글로벌 페치전략 수정
- 가장 간단한 방법
- 글로벌 페치 전략을 지연로딩(Lazy) 에서 즉시 로딩(Eager)로 변경하면 된다
- 연관된 엔티티에 붙어있는 어노테이션의 속성을 변경한다.
@ManyToOne(fetch = FetchType.Eager) // FetchType.Lazy에서 변경
엔티티에 있는 fetch 타입을 변경하면 애플리케이션 전체에서 이 엔티티를 로딩할 때마다 해당 전략을 사용하므로
글로벌 페치 전략
이라고 한다.
- 항상 Eager로 된 연관된 엔티티들을 즉시 로딩한다.
글로벌 페치 전략 즉시 로딩 단점
사용하지 않는 엔티티를 로딩한다
- A 화면에서 order와 member가 둘다 필요해서 로딩으로 사용하고, B화면에서 order만 사용할 때,
- B화면에서는 member 엔티티 까지 필요 없는데, 같이 가져와서 I/O 낭비를 일으키게 된다.
N+1 문제가 발생한다. -- 주의
N+1 문제
JPQL을 사용할 때, Join으로 나가지 않고 매번 연관된 엔티티를 새로 쿼리를 1개 날리는 문제 (+1)
JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL자체만 사용한다
- 따라서 즉시 로딩이든 지연 로딩이든 구분하지 않고, JPQL 쿼리 자체에 충실하게 SQL을 만든다
다음과 코드를 분석하면 다음과 같은 순서로 동작한다.
// Order의 member는 Eager로 설정되어있다.
List<Order> orders = em.createQuery("select o from Order o", Order.class)
.getResultList(); // 연관된 모든 엔티티 조회
실행된 SQL은 다음과 같다
select * from Order // JPQL로 실행된 SQL
// ???? 읭??
select * from Member where id=? //EAGER로 실행된 SQL
select * from Member where id=? //EAGER로실행된 SQL
select * from Member where id=? //EAGER로 실행된 SQL
select * from Member where id=? //EAGER로 실행된 SQL
- select o from Order o JPQL을 분석해서
select * from Order
를 생성 - 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성
- Order.member의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member도 로딩해야 한다
- 연관된 member를 영속성 컨텍스트에서 찾는다.
- 만약 영속성 컨텍스트가 없으면
select * from mbmer where id = ?
쿼리를 조회한 order 엔티티 수 만큼 실행
이처럼 조회한 order 엔티티가 10개이면 member를 조회하는 SQL도 10번 추가적으로 실행된다.처음 조회한 데이터 수만큼 다시 SQL를 사용해서 조회하는 것을 N+1 문제라 한다.
- N+1이 발생하면 SQL이 상당히 많이 호출되므로 조회 성능에 치명적이다.
최우선 최적화 대상
N+1문제는 JPQL 페치 조인 (fetch join) 으로 해결할 수 있다.
JPQL 페치 조인
- 페치 조인은 JPQL 조인 명령어 마지막에
fetch
를 넣어주면 된다.- 페치 조인을 사용하면 SQL JOIN을 사용해서 페치조인 대상까지 함께 조회하므로 N+1 문제가 발생하지 않는다.
select o from Order o join fetch o.member
- 조회할 엔티티.연관된엔티티 방식으로 호출해야 한다
JPQL 페치조인의 단점
- 무분별하게 사용하면 화면에 맞춘 레포지토리가 증가할 수 있다.
- 이것은 프리젠테이션(뷰) 계층이 알게모르게 데이터 접근 계층(Repository)를 침범하는것
이제 위로 돌아가서 다시 화면 A와 B를 위한 조회 메소드를 2개로 만들면 된다
- 화면 A를 위해 order와 연관된 member를
페치조인
하는 repository.findOrderWithMember() 메소드 - 화면 B를 위해 order만 조회하는 repository.findOrder()
또다른 대안은 그냥 화면 A와 B 둘다 페치조인하는 메소드를 공통으로 두고 둘 다 같은 메소드를 사용한다.
- 물론 화면 B는 약간의 로딩 시간이 증가하겠지만, 페치 조인은 JOIN을 사용해서 쿼리 한번으로 필요한 데이터를 조회하므로 성능에 미치는 영향이 미비하다 (상황에 따라 다르겠지만.)
- 무분별한 최적화로 프리젠테이션 게층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는 타협점을 차즌 것이 합리적이다.
3. 강제로 초기화
영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 모두 반환하는 방법
// 글로벌 페치 전략은 지연 로딩 (Lazy)인 상태
@Transatctional
public Order findOrder(id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 지연 로딩인 프록시 객체를 일부러 강제로 초기화
return order
}
- 사용하진 않지만, Lazy Loading인 order.getMember()를 강제로 호출하였으므로 member 엔티티는 프록시 객체가 아닌 실제 엔티티가 된다.
- 반환받은 order 엔티티는 준영속 상태이지만, order엔티티의 member 엔티티는 강제로 초기화된 상태이므로 프리제테잇녀 계층에서도 사용할 수 있다.
JPA 표준에는 프록시 초기화 메소드가 없다.
하지만 프록시 초기화 여부를 확인할 수 있는 코드는 있다.
PersistnceUnitUtil persistenceUnitUtil =
em.getEntityManagerFactory().getPersistenceUnitUtil();
boolean isLoaded = persistenceUnitUtil.isLoaded(order.getMember());
그러나 예쩨처럼 프록시를 초기화 하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 한다.
- -> 프리젠테이션 계층이 서비스 계층을 침범하는 상황
- 서비스 계층은 비즈니스 로직을 담당하는 계층이지 프리젠테이션 계층을 위한 일까지 하는것은 좋지 않다 .
- 따라서 비즈니스 로직을 담당하는 서비스 계층에서 프리젠테이션 계층을 위한
프록시 초기화 역할을 분리
해야함
-> FACADE 계층을 추가하여 역할을 담당한다.
FACADE (퍼사드) 계층
프리젠테이션 게층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법
- 뷰를 위한 프록시 초기화는 퍼사드 계층에서 담당하게 한다.
- 덕분에 서비스 게층은 프리젠테이션 계층을 위해 프록시를 강제로 초기화하지 않아도 된다.
결과적으로 퍼사드 계층을 도입해서 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다.
그러나 프록시를 초기화하려면
영속성 컨텍스트가 필요하므로, FACADE 계층의 메소드에서 트랜잭션을 시작해야 한다
FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층간의 논리적 의존성을 분리해준다.
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화 한다.
- 서비스 계층을 호출해서 비즈니스 로직을 실행한다
- 레포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
class OrderFacade {
@Autowired
private OrderSerivce orderSerivce;
public Order findOrderWithMember(id) {
Order order = orderService.findOrder(id);
order.getMember().getName(); // 강제로 프록시 객체 초기화ㅣ
return order
}
}
class OrderSerivce {
@Autowired
private OrderRepository orderRepository;
public Order findOrder(id) {
return porderRepository.findOrder(id);
}
}
- 컨트롤러에서는 OrderFacade의 findOrderWithMember() 메소드를 호출하면 된다
- FACADE 계층을 사용해서 서비스 계층과 프리젠테이션 계층 간의 논리적 의존 관계를 제거했다.
- 이제 서비스 계층은 비즈니스 로직에 집중하고 프리젠테이션을 위한 초기화 코드는 모두 FACADE가 담당하면 된다.
- 하지만 실용적인 관점에서 볼 떄 FACADE의 최대 단점은 중간에 계층이 하나 더 끼어들어 관리 포인트가 늘어나는 점이다.
- FACADE에는 서비스 계층을 호출만 하는 위임 코드가 상당히 많을 것이다..
준영속 상태와 지연 로딩의 문제점
글로벌 페치 전략, JPQL 페치 조인, 강제 초기화 위해 FACADE 계층까지 알아 보았다.
뷰를 개발할 때 필요한 엔티티를 미리 초기화 하는 방법은 생각보다 오류가 발생할 가능성이 높다
-> 왜냐하면 보통 뷰를 개발할 때는 엔티티 클래스를 보고 개발하지 초기화 되어 있는지 아닌지 확인하기 위해 FACADE나 서비스 클래스까지 열어보는것은 상당히 번거롭고 놓치기 쉽기 떄문
결국 영속성 컨텍스트가 없는 다른 계층에서 초기화하지 않은 프록시 엔티티를 조회하는 실수를 하게 되고
LazyInitionalzationException
을 만나게 될것이다.
결국 모든 문제를 엔티티가 프리젠테이션 계층에서 준영속상태이기때문에 발생한다.
영속성 컨텍스트를 뷰까지 살아 있게 열어둘 수 있는 방법이 있다.
뷰에서도 지연로딩을 사용할 수 있게 하는것이 이것이 OSIV
이다
OSIV (Open Session In View)
OSIV : 영속성 컨텍스트(세션)를 뷰까지 열어준다는 뜻
영속성 컨텍스트가 살아있으면 엔티티는 영속상태를 유지할 수 있고 뷰에서도 지연로딩이 가능하다
OSIV는 하이버네이트에서 사용하는 용어. JPA에서는 OEIV (Open EntityManager In View)라고 한다.
하지만 관례쌍 모두 OSIV 라고 부른다
과거 OSIV : 요청 당 트랜잭션
OSIV의 핵심은 뷰에서도 지연로딩이 가능하도록 하는것
가장 단순한 구현방법은 클라이언트의 요청이 들어오자마자
서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는것.
- 이것을 요청당 트랜잭션의 방식이 OSIV 라고 한다 (Transaction per request OSIV)
- 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속성컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스틀 함께 종료
- 이렇게 하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속상태 유지
- -> 뷰에서도 지연 로딩 가능하므로 미리 초기화 할 필요 없고 FACADE 계층 없이도 뷰에 독립적인 서비스 계층 유지 가능
그러나 문제점 존재
요청당 트랜잭션 방식의 OSIV 문제점
- 지연로딩이 가능하므로 변경감지도 가능하다
- 즉 비즈니스 로직을 가진 서비스계층이 아닌 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 문제점
- 애플리케이션에서 어디가 수정되었는지 변경점을 찾기도 힘들고 관리가 어려워지게 된다.
프레젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법들은 다음과 같다
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 레핑
- DTO만 반환
1. 엔티티를 읽기 전용 인터페이스로 제공
- 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공
interface MemberView {
public String getName();
}
@Entity
class Member implements MemberView {
...
}
class MemberService {
public MemberView getMember(id) {
return memberRepository.findById(id);
}
}
- 실제 회원 엔티티가 있지만 프리젠테이션 계층에는 MemberView 인터페이스를 제공하고 읽기 전용 메소드만 제공하므로 엔티티를 수정할 수 없다.
2. 엔티티 래핑
읽기 전용 인터페이스와 같이 엔티티를 감싼 객체를 만들어 반환하는 방식
class MemberWrapper {
private Member member;
public MemberWrapper(member) {
this.member = member;
}
// 읽기 전용 메소드만 제공
public String getName() {
member.getName();
}
}
3. DTO만 반환
가장 전통적인 방법. 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO를 반환
- 하지만 이방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 한다.
class MemberDTO {
private String name;
// get & set
}
..
MemberDTO MemberDTO = new MemberDTO();
memberDTO.setName(member.getName());
return memberDTO;
지금까지 설명한 3가지 방법 (인터페이스, 레핑, DTO) 모두 코드량이 상당히 증가한다는 단점이 있다.
개발자들끼리 다른 계층에서 수정하지 말자고 합의하거나 도구를 사용해서 잡아내는 것이 실용적일 수 있지만,
이것은 언제든지 부숴질 수 있다.
지금까지 설명한 OSIV는 요청 당 트랜잭션 방식의 OSIV 이며, 이것은 지금까지 설명했던 문제점들로 인해 최근에는 거의 사용하지 않는다.
이런 문제점을 보완해서 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.
스프링 프레임워크가 제공하는 OSIV가 이 방식을 사용하는 OSIV이다
스프링 OSIV : 비즈니스 계층 트랜잭션
스프링 프레임워크가 제공하는 OSIV 라이브러리
OSIV를 서블릿 필터
에서 적용할 지, 스프링 인터셉터
에서 적용할지에 따라 원하는 클래스를 선택해서 사용하면 된다
책에서는 하이버네이트4 버전 기준이고 다음은 하이버네이트5 기준이다
- 하이버네이트 OSIV 서블릿 필터 : org.springframework.orm.hibernate5.support.OpenSessionInViewFilter
- 하이버네이트 OSIV 스프링 인터셉터 : org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor
- JPA OEIV 서블릿 필터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
- JPA OEIV 스프링 인터셉터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor
JPA를 사용하면서 서블릿 필터에 OSIV를 적용하려면 OpenEntityManagerInViewFilter를 서블릿 필터에 등록
스프링 인터셉터에 OSIV를 적용하려면 .OpenEntityManagerInViewInterceptor를 스프링 인터셉터에 등록하면 된다.
스프링 OSIV 분석
요청당 트랜잭션 방식의 OSIV는 프리젠테이션 계층에서 데이터를 변경할 수 있다느 ㄴ문제가 있따.
스프링이 제공하는 OSIV는 이 문제를 어느정도 해결했다.
스프링 프레임워크가 제공하는 OSIV는 "비즈니스 계층에서 트랜잭션을 사용하는 OSIV"이다.
- 이름 그대로 OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다는 뜻
동작 원리
클라이언트의 요청이 들어오면 영속성 컨텍스트는 생성하지만 트랜잭션은 시작하지 않는다.
서비스 계층에서 트랜잭션을 시작하면 앞에서 생성해둔 영속성 컨텍스트에 트랜잭션을 시작
비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트는 플러시
- 이 때 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다
이후 클라이언트의 요청이 끝날 때 영속성 컨텍스트 종료
클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다.
- 단 이 때 트랜잭션은시작하지는않는다.
서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성컨텍스트를찾아와서트랜잭션을시작한다.
서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다.
- 이 때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를유지한다.
서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨테스트를 종료한다.
- 이 때 플러시를 호출하지 않고 바로 종료한다.
트랜잭션 없이 읽기
영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다
- 만약 트랜잭션이 없이 엔티틸르 변경하고 영속성 컨텍스트를 플러시 하면
javax.persistence.TransactionalRequiredException
발생
엔티티를 변경하지 않고 단순히 조회만 할때는 트랜잭션이 없어도 된다
- 이것을 Nontrasactional reads (트랜잭션 없이 읽기) 라고 한다
프록시를 초기화하는 지연로딩도 조회기능이므로 트랜잭션 없이 읽기가 가능하다.
- 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
- 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. -> 트랜잭션 없이 읽기
스프링이 제공하는 OSIV를 사용하면 프레젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.
따라서 프리젠테이션 계층에서 엔티티를 수정할 수 있는 기존 OSIV의 단점을 보완했다.
- 또한 트랜잭션 없이 읽기를 사용해서 다른 계층에서 지연로딩 사용 가능
스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV의 특징
- 영속성 컨텍스트를 프리젠테이션 계층까지 유지
- 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티 수정 불가
- 프리젠테이션 계층에서는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연로딩 가능.
예제 - 컨트롤러에서 회원 엔티티 변경
- 프리젠테이션 계층이지만 스프링 OSIV로 인해 영속성 컨텍스트는 살아있지만 플러시는 동작하지 않는다 - 왜?
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시 해야한다. 하지만 트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 이미 플러시 해버렸다. 또한 스프링 OSIV서블릿 필터나 스프링 OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고
em.close()
로 영속성 컨텍스트만 종료해 버리므로 플러시가 일어나지 않는다. - 프리젠테이션 계층에서
em.flush()
를 호출하여 강제로 플러시 해도 트랜잭션 범위 밖이므로 데이터 수정 불가 예외를 던진다- javax.persistence.TRansactionalREquiredException : no transaction is in progress
따라서 컨트롤러에서 영속상태의 엔티티를 수정해도 수정 내용이 데이터베이스에 반영되지 않는다
스프링 OSIV 주의사항
스프링 OSIV를 사용하면 프리젠테이션 계층에서 엔티티를 수정해도 수정 내용을 DB에 반영하지 않는다.
- 그러나 1가지 예외가 있다.
프리젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출 시 문제 발생
class MemberController {
public String viewMember(Long id) {
Member melmoer = memberServlce.getMember( id) ;
member.setName("XXX"); //보안상의 이유로 고객 아름을 XXX로 변경
memberServlce.biz() ; // 다른 트랜잭션을 호출하는 비즈니스로직
return "view";
}
}
class MemberSerivce {
@Transactional
public void biz() {
// 비즈니스 로직 실행
}
}
- 컨트롤러에서 회원 엔티티를 조회하고 회원 엔티티를 수정 - 트랜잭션은 이미 끝난 상태지만 영속성 컨텍스트는 살아있음
- biz() 메소드를 실행하면서 트랜잭션이 있는 로직 실행
- 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션 시작
- biz() 메소드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트 플러시 - 이때 변경감지 동작하면서 회원 엔티티 수정 사항을 데이터베이스에 반영
문제를 해결하는 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다.
OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같으므로
이런 문제가 발생하지 않는다
OSIV 정리
- 스프링 OSIV의 특징
- OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이
끝날때 까지 같은 영속성 컨텍스트를 유지한다. - 따라서 한번 조회한 엔티티는 요청이 끝날때까지 영속상태 를유지한다.
- 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다.
- 트랜잭션이 없는 프리젠테이션계층은 지연로딩을 포함해서 조회만할수있다
- OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이
- 스프링 OSIV의 단점
- OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는점을주의해야한다.
- 특히 트랜잭션 롤백시 주의해야 하는ep '15.1.4절을 참고하자.
- 앞서 스프링 OSIV 주의사항에서도 이야기했듯이 프리젠테이션 계층에서 엔티티를 수정하고나서 비즈니스로직을 수행하면 엔티티가 수정될수있다.
- 프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝시에 확인해야 할 부분이 넓다.
- OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는점을주의해야한다.
OSIV vs FACADE vs DTO
- OSIV를 사용하지 않는 대안은 FACADE 계층이나 그것을 조금 변형해서 사용하
는 다양한 방법이 있는데 어떤 방법을 사용하든 결국 준영속 상태가 되기 전에
프록시를 초기화해야 한다. - 다른 방법은 엔티티를 직접 노출하지 않고 엔티티와
거의 비슷한 DTO를 만들어서 반환하는 것이다. - 어떤 방법을 사용하든 OSIV를사용하는 것과 비교해서 지루한 코드를 많이 작성해야 한다.
- OSIV를 사용하지 않는 대안은 FACADE 계층이나 그것을 조금 변형해서 사용하
OSIV를 사용하는 방법이 만능은 아니다
- OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏
탐색할 수 있다. - 하지만 복잡한 화면을 구성할 때는 이 방법이 효과적이지 않은경우가 많다.
- 예를 들어 복잡한 통계 화면은 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한JPQL을 작성해서 DTO로 조회하는 것이 효과적이다.
- 그리고 수많은 테이블을 조인해서 보여주어야 하는 복잡한 관리자 화면도 객체 그래프로 표현하기 어려운 경우가 많다.
- 이때도 엔티티를 직접 조회하기보다는JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나은 해결책 일수있다.
- OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏
OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다
OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다.
예를 들어 JSON이나 XML을 생성할 때는 지연 로딩을 사용할 수 있지만 원격지인 클라이언트에
서 연관된 엔티티를 지연 로딩하는 것은 불가능하다.결국 클라이언트가 필요한데이터를 모두 JSON으로 생성해서 반환해야 한다.
보통 Jackson이나 Gson 같은 라이브러리를 사용해서 객체를 JSON으로 변환하는데, 변환 대상 객체로 엔
티티를 직접 노출하거나 또는 DTO를 사용해서 노출한다이렇게 JSON으로 생성한 API는 한 번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있다
외부 API
: 외부에 노출한다 한 번 정의하면 변경이 어렵다. 서버와 클라이언트를 동시에 수정하기 어렵다- 예: 타팀과협업하기위한API’타기업과협업하는API
- 내부 API: 외부에 노출하지 않는다 언제든지 변경할 수 있다 서버와 클라이언트를동시에수정할수있다
- 예 같은 프로젝트에 있는 화면을 구성하기 위한AJAX 호출
엔티티는 생각보다 자주 변경된다.
엔티티를 JSON 변환 대상 객체로 사용하면엔티티를 변경할 때 노출하는 JSON API도 함께 변경된다.
따라서 외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로
변환해서 노출하는 것이 안전하다.내부 API는 엔티티를 변경해도 클라이언트와서버를 동시에 수정할 수 있어서 실용적인 관점에서 엔티티를 직접 노출하는 방
법도 괜찮다고 생각한다.
너무 엄격한 계층
OSIV를 사용하기 전에는 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를미리 초기화해야 했다.
그리고 초기화는 아직 영속성 컨텍스트가 살아있는 서비스계층이나 FACADE 계층이 담당했다.
하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없다.
- 따라서 단순한엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없다
- 과거 EJB 시절에는 프리젠테이션 계층에 엔티티를 직접 반환하면 여러 가지 문제가 발생했다.
- 따라서 대부분 DTO를 만들어서 반환했고 엔티티가 계층을 뛰어넘는 것은 어려운 일이었다.
- OSIV를 사용하면 설명한 것처럼 좀 더 유연하고 실용적인 관점으로 접근하는 것도 좋은 방법이라 생각한다.
OSIV 추가 정리
spring boot에서 OSIV를 properties 또는 yml로 관리할 수 있다
spring:
jpa:
open-in-view: true # 기본값
- spring.jpa.open-in-view=값
- 기본값은 true이며 false로 하면 꺼진다.
- Spring Boot JPA 의존성을 주입 받아 어플리케이션을 구성할 경우 spring.jpa.open-in-view의 기본값인 true로 지정되어 있어 OSIV가 적용된 상태로 어플리케이션이 구성된다.
OSIV On (spring.jpa.open-in-view=true )
spring:
jpa:
open-in-view: true
spring.jpa.open-in-view = true
DB 트랜잭션을 시작할 때 JPA 영속성 컨텍스트가 DB 커넥션을 가져온다.
OSIV가 켜져있으면 @Transactional 메서드를 벗어나도
커넥션을 계속 유지
한다.API 응답이 나가고 화면이 렌더링 될 때까지 영속성 컨텍스트를 물고 있는다.
View Template이나 Controller 단에서도 지연 로딩으로 데이터를 가져올 수 있다.
- 지연 로딩은 영속성 컨텍스트가 살아 있어야 가능하다.
장점 - OSIV를 켰을 때
- 지연 로딩을 적극 활용할 수 있다는 장점이 있다.
- View Template이나 Controller 단에서도 지연 로딩으로 데이터를 가져올 수 있다
단점 - OSIV를 켰을 때
너무 오랫동안 DB 커넥션을 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다.
- -> 결국 장애로 이어진다.
로직 상에서 외부 API를 호출한다면 이걸 처리하는 시간만큼 커넥션 리소스를 반환하지 못한다.
OSIV OFF (spring.jpa.open-in-view=false)
spring:
jpa:
open-in-view: false
spring.jpa.open-in-view = true
- 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고 DB 커넥션을 반환한다.
- 따라서 커넥션 리소스를 낭비하지 않는다.
장점 - OSIV를 껐을 때
트랜잭션으로 가져온 데이터를 요청한 지점에서 반환한 다음엔 DB 커넥션을 쓰지 않는다.
- 사용자 요청이 많을 경우 유연하게 사용할 수 있다.
단점 - OSIV를 껐을 때
모든 지연 로딩을 트랜잭션 안에서 해결해야 한다.
지연 로딩을 하려면 영속성 컨텍스트가 살아있어야 한다.
지금까지 구현한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어줘야 한다.
트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해두거나 fetch join을 사용해야 한다
권장하는 방법
- 실시간 트레픽이 중요한 경우 OSIV를 사용하지 말고, DTO로 직접 조회하는 것을 권장
- 어드민 페이지같이 실시간 트레픽이 중요하지 않는 경우 OSIV를 사용해도 괜찮다.
- OSIV는 복잡한 화면을 구성하는 경우 효과적이지 않을 수 있다.
- 예를 들어 복잡한 화면은 엔티티로 조회하기보다는 처음부터 DTO로 조회하는 것이 효과적입니다.
커맨드와 쿼리 분리 (CQS(Command Query Separation) Pattern )
실무에서 OSIV를 끈 상태로 복잡성을 관리하려면 커맨드와 쿼리를 분리한다.
성능 문제는 주로 조회
에서 발생한다.
비즈니스 로직
정책적인 것이라 잘 변경되지 않는다.
특정 Entity 몇 개를 등록하거나 수정하는 것이 전부라서 성능이 크게 문제되진 않는다.
화면에 뿌리기 위한 조회 API
자주 바뀌고 라이프사이클이 빠르다.
복잡한 화면을 출력해야 하므로 성능 최적화가 중요하다.
ex) OrderService
- OrderService: 핵심 비즈니스 로직
- OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
두 Serivce로 분리한다.
이 둘 사이의 라이프 사이클이 다르기 때문에 명확하게 분리하는 것이 좋다.
'스터디 > JPA 프로그래밍 스터디' 카테고리의 다른 글
JPA 프로그래밍 스터디 10장 정리 (0) | 2022.10.14 |
---|---|
JPA 프로그래밍 스터디 9장 정리 (0) | 2022.10.06 |
JPA 프로그래밍 스터디 8장 정리 (0) | 2022.10.01 |
JPA 프로그래밍 스터디 7장 정리 (1) | 2022.10.01 |
JPA 프로그래밍 스터디 6장 정리 (0) | 2022.10.01 |