Project/Spring

Spring AOP

east-wind 2026. 5. 11. 17:23

들어가며

Spring을 공부하다 보면 @Transactional이나 @Cacheable을 아무 생각 없이 붙여쓰게 된다. 그런데 이게 대체 어떻게 동작하는 걸까? 내가 호출한 메서드에 트랜잭션 처리 코드를 직접 작성한 적이 없는데.

그 비밀이 바로 AOP(Aspect Oriented Programming)다. 오늘은 AOP가 왜 필요한지부터, 실제로 내부에서 어떻게 동작하는지까지 정리해본다.

문제 상황 — 반복되는 코드가 나를 괴롭힌다

메서드를 하나 작성한다고 생각해보자.

public void insert(Buddy buddy) {
    log.debug("buddy: " + buddy);             // 로깅
    long start = System.currentTimeMillis();   // 성능 측정 시작
    con.setAutoCommit(false);                 // 트랜잭션 시작
    try {
        repo.insert(con, buddy);              // ← 핵심 로직 (이것만 바뀜)
        con.commit();
    } catch (Exception e) {
        con.rollback();
    } finally {
        con.close();
    }
    log.debug("소요시간: {}", System.currentTimeMillis() - start);
}

로직의 95%가 로깅, 트랜잭션, 성능 측정이다. 비즈니스 로직은 딱 한 줄. 그리고 이 패턴이 delete(), update()에도 똑같이 반복된다.

이런 코드를 횡단 관심사(Cross-cutting Concern)라고 한다. 여러 메서드를 가로질러 반복되는 부가 기능이다. 로깅, 트랜잭션, 보안, 성능 측정이 대표적이다.

AOP는 이 횡단 관심사를 핵심 로직에서 분리해서, 비즈니스 메서드에는 핵심 로직만 남기는 패러다임이다.

핵심 용어 정리

처음엔 용어가 헷갈린다. 딱 이렇게 기억하면 된다.

용어한 줄 설명
TargetAOP가 적용될 진짜 객체 (비즈니스 로직 보유)
Aspect횡단 관심사를 모듈화한 클래스 (해결책)
AdviceAspect 안에서 실행될 실제 부가 로직 코드
PointcutAdvice를 어느 메서드에 적용할지 결정하는 필터
WeavingAspect를 Target에 적용하는 행위 자체
헷갈리기 쉬운 것: "횡단 관심사 = Aspect"가 아니다.
횡단 관심사는 문제고, Aspect는 그 문제를 모듈화해서 담은 해결책이다.

Spring AOP의 동작 원리 — Proxy

Spring AOP는 Proxy 기반으로 동작한다. Proxy는 대리인이다. Target 앞에 세워두는 대리 객체로, 호출자는 Proxy인지 모르고 그냥 호출하지만 Proxy가 중간에서 Advice를 실행한 뒤 Target을 부른다.

// 호출자는 그냥 mDao를 부르는 것 같지만
mDao.selectDetail(email);

// 실제로는 이렇게 동작한다
// 1. Proxy가 받아서 @Before Advice 실행
// 2. 진짜 selectDetail() 호출
// 3. @After Advice 실행
// mDao의 실제 타입: BasicMemberDao$$SpringCGLIB$$0

Spring은 @Aspect가 붙은 클래스를 감지하면, Pointcut에 해당하는 Target 빈 대신 CGLib이 생성한 Proxy 빈을 컨테이너에 등록한다. 그래서 개발자가 명시적으로 부가 로직을 호출하지 않아도 자동으로 끼어드는 것이다.

Aspect 작성 방법

의존성부터 추가한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

그리고 Aspect 클래스를 작성한다.

@Component  // 빈 등록
@Aspect     // AOP Aspect 선언
@Slf4j
public class LoggingAspect {

    @Before("execution(* com.ssafy..dao.*.*(..))")
    public void loggingDao(JoinPoint jp) {
        log.debug("method: {}, args: {}", jp.getSignature(), Arrays.toString(jp.getArgs()));
    }
}

execution(* com.ssafy..dao.*.*(..))는 Pointcut 표현식이다. 순서대로 읽으면 "리턴타입 무관 / com.ssafy 하위 dao 패키지 / 모든 클래스 / 모든 메서드 / 모든 파라미터"다.

Advice 5가지 타입

언제 실행되느냐에 따라 5종류가 있다.

Advice실행 시점특징
@Before호출 전파라미터 객체 속성 변경 가능
@AfterReturning정상 반환 후리턴 값 객체 속성 변경 가능
@AfterThrowing예외 발생 후예외 참조만 전달 (catch 불가)
@After항상 (finally)성공·예외 무관하게 실행
@Around전후 전체파라미터·리턴값 완전 대체, Target 실행 제어

특히 @Around는 가장 강력하다. proceed()를 호출해야 Target이 실행되므로, 호출하지 않으면 Target을 막을 수도 있다.

@Around("execution(* com.ssafy.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();  // Target 실행
    log.debug("소요시간: {}ms", System.currentTimeMillis() - start);
    return result;
}

@Around의 대표 활용 사례:

  • 성능 측정 — proceed() 전후 시간 측정
  • 캐싱 (@Cacheable) — 캐시 히트 시 proceed() 없이 바로 반환
  • 트랜잭션 (@Transactional) — proceed() 전후로 begin/commit/rollback
  • 재시도(Retry) — 예외 시 proceed() N번 재호출
  • 인증/권한 — 조건 불충족 시 proceed() 차단

정리

AOP를 한 줄로 요약하면: "비즈니스 메서드는 핵심 로직만, 부가 기능은 Aspect에게"다.

평소에 그냥 붙여쓰던 @Transactional이 사실 @Around Advice로 구현된 AOP라는 걸 알고 나면, 이 애너테이션이 어떤 시점에 어떻게 동작하는지 더 명확하게 보인다. Spring이 마법처럼 처리해주는 것들 대부분은 AOP Proxy가 조용히 중간에 끼어들어 하는 일이다.

참고

  • 삼성청년 SW·AI아카데미 Framework 강의 — AOP 실습 자료
  • Spring 공식 문서 — Aspect Oriented Programming with Spring