회사에서 프로젝트중 내부 호출 이슈를 만나서 클래스 분리를 통해 해결한 예제를 기록하고자 한다.
// 인증 번호 확인
@Transactional
public void confirmAuthenticationInfo(AuthenticationRequest.Confirm request){
log.info(" ======= 1. 인증 내역 조회 =======");
MemberCertificationHistory history = findBy... // 엔티티 조회
...
log.info(" ======= 3. 인증 만료 검증 ======= ");
validateExpiration(history);
...
}
// 만료 일시 검증
@Transactional
private void validateExpiration(MemberCertificationHistory history){
if(history.isExpired()){
processExpiration(history)
throw new ServerException("인증 정보 만료"...);
}
}
// 인증 정보 만료 처리
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void processExpiration(MemberCertificationHistory history){
MemberCertificationInfo memberCertInfo = history.getMemberCertificationInfo();
memberCertInfo.processExpiration();
memberCertInfoRepository.save(memberCertInfo);
}
구조는 아래와 같이 간단하다.
confirmAuthenticationInfo() -> validateExpiration() -> processExpiration() 순으로 호출되는데
validateExpiration()에서 만료를 검증하고 만료일자가 지났다면 트랜잭션이 별도로 생성된(REQUIRES_NEW) processExpiration()에서 만료 처리를 한후 validateExpiration()에서 에러를 throw하는 구조이다.
트랜잭션이 분리된 processExpiration()에서 .save()로 만료 처리를 db에 반영한후, 다시 validateExpiration()으로 돌아와서 throw를 날릴것으로 예상하였다.
그러나 이것은 잘못된 생각이였다.
곰곰히 생각해보니 안될 이유는 전에 강의에서 들었던 내부호출 이슈같은데 내가 개념을 잘못 알고있었다.
내가 알고있는 내부호출이란것은 동일 클래스내에서 1.트랜잭션이 없는 메서드에서 트랜잭션이 있는 메서드를 호출할때, 두 메서드 모두 트랜잭션이 적용되지 않는 현상을보고 내부호출이라고 생각하였는데 그게아니고,
정확히는 트랜잭션이 있던없던 2.동일한 클래스 내에서 자기 자신의 메서드를 직접 호출하는 것을 내부호출이라고 하는것이다.
결론적으로 1번은 내부 호출의 예시이고 2번이 정확한 의미이다.
따라서 위의 경우도 동일한 클래스 내이기때문에 자기 자신의 메서드를 직접 호출하고 있으며, 결국 this.로 인해 **프록시(proxy)**를 거치지 않고 직접 호출되므로 AOP, 트랜잭션(@Transactional) 등의 Spring 프레임워크 기능이 적용되지 않는것이다.
따라서 클래스를 분리하여 해결하였다.
기존의 validateExpiration()을 아래처럼 외부 클래스의 메서드를 호출하고
// 만료 일시 검증
@Transactional
private void validateExpiration(MemberCertificationHistory history){
if(history.isExpired()){
authTransactionService.processExpiration(history.getCertDsctnSeqNo()); // 외부 클래스 호출
throw new ServerException(HttpStatus.INTERNAL_SERVER_ERROR, "ER0501-4");
}
}
외부 클래스의 메서드는 아래와 같이 생성하였다.
public class AuthTransactionService {
...
/**
* 인증 정보를 만료 처리합니다.
*
* 1. Spring 트랜잭션 내부 호출이슈로 타 클래스로 분리
* 2. 기존 history 객체는 새로운 트랜잭션 생성으로 인해 생성된 영속성 컨텍스트에 없는상태
* 3. 2번으로 인해 아래 트랜잭션의 영속성 컨텍스트에서 관리하도록 다시 history 객체 조회가 필요
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processExpiration(Long seqNo){
MemberCertificationHistory history = memberCertHisRepository.findById(seqNo).orElseThrow();
MemberCertificationInfo info = history.getMemberCertificationInfo();
info.processExpiration();
// memberCertHisRepository.save(history); 1차 캐시로 dirtyChecking 되므로 필요없음
}
...
}
이때 만약 기존 클래스에서 파라미터로 엔티티 객체 자체를 넘기게되면 위에서 새로 생성된 트랜잭션내의 영속성 컨텍스트에선 해당 객체가 없는 상태이기 때문에 .save()를 해도 결론적으로 update쿼리가 나가지 않는다.
따라서 새롭게 조회를해주기 위해 key값을 파라미터로 넘기게 하였고, 이 값으로 조회된 객체는 새롭게 생성된 트랜잭션내의 영속성 컨텍스트의 1차 캐시에 저장이되고, .save() 및 dirtyChecking이 가능하게 된다.
'지식 저장소' 카테고리의 다른 글
등록 로직에서 dto를 Entity로 변환할때 변환하는 코드는 어디에? (0) | 2023.08.31 |
---|---|
JPA insert쿼리전에 select쿼리가 나가는 현상 ( feat.HHH000099 에러) (0) | 2023.08.28 |
MPA와 SPA란? (0) | 2023.05.11 |
같은 값 기준 정렬 후 페이징 시 목록 꼬임 문제 (0) | 2023.01.10 |
Replication(복제) (0) | 2022.09.08 |