본문 바로가기

Issue 해결

[JPA Lazy 지연로딩 오류]org.hibernate.LazyInitializationException:

안녕하세요 인포돈 입니다.


 Spring JPA를 학습해보면서, LazyInitializationException 에러를 만나서 해결해보았다.

 

오류 코드

 

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.shadow.domain.shadow.entity.Keyword.flowcharts, could not initialize proxy - no Sessio

 

우선 Lazy와 Eeager에 대해 이해해야 쉽게 해결할 수 있다. 쉽게 한국말로 표현해 보면, 지연 로딩과 즉시 로딩이라고 부를 수 있다.

 

 하나의 예시를 보자

@Entity
@Getter
@Setter
public class Keyword {

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

    @Column
    private String name;

    @Column
    private Boolean favorite;

    @OneToMany(mappedBy = "keyword", cascade = {CascadeType.ALL})
    private List<Flowchart> flowcharts;

    @ManyToOne
    @JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Shadow shadow;

    public void addFlowchart(Flow flow, int seq) {
        this.flowcharts.add(new Flowchart(this, flow, seq));
    }

}

해당 Entity가 있다고 가정해 보자. 이때 각각 즉시 로딩과 지연 로딩의 결과로 어떠한 값이 돌아오는지 이해해야 한다.

 

> 즉시 로딩 (Eager loading) : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.

 

> 지연 로딩(Lazy loading) : 연관된 엔티티를 실제 사용하는 시점에 데이터베이스에서 조회한다.

 

 즉, 언제 해당 데이터를 가져오는지에 대한 차이이다. 이렇게 보면, 사실 큰 영향을 미치지 않을 거라고 생각할 수 있다. 그러나 대량의 데이터를 다루게 될 때, 이러한 오류가 쉽게 발생할 수 있다. 이러한 특성을 생각한다면, 모든 엔티티를 조회해서 영속성 컨텍스트에 올려놓는 것도, 필요할 때마다 쿼리를 매번 실행해서 연관된 엔티티를 지연 로딩으로만 진행하는 것만은 좋은 것이 아니다.

 

 나의 코드를 예시로 들기에는 너무 많은 코드가 있어, 아래 블로그에서 팀과 회원의 관계가 있는 예시를 가져왔다.

https://programmer-chocho.tistory.com/81

// 팀 엔티티
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Ch05_TEAM")
public class Ch05Team {

    ...

    @OneToMany(mappedBy = "team") // 양방향 연관관계 설정
    private List<Ch05Member> memberList = new ArrayList<>();

    ...
}
package com.study.chapter05.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Ch05_MEMBER")
public class Ch05Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne 
    @JoinColumn(name = "TEAM_ID")
    private Ch05Team team;  // 연관관계 매핑

    public Ch05Member(String id, String username) {
        this.id = id;
        this.username = username;
    }
}

 이러한 연관 관계를 가지고 있다고 해보자. 이때 회원을 조회하는 상황이 발생한다고 생각해 보자

@Test
@DisplayName("조회")
void selectTest() {
    Optional<Ch05Member> optionalCh05Member = memberRepository.findById("member1");

    if (optionalCh05Member.isPresent()) {
        Ch05Member member = optionalCh05Member.get();
    } else {
        log.debug("조회 실패!, member is null");
    }
}

Optional<Ch05Member> optionalCh05Member = memberRepository.findById("member1");

        if (optionalCh05Member.isPresent()) {
            Ch05Member member = optionalCh05Member.get(); // 회원 조회
            Ch05Team team = member.getTeam(); // 팀 조회
                }
}

 @OneToMany의 기본 전략은 지연 로딩이다. 따라서 기본적으로 해당 조회를 하게 된다면, member에는 team의 프록시 객체만 들어있고, 모든 정보를 가져오지 않게 된다. (여기서 프록시 객체는 단순히 지연로딩 후 필요할 때 꺼내올 수 있도록, 하는 객체이다.) 이때 프록시 객체를 통해서 정보를 가져오려고 할 때 영속성 컨텍스트의 도움을 받아야 하는데 이미 member를 조회한 후에는 영속성 컨텍스트를 종료한 후임으로 해당 정보를 가져올 수 없게 된다. 

 

 좀 더 쉽게 말하면 memberRepository.findById를 할 때 객체를 가져오지만, 영속성 컨텍스트가 종료되어, member.getTeam()을 해서 정보를 가져올 때 해당 오류가 발생하게 된다.

 

 이러한 문제점을 해결하는 법은 2가지가 있다.

 

 1) 전략을 Eeager로 변경한다.

앞서 말했듯이, @OneToMany의 경우 디폴트가 lazy로 되어 있다. 해당 부분은 Eager로 변경하여, 찾아올 때, 하위 객체를 모두 가져오게 만들면 된다.

 

@OneToMany(fetch= FETCH.EAGER)

 

 2) Transaction 어노테이션을 붙인다.

해당 클래스 또는 메서드에서 발생하는 오류일 경우 알맞은 곳에 해당 어노테이션을 붙여 준다. 해당 어노테이션의 역할은 영속성 컨텍스트의 유지를 해당 메서드 또는 클래스가 있는 동안 유지되도록 하는 어노테이션이다. (그러나 해당 방법은, 다른 오류사항을 일으킬 수 있으니, 주의가 필요하다.)