Repository 테스트 코드에서 1차 캐시로 인한 null 발생
우선 테스트하려고 하는 2개의 클래스이다.
Scrum
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column(nullable = false)
private String name;
@CreatedDate
@Column(nullable = false)
private LocalDateTime createdDate;
@Column(nullable = true)
private LocalDateTime deleteDate;
@Column(nullable = false)
private int currentMember;
@Column(nullable = false)
private int maxMember;
@OneToOne(mappedBy = "scrum", fetch = FetchType.LAZY)
private ScrumInfo scrumInfo;
ScrumInfo
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "scrum_id")
private Scrum scrum;
@Column(nullable = false)
private String subject;
@Column(nullable = true)
private LocalDateTime startTime;
@Column(nullable = true)
private LocalDateTime endTime;
@Column(nullable = false)
여기서 Scrum과 ScrumInfo는 1:1의 관계를 가지고있다.
1. 스크럼 생성 -> 주제, 최대인원, 제목을 입력
2. Scrum 객체 만들어서 save
3. ScrumInfo 객체 만들어서 scrum 집어넣고 save
이 순서대로 동작한다. Scrum은 하나의 ScrumInfo만 가져야하므로 1:1 관계를 만들었다.
아래는 내가 테스트하려고 하는 쿼리이다.
@Query("SELECT s FROM Scrum s JOIN FETCH s.scrumInfo JOIN FETCH s.user WHERE s.team = :team AND s.deleteDate IS NULL")
Optional<List<Scrum>> findByTeamWithFetchJoinUserAndScrumInfoAndDeleteDateIsNull(Team team);
Scrum에는 ScrumInfo와 User, Team에 대한 정보가 들어있다.
여기서 ScrumInfo와 User는 조회시에 사용하기때문에 FETCH JOIN을 사용해서 N+1 문제를 해결하였다.
위 쿼리는 삭제시간이 없고, 입력받은 팀과 같은 스크럼을 조회하는 쿼리이다.
아래는 내가 작성한 테스트코드이다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ScrumRepositoryTest {
@Autowired
private ScrumRepository scrumRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private TeamRepository teamRepository;
@Autowired
private ScrumInfoRepository scrumInfoRepository;
private Team team;
@BeforeEach
void setUp(){
User user = User.builder()
.email("test@test.com")
.profileImage("ex")
.nickname("ex")
.isOnline(false)
.type("kakao").build();
userRepository.save(user);
team = Team.builder()
.user(user)
.maxMember(15)
.name("팀이름")
.currentMember(1)
.description("팀설명")
.teamProfileImage("팀프로필사진")
.build();
teamRepository.save(team);
Scrum scrum = Scrum.builder()
.user(user)
.team(team)
.currentMember(1)
.name("스크럼이름")
.maxMember(15).build();
scrumRepository.save(scrum);
ScrumInfo scrumInfo = ScrumInfo.builder()
.isStart(false)
.scrum(scrum)
.subject("주제").build();
scrumInfoRepository.save(scrumInfo);
}
@Test
@DisplayName("삭제되지 않고 현재 팀에 속한 scurm 모두 조회")
void 삭제되지_않고_팀에_속한_스크럼_모두조회(){
// given
// when
List<Scrum> scrums = scrumRepository.findByTeamWithFetchJoinUserAndScrumInfoAndDeleteDateIsNull(team).get();
// then
Assertions.assertFalse(scrums.isEmpty());
Assertions.assertNotNull(scrums.get(0).getScrumInfo());
Assertions.assertNotNull(scrums.get(0).getUser());
User fetchUser = scrums.get(0).getUser();
Assertions.assertEquals("test@test.com", fetchUser.getEmail());
ScrumInfo fetchScrumInfo = scrums.get(0).getScrumInfo();
Assertions.assertEquals("주제", fetchScrumInfo.getSubject());
}
BeforeEach에서 유저, 팀, 스크럼, 스크럼인포를 차례대로 생성해서 저장하고 조회 쿼리를 바로 동작시켰는데 여기서 문제가 발생했다.
분명 유저와 팀은 잘 들어가있는데 ScrumInfo가 null이 발생하는 문제가 생긴 것이다.
근데 실제 서버를 켜서 Swagger로 해당 Repository 쿼리를 돌려보면
이렇게 ScrumInfo가 잘 담겨서 나오는 것을 확인할 수 있다.
대체 뭐가 문제였을까?
먼저 테스트코드에서 확인할 수 있었던 점은 Scrum 클래스에는 ScrumInfo가 컬럼으로 들어있다.
양방향인데 여기에 ScrumInfo를 넣어주지 않아서 테스트코드에서 null이 발생했던 것이었다.
따로 Scrum 클래스에 ScrumInfo를 추가하는 메서드를 만들어서 돌렸더니
잘 들어가는 것을 확인할 수 있다.
근데 이렇게 되면 의문점이 생긴다.
왜 실제 서비스를 돌렸을 때는 스크럼에 스크럼인포를 넣어주지 않았는데 알아서 찾아오는 거지?
라는 의문점이 생겼다.
첫 번째 의심
DataJpaTest를 사용하면 자동으로 트랜잭셔널이 붙어있게 된다.
그래서 BeforeEach와 하나의 테스트가 한 트랜잭션 내에서 동작하게 되는데, BeforeEach에서 save만 하고 따로 커밋을 하지 않은 상태에서 repo를 테스트 해서 그런 건가? 라는 의심이 들었다.
그래서 TestEntityManager 클래스를 이용해서 persistAndFlush를 전부 진행해주었지만 결과는 같았다.
두번째 의심에서 문제를 해결할 수 있었다.
현재 테스트 코드에서는 BeforeEach에서 4개의 Entity를 만들어서 저장한다.
이 때 Scrum도 만들어서 save를 시키는데 문제는 현재 Scrum이 1차 캐시에 저장이 되어있다는 것이다.
그래서 Scrum에는 ScrumInfo가 null인 초기 상태의 Scrum을 찾아오기때문에 에러가 발생했던 것이다.
문제 해결
결국 BeforeEach 이후에 1차 캐시를 비워주어야 한다.
여기서 TestEntityManager 클래스를 이용해서 clear를 진행해주었다.
@BeforeEach
void setUp(){
User user = User.builder()
.email("test@test.com")
.profileImage("ex")
.nickname("ex")
.isOnline(false)
.type("kakao").build();
userRepository.save(user);
team = Team.builder()
.user(user)
.maxMember(15)
.name("팀이름")
.currentMember(1)
.description("팀설명")
.teamProfileImage("팀프로필사진")
.build();
teamRepository.save(team);
Scrum scrum = Scrum.builder()
.user(user)
.team(team)
.currentMember(1)
.name("스크럼이름")
.maxMember(15).build();
scrumRepository.save(scrum);
ScrumInfo scrumInfo = ScrumInfo.builder()
.isStart(false)
.scrum(scrum)
.subject("주제").build();
scrumInfoRepository.save(scrumInfo);
tem.clear();
}
이렇게 한번 clear를 통해 캐시를 비워주고 나서 테스트 코드를 돌리니 정상적으로 새롭게 Scrum을 찾아와서 문제를 해결할 수 있었다.