@Transactional 알고 쓰자

onlyJoon·2024년 7월 23일

Transactional

목록 보기
1/2
post-thumbnail

Spring Framework를 사용하면서 자주 접하게 되는 어노테이션 중 하나가 @Transactional입니다.
주로 데이터베이스 트랜잭션을 관리하기 위해 사용되며, 이 어노테이션을 통해 선언된 메서드는 트랜잭션 경계를 형성합니다.

하지만, @Transactional 어노테이션을 사용하다 보면 내부 호출(internal method call)과 관련된 문제를 종종 마주하게 됩니다.
이번 글에서는 이러한 문제와 그 해결 방법에 대해 자세히 알아보겠습니다.

@Transactional 내부 호출 문제

Spring의 @Transactional 어노테이션은 AOP(Aspect-Oriented Programming) 기반으로 동작합니다.
이는 프록시(proxy)를 사용하여 트랜잭션 경계를 관리합니다.

하지만, 프록시 기반의 AOP는 자기 자신을 호출할 때 트랜잭션 경계가 제대로 적용되지 않는 문제가 발생할 수 있습니다.

문제 상황 예시

아래의 세 가지 상황을 통해 @Transactional 어노테이션을 사용할 때 발생할 수 있는 문제를 자세히 살펴보겠습니다.

  1. @Transactional이 없는 메서드가 @Transactional이 있는 메서드를 호출하는 경우
  2. @Transactional이 있는 메서드가 @Transactional이 없는 메서드를 호출하는 경우
  3. @Transactional이 있는 메서드가 @Transactional이 있는 메서드를 호출하는 경우

1. @Transactional이 없는 메서드가 @Transactional이 있는 메서드를 호출하는 경우

@Transactional이 있는 메서드가 호출될 때 프록시를 거쳐야 트랜잭션이 적용되는데,
동일 클래스 내에서의 호출은 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않습니다.
따라서, 이 경우에도 내부 메서드의 트랜잭션이 제대로 시작되지 않을 수 있습니다.

@Service
public class MyService {

    public void nonTransactionalMethod() {
        transactionalMethod();
    }

    @Transactional
    public void transactionalMethod() {
        // 데이터베이스 작업
    }
}

2. @Transactional이 있는 메서드가 @Transactional이 없는 메서드를 호출하는 경우

이 경우에도 큰 문제가 발생하지 않습니다. 트랜잭션은 호출한 메서드(@Transactional이 있는 메서드)에서 시작됩니다.

@Service
public class MyService {

    @Transactional
    public void transactionalMethod() {
        nonTransactionalMethod();
    }

    public void nonTransactionalMethod() {
        // 데이터베이스 작업
    }
}

transactionalMethodnonTransactionalMethod를 호출하면, 트랜잭션은 여전히 transactionalMethod에 의해 관리됩니다. nonTransactionalMethod트랜잭션 경계 내에서 실행됩니다.

3. @Transactional이 있는 메서드가 @Transactional이 있는 메서드를 호출하는 경우

이 경우에는 문제가 발생할 수 있습니다. 호출된 내부 메서드의 트랜잭션이 적용되지 않습니다.

@Service
public class MyService {

    @Transactional
    public void outerMethod() {
        innerMethod();
    }

    @Transactional
    public void innerMethod() {
        // 데이터베이스 작업
    }
}

outerMethod가 호출되면 트랜잭션이 시작됩니다.
하지만 outerMethod가 innerMethod를 호출할 때, innerMethod에 설정된 @Transactional의 새로운 트랜잭션 설정이 적용되지 않습니다.
결과적으로 innerMethod는 트랜잭션 경계 밖에서 실행됩니다.

문제의 원인

문제의 원인은 프록시 메커니즘에 있습니다.

Spring은 @Transactional이 붙은 메서드를 호출할 때 프록시를 통해 트랜잭션을 관리합니다.
하지만, 자기 자신을 호출할 때는 프록시를 통하지 않고 직접 메서드를 호출하게 됩니다.
이로 인해 innerMethod의 트랜잭션이 제대로 적용되지 않는 것입니다.

해결 방법

이러한 문제를 해결하기 위해 몇 가지 방법을 사용할 수 있습니다.

1. 자기 자신을 프록시로 주입하기

가장 간단한 해결 방법은 자기 자신을 프록시로 주입받아 사용하는 것입니다.

@Service
public class MyService {

    @Lazy
    @Autowired
    private MyService self;

    @Transactional
    public void outerMethod() {
        self.innerMethod();
    }

    @Transactional
    public void innerMethod() {
        // 데이터베이스 작업
    }
}

위와 같이 자기 자신을 주입받아 사용하면 프록시를 통해 메서드를 호출하게 되어 트랜잭션이 정상적으로 적용됩니다.
또한, @Lazy를 추가하여 순환 참조 문제를 방지하는 것이 좋습니다.

2. AOP를 활용한 자기 자신 호출 방지

AOP를 활용하여 자기 자신 호출을 방지하는 방법도 있습니다.
먼저 AOP 설정을 추가합니다.

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
@Aspect
@Component
public class SelfInvocationAspect {

    @Autowired
    private ApplicationContext context;

    @Around("execution(* com.example..*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object target = context.getBean(pjp.getTarget().getClass());
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        return method.invoke(target, pjp.getArgs());
    }
}

이렇게 설정하면 @Transactional이 붙은 메서드 호출 시 AOP가 적용되어 프록시를 통한 메서드 호출이 이루어집니다.
하지만 잘 사용되지는 않습니다.

3. 트랜잭션 관리의 위임

트랜잭션 관리 자체를 별도의 클래스에 위임하는 방법도 있습니다.
트랜잭션 경계를 나누어 관리하고 싶은 경우 유용한 방법입니다.

@Service
public class OuterService {

    @Autowired
    private InnerService innerService;

    @Transactional
    public void outerMethod() {
        innerService.innerMethod();
    }
}

@Service
public class InnerService {

    @Transactional
    public void innerMethod() {
        // 데이터베이스 작업
    }
}

이 방법은 트랜잭션 경계를 명확히 분리할 수 있어 코드의 가독성과 유지보수성도 높일 수 있습니다.
하지만 "트랜잭션 경계를 나누는 것이 필요한 경우에만 이 방법을 사용하는 것이 좋습니다."
이로 인해 불필요한 서비스 분리가 생겨 코드가 지나치게 복잡해질 수 있기 때문입니다.

정리

Spring에서 @Transactional을 사용할 때 내부 호출로 인해 발생하는 문제는 프록시 메커니즘에 기인합니다.
이러한 문제를 해결하기 위해
1. 자기 자신을 프록시로 주입받아 사용하거나,
2. AOP를 활용하여 호출 방지를 설정할 수 있으며,
3. 트랜잭션 관리를 별도의 클래스에 위임하는
방법들을 고려할 수 있습니다.

저는 주로 3번의 방법을 선택하는 편이며 @Transactional 메서드를 외부에서 호출해야 트랜잭션이 제대로 관리된다는 점을 기억해야 합니다.

profile
A smooth sea never made a skilled sailor

0개의 댓글