김영한 개발자님의 스프링 DB 2편 강의를 수강하고 중요한 내용을 정리했습니다.
이름(String)만을 가지고있는 Member
와 로그내용(String)만을 가지고있는 Log
를 만든다. 그리고 각각의 Repository
를 만든다.
@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();
}
}
LogRepository
는 Log를 저장할 때 메세지가 로그예외
이면 예외를 발생시키는것 외에는 MemberRepository
와 동일하다
@Transactional // LogRepository
public void save(Log logMessage) {
log.info("log 저장");
em.persist(logMessage);
if (logMessage.getMessage().contains("로그예외")) {
log.info("log 저장시 예외 발생");
throw new RuntimeException("예외 발생");
}
}
public class MemberService {
private final MemberRepository memberRepository;
private final LogRepository logRepository;
public void joinV1(String username) {
... // 예외처리를 하지않는것 말곤 아래joinV2와 동일
}
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로그 저장시 예외가 발생하면 예외를 복구한다. 현재 별도의 트랜잭션은 설정하지 않는다.
MemberService
부터 MemberRepository
, LogRepository
를 모두 하나의트랜잭션으로 묶고 싶다.MemberRepository
만 호출하고 여기에만 트랜잭션을 사용하고 싶다.LogRepository
만 호출하고 여기에만 트랜잭션을 사용하고 싶다.트랜잭션 전파 없이 이런 문제를 해결하려면 아마도 트랜잭션이 있는 메서드와 트랜잭션이 없는 메서드를 각각 만들어야 할 것이다.
스프링은 @Transactional
이 적용되어 있으면 기본으로 REQUIRED
라는 전파 옵션을 사용한다. 이 옵션은 기존 트랜잭션이 없으면 트랜잭션을 생성하고, 기존 트랜잭션이 있으면 기존 트랜잭션에 참여한다. 참여한다는 뜻은 해당 트랜잭션을 그대로 따른다는 뜻이고, 동시에 같은 동기화 커넥션을 사용한다는 뜻이다.
모든 논리 트랜잭션을 커밋해야 물리 트랜잭션도 커밋된다. 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
회원 이력 로그를 DB에 남기는 작업에 가끔 문제가 발생해서 회원 가입 자체가 안되는 경우가 가끔 발생하게 되었다. 그래서 사용자들이 회원 가입에 실패해서 이탈하는 문제가 발생하기 시작했다.
회원 이력 로그의 경우 여러가지 방법으로 추후에 복구가 가능할 것으로 보인다. 그래서 비즈니스 요구사항이 변경되었다.
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 한다.
논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다.
어떻게 해결할까? REQUIRES_NEW
를 사용하면 된다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Log logMessage)
REQUIRES_NEW
는 항상 새로운 트랜잭션을 만든다. 따라서 해당 트랜잭션 안에서는 DB 커넥션도 별도로 사용하게 된다.
REQUIRES_NEW
를 사용한 신규 트랜잭션이므로 물리 트랜잭션을 롤백한다.
물리 트랜잭션을 롤백했으므로 rollbackOnly
를 표시하지 않는다. 여기서 REQUIRES_NEW
를 사용한 물리 트랜잭션은 롤백되고 완전히
끝이 나버린다.
이후 트랜잭션 AOP는 전달 받은 예외를 밖으로 던진다.
예외가 MemberService
에 던져지고, MemberService
는 해당 예외를 복구한다. 그리고 정상적으로 리턴한다.
결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백 되는 것을 확인할 수 있다.
REQUIRES_NEW
를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다. 따라서 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다.