[Spring] - AOP(관점 지향 프로그래밍)의 이해

2026. 4. 28. 20:27·Spring

AOP(Aspect Oriented Programming)

💡 AOP란 관점 지향 프로그래밍이란 의미로 프록시를 사용하여 핵심 비즈니스 로직과 공통 부가 기능을 분리하는 것
     ↳ 핵심 로직(주문, 회원가입 등) / 부가 기능(시간 측정, 트랜잭션 관리 등)
🎈 OOP(객체 지향 프로그래밍) - 객체를 기준으로 기능을 모듈화했다면,
     AOP(관점 지향 프로그래밍) - 여러 객체에 공통적으로 적용되는 기능(횡단 관심사)을 기준으로 모듈화 한 것,
      ↳ 즉 여러 곳에 흩어져 있는 공통 코드를 한 곳으로 모아서 관리하는 방식

*횡단 관심사(Cross-cutting Concern) : 여러 클래스에서 반복적으로 나타나는 공통 기능(로깅, 트랜잭션 처리, 성능 측정, 보안 체크 등)

프록시(Proxy)

💡 클라이언트가 실제 객체를 직접 호출하지 않고, 중간에 있는 대리 객체(Proxy)를 통해 호출하는 방식
      ↳ 클라이언트 -> 프록시(대리인) -> 실제 객체
🎈 프록시는 실제 메서드 실행 전후에 개입 하여 공통 기능을 수행한다(로깅, 권한 검사,  시간 측정, 보안 체크 등)

 

스프링 AOP의 3요소

용어 정의 역할
포인트컷(Pointcut) 적용 위치(어디에?) 부가 기능을 적용할 대상 메서드를 선정
ex) hello.aop.order 패키지의 모든 메서드
어드바이스(Advice) 적용 로직(무엇을?) 실직적으로 수행할 부가 기능 코드
ex) 로그 출력, 실행 시간 측정 등
어드바이저(Advisor) 포인트컷 + 어드바이스 결합체 스프링 AO의 가장 기본적인 형태로, 스프링 빈 등록 필요
🎈 어드바이저를 스프링 빈으로 등록하면, 자동 프록시 생성기가 알아서 처리

자동 프록시 생성기 동작 방식

✔어드바이저 등록 -> 스프링 내부의 자동 프록시 생성기 동작

1. 조회 : 등록된 모드 어드바이저를 탐색
2. 검사 : 생성되는 모든 스프링 빈이 포인트컷 조건에 맞는지 확인
3. 교체 : 포인트컷이 매칭 되면, 원본 대신 프록시 객체 자동 생성 후 사용, 매칭 안되면 원본 객체 사용
4. 실행 : 사용자가 빈을 호출하면, 원본이 아닌 프록시 객체가 호출되어 공통 기능 먼저 수행

 

@Aspect - 어드바이저 생성

💡 @Aspect는 해당 클래스는 어드바이저라고 스프링에게 알려주는 역할, 애노테이션 기반 프록시 적용할 때 필요
       ↳ 컴포넌트 스캔이 되지 않으며, 스프링 빈으로 직접 등록 필요
@Aspect //✔ 어드바이저
public class AspectV1 {

    //✔ 포인트컷, hello.aop order 패키지와 그 하위 패키지의 모든 메서드에 적용
    @Around("execution(* hello.aop.order..*(..))") 
    //✔ 어드바이스, 실제로 실행할 공통 기능(로그 출력)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();	// ✔ 실제 메서드 실행(원래 메서드)
    }
}

 

포인트컷 분리

💡 포인트컷 분리란, 포인트컷 표현식을 별도의 메서드로 추출하여 이름을 붙여주는 것
      ↳ 프로젝트가 커지면 같은 범위에 여러 기능을 넣고 싶을 때 사용

 

@Slf4j
@Aspect
public class AspectV2 {

    //✔ pointcut signature, hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){}   

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

 

✔ @Pointcut에 포인트컷 표현식 사용
✔ 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라고 함
✔ 메서드 반환 타입은 void여야 한다.
✔ 코드 내용은 비워둔다(선언만 하는 용도)
✔ 내부에서만 사용하면 private, 다른 애스팩트에서 참고하려면 public
🎈 포인트컷 분리를 하면 하나의 포인트컷 표현식을 여러 어드바이스 함께 사용하기에 재사용할 수 있으며, 가독성이 좋아 의도를 파악하기 쉽습니다.

 

포인트컷 조합(Logic Operators)

💡 AOP에서 중요한 건 단순히 공통 기능을 넣는 것이 아닌, 어디에 적용할지(포인트컷) 언제 실행할지(어드바이스), 여러 개가 있을 대 어떤 순서로 실행될 지 이 세가지를 정확하게 제어하는 것이다.

트랜잭션(Transaction) 기능을 추가해보겠습니다. 핵심은 여러 개의 어드바이스가 어떤 기준으로, 어떤 순서로 동작하는가?를 이해하는 것입니다.

트랜잭션(Transaction) 기능 추가

💡 트랜잭션(Transaction)이란 하나의 작업을 전부 성공시키거나, 전부 실패시키는 것을 보장하는 기능
✔ 트랜잭션(Transaction) 시뮬레이션
1. 시작 : 핵심 로직 실행 직전
2. 커밋 : 로직이 성공적으로 끝나면 확정
3. 롤백 : 중간에 예외(Exception)가 터지면 취소
4. 릴리즈 : 성공/실패 상관 없이 자원 반납
🎈 트랜잭션은 "하나의 어드바이스"가 아니라 여러 어드바이스가 조합된 구조
@Slf4j
@Aspect
public class AspectV3 {

    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){}   //pointcut signature

    //클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    private void allService() {}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    //트랜잭션 처리
    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e ) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

}
💡 포인트컷은 자바 논리 연산자(&&, ||, !)와 동일하게 조합 가능, 논리 연산자를 통해 더 세밀한 타겟팅 가능
1. doLog() 실행 (로그 시작)
2. doTransaction() 실행 (트랜잭션 시작)
3. 실제 비즈니스 로직 (orderItem()) 실행
4. doTransaction() 종료 (트랜잭션 커밋 -> 리소스 릴리즈)
5. doLog() 종료 (로그 끝)

어드바이스의 실행 순서

💡 어드바이스는 기본적으로 순서를 보장하지 않습니다. 순서를 보장하고 싶으면, @Order는 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있습니다. 그래서 하나의 애스팩트에 여러 어드바이스가 있으면 순서를 보장받을 수 없습니다.
🎈이를 해결하기 위해서는 어드바이스를 별도의 클래스로 분리하고, 클래스 단위에 @Order 애노테이션을 사용합니다.
@Slf4j
public class AspectV5Order {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around("hello.aop.order.aop.PointCuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around("hello.aop.order.aop.PointCuts.orderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

            try {
                log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
                Object result = joinPoint.proceed();
                log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
                return result;
            } catch (Exception e ) {
                log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
                throw e;
            } finally {
                log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
            }
        }
    }
}

 

@Slf4j
@SpringBootTest
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }

}

포인트 컷 참조(공용 사용)

💡 포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 됨.
      ↳ 외부에서 호출할때는 포인트컷의 접근 제어자를 public으로 변경 필요
public class PointCuts {
    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){}   //pointcut signature

    //클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    public void allService() {}

    //allOrder && allService
    @Pointcut("allOrder() && allService()")
    public void orderAndService(){}
}
@Slf4j
@Aspect
public class AspectV4 {

    @Around("hello.aop.order.aop.PointCuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); 
        return joinPoint.proceed();
    }

    @Around("hello.aop.order.aop.PointCuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e ) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

}

조인 포인트(Join Point)와 프로시딩 조인 포인트(ProceedingJoinPoint)

조인 포인트(Join Point)

💡 조인 포인트(Join Point)란 어드바이스가 적용될 수 있는 모든 지점을 의미합니다.
     ↳ 스프링 AOP는 '프록시' 방식을 사용하기에, 메서드 호출 시점이 곧 조인 포인트이며 조인 포인트 파라미터 같은 객체에는 호출 정보가 담겨있습니다.

주요 기능

메서드 의미 예시
getArgs() 파라미터 정보 추출
(사용자가 메서드에 전달한 파라미터 값을 배열로 가져옴)
orderItem("itemA") 호출시 ["itemA]
getSignature() 호출된 메서드 정보 확인
(메서드의 상세 선언 정보(이름, 반환 타입, 파라미터 타입 등)
로그에 어떤 메서드가 실행중인지 확인
getTarget() 대상 객체 참조
(프록시가 아닌, 실제 비즈니스 로직을 수행하는 원본 객체 반환)
원본 클래스의 필드 값 참조 및 다른 메서드와의 연동이 필요할 때 사용

프록시는 포인트컷에 걸려 있는 메서드나, 인터페이스에 정의된 메서드만 대행, 원본의 멤버 변수 값을 직접 갖고 있지 않음.

프로시딩 조인 포인트(Proceeding Join Point)

💡JoinPoint의 모든 기능을 가지고 있으면서, 딱 한 개의 기능(proceed())이 추가된 자식 객체입니다.
🎈 proceed() : 다음 로직을 실행하라는 신호로 @Around에서만 사용되며 조인 포인트 실행 여부 결정이 가능합니다.
    ↳ @Around에만 사용되는 이유는 메서드 실행 전과 후를 모두 직접 제어해야 하기 때문이며, proceed()를 호출하지 않으면 실제 로직이 아예 실행되지 않게 막을 수 있습니다.

 

어드바이스 종류

💡 어드바이스는 프록시가 호출되는 시점에 따라 5가지로 나뉩니다.

 

종류 의미 특징
@Around
(전, 후 처리)
메서드 실행 전/후 + 예외 까지 모든 흐름을 제어하는 가장 강력한 메서드 - ProceedingJoinPoint 사용(유일)
-  proceed() 호출 시 실제 메서드 실행, 호출 안 하면 비즈니스 로직 실행 x
- 실행 여부 제어, 여러번 실행(재시도 가능)
*성능 측정, 트랜잭션, 공통 처리에 많이 사용
@Before
(사전 처리)
메서드 실행 직전에 동작 - 메서드 종료되면 자동으로 다음 타겟 호출
* 권한 체크, 입력 값 로그 기록 등 실행 전 검증 로직에 적합
@AfterReturning
(정상 종료 후 처리)
메서드가 정상적으로 실행된 경우만 실행 - returning 속성과 매개변수 일치해야 함.
- 지정한 타입 또는 그 부모 타입일때만 동작
- 반환 객체를 다른 객체로 교환 불가(내부 값 변경만 가능)
* 결과 로그 기록, 반환 데이터 가공에 적합
@AfterThrwoing
(예외 발생 처리)
메서드 실행 중 예외 발생했을 때만 실행 - throwing 속성에 지정된 이름과 매개변수명 일치해야 함
- 지정된 예외 타입 발생했을때만 동작
*예외 발생시 공통 로깅, 알림발송, 롤백 등에 적합
@ After
(항상 처리)
성공, 실패 상관 없이 메서드 종료 시 무조건 실행
(자바 finally와 유사)
*리소스 정리 및 DB 연결 해제에 적합
@Slf4j
@Aspect
public class AspectV6Advice {

    @Around("hello.aop.order.aop.PointCuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            //@Before
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            //@AfterReturning
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e ) {
            //@AFterThrowing
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            //@After
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

    @Before("hello.aop.order.aop.PointCuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    @AfterReturning(value = "hello.aop.order.aop.PointCuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return = {}", joinPoint.getSignature(), result);
    }

    @AfterThrowing(value = "hello.aop.order.aop.PointCuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint jpoinPoint, Exception ex) {
        log.info("[ex] {} message = {}", ex);
    }

    @After(value = "hello.aop.order.aop.PointCuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

 

'Spring' 카테고리의 다른 글

[Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃)  (0) 2026.05.13
[Spring Boot] 테스트의 이해와 @Transactional 동작 원리  (0) 2026.05.12
[Spring] - BCryptPasswordEncoder를 이용한 비밀번호 암호화와 보안 원리  (1) 2026.04.16
[Spring] 싱글톤 패턴과 싱글톤 컨테이너 정리  (0) 2026.03.23
[Spring] 스프링 프레임워크의 기초 이해(2) - 스프링 애플리케이션 구조와 스프링 컨테이너, 빈(Bean)  (0) 2026.03.15
'Spring' 카테고리의 다른 글
  • [Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃)
  • [Spring Boot] 테스트의 이해와 @Transactional 동작 원리
  • [Spring] - BCryptPasswordEncoder를 이용한 비밀번호 암호화와 보안 원리
  • [Spring] 싱글톤 패턴과 싱글톤 컨테이너 정리
mins0on
mins0on
비전공자의 백엔드 개발자 공부 기록 일지입니다.
  • mins0on
    꾸준함의 가치
    mins0on
  • 전체
    오늘
    어제
    • 분류 전체보기 (65) N
      • Java (7)
      • Spring (9)
      • DataBase (1)
      • Algorithm (1)
      • Network (6)
      • 운영체제 (2)
      • 코드 분석 (26)
      • Trouble Shooting (4) N
      • Project (1)
      • Migration (3)
      • 기타 (1)
      • 개념 정리 (3)
      • Coding Test (1)
        • Baekjoon (1)
  • hELLO· Designed By정상우.v4.10.6
mins0on
[Spring] - AOP(관점 지향 프로그래밍)의 이해
상단으로

티스토리툴바