백엔드

[자바/JAVA] 자바 스프링 AOP(Aspect-Oriented Programming) 이해하기

Newbie Developer 2025. 2. 8. 15:50

안녕하세요. 새내기 개발자입니다. 공부하면서 정리하는 글로 틀린 부분은 언제나 댓글로 환영입니다!

1. AOP란?

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 공통 관심 사항(cross-cutting concerns) 을 분리하여 모듈화하는 프로그래밍 기법입니다. 스프링 AOP는 주로 로그, 트랜잭션, 보안, 예외 처리 같은 공통적인 기능을 분리하여 관리할 때 사용됩니다.

기존의 객체지향 프로그래밍(OOP)에서는 관심사(비즈니스 로직)공통 기능(로깅, 보안 등) 이 뒤섞이기 쉬운데, AOP를 사용하면 핵심 로직과 공통 기능을 분리할 수 있습니다.


2. AOP 핵심 개념

🎯 Aspect(애스펙트)

  • 공통 기능을 정의하는 모듈입니다.
  • 예를 들어, 로깅 기능을 @Aspect로 정의할 수 있습니다.

🎯 Advice(어드바이스)

  • 언제(어떤 시점)에 공통 기능을 실행할지 정의하는 로직입니다.
  • 종류
    • @Before : 메서드 실행 전에 동작
    • @After : 메서드 실행 후에 동작
    • @AfterReturning : 메서드가 정상적으로 종료된 후 동작
    • @AfterThrowing : 예외가 발생했을 때 동작
    • @Around : 메서드 실행 전후로 동작

🎯 JoinPoint(조인포인트)

  • 어드바이스가 적용될 수 있는 지점입니다.
  • 메서드 호출, 객체 생성 등 다양한 지점이 가능하지만, 스프링 AOP는 메서드 호출에 대해서만 지원합니다.

🎯 Pointcut(포인트컷)

  • 어드바이스를 적용할 대상(메서드)을 필터링하는 표현식 입니다.
  • execution(), within(), bean() 같은 표현식을 사용하여 특정 메서드나 클래스만 지정할 수 있습니다.

🎯 Weaving(위빙)

  • 애스펙트(Aspect)를 대상 객체에 적용하는 과정입니다.
  • 스프링 AOP는 런타임 시 프록시(Proxy)를 사용하여 위빙합니다.

3. 스프링 AOP 실습 예제

🔹 AOP 의존성 추가 (Spring Boot 프로젝트 기준)

Spring Boot에서는 spring-boot-starter-aop를 추가하면 쉽게 사용할 수 있습니다.

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

 

🔹 로그를 남기는 AOP 구현하기

다음과 같이 컨트롤러의 메서드 실행 전후로 로그를 출력하는 AOP 를 만들어 보겠습니다.

1️⃣ AOP 설정 클래스 만들기

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("execution(* com.example.service.*.*(..))") // 특정 패키지의 모든 메서드 적용
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 실제 메서드 실행
        long end = System.currentTimeMillis();

        logger.info("[LOG] {} 실행시간: {}ms", joinPoint.getSignature(), (end - start));
        return result;
    }
}

✅ @Around("execution(* com.example.service.*.*(..))")

  • com.example.service 패키지 내의 모든 메서드에 적용
  • 실행 전후로 시간을 측정하고 로그를 남김

2️⃣ AOP가 적용될 서비스 클래스

import org.springframework.stereotype.Service;

@Service
public class SampleService {
    public void doSomething() {
        System.out.println("비즈니스 로직 실행 중...");
    }
}

3️⃣ 실행 테스트

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class AopApplication implements CommandLineRunner {

    private final SampleService sampleService;

    public AopApplication(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    public static void main(String[] args) {
        SpringApplication.run(AopApplication.class, args);
    }

    @Override
    public void run(String... args) {
        sampleService.doSomething();
    }
}

🔹 실행 결과 (로그)

비즈니스 로직 실행 중...
[LOG] void com.example.service.SampleService.doSomething() 실행시간: 2ms
 

4. 다양한 AOP 예제

1️⃣ 특정 어노테이션이 붙은 메서드에만 AOP 적용하기

@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object logTransactionMethods(ProceedingJoinPoint joinPoint) throws Throwable {
    logger.info("트랜잭션 시작: {}", joinPoint.getSignature());
    Object result = joinPoint.proceed();
    logger.info("트랜잭션 종료: {}", joinPoint.getSignature());
    return result;
}

✅ @Transactional이 붙은 메서드만 감싸서 로그를 출력합니다.

2️⃣ 메서드 실행 전/후에 다른 로직 실행하기

@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
    logger.info("메서드 실행 전: {}", joinPoint.getSignature());
}

@After("execution(* com.example.service.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
    logger.info("메서드 실행 후: {}", joinPoint.getSignature());
}

✅ 메서드 실행 전과 후에 각각 로그를 출력하는 방식입니다.

3️⃣ 예외가 발생한 경우에만 AOP 실행하기

@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, Throwable ex) {
    logger.error("예외 발생: {} - {}", joinPoint.getSignature(), ex.getMessage());
}

✅ 특정 메서드에서 예외가 발생했을 때만 로그를 남깁니다.


5. AOP의 장점과 단점

장점

  1. 중복 코드 제거 : 로깅, 보안, 트랜잭션 관리 같은 공통 관심사를 한 곳에서 관리 가능
  2. 유지보수성 증가 : 핵심 비즈니스 로직과 부가적인 기능을 분리하여 코드 가독성 향상
  3. 기능 확장 용이 : 새로운 공통 기능을 추가할 때 기존 코드 수정 없이 적용 가능

단점

  1. 디버깅 어려움 : AOP가 적용된 코드는 프록시를 사용하기 때문에 디버깅이 까다로울 수 있음
  2. 잘못된 포인트컷 설정 위험 : 부적절한 포인트컷 설정으로 원치 않는 코드가 실행될 수 있음
  3. 프록시 오버헤드 : 런타임 프록시를 사용하므로 성능 저하 가능성 존재

6. 결론

스프링 AOP는 공통 관심 사항을 분리하여 유지보수를 용이하게 하고, 코드의 가독성을 높이는 강력한 기능 입니다. 특히 로깅, 보안, 트랜잭션 관리 같은 기능에 유용하게 활용할 수 있습니다.

AOP를 적절히 활용하면 중복 코드를 줄이고 유지보수성을 높일 수 있지만, 과도한 사용은 코드의 복잡도를 증가시킬 수 있으므로 적절한 적용이 필요합니다. 🚀