| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- builder-pattern
- QueryDSL
- 글또
- Level2
- 클라우드아키텍처
- constructor
- 글또10기
- DevOps
- static-factory-method
- 코엑스그랜드볼룸
- OpenSearch
- axios
- 레벨1
- UserLand
- React
- 다짐글
- design-pattern
- 3계층 아키텍처
- 클린 아키텍처
- 회고
- 글쓰기세미나
- 포트앤어댑터 아키텍처
- 프로그래머스
- ReverseNested
- 코딩테스트
- object-creation
- 헥사고날 아키텍처
- HashMap
- SpringBoot
- 가용영역
- Today
- Total
oguri's garage
(Spring) @Transactional을 Service 계층에 적용해야 하는 이유와 올바른 사용법 본문
Spring Framework에서 데이터의 일관성을 보장하는 @Transactional 어노테이션은 강력한 도구다.
처음 사용할 때는 해당 어노테이션을 Service 계층과 Repository 계층 사이에서 어디에 사용해야하는지 혼란이 있었다.
그래서 이 글을 통해 각 계층에 트랜잭션을 적용했을 때 발생하는 차이를 통해 올바른 위치를 확인해보려한다.
1. 트랜잭션의 본질: 원자성(Atomicity)
@Transactional의 존재 이유는 명확하다.
하나의 논리적 단위로 묶인 작업들의 원자성을 보장하는 것이다.
'계좌 이체'라는 고전적인 예시를 생각해 보자.
- A 계좌에서 금액을 차감한다.
- B 계좌에 동일한 금액을 증액한다.
이 두 작업은 논리적으로 분리될 수 없다.
하나의 작업만 성공하고 다른 작업이 실패하면 데이터의 상태는 심각하게 오염된다.
트랜잭션은 이처럼 '모두 성공'하거나 '모두 실패(원상복구)'하는 것을 보장하여 데이터의 무결성을 지키는 역할을 한다.
2. 계층의 역할과 책임: Service는 지휘자, Repository는 실행자
그렇다면 트랜잭션의 범위, 즉 '하나의 논리적 단위'는 무엇을 기준으로 삼아야 하는가.
답은 '비즈니스 로직'에 있다.
- Repository: 데이터베이스와의 통신을 책임지는 계층이다. 데이터를 저장하고, 조회하고, 수정하는 등 단일 데이터 소스에 대한 개별적인 실행에 집중한다. 이는 잘 만들어진 '도구'에 비유할 수 있다.
- Service: 비즈니스 요구사항을 해결하기 위해 Repository라는 도구들을 조합하고 지휘하는 계층이다. '게시글 작성', '주문 처리'와 같은 구체적인 시나리오를 완성하는 '프로젝트 매니저'의 역할을 수행한다.
'계좌 이체'라는 비즈니스 로직은 '출금 Repository'와 '입금 Repository'의 호출을 모두 포함한다.
따라서 트랜잭션은 이 전체 과정을 지휘하는 서비스 계층에서 관리하는 것이 자연스러운 귀결이다.
3. Case 1: Service 계층에 트랜잭션을 적용한 경우
가장 이상적이고 안전한, 교과서적인 방법이다. '게시글'과 관련 '태그'를 함께 저장하는 시나리오를 통해 살펴보자.
// PostService.java
@Service
public class PostService {
private final PostRepository postRepository;
private final TagRepository tagRepository;
// ... Constructor ...
@Transactional // 비즈니스 로직의 단위인 서비스 메서드에 트랜잭션을 선언한다.
public void createPostWithTags(Post post, List<Tag> tags) {
// 1. 게시글 저장
postRepository.save(post);
// 2. 태그 목록 저장
tagRepository.saveAll(tags);
}
}
이 구조에서 createPostWithTags 메서드는 하나의 트랜잭션으로 묶인다.
만약 tagRepository.saveAll() 실행 중 예외가 발생하면, 앞서 실행된 postRepository.save()의 결과까지 함께 롤백된다.
비즈니스 요구사항의 원자성이 지켜지며 데이터 일관성은 완벽하게 유지된다.
4. Case 2: Repository 계층에 트랜잭션을 적용한 경우
반면, 각 Repository 메서드에 @Transactional을 적용하면 문제는 복잡해진다.
// PostService.java
@Service
public class PostService {
// ...
// 서비스 계층에는 트랜잭션이 선언되어 있지 않다.
public void createPostWithTags(Post post, List<Tag> tags) {
// `postRepository.save()` 호출 시점에 TX 1이 시작되고, 성공과 함께 커밋된다.
postRepository.save(post);
// `tagRepository.saveAll()` 호출 시점에 TX 2가 시작되고, 실패 시 TX 2만 롤백된다.
tagRepository.saveAll(tags);
}
}
// 각 Repository 인터페이스의 메서드에 @Transactional이 선언된 상태
// public interface PostRepository extends JpaRepository<Post, Long> { @Transactional Post save(Post post); }
// public interface TagRepository extends JpaRepository<Tag, Long> { @Transactional <S extends Tag> List<S> saveAll(Iterable<S> entities); }
이 방식의 치명적인 문제는 각 Repository 호출이 독립적인 트랜잭션으로 실행된다는 점이다.postRepository.save()가 성공적으로 완료되면 해당 트랜잭션은 즉시 커밋되어 데이터베이스에 영구 반영된다.
이후 tagRepository.saveAll()에서 문제가 발생하더라도 이미 커밋된 게시글 데이터는 롤백되지 않는다.
결과적으로 DB에는 태그가 없는 불완전한 게시글 데이터만 남게 되며, 데이터의 정합성은 파괴된다.
5. 결론: 트랜잭션의 책임은 비즈니스 로직의 단위와 일치해야 한다
트랜잭션의 범위는 비즈니스 로직의 범위와 일치해야 한다.
Service 계층은 비즈니스 로직을 완성하는 '프로젝트 매니저'로서, 프로젝트 전체의 성공과 실패(커밋과 롤백)를 책임져야 마땅하다.
Repository는 단일 작업을 수행하는 '작업자'일 뿐, 프로젝트 전체의 책임을 지는 것은 그 역할과 맞지 않다.
따라서 @Transactional 어노테이션은 비즈니스 로직을 총괄하는 Service 계층의 public 메서드에 위치시키는 것이 가장 바람직한 설계라 할 수 있다.
'개발하다 > Spring' 카테고리의 다른 글
| @ControllerAdvice와 @ExceptionHandler로 전역 예외 처리하기 (0) | 2025.10.16 |
|---|---|
| MyBatis에서 JPA로 점진적 마이그레이션하기 (0) | 2025.10.12 |
| (Spring) Jackson 역직렬화 동작 방식과 안전한 코딩 패턴 (0) | 2025.10.04 |
| Spring vs Spring Boot 핵심 차이점 (0) | 2025.10.03 |
| Lombok 어노테이션(@Builder, @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor, @Data) (0) | 2025.10.02 |