AOP 관점지향프로그래밍
- AOP: Aspect Oriented Programming
- AOP는 관점 지향 프로그래밍으로, 메소드에 동일한 로직이 반복적으로 적용되는 핵심 로직과는 별개의 공통 관심 사항(Cross-Cutting Concern)을 분리하여 모듈화하는 방법이다.
- Core Concern (핵심 관심 사항): 비즈니스 로직 그 자체 (예: 결제 처리, 회원 가입 등)
- Cross-Cutting Concern (공통 관심 사항): 여러 로직에 걸쳐 공통으로 적용되는 기능 (예: 로그, 보안, 트랜잭션 등)
- OOP(객체지향 프로그래밍)에서도 동일한 로직을 도입할 수 있지만, 상속이나 의존성 관리가 복잡해질 수 있수 있기에 이를 간편히 관리하는 방식
- AOP는 관점 지향 프로그래밍으로, 메소드에 동일한 로직이 반복적으로 적용되는 핵심 로직과는 별개의 공통 관심 사항(Cross-Cutting Concern)을 분리하여 모듈화하는 방법이다.
AOP 개념
용어 | 설명 |
Aspect | 공통 관심 사항을 모듈화한 클래스. 예: 로깅 Aspect, 트랜잭션 Aspect 등 |
Advice | 실제로 수행될 AOP 기능 코드로 메소드. (ex: 메서드 실행 전 로그 출력), 메소드의 동작을 "조언(advising)"하는 역할 |
Join Point | Advice를 적용할 수 있는 지점 (ex: 메서드 호출, 예외 발생 등) |
Pointcut | Join Point 중에서 Advice를 적용할 대상을 선별하는 조건식 |
Target Object | Advice가 적용될 실제 객체 |
AOP Proxy | Advice 적용을 위해 생성되는 프록시 객체. 호출을 가로채어 대상 객체를 감싼다 . 주로 CGLIB(Code Generation Library, 실행 중에 실시간으로 코드를 생성하는 라이브러리)를 사용하여 런타임에 프록시 객체를 생성한다. |
Weaving | Advice를 실제 코드 흐름에 결합하는 과정 (런타임, 컴파일타임, 클래스 로딩 시점 등) |
AOP 작동 방식 요약
- AOP Proxy : 공통관심사항을 작동하는 코드를 만들고 적용할 대상을 지정해주면된다. 그러면 AOP는 JavaBean으로 등록된 객체를 프록시로 생성하여 이에 대한 메소드 호출을 가로채어, 추가적인 코드동작들을 메소드 호출 전후에 주입하는 방식으로 작동한다.
- AOP가 적용되려면 해당 객체가 반드시 스프링 빈이어야 합니다. 스프링 컨테이너가 프록시 빈(Proxy Bean) 을 대신 주입함으로써 AOP가 자연스럽게 동작합니다.
- 스프링은 AOP를 적용할 때, "원본 객체" 대신 "프록시 객체"를 빈 컨테이너(Map)에 등록합니다. 그래서 @Transactional, @LogExecutionTime 등의 AOP가 동작하는 이유는
ApplicationContext에서 가져오는 객체가 "원본"이 아니라 "프록시"이기 때문입니다.
- 스프링 컨테이너는 JavaBean으로 등록된 객체(= Target Object)에 대해 프록시를 생성한다.
- 이 프록시 객체가 메서드 호출을 가로채어(= Join Point),
- 지정된 Advice를 Pointcut 조건에 따라 실행 전후에 주입한다.
- 런타임에 프록시를 생성할 때는 CGLIB 또는 JDK 동적 프록시를 사용한다.
- 이러한 동작 과정을 통해 로직과는 분리된 공통 기능이 안전하고 일관되게 적용된다.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Service { void doSomething(); }
class ServiceImpl implements Service {
public void doSomething() {
System.out.println("핵심 기능 실행");
}
}
class Handler implements InvocationHandler {
private final Service target;
public Handler(Service target) { this.target = target; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("공통로직(메소드 전)");
Object result = method.invoke(target, args);
System.out.println("공통로직(메소드 후)");
return result;
}
}
public class ProxyTest {
public static void main(String[] args) {
Service realService = new ServiceImpl();
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class[] { Service.class },
new Handler(realService));
proxy.doSomething();
}
}
AOP 주요 어노테이션 및 개념 설명
- @Aspect : 해당 클래스가 AOP의 핵심 단위인 Aspect임을 나타냅니다.
- @Component : Aspect 클래스를 Spring Bean으로 등록합니다.
- Advice 어노테이션
- 부가기능(Advice)을 적용할 시점을 지정하는 AOP 어노테이션으로 PointCut 표현식도 함께 지정합니다.
- @Around : 메서드 실행 전/후 또는 예외 상황에서 실행될 코드를 정의합니다.
- ProceedingJoinPoint.proceed() : 원래 대상 메서드를 실행하는 코드입니다. @Around에서 반드시 호출해야 합니다.
@Component
@Aspect
public class TimeTraceAop {
// Conditional property to enable or disable the tracing
@Value("${aop.trace.enabled:true}") // This can be set in application.properties
private boolean isTraceEnabled;
@Around("execution(* hello.hellospring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// Check if tracing is enabled
if (!isTraceEnabled) {
return joinPoint.proceed(); // Skip timing and proceed if tracing is disabled
}
// Proceed with tracing if enabled
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
return joinPoint.proceed(); // Proceed with the method execution
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
@RequiredArgsConstructor
@Aspect
@Component
@Slf4j
public class LockAopAspect {
private final LockService lockService;
@Around("@annotation(com.zerobase.travel.redis.PostLock)")
public Object aroundMethod(
ProceedingJoinPoint pjp) throws Throwable {
long postId;
try {
postId = (long) pjp.getArgs()[0];
} catch (Exception e) {
throw new BizException(ParticipationErrorCode.LOCK_PARAM_PROCESSING_ERROR);
}
// lock 취득시도
log.info("Asepct interception triggered");
lockService.lock(postId);
try {
return pjp.proceed();
} finally {
// lock 해체
lockService.unlock(postId);
}
}
}
PointCut 표현식 요약
Spring AOP에서는 PointCut 표현식을 통해 어떤 메서드에 Advice를 적용할지 지정합니다.
- execution(...) : 특정 메서드 시그니처에 매칭되는 실행 지점을 선택합니다. 예: execution(* com.example.service..*(..))
@Aspect
@Component
public class SimpleLoggingAspect {
@Before("execution(* com.example.service..*(..))")
public void beforeMethod() {
System.out.println("메서드 실행 전");
}
}
@Aspect
@Component
public class ReusableLoggingAspect {
@Pointcut("execution(* com.example.service..*(..))")
private void serviceMethods() {}
@Before("serviceMethods()")
public void beforeMethod() {
System.out.println("서비스 메서드 실행 전");
}
@AfterReturning("serviceMethods()")
public void afterMethod() {
System.out.println("서비스 메서드 실행 후");
}
}
- args(...) : 메서드의 인자 타입을 기준으로 조인 포인트를 지정합니다.
- @annotation(...) : 특정 어노테이션이 붙은 메서드를 대상으로 Advice를 적용합니다.
@Around("@annotation(com.zerobase.travel.redis.PostLock)")
public Object aroundMethod(
Advice 메서드 내부에서는:
- 메서드 실행 전 원하는 코드를 작성하고,
- joinPoint.proceed()를 호출하여 원래 비즈니스 로직을 실행하며,
- finally 블록에서 실행 후 처리할 로직을 작성할 수 있습니다.
'개발기술 > Spring' 카테고리의 다른 글
배치처리 (1) | 2025.04.21 |
---|---|
스프링 Bean 컨테이너와 생명주기 (0) | 2025.02.11 |
스프링 환경설정 ; 스프링 부트 (0) | 2025.01.25 |
비동기 Web Flux (0) | 2024.11.19 |
Spring 스케쥴러 (1) | 2024.11.15 |