테스트 코드를 작성하며 기능을 검증하던 도중, 예상치 못한 오류를 발견하게 되었다.
다음과 같이 엔티티가 있다고 가정한다.
User
엔티티는 양방향으로 Review
를 여러 개 가질 수 있다.
@Entity
public class Review extends TimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String content;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
@Entity
public class User extends TimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserRole role;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Review> reviews = new ArrayList<>();
}
테스트할 기능은 다음과 같다.
해당 기능은 조회한 사용자의 리뷰 목록을 조회하는 것이다.
@Service
public class ReviewServiceImpl implements ReviewService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public List<ReviewResponseDto> getUserReviews(final Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundUserException(ReviewErrorCode.NOT_FOUND_USER));
return user.getReviews()
.stream()
.map(this::toDto)
.toList();
}
}
테스트 코드는 다음과 같다.
5개의 주문을 생성하고, 각 주문당 리뷰를 하나씩 생성한다.
@Nested
@DisplayName("리뷰 조회 테스트")
class ReadReviewTest {
@Test
@DisplayName("사용자가 작성한 리뷰 조회")
void readUserReview() {
// given
int count = 5;
User user = User.builder()
.email("asdas@asd.asd")
.username("asdasd")
.password("asdasdasd")
.role(UserRole.USER)
.build();
user = userRepository.save(user);
for(int i=0;i<count;i++) {
Order order = Order.builder()
.user(user)
.state(OrderState.COMPLETED)
.build();
orderRepository.save(order);
Review review = Review.builder()
.user(user)
.order(order)
.content("asdsa")
.build();
reviewRepository.save(review);
}
// when
List<ReviewResponseDto> responseDto = reviewService.getUserReviews(user.getId());
// then
assertThat(responseDto.size()).isEqualTo(count);
}
}
위의 테스트 결과를 예상해본다면, 사용자가 리뷰를 5개 생성했으니 reviewService.getUserReviews(user.getId());
를 호출하게 된다면 5개의 리뷰 목록을 가져올 수 있다고 생각했다.
그러나, 결과는 다음과 같이 리뷰가 조회되지 않는다.
왜 이러한 문제가 일어났으며, 어떻게 해결했는지에 대해 알아보는 시간을 가져본다.
사실, 해당 테스트 클래스에는 @Transactional
이 선언되어 있다.
이를 통해 트랜잭션이 시작되며, save
를 호출해줘도 DB에 바로 플러시가 되지 않는다.
그렇다면, 해당 트랜잭션 내에서 findById
로 엔티티를 조회하면 어떻게 되는가?
DB에서 찾기 전에, 영속성 컨텍스트에 해당하는 ID값을 가진 엔티티가 있는지를 조회하게 된다.
이 부분이 위의 테스트 코드에서 문제가 되는 부분이다.
그 이유는, DB에서 유저를 조회한다면 양방향 매핑으로 연결된 엔티티인 reviews
도 조회하게 될 것이다.
그러나, 영속성 컨텍스트의 1차 캐시에 해당 엔티티가 존재하기 때문에 DB에서 조회하지 않는다.
그렇기에 리뷰 엔티티를 아무리 생성해줘도, 영속성 컨텍스트에 존재하던 User
엔티티는 처음부터 reviews
가 존재하지 않기 때문에 조회가 되지 않는 것이다.