[Spring] AOP(Aspect Oriented Programming)

chanbyeong·2025년 4월 2일

Spring

목록 보기
1/2

개념

Spring AOP는 스프링 프레임워크에서 제공하는 모듈로, 애플리케이션의 공통 관심사(ex: 로깅, 보안, 트랜잭션 관리 등)를 핵심 비즈니스 로직과 분리하여 관리할 수 있게 해준다. 이를 통해 코드 중복을 줄이고 유지보수를 용이하게 만들 수 있다.

  • Aspect: 공통 관심사를 모듈화한 단위, 보통 @Aspect 어노테이션을 사용해 정의하며, 여러 Adivce를 포함할 수 있다.
  • Join Point: Advice가 적용될 있는 실행 지점이다. Spring AOP에서는 주로 메서드 실행 시점을 의미한다.
  • Pointcut: 특정 Join Point를 선택하는 표현식
    • Execution: 메서드 이름, 반환 값, 파라미터로 지정(가장 일반적임)
    • Annotation 방식: 특정 애너테이션 붙은 메서드나 클래스 지정
    • Within 방식: 특정 클래스나 패키지 단위로 지정
    • Args 방식: 메서드 인자 타입을 기준으로 지정
    • Bean 방식: 스프링 빈 이름으로 지정
  • Advice: 실제로 공통 기능을 수행하는 코드, 실행 시점에 따라 다음과 같이 분류된다.
    • Before: 메서드 실행 전에 수행
    • After: 메서드 실행 후에 수행
    • Around: 메서드 실행 전후에 모두 수행
    • AfterReturning: 메서드가 정상적으로 종료된 후 수행
    • AfterThrowing: 메서드 실행 도중 예외가 발생했을 때 수행
  • Weaving: Aspect를 실제 객체에 적용하는 과정을 말한다. Spring Aop는 런타임에 프록시에 생성하여 이 작업을 수행한다.

문제 상황

관심사가 섞여 있는 코드

public void registerUser(String username, String email) {
    // 로깅
    long start = System.currentTimeMillis();
    log.info("사용자 등록 시작 - username: {}", username);
    // registerUser 에서 유저 입력 유효성 체크를 담당하는 파트를 담는다
    if (username == null || email == null) {
        throw new IllegalArgumentException("입력값이 비어 있습니다.");
    }
    // 이메일 검증
    if (!email.endsWith("@example.com")) {
        throw new SecurityException("허용되지 않은 이메일 도메인입니다.");
    }
    // 핵심 로직
    log.info("DB에 사용자 저장 로직 실행");
    // 로깅
    long end = System.currentTimeMillis();
    log.info("사용자 등록 완료 - username: {} 걸린 시간: {}", username, end - start);
}
  • 핵심기능은 사용자 등록이지만 로깅 로직과, 입력 유효성 검사/이메일 체크가 하나의 메서드 안에 모여있다.

기능 단위로 메서드를 분리한다면

public void registerUser(String username, String email) {
    // 로깅
    long start = System.currentTimeMillis();

    logStartExecutionTime(username);
    // registerUser 에서 유저 입력 유효성 체크를 담당하는 파트를 담는다
    validateUserInput(username, email);
    // 핵심 로직
    log.info("DB에 사용자 저장 로직 실행");
    // 로깅
    long end = System.currentTimeMillis();

    logEndExecutionTime(username, end-start);
}

private void logStartExecutionTime(String username) {
    log.info("사용자 등록 시작 - username: {}", username);
}

private void logEndExecutionTime(String username, long executionTime) {
	log.info("사용자 등록 완료 - username: {} {}", username, executionTime);
}

private void validateUserInput(String username, String email) {
	// 유효성 검사
	if (username == null || email == null) {
		throw new IllegalArgumentException("입력값이 비어 있습니다.");
	}
	// 이메일 검증
	if (!email.endsWith("@example.com")) {
		throw new SecurityException("허용되지 않은 이메일 도메인입니다.");
	}
}
  • 로깅과 입력 검증을 별도의 메서드로 분리해서 나름 관심사가 분리된것으로 보인다.
  • 하지만 여전히 registerUser메서드 내에서 이 부수 기능들을 직접 호출하고 있다.
  • 이는 코드의 응집도를 떨어뜨리고, 공통 관심사의 분산과 중복 발생 문제를 완전히 해결하지 못한다.
  • 또한 로깅이나 사용자 검증 같은 경우 다른 클래스에서도 사용해야 한다면 그 클래스에서 다시 이 메서드들을 정의해줘야 할 것이다.

AOP의 적용 및 해결

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
  • @EnableAspectJAutoProxy를 사용하여 AspectJ 기반 AOP 프록시를 활성화하여, 애스팩트를 자동으로 적용할 수 있게 해준다.
@Aspect
@Component
public class LogExecutionTimeAspect {
    private static final Logger log = LoggerFactory.getLogger("LogExecutionTimeAspect");
    @Around("@annotation(com.inspire12.likelionbackend.module.core.aop.aspect.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String username = (args.length > 0 && args[0] instanceof String) ?  (String) args[0]: "unknown";
        log.info("사용자 등록 시작 - username: {}", username);
        long startTime = System.currentTimeMillis();
        Object proceed = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        log.info("사용자 등록 완료 - username: {} {}", username, endTime - startTime);
        return proceed;
    }
}

@Aspect
@Component
public class UserInputValidateAspect {

    @Pointcut("@annotation(com.inspire12.likelionbackend.module.core.aop.aspect.UserInputValidate)")
    public void userInputValidatePointcut() {}

    @Around("userInputValidatePointcut()")
    public Object userInputValidate(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String username = args[0].toString();
        String email = args[1].toString();

        if (username == null || email == null) {
            throw new IllegalArgumentException("입력값이 비어 있습니다.");
        }
        // 이메일 검증
        if (!email.endsWith("@example.com")) {
            throw new SecurityException("허용되지 않은 이메일 도메인입니다.");
        }


        return joinPoint.proceed();
    }
}
  • @Aspect 어노테이션을 사용하여 AOP 기능을 담당하는 클래스임을 명시한다.
  • @Aruound 안의 값을 통해 특정 어노테이션을 지정할 수 있다.
    • 내부에 특정 어노테이션을 지정할 수 있으며, 이 어노테이션이 실제 메서드에 적용되면 해당 메서드가 Pointcut 역할을 하여 AOP 로직이 동작하게 된다.
  • joinPoint.proceed()은 AOP 로직 실행 중 실제 메서드 호출 시점을 결정할 수 있도록 해준다.
  • joinPoint.getArgs()은 메서드 호출 시 전달된 인자들을 가져오는 역할을 한다.

결과

@LogExecutionTime
@UserInputValidate
public void registerUser(String username, String email) { // 변경됨
	log.info("DB에 사용자 저장 로직 실행");
}
  • 두 개의 AOP 관련 어노테이션을 적용함으로써, 메서드 호출 시 자동으로 로깅과 입력 검증 기능이 실행된다.
  • 이로 인해 registerUser 메서드 내에서는 핵심 로직만 남아, 공통 관심사가 완전히 분리된다.
  • 결과적으로 코드의 중복이 제거되고, 유지보수가 용이해진다.

0개의 댓글