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 만 사용
      1. JPQL 분석해서 SQL 생성
      2. 생성한 쿼리로 DB 조회해서 엔티티 인스턴스들 생성
      3. 글로벌 페치 전략이 즉시 로딩으로 설정된 엔티티면 연관된 엔티티도 로딩해야 함
      4. 연관된 엔티티를 영속성 컨텍스트에서 조회
      5. 없으면 연관된 엔티티 조회하는 쿼리를 2번에서 조회된 엔티티 수만큼 실행
    • 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

  1. 클라이언트 요청 들어오면 서블릿 필터 스프링 인터셉터에서 영속성 컨텍스트 생성
  2. 서비스 레이어에서 @Transactional 로 트랜잭션 시작 시 1번에서 생성한 영속성 컨텍스트 찾아와서 트랜잭션 시작
  3. 서비스 레이어 끝나면 트랜잭션 커밋, 영속성 컨텍스트 플러시, 영속성 컨텍스트는 종료시키지 않고 살려둠
  4. 컨트롤러와 뷰까지 영속성 컨텍스트 유지되므로 조회한 엔티티는 영속 상태, 수정은 불가능(수정 사항이 DB 반영 안됨)
  5. 서블릿 필터 스프링 인터셉터로 요청이 돌아오면 플러시 하지 않고 바로 영속성 컨텍스트를 종료


트랜잭션 없이 읽기

  • 트랜잭션 없이 엔티티 변경 후 영속성 컨텍스트 플러시하면 예외 발생
  • 변경 없이 조회만 할 때는 트랜잭션 없어도 됨 => 트랜잭션 없이 읽기 (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() {
    // 비즈니스 로직 실행
    ...

  }
}

  1. 컨트롤러에서 Member entity 조회하고 member.setName(“XXX”) 로 name 수정
  2. biz() 메소드 호출해서 트랜잭션 있는 비즈니스 로직 실행
  3. 트랜잭션 AOP 동작하면서 영속성 컨텍스트에 트랜잭션을 시작 후 biz() 메소드 실행
  4. biz() 메소드 끝나면 트랜잭션 AOP가 트랜잭션 커밋하고 영속성 컨텍스트 플러시
  5. 변경 감지가 동작하며 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를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 유지됨
  • 따라서 위 예제처럼 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 문제가 없음