[Java][Spring]Spring Boot 통합 테스트, 왜 @Transactional을 제거했을까?

JUNYOUNG·2025년 4월 28일

항해 플러스 백엔드

목록 보기
10/14
post-thumbnail

1. 들어가며

테스트가 테스트답기 위해서는, 운영 환경과 최대한 유사하게 만들어야 한다.

Spring Boot 통합 테스트를 하면서 우리는 종종 @Transactional을 테스트 클래스에 붙이는 실수를 저지른다.

처음에는 빠르고 편리해보이지만, 장기적으로는 심각한 문제를 초래할 수 있다.

이 글에서는

  • 그렇게 하면 안 되는지
  • 무엇을 대신 사용해야 하는지
  • 바꾸는 과정에서 발생한 문제해결 방법
    까지 실전 사례를 기반으로 자세히 설명한다.

2. 흔히 하는 실수: 테스트에 @Transactional

테스트 클래스에 @Transactional을 걸면 테스트 메서드가 끝날 때 자동으로 롤백되기 때문에 DB를 깔끔하게 유지할 수 있다.

하지만 이 방식은 실제 운영에서 발생하는 트랜잭션 흐름과 완전히 다르다.

구분운영 환경테스트 환경(@Transactional 사용)
트랜잭션 시작/종료메서드 단위 트랜잭션클래스 전체 트랜잭션
DB 상태실제 커밋/롤백 발생항상 롤백으로 끝
부작용정상적인 트랜잭션 흐름 검증 가능트랜잭션 경계 깨짐, Lazy 로딩 이슈

3. 왜 문제가 되는가

3.1 서비스 추상화 깨짐

Spring은 서비스 계층을 통해 트랜잭션 경계를 명확히 설정한다.

➡️ @Transactional이 테스트 전체를 감싸면, 서비스 메서드별 트랜잭션 검증이 불가능하다.


3.2 트랜잭션 경계 불일치

운영에서는 트랜잭션 시작 → 작업 → 커밋이 명확하지만, 테스트에서는 통째로 하나의 트랜잭션에 묶인다.

➡️ 커밋 타이밍이 다르면, 실제 장애와 다른 결과가 나온다.


3.3 프록시 초기화 실패 (LazyInitializationException)

JPA 연관관계 매핑 시, 일반적으로 fetch = LAZY를 사용한다.

그런데 세션이 이미 종료된 상태에서 프록시 객체를 접근하면 다음과 같은 에러가 터진다:

failed to lazily initialize a collection of role: kr.hhplus.be.server.domain.order.Order.items: could not initialize proxy - no Session

또는

Could not initialize proxy [Coupon#3] - no session

➡️ 운영 환경에서는 커밋 후 세션이 닫히기 때문에,

제대로 검증 안 된 테스트는 실제 배포 후 터질 수 있다.

➡️ 테스트 통과하더라도, 실제 배포 시 운영에서 Lazy 로딩 에러가 발생할 수 있다.


4. Testcontainers를 도입한 이유

Testcontainers를 통해

  • 실제 MySQL 컨테이너를 띄우고
  • init.sql로 초기 스키마를 세팅하고
  • 테스트마다 깨끗한 환경을 구축했다.

이 방식은

"진짜 운영 환경처럼 DB 상태를 구성하고 테스트한다."
는 것을 가능하게 한다.

결과적으로 신뢰성 있는 테스트를 만들어준다.


5. 테스트를 바꾸는 과정에서 발생한 문제

@Transactional을 제거한 직후,

  • 연관관계(Order.items, CouponIssue.coupon)가 LAZY 로딩인데,
  • 테스트 검증 시점에는 이미 세션이 닫혀서 LazyInitializationException 발생했다.

✅ 원인

  • 기본 JPA 조회 (findById, findByUserIdAndCouponId)가 Fetch Join을 사용하지 않았다.
Order order = orderRepository.findById(order.getId()).orElseThrow();
assertThat(order.getItems()).hasSize(1); // 여기서 no session
CouponIssue issue = couponIssueRepository.findByUserIdAndCouponId(userId, coupon.getId()).orElseThrow();
assertThat(issue.getCoupon().getCode()).isEqualTo("TESTONLY1000"); // 여기서 no session

6. 문제를 해결한 방법

Fetch Join JPQL로 연관된 엔티티까지 한 번에 조회하도록 수정했다.

수정 전

Optional<Order> findById(String orderId);

수정 후

@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :orderId")
Optional<Order> findByIdWithItems(@Param("orderId") String orderId);
@Query("SELECT ci FROM CouponIssue ci JOIN FETCH ci.coupon WHERE ci.userId = :userId AND ci.coupon.id = :couponId")
Optional<CouponIssue> findByUserIdAndCouponId(@Param("userId") Long userId, @Param("couponId") Long couponId);

➡️ 조회 시점에 연관 객체까지 초기화해서 세션 종료에도 안전하게 테스트 가능하게 만들었다.

이렇게 변경함으로써

  • 조회 시점에 LAZY 연관관계도 즉시 초기화
  • 세션 종료와 관계없이 테스트 통과
  • 운영 환경과 일치하는 트랜잭션 플로우 유지

를 달성할 수 있었다.


7. 그렇다면 언제 @Transactional을 써도 괜찮을까?

모든 테스트에 @Transactional을 금지하는 건 아니다.

특정 목적에 따라 @Transactional을 활용하는 것이 오히려 유리할 때도 있다.

✅ 단위 테스트 (Repository 테스트)

  • 간단한 CRUD Repository 검증
  • Repository Layer 단독 테스트할 때
  • DB 상태를 매번 정리하는 게 귀찮을 때
  • 롤백 기반으로 DB 초기화를 간단하게 하고 싶을 때
@DataJpaTest
@Transactional
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void saveUser() {
        User user = new User("junyoung");
        userRepository.save(user);

        assertThat(userRepository.count()).isEqualTo(1L);
        // 테스트 끝나면 자동 롤백됨
    }
}

➡️ 단순한 CRUD Repository 테스트에서는 @Transactional이 유용하다.


✅ "데이터 정리"가 중요한 경우

  • 테스트 데이터가 많고
  • AfterEach로 매번 delete 하는 게 복잡하거나 성능에 부담될 때

@Transactional을 걸어서 테스트 끝날 때 깔끔하게 롤백시켜주는 것도 전략이 될 수 있다.

단, 이 경우도 통합 테스트(서비스 흐름 검증) 에는 적용하면 안 된다.


8. 그래서 한 줄 정리

구분@Transactional 사용 여부
Repository 단위 테스트✅ 써도 된다
단순 CRUD 검증✅ 써도 된다
통합 테스트 (Service-DB 흐름)❌ 쓰면 안 된다
트랜잭션 커밋/롤백 플로우 검증❌ 쓰면 안 된다

9. 최종 요약

포인트내용
❌ 테스트 전체에 @Transactional 금지트랜잭션 경계 깨짐
✅ Testcontainers 사용운영과 똑같은 DB 구성
✅ 필요한 데이터만 삽입필요한 경우 @Sql 사용 가능
✅ Fetch Join 사용LazyInitializationException 해결

10. 마치며

"테스트는 개발자에게 가장 솔직한 피드백을 준다."

코드를 그냥 맞추는 것이 목표가 아니다.

운영과 동일한 환경에서 테스트를 검증하는 것이 진짜 목표다.

Testcontainers, 트랜잭션 경계 유지, 연관관계 초기화 등은 모두

장애 없는 서비스를 만드는 기본기다.

오늘부터, 테스트를 "운영처럼" 짜보자.

profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글