본문 바로가기
학습/JPA

[JPA] Open Session In View (OSIV)

by KKambi 2021. 5. 23.

사전 개념1 - Persistence Context & Transaction

- 스프링에서 엔티티 객체를 관리하는 영속성 컨텍스트는 트랜잭션과 1:1로 연결됩니다.
- 즉, 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 동일합니다.
- 그래서 트랜잭션이 종료될 때 엔티티 매니저가 컨텍스트를 flush하고, 변경사항을 commit 합니다.

 

 

사전 개념2 - Lazy Loading & Proxy Intiailization

- Lazy loading으로 연관 관계를 맺고 있는 객체에는, 초기화 전까지 프록시 객체가 할당되어 있습니다.
- 연관 엔티티의 데이터에 접근할 때 영속성 컨텍스트에 실제 엔티티가 없다면 객체를 생성합니다 (프록시 초기화)
- member.getTeam()처럼 연관 객체 자체에 접근할 때가 아닌, member.getTeam().getIdx()처럼 연관 프록시 객체의 데이터에 접근할 때 프록시 초기화가 이루어집니다.
- 결국 프록시 초기화는 영속성 컨텍스트의 도움을 받아 이뤄진다 할 수 있습니다.

 

 

등장 배경

- 따라서 트랜잭션이 종료되면 영속성 컨텍스트가 닫히게 됩니다.
- 그런데 일반적으로 우리는 Spring Layer 중 Service Layer에서 트랜잭션의 시작과 종료를 설정합니다.
- 결국 Controller Layer에서 지연 로딩된 연관 객체의 정보에 접근할 때 프록시 초기화가 불가능해집니다.
(ex. 서비스 메소드로 거쳐 조회한 Student 엔티티 객체의 연관 School 정보를 API 응답에 담을 때)

어떤 경우에 이런 문제가 발생하는지 간단한 코드로 살펴볼게요.
Team과 Member 엔티티가 존재하며, 1:N 단방향 관계라고 합시다.
하나의 팀에는 다수의 회원이 속할 수 있고, 컨트롤러에서 응답으로 회원 정보를 반환할 때 속한 팀도 같이 반환하려고 합니다.
Member 객체를 조회할 때 Team 연관 객체는 지연 로딩하도록 설정했습니다.

// Domain
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long idx;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long idx;
}

// Controller
@RequiredArgsConstructor
@RestController
public class MemberController {
	
    private final MemberService memberService;
    
    @GetMapping("/v1/members/{memberIdx}")
    public ResponseEntity<MemberResponse> getMemberWithTeam(@PathVariable Long memberIdx) {
    	Member member = memberService.getMemberWithTeam(memberIdx);
        
        return ResponseEntity.ok(MemberResponse.from(member));
    }
}

// Service
@RequiredArgsConstructor
@Service
public class MemberController {
	
    private final MemberRepository memberRepository;
    
    @Transactional(readOnly = true)
    public Member getMemberWithTeam(Long memberIdx) {
    	return memberRepository.findById(memberIdx)
      		.orElseThrow(IllegalArgumentException::new); 
    }
}

// DTO
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MemberResponse {

    private Long idx;
    private TeamResponse team;
    
    public static MemberResponse from(Member member) {
    	return new MemberResponse(
            member.getIdx(), 
            TeamResponse.from(member.getTeam())
        );
    }
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TeamResponse {

    private Long idx;
    
    public static TeamResponse from(Team team) {
    	return new TeamResponse(
            team.getIdx(), 
        );
    }
}

코드를 보시면 트랜잭션은 서비스 메소드에 설정되어있고, 컨트롤러에서 MemberResponse.from() 팩토리 메소드를 사용하면서 조회한 Member 객체의 Team 객체의 idx에 접근하게 됩니다. 영속성 컨텍스트는 트랜잭션 종료와 함께 닫히게 되고, 지연 로딩된 Team 객체를 초기화할 수 없는 문제가 발생합니다.

그런데 우리는 이런 상황에서 예외를 마주한 적이 없어요. 왜 그럴까요?
스프링 부트에서 자동으로 설정해주는 Open Session In View(OSIV)를 true로 설정해놓기 때문입니다.

 

 

Open Session In View (OSIV)

이름에서 알 수 있듯이 View Layer에서 Session을 열겠다는 설정인데요. Session은 JPA의 구현체인 Hibernate에서 어플리케이션과 데이터베이스 사이의 인터페이스를 제공하는 객체이며, JDBC 연결을 래핑하고 1차 캐시를 관리합니다. 영속성 컨텍스트의 역할을 한다고 보면 되겠습니다.

MVC 패턴을 사용하는 어플리케이션들은 서버에서 View 객체를 반환했기에 "In View"라는 이름이 붙었습니다.
(서버가 데이터를 서빙하는 API의 역할만을 수행하게 되면서, View의 의미가 없어지고 있긴 하지만요)

결과적으로 트랜잭션은 서비스 레이어에서 종료되지만, 영속성 컨텍스트는 View Layer까지 열려있게 되면서 프록시 객체의 초기화가 가능해지는 것입니다.

만약 설정 파일에서 해당 설정을 false시킨다면 LazyInitializationException이 발생하게 될 것입니다.

// application.properties
spring.jpa.open-in-view=false

 

 

궁금증 - 트랜잭션은 왜 보통 서비스 레이어에 설정할까?

OSIV를 공부하다 궁금해진 게 있었는데요. 메소드를 프록시 객체로 감싸 트랜젝션의 BEGIN / COMMIT, ROLLBACK을 관리해주는 @Transactional 어노테이션을 왜 항상 Service Layer에 설정하냐는 것입니다. 컨트롤러 레이어에서 트랜잭션을 시작하고 종료하면 OSIV라는 설정이 필요 없지 않았을텐데 말이에요.

구글링을 좀 해보니, Controller - Service - Repository의 레이어 구조에서 트랜잭션을 설정하기 제일 적합한 위치가 서비스쪽이라고 합니다.

  • Controller Layer는 Data Persistence의 작업을 알 수 없다.
  • Service Layer에서 다양한 DAO(또는 Repository)를 통해 필요한 데이터를 가져온다. 특정 서비스 메소드에서 다수의 트랜잭션을 수행하기도 한다.
  • Service Method는 use-case에 기반하여 만들어진 비즈니스 로직으로, 재사용을 고려하여 구현된 것인데 프레젠테이션 레이어의 수정이 서비스 코드에 영향을 끼쳐선 안 된다.

물론 스프링은 Transaction Boundary를 어디에 설정하든 신경쓰진 않지만, 이런 점을 고려해봤을 때 서비스 레이어에 위치시키는 것이 Best Practice라는 것이죠. 게다가 대부분의 개발자들이 이 관행을 따른다는 점에서 좋기도 합니다.

 

 

 

참고

간단하게 개념을 훑기 좋은 우아한테크코스 포스트입니다.

 

Open Session In View

상황 이번 포스팅은 Spring boot와 JPA를 활용하여 개인 프로젝트를 개발 중 JPA…

woowacourse.github.io

 

등장 배경부터 기본 개념까지 자세하게 설명해주신 블로그 글입니다.

 

Spring - Open Session In View

Spring에서 ORM을 사용하여 개발을 하며, Transaction 을 이해할 때 쯔음 닥쳐온 혼란이 있습니다. 지인에게 자신있게 Transaction 을 설명해주기 위해 Spring Boot로 빠르게 어플리케이션을 올렸고 @GetMapping(

blog.kingbbode.com

 

 

MVC패턴 -> API서버 패러다임 전환에 대한 생각을 정리한 글입니다.
많이 헷갈렸었는데 이 글을 보고 저도 어느 정도 정리가 되었어요.

 

API 서버와 MVC에 관한 소고

0. 앨런 케이가 OOP를 이야기하며 스몰토크를 만들었을 때 결국 가장 크게 염두에 둔 것은 GUI에 대한 접근이었다. 그리고 GUI는 실제로 아이콘을, 버튼을, 창의 동작을 어떻게 컴퓨터에게 이해시킬

blog.kivol.net

 

트랜잭션을 서비스 레이어에서 시작하는 이유를 정리한 글들입니다.

 

Why use @Transactional with @Service instead of with @Controller

I have seen many comments in stack-overflow articles I found certain things about either @Transactional use with @Service or with @Controller "Usually, one should put a transaction at the ser...

stackoverflow.com

 

Where should "@Transactional" be place Service Layer or DAO

Firstly it is possible that I am asking something that has been asked and answered before but I could not get a search result back. We define transactional annotations on service layer typical spring

stackoverflow.com

'학습 > JPA' 카테고리의 다른 글

[Querydsl] 우아한테크콘서트 2020 Querydsl 강의 정리  (5) 2021.01.10

댓글