Close
Close full mode
logo만렙 개발자 키우기

(3) OSIV

Git RepositoryEdit on Github
Last update: 9 months ago by nowwaterReading time: 8 min

OSIV (Open Session In View)

OSIV 의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것

OSIV 는 하이버네이트에서 사용하는 용어. JPA에서는 OEIV(Open EntityManager In View)라 한다. 하지만 관례상 모두 OSIV 로 부른다 !

결국 모든 문제는 엔티티가 프레젠테이션 계층에서 준영속 상태이기 때문에 발생한다.

따라서 영속성 컨텍스트를 뷰까지 열어둠으로써 뷰에서도 지연 로딩을 사용할 수 있다.


13.3.1 과거 OSIV : 요청 당 트랜잭션 방식

클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 끝내는 방법

image

이렇게하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속 상태를 유지한다. -> 뷰에서 지연 로딩이 가능하므로 엔티티를 미리 초기화할 필요 X, FACADE 계층 없이도 뷰에 독립적인 서비스 계층을 유지할 수있다.

요청 당 트랜잭션 방식의 OSIV 문제점

컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다.

즉, 렌더링 이후 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시한다. 이때 영속성 컨텍스트의 변경 감지 기능이 작동해서 변경된 엔티티를 데이터베이스에 반영해버린다.

ex) 고객의 이름을 XXX로 변경해서 노출하길 원할 경우 member.setName("XXX") 를 하면 DB의 정보도 변경되버림!

이렇게되면 애플리케이션을 유지보수하기 매우 힘들어진다. 따라서 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막아야한다.

해결방법

위와 같은 문제를 해결하려면 프레젠테이션 계층에서 엔티티를 수정하지 못하도록 막으면 된다.

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

엔티티를 읽기 전용 인터페이스로 제공

: 엔티티를 직접 노출하는 대신 읽기 전용 메소드만 제공하는 인터페이스를 프레젠테이션 계층에 제공하는 방법

interface MemberView {
public String getName();
}
@Entity
class Member implements MemberView {
...
}
class MemberService {
public MemberView getMember(id){
return memberRepository.findById(id); // 프레젠테이션 계층에는 읽기 전용 메소드만 있는 인터페이스를 제공
}
}

엔티티 레핑

: 엔티티의 읽기 전용 메소드만 가지고 있는 엔티티를 감싼 객체를 만들고 이를 프레젠테이션 계층에 반환

class MemberWrapper {
private Member member;
public MemberWrapper (Member member){
this.member = member;
}
// 읽기 전용 메소드만 제공
public String getName(){
return member.getName();
}
}

DTO만 반환

프레젠테이션 계층에 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO 를 생성해서 반환한다.

이 방법은 OSIV 를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 한다.

class MemberDTO {
private String name;
// Getter, Setter
}
...
MemberDTO memberDTO = new MemberDTO();
memberDTO.setName(member.getName());
return memberDTO;

엔티티와 거의 비슷한 MemberDTO를 만들고 엔티티 값을 여기에 채워서 반환

이러한 방법들은 모두 코드량이 상당히 증가한다는 단점이 있다.

따라서 최근에는 거의 사용하지 않는다.

문제점들을 보완해서 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다 -> 스프링 OSIV


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

비즈니스 계층에서 트랜잭션을 사용하는 OSIV - 요청 당 트랜잭션 방식의 OSIV 를 썼을 때 프레젠테이션 계층에서 데이터를 변경 가능한 문제를 해결해준다.

image

  1. 클라이언트의 요청이 들어오면 영속성 컨텍스트를 생성한다. 이때 트랜잭션은 시작하지 않는다.

  2. 서비스 계층에서 @Transactional 로 트랜잭션을 시작하면 앞에서 생성해둔 영속성 컨텍스트에 트랜잭션을 시작한다.

  3. 비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시한다.

  4. 이때 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.

  5. 이후 클라이언트의 요청이 끝날 때 플러시를 호출하지 않고 영속성 컨텍스트를 종료한다.

트랜잭션 없이 읽기

엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 된다. 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.

  • 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
  • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. -> 트랜잭션 없이 읽기(Nontransactional reads)

스프링이 제공하는 OSIV를 사용하면 프레젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없다. -> 기존 OSIV의 단점 보완

특징 정리

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

ex) 고객의 이름을 XXX로 변경해서 노출하길 원해서 member.setName("XXX") 를 할 경우

  1. 트랜잭션을 사용하는 서비스 계층이 끝날 때memberService.getMember(id), 트랜잭션이 커밋되면서 이미 플러시 되었다. 요청이 끝날 때 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 em.close() 로 영속성 컨텍스트만 종료해 플러시가 일어나지 않는다.

2) 프레젠테이션 계층에서 em.flush() 를 호출해서 강제로 플러시해도 트랜잭션 범위 밖이므로 데이터 수정이 불가능하다는 예외가 발생한다.

따라서 프레젠테이션 계층에서 영속 상태의 엔티티를 수정해도 수정 내용이 DB에 반영되지 않는다.

주의사항

프레젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.

image

class MemberController {
public String viewMember(Long id){
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상 이유로 고객 이름을 XXX로 변경
memberService.biz(); // 비즈니스 로직
return "view";
}
}
class MemberService {
@Transactional
public void biz(){
// ... 비즈니스 로직 실행
}
}
  • 컨트롤러에서 회원 엔티티를 조회하고 이름을 member.setName("XXX") 로 수정
  • biz() 메소드를 실행해서 트랜잭션이 있는 비즈니스 로직을 실행
  • 트랜잭션 AOP 가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작. 그리고 biz() 메소드를 실행
  • biz() 메소드가 끝나면 트랜잭션 AOP는 트랙잭션을 커밋하고 영속성 컨텍스트를 플러시. 이때 변경 감지가 동작하면서 회원 엔티티의 수정사항을 DB에 반영

컨트롤러에서 엔티티 수정 후 즉시 뷰를 호출하는 것이 아니라 트랜잭션이 동작하는 비즈니스 로직을 호출해서 발생하는 문제

단순한 해결 방법

트랜잭션이 있는 비즈니스 로직을 모두 호출 후 엔티티를 변경하면 된다!

보통은 컨트롤러에서 비즈니스 로직을 먼저 호출하고 그 결과를 조회하는 순서로 실행하므로 사실 이런 문제는 거의 발생하지 않는다.

이러한 문제가 왜 발생할까

스프링 OSIV 는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어서 이런 문제가 발생한다.

OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략 은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같아서 이런 문제 발생 X


13.3.3 OSIV 정리

스프링 OSIV의 특징

  • OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서, 요청이 끝날 때 까지 같은 영속성 컨텍스트를 유지한다.
    따라서 한 번 조회한 엔티티는 요청이 끝날 때 까지 영속 상태를 유지한다.
  • 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다.
    트랜잭션이 없는 프리젠테이션 계층은 지연 로딩을 포함해서 조회만 가능하다.

스프링 OSIV의 단점

  • OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다. 특히 트랜잭션 롤백 시 주의 (15장 내용)
  • 프레젠테이션 계층에서 엔티티 수정 후 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다.
  • 프레젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝 시 확인해야 할 부분이 넓다.

OSIV 가 만능 방법은 아니다

  • OSIV를 사용하면 화면 출력 시, 엔티티를 유지하면서 객체 그래프를 마음껏 탐색 가능. 하지만 복잡한 화면 구성 시에는 효과적이지 않다

    • ex) 복잡한 통계 화면, 수많은 테이블 조인해서 보여주는 복잡한 관리자 화면

    • 그럴 때에는 처음부터 통계 데이터를 구상하기 위한 JPQL을 작성해서 DTO로 조회하는 것이 효과적이다.

OSIV 는 같은 JVM을 벗어난 원격 상황에서는 사용 불가

  • JSON 이나 XML 을 생성할 때는 지연 로딩을 사용할 수 있지만, 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다.
  • 결국 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다. 이때 변환 대상 객체로 엔티티를 직접 노출하거나 DTO를 사용해서 노출한다.
  • JSON 으로 생성한 API는 두 가지로 나눌 수 있다.

    외부 API
    : 외부에 노출하며, 한 번 정의하면 수정하기 어렵다. 서버와 클라이언트를 동시에 수정하기 어렵다.
    ex) 타팀과 협업하기 위한 API, 타 기업과 협업하는 API

    내부 API
    : 외부에 노출하지 않으며, 언제든 변경할 수 있다. 서버와 클라이언트를 동시에 수정할 수 있다.
    ex) 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출


엔티티는 생각보다 자주 변경된다. 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출하는 JSON API도 함께 변경된다.

따라서 외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다.

내부 API는 엔티티를 변경해도 클라이언트와 서버를 동시에 수정할 수 있어서 실용적인 관점에서 엔티티를 직접 노출하는 방법도 괜찮다.

13.3.4 너무 엄격한 계층

OSIV를 사용하기 전에는 프레젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화해야 했다. 그리고 초기화는 아직 영속성 컨텍스트가 살아있는 서비스 계층이나 FACADE 계층이 담당했다.

하지만 OSIV를 사용하면 영속성 컨텍스트가 프레젠테이션 계층까지 살아 있어 미리 초기화할 필요가 없다.

따라서 단순한 엔티티 조회는 컨트롤러에서 레포지토리를 직접 호출해도 아무런 문제가 없다!

image

🚀 JPA — Previous
(2) 준영속 상태와 지연 로딩
Next — 🚀 JPA
(4) 정리