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

Stormi·2022년 5월 27일
0

JPA

목록 보기
1/4

트랜잭션 범위의 영속성 컨텍스트

  • 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
  • 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다.
  • 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
@Transaction
	a()

이런식으로 밖에서 a()를 호출하면 a()를 감싸는 a'()가 실행된다.
즉, 프록시이다. a'()의 트랜잭션이 실행되는것이다. 즉, 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.

@Service
class HelloService{
		@PersistenceContext //엔티티 매니저 주입
        EntityManager em;
        
        @Autowired Repository1 repository1;
        @Autowired Repository2 repository2;
        
        //트랜잭션 시작
        @Transactional
        public void logic();
        repository1.hello();
        
        		//members는 영속상태이다.
                Member member = repository2.findMember();
                return member;
       //트랜잭션 종료
}
@Repository
class Repository1{
		@PersistenceContext
        EntityManager em;
        
        public void hello(){
        	em.xxx(); //영속성 컨텍스트 접근
        }
}

               
  1. HelloService.logic() 메소드에 @Transactional 을 선언해서 메소드를 호출할때 트랜잭션을 먼저 시작한다.
  2. repository2.findMember()를 통해 조회한 member 엔티티는 트랜잭션 범위안에 있으므로 영속성 컨텍스트의 관리를 받는다. 따라서 영속상태이다.
  3. @Transactional을 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데, 이때 영속성 컨텍스트를 종료한다. 영속성 컨텍스트가 사라졌으므로 조회한 엔티티(member)는 이제부터 준영속 상태가 된다.
  4. 서비스 메소드가 끝나면서 트랜잭션과 영속성 컨텍스트가 종료되었다. 따라서 컨트롤러에 반환된 member 엔티티는 준영속 상태이다.

  • 스프링이나 J2EE컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티스레드 상황을 컨테이너가 처리해준다는 점이다. 따라서 개발자는 싱글 스레드 애플리케이션처럼 단순하게 개발할 수 있고 결과적으로 비지니스 로직 개발에 집중할 수 있다.

준영속 상태와 지연로딩

  • 조회한 엔티티가 서비스와 레포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속상태를 유지하지만 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속상태가 된다. because 트랜잭션을 빠져나왔기 때문에.
@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(); // 여기서부터
        //예외가 터짐, 위에 order.getMember()할때는 
        //가짜클래스를 돌려줘서 저때는 예외가 안터지나, 
        //member.getName()할때는 영속성 컨텍스트가 끝나있으니,
        //예외가 터짐!!
        
   
        
  • 준영속 상태와 변경 감지
    변경감지 기능은 영속성 컨텍스트가 살아있는 서비스 계층(트랜잭션 범위)까지만 동작하고 영속성 컨텍스트가 종료된 프레젠테이션 계층(컨트롤러 단)에서는 동작하지 않는다.
    보통 변경감지 기능은 서비스 계층에서 비지니스 로직을 수행하면서 발생한다. 단순히 데이터를 보여주기만 하는 프레젠테이션 계층에서 데이터를 수정할 일은 거의 없다.
    변경 감지 기능이 프리젠테이션 계층에서 동작하지 않는 것은 문제가 되지 않는다. 즉, 컨트롤러에서는 변경감지를 하면안 됨, 트랜잭션안에서만 하면됨!!
  • 준영속 상태와 지연 로딩
    준영속 상태의 가장 문제는 지연로딩 기능이 동작하지 않는다는 점이다. 예를 들어 뷰를 렌더링할때 연관된 엔티티도 함께 사용해야하는데 연관된 엔티티를 지연로딩으로 설정해서 프록시 객체로 조회했다고 가정하자. 아직 초기화하지 않은 프록시 객체를 사용하면 실제 데이터를 불러오려고 초기화를 시도한다. 하지만 준영속상태는 영속성 컨텍스트가 없으므로 지연로딩을 할 수 없다.
    org.hibernate.LazyInitializationException예외가 발생한다.

준영속 상태의 지연로딩 문제를 해결하는 방법 2가지
1. 뷰가 필요한 엔티티를 미리 로딩해두는 방법
2. OSIV를 사용해서 엔티티를 항상 영속상태로 유지하는 방법

뷰가 필요한 엔티티를 미리 로딩해두는 방법

1. 글로벌 페치 전략 수정

-> 엔티티클래스에서 FetchType을 Eager로 바꾸기
-> 프록시로 대체하지 않고 바로 리턴함 -> 문제점!! -> N+ 1 문제가 있음 -> 쿼리가 하나 더 나감 . 하나의 쿼리를 썼는데 N개가 더 나감.

JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다. 따라서 즉시 로딩이든 지연로딩이든 구분하지 않고 JPQL쿼리 자체에 충실하게 SQL을 만든다.
1. select o from Oreder o JPQL을 분석해서 select from Order SQL을 생성한다.
2. 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
3. Order.member의 글로벌 페치 전략이 즉시로딩(eager)이므로 order를 로딩하는 즉시 연관된 member도 로딩해야한다. 즉, order별로 쿼리가 나감
4. 연관된 member를 영속성 컨텍스트에서 찾는다.
5. 만약 영속성 컨텍스트엥 없으면 SELECT
FROM MEMBER WHERE id=? SQL을 조회한 order 엔티티 수만큼 실행된다.

  • 만약 조회한 order 엔티티가 10개이면 member를 조회하는 SQL도 10번 실행된다. 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1문제라고한다.

2. JPQL 페치조인

  • 위의 N+1 문제를 해결하기 위해 JPQL 페치 조인을 사용한다

  • select from Order o inner join Member m 을 select from Order o inner join fetch Member m로 바꾼다.

  • 페치 조인을 사용하면 SQL JOIN을 사용해서 페치조인대상까지 함께 조회한다. 따라서 N+1문제가 발생하지 않는다. (연관된 엔티티를 이미 로딩했으므로 글로벌 페치 전략은 무의미하다.)

페치 조인 단점

  • 상황마다 페치 조인을 쓰는게 달라짐, 항상 페치 조인을 쓰면 성능에 문제가 생김
  • 프리젠테이션 계층이 알게모르게 데이터 접근 계층을 침범하게 된다
  • 화면 A는 order엔티티만 필요하고 화면 B는 order엔티티와 관련된 member엔티티 둘다 필요로한다. 결국 두 화면을 위해 둘을 지연로딩으로 설정하고 레포지토리에 2가지 메소드를 만든다
    1. 화면 A를 위해 order만 조회하는 repository.findOrder()메소드
    2. 화면 B를 위해 order와 연관된 member를 페치조인으로 조회하는 repository.findOrderWithMember() 메소드
  • 화면 A와 화면 B에 각각 필요한 메소드를 호출하면된다. 메소드를 각각 만들면 최적화할수있지만 뷰와 레포지토리간에 논리적인 의존관계가 발생.
  • 다른 대안은 repository.findOrder()하나만 만들고 여기서 페치 조인으로 order와 member를 함께 로딩하는 것이다. 그리고 화면 둘다에서 이 메소드를 호출하게 한다. 물론 order엔티티만 필요한 화면 B는 로딩시간이 증가하지만 페치조인은 JOIN을 사용해서 쿼리한개에 필요한 데이터를 조회하므로 성능에 미치는 영향이 미비하다.

3. 강제로 초기화

OSIV

OSIV(open session in view)는 영속성컨텍스트를 뷰까지 열어둔다는 뜻이다. 즉. 컨트롤러까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연로딩을 사용할 수 있다.

아래코드 :
컨트롤러에서 멤버이름을 변경해서 렌더링해서 뷰에 넘겨주었다. 개발자의 의도는 단순히 뷰에 노출할때만 고객의 이름을 XXX로 변경하고 싶은 것이지 실제 데이터베이스에 있는 고객 이름까지 변경하고 싶은 것이아니다.

class MemberControlle{
	public String viewMember(Long id){
    	Member member = memberService.getMember(id);
        member.setName("XXX"); // 보안상의 이유로 이렇게 바꿔줌
        model.addAttribute("member", member);
        

문제있는 코드이다.

요청당 트랜잭션 방식의 OSIV는 뷰를 렌더링한 후에 트랜잭션을 커밋한다. 트랜잭션을 커밋하면 당연히 영속성 컨텍스트에 플러시하니까 영속성 컨텍스트의 변경 감지 기능이 작동해서 변경된 엔티티를 데이터베이스에 반영해버린다. 결국 데이터베이스에 멤버의 이름이 XXX로 바뀌게 되는 문제가 발생한다.

  • 이렇게 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법들이 세가지있다.
  1. 엔티티를 읽기 전용 인터페이스로 제공
  2. 엔티티 레핑
  3. DTO만 반환

스프링 OSIV 분석

  • 요청당 트랜잭션 방식의 OSIV는 프레젠테이션 계층에서 데이터를 변경할 수 있다는 문제가 있다. 영속성 컨텍스트가 트랜잭션 단위가 아닌 요청당 영속성 컨텍스를 만들어준다.
  1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단 이때 트랜잭션은 시작하지 않는다 -> 컨트롤러가 생성되기 전 코드
  2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다. (한 요청이 들어왔다)
  3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
  4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다. -> 준영속상태가 아님
  5. 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스틀 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.

해결방법 -> 비지스 로직을 먼저 수행하게하기

0개의 댓글