코드
전체 코드는 Github에 있습니다 :)
- TeamServiceTest
@RunWith(SpringRunner.class)
@SpringBootTest
public class TeamServiceTest {
@Autowired
private TeamService teamService;
@Before
public void setup() {
Team team = new Team(null, "team", new ArrayList<>());
for (int i = 1; i <= 3; i++) {
Member member = new Member(null, "member" + i, null);
member.updateTeam(team);
}
teamService.save(team);
}
@Test
public void lazy_exception() {
List<Team> teams = teamService.findAll();
}
}
- TeamService
@Service
public class TeamService {
@Autowired
private TeamRepository teamRepository;
public Team save(Team team) {
return teamRepository.save(team);
}
public List<Team> findAll() {
List<Team> teams = teamRepository.findAll();
extractMembers(teams); // Exception
return teams;
}
private void extractMembers(List<Team> teams) {
// LazyInitializationException!
teams.forEach(t -> t.getMembers().get(0).getName());
}
}
- TeamRepository
@Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
}
상황
- Serivce단에서 Team을 조회하는데 Team 내부의 MemberList는 지연로딩으로 조회를 한 상태이다.
- Team 내부에 있는 Member에 접근을 하려하니
LazyInitialiazationException
이 발생하면서, 지연로딩을 할 수 없다고 에러가 뜨는 것이다.
착각
이전에는 Service에서 Repository.findAll()
을 호출하니 Service에서는 영속성 컨텍스트가 살아있을 것이라고 착각했었다.
Team의 멤버 변수들은 접근이 가능했지만, Member에 접근하려하니 Proxy로 가져온 Member를 초기화 할 수 없다는 것이였다.
왜그럴까?
정답은 영속성 컨텍스트의 범위에 있다. Spring에서는 영속성 컨텍스트의 라이프사이클이 트랜잭션과 동일하게 유지되는데, 현재는 findAll() 에서 트랜잭션이 시작되었다가 반환이 되면서 종료가 되었기 때문에 조회해온 Team이 준영속(Detached) 상태가 되버린 것이다.
여기서 들었던 의문은 트랜잭션이 어디서 생겨서 지 혼자 닫히는 것인가.. 이다.
TeamRepository를 만들 때 상속한 인터페이스는 JpaRepository를 사용한다.
JpaRepository의 기본 구현체인 SimpleRepository를 살펴보자.
SimpleJpaRepository (Spring Data JPA 2.6.3 API)
@Repository
@Transactional(readOnly=true)
public class SimpleRepository<T, ID>
클래스 레벨에 @Transactional
이 붙어있는 것을 볼 수 있다.
@Transactional의 디폴트 옵션은 Propagation.REQUIRED
이다.
이전에 생성된 트랜잭션이 있다면 참여하고, 없다면 새로 트랜잭션을 시작하게 된다.
SimpleJpaRepository.findAll()
메소드 이름으로 트랜잭션이 생성이 되고, TeamList를 반환하고 트랜잭션을 완료처리하는 것이다.
Team은 영속성 컨텍스트에 의해 관리를 받지 않으니, 준영속 상태가 되버리고 Service단에서 Team.memberList
에 접근하려하니 LazyInitializationException
이 발생하는 것이다.
아래에서 실제 로그를 살펴보도록 하자.
로그를 살펴보자
관련된 로그만 간추려봤습니다.
o.s.orm.jpa.JpaTransactionManager <1>: Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
o.s.orm.jpa.JpaTransactionManager : Opened new. EntityManager [SessionImpl(220766773<open>)] for JPA transaction
o.h.e.t.internal.TransactionImpl : begin
o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
... <2> select 조회 ...
o.s.t.i.TransactionInterceptor <3>: Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
o.s.orm.jpa.JpaTransactionManager <4>: Closing JPA EntityManager [SessionImpl(220766773<open>)] after transaction
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: me.iseunghan.testjpa.domain.Team.members, could not initialize proxy - no Session
- <1> :
SimpleJpaRepository.findAll
메소드 이름으로 새로운 트랜잭션을 생성 - <2> : select 조회를 합니다.
- <3> :
SimpleJpaRepository.findAll
트랜잭션을 완료처리 합니다. - <4> : 영속성 컨텍스트가 닫히게 됩니다.
영속성 컨텍스트의 라이프 사이클은 트랜잭션의 라이프 사이클과 동일하다고 했습니다.
지연 로딩을 하기 위해서는 영속성 컨텍스트에 의해 관리가 되고 있어야 합니다. 하지만 <3> 트랜잭션이 완료 처리가 되면서 Team은 준영속 상태가 되면서 Member를 초기화 하려다 실패하게 된 것입니다.
해결 방법
- 1 TeamSerivce.findAll @Transactional 붙이기
TeamService.findAll()
에서부터 트랜잭션을 생성하여 전이(전파)되도록 하는 것이다.- 그렇다면,
SimpleRepository
에서@Transactonal
의 기본 속성으로 인해 트랜잭션에 참여하게 된다.
- 2 Join Fetch 사용
@Query(value = "select t from Team t join fetch t.members")
List<Team> findAllJoinFetch();
@EntityGraph(attributePaths = "members")
@Query("select t from Team t")
List<Team> findAllEntityGraph();
4. 글로벌 페치 전략을 EAGER로 변경(웬만하면 사용하지 말자)
더 알아보면 좋은 내용들
- FACADE 패턴
- OSIV(Open Session In View)
- @Transactional 속성, 설정들
REFERENCES
LazyInitializationException - What it is and the best way to fix it
응? 이게 왜 롤백되는거지? | 우아한형제들 기술블로그
https://www.inflearn.com/questions/227574