You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
packagehello.springtx.propagation; // 패키지 선언importlombok.RequiredArgsConstructor;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.stereotype.Repository;
importorg.springframework.transaction.annotation.Transactional;
importjavax.persistence.EntityManager;
importjava.util.Optional;
@Slf4j// 로그 객체를 자동으로 생성해주는 Lombok 어노테이션@Repository// 이 클래스가 데이터 접근 계층(DAO)임을 나타냄@RequiredArgsConstructor// final 필드에 대해 생성자를 자동으로 생성해주는 Lombok 어노테이션publicclassMemberRepository {
privatefinalEntityManagerem; // JPA의 EntityManager로 데이터베이스 작업을 처리/** * 회원 정보를 데이터베이스에 저장하는 메서드 * @param member 저장할 회원 객체 */@Transactional// 메서드를 트랜잭션 안에서 실행하도록 설정publicvoidsave(Membermember) {
log.info("member 저장"); // 로그 출력em.persist(member); // JPA를 통해 회원 정보를 영속성 컨텍스트에 저장
}
/** * username으로 회원 정보를 조회하는 메서드 * @param username 조회할 회원의 사용자 이름 * @return 조회된 회원 정보(Optional로 반환) */publicOptional<Member> find(Stringusername) {
// JPQL을 사용하여 username으로 회원 정보를 조회returnem.createQuery("select m from Member m where m.username=:username", Member.class)
.setParameter("username", username) // JPQL의 파라미터 바인딩
.getResultList() // 결과를 리스트로 반환
.stream() // 스트림 변환
.findAny(); // 첫 번째 결과를 Optional로 반환
}
}
packagehello.springtx.propagation;
importlombok.RequiredArgsConstructor;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.stereotype.Repository;
importorg.springframework.transaction.annotation.Transactional;
importjavax.persistence.EntityManager;
importjava.util.Optional;
@Slf4j// 로그 객체를 생성@Repository// 데이터 접근 계층(DAO)을 나타냄@RequiredArgsConstructor// final 필드의 생성자를 자동 생성publicclassLogRepository {
privatefinalEntityManagerem; // JPA의 EntityManager로 데이터베이스 작업 수행/** * 로그 메시지를 데이터베이스에 저장하는 메서드 * @param logMessage 저장할 로그 객체 */@Transactional// 트랜잭션을 처리publicvoidsave(LoglogMessage) {
log.info("log 저장"); // 로그 출력em.persist(logMessage); // 로그 데이터를 영속성 컨텍스트에 저장if (logMessage.getMessage().contains("로그예외")) { // 특정 문자열 포함 여부 확인log.info("log 저장시 예외 발생"); // 예외 발생 전 로그 출력thrownewRuntimeException("예외 발생"); // 강제로 예외 발생
}
}
/** * 메시지를 기준으로 로그 데이터를 조회하는 메서드 * @param message 조회할 메시지 * @return 조회된 로그(Optional로 반환) */publicOptional<Log> find(Stringmessage) {
returnem.createQuery("select l from Log l where l.message = :message", Log.class) // JPQL 실행
.setParameter("message", message) // 파라미터 바인딩
.getResultList() // 결과 리스트 반환
.stream() // 스트림 변환
.findAny(); // 첫 번째 결과를 Optional로 반환
}
}
MemberService
회원을 등록하면 동시에 회원 등록에 대한 DB 로그도 함께 남김
JoinV1()
회원과 DB로그를 함께 남기는 비즈니스 로직
현재 별도의 트랜 잭션은 설정하지 않음
JoinV2()
joinV1()과 같은 기능을 수행
DB로그 저장시 예외가 발생하면 예외 복구
별도의 트랜잭션 저장하지 않음
packagehello.springtx.propagation;
importlombok.RequiredArgsConstructor;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.stereotype.Service;
@Slf4j// 로그 객체 생성@Service// 서비스 계층을 나타냄@RequiredArgsConstructor// final 필드의 생성자를 자동 생성publicclassMemberService {
privatefinalMemberRepositorymemberRepository; // Member 저장소privatefinalLogRepositorylogRepository; // Log 저장소/** * 회원과 로그를 저장하는 메서드 - 예외 발생 시 트랜잭션 롤백 * @param username 저장할 회원 이름 */publicvoidjoinV1(Stringusername) {
Membermember = newMember(username); // Member 객체 생성LoglogMessage = newLog(username); // Log 객체 생성log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member); // Member 저장log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
logRepository.save(logMessage); // Log 저장log.info("== logRepository 호출 종료 ==");
}
/** * 회원과 로그를 저장하는 메서드 - 로그 저장 중 예외를 처리 * @param username 저장할 회원 이름 */publicvoidjoinV2(Stringusername) {
Membermember = newMember(username); // Member 객체 생성LoglogMessage = newLog(username); // Log 객체 생성log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member); // Member 저장log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
try {
logRepository.save(logMessage); // Log 저장
} catch (RuntimeExceptione) {
log.info("log 저장에 실패했습니다. logMessage={}", logMessage.getMessage()); // 실패 로그 출력log.info("정상 흐름 변환"); // 예외를 정상 흐름으로 변환
}
log.info("== logRepository 호출 종료 ==");
}
}
클라이언트A가 MemberService를 호출하면서 트랜잭션 AOP 호출 : 여기서 신규 트랜잭션이 생성되고, 물리 트랜잭션도 시작
MemberRepository를 호출하면서 트랜잭션 AOP 호출 : 이미 트랜잭션이 있으므로 기존 트랜잭션에 참여
MemberRepository의 호출이 끝나고 정상 응답하면 트랜잭션 AOP가 호출 : 트랜잭션 AOP는 정상 응답이므로 트랜잭션 매니저에 커밋 요청, 이 경우 신규 트랜잭션이 아니므로 실제 커밋 호출하지 않음
LogRepository를 호출하면서 트랜잭션 AOP 호출 : 이미 트랜잭션이 있어, 기존 트랜잭션에 참여
LogRepository 로직에서 런타임 예외가 발생, 예외를 던지면 트랜잭션 AOP가 해당 예외를 받음 : 트랜잭션 AOP는 런타임 예외가 발생했으므로 트랜잭션 매니저에 롤백을 요청, 이 경우 신규 트랜잭션이 아니므로 물리 롤백을 호출하지 않음, 대신에 rollbackOnly를 설정
LogRepository가 예외를 던졌기 떄문에 트랜잭션 AOP도 해당 예외를 그대로 밖으로 던짐
MemberService에서도 런타임 예외를 받게 됨, 여기 로직에서는 해당 런타임 예외를 처리하지 않고 밖으로 던짐 : 트랜잭션 AOP는 런타임 예외가 발생했으므로 트랜잭션 매니저에 롤백을 요청, 이 경우 신규 트랜잭션이므로 물리 롤백을 호출, 참고로 이 경우 어차피 롤백이 되었기 떄문에, rollbackOnly 설정은 참고하지 않음, MemberService가 예외를 던졌기 때문에 트랜잭션 AOP도 해당 예외를 그대로 밖으로 던짐
클라이언트 A는 LogRepository 부터 넘어온 런타임 예외를 받음
정리
회원과 회원 이력 로그를 처리하는 부분을 하나의 트랜잭션으로 묶은 덕분에 문제가 발생했을 때 회원관 회원 이력 로그가 모두 함께 롤백 됨, 따라서 데이터 적합성에 문제가 발생하지 않음 트랜잭션 전파 활용6 - 복구 REQUIRED
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 함
단순하게 생각하면 LogRepository에서 예외가 발생하면 그것을 MemberService에서 예외를 잡아서 처리하면 될 것 같음
이렇게 하면 MemberService에서 정상 흐름으로 바꿀 수 있기 때문에 MemberService의 트랜잭션 AOP에서 커밋을 수행할 수 있음, 근데 좀 이상함 -> 실패함
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
트랜잭션 전파 활용1 - 예제 프로젝트 시작
비즈니스 요구사항
Member
MemberRepository
Log
LogRepository
MemberService
JoinV1()
JoinV2()
테스트 코드
참고
JPA와 데이터 변경
트랜잭션 전파 활용2 - 커밋, 롤백
서비스 계층에 트랜잭션이 없을 때 - 커밋
상황
outerTxOff_success
1. MemberService에서 MemberRepository를 호출, MemberRepository에는 @transactional 애노테이션이 있으므로 트랜잭션 AOP가 작동, 여기서 트랜잭션 매니저를 통해 트랜잭션을 시작(여기서 시작한 트랜잭션을 트랜잭션 B라 함)
1-1. 그림에서는 생략했지만, 트랜잭션 매니저에 트랜잭션을 요청하면 데이터소스를 통해 커넥션 con1을 획득하고, 해당 커넥션을 수동 커밋 모드로 변경해서 트랜잭션을 시작
1-2. 트랜잭션 동기화 매니저를 통해 트랜잭션을 시작한 커넥션을 보관
1-3. 트랜잭션 매니저의 호출 결과로 status를 반환, 여기서는 신규 트랜잭션 여부가 참이 됨
2. MemberRepository는 JPA를 통해 회원을 저장하는데, 이때 JPA는 트랜잭션이 시작된 con1을 사용해서 회원을 저장
3. MemberRepository가 정상 응답을 반환했기 떄문에 AOP는 트랜잭션 매니저에 커밋을 요청
4. 트랜잭션 매니저는 con1을 통해 물리 트랜잭션을 커밋
4-1. 이 시점에 앞서 설명한 신규 트랜잭션 여부, rollbackOnly 여부 모두 체크
@transactional과 REQUIRED
서비스 계층에 트랜잭션이 없을 때 - 롤백
상황
outerTxOff_fail
로그예외 로직
LogRepository 응답 로직
참고
트랜잭션 전파 활용3 - 단일 트랜잭션
트랜잭션 하나만 사용하기
singleTx
MemberService - joinV1()
MemberRepository - save()
LogRepository - save()
참고
각각 트랜잭션이 필요한 상황(아래 문제 발생 상황)
으로 묶고 싶음
LogRepository 의 트랜잭션 코드를 제거하면 앞서 배운 것 처럼 깔끔하게 하나의 트랜잭션을 적용 가능
적용할 수 없음
###트랜잭션 전파 활용4 - 전파 커밋
모든 논리 트랜잭션 커밋
outerTxOn_success
###트랜잭션 전파 활용5 - 전파 롤백
outerTxOn_fail
여기서는 로그예외로 넘겼기 때문에 LogRepository에서 런타임 예외 발생
정리
트랜잭션 전파 활용6 - 복구 REQUIRED
회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 함
실패하는 이유
recoverException_fail
/**
MemberService @transactional:ON
MemberRepository @transactional:ON
LogRepository @transactional:ON Exception
*/
@test
void recoverException_fail() {
//given
String username = "로그예외_recoverException_fail";
}
모든 트랜잭션을 켜자
여기서 memberService.joinV2()를 호출하는 부분을 주의해야 함, JoinV2()에는 예외를 잡아서 정상 흐름으로 변환하는 로직이 추가 됨
정리
트랜잭션 전파 활용7 - 복구 REQUIRES_NEW
recoverException_success
/**
MemberService @transactional:ON
MemberRepository @transactional:ON
LogRepository @transactional(REQUIRES_NEW) Exception
*/
@test
void recoverException_success() {
//given
String username = "로그예외_recoverException_success";
}
LogRepository - save()
REQUIRES_NEW - 복구
결과적으로 회원 데이터는 저장되고, 로그 데이터만 롤백 되는 것을 확인
정리
주의
REQUIRES_NEW를 사용하지 않고 구조를 변경하는 것
All reactions