지금까지 스프링을 공부하면서 AOP에 대해 들어봤지만, 실제로 사용한 적은 없었던 것 같습니다.
그리고 이 기술이 어디서 사용되는지도 모른채 학습한 것 같습니다.
그래서 이번에는, AOP를 왜 쓰고, 어떻게 쓰는지에 대해 중점을 가지며 학습했습니다.
AOP는 Asepct Oriented Programming의 약자로서 관점 지향 프로그래밍을 의미합니다.
관심사의 분리를 통해 모듈성을 높이는 것을 목표로 하는 프로그래밍 패러다임입니다.
위의 사진을 보면, 치킨집과 쿠팡은 각기 다른 서비스를 제공하는 것을 알 수 있습니다.
치킨집은 주문을 받은 뒤, 치킨을 만들기 위한 재료를 통해 조리를 하고, 조리된 음식을 포장한 이후에 배달을 진행합니다.
쿠팡의 경우에는 주문을 받고, 요청된 물건을 정리한 뒤, 정리된 물건을 포장하고 배달을 진행합니다.
핵심기능 관점에서 치킨집과 쿠팡이 하는 업무는 다릅니다. 치킨집은 요식업과 관련된 기능을 제공하고, 쿠팡은 물류배송 서비스를 제공합니다.
하지만 부가기능 관점으로 바라본다면, 주문받기와 배달하기는 핵심적인 비즈니스로직이라기 보다는 부가적인 기능으로 생각할 수 있다. 정말 핵심적인 기능은 요청된 주문을 어떻게 처리할지에 대한 내용을 담은 2번과 3번이라고 할 수 있습니다.
이렇게 핵심적인 비즈니스 로직은 아니지만, 반드시 진행해야 되는 반복적인 작업을 정리하는데 사용하는 기술이 AOP입니다.
AOP는 관점 지향 프로그래밍이라는 명칭을 갖고 있지만, 객체지향 프로그래밍을 대체하는 기술은 아닙니다.
개발자가 실제 비즈니스 로직에 더 집중할 수 있도록 부가적인 로직을 분리시켜 한층 더 추상화를 진행할 수 있게 됨으로서 OOP를 더 OOP스럽게 만들 수 있는 보조자 역할을 맡습니다.
OOP는 중복된 다수의 기능이 존재하더라도 객체가 책임을 같고 로직을 수행하도록 구성되게 됩니다. 이렇게 진행될 경우, 불가피하게 중복된 다양한 코드가 발생하게 되고 수정과 확장시 복잡한 구조와 중복된 코드로 어려움을 겪을 수 있습니다.
하지만 AOP를 같이 적용하게 되면서 실제 중요하게 사용되는 비즈니스 도메인을 OOP를 통해 구체화시키고, AOP를 통해 부가적인 기능인 트랜잭션, 로깅, 모니터링과 같은 기능을 수행할 수 있게 됩니다.
Target은 부가기능을 제공할 대상을 의미합니다. 주문받기, 배달하기 등의 부가기능을 제공받을 대상인 치킨집이 Target이 됩니다.
Advice는 부가기능 자체를 의미합니다. 주문받기와 같이 부가기능을 담은 구현체 자체이며, 어떠한 기능이 언제 사용될지 정의합니다. 주문받기가 언제 사용될지 정의된 구현체를 의미합니다.
JoinPoint는 Advice가 적용될 위치를 의미합니다. 해당 Advice가 어느 메서드에 실행되는지를 의미합니다.
PointCut은 Advice를 적용할 JoinPoint를 선별하는 기능을 제공합니다. 결국 포인트컷은 부가기능이 적용될 대상을 선정하는 방법이라고 할 수 있습니다. 주문받기라는 기능이 치킨집 또는 쿠팡의 어떠한 특정 메서드에서 실행될지 설정할 수 있습니다.
Aspect는 핵심기능에 부가되어 의미를 갖도록하는 부가기능이 모여진 모듈을 의미합니다.
Advice와 PointCut 등 부가기능을 다루기 위한 모듈이 모인 집합입니다.
Proxy는 타겟을 랩핑한 오브젝트입니다. 어플리케이션에서 타켓을 호출하게 되면 Proxy가 호출되어 실행전, 후에 필요한 기능을 실핼시킬 수 있습니다.
실제 코드로 AOP를 적용하면서 AOP에 대해 간략하게 학습을 진행했습니다.
@Component
public class ChickenHouse {
public void open(){
System.out.println("환영합니다! 치킨집입니당!");
}
public void makeChicken() {
System.out.println("재료를 가공하고 튀김기를 통해 조리하기");
}
public void packChicken() {
System.out.println("조리된 음식을 박스에 포장하기");
}
public void nonException() {
System.out.println("제대로 실행되었습니다!");
}
public void exception() {
throw new RuntimeException("예외 발생!");
}
}
ChickenHouse의 경우에는 간단한 메서드를 담고 있는 클래스입니다. 해당 클래스를 통해 Aop를 Test해봤습니다.
package com.example.springaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ChickenHouseAspect {
...
}
ChickenHouseAspect 클래스를 구성하였고 해당 클래스에 부가기능을 담았습니다.
Aspect 어노테이션을 통해 해당 클래스가 Aspect로서 기능한다는 것을 명시할 수 있습니다.
@Pointcut("execution(* com.example.springaop.ChickenHouse.*(..))")
public void commonPointcut() {
}
Pointcut 어노테이션을 통해 해당 Adivce가 어떤 로직이 수반된 이후에 동작할 것인지 명시할 수 있습니다.
@Before("execution(* com.example.springaop.ChickenHouse.makeChicken(..))")
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() +
" : 실행전 먼저, 주문을 요청 받습니다!");
}
Before 어노테이션의 경우에는 pointcut으로 지정한 로직이 수행되기 이전에 부가기능이 실행되도록 하는 기능을 제공합니다.
@After("execution(* com.example.springaop.ChickenHouse.packChicken(..))")
public void after(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() + " : 실행후, 배달을 시작합니다.");
}
After 어노테이션은 pointcut으로 지정된 로직이 수행된 이후에 부가기능이 실행되도록 지원합니다.
@Pointcut("execution(* com.example.springaop.ChickenHouse.returnNumber())")
public void returnNumber() {
}
@AfterReturning(pointcut = "returnNumber()", returning = "value")
public void afterReturning(JoinPoint joinPoint, int value) {
System.out.println(joinPoint.getSignature().getName() + " : 반환된 값은 : " + value);
}
AfterReturning 어노테이션은 지정된 로직이 정상적으로 실행된 이후에 반환된 값을 받아서 추후 처리를 진행할 수 있는 기능을 제공합니다.
@AfterThrowing(value = "execution(* com.example.springaop.ChickenHouse.exception(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) throws RuntimeException {
System.out.println(joinPoint.getSignature().getName() + " : exception이 터졌습니다!!");
System.out.println("exception : " + e.toString());
}
AfterThrowing 어노테이션은 지정된 로직에서 Exception이 발생했을 경우 해당 Exception을 받아서 추후 로직을 처리할 수 있는 기능을 제공합니다.
@Around("execution(* com.example.springaop.ChickenHouse.open())")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("-----------------실행전---------------");
joinPoint.proceed();
System.out.println("-----------------실행후---------------");
}
Around 어노테이션은 지정된 로직이 실행되기 전, 그리고 실행된 이후에 수행될 부가기능을 설정할 수 있습니다.
이번 스터디에서는 간단하게 AOP를 왜 사용하는지, 그리고 어떻게 사용해야 하는지에 대해 학습했습니다.