트랜잭션 분리 작업을 하면서 발생한 문제들에 대해 정리한 포스트 입니다.
💡 @Transactional 선언전 트랜잭션
→ 트랜잭션 기능이 포함된 프록시 객체가 생성 자동으로 commit or rollback
💡 @Transactional propagation(전파속성)
→ @Transactional(propagation=Propagation.REQUIRES_NEW)
항상 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션이 있다면 잠깐 보류하고
해당 트랜잭션을 먼저 진행
트랜잭션 분리 작업을 하면서 크게 아래의 두 문제가 발생했다.
원인은 아래의 문제들이 복합적으로 엮여서 발생했다.
// O
@Transactional
public void search(SearchDto searchDto) {
...
}
// X
@Transactional
protected void search(SearchDto searchDto) {
...
}
// X
@Transactional
private void search(SearchDto searchDto) {
...
}
@Controller
class PackingController {
packingService.packing();
}
@Service
class PackingService {
// 트랜잭션 선언되어있지 않음.
public void packing() {
...
callExternalApi();
processCompleted();
}
@Transactional
public void callExternalApi() {
...
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void processCompleted() {
...
}
}
PackingController에서 PackingService의 packing() 메소드를 호출했지만
packing() 메소드에 @Transactional이 선언되어있지 않기 때문에,
callExternalApi(), processComplete() 두 메소드의 트랜잭션 역시 적용되지 않은 상태이다.
이 상황의 문제점은 동작해보면 잘 되는것 같지만 트랜잭션이 모두 적용되어있지 않아서 예외가 발생하더라도 데이터가 롤백되지 않는 문제가 발생한다
@Transactional(propagation=Propagation.REQUIRES_NEW) 새로운 트랜잭션은
동일한 클래스 메소드끼리 호출하면 새로 생성되지 않음. 반드시 다른 클래스 메소드를 호출해야함.
// 문제상황
class PackingService {
@Transactional
public void packing() {
...
callExternalApi();
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void callExternalApi() {
...
}
}
packing() 메소드에서 callExternalApi()를 호출해도 동일한 클래스이기 때문에
새로운 트랜잭션이 시작되지않고 기존 packing() 트랜잭션이 이어서 적용
된다.
// 문제해결
class PackingService {
@Transactional
public void packing() {
...
packingApiService.callExternalApi();
}
}
class PackingApiService() {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void callExternalApi() {
...
}
}
PackingService 클래스의 packing() 메소드에서 다른 클래스 PackingApiService의
callExternalApi() 메소드를 호출해야 새로운 트랜잭션이 정상적으로 적용된다.
원인은 아래 링크 참조
정상적으로 트랜잭션이 적용됐음에도 불구하고
@Transactional(propagation=Propagation.REQUIRES_NEW) 메소드에서 무한로딩이 발생하는 상황
class PackingApiService {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void callExternalApi(ApiDto apiDto) {
Long packingId = apiDto.getPackingId();
String name = apiDto.getName();
saveInvoiceEntity(id, name);
...
}
private void saveInvoiceEntity(Long packingId, String name) {
PackingEntity packingEntity = packingRepository.getOne(packingId);
InvoiceEntity invoiceEntity = InvoiceEntity.builder()
.packing(packingEntity)
.name(name)
.build();
invoiceRepository.save(invoiceEntity);
}
}
callExternalApi() 메소드 파라미터로 넘어온 apiDto는 이전 트랜잭션의 Querydsl 결과값에서 가져온 dto다.
saveInvoiceEntity() 메소드에서 해당 dto 내부 값으로 Packing엔티티를 조회하고 새로운 엔티티를
저장하는데 이 때, API 응답 무한로딩이 발생한다.
class PackingApiService() {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void callExternalApi(Long packingId) {
ApiDto apiDto = apiRepository.findApiDtoById(packingId).orElseThrow();
Long packingId = apiDto.getPackingId();
String name = apiDto.getName();
saveInvoiceEntity(packingId, name);
...
}
private void saveInvoiceEntity(Long packingId, String name) {
PackingEntity packingEntity = packingRepository.getOne(packingId);
InvoiceEntity invoiceEntity = InvoiceEntity.builder()
.packing(packingEntity)
.name(name)
.build();
invoiceRepository.save(invoiceEntity);
}
}
해결방법은 새로운 트랜잭션 내에서 파라미터로 받은 id로 apiDto를 조회 후 다음 로직을 수행하면
정상적으로 동작한다.
Spring 동일한 Bean(Class)에서 @Transactional 동작 방식 - Yun Blog | 기술 블로그