BackEnd/자바 스프링

[Java Spring] AOP : Aspect Oriented Programming

세모 2026. 3. 3. 19:57

AOP란?

관점 지향 프로그래밍으로 코드를 핵심 로직과 공통 기능으로 분리하는 방식

코드는 두가지의 관심사로 나뉘게된다.

  • 핵심 관심사
    • 실제 비지니스 로직 (회원가입, 주문처리 등)
  • 공통 관심사
    • 여러 곳에서 반복되는 부가 기능 (시간 측정, 로깅, 트랜잭션, 권한 체크 등등)

AOP가 필요한 이유

공통 기능을 비지니스 로직 내부에 심게되면 다음과 같은 문제가 발생한다

  • 핵심 로직과 공통 기능이 섞여 코드 가독성을 저하시킴
  • 공통 기능 수정 시 모든 메서드를 일일이 수정해야 함
  • 유지보수 비용 증가
  • 단순 분리와 AOP의 차이점
    • 단순 분리는 개발자가 직접 메소드를 호출 해야함
    • AOP는 핵심로직에 메소드를 넣지 않아도 자동으로 실행이 됌

 

 

 

동작 방식

AOP는 프록시 패턴으로 동작하는데 실제로 접근하는 로직이 아닌 프록시를 향해 접근하고 프록시에서 실제로직으로 보내주는 방식을 하게 됌

전체적으로 AOP를 걸었다는 전제로 대략적인 사용 방법은 아래와 같음

  1. 클라이언트의 요청을 받아 프록시 컨트롤러로 접근
  2. 공통 기능을 자동으로 실행시키고 실제 컨트롤러에 접근
  3. 실제 컨트롤러에서 프록시 서비스로직에 접근
  4. 공통 기능을 자동으로 실행 시키고 실제 서비스 로직에 접근
  5. 실제 서비스로직에서 프록시 레포지토리에 접근
  6. 공통 기능을 자동으로 실행 시키고 실제 레포지토리에 접근
  7. 실제 레포지토리에서 반환을 하면 프록시 레포지토리로 반환하여 값을 넘김
  8. 프록시 레포지토리에서 공통 기능 마무리 후 반환 값을 실제 서비스 로직으로 넘김
  9. 실제 서비스 로직에서 프록시 서비스 로직으로 반환 값을 넘김
  10. 프록시 서비스에서 공통 기능 마무리 후 반환 값을 실제 컨트롤러로 넘김
  11. 실제 컨트롤러 작업후 반환 값을 프록시 컨트롤러로 넘김
  12. 프록시 컨트롤러에서 공통 기능 마무리 후 반환 값을 클라이언트로 반환

AOP 동작 방식

사용하기 위해선 DI(의존성 주입)이 필요함

 

주요 어노테이션

@Aspect // AOP 클래스 선언
@Before // 메서드 실행전
@After // 메서드 실행 후 (예외 발생 여부와 관계없이 항상 실행됨)
@AfterReturning // 메서드 정상 반환 후 (예외 발생시 실행이 안되며, 반환값 접근 가능)
@AfterThrowing // 메서드 예외 발생 후 (정상 반환시 실행 안되며, 예외 객체 접근 가능)
@Around // 실행 전후 모두

 

실제 예시

@Aspect
@Component
public class TimeTraceAop {
    @Around("execution(* hello.hello_spring.service..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START : " + joinPoint.toString());
        try{
            return joinPoint.proceed();
        }finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END : " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

대표적인 AOP 활용 사례

  • 실행 시간 측정
  • 로깅
  • 트랜잭션 처리 (내부적으로 AOP 형식)
  • 권한체크

AOP가 적용되지 않는 사례

@Aspect
@Component
public class TimeTraceAop {
    @Around("execution(* hello.hello_spring.service..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START : " + joinPoint.toString());
        try{
            return joinPoint.proceed();
        }finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END : " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}
@Service
public class MemberService {
    
    public void register() {
        System.out.println("회원가입");
        this.sendWelcomeEmail();
    }
    
    public void sendWelcomeEmail() {
        System.out.println("이메일 발송");
    }
}

이와 같이 사용한다고 예시를 들면 register내부에서 사용되는 sendWelcomeEmail()은 시간을 알수 없다.

프록시는 Spring 빈으로 등록된 class에 대해 생성이 되며, 실제 class 내부 메서드로 이동하기 때문에 this가 가리키는 객체는 실제 객체가 되어 AOP를 적용할 수 없는 것이다.

흐름을 보면 다음과 같다.

  1. 외부 호출
  2. MemberService 프록시 <- Spring이 관리하는 프록시
    • 이때 AOP가 실행됨
  3. 실제 MemberService.register()
  4. this.sendWelcomeEmail() <- 여기서 나온 this는 원본 객체임
    • 따라서 AOP가 적용되지 않음
  5. 실제 MemberService.sendWelcomeEmail()

Spring AOP는 프록시를 통해 들어오는 호출에만 동작한다. 따라서 해결 방안은 다음과 같다.

  • ApplicationContext로 프록시 객체를 직접 꺼내서 호출하는 방법
  • AopContext.currentProxy()로 프록시 객체를 직접 꺼내서 호출하는 방법
  • sendWelcomeEmail()을 별도 class로 분리해서 DI를 통해 호출하기
    • 위 두가지의 경우 관심사 분리가 어려워 이방식을 주로 사용한다.

이런 경우는 AOP를 사용하는 트랜잭션에서도 흔히 일어나는 방식으로 의도하지 않는 경우가 발생하게 된다.

'BackEnd > 자바 스프링' 카테고리의 다른 글

[Java Spring] Transaction  (0) 2026.03.22
[Java Spring] 웹 서버 구조와 IoC/DI 핵심 개념  (0) 2026.03.18
[Java Spring] 객체 지향 설계와 스프링  (0) 2026.03.04
[JAVA] 중급 정리  (0) 2026.02.26
[JAVA] 입문 정리  (0) 2026.02.21