Spring Data JPA : 영속성 컨텍스트와 지연로딩

정 승 연·2023년 8월 17일
0

목록 보기
9/9
package com.moing.backend.domain.mission;

import com.moing.backend.domain.mission.entity.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;

import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@Rollback(value=false)
@SpringBootTest(properties = { "spring.config.location=classpath:application-test.yml" })
//@SpringBootTest
public class MissionCreateTest {

    @Autowired
    private TeamRepository teamRepository;
    @Autowired
    private TeamMemberRepository teamMemberRepository;
    @Autowired
    private OnceMissionRepository onceMissionRepository;
    @Autowired
    private MissionRepository missionRepository;
    @Autowired
    private TestMemberRepository testMemberRepository;

    @Test
    public void 한번미션_생셩() {
        Mission mission = new Mission();

        Team makeTeam = Team.builder()
                .name("hi")
                .build();

        teamRepository.save(makeTeam);

        for (int i = 0; i < 3; i++) {
            TestMember newMember = new TestMember("name"+i);
            testMemberRepository.save(newMember);

            TeamMember newTeams = TeamMember.builder()
                    .member(newMember)
                    .team(makeTeam)
                    .build();
            teamMemberRepository.save(newTeams);
        }
        assertThat(teamRepository.findAll().size()).isEqualTo(1);
//
        List<MissionArchive> missionArchives = new ArrayList<>();

        // fetch 주의
        Team newTeam = teamRepository.findById(1L).get();
        List<TeamMember> teamMembers = newTeam.getTeamMembers();

//        List<TeamMember> teamMembers = makeTeam.getTeamMembers();

        assertThat(teamMembers.size()).isEqualTo(3);

        for (TeamMember teamMember : teamMembers) {
            MissionArchive newMissionArchive = MissionArchive.builder()
                    .member(teamMember.getMember())
                    .status(MissionStatus.WAITING)
                    .build();

            missionArchives.add(newMissionArchive);
        }

        OnceMission newOnceMission = OnceMission.builder()
                .missionArchives(missionArchives)
                .build();
        onceMissionRepository.save(newOnceMission);

        Mission newMission = Mission.builder()
                .title("new")
                .team(newTeam)
                .dueTo(LocalDateTime.now())
                .rule("rule")
                .content("content")
                .type(MissionType.ONCE)
                .onceMission(newOnceMission)
                .build();

        missionRepository.save(newMission);

        System.out.println("title : " + newMission.getTitle());
        System.out.println("title : " + newMission.getRule());
        System.out.println("user " + newMission.getTeam().getTeamMembers().size());

    }

    @Test
    public void 반복미션_생성() {

    }
}
failed to lazily initialize a collection of role: com.moing.backend.domain.mission.entity.Team.teamMembers, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.moing.backend.domain.mission.entity.Team.teamMembers, could not initialize proxy - no Session
	at app//org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:614)
	at app//org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
	at app//org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:162)
	at app//org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:371)
	at app//com.moing.backend.domain.mission.MissionCreateTest.한번미션_생셩(MissionCreateTest.java:66)

에러가 발생한다.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.moing.backend.domain.mission.entity.Team.teamMembers, could not initialize proxy - no Session 으로,

영속성 컨텍스트가 종료되어, 지연 로딩을 할 수 없어서 발생하는 오류 이다. JPA에서 지연로딩을 하려면 객체가 영속성 컨텍스트에 있어야 한다.

영속성 컨텍스트란?

엔티티를 영구 저장하는 환경

논리적인 개념으로, 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할

@Transactional 을 추가해 영속성 컨텍스트를 유지한다. 이유는 아래에서 설명하겠다.

엔티티의 생명 주기

영속

엔티티를 영속성 컨텍스트에 저장한 상태를 말하며, 영속성 컨텍스트에 의해 관리된다.

준영속

엔티티를 영속성 컨텍스트에서 분리, 영속 상태의 엔티티를 더이상 관리하지 않는다.

commit

트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다.

영속성 컨텍스트가 엔티티를 관리하면,

1차 캐시

영속성 컨텍스트 내부의 캐시. 영속 상태의 엔티티를 이곳에 저장.

동일성 보장

실제 인스턴스가 같다

트랜잭션을 지원하는 쓰기 지연

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 insert 문을 모아둔다. 그리고 트랜잭션을 커밋할 때 모다운 쿼리를 db에 보낸다. 이것이 트랜잭션을 지원하는 쓰기 지연이다.

변경 감지(Dirty Checking)

JPA로 수정할 때는 단순히 엔티티를 조회해서 데이터를 변경하면 된다.

변경 감지에 의한 데이터 수정 과정은 다음과 같다.

  1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush가 호출된다.
  2. 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 수정 쿼리를 쓰기 지연 저장소에 저장한다.
  4. 쓰기 지연 저장소의 sql을 flush 한다
  5. 트랜잭션을 커밋한다.

변경 감지는 영속선 컨텍스트가 관리하는 영속 상태 엔티티만 적용된다.

JPA에서 영속성 컨텍스트가 관리하고 있는 엔티티를 조회하면, 해당 조회 상태로 스냅샷을 만들어 두고, 트랜잭션 끝나는 시점에 스냅샷과 비교하여 변경을 감지한다면 update 해서 데이터베이스로 전달.

Dirty Checking을 하면, 당연히 save 가 필요 없다. 하지만 Dirty Checking 은 transaction이 커밋 될 때 작동하기 때문에 transaction이 실행되도록 해주어야하는데 이 때 @Transancational 어노테이션을 사용하면 된다.

@Transancational란?

Transactional이 붙어있지 않은 경우, 준영속 상태. 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 더이상 관리하는 엔티티는 지연로딩을 할 수 없다.

트랜잭션 어노테이션이 없을 경우 @OneToMany 등 지연 로딩을 default로 사용하는 엔티티를 정상적으로 조회할 수 없다.

따라서 트랜잭션이 있어야 지연로딩이 필요한 엔티티를 정상 조회할 수 있다.

@Transanctional(readOnly=True)

위 모든 상황들을 고려했을 때, 클래스 레벨에는 공통적으로 많이 사용하는 읽기 전용 트랜잭션을 추가해 주고, 수정 및 삭제, 저장 기능이 있는 메소드에 별도로 @Transactional 어노테이션을 추가해 주는 게 효율적이라고 볼 수 있습니다.

지연 로딩

JPA 구현체들은 프록시 패턴을 통해 객체를 조회할 때 연관 객체를 바로 조회하지 않고 실제 사용할 때만 조회한다. 프록시를 사용해 조회할 때 해당 객체에 접근할 때 조회 하겠다고 요청하는데, 이것이 지연로딩.

지연로딩을 사용해 프록시 객체로 존재했을 때 해당 객체에서 실제 값을 뽑으려고 하는 행위는 불가능하다.

expected: 3
 but was: 0
org.opentest4j.AssertionFailedError: 
expected: 3
 but was: 0
	at java.base@19.0.2/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67)
	at java.base@19.0.2/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at app//com.moing.backend.domain.mission.MissionCreateTest.한번미션_생셩(MissionCreateTest.java:66)
	at java.base@19.0.2/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base@19.0.2/java.lang.reflect.Method.invoke(Method.java:578)
	at app//org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at app//org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)

그렇지만. (아마 지연로딩 때문에) 에러가 발생한다.

0개의 댓글