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
@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<>();
...
}
BoardEntity
와 UserEntity
는 일대다 관계를 가지고 있습니다.
public void toStringTest(){
BoardEntity board = br.findByBoardNum(2);
log.info(board+": board)
}
그런 관계를 가지고 있는 상황에서 pk를 이용해 조회한 BoardEntity 객체를 board에 담고 그를 출력합니다. 이런 간단한 코드에서 전 에러를 마주하게 됐습니다.
저같은 경우 에러의 원인은 @ToString
이 친구였습니다. 그런데 @ToString
을 보기 전, 우리는 위 BoardEntity
클래스에서 집중🙌해야할 부분이 있습니다.
@ManyToOne의 fetch 타입으로 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
은 toString()
메서드를 Override 해서 각각의 필드를 출력할 수 있도록 합니다. 하지만 @ToString
은 특히 주의할 필요가 있는데, 연관 관계를 가질 경우 잘못하면 서로를 계속 참조하면서 무한 순환 참조가 발생한다고 합니다.
그러니까 A 엔티티의 toString에서는 연결된 B 엔티티를 확인하고 싶어서 b.toString을 호출하고 B 엔티티의 toString에서도 연결된 A 엔티티 데이터를 확인하고 싶어서 a.toString을 호출하는 것입니다. 결국 이는 무한 순환 참조 에러를 발생시킵니다. 😰😱
그래서 가급적이면 엔티티에서 @Data와 @ToString을 지향한다고 합니다.
그럼 어떻게 해결할까요? 다른 방법도 많습니다. 무한 순환 참조의 원인이 @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로 인해 서로를 궁금해하지 않게 된 것입니다. 이렇게 하면 출력이 잘 되는 것을 보실 수 있을 것입니다.