@Transactional 이모저모 1 - 테스트코드와 @Transactional

여우·2023년 8월 30일
83

처음 스프링을 배울 때 제일 신기했던 것 중 하나가
@Transactional이라는 어노테이션이었어요!
당시에는 리플렉션이니 AOP니 하는 걸 전혀 몰랐기 때문에,

‘아니 테스트 메서드에 이걸 붙이면 끝나고 자동으로 초기화해준다고?’
’DB 수정을 바로바로 하지 않고 트랜잭션이 끝나는 시점에 업데이트를 하네!’
하면서 마치 마법을 보는 기분을 느꼈던 기억이 납니다.

하지만 지금은 @Transactional이
프록시 패턴을 활용한 AOP 프로그래밍 기술이라는 것을 알고 있고,
마법이 아니기 때문에 나름의 단점을 가지고 있음을 또한 압니다.

또 어떤 단점들은 때때로 너무 치명적이어서,
@Transactional을 잘못 이해하고 사용하면
유용한 도구가 아니라 프로젝트에 큰 손상을 입히는 흉기가 될 수도 있어요!

다양한 상황에서 발생할 수 있는 @Transactional의 변칙 상황을
에피소드별로 간단히 정리해보고자
오랜만에 개인 블로그에 글을 올리게 되었습니다 😊

💬 Spring Boot, JPA 그리고 AOP 개념을 이미 알고 있거나 사용하는 분들을 대상으로 작성한 글입니다!

테스트 코드와 @Transactional

테스트 레이어에 @Transactional을 붙이면 크게 두가지 이점을 누릴 수 있습니다!

  1. 테스트 통과 / 실패 시 트랜잭션을 롤백해주는 기능
  2. JPA 영속성 컨텍스트의 범위를 테스트 레이어까지 확장하여
    지연 로딩 전략으로 설정된 연관관계 엔티티들을 테스트 코드에서 조회할 수 있게 해주는 기능

이 특징들 덕분에 테스트에 사용한 데이터를 전후처리하거나
결과를 확인하는 검증 코드를 작성하는 것이 엄청나게 수월해졌는데요,

@Transactional이 무엇을 해주길래 이런 게 가능할까요?

이는 테스트 레이어의 트랜잭션이 서비스 레이어를 감쌌기 때문에 가능한 일입니다.

조금 고급지게 표현하면
'트랜잭션이 시작하고 끝나는 시점을 나타내는 트랜잭션 경계가
서비스 레이어에서 테스트 레이어까지 확장되었다'
고 하고,

그래서 우리가 테스트 코드를 실행할 때
엄밀히는 서비스 레이어에 있는 트랜잭션이 아닌
테스트 레이어에서 만들어진 트랜잭션을 가지고 일련의 작업을 수행하는 것인데요,

짱이죠!

하지만 모순적이게도 이런 편리한 기능으로 인해 2가지 큰 문제가 생길 여지가 생겼습니다.
헊! 어떤 문제일까요, 너무 궁금하군요 (국어책 읽기)

문제 1 : 실제 코드에 트랜잭션이 없음

에피소드

우테코 커리큘럼을 얼레벌레 마치고 서비스 회사에 입사한 신입 개발자 김춘배.
JPA 능력을 어필해 회사에 입사한 춘배는
JPA의 변경감지를 활용해
[로그인이 완료되면 로그인한 디바이스의 ‘활성화’ 여부를 업데이트하는 기능] 을 개발하게 되었다.

우테코에서 테스트 작성을 습관화한 김춘배는
비즈니스 상황 별 테스트 코드를 꼼꼼히 작성하고,

테스트 간 데이터 격리를 위해
테스트 클래스에 @Transactional도 붙여주었다.

모든 테스트가 통과하도록 멋진 서비스 코드를 작성한 김춘배는
한껏 뿌듯해하며 PR을 제출했고,
테스트가 잘 통과하는 것을 확인한 팀원들도
별 말 없이 PR을 승인해 주었다.

신난다!
그런데 어느 날 사수로부터 날아온 다급한 연락.

분명 테스트할 때 LazyInitializationException같은 건 나지 않았는데,
코드를 실제로 배포하고 나니 난데없이 터지기 시작했다.
어떻게 이런 일이 일어난 것일까?

원인

이유는,
정말 황망할 정도로 터무니 없었습니다.

김춘배의 서비스 코드를 살펴봅시다.

이.. 똥고양이!
서비스 코드에는 @Transactional이 없습니다.

반면 테스트 코드에는 @Transactional이 붙어있고,
테스트할 때 사용하는 프로덕션 코드는
실제 프로덕션 레이어에 트랜잭션이 없음에도
테스트 레이어에서 생성한 트랜잭션을 이용해 작업을 수행했기 때문에
테스트가 정상적으로 통과할 수 있었던 것.

반면 운영환경에서는 @Transactional이 없기 때문에
데이터베이스가 지켜야 할 4원칙은 물론
영속성 컨텍스트의 지연로딩도 제대로 작동하지 않아
아무도 제대로 사용할 수 없는 폭탄같은 코드가 탄생하게 된 것입니다.

‘ㅋㅋ 그럼 서비스에 빼먹지 않고 @Transactional만 붙여주면 되는 것 아님?
그걸 누가 빼먹냐 바보가 으하핫’ 할 수 있겠지만,
아니 사람이 한번도 실수 안 할거면 바로 뚝딱뚝딱 개발하지 테스트코드같은 걸 왜 짜겠어요.

개발자의 실수를 바로바로 인식해 잡아내는 것이 테스트 코드의 역할인데도
테스트 레이어에서 생성된 트랜잭션에 의해 실제 버그가 가려지게 되고,

더 큰 문제는
기능을 개발하는 개발자 본인 또는
PR을 리뷰하는 동료가 서비스 코드에 어노테이션이 잘 있는지 직접 눈으로 확인하지 않으면
운영 환경에 배포되어 실제 장애가 발생할 때까지 아무도 눈치채지 못한다라는 점입니다.

이와 동일한 맥락으로,
운영환경 메서드에는 @Transactional에 readOnly 속성이 활성화되어 있는데
테스트의 @Transactional에서 그것이 덮어씌워져 생긴
버그 사례도 있습니다.

너무너무 무섭군요!

’개발자가 평소에 주의를 기울인다’ 같은 뭉뚱그린 해법이 아니라
사전에 확실히 예방하는 방법이 있지 않을까요?

이는 글의 맨 마지막에 알아보도록 하겠습니다.


문제 2 : 테스트해야 할 쿼리가 안 나감

에피소드

서비스 레이어에 @Transactional을 붙여 상황을 수습한 김춘배.
이제 더이상 버그는 없겠거니 하고 한시름 놓으려던 그 때!

본인이 구현한 코드의 모든 상황을 이미 테스트했는데도,
분명 테스트 때는 일어나지 않았던 예외가
또 운영환경에서 발생하는 상황!
이번엔 무엇이 문제였을까?

원인

아주 아주 간단한 테스트 코드를 만들어서
김춘배가 겪은 문제의 원인을 유추해 보겠습니다.

<회원 엔티티>

실험에 사용할 단 하나의 회원 엔티티 클래스입니다!
식별자와 이름을 가지고 있고, ‘이름바꾸기()’ 메서드를 통해 이름을 변경할 수 있습니다.

<테스트 코드>

회원의 기능을 테스트하는 코드입니다!
망고라는 이름의 회원을 저장하고,
저장한 망고의 이름 값을 ‘시든망고’로 변경합니다.

테스트를 실행해 봅시다!
그리고 어떤 SQL이 실행되는 지 확인해 봅시다.

헉!
SQL 결과가 예상과 다릅니다.

JPA에서 지원하는 변경 감지(Dirty Checking)가 SQL로 나타나지 않았습니다.

테스트 코드에 트랜잭션도 적용되어 있으니
트랜잭션 안에서 일어나는 엔티티의 변경, 여기서는 이름 값의 변경을 감지하여
마지막에 update 쿼리를 보내주어야 정상인데,
어째서 insert 쿼리만 있고 update 작업은 하지 않은 걸까요?

서비스 레이어에 적용된 트랜잭션 작업은
작업이 끝날 때 트랜잭션을 커밋하는 것이 기본으로 되어있는 반면,

테스트 레이어에 적용된 트랜잭션 작업은
각 테스트가 끝날 때 트랜잭션을 롤백하는 것이 기본으로 되어있습니다.
(어떻게 이렇게 동작하는 건지는 노션 조각글 참고)

그런데 JPA의 변경 감지는
트랜잭션을 커밋하는 경우에 변경 내용을 SQL로 바꾸어 데이터베이스에 반영하는 원리이기 때문에
롤백이 기본인 테스트 코드에서는 insert나 update 쿼리로 데이터베이스에 반영하는 작업을 하지 못하고
1차 캐시의 내용이 그대로 휘발
되어 버리는 것이죠!

그래서 테스트와 @Transactional을 주제로 하는 기술 블로그를 찾아보면

엔티티를 저장하는 스레드와 조회하는 스레드가 다를 때 테스트가 깨지더라,
엔티티 정보가 DB에 커밋되지 않고 1차 캐시에만 남아있을 때 DB 조회 테스트를 하면 깨지더라 처럼

당연히 정상적으로 동작할 줄 알았던 코드가
테스트 트랜잭션의 롤백 정책 때문에 오작동하는 사례를 많이 찾아볼 수 있습니다.

춘배네 회사의 코드를 유출할 수 없기 때문에
자세한 코드 내용을 볼 수는 없지만,

이 단락에서 나눈 이야기를 바탕으로
그가 겪은 문제의 원인을 대략 짐작할 수 있을 듯 합니다.

  1. 김춘배의 서비스 코드에는 무언가 문제가 있었을 것입니다.
    UPDATE 또는 INSERT 쿼리를 전송할 때
    외래키 제약조건과 관련된 예외가 발생할 수 있는 잘못된 코드를 작성한 것이죠!

  1. 하지만 이 코드를 테스트하는 메서드에 @Transactional이 붙어있으면
    1차 캐시에 있는 내용을 SQL로 바꾸어 반영하는
    commit 작업을 하지 않고 롤백을 수행하기 때문에,
    테스트 코드에서 해당 쿼리가 발생하지 않았고
    테스트가 정상적으로 통과하는 것으로 처리되었을 것입니다.

  1. 하지만 서비스를 배포한 운영 환경에서는
    트랜잭션이 끝나면 커밋도 하고 SQL도 실행하기 때문에
    춘배가 테스트에서 놓친 SQL 예외가 방방 터지기 시작한 것입니다.

불쌍한 고양이 😿

편리한 줄로만 알았던 @Transactional이
상황에 따라서는 애플리케이션에 엄청 나쁜 영향을 끼치기도 한다는 것을 알게 되었군요!

이렇게 무서운 현상들을 어떻게 해야 예방할 수 있을까요?
겁먹는 건 충분히 한 것 같으니
지금부터는 해결법과 예방법에 대해 이야기해 보겠습니다.


예방하기

“쥐엔장 ,, 트랜잭션 위험한 게 뭐 어쨌다는 거야!
그럼 테스트 때 @Transactional같은 거 쓰지 말라는 거야 뭐야!”

웅 정확합니다! (두 둥)

1. @Transactional 안쓰기

이 문제의 가장 직접적인 원인은
테스트 메서드에 @Transactional을 붙임으로써,
테스트 레이어의 트랜잭션이 서비스 레이어의 트랜잭션을 먹어버려 생긴 문제이니,

애초에 테스트 레이어에 트랜잭션을 안 씌우면
해당 문제로 골치를 앓을 일이 애초에 없겠죠.

하지만 이 방식은
테스트에 @Transactional을 붙였을 때 누릴 수 있는 모든 이점 또한 포기하겠다
라는 것과 동일하기 때문에,

@Transactional이 제공해 주던 지연 로딩 엔티티 조회 기능과 데이터 롤백 기능을
직접 만들어주는 수밖에 없습니다.

어노테이션을 제거하여 문제를 해결하고자 한다면,
추가로 고려해야 할 점이 최소 두 가지 더 생긴 셈이죠!

고려할 점 - 1. 테스트에 사용한 데이터를 어떻게 롤백할 것인가?

테스트 레이어에서 트랜잭션을 관리할 때에는
트랜잭션 매니져(TransactionManager)의 롤백 메서드를 호출하여
모든 데이터를 아주 간단하게 초기 상태로 되돌릴 수 있었습니다.

하지만 이제 막 그 기능을 없애버렸으니,

테스트 과정에서 생기거나 삭제된 데이터를 테스트 전으로 원상복구하는 작업을
수동으로 해주어야 한다
는 과제가 생기게 됩니다.

이는 각자의 프로젝트 성격에 따라
Junit의 기능과 Spring의 기능을 다양하게 혼합해 구현할 수 있어요!

각 테스트 케이스 실행 전/후에

  • Junit 5의 @BeforeEach, @AfterEach
  • Junit 5의 BeforeEachCallback, AfterEachCallback
  • @Sql의 executionPhase 속성

테스트에 사용한 데이터를 롤백하는 코드를

  • 각 엔티티별 Repository의 deleteAll() 메서드
  • 각 테이블에서 모든 데이터를 delete하는 SQL 파일
  • 각 테이블을 truncate하는 SQL 파일
  • 내장 DB를 포함해 스프링 컨테이너를 전부 무너뜨리고 다시 세우는 @DirtiesContext

호출함으로써
테스트 이전과 동일한 상태로 데이터를 롤백할 수 있을 것입니다.

근데 고려할 게 또 있어요!

고려할 점 - 2. 지연 로딩으로 설정된 연관관계를 테스트 레이어에서 어떻게 조회할 것인가?

글 서문에 잠깐 언급하긴 했으나
이게 무슨 소리인가 싶으실 것 같아,
간단한 예제를 가지고 자세한 설명을 해보겠습니다!


간단한 회원 엔티티가 있습니다.
회원은 팀을 연관관계로 가지고 있는데, 팀과의 연관관계를 지연 로딩으로 설정해 두었습니다.

그리고 이 회원을 가지고 아주 간단한 테스트 코드를 하나 작성했습니다!
회원과 연관된 팀 엔티티를 getter로 조회해, 팀 이름이 ‘한화이글스’인지 검증하는 코드입니다.

너무 간단한 코드라서 2살 꼬꼬마도 ‘움 이건 통과하겠군’ 할만한 코드입니다.
하지만 영유아의 판단력이 으레 그렇듯
이 테스트는 런타임 예외를 터뜨리며 보기좋게 깨질 거에요!

LazyInitializationException!
지연로딩으로 설정된 연관관계 엔티티는 처음에 프록시 객체로 만들어지는데,
이 객체를 트랜잭션 경계 밖에서 조회하려고 하면 발생하는 런타임 예외입니다.


뭐임 이게 뭐 어쨌다는 건가 싶지만,
@Transactional을 없앤 후의 여러분에게는 아주 귀찮은 장벽이 될 것입니다..!

우리가 특정 서비스 레이어를 테스트할 때
서비스 메서드의 아웃풋을 검증하는 것만으로는 한계가 있어서,
Repository로부터 엔티티를 조회하여 그 상태를 검증하는 방식을 많이 사용합니다.

이 때 특정 엔티티와 연관관계에 있는 다른 엔티티의 상태를 조회해야 하는 경우도 매우 흔히 일어나는데,
@Transactional을 테스트에서 제거해버리면
트랜잭션 경계가 테스트 레이어까지 미치지 못해

지연 로딩 연관관계에 있는 엔티티를 검증하려 들 때마다 예외가 발생하는
매우 번거로운 상황이 발생할 가능성이 높습니다.

그케 막 치명적이진 않지만
번거롭고 귀찮은 이 문제를 어떻게 해결할 수 있을까요?

근본적으로 JPA의 특성으로 일어난 문제이기 때문에,
JPA의 특성을 사용한, 다양한 시도를 통해 해결할 수 있습니다.

  • 지연로딩이 문제잖아! 모든 엔티티의 FetchType을 즉시로딩으로 바꾸어 해결한다
    (실제로 했다간 팀원들이 당신을 창 밖으로 던질 것입니다)
  • Repository를 사용하지 말고, JPQL의 fetch join을 사용해
    지연로딩으로 설정된 엔티티도 함께 가져온다

  • 테스트 안에서 엔티티 조회용 트랜잭션을 새로 시작한다
    (Spring Data JPA는 EntityManager의 트랜잭션을 직접 조작하는 것을 허용하지 않는다고 합니다.
    출처: https://dong-gi.github.io/posts/java/jpa.html)

근본적으로 지연 로딩 관계인 엔티티를 즉시 조회할 수 있도록 바꾸기만 한다면 해결할 수 있는 문제이므로
다양한 시도를 해보시기 바랍니다 😼


이렇게 두 가지를 추가로 고려하면서 사용하면
@Transactional을 사용하여 일어날 수 있는 문제점을 예방하고
조금 더 안전한 애플리케이션을 사용할 수 있을 것이라는 내용으로 포스팅을 마치려 하였으나,

한 달여 전
이런 극단적인 해결 방식에 의문을 제기한 유명한 분들의 이야기를 듣고
생각을 다르게 하기 시작했습니다.


2. 조심해서 사용하기

저는 사실 이렇게 극단적인 해결책을 다룬 글이 많다는 게 좀 놀라웠거든요,
편리한 것을 넘어 약간의 문제가 되는 상황이 존재할 수 있다는 것을 이전부터 인지하고는 있었지만 …

그러면 실용성이 너무 떨어지잖아요.
몇가지 조심하면 되는데 그것 때문에 오만가지 불편함을 감수하면서 초가삼간 다 태울 수 없으니 …

  • 지식공유자 김영한

어노테이션도 존재하지 않던 시절부터
서버 애플리케이션을 개발해온 개발자분들은
’😮😮 아니 몇 가지 상황에서만 주의해서 사용하면 되는 건데,, 아예 안 쓴다고?’
라는 의아함을 느끼신 것 같습니다.

’@Transactional을 테스트에서 배제한다’ 라는 전략이 왜 부적절한 지
토비님의 유튜브 영상에 아주아주 자세히 나와있기 때문에,
해당 영상의 내용을 인용하는 방식으로 이 단락을 이어보겠습니다.


@Transactional을 없애는 것이 비현실적인 이유

  • 테스트 데이터를 롤백하는 작업을 어떻게 수동으로 할 수 있단 말인가?
    • 혼자 또는 소수의 인원이 참여하는 팀프로젝트일 때에는
      도메인의 수가 적기 때문에 수동 롤백이 가능할 수도 있지만,
      회사에서 사용하는 프로젝트에는 엄청나게 많은 도메인이 있고,
      이 도메인들 대부분 복잡한 외래키 제약조건으로 얽혀있기 때문에
      데이터를 롤백하는 코드를 작성하는 것 자체가 힘듭니다.
    • 어떤 테스트에서 어떤 도메인의 데이터를 조작했는 지
      테스트 클래스마다 다르기 때문에,
      각 테스트의 특성을 고려해 롤백 코드를 작성해야 합니다.
      이게 귀찮아 데이터베이스 전체를 엎고 다시 만드는 방식으로 롤백할 수도 있지만,
      이는 트랜잭션 롤백 작업보다 훨씬 많은 오버헤드가 발생합니다.
    • 프로젝트에서 도메인이 추가되거나 수정되었을 때
      데이터 롤백 코드에 해당 도메인에 대한 롤백 코드를 빠뜨리면
      테스트 롤백이 제대로 되지 않아 버그가 발생할 가능성이 높습니다.
    • 이는 시간이 지남에 따라
      테스트할 내용 자체보다 테스트 데이터를 롤백하는 작업에
      더 많은 시간과 체력을 소모하게 되고,
      테스트를 쓰는 게 골치아파지면 점점 테스트 작성을 꺼리게 됩니다.
  • @Transactional이 주는 이점이 소수의 단점보다 훨씬 크다!
    • 데이터베이스와 상호작용하는 테스트를 작성할 때
      트랜잭션과 관련된 고민을 줄이고
      테스트 내용에만 집중할 수 있도록 다양한 기능을 제공하기 때문에,
      테스트 작성 속도를 훨씬 빠르게 만들어주고
      테스트를 개발에 적극적으로 활용할 수 있도록
      도와줍니다.

결과적으로
’@Transactional을 아예 제거하고 쓰겠다’ 라는 조치는
작은 규모의 개인 또는 팀프로젝트에서는 유효할 수도 있지만
실무 환경에서 사용하기에는 득보다 실이 더 많다는 이야기이네요! 🤨

그럼 @Transactional을 유지하면서도
앞에서 말했던 문제점들을 예방하는 방법으로 어떤 게 있을까요?


조심해서 사용하는 방법 - 1. 강제로 DB에 반영하고 테스트하기

메모리에만 존재하며 최대한 flush(DB에 SQL을 날리는 작업)를 뒤로 지연시키는 상태에서,
마지막에 commit을 할 때 DB에 insert든 update든 하는 상황인데,
롤백 테스트를 해버리면 메모리에 있는 내용으로 아무것도 하지 않고 그대로 롤백해 버려요.

그러면 테스트 코드에서는 명시적으로 flush를 반드시 해서 테스트 해주어야 합니다.
이건 제가 회사에 처음 들어온 팀원들을 교육할 때 항상 강조하는 것 중 하나에요.

- 테스트에서의 @Transactional 사용에 대해 질문이 있습니다

[문제 2 : 테스트해야 할 쿼리가 안 나감] 문제를 해결하는
가장 직접적이고 효과적인 방법이에요!

테스트 레이어의 트랜잭션은 1차 캐시의 내용을 DB에 반영하지 않고 롤백하는 것이 기본으로 되어 있으니,
검증 작업을 실행하기 전 1차 캐시를 강제로 DB에 반영하게 만드는 해결 방법입니다.

이렇게 하면 운영환경에서는 나가는 쿼리가 테스트에서 나가지 않는 문제를 예방할 수 있기 때문에
운영 환경과 테스트 사이의 격차를 많이 해소할 수 있겠네요! 😊

조심해서 사용하는 방법 - 2. 테스트 환경과 실제 환경의 격차 최소화하기

개발자가 작성하는 테스트, 단위 테스트나 통합 테스트 외에 실제 환경과 유사하게 환경을 구성하고 진행하는 인수 테스트, e2e 테스트, 혹은 http api 테스트 같은 것을 추가로 진행해야 합니다.

이 과정에서 전방위적인 중복이 어느 정도 발생하더라도, 테스트를 잘 만들어서 내가 작성하는 코드를 잘 보호하고 검증하는 작업을 해주는 게 중요하다고 생각합니다.
- 테스트에서의 @Transactional 사용에 대해 질문이 있습니다

특정 도메인 레이어나 서비스 레이어만을 똑 떼서 테스트하는 방식을 슬라이스 테스트(Slice Test)라고 합니다.

그런데 슬라이스 테스트 방식만으로 테스트를 하다 보면
여러 트랜잭션을 넘나드는 복잡한 비즈니스 작업 과정 또는
외부의 다른 레이어들과 상호작용하는 과정에서 일어나는 버그를 잡아내지 못할 가능성이 있어요!

그렇기 때문에 검증하려는 대상이 동일하더라도
위 인용에 있듯 인수 테스트, e2e테스트, http api 테스트 등의 통합 테스트를 추가로 작성하고 검증하면서
슬라이스 테스트 뿐만 아니라 최대한 다양한 관점에서 테스트 코드를 작성하는 것이 필요합니다.

이렇게 하면 작은 단위의 테스트에서는 미처 잡지 못했던
트랜잭션 관련 버그를 조기에 잡아낼 가능성이 훨씬 높아지겠죠. 😊

조심해서 사용하는 방법 - 3. 완벽한 테스트는 없다는 사실을 인지하기

테스트를 웬만큼 잘 작성해도 애플리케이션 코드를 완벽하게 검증할 수는 없다는 사실을 인식해야 합니다.’
저는 이게 되게 중요하다고 봅시다.

뭐 내가 롤백 테스트를 했건, 아니면 직접 클린업라는 코드를 만들어 테스트 했건 상관없이
’내가 테스트를 잘 만들면 애플리케이션 코드는 100% 검증이 됐다’ 이렇게 생각하면 안돼요!

분명히 빠진 부분이 있습니다.
내가 테스트 코드로는 검증할 수 없는 영역도 있고,
테스트는 분명히 커버리지 100%를 이루고 있는데 그럼에도 발생할 수 있는 어떤 특별한 상황을
테스트에서 검증하지 하는 경우가 있다는 사실을 잘 인식하셔야 해요.

…(중략)

이거를 전제로 깔아야 돼요.
’아, TDD를 했으니까 나는 완벽해요’ 그렇지 않습니다.
물론 안 하는 거에 비하면 마음이 편하겠죠.
하지만 완벽하다고 해서 안심할 수 없습니다.

그리고 코드에서 발생하는 전형적인 오류, 예를 들어 트랜잭션 경계 밖에서 detached 엔티티의 값 변경같은 것, 이런 건 코딩 가이드를 작 작성하고,

또 이런 건 한두 번 경험하고 나면 ‘아 요거 놓치지 쉽겠구나. 그러면 이런이런 방식의 코드를 작성했을 경우에 트랜잭션을 사용하기 때문에 검증이 잘 안될 수 있겠다.’

그러니 ‘코드를 더 주의깊게 리뷰하자’, 아니면 ‘정적 분석 도구 등을 알맞게 이용해서 이런 것들을 잡아낼 수 있는 방법을 만들자’ 이런 이야기들을 개발팀 안에서 나누는 게 필요합니다.
- 테스트에서의 @Transactional 사용에 대해 질문이 있습니다


마무리

지금까지 테스트 레이어에 @Transactional을 붙여 실행했을 때 일어날 수 있는 문제점과
다양한 해결 방법에 대해 이야기 하였습니다.

어노테이션 하나만 띡 붙여주면 트랜잭션이 적용되는 혁신적인 사용성을 가지고 있지만,
이 너머에 AOP, JPA, 트랜잭션이라는 거대한 세 개념이 복잡하게 얽혀있다 보니

@Transactional로 인해 생기는 문제 자체도 복잡하고
그 해결방법도 복잡한 것 같아요!

하지만 토비님의 영상에 언급된 것처럼,
’이런이런 문제가 발생할 수 있다는 것을 계속 인지하고 신경쓰는 것’ 만으로도
충분히 효과적인 예방이 되지 않을까 생각을 합니다.

김춘배와 함께하는 트랜잭션 1 이야기를 마무리 하겠습니다.

춘배야~ 앞으론 행복하게 잘 지내야 한다~? (동물농장 엔딩 멘트)

profile
얼레벌레

9개의 댓글

comment-user-thumbnail
2023년 8월 30일

날 춘배로 유혹하다니..! 글 잘봤습니다👍 요즘 Transactional 관련 글 많이 읽는데 도움이되네요

1개의 답글
comment-user-thumbnail
2023년 9월 2일

좋은 글 잘 읽었습니다. 감사합니다!

답글 달기
comment-user-thumbnail
2023년 9월 4일

많은 방법을 제시해주셨네요. 감사합니다.

저도 해당 내용에 대해서 고민해봤는데, @Transactional을 제거하는 게 더 나은 것 같아요.

혼자쓰면 트랜잭션 경계범위를 스스로 인지한 상태에서 테스트를 작성할 수 있을 것 같지만,
여러 명이서 작업할 때에는 그렇게 사용하기 어렵다고 생각하거든요.

롤백의 편의성을 위해서 사용할 수도 있지만, 디비 롤백을 하는 다른 방법이 있기도 하니까요.

여우 화이팅!

답글 달기
comment-user-thumbnail
2023년 9월 20일

이제서야 이런 좋은 글을,,, 잘 읽고 가요!

답글 달기
comment-user-thumbnail
2024년 3월 11일

매우 좋은글 감사합니다. 진작에 이 글을 읽었더라면.. 이미 여기적힌 에러를 다 경험하고 읽어버렸네요. 그런데 글을 읽는 중에 궁금한게 있어서 질문드립니다!

조심해서 사용하는 방법 - 1. 강제로 DB에 반영하고 테스트하기 방법으로 flush() 를 사용해 1차캐시에 있는 내용을 DB 에 반영한다고 하는데 flush() 를 사용한다고해서 실제 DB에 커밋을 날리는게 맞는지 의문입니다.

제가 @Transactional 을 메서드에 걸고 멀티스레드를 테스트하는데 메인스레드(테스트하는 스레드)에서 데이터를 저장하고, 다른스레드에서 해당데이터를 조회하면 값이 없다고 나옵니다. 당연히 메인스레드는 실제 DB 에 커밋을 하지않았고 1차캐시에만 저장되어있고, 다른 스레드는 1차캐시를 메인스레드와 공유하지 않기때문에 값이 없는건 당연합니다.
그래서 제가 메인스레드에서 데이터를 저장하고 flush() 를 하고 다른스레드에서 해당데이터를 조회해봤는데 그래도 값이 없다고 나와요. flush() 로 실제 DB에 반영된다면 다른스레드에서 조회할때도 나와야되는데 왜 그럴까요?

1개의 답글
comment-user-thumbnail
2024년 5월 5일

코드 리뷰를 받는 과정에서 이 부분에 대해 알아보고 있었는데
설명이 너무 잘돼있어 이해가 바로 되네요 .. 너무나도 좋은 글 감사드립니다!

한 가지 질문이 있는데요!
이 문제점들이 레포지토리 테스트 클래스에 @Transactional 어노테이션을 사용했을 때의 문제점이 있을까요?
스프링이 제공하는 @JdbcTest, @DataJpaTest 어노테이션으로 레포지토리 테스트에 필요한 Bean들만 등록되어 훨씬 가볍게 테스트할 수 있다는 장점이 있다고 생각이 드는데요
내부에는 @Transactional 어노테이션이 선언되어 있어 만약 레포지토리 테스트시에도 @Transactional으로 인한 문제가 있다면 위 테스트 메서드를 사용하지 못하고 테스트용 설정 파일을 사용해 해당 테스트에 필요한 Bean을 등록해 사용하는 등의 방식이 생각나는데 이러한 방식은 너무나도 불편하게 느껴졌습니다

혹시 실례가 안된다면 @Transactional이 레포지토리 테스트에도 큰 영향을 미치는지 질문드려도 될까요??

답글 달기