백엔드/Spring

자기호출 Self Invocation

infitry 2024. 5. 25. 17:25
반응형

같은 팀원의 코드리뷰 중 JPA 변경감지를 사용하지 않고 명시적으로 saveAll 메서드를 호출하고 있는 코드를 발견하였습니다.

변경감지를 사용하여 처리하는 게 어떻겠냐고 제안하였고 팀원 분께서는 변경감지가 동작하지 않는다고 하였습니다.

메서드에는 @Transactional 어노테이션이 걸려있었고 트랜잭션 커밋 시 영속성 컨텍스트가 flush 되기 때문에 저는 동작할 것이라고 예상했는데 말이죠.

 

팀원 분께 괜찮다면 제가 한 번 실행해 봐도 되냐고 여쭤본 후 실행해 보니 정말 변경감지가 동작하지 않았습니다!🫨

계속 확인해 보다가 이상한 점을 발견했습니다.

팀원 분이 작성하신 코드는 다음과 같은 구조를 가지고 있었습니다.

@Service
public class SelfInvocation {
    public String parent() {
    	...
        child();
    }
    
    @Transactional
    public void child() {
    
    }
}

 

컨트롤러에서는 parent() 메서드를 호출하고 있었습니다.

문제는 @Transactional 어노테이션이 프록시 객체를 통해 해당 로직을 실행하기 때문에 발생하였습니다.

공식 문서에서는 다음과 같이 설명하고 있습니다.

같은 클래스 내 호출은 차단된다고 합니다.

 

실제로 테스트를 해보겠습니다.

다음과 같은 코드가 있습니다. Order는 간단한 JPA Entity입니다.
전체 코드는 다음 GITHUB 에서 확인해 보실 수 있습니다.

@Service
@RequiredArgsConstructor
public class SelfInvocationService {

    private final OrderRepository orderRepository;

    public OrderDto useSelfInvocation() {
        var savedOrder = createNewOrder();
        child(savedOrder);
        return orderRepository.findById(savedOrder.getId())
                .map(OrderDto::from)
                .orElseThrow();
    }

    @Transactional
    public OrderDto unUseSelfInvocation() {
        var savedOrder = createNewOrder();
        child(savedOrder);
        return orderRepository.findById(savedOrder.getId())
                .map(OrderDto::from)
                .orElseThrow();
    }

    /**
     * Self-invocation 으로 인해 트랜잭션이 적용될 수 없음.
     * 따라서 트랜잭션 커밋이 발생하지 않는다.
     */
    @Transactional
    public void child(Order order) {
        order.setOrderName("order2");
    }

    private Order createNewOrder() {
        var order = new Order();
        order.setOrderName("order1");
        return orderRepository.save(order);
    }
}

 

useSelfInvocation() 메서드와 unUseSelfInvocation() 메서드가 외부로 호출되는 메서드입니다.

useSelfInvocation() 메서드 내부에서 호출되는 클래스 내 다른 메서드 child() 에는 @Transactional 어노테이션을 추가하였습니다.

unUseSelfInvocation() 메서드는 @Transactional 어노테이션이 추가되어 있고 child() 메서드에 @Transactional 어노테이션이 있어도 없어도 동일하게 동작합니다. (상위 메서드의 트랜잭션이 전파되기 때문에..)

 

 

테스트 코드에서는 트랜잭션 테스트가 어려워 다음과 같은 컨트롤러를 작성합니다.

@RestController
@RequestMapping("/self-invocation")
@RequiredArgsConstructor
public class SelfInvocationController {
    private final SelfInvocationService selfInvocationService;

    @GetMapping("/1")
    public OrderDto useSelfInvocation() {
        return selfInvocationService.useSelfInvocation();
    }

    @GetMapping("/2")
    public OrderDto unUseSelfInvocation() {
        return selfInvocationService.unUseSelfInvocation();
    }
}

 

먼저 자기 호출을 하고 있는 메서드인 useSelfInvocation()을 실행해보면?

insert 쿼리와 select 쿼리하나만 호출됩니다.

위에서 설명했던 것과 같이 child() 메서드의 @Transactional 은 실행되지 않았고 그로 인해 변경한 orderd의 name 은 "order2"로 변경되지 않았습니다.

 

이번엔 자기 호출을 하고 있지 않은 unUseSelfInvocation() 메서드를 실행해 보겠습니다.

정상적으로 "order2"로 업데이트되었습니다.

어라 근데 useSelfInvocation()를 실행했을 때는 insert 후 select를 하는데 왜 동일한 로직을 사용하는 unUseSelfInvocation() 메서드에서는 select 쿼리가 실행되지 않았을까요? 동일하게 JPA repository에서 findById() 메서드를 호출하는데 말이죠.

 

정답은 영속성 컨텍스트의 1차 캐시에 있습니다. 영속성 컨텍스트는 하나의 트랜잭션에서 유효하고 useSelfInvocation()는 트랜잭션으로 묶여있지 않습니다.

하지만 unUseSelfInvocation() 메서드는 하나의 트랜잭션으로 묶여있고 영속성 컨텍스트를 사용하기 때문에 DB 조회 이전에 영속성 컨텍스트에서 동일한 엔티티가 있는지 먼저 확인합니다.

따라서 해당 엔티티가 영속성 컨텍스트에 존재한다면 해당 엔티티를 가져오기 때문에 DB 조회가 일어나지 않습니다.

 

자 이제 왜 self invocation으로 인해 트랜잭션이 동작하지 않는지는 알아냈습니다.

어떻게 해야 해당 현상을 막을 수 있을까요?

 

결론

제 생각엔 내부에서 호출되는 메서드는 private으로 관리하고 public 메서드에만 @Transactional 어노테이션을 사용하는 게 좋을 것 같습니다. 그리고 @Transactional 어노테이션을 추가할 때는 항상 신중하게 선택해야 할 것 같습니다.

반응형