서론
다들 트랜잭션에 관한 고민을 해보신 적이 있으신가요?
저는 서비스 로직을 짜거나, TestCode를 작성하거나 JPA와 같은 ORM을 사용할 때 트랜잭션에 대한 고민들이 많이 있었습니다.
" 어떤 단위까지 한 트랜잭션으로 간주해야 할 까?"
" 외부 API를 호출하는 로직에서 만약 트랜잭션을 걸고, 해당 로직에서 문제가 발생하면??"
" 만약 A 라는 서비스 로직에서 @Transactional 어노테이션을 추가하거나, 추가 하지 않거나 두 결과값이 어떻게 다를까? "
" 그럼 과연 어느 서비스 로직에서는 @Transactional을 추가해야하고, 어떤 서비스 로직에서는 추가하지 않아야 할까? "
이렇게 다양한 고민들이 있었습니다.
오늘은 제가 입사하고 맡은 업무에서 경험한 트랜잭션과 관련된 내용을 정리해보고자 합니다.
경험을 공유하기에 앞서 간단하게 트랜잭션에 대해 짚고 넘어가겠습니다.
우리는 작업 단위를 트랜잭션이라고 명명하고 있습니다.
그래서 Java 진영에서는 트랜잭션 어노테이션( @Transactional )을 이용해 작업단위로 묶습니다.
트랜잭션은 데이터베이스에서 일련의 작업을 원자적으로 처리하여 데이터 일관성과 무결성을 유지하는데 사용됩니다.
어노테이션 방식을 통해 서비스 로직이 하나의 작업 단위임을 명시해주는 것인데요, 어노테이션 방식 외에도 직접 프로그래밍 방식으로 트랜잭션을 구현해줄 수 있습니다.
그러나!
(오늘은 트랜잭션의 기본 내용을 다루고자 작성한 글은 아닙니다.)
만약 트랜잭션에 관한 기본적인 내용이 궁금할시 아래 링크를 참고해주세요 !
https://jipang9-greedy-pot.tistory.com/124
https://jipang9-greedy-pot.tistory.com/164
제가 이번 글에서 전달하고 싶은 내용은 지금부터 시작입니다.
앞에서 트랜잭션은 작업 단위를 묶는 것이라고 했습니다.
그럼 모든 서비스 로직이나 한 작업내에 이루어진다면 트랜잭션으로 간주해야 할까요?
물론 한 작업내에 여러 작업이 섞여서 이루어저야 한다면 트랜잭션으로 간주해야하지만 트랜잭션 어노테이션을 선언하는 방식은 위험한 방식입니다. 때문에 결제, 취소 와 같은 로직에서는 트랜잭션의 선언을 지양하고 있습니다.
그 이유가 무엇일까요?
과거에 해당 내용을 가지고 저만의 결론을 내렸던 내용은 다음과 같습니다.
- 외부 API 호출 시 트랜잭션이 끝날 때까지 커넥션 풀을 유지하고 있어 자원을 낭비하게 된다.
( 이러한 작업이 무수히 많고, 계속 커넥션 풀을 물고 있으면? 다른 자원이 커넥션을 이용하지 못하게 될 것 )
- 해당 로직에서 오류가 있을 시 롤백(RollBack)되어버려서 발생하는 문제가 있을 것이다.
- 트랜잭션으로 인해 서비스 로직 간 종속성이 증가할 것이다.
외에도 다양한 이유가 있을 것입니다.
스프링에서는 클래스 혹은 메서드 단에서 @Transactional 어노테이션을 사용하게 되면 다음과 같은 일이 발생합니다.
1. 트랜잭션 기능이 적용된 프록시 객체 빈으로 등록
2. 이 프록시 객체는 트랜잭셔널 어노테이션이 포함된 메서드가 호출될 경우 PlatformTransactionManager을 사용해 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 한다.
본론
결제, 정산, 결제 취소 등과 같이 외부 API를 호출하는 상황에서 트랜잭션 어노테이션을 선언해서 문제가 발생했던 경험이 있습니다.
( 팀에서 작업자가 실수한 내용을 바탕으로 팀원이 같이 해결했던 경험을 공유하고자 합니다.)
A라는 결제 로직이 있다고 가정해보겠습니다. 외부의 Bridge Service(Server)를 호출하고, 이 브릿지 서버는 다날 결제를 호출합니다.
( 브릿지 서버는 중계 역할 및 들어온 요청에 따른 로그 저장 서버로 활용되고 있습니다. )
결제 프로세스는 브릿지 서버에서 다시 서비스의 pay 상태를 확인하는 추가적인 프로세스가 있는데, 한 트랜잭션 내에 트랜잭션이 아직 끝나지 않아서 pay의 상태가 아직 update 되지 않아 pay의 상태를 확인하는 로직에서 원하지 않은 return값을 전달해주는 이슈가 있었습니다.
그래서 이 문제로 인해 결제가 정상적으로 되지않은 이슈까지 확장되었습니다.
사용자의 예약이 서비스에 전달되면 우리 서버에서는 파트너 사에게 예약에 관한 orderNo와 afteUrl를 전달하게 되고, 해당 orderNo를 이용해 afterUrl을 이용해 파트너 사에서는 우리 서비스 쪽으로 결제 요청을 보냅니다.
그 이후 결제 프로세스를 진행하게 됩니다.
이러한 일련의 작업이 맨 처음에는 트랜잭션으로 처리되어 있었고, 트랜잭션 내에서 결제 이전에 pay의 상태를 바꾸더라도 아직 commit, flush가 되지 않았기에 변경된 데이터가 저장되어 있지 않았음을 알게 되었습니다.
이는 트랜잭션의 원리 및 내용을 확인해보면 확인이 가능한 문제였습니다.
JPA와 트랜잭션을 이해해야 이번 문제를 해결할 수 있었습니다.
JPA의 장점 및 특징에는 여러 가지가 있습니다.
- 영속성 컨텍스트 ( Persistence Context )
- 영속성 컨텍스트로 인한 데이터 캐싱 ( Data Caching)
- 변경 감지 ( Dirty Checking )
- 트랜잭션 쓰기 지연
- 엔티티의 영속화
등등
JPA와 트랜잭션은 긴밀한 관계를 맺고 있었습니다.
JPA에서는 데이터를 읽고, 쓰고, 저장하는 등의 행위를 트랜잭션과 같이 합니다.
트랜잭션이 선언된 메서드가 시작되면 트랜잭션이 실행됩니다.
( 엄밀히 말하자면 트랜잭션 내 SQL 쿼리가 발생하면 트랜잭션이 실행됩니다. -> 이미 만들어져 있는 커넥션 풀의 커넥션을 사용함)
그래서 트랜잭션 네 메서드가 정상적으로 완료되면 Commit을, 예외가 발생하면 Rollback이 이루어집니다.
그래서 위에서 트랜잭션의 commit과 관련된 내용으로 인해 발생한 문제를 해결할 방법으로 맨 처음 생각했던 것이, saveAndFlush였습니다.
맨 처음 생각했던 이유는 객체의 상태값을 바꾸고, 바꾼 객체의 상태값을 조회하기 위해 강제로 flush를 호출해주면 될 것이라 생각했습니다.
그러나 이 saveAndFlush()도 save 기능에 flush()가 추가된 것이지, saveAndFlush()가 호출되어도 원하는 쿼리는 발생하지 않았습니다.
그 이유는 트랜잭션 어노테이션때문이였습니다.
@transactional 어노테이션 이 선언되어 있으면 해당 메서드 단에서 작업이 완료되기 전까지 아직 commit이 이루어지지 않았기에 쿼리가 발생되지 않습니다.
결국 해당 로직을 해결하기 위한 방법으로 모색한 것이 @Transactional을 삭제하는 것이였습니다.
하지만 트랜잭션 어노테이션을 사용하지 않으면 결국 트랜잭션 어노테이션을 사용해서 얻는 이점과 관련된 일련의 행동들을 직접 구현해줘야 하는 단점이 있었습니다.
또한 트랜잭션과 긴밀한 관계를 유지하고 있는 JPA의 이점을 잘 활용하지 못하는 점도 있었습니다.
@Transactional 어노테이션을 삭제하며 다양한 valid 확인과 불필요한 로직이 추가되었지만, 원하는 방향으로 서비스 로직을 구현할 수 있었습니다.
이 일을 계기로 트랜잭션에 대해 다시 공부하고, 트랜잭션에 대해 많은 고민과 생각을 할 수 있었습니다.
무심코 지나갔던 트랜잭션이지만, 다시 짚어보며 내가 어느 부분을 놓치고 있는지, 이 상황을 계기로 어떤 공부를 할 수 있었는지 많은 것을 배우고 학습한 계기였습니다.
결론
서비스 로직에서 외부 API를 호출 ->
이 API는 다시 우리 서비스의 객체를 호출 ->
아직 트랜잭션이 끝나지 않았기에 객체의 상태 값이 변경되지 않아 원하는 방향대로 로직이 동작하지 않음 ->
트랜잭션 어노테이션의 삭제 및 saveAndFlush()로 문제 해결
- 트랜잭션 단위 중요하다.
- 트랜잭션 어떻게 잘 활용할 수 있을까?
'스프링 > 백엔드' 카테고리의 다른 글
그쪽도 홍박사(Redis)님을 아세요? (1) (0) | 2024.12.11 |
---|---|
Backend Layered Architecture (1) | 2023.11.06 |
Repository와 Service (0) | 2022.06.12 |
로깅 간단히 알아보기 (0) | 2022.05.18 |
REST API 기초와 사용법 (2022.12.08 추가) (0) | 2022.03.28 |