스프링을 공부하면 초반에 발생하는 의문 중 하나는, 어떻게 @Transactional 하나로 DB 데이터를 조작할 수 있는 것인가? 일 것이다.
이는 스프링의 AOP에 대한 개념으로 이어진다.
Aspect Oriented Programming이라고 해서 관점 지향 프로그래밍이다. 어떤 로직을 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화한다는 뜻인데,
쉽게 말하면, 프로그램의 핵심 로직과는 별개로 여러 곳에서 반복되는 공통 기능을 따로 분리해서 관리하는 방식이다.
예를 들어 애플리케이션을 만들다 보면 다음과 같은 코드가 여러 곳에 반복된다.
이런 기능들은 중요하긴 하지만, 비즈니스 핵심 로직 자체는 아니다. 예를 들어, 주문 서비스의 핵심은 주문을 생성하거나 결제를 처리하는 것이지, 매번 로그를 출력하는 것이 본질은 아니다. 그런데 이런 부가 기능이 서비스 메서드마다 직접 들어가기 시작하면 코드가 금방 지저분해진다.
따라서 이런 로직을 따로 묶어놓고 가져다가 쓰는게 AOP이다.
AOP를 찾아보면 적용되는 용어들이 있는데 각 용어들은 찾아보면 자세히 나온다.
(Aspect / Advice / Join Point / Pointcut / Weaving)
중요한 포인트는 Spring AOP는 프록시로 동작한다. 즉, 스프링은 원본 객체를 직접 바꾸는 것이 아니라, 그 앞에 프록시 객체를 하나 세워서 메서드 호출을 가로챈다.
흐름: 클라이언트 -> 프록시 -> 실제 객체
대표적인 것이 @Transactional 인데, 개발자가 직접 DB 커넥션을 생성하고 데이터 변경에 대한 롤백 및 커밋을 하지 않아도 되는 이유가 @Transactional에 의해 메서드 자체에 내장된 기능 때문이 아니라, 프록시가 그 메서드 호출 전후를 감싸면서 트랜잭션 로직을 수행하는 AOP 기능 때문인 것이다.
하지만, 프록시를 사용하기 때문에 발생하는 단점이 존재한다.
자기 자신 내부 호출 문제(self-invocation)인데,
@Service
public class MyService {
public void outer() {
inner();
}
@Transactional
public void inner() {
// DB 작업
}
}
위와 같은 코드에서 outer()를 호출하면 inner()에는 트랜잭션이 걸리지 않는다.
흐름: 클라이언트 -> 프록시 -> outer() -> 실제 객체의 inner()
스프링은 @Transactional이 있는 메소드를 통해 해당 서비스 클래스를 프록시 객체로 생성하지만, 해당 프록시 객체를 직접적으로 통해야만 AOP가 적용되기 때문에 outer() 메소드는 프록시를 거치지만 inner() 메소드는 실제 객체를 거치기 때문에 트랜잭션 로직이 적용되지 않을 수 있다.
그 외에도 스프링 AOP는 메서드 실행 중심으로 동작하고 등록된 빈에만 주로 적용되기 때문에 한계가 존재한다.
이러한 문제점을 해결하기 위해서 필요한 더 강력한 기능이 AspectJ라는 것이다.
AOP를 위한 별도의 기술로써 Spring AOP의 프록시 방식과 달리 바이트코드 수준에서 적용하여 코드 자체에 부가 기능을 엮어 넣는 방식에 가깝다. 메소드, 생성자 등 더 넓게 적용 가능하고 빈으로 등록되어 있지 않아도 적용 가능한 기술이다. 그 만큼 더 강력하고 복잡하다고 한다.