지난 포스팅에서 스프링 트랜잭션 전파에 대해서 알아보았다. 이번 포스팅에서는 문제가 발생했을 때 상황에 따라 트랜잭션 전파를 활용해 해결하는 방법에 대해 알아보자.
public void joinV1(String username) {
// 회원 정보 저장
memberRepository.save(member);
// 로그 정보 저장
logRepository.save(logMessage);
}
@Transactional
public void save(Member member) {
}
public Optional<Member> find(String username) {
}
@Transactional()
public void save(Log logMessage) {
}
public Optional<Log> find(String message) {
}
트랜잭션을 리포지토리에서 시작하므로,
memberRepository.save와 logRepository.save는 각각 다른 트랜잭션으로 처리된다.
따라서 로그 저장이 실패하더라도 회원 저장은 영향을 받지 않는다.
@Transactional
public void joinV1(String username) {
// 회원 정보 저장
memberRepository.save(member);
// 로그 정보 저장
logRepository.save(logMessage);
}
public void save(Member member) {
}
public Optional<Member> find(String username) {
}
public void save(Log logMessage) {
}
public Optional<Log> find(String message) {
}
트랜잭션을 서비스에서 시작하므로,
memberRepository.save와 logRepository.save는 하나의 물리 트랜잭션으로 묶인다.
따라서 하나의 작업이라도 예외가 발생하면 트랜잭션은 롤백된다.
여기까지는 되게 단순한 상황이다.
그런데 만약, 로그만 따로 저장하고 싶다면? 아니면 회원 정보만 따로 저장하고 싶다면? 내부 트랜잭션에서 예외가 발생했다면? 다른 서비스 계층에서 MemberService를 참조한다면?
여기서부터는 점점 뇌에 과부하가 오기 시작한다.
위의 예시는 서비스단에서만 트랜잭션을 사용하였기 때문에 자연스럽게 두개의 작업도 하나의 트랜잭션으로 묶여 처리되었다. 하지만 아래의 상황이라면 어떨까?
만약 트랜잭션 전파가 없다면
클라이언트 A는 MemberService를 그대로 사용하면 되지만, 클라이언트 B와 C는 리포지토리에 접근하기 때문에 트랜잭션 없이 작업을 진행해야 한다.
때문에 개발자는 MemberRepository와 LogRepository에 @Transactional이 있는 메서드와 없는 메서드를 각각 따로 만들어야 할 것이다.
아래와 같이 더 복잡한 상황이 발생할 수도 있다.
이런 복잡한 문제를 해결할 수 있는 방법이 트랜잭션 전파이다. 코드로 확인해보자.
@Transactional
public void joinV1(String username) {
memberRepository.save(member);
logRepository.save(logMessage);
}
@Transactional
public void save(Member member) {
}
public Optional<Member> find(String username) {
}
@Transactional
public void save(Log logMessage) {
}
public Optional<Log> find(String message) {
}
2번의 예시와는 달리 리포지토리에도 @Transactional이 걸려있다.
MemberRepository만 사용하는 클라이언트는 MemberRepository.save를 사용해도 트랜잭션을 적용할 수 있다.
LogRepository만 사용하는 클라이언트도 LogRepository.save만 사용하더라도 트랜잭션을 적용할 수 있다.
이것이 가능한 이유는 트랜잭션 전파 때문이다.
@Transactional을 확인해보면 REQUIRES가 default값으로 설정이 되어 있는데, 이 값은 지난 포스팅에서 얘기했듯이
트랜잭션이 없으면 새로운 트랜잭션을 생성하고,
트랜잭션이 있다면 기존 트랜잭션에 참여한다고 했다.
따라서 클라이언트가 MemberService에 접근을 하든,MemberRepository에 접근을 하든, LogRepository에 접근을 하든 유연하게 트랜잭션을 적용할 수 있는 것이다.
물론 논리 트랜잭션 중 하나의 트랜잭션이라도 실패하면 물리 트랜잭션은 롤백이 될 것이다.
@Transactional
public void joinV2(String username) {
// 회원 정보 저장
memberRepository.save(member);
// 로그 정보 저장
try {
logRepository.save(logMessage);
} catch (RuntimeException e) {
log.info("log 저장에 실패했습니다. logMessage={}",logMessage.getMessage());
log.info("정상 흐름 반환");
}
}
@Transactional
public void save(Member member) {
}
public Optional<Member> find(String username) {
}
@Transactional
public void save(Log logMessage) {
if (logMessage.getMessage().contains("로그예외")) {
log.info("log 저장시 예외 발생");
throw new RuntimeException("예외 발생");
}
}
public Optional<Log> find(String message) {
}
흐름을 자세히 알아보자
@Transactional
public void joinV2(String username) {
memberRepository.save(member);
try {
logRepository.save(logMessage);
} catch (RuntimeException e) {
log.info("log 저장에 실패했습니다. logMessage={}",logMessage.getMessage());
log.info("정상 흐름 반환");
}
}
@Transactional
public void save(Member member) {
}
public Optional<Member> find(String username) {
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void save(Log logMessage) {
if (logMessage.getMessage().contains("로그예외")) {
log.info("log 저장시 예외 발생");
throw new RuntimeException("예외 발생");
}
}
public Optional<Log> find(String message) {
}
자세한 흐름을 보자
이번 포스팅에서는 트랜잭션 전파를 활용해 상황에 따라 문제를 해결하는 방법에 대해 알아보았다. 트랜잭션 전파에 대해서 알기 전에는 이런 문제가 발생할 수 있다는 생각을 하지 못했다. 실제로 프로젝트를 진행하면서 서비스 계층에 @Transactional을 사용하면 리포지토리에 @Transactional을 사용할 필요가 없지 않나? 하는 생각을 하였고, 리포지토리에서는 @Transactional을 사용하지 않았다.
하지만 실무에서 트랜잭션 적용에 있어서 직면할 수 있는 상황이 수십 수백가지가 될 수 있다는 것을 생각해봤을 때 다양한 방법을 고려해보는 과정이 필요하다는 것을 알게 되었다.
이번 트랜잭션 전파에 대해 공부하면서 여러 상황에서 직면할 수 있는 문제에 대한 해결 방법을 알게 되었고, 어플리케이션을 구성할 때 여러 방법을 고려해볼 수 있게 되는 시간이었다.
출처 : 김영한 - 스프링 DB 2편