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

동현·2021년 8월 6일
0

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

스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 트랜잭션과 영속성 컨텍스트를 관리해 주기때문에 개발을 손쉽게 할수 있다. 하지만 내부 동작에 대해 잘 모른채로 개발을 해 발생할수 있는 문제에 대해 다뤄보겠다.

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

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 사용한다.

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

스프링 프레임워크 사용시 비즈니스 로직을 시작하는 서비스 계층에 @Transcational 어노테이션을 선언해 트랙잭션을 시작한다. 이는 위의 그림의 Controller 와 Service단의 트랜잭션 범위를 의미한다.

트랜잭션과 영속성 컨텍스트는 같은 범위를 유지하며 보통 service 계층과 repository의 계층에 존재한다.

웹계층에서 컨트롤러에 요청을 하는 과정을 살펴보면

  1. controller는 service를 호출하기 이전에 트랜잭션AOP를 동작시킨다.
  2. 메소드 호출 직전에 트랜잭션을 시작하고. 메소드가 종료되면 트랜잭션을 커밋하고 종료한다. (이때 JPA는 커밋 이전에 영속성 컨텍스트의 내용을 테이터베이스에 반영킨다.)

☝️ 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용

→ 엔티티메니저가 달라도 같은 트랙잭션 안이라면 같은 영속성 컨텍스트를 공유함

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


준영속 상태와 지연 로딩

보통 트랜잭션은 서비스 계층에서 시작되므로 컨트롤러 계층은 트랜잭션의 범위에서 벗어난다. 따라서 트랜잭션 범위에서 조회된 엔티티는 준영속 상태가 된다.

ex)

class OrderController {
	public String view(Long orderId) {
		Order order = orderService.findOne(orderId);
		Member member = order.getMember();
		member.getName()//지연 로딩시 예외 발생
	}
}
  • 지연로딩은 영속상태일때 발생한다. 하지만 controller계층은 트랜잭션이 종료된 이후이기 때문에 준영속 상태이다. 따라서 order.getMember()를 사용해도 프록시가 초기화 되지 않는다.
  • 만약 controller 에도 트랜잭션을 선언하게 된다면 프리젠테이션 계층서도 엔티티의 수정이 가능해지므로 애플리케이션 계층이 가지는 책임이 모호해진다. → 어디서 변경된지 찾기 위해 유지보수가 힘들어짐

준영속 상태의 지연 로딩 문제를 해결하는 방법

  • 뷰가 필요한 엔티티를 미리 로딩
  • OSIV를 사용해 엔티티를 항상 영속 상태로 유지

뷰가 필요한 엔티티 미리 로딩하는 방법

  • 글로벌 페치 전략 수정
  • JPQL 페치 조인
  • 강제로 초기화

1. 글로벌 페치 전략 수정

가장 간단한 방법은 글로벌 페치 전략을 Eager로 변경하면 된다.

 @ManyToOne(fetch = FetchType.EAGER)
  • 엔티티의 페치 타입을 변경하면 애플리케이션 전체에 이 전략을 적용하게 된다. 이를 글로벌 페치 전략이라 한다.

‼️ 글로벌 페치 전략에 즉시로딩 사용시 단점

  • 사용하지 않는 엔티티 로딩

    → 이는 간단하게 연관된 엔티티가 즉시 로딩이 되면서 사용하지 않더라도 조회가 된다.

  • N+1 문제 발생

    → JPA가 JPQL을 분석해서 SQL을 사용할때는 글로벌 페치 전략을 참고하지 않고 JPQL 자체만 사용한다. 만약 조회한 order엔티티가 10개면 이름 참조하는 member 도 10개가 조회된다. 따라서 상당히 많은 SQL문이 호출되면서 조회성능에 치명적이다.

2. JPQL 페치 조인

글로벌 페치 전략을 즉시로딩으로 설정시 애플리케이션 전체에 영향을 주게 된다.

N+1을 해결하기위해 JPQL만 페치 조인을 사용하게 수정해보자

select o
from Order o
join fetch o.member
  • 페치 조인을 사용하게 되면 SQLJOIN을 사용해 페치 조인 대상까지 함께 조회를해 N+1 의 문제가 발생하지 않게 된다.

‼️ 단점

  • 무분별하게 사용하면 화면에 맞춘 레포지토리 메소드가 증가할 가능성이있다.
  • 이는 결국 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범하게 되는 현상을 야기한다.

3. FACADE 계층 추가

이는 프리젠테이션 계층과 서비스 계층 사이에 FACADE계층을 추가해 뷰를 위한 프록시 초기화를 담당하는 계층을 만들어준다. → 논리적 의존성 분리 가능

특징

  • 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리
  • 프리젠테이션 계층에서 필요한 프록시 객체를 초기화
  • 서비스 계층을 호출해 비즈니스 로직 실행
  • 레포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾는다.

💁 위의 모든 문제들은 결국 엔티티가 프리젠테이션 계층에서 준영속이기에 발생하는 문제들이다.

OSIV

osiv란 영속성 컨텍스트를 뷰 까지 열어 둔다는 뜻이다.

과거의 OSIV : 요청당 트랜잭션

  • 과거에는 클라이언트 요청이 들어오자마자 트랜잭션이 시작하고 요청이 끝날때 트랜잭션도 종료하게 만들었었다.
  • 이는 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있는 치명적인 단점을 지니고 있다.
Member member = memberSerivce.getMember(id);
member.setName("xxx");
model.addAttribute("Member", member);
  • 위 코드의 의도는 조회한 멤버의 이름을 감추기 위해 임시적으로 xxx를 세팅한것을 볼수있다.
  • 위 같이 트랜잭션이 보장된 컨트롤러 계층에서 member의 속성값을 변경하면 변경 감지에 의해 member의 속성값이 변하는 심각한 문제가 발생한다.

이를 막기위해 다음과 같은 방법이 있다.

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

‼️ 이방법들은 코드가 상당히 증가한다는 단점이 있다.

스프링 OSIV: 비즈니스 계층 트랜잭션

이전 요청당 트랜잭션은 프리젠테이션 계층에서 데이터를 변경할수 있다는 단점이 있었다. 이 방법은 문제들을 어느정도 해결한 방법이다.

동작 원리

  1. 요청이 들어오면 영속성 컨텍스트 생성(트랜잭션은 X)
  2. 서비스 계층에서 트랜잭션을 시작하면 양석상 칸텍스트에 트랜잭션 시작
  3. 비즈니스 로직 실행후 서비스 계층이 끝날때 트랜잭션 커밋 영속성 컨텍스트 플러시 ( 영속성 컨텍스트는 살려둠 )
  4. 요청 종료시 영속성 컨텍스트 종료

특징

  • 영속성 컨텍스트를 프리젠테이션 계층까지 유지
  • 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티 수정 불가능
  • 트랜잭션 없이 읽기를 사용해 지연로딩 가능

‼️ 문제점

Member member = memberService.getMember(id);
member.setName("xxx");
memberService.biz();

위 코드의 문제점은 무엇일까?

비즈니스 계층 트랜잭션의 특징을 살펴보면 영속성 컨텍스트의 생명주기와 트랜잭션의 생명주기가 다르다.

즉 영속성 컨텍스트가 종료되기 이전에 다시 트랜잭션을 살린다면 값이 변경 될수 있다.

profile
여긴 어디 나는 누구?

0개의 댓글