서로를 궁금해하는 그들

초코파이·2021년 6월 6일
0

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause

could not initialize proxy [com.~.~.entity.UserEntity#13] - no Session


  1. error: 에러가 난 상황
  2. why
  3. FetchType.LAZY: 지연 로딩이란?
  4. @ToString: 에러의 이유
  5. how: 해결 방안


error - no Session 💥

@Entity
@ToString
public class BoardEntity {
    	...
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "u_num", nullable = false)
    UserEntity userEntity;
    	...
}
@Entity
@Getter
public class UserEntity{
	...
    @OneToMany
    private List<BoardEntity> boards = new ArrayList<>();
    	...
}

BoardEntityUserEntity일대다 관계를 가지고 있습니다.

public void toStringTest(){
    BoardEntity board = br.findByBoardNum(2);
    log.info(board+": board)
}

그런 관계를 가지고 있는 상황에서 pk를 이용해 조회한 BoardEntity 객체를 board에 담고 그를 출력합니다. 이런 간단한 코드에서 전 에러를 마주하게 됐습니다.

why

저같은 경우 에러의 원인은 @ToString 이 친구였습니다. 그런데 @ToString을 보기 전, 우리는 위 BoardEntity 클래스에서 집중🙌해야할 부분이 있습니다.
@ManyToOne의 fetch 타입으로 FetchType.LAZY가 적용이 된 걸 볼 수 있습니다. 그렇다면 FetchType.LAZY는 뭘 나타내는 거길래 이 부분에 집중해야 하는 걸까요? 🔍

FetchType.LAZY - 💤 지연 로딩과 즉시 로딩

두 개의 테이블이 관계가 있을 때, 예를 들어 Board 테이블과 User 테이블이 있을 때, Board 테이블을 조회할 때 User 테이블도 함께 조회해야 할까요? 아니면 조회하지 말아야 할까요? 이를 정해주는 게 FetchType입니다. 두 개의 테이블에 관계가 있을 때 @ManyToOne 등의 어노테이션에는 fetch 타입을 줄 수 있습니다.
FetchType.LAZY는 지연 로딩입니다. FetchType.LAZY를 설정하고 Board를 조회한 후 User을 확인해보면 Proxy 객체가 조회됩니다. 후에 board.getUser.getName()으로 User 필드에 접근하려 하면 그때서야 User의 name을 찾는 쿼리가 나갑니다.

Proxy 객체란?
실제 엔티티 객체 대신에 사용되는 객체로써 실제 엔티티 클래스와 상속 관계 및 위임 관계에 있는 객체입니다. 실제 엔티티와 겉모습이 같습니다. 가짜 객체라고 보시면 될 것같습니다.

하지만 대부분의 로직에서 Board와 User을 같이 사용한다면 select 쿼리가 두 번 실행되는 것은 좋지 않습니다. 그래서 이런 경우엔 즉시 로딩 (FetchType.EAGER)을 사용합니다. 즉시 로딩을 사용하면 JOIN을 사용해서 한 번에 다 조회해 옵니다. 하지만 그럼에도 실무에서는 지연 로딩을 많이 사용합니다.
첫 째로, 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생합니다. 연관된 테이블이 많은 경우 JOIN이 여러번 일어나기 때문입니다.
둘 째로, N+1 문제가 일어납니다.
@OneToMany@ManyToMany는 기본이 지연 로딩이지만, @XXXToOne은 기본이 즉시 로딩이기때문에 LAZY로 명시해서 사용하는 것이 좋습니다.

@ToString

@ToStringtoString()메서드를 Override 해서 각각의 필드를 출력할 수 있도록 합니다. 하지만 @ToString은 특히 주의할 필요가 있는데, 연관 관계를 가질 경우 잘못하면 서로를 계속 참조하면서 무한 순환 참조가 발생한다고 합니다.
그러니까 A 엔티티의 toString에서는 연결된 B 엔티티를 확인하고 싶어서 b.toString을 호출하고 B 엔티티의 toString에서도 연결된 A 엔티티 데이터를 확인하고 싶어서 a.toString을 호출하는 것입니다. 결국 이는 무한 순환 참조 에러를 발생시킵니다. 😰😱
그래서 가급적이면 엔티티에서 @Data와 @ToString을 지향한다고 합니다.

how 💨😎

그럼 어떻게 해결할까요? 다른 방법도 많습니다. 무한 순환 참조의 원인이 @ToString 뿐만이 아닌 만큼요.
@ToString이 원인일 경우 위에 언급했던 지연 로딩을 즉시 로딩으로 바꾸는 해결 방법도 있습니다. 하지만 그런 해결은 좋지 않다 느꼈습니다. 그래서 저는 @ToString에 exclude를 사용했습니다.

@Entity
@ToString(exclude={"제외할 필드","제외할 필드", "userEntity"})
public class BoardEntity {
    ...
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "u_num", nullable = false)
    UserEntity userEntity;
    ...
}

exlude는 특정 필드를 toString에 포함되지 않게 합니다. 무한 참조가 일어나는 클래스를 exclude 하면 그 클래스의 toString을 호출하지 않습니다. 서로를 궁금해 하던 클래스들이 exclude로 인해 서로를 궁금해하지 않게 된 것입니다. 이렇게 하면 출력이 잘 되는 것을 보실 수 있을 것입니다.

profile
열심히 흡수하기

0개의 댓글