[자바 ORM 표준 JPA 프로그래밍] 13장. 웹 애플리케이션과 영속성 관리
by Jo
13장. 웹 애플리케이션과 영속성 관리
13.1 트랜잭션 범위의 영속성 컨텍스트
- 순수 J2SE 환경에선 개발자가 직접 엔티티 매니저 생성하고 트랜잭션 관리
- 스프링, J2EE 컨테이너 환경에서 JPA 사용시엔 컨테이너가 제공하는 전략 따라야 함
13.1.1 스프링 컨테이너의 기본 전략
- 트랜잭션 범위의 영속성 컨텍스트 가 기본 전략
- 트랜잭션 시작시 영속성 컨텍스트를 생성하고 트랜잭션 끝날 때 영속성 컨텍스트 종료
- 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근
@Transactional
어노테이션 사용하면 메소드 실행 직전에 스프링의 트랜잭션 AOP가 먼저 동작- 스프링 트랜잭션 AOP는 대상 메소드 호출 전 트랜잭션을 시작
- 대상 메소드 정상 종료시 트랜잭션을 커밋하면서 종료
- 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트 플러시해서 변경 내용 DB 반영 후에 DB 트랜잭션을 커밋함
- 예외 발생시 트랜잭션을 롤백하고 종료하며 플러시 호출 안함
동일 트랜잭션, 동일 영속성 컨텍스트
- 다양한 위치에서 엔티티 매니저 주입받아 사용해도 트랜잭션이 같으면 동일한 영속성 컨텍스트를 사용함
다른 트랜잭션, 다른 영속성 컨텍스트
- 여러 스레드에서 동시에 요청 와서 같은 엔티티 매니저 사용해도 트랜잭션이 다르면 서로 다른 영속성 컨텍스트를 사용
- 스프링 컨테이너는 스레드마다 각기 다른 트랜잭션을 할당함
13.2 준영속 상태와 지연 로딩
- 트랜잭션 범위의 영속성 컨텍스트 전략에서 트랜잭션은 보통 서비비스 레이어에서 시작되고 끝남
- ∴ 컨트롤러, 뷰와 같은 프리젠테이션 레이어에서는 영속성 컨텍스트가 없어 준영속 상태임
- 변경 감지, 지연 로딩 동작 안함
준영속 상태와 변경 감지
- 프리젠테이션 레이어에서는 데이터 수정 안되는게 레이어 간 책임 더 확실해지고 좋음
- ∴ 문제 안됨
준영속 상태와 지연 로딩
- 프리젠테이션 레이어에서 지연 로딩 사용하면 예외 발생함
- 해결 방법은 크게 두 가지
- OSIV 사용해서 엔티티를 항상 영속 상태로 유지
- 뷰에서 필요한 엔티티를 미리 로딩
- 영속성 컨텍스트가 살아있을 때 미리 다 로딩하거나 초기화해서 반환해두는 방법
- 어디서 미리 로딩해두는 지에 따라 세 가지 방법이 있음
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
13.2.1 글로벌 페치 전략 수정
- 글로벌 페치 전략을 lazy loading 에서 eager로 수정
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 전략 설정
- 엔티티에 있는 fetch 타입 변경하면 애플리케이션 전체에서 해당 엔티티 로딩할 때마다 설정한 전략을 사용함
글로벌 페치 전략에 즉시 로딩 사용 시 단점
- 사용하지 않는 엔티티를 로딩함
- 한 곳에서 즉시 로딩 필요해서 설정해두면 필요 없는 다른 곳에서 조회해서 사용할때도 언제나 즉시 로딩됨
- N+1 문제 발생
- JPA가 JPQL 분석해서 SQL 생성 시 글로벌 페치 전략 참고하지 않고 오직 JPQL 만 사용
- JPQL 분석해서 SQL 생성
- 생성한 쿼리로 DB 조회해서 엔티티 인스턴스들 생성
- 글로벌 페치 전략이 즉시 로딩으로 설정된 엔티티면 연관된 엔티티도 로딩해야 함
- 연관된 엔티티를 영속성 컨텍스트에서 조회
- 없으면 연관된 엔티티 조회하는 쿼리를 2번에서 조회된 엔티티 수만큼 실행
- JPQL 페치 조인으로 해결 가능
- JPA가 JPQL 분석해서 SQL 생성 시 글로벌 페치 전략 참고하지 않고 오직 JPQL 만 사용
// N+1 문제 JPQL 예시
String query =
"SELECT o " +
"FROM Order o ";
List<Order> orders = em.createQuery(query, 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 * FROM order 결과 수만큼 반복됨)
13.2.2 JPQL 페치 조인
- JPQL 호출 시점에 함께 로딩할 엔티티를 선택할 수 있음
- fetch join 사용하면 JPQL에서 연관된 엔티티도 같이 조회하기 때문에 추가적으로 엔티티 조회 안함 => N+1 문제 해결
// N+1 문제 발생했던 예시에 페치 조인 사용하여 문제 해결
String query =
"SELECT o " +
"FROM Order o "
"JOIN FETCH o.member "; // fetch join으로 JPQL이 연관된 엔티티도 함께 조회해서 로딩하도록 함
List<Order> orders = em.createQuery(query, Order.class)
.getResultList();
JPQL 페치 조인의 단점
- 무분별하게 사용시 화면에 맞춘 레포지토리 메소드가 증가
- 뷰와 레포지토리 간 논리적 의존 관계가 발생
- ∴ 프리젠테이션 레이어가 데이터 엑세스 레이어를 침범하는 결과를 낳음
13.2.3 강제로 초기화
- 영속성 컨텍스트가 살아있을 때 프리젠테이션 레이어에서 필요한 엔티티를 강제로 초기화해서 리턴하는 방법
- 하이버네이트에서는
initialize()
메소드를 사용해 강제로 초기화할 수도 있음 - JPA 표준에는 프록시 초기화 메소드는 없고 초기화여부 확인은 가능함
- 이렇게 프록시 초기화를 서비스 레이어에서 담당하게 되면 JPQL 페치 조인 때처럼 프리젠테이션 레이어가 서비스 레이어를 침범하는 상황이 됨
- ∴ 프리젠테이션 레이어를 위한 프록시 초기화 역할을 분리해야 함 => FACADE 레이어
코드
// 프록시 강제 초기화 예시
class OrderService {
@Transactional
public Order findOrder(Long id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체 강제 초기화
return order;
}
}
...
// 하이버네이트에서 강제 초기화 예시
class OrderService {
@Transactional
public Order findOrder(Long id) {
Order order = orderRepository.findOrder(id);
Hibernate.initialize(order.getMember()); // 프록시 객체 강제 초기화
return order;
}
}
...
// JPA 표준에서의 프록시 초기화 여부 확인 코드
PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();
boolean isLoaded = persistenceUnitUtil.isLoaded(order.getMember());
13.2.4 FACADE 레이어 추가
- 프리젠테이션 레이어와 서비스 레이어 사이에 FACADE 레이어를 하나 더 두고 뷰를 위한 프록시 초기화를 담당시킴
- 프록시 초기화하려면 영속성 컨텍스트 필요하므로 FACADE 에서 트랜잭션을 시작해야 함
FACADE 계층의 역할과 특징
- 프리젠테이션 레이어 <-> 서비스 레이어 간 논리적 의존성 분리
- 프리젠테이션 레이어에서 필요한 프록시 객체를 초기화함
- 서비스 레이어를 호출해서 비즈니스 로직을 실행
- 레포지토리를 직접 호출해서 뷰가 요구하는 엔티티 조회
// 강제 초기화 예시에 FACADE 레이어 추가
@RequiredArgsConstructor
class OrderFacade {
private final OrderService orderService;
@Transactional // 영속성 컨텍스트 살아있어야 lazy loading 가능하므로 트랜잭션을 FACADE 레이어에서 시작
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);
}
}
13.2.5 준영속 상태와 지연 로딩의 문제점
- 준영속 상태일 때 지연 로딩 문제 극복을 위한 방법들 사용해서 지연 로딩 문제 자체는 해결 가능
- 하지만 이렇게 미리 초기화 하는 방법은 오류 발생 가능성이 높음
- ∵ 뷰에서 엔티티 사용할 때 초기화 여부 확인하기 위해 FACADE, 서비스 클래스까지 다 확인해야해서 실수하기 쉬움
- ∴ 초기화되지 않은 프록시 엔티티를 뷰에서 조회하여 예외가 발생할 가능성 높음
- 또한 애플리케이션 로직과 뷰의 논리적 의존 관계가 발생
- FACADE 로 어느정도 해소 가능하나 굉장히 번거로움
- 이러한 문제를 해결하려면 영속성 컨텍스트를 뷰까지 살아있게 해야함 => OSIV
13.3 OSIV
- Open Session In View
- 영속성 컨텍스트를 뷰까지 열어둔다는 의미
- OSIV 는 하이버네이트에서 사용하는 용어
- JPA에서는 OEIV(Open EntityManager In View)
13.3.1 과거 OSIV: 요청 당 트랜잭션
- 클라이언트의 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트 만들면서 트랜잭션 시작
- 요청 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료
- 뷰에서도 영속성 컨텍스트가 살아있어서 지연 로딩 할 수 있음
요청 당 트랜잭션 방식의 OSIV 문제점
- 컨트롤러, 뷰 같은 프리젠테이션 레이어에서 엔티티 변경이 가능함
- 컨트롤러에서 엔티티 변경해서 뷰에 넘겨주면 OSIV는 뷰를 렌더링 후 트랜잭션을 커밋 => 영속성 컨텍스트 플러시 돼서 변경이 반영됨
- 문제를 해결하기 위해서는 프리젠테이션 레이어에서 엔티티 수정 못하도록 막아야 함
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 래핑
- DTO만 반환
엔티티를 읽기 전용 인터페이스로 제공
// 읽기 전용 인터페이스
interface MemberView {
public String getName();
}
...
// 엔티티
@Entity
class Member implements MemberView {
...
}
...
// 서비스 레이어에서 entity를 읽기 전용 인터페이스로 반환
class MemberService {
public MemberView getMember(Long id) {
return memberRepository.findById(id);
}
}
- 프리젠테이션 레이어에는 실제 엔티티 대신 읽기 전용 메소드만 있는 인터페이스를 제공
엔티티 래핑
class MemberWrapper {
private Member member;
public MemberWrapper(Member member) {
this.member = member;
}
public String getName() {
member.getName();
}
}
- 읽기 전용 메소드만 가지고 있는 래퍼 객체를 만들어서 프리젠테이션 레이어에 제공
DTO만 반환
class MemberDTO {
private String name;
// Getter, Setter
...
}
...
MemberDTO memberDTO = new MemberDTO();
memberDTO.setName(member.getName());
return memberDTO;
- 엔티티와 거의 비슷한 DTO 를 만들어서 값 채운다음 이걸 프리젠테이션 레이어에 반환
- 이 방법 사용시엔 OSIV 사용 장점 살릴 수 없음(지연 로딩 못하고 미리 로딩하게 됨)
13.3.2 스프링 OSIV: 비즈니스 계층 트랜잭션
- 요청 당 트랜잭션 방식은 위와 같은 문제점들로 인해 거의 사용 X
- 이러한 문제점들을 보완하여 비즈니스 계층에서만 트랜잭션을 유지하는 방식 => 스프링의 OSIV
스프링 프레임워크가 제공하는 OSIV 라이브러리
- spring-orm.jar 는 다양한 OSIV 클래스 제공
- OSIV 를 서블릿 필터에 적용할지 스프링 인터셉터에 적용할지에 따라 원하는 클래스 선택하면 됨
- 하이버네이트 OSIV 서블릿 필터:
org.springframework.orm.jpa.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번에서 생성한 영속성 컨텍스트 찾아와서 트랜잭션 시작 - 서비스 레이어 끝나면 트랜잭션 커밋, 영속성 컨텍스트 플러시, 영속성 컨텍스트는 종료시키지 않고 살려둠
- 컨트롤러와 뷰까지 영속성 컨텍스트 유지되므로 조회한 엔티티는 영속 상태, 수정은 불가능(수정 사항이 DB 반영 안됨)
-
서블릿 필터 스프링 인터셉터로 요청이 돌아오면 플러시 하지 않고 바로 영속성 컨텍스트를 종료
트랜잭션 없이 읽기
- 트랜잭션 없이 엔티티 변경 후 영속성 컨텍스트 플러시하면 예외 발생
- 변경 없이 조회만 할 때는 트랜잭션 없어도 됨 => 트랜잭션 없이 읽기 (Nontransactional reads)
- 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기 가능
스프링 OSIV 주의사항
- 프리젠테이션 레이어에서 엔티티 수정해도 수정 내용 DB 반영 안되지만 예외가 있음
- 프리젠테이션 레이어에서 엔티티 수정 직후 트랜잭션을 시작하는 서비스 레이어 호출시 문제 발생
// 프리젠테이션 레이어
class Member Controller {
public String viewMember(Long id) {
Member member = membrerSerivce.getMember(id);
member.setName("XXX"); // view 에서만 고객 이름을 X로 표시하기위해 수정. DB 반영은 의도 X
memberService.biz(); // 비즈니스 로직 호출
return "view";
}
}
...
// 서비스 레이어
class MemberSservice {
@Transactional
public void biz() {
// 비즈니스 로직 실행
...
}
}
- 컨트롤러에서 Member entity 조회하고 member.setName(“XXX”) 로 name 수정
biz()
메소드 호출해서 트랜잭션 있는 비즈니스 로직 실행- 트랜잭션 AOP 동작하면서 영속성 컨텍스트에 트랜잭션을 시작 후
biz()
메소드 실행 biz()
메소드 끝나면 트랜잭션 AOP가 트랜잭션 커밋하고 영속성 컨텍스트 플러시- 변경 감지가 동작하며 Member entity 의 수정사항이 DB에 반영됨
- 이와 같은 문제를 해결하려면 트랜잭션 있는 비즈니스 로직을 모두 실행하고 난 후에 엔티티를 변경해야 함
13.3.3 OSIV 정리
- 스프링 OSIV 특징
- 요청 들어오면 끝날때 까지 같은 영속성 컨텍스트를 유지 => 한 번 조회한 엔티티는 요청 끝날 때 까지 영속 상태임
- 엔티티 수정은 트랜잭션이 있는 계층에서만 동작, 프리젠테이션 계층은 지연 로딩을 포함한 조회만 가능
- 스프링 OSIV 단점
- 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있음. 특히 트랜잭션 롤백 시 주의 요함
- 프리젠테이션 레이어에서 엔티티 수정 후 비즈니스 로직 수행 시 엔티티 수정될 수 있음
- 프리젠테이션 레이어에서 지연 로딩으로 인한 SQL 실행되므로 성능 튜닝시 확인할 범위가 넓음
- OSIV는 같은 JVM 벗어난 원격 상황에서는 사용 불가
- 서버에서 JSON, XML 생성시엔 지연 로딩 되지만 클라이언트에서 연관된 엔티티 지연 로딩은 불가능
- 따라서 클라이언트에서 필요한 데이터는 모두 JSON 생성해서 반환해야 함
- 엔티티는 생각보다 자주 변경되므로 외부 API에는 완충 역할 할 수 있는 DTO 로 변환해서 노출하는 것이 안전함
13.4 너무 엄격한 계층
// 프리젠테이션 레이어
@Controller
@RequiredArgsConstructor
class OrderController {
private final OrderService orderService;
private final OrderRepository orderRepository;
public String orderRequest(Order order, Model model) {
Long id = orderService.order(order); // 상품 구매
// 컨트롤러에서 리포지토리 직접 접근
Order orderResult = orderRepository.findOne(id);
model.addAttribute("order", orderResult);
}
}
...
// 서비스 레이어
@Transcational
@RequiredArgsConstructor
class OrderService {
private final 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를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 유지됨
- 따라서 위 예제처럼 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 문제가 없음
Subscribe via RSS