스터디/JPA 스터디
[JPA] 웹 애플리케이션과 영속성 관리
jpa-study에서 스터디를 진행하고 있습니다.
트랜잭션 범위의 영속성 컨텍스트
스프링 컨테이너의 기본 전략
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스틀 전략을 사용한다.
- 이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
- 스프링 프레임워크를 사용하면 비즈니스 로직을 시작하는 서비스 계층에 @Transactional을 선언하여 트랜잭션을 시작한다. 그래서 서비스 위 계층은 준영속 상태가 된다.
- 이때 같은 트랜잭션 안에서는 같은 영속성 컨텍스트에 접근한다.
// repository1과 repository2는 같은 영속성 컨텍스트에 접근
@Transactional
public void logic() {
repository1.hello();
repository2.hello();
}
// repository3는 repository1과 repository2와 다른 영속성 컨텍스트에 접근
@Transactional
public void logic2() {
repository3.hello();
}
준영속 상태와 지연 로딩
- 앞서 이야기했듯이, 컨트롤러나 뷰 같은 프레젠테이션 계층은 준영속 상태가 된다. 이때 지연 로딩 전략을 가진 객체를 조회하면 예외가 발생한다.
@Entity
public class Order {
@Id
@GeneratedValue
private Long Id;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}
- 가령 위 Order 객체를 컨트롤러에서
getMember()
를 통해 지연 로딩 객체를 초기화하려고 하면 예외가 발생하는 것이다. - 준영속 상태의 지연 로딩을 해결하는 방법은 크게 2가지가 있다.
- 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
- OSIV를 사용하여 엔티티를 항상 영속 상태로 유지하는 방법
- 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
글로벌 페치 전략 수정
@Entity
public class Order {
@Id
@GeneratedValue
private Long Id;
@ManyToOne(fetch = FetchType.EAGER)
private Member member;
}
- 글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하면 된다. 그러면 항상 비즈니스 로직에서 연관 관계가 다 들어가 있는 객체를 반환해 줄 수 있다.
글로벌 페치 전략의 단점
- 사용하지 않는 엔티티를 로딩한다.
- 예를 들어 화면 A에서 order과 member 둘 다 필요해서 글로벌 로딩 전략을 즉시 로딩으로 설정했다. 반면 화면 B는 order 엔티티만 있으면 충분하다. 하지만 화면 B는 로딩 전략으로 인해, order를 조회하면서 사용하지 않는 member도 함께 조회하게 된다.
- N + 1 문제가 발생한다.
- 단일 조회는 괜찮지만, JPQL을 사용하여
List<Order>
을 반환하면 Order 엔티티 하나를 가져올 때마다 Order 개수 N개만큼 Member 단 건 조회 쿼리를 날리게 된다.
- 단일 조회는 괜찮지만, JPQL을 사용하여
JPQL 페치 조인
- 페치 조인을 사용하면 N + 1 문제를 해결하면서 연관된 엔티티를 한꺼번에 가져올 수 있다.
JPQL 페치 조인의 단점
- 무분별하게 사용하면 View에 맞춘 Repository 메소드가 증가하여, 프레젠테이션 계층이 데이터 접근 계층을 침범하게 된다.
- 가령, 화면 A는 order 엔티티만 필요하고 화면 B는 order 엔티티와 member 엔티티가 필요하다면, 이들을 위한 Repository 메소드가 증가하게 된다.
강제로 초기화
class OrderService {
@Transactional
public Order findOrder(Long Id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체를 강제로 초기화
return order;
}
- 손 쉽게 View에서 필요한 연관 관계를 넣어서 반환해 줄 수 있지만, 프레젠테이션 계층이 서비스 계층을 침범하고 있다.
- 따라서 비즈니스 로직을 담당하는 서비스 계층과 프레젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. 이때 FACADE 계층이 사용된다.
FACADE 계층 추가
- FACADE 계층은 프레젠테이션 계층과 서비스 계층 사이에서 프록시 객체를 초기화하는 역할을 한다.
- 기존에는 트랜잭션의 시작을 서비스에서 진행하였지만, 이제는 Facade 계층에서 시작하면 된다.
class OrderFacade {
@Autowired
private OrderSerivce orderService;
public Order findOrder(Long id) {
Order order = orderService.findOrder(id);
order.getMember().getName();
return order;
}
}
class OrderService {
public Order findOrder(Long Id) {
Order order = orderRepository.findOrder(id);
return order;
}
- 서비스 계층과 프레젠테이션 계층 사이의 의존 관계를 끊어냈지만, 계층 하나를 더 추가해야하니 복잡도가 올라간다는 단점이 있다.
준영속 상태와 지연 로딩의 문제점
- View 개발할 때 엔티티 클래스를 보고 개발하지, FACADE나 서비스 클래스까지 열어 보는 것은 번거롭다. 그래서 영속성 컨텍스트가 없는 View에서 초기화하지 않은 프록시 엔티티를 조회하는 실수가 생기게 된다.
- FACADE 계층을 사용하더라도 각 화면마다 필요한 여러 종류의 조회 메소드를 추가해야 하므로 유지 보수하기 나쁘다.
- 화면 A는 order만 필요함.
- 화면 B는 order, member가 필요함.
- 화면 C는 order, orderItem이 필요함.
OSIV
- OSIV는 Open Session In VIEW의 약자로, 영속성 컨텍스트를 View까지 열어 둔다는 뜻이다. 따라서 View에서도 지연 로딩을 사용할 수 있게 된다.
- OSIV는 하이버네이트에서 부르는 용어고, JPA에서는 OEIV라고 부른다. 하지만 둘 다 관례상 OSIV라고 부른다.
과거 OSIV: 요청 당 트랜잭션
- OSIV의 가장 단순한 구현은 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션을 끝내는 것이다.
- 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 데이터도 영속 상태를 유지한다. 따라서 View에서도 지연 로딩이 가능하므로 FACADE 계층 없이 필요 없어진다.
- 하지만 이 방식은 컨트롤러나 View 같은 프레젠테이션 계층이 엔티티를 변경할 수 있으므로, 프레젠테이션 계층에서 변경 사항이 데이터베이스에도 적용이 되는 심각한 문제가 발생할 수 있다.
- 따라서 프레젠테이션 계층에서 엔티티를 수정하지 못하도록 막아야 한다.
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 레핑
- 엔티티를 한 단계 감싼 객체를 만들고, 엔티티의 읽기 전용 메소드만 제공하는 방식
- DTO만 반환
- 위 방식 모두 코드량이 증가한다는 단점이 있어서, 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.
스프링 OSIV: 비즈니스 계층 트랜잭션
- 스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이다.
- 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단, 이때 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때, 1번에서 미리 생성해 둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝나지만 영속성 컨텍스트는 그대로 유지된다.
- 컨트롤러와 View까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.
트랜잭션 없이 읽기
- 엔티티를 변경하지 않고 단순히 조회할 때는 트랜잭션이 없어도 되는데, 이를 트랜잭션 없이 읽기라고 한다.
- 프레젠테이션 계층에는 트랜잭션이 없지만, 트랜잭션 없이 읽기를 사용해서 지연 로딩을 수행할 수 있다.
class MemberController {
public void viewMember(Long id) {
Member member = memberService.getMember(id);
member.setName("XXX");
model.addAttribute("member", member);
}
}
- OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고, 영속성 컨텍스트를 종료하므로 플러시가 발생하지 않는다. 따라서 위처럼 회원의 이름을 변경해도 문제가 없다.
- 프레젠테이션 계층에서 강제로 플러시해도 에러를 띄워서 데이터베이스에 변경 사항이 일어나지 않도록 막는다.
스프링 OSIV 주의 사항
- 프레젠테이션 계층에서 엔티티를 수정한 직후에, 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
- 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 롤백 같은 일이 발생할 때 문제가 생길 수 있다.
- 복잡한 화면은 객체 그래프를 사용하기보다는 DTO로 반환하는 것이 좋다.
- OSIV는 JVM을 벗어난 원격 상황에서는 사용할 수 없다.
- JSON으로 생성한 API를 외부 API, 내부 API로 나눌 수 있는데, 외부 API는 변경이 잦으므로 DTO를 사용하고 내부 API는 변경이 적으므로 OSIV를 사용하는 것이 좋다.
너무 엄격한 계층
- OSIV를 사용하기 전에는 프레젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화해야 했다. 그리고 초기화는 서비스 계층이나 FACADE 계층이 담당했다.
- OSIV를 사용하게 되면 영속성 컨텍스트가 프레젠테이션 계층까지 살아있으므로 미리 초기화 할 필요가 없으므로 단순한 엔티티 조회는 컨트롤러에서 Repository를 호출해도 상관 없다.
출처
김영한 - 자바 ORM 표준 JPA 프로그래밍
'스터디 > JPA 스터디' 카테고리의 다른 글
[JPA] 고급 주제와 성능 최적화 (0) | 2022.03.22 |
---|---|
[JPA] 컬렉션과 부가 기능 (0) | 2022.03.01 |
[JPA] 스프링 데이터 JPA (0) | 2022.02.05 |
[JPA] QueryDSL, 네이티브 SQL, 객체지향 쿼리 심화 (1) | 2022.01.30 |
[JPQ] 객체 지향 쿼리 소개, JPQL (0) | 2022.01.20 |
댓글