Spring/JPA

[JPA] OSIV란?

ummchicken 2023. 2. 7. 22:49

모든 출처는 김영한 개발자님 [자바 ORM 표준 JPA 프로그래밍] 입니다.

 

 

예전에 정리한 적 있었는데, 당최 이해가 가야말이지...

그래서 결국 또다시 정리한다;;;

(파도파도 끝이 없는 코딩의 세계)

 

 

✔️ OSIV란?

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
영속성 컨텍스트가 살아있으면, 엔티티는 영속 상태로 유지된다.
따라서 View에서도 지연 로딩을 사용할 수 있다.

※ 영속성 컨텍스트(persistence context) : 엔티티를 영구 저장하는 환경
 영속성 컨텍스트에 엔티티를 저장하면, 이 엔티티는 언제 데이터베이스에 저장될까?
→ JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다.
→ 이것을 플러시(flush)라 한다.

 

 

💡 OSIV의 핵심!

View에서도 지연 로딩이 가능하도록 하는 것이다!
🚨 만약 지연 로딩일 때, 영속성 컨텍스트가 없는 View에서 초기화하지 않은 프록시 엔티티를 조회한다면?
→ LazyInitializationException 발생.
∵ 초기화는 영속성 컨텍스트의 도움을 받아야 하므로, 준영속 상태의 프록시를 초기화하면 문제 발생.


※ 프록시 객체 : 가짜 객체. 지연로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것이 프록시 객체이다.
※ 프록시 객체가 초기화되면, 프록시 객체를 통해서 실제 엔티티에 접근 가능.


아 진짜 몇 번째 정리하는 건데 개어렵네;;;

※ 지연로딩과 프록시

더보기

1. 지연 로딩 설정 : FetchType.LAZY 지정.

2. 지연 로딩 실행 코드

Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용
지연로딩, 회원을 조회할 때 팀 지연로딩

→ em.find(Member.class, "member1")를 호출하면, 회원만 조회하고 팀은 조회X.

→ 대신에 조회한 회원의 team 멤버 변수에 프록시 객체를 넣어둔다.

→ Team team = member.getTeam(); : 반환된 팀 객체는 프록시 객체다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 그래서 지연로딩이라 한다.

team.getName(); : 이처럼 실제 데이터가 필요한 순간이 되면, 데이터베이스를 조회해서 프록시 객체를 초기화한다.

 

※ 조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체 사용할 이유 X.

따라서 프록시가 아닌 실제 객체를 사용함.

 

※ 프록시 초기화

 

 

 

 

흐름을 따라가보자.

 

✔️ 과거 OSIV : 요청 당 트랜잭션

초창기 OSIV: 요청 당 트랜잭션

 

  • 위 그림처럼, 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다.
  • 이렇게하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로, 조회한 엔티티도 영속 상태를 유지한다.
  • 이제 View에서도 지연 로딩을 할 수 있으므로 엔티티를 미리 초기화할 필요가 없다.

 

 

🚨 하지만 이런 '요청 당 트랜잭션 방식의 OSIV'의 문제점은?

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

 

 

예를 들어보자.

만약, 고객 이름을 출력해야 하는데, 보안상의 이유로 고객 이름을 XXX로 변경해서 출력해야 한다고 가정하자.

class MemberController {
	
    public String viewMember(Long id) {
    	Member member = memberService.getMember(id);
        member.setNmae("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경
        model.addAttrivute("member", member);
        ...
    }
}
  1. 컨트롤러에서 고객의 이름을 XXX로 변경해서 렌더링 할 뷰에 넘겨주었다.
  2. 개발자의 의도는 단순히 View에 노출할 때만 고객 이름을 XXX로 변경하고 싶은 것이지, 실제 데이터베이스에 있는 고객 이름까지 XXX로 변경하고 싶은 것은 아니었다.
  3. 하지만, 요청 당 트랜잭션의 방식의 OSIV는 View를 렌더링한 후에 트랜잭션을 커밋한다.
  4. 트랜잭션을 커밋하면 무슨 일이 일어나겠는가?
  5. 당연히 영속성 컨텍스트를 플러시(flush)한다. (플러시: 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업)
  6. 이때 영속성 컨텍스트의 변경 감지 기능이 작동해서, 변경된 엔티티를 데이터베이스에 반영해버린다.
  7. 결국 데이터베이스의 고객 이름이 XXX로 변경되는 심각한 문제가 발생한다.

 

 

💡 서비스 계층처럼 비즈니스 로직을 실행하는 곳에서 데이터를 변경하는 당연하지만, 

프리젠테이션 계층에서 데이터를 잠시 변경했다고 실제 데이터베이스까지 변경 내용이 반영되면, 

애플리케이션을 유지보수하기 상당히 힘들어진다.

 

 

💡 이런 문제를 해결하려면?

→ 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막으면 된다.

 

 

프리젠테이션 계층에서 엔티티를 수정하지 못하게 하는 방법들은 다음과 같다.

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 레핑
  • DTO만 반환 : 엔티티와 거의 비슷한 DTO를 만들고, 엔티티의 값을 여기에 채워서 반환한다.

 

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

 

적절한 도구를 사용해서 프리젠테이션 계층에서 엔티티의 수정자(Setter)를 

호출하는 코드를 잡아내는 것도 하나의 방법이지만 쉽지 않다.

 

 

지금까지 설명한 OSIV는 요청 당 트랜잭션 방식의 OSIV다.

이것은 지금까지 설명했던 문제점들로 인해, 최근에는 거의 사용하지 않는다.

 

 

💡 최근에는 이런 문제점을 보완해, 
비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.

스프링 프레임워크가 제공하는 OSIV가 바로 이 방식을 사용하는 OSIV다.

 

 


 

✔️ 스프링 OSIV 분석

이전에 설명했던 요청 당 트랜잭션 방식의 OSIV는 

프리젠테이션 계층에서 데이터를 변경할 수 있다는 문제가 있다.

 

하지만 스프링이 제공하는 OSIV는 이런 문제를 어느정도 해결해준다.

 

 

💡 스프링이 제공하는 OSIV는 
"비즈니스 계층에서 트랜잭션을 사용하는 OSIV"다.

이름 그대로 OSIV를 사용하기는 하지만, 
트랜잭션은 비즈니스 계층에서만 사용한다는 뜻이다.

 

 

✔️ 스프링 OSIV - ON 상태

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

 

💡 위 그림의 동작 원리는 다음과 같다.

 

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

 

 

✔️ 트랜잭션 없이 읽기

💡 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데,
이것을 트랜잭션 없이 읽기라 한다.
  • 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다.
  • 만약 트랜잭션 없이 엔티티를 변경하고, 영속성 컨텍스트를 플러시하면 예외가 발생한다.
  • 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.

 

 

💡 정리하면 다음과 같다.

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

 

 

💡 스프링 OSIV를 사용하면, 프리젠테이션 계층에서는 트랜잭션이 없으므로, 엔티티를 수정할 수 없다.
  • 따라서 프리젠테이션 계층에서 엔티티를 수정할 수 있는 기존 OSIV의 단점을 보완했다.
  • 그리고 트랜잭션 없이 읽기를 사용해서, 프리젠테이션 계층에서 지연 로딩을 사용할 수 있다.

 

 

정리해보면 스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV는 다음과 같은 특징이 있다.

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

 

 

예를 들어보자. 위에 들었던 예제 코드이다.

class MemberController {
	
    public String viewMember(Long id) {
    	Member member = memberService.getMember(id);
        member.setNmae("XXX"); // 보안상의 이유로 고객 이름을 XXX로 변경
        model.addAttrivute("member", member);
        ...
    }
}
  1. 컨트롤러에서 회원 엔티티를 member.setNmae("XXX")로 변경했다.
  2. 프리젠테이션 계층이지만, 아직 영속성 컨텍스트가 살아있따.
  3. 만약 영속성 컨텍스트를 플러시하면, 변경 감지가 동작해서 데이터베이스에 해당 회원의 이름을 XXX로 변경할 것이다.
  4. 하지만 여기서는 2가지의 이유로 플러시가 동작하지 않는다.
    1. 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면, 영속성 컨텍스트를 플러시해야 한다. 하지만 트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 이미 플러시해버렸다. 그리고 스프링이 제공하는 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 플러시를 호출하지 않고, em.close()로 영속성 컨텍스트만 종료해 버리므로 플러시가 일어나지 않는다.
    2. 프리젠테이션 계층에서 em.flush()를 호출해서 강제로 플러시를 해도, 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외가 발생한다.
  5. 따라서 예제는 프리젠테이션 계층에서 영속 상태의 엔티티를 수정했지만, 수정 내용이 데이터베이스에는 반영되지 않는다.

 

 


 

✔️ OSIV 정리

💡 스프링 OSIV의 특징

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

 

 

💡 스프링 OSIV의 단점

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

 

 

💡 OSIV를 사용하지 않는 대안이 몇가지가 있는데, 어떤 방법을 사용하든 결국 준영속 상태가 되기 전에 프록시를 초기화 해야 한다. 

다른 방법은 엔티티를 직접 노출하지 않고, 엔티티와 거의 비슷한 DTO를 만들어서 반환하는 것이다.

어떤 방법을 사용하든 OSIV를 사용하는 것과 비교해서 지루한 코드를 많이 작성해야 한다.

 

 

 


 

 

으 어렵다 어려워;;; 

 

 

 

 

 

'Spring > JPA' 카테고리의 다른 글

[JPA] Querydsl 찍먹해보기  (0) 2023.02.06
[JPA] MyBatis와 JPA, 도대체 뭐가 다를까?  (0) 2023.02.06
스프링 데이터 JPA - (2)  (0) 2022.12.20
스프링 데이터 JPA - (1)  (0) 2022.12.19
JPA 활용 2 - API 개발과 성능 최적화  (0) 2022.12.19