[TIL] JPA - 양방향과 트랜잭션, 영속성의 관계에 대해

phdljr·2023년 12월 8일
0

TIL

목록 보기
43/70

테스트 코드를 작성하며 기능을 검증하던 도중, 예상치 못한 오류를 발견하게 되었다.

다음과 같이 엔티티가 있다고 가정한다.
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

사실, 해당 테스트 클래스에는 @Transactional이 선언되어 있다.

이를 통해 트랜잭션이 시작되며, save를 호출해줘도 DB에 바로 플러시가 되지 않는다.
그렇다면, 해당 트랜잭션 내에서 findById로 엔티티를 조회하면 어떻게 되는가?

DB가 아닌 영속성 컨텍스트의 1차 캐시에서 찾기

DB에서 찾기 전에, 영속성 컨텍스트에 해당하는 ID값을 가진 엔티티가 있는지를 조회하게 된다.

이 부분이 위의 테스트 코드에서 문제가 되는 부분이다.

그 이유는, DB에서 유저를 조회한다면 양방향 매핑으로 연결된 엔티티인 reviews도 조회하게 될 것이다.
그러나, 영속성 컨텍스트의 1차 캐시에 해당 엔티티가 존재하기 때문에 DB에서 조회하지 않는다.

그렇기에 리뷰 엔티티를 아무리 생성해줘도, 영속성 컨텍스트에 존재하던 User 엔티티는 처음부터 reviews가 존재하지 않기 때문에 조회가 되지 않는 것이다.

해결 방법

  • 리뷰 엔티티를 생성할 때, 유저 엔티티의 reivews에 추가한다.
  • @Transactional을 사용하지 않거나 propagation을 설정해준다.
profile
난 Java도 좋고, 다른 것들도 좋아

0개의 댓글