웹 애플리케이션 영속성 관리

스프링이나 J2EE 컨테이너 환경에서 JPA를 사용 하면 컨테이너가 제공하는 전략을 따라야 한다. 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.

스프링 컨테이너의 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성 컨테스트 전략을 기본으로 사용한다. 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다.

           -   트랙잰셕을 시작할 때 영속성 컨텍스트를 생성
           -   트랜잭션을 종료할 때 영속성 컨텍스트를 종료
           -   같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근

스프링 컨테이너 트랜잭션

    1. @Transactional 어노테이션 메소드 호출 ->
    2. 스프링 트랜잭션 AOP 동작 ->
    3. 트랜잭션 시작 ->
    4. 메소드 수행 - >
    5. 메소드 종료 ->
    6. 트랜잭션 커밋 ->
    7. 영속성 컨텍스트 플러쉬 ->
    8. 데이터 베이스 반영 ->
    9. 데이터베이스 트랜잭션 커밋

<aop:config>
  <aop:advisor id="managerTx" advice-ref="txAdvice" pointcut="execution(* *..service.*Manager.*(..))" order="2" />
</aop:config>
@Service("adminLogManager")
public class AdminLogManagerImpl extends GenericManagerImpl<AdminLog, String> implements AdminLogManager {

    @Autowired
    public void setAdminLogDao(AdminLogDao adminLogDao) {
        this.dao = adminLogDao;

@Repository("adminLogListDao")
public class AdminLogListDaoJpa extends GenericListDaoJpa implements AdminLogListDao {

    @Override
    public AdminLogListVO getAdminLogListData(AdminLogListVO adminLogListVO) {
        Search search = adminLogListVO.getSearch();
        JPAQuery query = new JPAQuery(getEntityManager());
        QAdminLog adminLog = QAdminLog.adminLog;
    }
public class GenericListDaoJpa implements GenericListDao {
    protected final Log log = LogFactory.getLog(getClass());

    @PersistenceContext(unitName = GenericDaoJpa.PERSISTENCE_UNIT_NAME)
    protected EntityManager entityManager;
1.     Service Method 호출시 트랜잭션을 먼저 시작한다.
2.     getAdminLoginListData() 를 통해 조회한  AdminLongListVO는 트랜잭션 범위 안에 있으므로 영속성 컨텍스트의 관리를 받는다.
3.     Service Method 가 정상 종료되면 트랜잭션을 커밋 하고 이때 영속성 컨텍스트를 종료한다.
4.     Service Method 가 끝나면서 트랜잭션과 영속성 컨텍스트가 종료되었다. 따라서 컨트롤러에 반환된 엔티티는 준영속 상태다.

트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.

다양한 위치에서 엔티티 메니저를 주입 받아도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.

트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.

여러 스레드에서 동시에 요청이 와서 같은 엔티티 메니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다. 따라서 멀티 스레드 상황에 안전한다.

준영속 상태와 지연로딩

 스프링, J2EE 컨테이더는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용.
 트랜잭션은 보통 서비스 계층에서 시작하고 종료되며 영속성 컨텍스트도 트랜잭션이 종료됨에 따라 종료된다.
 조회한 엔티티가 서버스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되지만 컨트롤러나 뷰 같은 프리젠티이션 계층에서는 준영속 상태가 된다.
@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}
class OrderController {

    public String view(Long orderId) {
        Order order = orderService.findOne(orderId);
        User user = order.getUser();
        user.getName();   //예외 발생
    }
}

위와 같은 경우 변경감지와 지연로딩이 동작 하지 않는다.

  • 준영속 상태의 변경 감지 기능은 서비스 계층에서 비지니스 로직을 수행하면서 발생하므로 문제가 되지 않는다.
  • 준영속 상태와 지연로딩시 뷰를 렌더링 할 때 org.hibernate.LazyInitializeException 이 발생 할 수 있다.
  • 준영속 상태의 지연 로딩 문제를 해결하는 방법 2가지

    • 뷰가 필요한 엔티티를 미리 로딩

        1. 글로벌 패치 전략 수정    
        2. JPQL  패치 조인
        3. 강제로 초기화
      
    • OSIV 를 사용해서 엔티티를 항상 영속성 상태로 유지

  • 뷰가 필요한 엔티티를 미리 로딩

1. 글로벌 패치 전략 수정(지연로딩에서 즉시로딩으로 수정하여 엔티티 메니저를 통해 조회 할 경우 조회에 연관되 엔티티도 항상 조회)

                 변경전  :      @ManyToOne(fetch = FetchType.Lazy)  
                 변경후 :    @ManyToOne(fetch = FetchType.EAGER)                                

      - 단점 :   사용하지 않는 엔티티를 로딩
                     N+1문제 발생

 N+1 문제 ?

 즉시로딩이면(FetchType.EAGER) 일 경우 데이터 베이스에 JOIN 쿼리를 사용해서 한번에 연관된 엔티티 까지 조회 한다.
     Order order = em.find(Order.class, 1L);

     select o.*, m.*
        from order o
     left outer join Member m on o.MEMBER_ID = m.MEMBER_ID
         where o.id = 1
JPQL  조회시
List<Order> orders = em.createQuery("select o from Order o", Order.class).getResultList();
   select * from Order
   select * from Member where id = ?
   select * from Member where id = ?
   select * from Member where id = ?
   select * from Member where id = ?
   select * from Member where id = ?

JPA가 JPQL을 분석해서 SQL 을 생성할 때는 글로벌 패치 전략을 참고 하지 않고 오직 JPQL 자체만 사용.

조회한 order 엔티티가 10개이면 member를 조회 하는 SQL 도 10번 실행 한다.

N+1 문제 => 처음 조회한 데이터 수만큼 다시 SQL 을 사용해서 조회하는 것을 N+1 문제라 한다.

위의 해결 방법은 JPQL 패치 조인을 해결

1. JPQL 패치 조인

 JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인
select o from Order o join fetch o.member


1. 강제 초기화(강제 초기화는 영속성 컨텍스트가 살아 있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화 해서 반환 하는 방법)  

class OrderService {

public Order findOne(Long orderId){
    Order order = orderRepository.findOrder(id);
    order.getUser().getName(); //프록시 객체를 강제로 초기화 한다.
    return order;
}

} public void touch() { this.relayDetails.size(); this.relayBusinessTimes.size(); this.relayHolidays.size(); this.company.getLicenses().size(); this.networkGroup.getNetworkGroupServers().size(); for (Group group : this.groups) { group.getUsers().size(); } }

1.  JPQL 패치 조인
  • OSIV(open Session in View) 영속성 컨텍스트를 뷰까지 열어 두는 방식으로 뷰에서도 지연로딩을 사용할 수 있게 해준다.

    1. 과거 OSIV : 요청 당 트랜잭션

요청이 들어오면 서블릿 필터 또는 스프링 인터셉터에서 트랜잭션을 시작, 요청이 끝날 때 트랜잭션을 종료한다. 단점 : 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점. [해결 방안]

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 레핑
  • DTO만 반환

위의 3가지 해결 방안 모두 ENTITY 가 아닌 읽기 전용 메소드를 제공하는 방법으로 해결 코드량이 증가하므로 해결방안이 되기 어렵다. 애플리케이션 로직과 뷰가 물리적으로는 나누어져 있지만 논리적으로는 서로 의존하는 문제 발생

위의 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생

 2. 스프링 OSIV(Spring-orm.jar 는 다양한 OSIV 클래스를 제공한다.)

스프링 프레임워크가 제공하는 OSIV는 “ 비즈니스 계층에서 트랜잭션을 사용하는 OSIV다” 트랜잭션은 비즈니스 계층만 사용한다.

  • 하이버네이트 OSIV 서블릿 필터 : OpenSessionInViewInterceptor
     -    하이버네이트 OSIV 스프링 인터셉터 : OpenSessionInViewInterceptor
     -    JPA OEIV 서블릿 필터 : OpenEntityManagerInViewFilter
     -    JPA OEIV 스프링 인터셉터 : OpenEntityManagerInViewInterceptor
    

org.springframework spring-orm ${spring.version}

  1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단, 이때 트랜잭션은 시작하지 않는다.
  2. 서비스 계층에서 @Transactional 로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
  3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러쉬 한다. ->이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
  4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지 되므로 조회한 엔티티는 영속 상태를 유지한다.
  5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.

 스프링 OSIV 특징

  • 영속성 컨텍스트를 프리젠테이션 계층 까지 유지한다.
  • 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정 할 수 없다.
  • 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩 할 수 있다. class MemberController {

    @Autowired private MemeberDao memeberDao;

    public String viewMember(Long id) { Member member = memberService.getMember(id); member.setName(“XXX”);

    memberDao.save(member); //강제 플러쉬 -> // 예외 발생 : javax.persistence.TransactionRequireException : no transaction is in process

    model.addAttribute(“member”, member); } }

 주의 사항 class MemberController {

public String viewMember(Long id) {
    Member member = memberService.getMember(id);
    member.setName(“XXX”);
    memberService.biz();
}

}

class memberService {

public void biz() {
    //비지니스 로직 수행
}

} biz 메소드가 끝나면 트랜잭션 AOP 는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러쉬 한다. 이때 변경 감지가 동작하면서 회원 엔티티의 수정사항을 데이터 베이스에 반영한다.

 해결방안

class MemberController {

    public String viewMember(Long id) {
        Member member = memberService.getMember(id);
        memberService.biz();
        member.setName(“XXX”);
    }
}
class memberService {

    public void biz() {
        //비지니스 로직 수행
    }

}

트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경 하도록 한다.

  • OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유 할 수 있다는 점을 주의

     => 트랜잭션 롤백시 문제가 발생 할 수 있다.
    
    • 트랜잭션 롤백시 데이터베이스의 반영사항만 롤백
    • 자바객체까지 원상태로 복구해 주지 않는다.
    • 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다.
    • 새로운 영속성 컨텍스트를 생성하거나 EntityManager.clear() 를 호출
    • 스프링 프레임워크의 기본 전략은 트랜 잭션 AOP 종료 시점에 트랜잭션 롤백하면서 영속성 컨텍스트를 종료하므로 문제가 발생 하지 않는다.
    • OSIV는 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용 -> 다른 트랜잭션에서 해당 영속성 컨텍스트를 그래도 사용하면 문제 발생
    • 스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트랜잭션 롤밸시 영속성 컨텍스트를 초기화(EntityManager.clear())해서 문제를 예방한다.
    • JpaTransactionManager

@Override
protected void doRollback(DefaultTransactionStatus status) {
   JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction();
   if (status.isDebug()) {
      logger.debug("Rolling back JPA transaction on EntityManager [" +
            txObject.getEntityManagerHolder().getEntityManager() + "]");
   }
   try {
      EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction();
      if (tx.isActive()) {
         tx.rollback();
      }
   }
   catch (PersistenceException ex) {
      throw new TransactionSystemException("Could not roll back JPA transaction", ex);
   }
   finally {
      if (!txObject.isNewEntityManagerHolder()) {
         // Clear all pending inserts/updates/deletes in the EntityManager.
         // Necessary for pre-bound EntityManagers, to avoid inconsistent state.
         txObject.getEntityManagerHolder().getEntityManager().clear();
      }
   }

results matching ""

    No results matching ""