본문 바로가기

삽질

Spring Data Jpa의 LazyInitializationException과 OSIV

Spring Data Jpa로 프로젝트를 진행하면서 LazyInitializationException을 겪었었다.

LazyInitializationException은 영속성 컨텍스트에 있던 Entity가 준영속(Detached) 상태가 됐을 때 Lazy Loading을 실행해서 영속성 컨텍스트에서 해당 객체를 조회해오려고 했을 때 발생하는 예외이다.

김영한님의 Tacademy JPA 강의 정리하며 공부했기 때문에 당황하지 않고 원인을 금방 파악할 수 있었다.

프로젝트를 진행할 당시에는 해당 Entity에서 불필요한 객체를 조회해오지 않는 로직이라 Fetch Join으로 문제를 해결했었다. 그리고 Hibernate.initialize() 메서드 등 다른 방법으로도 LazyInitializationException을 해결해보고 싶어서 LazyInitializationException을 임의로 터트리고자 했다.

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    ...
}
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
    ...
}
@Service
public class TempService {
    private final MemberRepository memberRepository;
    private final TeamRepository teamRepository;

    public TempService(MemberRepository memberRepository, TeamRepository teamRepository) {
        this.memberRepository = memberRepository;
        this.teamRepository = teamRepository;
    }

    @Transactional
    public void save() {
        Team team = new Team();
        team.setName("RED팀");
        teamRepository.save(team);

        Member member = new Member();
        member.setName("dundung");
        member.setTeam(team);
        memberRepository.save(member);
    }

    public Member findById(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(IllegalArgumentException::new);
    }
}
@RestController
public class Controller {
    private TempService tempService;

    public Controller(TempService tempService) {
        this.tempService = tempService;
    }

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public Team find() {
        tempService.save();
        Member member = tempService.findById(1L);
        return member.getTeam();
    }
}

나름 최소한의 코드로 LazyInitializationException를 터트리려고 노력했다.
그런데 LazyInitializationException은 터지지 않았다. Team의 정보가 아주 잘 반환됐다.

이거 왜이러지.. 멘붕이 왔다. 이론적으로 분명 LazyInitializationException이 터져야만 하는데 안터지니까 너무 답답했다. 팀원 중 한 명에게 도움을 요청하고 같이 2시간이 넘게 열심히 디버깅과 실험을 거듭했다. (Ra빈아 고맙다...)

그러던 도중 SELECT 쿼리가 나가지 않는 것을 발견하고 의문을 갖기 시작했다.

Hibernate: 
    insert 
    into
        team
        (team_id, name) 
    values
        (null, ?)

Hibernate: 
    insert 
    into
        member
        (id, username, team_id) 
    values
        (null, ?, ?)

위의 쿼리 두개가 출력된 쿼리의 전부였다.

그럼 Member 엔티티를 조회해올 때 DB가 아니라 영속성 컨텍스트에서 조회해오고 있는 건가? 너무 이상했다. 분명 save 메서드가 끝나면 Transaction이 commit되면서 영속성 컨텍스트가 닫히고 find해올 때 select 쿼리로 DB에 찔러서 Member를 조회해와야 맞는 것이었다.

@Transactional
public void save() {
    Team team = new Team();
    team.setName("RED팀");
    teamRepository.save(team);

    Member member = new Member();
    member.setName("dundung");
    member.setTeam(team);
    memberRepository.save(member);
}

public Member findById(Long id) {
    return memberRepository.findById(id)
    .orElseThrow(IllegalArgumentException::new);
}

이러한 의문을 갖고 나를 도와주던 크루는 김영한님의 책을 뒤지고 나는 검색을 하며 이유를 알 수 있었다.

이유는 SpringBoot에서 자동으로 설정해주는 Open Session In View(OSIV) 설정 때문이었다.

Open Session In View란?

Open Session In View는 Kingbbode님의 블로그 글을 보고 이해했다. 매우 설명이 잘 되어있어서 쉽게 이해할 수 있었다.

간단히 정리하자면

Open Session In View란 뷰 렌더링 시점에 영속성 컨텍스트가 존재하지 않기 때문에 Detached 객체의 프록시를 초기화할 수 없다면 영속성 컨텍스트를 오픈된 채로 뷰 렌더링 시점까지 유지하자는 것 입니다. 즉, 작업 단위를 요청 시작 시점부터 뷰 렌더링 완료 시점까지로 확장하는 것 입니다.

Open Session In View 상태가 아닐 때

Open Session In View 상태일 때

Open Session In View 상태에서는 Controller의 Hibernate Session과 Persistence Context가 열려 있는 것을 확인할 수 있다.

뷰 렌더링 시점까지 Hibernate Session이 열려 있기 때문에 Open Session In View라는 이름이 붙은거구나.라고 이해했다.

Open Session In View에 대해 Kingbbode님의 글에 아래와 같은 내용이 있었다.

Open Session In View 패턴에 대한 많은 논쟁들이 있었지만, 결론은 Open Session In View 패턴은 레이어 아키텍처를 해치는 안티패턴이 아니라는 것 입니다. 이 내용에 대한 자세히 알고 싶다면 꼭 참조한 문서를 읽어보시길 바랍니다.

kingboode님이 참조한 문서, Eternity’s Chit-Chataeternum - Open Session in View를 읽어보았다. 나에게는 어려운 내용이어서 100% 이해는 못했지만 Open Session In View가 안티패턴이 아니라는 것에는 납득할 수 있었다.

인상 깊었던 내용을 적어두어야겠다.

나는 OPEN SESSION IN VIEW 패턴이 DOMAIN MODEL 패턴의 원칙과 원리에 부합하는 객체지향적인 도메인 레이어를 개발할 수 있도록 해주는 훌륭한 솔루션이라고 생각한다. 물론 OPEN SESSION IN VIEW 패턴 이외에도 뷰에서의 렌더링 이슈를 해결해 주는 다양한 솔루션이 존재한다. 이들 대부분은 기존의 상태 없는(Stateless) 애플리케이션 레이어 SERVICE 대신 상태를 가진(Stateful) SERVICE 나 플로우 기반으로 영속성 컨텍스트를 확장하여 컨버세이션(conversation)을 구현하는 방식을 사용하고 있다. 개인적으로 이러한 기술에 대한 깊은 지식이 없기 때문에 장단점을 논의하기는 어렵지만 분산되지 않은 환경에서 상태 없는애플리케이션 레이어 SERVICE 를 사용하고 있다면 OPEN SESSION IN VIEW 패턴을 선택하는 것은 올바른 결정이라는 점을 강조하고 싶다.


결론적으로 application.properties에서 아래와 같은 설정으로 default로 걸려있던 OSIV를 false로 설정하니 LazyInitializationException이 예상대로 잘 터졌다.

spring.jpa.open-in-view=false

알고보니 내가 예전에 LazyInitializationException를 경험했던 우리 프로젝트는 한 팀원이 OSIV를 이미 false로 걸어두었었다.

이번 삽집을 통해 팀 프로젝트를 하면서 내가 작성한 코드가 아닌 코드를 천천히 살펴보고 이해했다고 생각했는데 그냥 넘어간 부분도 있다는 것에 반성을 하게되었다. 앞으로는 바쁘더라도 추측으로 이해했다고 생각하고 넘어가지말고 확실히 이해하고 넘어가려고 더 노력해야겠다. 또 설정이 되어있는지 모르고 헤매면서 편리하다고만 생각한 SpringBoot의 자동 설정에 대한 문제점?을 조금이나마 알 수 있었다. 물론 내가 무지한 것도 있다.😅