
실무에서 @Transactional 어노테이션을 사용하면서 겪은 문제이다. 다음과 같은 코드에서 문제가 발생했다.
public void update(){
_update(); // 경고 발생
// 비즈니스 로직
}
@Transactional
public void _update() {
// DB 접근 로직
}
IntelliJ의 플러그인으로 사용하고 있는 SonarLint에서 버그를 감지했는데, 메세지를 확인해 보면 다음과 같다.

확인해보면 실제 트랜잭션이 동작되지 않는다고 써있는데, 그 이유는 뭘까?
먼저 @Transactional은 스프링AOP 기반(프록시)으로 동작된다. 스프링 AOP는 부가기능(Advice)을 적용할 대상(Pointcut)에 대해 런타임에 프록시를 생성하고, 생성한 프록시를 빈으로 등록해서 사용하게 된다.

위 그림은 스프링 AOP에서 부가기능을 적용한 프록시를 생성하는 과정을 나타낸다. 먼저 생성된 객체가 BeanPostProcessor에 전달되면, 해당 객체를 부가기능을 적용한 프록시 객체로 변환 후 스프링 빈 저장소에 저장한다. 여기서 핵심은 부가기능을 적용한 메서드를 사용하기 위해서는 저장소에 등록된 프록시 객체를 사용해야한다.
참고:
@Transactional에 관련된 AOP 클래스는 다음과 같다.
Advisor:BeanFactoryTransactionAttributeSourceAdvisor
Advice:TransactionInterceptor
Pointcut:TransactionAttributeSourcePointcut
위 클래스에 의해서 코드 내의@Transactional가 존재하면 트랜잭션 적용 대상으로 인식하여, 프록시 객체를 생성한 뒤 빈 저장소에 등록한다.
public void update(){
_update(); // 경고 발생
// 비즈니스 로직
}
@Transactional
public void _update() {
// DB 접근 로직
}
그래서 위 코드에서 발생한 경고 메세지에 대한 원인은 다음과 같이 정리할 수 있다.
@Transactional이 있으므로, 트랜잭션이 적용되는 대상에 선정됨_update()를 클래스 내부에서 직접 호출하기 때문에 트랜잭션이 적용되지 않음간단한 예제 코드와 함께 여러 문제 해결 방법을 소개한다.
@Aspect
@Slf4j
public class SimpleTransactionAspect {
@Around("@annotation(simpleTransactional)")
public Object doTx(ProceedingJoinPoint joinPoint, SimpleTransactional simpleTransactional) throws Throwable {
log.info("[tx] start");
try {
Object result = joinPoint.proceed();
log.info("[tx] commit");
return result;
} catch (Throwable throwable) {
log.info("[tx] rollback");
throw e;
} finally {
log.info("[tx] end");
}
}
}
@Service
@Slf4j
public class MemberService {
public void save() {
_save();
// 트랜잭션이 끝나고 로직 수행
}
@SimpleTransactional
public void _save() {
log.info("[MemberService] save");
}
}
@SimpleTransactional이 존재하는 클래스에 대해 트랜잭션을 적용하는 Aspect를 생성하였다. MemberService의 save()메서드를 수행하는 테스트 코드를 작성하고 실행했다.
@SpringBootTest
@Import(SimpleTransactionAspect.class)
class MemberServiceTest {
@Autowired private MemberService memberService;
@Test
void save() {
memberService.save();
}
}

역시나 트랜잭션 적용대상임에도 불구하고 적용이 되어있지 않다.
Spring boot 2.6 이상인 경우에는 Bean간의 순환 참조는 금지되어 있기 때문에, 자기 자신을 주입하기 위해서는 지연 로딩을 같이 사용해야한다.
@Service
@Slf4j
public class MemberService {
private MemberService memberService;
@Autowired
@Lazy
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
public void save() {
memberService._save();
// 트랜잭션이 끝나고 로직 수행
}
@SimpleTransactional
public void _save() {
log.info("[MemberService] save");
}
}
MemberService에 자기자신을 Setter 주입받는다. @Lazy 를 통해 memberService를 사용하는 시점에 Setter 주입한다.

다시 한번 테스트 코드를 실행해보면, 정상적으로 트랜잭션 기능이 적용되어 있다.
@Component
@Slf4j
public class InternalMemberService {
@SimpleTransactional
public void _save() {
log.info("[MemberService] save");
}
}
@RequiredArgsConstructor
@Service
@Slf4j
public class MemberService {
private final InternalMemberService memberService;
public void save() {
memberService._save();
// 트랜잭션이 끝나고 로직 수행
}
}
클래스를 따로 만든 뒤에, MemberSerivce에서 해당 클래스를 주입받아서 사용하는 방법이다. 해당 방법도 마찬가지로 정상적으로 트랜잭션이 적용된다. 구조 변경하는 방법은 위 방법 말고도 여러가지가 있을 수 있다.