AOP (Aspect Oriented Programming) - 관점지향 프로그램

횡단 관심사를 모듈화하여 여러 객체나 계층에 걸쳐 적용할 수 있는 프로그래밍 방식이다.

코드의 중복을 줄이고, 유지 보수성을 높일 수 있다.

- Web Layer : REST API를 제공, Client 중심의 로직 적용
- Business Layer : 비즈니스 로직 처리, 트랜잭션 관리
- Data Layer: 데이터베이스와의 통신, 데이터 저장 및 조회.

 

어노테이션  설명
@Aspect 해당 클래스가 Aspect임을 명시합니다.
@Pointcut Advice가 적용될 메서드의 범위를 지정합니다.
@Before 대상 메서드 실행 전에 Advice를 실행합니다.
@AfterReturning 대상 메서드가 정상적으로 실행된 후에 Advice를 실행합니다.
@AfterThrowing 대상 메서드에서 예외가 발생했을 때 Advice를 실행합니다.
@After 대상 메서드 실행 후에 Advice를 실행합니다.
@Around 대상 메서드 실행 전, 후 또는 예외 발생 시에 Advice를 실행합니다.

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop' // 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Gradle에서 aop dependency를 추가한다.

 

package com.example.aop.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect // AOP 적용
@Component // 컴포넌트로써 스프링에서 관리를 위해 빈으로 등록
public class ParameterAop {

    //aop.controller 패키지안의 메소드 전체
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}

    @Before("cut()")
    public void before(JoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName());

        Object[] args = joinPoint.getArgs();
        for(Object obj : args){
            System.out.println("type : " + obj.getClass().getSimpleName() );
            System.out.println("value : " + obj );
        }
    }

		//returning 응답으로 반환되는 객체
    @AfterReturning(value = "cut()", returning = "returnObj" )
    public void afterReturn(JoinPoint joinPoint,Object returnObj){
        System.out.println("return obj");
        System.out.println(returnObj);
    }

}

// JoinPoint 들어가는 지점에 대한 정보들을 가지고 있는 객체

1. @Pointcut 메소드를 생성하여 AOP 적용할 범위 설정
2. @Before 설정을 통해 Pointcut 메소드가 실행 되기 전에 들어온 값(인자) 확인
3. @AfterReturning설정을 통해 Pointcut 메소드가 정상 실행 후 반환되는 값(Object) 확인

이와 같은 방법으로 로깅과 같은 기능을 서비스 로직과 코드를 분리할 수 있다.

 

package com.example.aop.annotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD}) // 이 어노테이션을 클래스, 인터페이스 등에 적용, 메소드에 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임 시에 어노테이션 정보를 조회할 수 있음
public @interface Timer {
}

@Target : 어노테이션을 적용할 요소를 정의 한다. 
@Retention : 어노테이션의 유지 정책을 설정

위와 같이 커스텀 어노테이션 정의를 할 수있다.

 

예제 1 : 실행시간 측정

package com.example.aop.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

// @Bean 은 클래스에 사용할수 없고, 메소드에 가능하다.
// @Component 는 클래스 단위로 빈을 등록 @Configuration 은 하나의 클래스에 여러 빈을 등록 할수 있다.
@Aspect
@Component
public class TimerAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}


    @Pointcut("@annotation(com.example.aop.annotaion.Timer)")
    private void enableTimer(){}


    @Around("cut() && enableTimer()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 메소드 실행 후 return 값이 있으면 반환
        Object result = joinPoint.proceed();

        stopWatch.stop();

        System.out.println("total time: " + stopWatch.getTotalTimeSeconds());
    }

}

&& 연산자를 이용하여 Pointcut 조건을 여러 개 설정 할 수 있다. @Around를 활용하여 aop 설정이 적용한 메소드에 한하여 joinPoint.proceed(실제 메소드 실행) 코드 전과 후에 일괄적으로 특정한 처리를 추가 할 수 있다. 위의 예시는 해당하는 메소드의 실행 시간을 측정한 것이다.

 

예제 2 : 디코딩

package com.example.aop.annotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {
}

 

package com.example.aop.aop;

import com.example.aop.dto.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Aspect
@Component
public class DecodeAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}


    @Pointcut("@annotation(com.example.aop.annotaion.Decode)")
    private void enableDecode(){}


    @Before("cut() && enableDecode()")
    public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
        Object[] args = joinPoint.getArgs();

        for(Object arg : args){
            if(arg instanceof User){
                User user = User.class.cast(arg);
                String base64Email = user.getEmail();
                String email = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
                user.setEmail(email);
            }

        }
    }

    @AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
    public void afterReturning(JoinPoint joinPoint, Object returnObj){
        if(returnObj instanceof User){
            User user = User.class.cast(returnObj);
            String email = user.getEmail();
            String base64Email = new String(Base64.getEncoder().encode(email.getBytes(StandardCharsets.UTF_8)));
            user.setEmail(base64Email);
        }
    }

}

클라이언트 측에서 암호화 된 요청을 서버에서 복호화 할 때 인터셉터나 필터 단계에서는 변환하려고 하면 톰캣 자체에서 Body를 한번 읽으면 더 이상 읽을 수 없도록 막아놨기 때문에 작업이 어렵다.

하지만 AOP 같은 구간에서는 이미 필터와 인터셉터를 지나서 값 자체가 객체화 되었기 때문에 그 값을 변환 해주거나 특정한 객체를 넣어 줄 수도 있다.

'Spring' 카테고리의 다른 글

Validation  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24
IoC와 DI  (0) 2025.03.19
ObjectMapper  (0) 2025.03.18
Response 내려주기  (0) 2025.03.18

+ Recent posts