스프링 DB 2편 - 데이터 접근 활용 기술 (16) - 데이터 접근 기술 - 스프링 트랜잭션 전파 2 - 활용 1
인프런 스프링 DB 2편 - 데이터 접근 활용 기술편을 학습하고 정리한 내용 입니다.
트랜잭션 전파 활용 1 - 예제 프로젝트 시작
비즈니스 요구사항
- 회원을 등록하고 조회한다.
- 회원에 대한 변경 이력을 추적할 수 있도록 회원 데이터가 변경될 때 변경 이력을 DB LOG 테이블에 남겨야 한다.
- 여기서는 예제를 단순화 하기 위해 회원 등록시에만 DB LOG 테이블에 남긴다.
Member - 도메인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter @Setter
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
public Member(String username) {
this.username = username;
}
public Member() {}
}
- JPA를 통해 관리하는 회원 엔티티이다.
- 예제라서
@setter같은 것 그냥 쓰겠다.
MemberRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
@Transactional
public void save(Member member) {
log.info("member 저장");
em.persist(member);
}
public Optional<Member> find(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList().stream().findAny();
}
}
- JPA를 사용하는 회원 리포지토리이다. 저장과 조회 기능을 제공한다.
Log - 도메인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Getter
@Setter
@Entity
public class Log {
@Id @GeneratedValue
private Long id;
private String message;
public Log(String message) {
this.message = message;
}
public Log() {}
}
- JPA를 통해 관리하는 로그 엔티티이다.
LogRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@RequiredArgsConstructor
@Repository
public class LogRepository {
private final EntityManager em;
//@Transactional
public void save(Log logMessage) {
log.info("log 저장");
em.persist(logMessage);
if (logMessage.getMessage().contains("로그예외")) {
log.info("log 저장 시 예외 발생");
throw new RuntimeException("예외 발생");
}
}
public Optional<Log> find(String message) {
return em.createQuery("select l from Log l where l.message = :message", Log.class)
.setParameter("message", message)
.getResultList().stream().findAny();
}
}
- JPA를 사용하는 로그 리포지토리이다. 저장과 조회 기능을 제공한다.
- 중간에 예외 상황을 재현하기 위해
로그예외라고 입력하는 경우 예외를 발생시킨다.
MemberService
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
31
32
33
34
35
36
37
38
39
40
@Slf4j
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final LogRepository logRepository;
//@Transactional
public void joinV1(String username) {
Member member = new Member(username);
Log logMessage = new Log(username);
log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member);
log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
logRepository.save(logMessage);
log.info("== logRepository 호출 종료 ==");
}
public void joinV2(String username) {
Member member = new Member(username);
Log logMessage = new Log(username);
log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member);
log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
try {
logRepository.save(logMessage);
} catch (RuntimeException e) {
log.info("log 저장에 실패했습니다. logMessage={}",logMessage.getMessage());
log.info("정상 흐름 반환");
}
log.info("== logRepository 호출 종료 ==");
}
}
- 회원을 등록하면서 동시에 회원 등록에 대한 DB 로그도 함께 남긴다.
joinV1()- 회원과 DB로그를 함께 남기는 비즈니스 로직이다.
- 현재 별도의 트랜잭션은 설정하지 않는다.
joinV2()joinV1()과 같은 기능을 수행한다.- DB로그 저장 시 예외가 발생하면 예외를 복구한다.
- 현재 별도의 트랜잭션은 설정하지 않는다.
MemberServiceTest
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
31
@Slf4j
@SpringBootTest
class MemberServiceTest {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepository;
@Autowired
private LogRepository logRepository;
/**
* memberService @Transactional : OFF
* memberRepository @Transactional : ON
* LogRepository @Transactional : ON
*/
@Test
void outerTxOff_success() {
//given
String username = "outerTxOff_success";
//when
memberService.joinV1(username);
//then
assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}
}
정상 동작하는지 테스트 코드를 만들어서 수행해보자.
JPA와 데이터 변경
- JPA를 통한 모든 데이터 변경(등록, 수정, 삭제)에는 트랜잭션이 필요하다. (조회는 트랜잭션 없이 가능하다.)
- 현재 코드에서 서비스 계층에 트랜잭션이 없기 때문에 리포지토리에 트랜잭션이 있다.
트랜잭션 전파 활용 2 - 커밋, 롤백
서비스 계층에 트랜잭션이 없을 때 - 커밋
예제를 통해 서비스 계층에 트랜잭션이 없을 때 트랜잭션이 각각 어떻게 작동하는지 확인해보자.
상황
- 서비스 계층에 트랜잭션이 없다.
- 회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있다.
- 회원, 로그 리포지토리 둘 다 커밋에 성공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* memberService @Transactional : OFF
* memberRepository @Transactional : ON
* LogRepository @Transactional : ON
*/
@Test
void outerTxOff_success() {
//given
String username = "outerTxOff_success";
//when
memberService.joinV1(username);
//then
assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}

MemberService에서MemberRepository를 호출한다.MemberRepository에는@Transactional애노테이션이 있으므로 트랜잭션 AOP가 작동한다. 여기서 트랜잭션 매니저를 통해 트랜잭션을 시작한다. 이렇게 시작한 트랜잭션을 트랜잭션B라 하자.- 그림에서는 생략했지만, 트랜잭션 매니저에 트랜잭션을 요청하면 데이터소스를 통해 커넥션
con1을 획득 하고, 해당 커넥션을 수동 커밋 모드로 변경해서 트랜잭션을 시작한다. - 그리고 트랜잭션 동기화 매니저를 통해 트랜잭션을 시작한 커넥션을 보관한다.
- 트랜잭션 매니저의 호출 결과로
status를 반환한다. 여기서는 신규 트랜잭션 여부가 참이 된다.
- 그림에서는 생략했지만, 트랜잭션 매니저에 트랜잭션을 요청하면 데이터소스를 통해 커넥션
MemberRepository는 JPA를 통해 회원을 저장하는데, 이때 JPA는 트랜잭션이 시작된con1을 사용해서 회원을 저장한다.MemberRepository가 정상 응답을 반환했기 때문에 트랜잭션 AOP는 트랜잭션 매니저에 커밋을 요청 한다.- 트랜잭션 매니저는
con1을 통해 물리 트랜잭션을 커밋한다.- 물론 이 시점에 앞서 설명한 신규 트랜잭션 여부,
rollbackOnly여부를 모두 체크한다.
- 물론 이 시점에 앞서 설명한 신규 트랜잭션 여부,
이렇게 해서 MemberRepository와 관련된 모든 데이터는 정상 커밋되고, 트랜잭션B는 완전히 종료된다. 이후에 LogRepository를 통해 트랜잭션C를 시작하고, 정상 커밋한다.
결과적으로 둘다 커밋되었으므로 Member, Log모두 안전하게 저장된다.
@Transactional과 REQUIRED
- 트랜잭션 전파의 기본 값은
REQUIRED이다. 따라서 다음 둘은 같다.@Transactional(propagation = Propagation.REQUIRED)@Transactional
REQUIRED는 기존 트랜잭션이 없으면 새로운 트랜잭션을 만들고, 기존 트랜잭션이 있으면 참여한다.
서비스 계층에 트랜잭션이 없을 때 - 롤백
상황
- 서비스 계층에 트랜잭션이 없다.
- 회원, 로그 리포지토리가 각각 트랜잭션을 가지고 있다.
- 회원 리포지토리는 정상 동작하지만 로그 리포지토리에서 예외가 발생한다.
outerTxOff_fail
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* memberService @Transactional : OFF
* memberRepository @Transactional : ON
* LogRepository @Transactional : ON EXCEPTION
* */
@Test
void outerTxOff_fail() {
//given
String username = "로그예외_outerTxOff_fail";
//when
assertThatThrownBy(() -> memberService.joinV1(username))
.isInstanceOf(RuntimeException.class);
//then
assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isEmpty());
}
- 사용자 이름에
로그예외라는 단어가 포함되어 있으면LogRepository에서 런타임 예외가 발생한다. - 트랜잭션 AOP는 해당 런타임 예외를 확인하고 롤백 처리한다.


MemberService에서MemberRepository를 호출하는 부분은 앞서 설명한 내용과 같다. 트랜잭션이 정상커밋되고, 회원 데이터도 DB에 정상 반영된다.MemberService에서LogRepository를 호출하는데,로그예외라는 이름을 전달한다. 이 과정에서 새로운 트랜잭션 C가 만들어진다.
LogRepository 응답 로직
LogRepository는 트랜잭션C와 관련된con2를 사용한다.로그예외라는 이름을 전달해서LogRepository에 런타임 예외가 발생한다.LogRepository는 해당 예외를 밖으로 던진다. 이 경우 트랜잭션 AOP가 예외를 받게된다.- 런타임 예외가 발생해서 트랜잭션 AOP는 트랜잭션 매니저에 롤백을 호출한다
- 트랜잭션 매니저는 신규 트랜잭션이므로 물리 롤백을 호출한다.
이 경우 회원은 저장되지만, 회원 이력 로그는 롤백된다. 따라서 데이터 정합성에 문제가 발생할 수 있다. 둘을 하나의 트랜잭션으로 묶어서 처리해보자.
트랜잭션 전파 활용 3 - 단일 트랜잭션
트랜잭션 하나만 사용하기
회원 리포지토리와 로그 리포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 이 둘을 호출하는 회원 서비스에만 트랜잭션을 사용하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* memberService @Transactional : ON
* memberRepository @Transactional : OFF
* LogRepository @Transactional : OFF
*/
@Test
void singleTx() {
//given
String username = "singleTx";
//when
memberService.joinV1(username);
//then
assertTrue(memberRepository.find(username).isPresent());
assertTrue(logRepository.find(username).isPresent());
}
- 테스트 상위의 주석을 참고하자. 어디에 트랜잭션을 걸고 빼야 하는지 나와있다.
MemberRepository,LogRepository의@Transactional코드를 제거하자.- 그리고
MemberService에만@Transactional코드를 추가하자.

- 이렇게 하면
MemberService를 시작할 때 부터 종료할 때 까지의 모든 로직을 하나의 트랜잭션으로 묶을 수 있다.- 물론
MemberService가MemberRepository,LogRepository를 호출하므로 이 로직들은 같은 트랜잭션을 사용한다.
- 물론
MemberService만 트랜잭션을 처리하기 때문에 앞서 배운 논리 트랜잭션, 물리 트랜잭션, 외부 트랜잭션, 내부 트랜잭션, rollbackOnly, 신규 트랜잭션, 트랜잭션 전파와 같은 복잡한 것을 고민할 필요가 없다. 아주 단순하고 깔끔하게 트랜잭션을 묶을 수 있다.

@Transactional이MemberService에만 붙어있기 때문에 여기에만 트랜잭션 AOP가 적용된다.MemberRepository,LogRepository는 트랜잭션 AOP가 적용되지 않는다.
MemberService의 시작부터 끝까지, 관련 로직은 해당 트랜잭션이 생성한 커넥션을 사용하게 된다.MemberService가 호출하는MemberRepository,LogRepository도 같은 커넥션을 사용하면서 자연스럽게 트랜잭션 범위에 포함된다.
댓글남기기