Post

Jpa의 Open-Session-In-View 기능과 주의점

들어가며

스프링 부트에는 Open-Session-In-View라는 옵션이 기본적으로 true로 활성화되어있다.
이 옵션은 무엇인지, 미치는 영향과 주의해야 할 점은 무엇인지 알아보겠다.


Open-Session-In-View

OSIV는 데이터베이스 세션(Hibernate/JPA EntityManager)을 요청의 시작부터 끝까지 열어두는 옵션이다.
이름 그대로 View 레이어에서 Lazy 설정된 데이터들을 사용할 수 있게 한다.

osiv

OSIV가 true로 설정되면 스프링 인터셉터에서 영속성 컨텍스트를 생성한다.
조회는 가능하지만, 트랜잭션을 사용하지 않는다면 값을 변경하더라도 DB에 반영되지 않고, 메모리상의 영속성 컨텍스트에만 반영이 된다.

세션은 데이터베이스의 커넥션과는 살짝 다르다.
커넥션은 DB 작업과 직접적으로 관련되어 있는 반면, 세션은 Lazy 로딩/변경 감지 등의 JPA 부가기능을 제공하는 개념이다.
즉, 영속성 컨텍스트를 관리하는 논리적 단위이다.


장단점

[OSIV true]

Lazy 로딩된 데이터를 컨트롤러나 뷰에서 자유롭게 사용할 수 있다.
하지만 연결 리소스가 그만큼 오래 유지되어 성능이 저하될 수 있다.
이외에도 계층 분리 원칙 위반, N+1 문제 등이 있다.

GPT에 따르면 15% ~ 20%이 성능 저하가 발생한다고 한다.

DB 작업이 필요할 때에만 커넥션을 사용한다는 것에 주의하자.

[OSIV false]

DB 리소스 사용이 최적화되고, N+1 문제 예방, 명확한 계층 분리 등의 장점이 있지만 필요한 데이터를 명시적으로 가져와야 하므로 추가 작업이 발생한다.


OSIV 논쟁

위와 같은 장단점을 살펴봤을 때, OSIV를 true 그대로 써야할까? 아니면 false로 써야할까?

https://github.com/spring-projects/spring-boot/issues/7107

오래전 깃허브 이슈에서 OSIV 기본값에 대한 논쟁이 있었다.
사람들이 주장하는 주요 내용들을 살펴보겠다.


[OSIV는 true로 해야한다.]

  1. OSIV를 true로 한다고 해서 큰 문제가 발생하지 않는다.
  2. 스프링 부트의 핵심 목표는 개발자 경험 향상이다. OSIV를 true로 설정하는 것은 초보 개발자에게 보다 완만한 학습 곡선을 제공한다.


[OSIV는 false로 해야한다.]

  1. 소프트웨어 개발의 기본 중 하나인 관심사 분리를 심각하게 어기는 기능이다.
  2. true로 했다가 성능 문제로 false로 설정했을 때 코드 변경 비용은 매우 크다. 대규모 시스템일수록 더욱 크다. 반대의 경우에는 비용이 발생하지 않는다.
  3. 프로덕션 환경에서 OSIV를 true로 사용하게 되면 성능 문제는 꽤 심각하다.


[결과]

꽤 긴 논쟁 끝에 스프링 개발자 분들은 OSIV의 기본값을 다음과 같은 이유로 true로 유지하기로 했다.

  1. false로 변경하게 되면 레거시 프로젝트의 버전업 과정에서 많은 버그가 발생할 수 있다.
  2. 성능 문제를 인식할 수준의 개발자라면 알아서 false로 설정하고 프로젝트를 시작할 것이다.
  3. 초보 개발자에게도 일단 편의성을 제공하고, 추후 학습을 통해 OSIV를 깨닫는 것이 더 이로울 것이다.

하지만 OSIV를 인식하지 못한 채 활성화하는 것은 문제가 될 수 있다고 인정하여 경고 문구가 콘솔창에 표시되도록 추가되었다.

1
2
3
4
5
6
# 별다른 OSIV 설정 없이 스프링 부트를 실행하면 다음과 같은 경고문구가 출력된다.

JpaBaseConfiguration$JpaWebConfiguration : 
spring.jpa.open-in-view is enabled by default. 
Therefore, database queries may be performed during view rendering. 
Explicitly configure spring.jpa.open-in-view to disable this warning


OSIV 설정에 따라 달라지는 오류

Lazy 로딩된 데이터를 응답데이터로 사용할 때 OSIV 설정에 따라 발생하는 오류가 달라진다.
DTO를 사용하여 응답하지만, 필드에 Lazy 데이터를 가지는 엔티티가 존재하여 직렬화 시에 순환참조가 발생하는 상황이다.


Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "users")
@Getter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Getter
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}


DTO

1
2
3
4
5
6
7
8
9
@Getter
@Setter
@AllArgsConstructor
public class UserDto {

    private Long id;
    private String name;
    private List<Post> posts;
}


Service

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository; // JpaRepository를 사용한다.

    public UserDto getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("유저가 없음"));
        return new UserDto(user.getId(), user.getName(), user.getPosts());
    }
}


Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUser(
            @PathVariable("id") Long id
    ) {
        return ResponseEntity.ok(userService.getUserById(id));
    }
}


[OSIV가 true인 경우]

1
2
3
4
# StackOverflowError 발생

Could not write JSON: 
Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamWriteConstraints.getMaxNestingDepth()`)

recursion

Post에서 다시 User를 조회하고, User에서 다시 Post 리스트를 조회하는 순환 참조가 발생한다.


[OSIV가 false인 경우]

1
2
3
4
# LazyInitializationException 발생

Could not write JSON: 
failed to lazily initialize a collection of role: jg.practice.osiv.entity.User.posts: could not initialize proxy - no Session

no_session

Controller는 프록시 객체가 담긴 응답 데이터를 전달받고, 이를 Jackson 라이브러리를 통해 응답 메세지로 변환하려 시도한다.
변환 과정에서 Lazy 데이터들을 불러와야 하는데 세션이 닫혀 있으므로 영속성 컨텍스트 접근이 불가하고 Lazy 로딩 작업에 실패하게 된다.
따라서 LazyInitializationException이 발생하게 된다.


[해결 방법]

설정에 따라 다른 오류가 발생한다는걸 보여주는게 목적이었지만, 어쨋든 어떻게 해결할 수 있는지도 살펴보자.

간단하다. Lazy 로딩된 데이터를 담지않는 DTO로 변환하여 Controller로 넘겨주던가,
JSON 직렬화 시 특정 필드를 무시하는 @JsonIgnore를 사용하여 해결할 수 있다.

참고자료

예시 프로젝트 - OSIV

[JPA] OSIV (Open-Session-In-View) 동작원리 및 주의사항

더 깊은 케이스들이 궁금하다면 이 글을 참고하자.

This post is licensed under CC BY 4.0 by the author.