트랜잭션 범위의 영속성 컨텍스트
- 순순하게 JS2E 환경에서 JPA를 사용하면 개발자가 직접 엔티티 매니저를 생성하고 트랜잭션도 관리해야 함
하지만 스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 함 - 스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용하며
이는 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻으로 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고
트랜잭션이 끝날 때 영속성 컨텍스트를 종료하여 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근함
스프링 프레임워크를 사용하면 보통 비즈니스 로직을 사용하는 서비스 계층에서 @Transactional 어노테이션을 선언해서
트랜잭션을 시작하며 외부에서는 단순히 서비스 계층의 메소드를 호출하는 것처럼 보이지만 이 어노테이션이 있으면
호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작
- 스프링 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작
- 트랜잭션을 커밋하면
JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋
따라서 영속성 컨텍스트의 변경 내용이 데이터베이스에 정상 반영됨 - 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하며 이때는 플러시를 호출하지 않음
- 트랜잭션 범위의 영속성 컨텍스트 전략
@Controller
class HelloController {
@Autowired HelloService helloService;
public void hello() {
/* 4. 반환된 member 엔티티는 준영속 상태
서비스 메소드가 끝나면서 트랜잭션과 영속성 컨텍스트가 종료되었으므로
컨트롤러에 반환된 member 엔티티는 준영속 상태 */
Member member = helloService.logic();
}
}
@Service
class HelloService {
@PersistenceContext // 엔티티 매니저 주입
EntityManager em;
@Autowired
Repository1 repository1;
@Autowired
Repository2 repository2;
/* 1. 트랜잭션 시작
HelloService.logic() 메소드에 @Transactional을 선언해서 메소드를 호출할 때 트랜잭션을 먼저 시작 */
@Transactional
public void logic() {
repository.hello();
/* 2. member는 영속성 상태
repository2.findMember()를 통해 조회한 member 엔티티는
트랜잭션 범위 안에 있으므로 영속성 컨텍스트의 관리를 받아 영속 상태 */
Member member = repository2.findMember();
return member;
}
/* 3. 트랜잭션 종료
@Transactional을 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데,
이때 영속성 컨텍스트를 종료하여 영속성 컨텍스트가 사라졌으므로
조회한 엔티티(member)는 이제부터 준영속 상태가 됨 */
}
@Repository
class Repository1 {
@PersistenceContext
EntityManager em;
public void hello() {
em.xxx(); // 영속성 컨텍스트 접근
}
}
@Repository
class Repository2 {
@PersistenceContext
EntityManager em;
public Member findMember() {
retrun em.find(Member.class, "id1"); // 영속성 컨텍스트 접근
}
}
- 구체적인 트랜잭션 범위의 영속성 컨텍스트 전략
트랜잭션이 같으면 같은 영속성 컨텍스트를 사용
트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도
트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용함
위의 경우에도 em.xxx()와 em.find(Member.class, "id1")은 모두 같은 트랜잭션 범위에 있으므로
따라서 엔티티 매니저는 달라도 같은 영속성 컨텍스트를 사용
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용
여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름
스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당하므로 같은 엔티티 매니저를 호출해도
접근하는 영속성 컨텍스트가 다르므로 멀티스레드 상황에 안전
즉, 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 처리해주기 때문에 개발자는 단순하게 비즈니스 로직 개발에 집중 가능
준영속 상태와 지연 로딩
- 트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되며 영속성 컨텍스트도 함께 종료
따라서 조회한 엔티티가 서비스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속 상태를 유지하지만
컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 됨
// 주문 엔티티 코드
@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가지
- 뷰가 필요한 엔티티를 미리 로딩해주는 방법
영속성 컨텍스트가 살아 있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법으로
따라서 엔티티가 준영속 상태로 변해도 연관된 엔티티를 이미 다 로딩해두어서 지연 로딩이 발생하지 않음
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 존재
1) 글로벌 페치 전략 수정
2) JPQL 페치 조인
3) 강제로 초기화
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 존재
- OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
다음 부분에서 설명
- 뷰가 필요한 엔티티를 미리 로딩해주는 방법
- 글로벌 페치 전략 수정
글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경
하지만 이렇게 글로벌 페치 전략을 즉시 로딩에서 설정하는 것은 2가지 단점이 존재
- 사용하지 않는 엔티티를 로딩함
order과 member 둘 다 필요해서 글로벌 전략을 즉시 로딩으로 설정하는 경우도 있는 반면에
order 엔티티만 있으면 충분하지만 order를 조회하면서 사용하지 않는 member도 함께 조회하는 경우도 발생 - N+1 문제가 발생함
JPA를 사용하면서 성능상 가장 조심해야 하는 것이 N+1 문제이며
처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 함
만약 em.find() 메소드로 엔티티를 조회할 때 연관된 엔티티를 로딩하는 전략이 즉시 로딩이면
데이터베이스에 JOIN 쿼리를 사용해서 한 번에 연관된 엔티티까지 조회하기 때문에 문제가 발생하지 않음
하지만 이 때 JPQL을 사용해서 조회하게 될 경우 JPA가 JPQL을 분석해서 SQL을 생성할 때는
글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용하므로 즉시 로딩이든 지연 로딩이든 구분하지 않고
JPQL 쿼리 자체에 충실하게 SQL을 만들게 되므로 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하게 되어
N+1 문제가 발생하여 SQL이 상당히 많이 호출되어 조회 성능에 치명적이므로 이런 N+1 문제는 JPQL 페치 조인으로 해결
- 사용하지 않는 엔티티를 로딩함
// 글로벌 페치 전략 - 즉시 로딩 설정
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 전략
private Member member; // 주문 회원
...
}
// 프리젠테이션 로직
// 엔티티 매니저로 주문 엔티티를 조회하면 연관된 member 엔티티도 항상 함께 로딩
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); // 이미 로딩된 엔티티
// N+1 문제 발생 X
// Order.member를 즉시 로딩으로 설정했다고 가정하고 em.find()로 조회 시
Order order = em.find(Order.class, 1L);
// 실행된 SQL
select o.*, m.*
from Order o
left outer join Member m on o.MEMBER_ID=m.MEMBER_ID
where o.id=1
// N+1 문제 발생 O
// Order.member를 즉시 로딩으로 설정했다고 가정하고 JPQL로 조회 시
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
...
/* 1. select o from Order o JPQL을 분석해서 select * from Order SQL을 생성
2. 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성
3. Order.member의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member도 로딩
4. 연관된 member를 영속성 컨텍스트에서 찾음
5. 만약 영속성 컨텍스트에 없으면 select * from Member where id=? SQL을 조회한 order 엔티티 수만큼 실행
그러므로 만약 조회한 order 엔티티가 10개이면 member를 조회하는 SQL도 10번 실행됨 */
- JPQL 페치 조인
글로벌 페치 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에 영향을 주므로 너무 비효율적이기 때문에
JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인을 사용
페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조인하므로 연관된 엔티티를 이미 로딩했으므로
글로벌 페치 전략은 무의미해지기 때문에 N+1 문제가 발생하지 않음
그러므로 페치 조인은 N+1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현실적인 방법
하지만 페치 조인이 현실적인 대안이지만 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있어
결국 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범하게 되는 단점 발생
그러므로 무분별한 최적화로 프리젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는
적절한 선에서 타협점을 찾는 것이 합리적
// 페치 조인 사용 전
JPQL : select o from Order o
SQL : select * from Order
// 페치 조인 사용 후
JPQL :
select o
from Order o
join fetch o.member
SQL :
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID=m.MEMBER_ID
// 페치 조인의 단점
// 예) 화면 A는 order 엔티티만 필요한 반면, 화면 B는 order 엔티티와 연관된 member 엔티티 둘 다 필요
// 그러므로 두 화면을 모두 최적화하기 위해 둘을 지연 로딩으로 설정하고 리포지토리에 2가지 메소드 생성
1) 화면 A를 위해 order만 조회하는 repository.findOrder() 메소드
2) 화면 B를 위해 order와 연괸된 member를 페치 조인으로 조회하는 repository.findOrderWithMember() 메소드
// 이후 화면 A와 화면 B에 각각 필요한 메소드를 호출하면 됨
// 이처럼 메소드를 각각 만들면 최적화는 할 수 있지만 뷰와 리포지토리 간에 논리적인 의존관계가 발생
// 타협점 (대안)
repository.findOrder() 하나만 만들고 여기서 페치 조인으로 order와 member를 함께 로딩하는 것
그리고 화면 A, 화면 B 둘다 repository.findOrder() 메소드를 사용하도록 함
// 물론 order 엔티티만 필요한 화면 A는 약간의 로딩 시간이 증가하겠지만
// 페치 조인을 JOIN을 사용해서 쿼리 한번으로 필요한 데이터를 조회하므로 성능에 미치는 영향이 미비
// 무분별한 최적화로 프리젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는
// 적절한 선에서 타협점을 찾는 것이 합리적
- 강제로 초기화
영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법
글로벌 페치 전략을 지연 로딩으로 설정하면 연관된 엔티티를 실제 엔티티가 아닌 프록시 객체로 조회하며
프록시 객체는 실제 사용하는 시점에 초기화되는데 프리젠테이션 계층에서 필요한 프록시 객체를
영속성 컨텍스트가 살아 있을 때 강제로 초기화해서 반환하면 이미 초기화했으므로 준영속 상태에서도 사용할 수 있음
하지만 프록시를 초기화하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 함
그러므로 슬쩍 프리젠티에션 계층이 서비스 계층을 침범하는 상항이 발생하므로 좋지 않음
따라서 FACADE 계층을 이용해 비즈니스 로직을 담당한 서비스 계층에서 프리젠테이션 계층을 위한 프록시 초기화 역할을 분리
// 프록시 강제 초기화
// 글로벌 페치 전략은 지연 로딩일 때
class OrderService {
@Transactional
public Order findOrder(id) {
Order order orderRepository.findOrder(id);
/* order.getMember() -> 단순히 프록시 객체만 반환하고 초기화 X
member.getName() -> 실제 값을 사용하는 시점에 초기화 O */
order.getMember().getName(); // 실제 값을 사용하는 시점을 사용해 프록시 객체를 강제로 초기화
return order;
}
}
// 하이버네이트를 사용하면 initialize() 메소드를 사용해서 프록시를 강제로 초기화
org.hibernate.Hibernate.initialize(order.getMember()); // 프록시 초기화
// JPA 표준에는 프록시 초기화 메소드가 없으며 단지 초기화 여부만 확인 가능
PersistenceUnitUtil persistenceUnitUtil = em.getEntityMangerFactory().getPersistenceUnitUtil();
boolean isLoaded = persistenceUnitUtil.isLoaded(order.getMember());
- FACADE 계층 추가
프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 둔 후 뷰를 위한 프록시 초기화를 담당하는 곳으로 설정
덕분에 서비스 계층은 프리젠테이션 계층을 위한 프록시를 초기화하지 않아도 되므로
서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있어 서비스 계층은 비즈니스 로직에 집중하고
프리젠테이션은 계층을 위한 초기화 코드는 모두 FACADE가 담당하면 됨
하지만 실용적인 관점에서 볼 때 FACADE의 최대 단점은 중간에 계층이 하나 더 끼어든다는 것이므로
결국 더 많은 코드를 작성해야 하며 FACADE에는 단순히 서비스 계층을 호출만 하는 위임 코드가 상당히 많아짐
FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해줌
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화함
- 서비스 계층을 호출해서 비즈니스 로직을 실행함
- 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾음
// FACADE 계층 추가
// OrderService에 있던 프록시 초기화 코드를 OrderFacade로 이동
// FACADE 계층을 사용해 서비스 계층과 프리젠테이션 계층 간에 논리적 의존 관계를 제거
class OrderFacade {
@Autowired
OrderService orderService;
public Order findOrder(id) {
Order order orderRepository.findOrder(id);
// 프리젠테이션 계층이 필요한 프록시 객체를 강제로 초기화
order.getMember().getName();
return order;
}
}
class OrderService {
public Order findOrder(id) {
return orderRepository.findOrder(id);
}
}
- 준영속 상태와 지연 로딩의 문제점
준영속 상태일 때 지연 로딩 문제를 극복하기 위해 글로벌 페치 전략도 수정하고, JPQL의 페치 조인도 사용하고,
강제로 초기화까지 하다가 결국 FACADE 계층까지 알아보게 되었음
하지만 뷰를 개발할 때 엔티티 클래스를 보고 개발하지 초기화되어 있는지 아닌지 확인하기 위해 FACADE나 서비스 클래스까지
열어보기 번거로우므로 놓치기 쉬워 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류가 발생할 가능성이 높음
결국 영속성 컨텍스트가 없는 뷰에서 초기화하지 않은 프록시 엔티티를 조회하는 실수가 발생하여 오류가 발생
그리고 애플리케이션 로직과 뷰가 물리적으로는 나누어져 있지만 논리적으로는 서로 의존한다는 문제가 있어
FACADE를 사용해서 이런 문제를 어느 정도 해소할 수는 있지만 상당히 번거로움
예) 주문 엔티티와 연관된 회원 엔티티를 조회할 때
화면별로 최적화된 엔티티를 딱딱 맞아떨어지게 초기화해서 조회하려면 FACADE 계층에 여러 종류의 조회 메소드가 필요함
조회 메소드 1) 화면 A는 order만 필요 : getOrder()
조회 메소드 2) 화면 B는 order, order.member가 필요 : getOrderWithMember()
조회 메소드 3) 화면 C는 order, order.orderItems가 필요 : getOrderWithOrderItems()
조회 메소드 4) 화면 D는 order, order.member, order.orderItems가 필요 : getOrderWithMemberWithOrderItems()
이 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생하므로
OSIV를 사용해 엔티티 컨텍스트를 뷰까지 살아있게 열어두면 뷰에서도 지연 로딩을 사용할 수 있음
OSIV
- OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻으로
영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지되므로 뷰에서도 지연 로딩을 사용할 수 있음 - 과거 OSIV : 요청 당 트랜잭션
가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마다 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서
트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 끝내는 방식인 요청 당 트랜잭션 방식의 OSIV
이렇게 하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속성 상태를 유지하므로
뷰에서 지연 로딩을 할 수 있으므로 엔티티를 미리 초기화할 필요가 없으며 FACADE 계층 없이 독립적인 서비스 계층 유지 가능
- 요청 당 트랜잭션 방식의 OSIV 문제점
요청 당 트랜잭션 방식의 OSIV의 문제점은 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있음
예) 고객을 출력할 때 보안상의 이유로 고객 이름을 XXX로 변경해서 출력해야할 때
컨트롤러에서 고객 이름을 XXX로 변경해서 렌더링할 뷰에 넘겨주어 단순히 뷰에 노출할 때만
고객 이름을 XXX로 변경하고 싶었지만 요청당 트랜잭션 방식의 OSIV는 뷰를 렌더링한 후에 트랜잭션을 커밋하고
영속성 컨텍스트를 플러시하므로 이 때 영속성 컨텍스트의 변경 감지 기능이 작동해서
변경된 엔티티를 데이터베이스에 반영해버리게 되어 데이터베이스에서 고객 이름이 XXX로 변경되는 심각한 문제 발생
이처럼 서비스 계층처럼 비즈니스 로직을 실행하는 곳에서 데이터를 변경하는 것이 아니라
프리젠테이션 계층에서 데이터를 잠시 변경했다고 실제 데이터베이스까지 변경 내용이 반영되면 유지보수가 어려워짐
이런 문제를 해결하기 위해서는 프리젠테이션 계층에서 엔티티를 수정하지 못하도록 막아야 하는데 3가지 방법이 존재
하지만 이러한 방법은 모두 코드량이 상당히 증가하는 단점이 존재하므로
프리젠테이션 계층에서 엔티티를 수정하면 안된다고 개발자들끼리 합의하는 것이 더 실용적일 수 있으며
또는 적절한 도구를 사용해서 프리젠테이션 계층에서 엔티티의 수정자를 호출하는 코드를 잡아내는 것도 하나의 방법
그러므로 이러한 요청 당 트랜잭션 방식의 OSIV는 위와 같은 문제들로 최근에는 거의 사용하지 않음
- 엔티티를 읽기 전용 인터페이스로 제공
엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법 - 엔티티 레핑
엔티티의 읽기 전용 메소드만 가지고 있는 엔티티를 감싼 객체를 만들고 이것을 프리젠테이션 계층에 반환하는 방법 - DTO만 반환
가장 전통적인 방법으로 프리젠테이션 계층에 엔티티 대신 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환
하지만 이 방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 함
- 엔티티를 읽기 전용 인터페이스로 제공
// OSIV의 문제점
class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안 상의 이유로 고객의 이름을 XXX로 변경
model.addAttribute("member", member);
...
}
}
// 엔티티를 읽기 전용 인터페이스로 제공
/* 실제 회원 엔티티가 있지만 프리젠테이션 계층에는 Member 엔티티 대신
회원 엔티티의 읽기 전용 메소드만 있는 MemberView 인터페이스를 제공하여
프리젠테이션 계층은 읽기 전용 메소드만 있는 인터페이스를 사용하므로 엔티티를 수정할 수 없음 */
interface MemberView {
public String getName();
}
@Entity
class Member implements MemberView {
...
}
class MemberService {
public MemberView getMember(id) {
retrun MemberRepository.findById(id);
}
}
// 엔티티 레핑
// member 엔티티를 감싸고 있는 MemberWrapper 객체를 만들어 member 엔티티의 읽기 전용 메소드만 제공
class MemberWrapper {
private Member member;
public MemberWrapper(member) {
this.member = member;
}
// 읽기 전용 메소드만 제공
public String getName() {
member.getName();
}
}
// DTO만 반환
// Member 엔티티와 거의 비슷한 MemberDTO를 만들고 엔티티의 값을 여기에 채워서 반환
class MemberDTO {
private String name;
// Getter, Setter
...
}
...
MemberDTO memberDTO = new MemberDTO();
memberDTO.setName(member.getName());
return memberDTO;
- 스프링 OSIV : 비즈니스 계층 트랜잭션
스프링 프레임워크가 제공하는 OSIV로 스프링 프레임워크의 spring-orm.jar는 다양한 OSIV 클래스를 제공하므로
OSIV는 서블릿 필터에서 적용할지 스프링 인터셉트에서 적용할지에 따라 원하는 클래스를 선택해서 사용하면 됨
1) 하이버네이트 OSIV 서블릿 필터 : OpenSessionInViewFilter
2) 하이버네이트 OSIV 스프링 인터셉터 : OpenSessionInViewInterceptor
3) JPA OEIV 서블릿 필터 : OpenEntityManagerInViewFilter
4) JPA OEIV 스프링 인터셉터 : OpenEntityManagerInViewInterceptor
예) JPA를 사용하면서 서블릿 필터에 OSIV를 적용하려면 OpenEntityManagerInViewFilter를 서블릿 필터에 등록 - 스프링 OSIV 분석
스프링 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이므로 트랜잭션은 비즈니스 계층에서만 사용
동작 원리
- 클라이언트의 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성
단 이 때 트랜잭션을 시작하지는 않음 - 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시함
이 때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료되지 않음 - 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지
- 서블릿 필터나 스프링 인터셉터로 요청이 들어오면 플러시를 호출하지 않고 영속성 컨텍스트를 바로 종료
- 클라이언트의 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성
- 트랜잭션 없이 읽기
영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 하며
만약 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 예외가 발생함
엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되므로 트랜잭션 없이 읽기라고 함
프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능
스프링이 제공하는 OSIV를 사용하면 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없음
따라서 프리젠테이션 계층에서 엔티티를 수정할 수 있는 기존의 OSIV의 단점을 보완했음을 알 수 있음
그리고 트랜잭션 없이 읽기를 사용해서 프리젠테이션 계층에서 지연 로딩 기능을 사용할 수 있음
예) 고객을 출력할 때 보안상의 이유로 고객 이름을 XXX로 변경해서 출력해야할 때
컨트롤러에서 회원 엔티티를 member.setName("XXX")로 변경했으며
프리젠테이션 계층이지만 아직 영속성 컨텍스트가 살아 있으므로
만약 영속성 컨텍스트를 플러시하면 변경 감지가 동작해서 데이터베이스에 해당 회원의 이름을 XXX로 변경하게 되지만
스프링이 제공하는 OSIV의 경우 플러시가 2가지 이유로 동작하지 않으므로
프리젠테이션 계층에서 영속 상태의 엔티티를 수정했지만, 수정 내용이 데이터베이스에는 반영되지 않음
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시해야하는데
트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 이미 플러시를 해버리며
스프링이 제공하는 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고
em.close()로 영속성 컨텍스트만 종료해 버리므로 플러시가 일어나지 않음 - 프리젠테이션 계층에서 em.flush()를 호출해서 강제로 플러시해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없음
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시해야하는데
// 스프링 OSIV 적용 후
class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안 상의 이유로 고객의 이름을 XXX로 변경
model.addAttribute("member", member);
}
}
- 스프링 OSIV 주의사항
스프링 OSIV를 사용하면 프리젠테이션 계층에서 엔티티를 수정해도 수정 내용을 데이터베이스에 반영하지 않음
하지만 프리젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생
예)
1. 컨트롤러에서 회원 엔티티를 조회하고 이름을 member.setName("XXX")로 수정
2. biz() 메소드를 실행해서 트랜잭션이 있는 비즈니스 로직 실행
3. 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작한 후 biz() 메소드를 실행
4. biz() 메소드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시하는데 변경 감지 기능 동작
이를 통해 회원 엔티티의 수정 사항을 데이터베이스에 반영하는 문제가 발생
즉, 컨트롤러(프리젠테이션 계층)에서 엔티티를 수정하고 즉시 뷰를 호출하는 것이 아니라
트랜잭션이 동작하는 비즈니스 로직을 호출할 경우 문제가 발생한다는 것이며
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생
이를 해결하는 단순한 방법으로는 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 됨
// 스프링 OSIV 주의사항
class MemberController {
public String viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안 상의 이유로 고객의 이름을 XXX로 변경
memberService.biz(); // 비즈니스 로직
return "view";
}
}
class MemberService {
@Transactional
public void biz() {
// ... 비즈니스 로직 실행
}
}
// 위의 문제를 단순한 방법으로 해결
// 비즈니스 로직을 먼저 수행 예제
memberService.biz(); // 비즈니스 로직 먼저 실행
Member member = memberService.getMember(id);
member.setName("XXX"); // 마지막에 엔티티를 수정
너무 엄격한 계층
- 예) 상품을 구매한 후에 구매 결과 엔티티를 조회하려고 컨트롤러에서 리포지토리를 직접 접근
과거 EJB 시절에는 프리젠테이션 계층에서 엔티티를 직접 반환하면 문제가 발생했어서
대부분 DTO를 만들어서 반환했고 엔티티가 계층을 뛰어넘는 것은 어려운 일이었음
OSIV를 사용하기 전에 프리젠테이션 계층에서 사용할 지연 로딩된 엔티리를 미리 초기화해야 함
그리고 초기화는 아직 영속성 컨텍스트가 살아있는 서비스 계층이나 FACADE 계층이 담당했음
하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없음
따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없음
// 컨트롤러에서 리포지토리 직접 접근
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) {
... 비즈니스 로직
retrun orderRepository.save(order);
}
}
class OrderRepository {
@PersitenceContext
EntityManager em;
public Order findOne(Long id) {
return em.find(Order.class, id);
}
}
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ① (0) | 2022.06.04 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 컬렉션과 부가 기능 (0) | 2022.05.27 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ③ (0) | 2022.05.18 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ② (0) | 2022.05.17 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ① (0) | 2022.05.16 |