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 |