문제
@Transaction이 붙은 메서드가 해당 클래스 내에 또다른 @Transactional이 붙은 메서드를 호출합니다.
이 때 새로운 트랜잭션이 적용될까요, 적용되지 않을까요?
정답
새 트랜잭션은 적용되지 않습니다.
이유
프록시 때문이야~🎶
설명
왜 프록시 때문이라고 하는지, 왜 두 번째 @Transactional은 제대로 동작하지 않는지 궁금하시죠? 바로 프록시 때문입니다.
프록시란?
프록시는 객체에 대한 접근을 제어해야 하거나 객체에 접근 시 추가 기능을 제공해야 할 경우 사용될 수 있습니다.
예를 들어, 요청을 보낸 클라이언트에게 응답하기 전 먼저 올바른 권한이 있는지 검사하고 싶을 때 사용될 수 있다는 말이죠.
프록시를 사용할 때의 장점은 다음과 같습니다.
- 사이즈가 큰 객체가 로딩되기 전이라도 프록시를 통해 참조할 수 있다.
- 로컬에 있지 않은, 원격에 있는 객체를 사용할 수 있다.
- 민감 객체에 대한 접근 시 전처리를 할 수 있다.
단점도 있어요.
- 객체 생성이 빈번하게 일어나는 경우 성능이 저하될 수 있다.
- 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능이 저하될 수 있다.
프록시는 원본 객체를 상속받아 만들어집니다.
따라서 클라이언트는 원본 객체라고 생각할 수 있어요.
프록시가 적용될 때, 이런 흐름으로 Request 파이프라인이 구성될 수 있습니다.
트랜잭션과는 어떤 관련이 있나요?
스프링에서는, 프록시가 @Transactional이 붙은 빈 메서드로의 요청을 가로챕니다.
이런 코드가 있다고 가정합시다.
1) AnswerService
@Service
@RequiredArgsConstructor
public class AnswerService {
private final AnswerMapper answerMapper;
private final CounselService counselService;
@Transactional
public void registerAnswerForCounsel(Answer answer, User user) {
answer.setCreatorId(counselorId);
answer.setModifierId(counselorId);
registerAnswer(answer);
counselService.updateCounselStatus(counsel, counselorId, CounselStatus.COMPLETED);
}
@Transactional
public void registerAnswer(Answer answer) {
answerMapper.insertAnswer(answer);
answerMapper.insertAnswerInHistory(answer.getId());
}
}
2. CounselService
@Service
@RequiredArgsConstructor
public class CounselService {
@Transactional
public void updateCounselStatus(Counsel counsel, String counselorId, CounselStatus status) {
counsel.setModifierId(counselorId);
counsel.setStatus(status.toString());
counselMapper.updateCounselStatus(counsel);
counselMapper.insertCounselHistory(counsel.getId());
}
}
코드 흐름은 다음과 같습니다.
1. AnswerController로 답변 등록 요청이 매핑됩니다.
2. AnswerService의 registerAnswerForCounsel이 호출됩니다.
3. registerAnswer() 메서드가 호출됩니다.
4. updateCounselStatus() 메서드가 호출됩니다.
메서드 명을 보면 어떤 일을 하는 코드인지 알 수 있겠죠?
상담 건에 대한 답변을 등록하고, 상담의 상태를 COMPLETED로 변경해주는 코드입니다.
여기서 저는 이런 식으로 트랜잭션이 처리되기를 원했습니다.
하지만 실제로는 이렇게 처리되었을 거예요.
왜 그럴까요?
문제는 registerAnswer() 메서드가 AnswerService 클래스 내부에 있는 메서드라는 데에서부터, 아니 AnswerService의 프록시가 이 요청을 받았다는 데에서부터 출발합니다.
@Transactional이 붙은 registerAnswerForCounsel() 메서드가 호출되었으므로 AnswerService 프록시가 요청을 받습니다.
1. 트랜잭션을 시작하고
2. 원본 AnswerService 객체의 registerAnswerForCounsel() 메서드를 호출해요.
3. 코드가 실행되다가, registerAnswer() 메서드를 호출합니다.
여기서 잘 생각해야 합니다.
지금은 원본 객체에서 메서드가 실행되고 있어요.
이 메서드는 프록시에서 호출되지 않았고,
그래서 registerAnswer() 메서드의 트랜잭션 처리는 우리가 원하는대로 이루어지지 않습니다.
그럼 다음 흐름을 계속 볼까요?
4. 또 코드가 실행되다가 이번에는 updateCounselStatus() 메서드를 호출하네요.
5. CounselService 프록시가 해당 요청을 받습니다.
6. CounselService 프록시는 트랜잭션을 시작하고
7. 원본 CounselService 객체의 updateCounselStatus() 메서드를 호출해요.
8. 정상적으로 수행되었다면,
9. CounselService 프록시에게 리턴되어, 프록시는 트랜잭션을 커밋합니다.
10. 정상적으로 수행되었다면, 원본 AnswerService에게 리턴되고 여기서 또 AnswerService 프록시에게 리턴됩니다.
11. AnswerService 프록시는 트랜잭션을 커밋하고, Controller에게 리턴됩니다.
간략하게 표현하면 다음과 같이 동작하게 되는 것이지요.
그래서 내부 메서드인 registerAnswer() 메서드에 붙인 @Transactional이 작동하지 않는 거랍니다.
프록시 때문이야~🎶
'Spring' 카테고리의 다른 글
테스트코드의 필요성 (0) | 2022.05.09 |
---|---|
Spring REST Docs + ɑ 도입기 (0) | 2022.04.04 |