스프링 AOP (Aspect Oriented Programming)
스프링 AOP (Aspect Oriented Programming)
스프링 AOP 란?
- AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.
- 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하는 것 (모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.)
- 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
-
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.
- 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.
AOP 주요 개념
- Aspect: 흩어진 관심사를 모듈화 한 것.
- Target: Aspect를 적용하는 곳 (클래스, 메서드)
- Advice: 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
- JointPoint: Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
- PointCut: JointPoint의 상세한 스펙을 정의한 것
AOP 구현체
- 자바
- AspectJ
- 스프링 AOP
AOP 적용 방법
- 컴파일: 자바 파일을 클래스 파일로 만들 때 바이트코드를 조작하여 적용된 바이트코드를 생성
- 로드 타임: 컴파일은 원래 클래스 그대로 하고, 클래스를 로딩하는 시점에 끼워서 넣는다.
- 런타임: A라는 클래스를 빈으로 만들 때 A라는 타입의 프록시 빈을 감싸서 만든 후에, 프록시 빈이 클래스 중간에 코드를 추가해서 넣는다.
스프링 AOP 특징
- 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서다.
- 스프링 빈에만 AOP를 적용할 수 있다.
- 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 … )에 대한 해결책을 지원하는 것이 목적
프록시 패턴
- 프록시 패턴에는 interface가 존재하고 Client는 이 interface 타입으로 Proxy 객체를 사용하게 된다.
- 프록시는 Real Subject를 감싸서 클라이언트의 요청을 처리하게 된다. 프록시 패턴의 목적은 기존 코드 변경 없이 접근 제어 또는 부가 기능을 추가하기 위해서이다.
- Subject 인터페이스 역할 (EventService.java)
public interface EventService{ void createEvent(); void publishEvent(); void deleteEvent(); }
- Client 역할 (AppRunner.java)
@Component public class AppRunner implements ApplicationRunner{ @Autowired EventService eventService; @Override public void run(ApplicationArguments args) throws Exception{ eventService.createEvent(); eventService.publishEvent(); } }
- Real Subject 역할 (SimpleEventService.java)
@Service public class SimpleEventService implements EventService { @Override public void createEvent() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Created an event"); } @Override public void publishEvent() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Published an event"); } @Override public void deleteEvent() { } }
- 아래의 코드에서 단순히 메소드가 2개만 존재할 뿐이기에 괜찮을 수도 있지만 실제 프로젝트라면 많은 중복 코드가 필요할 것이다. 이러한 상황에서 AOP를 사용한다.
@Service public class SimpleEventService implements EventService{ @Override public void createEvent(){ long begin= System.currentTimeMillis(); try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println("Created an event"); System.out.println(System.currentTimeMillis()-begin); } @Override public void publishEvent(){ long begin = System.currentTimeMillis(); try{ Thread.sleep(2000); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println("Published an event"); System.out.println(System.currentTimeMillis() - begin); } @Override public void deleteEvent(){ } }
- Proxy로 코드를 구현하였다. Proxy가 Real Subject에 해당하는 SimpleEventService를 가지고 있고 시간을 측정하는 기능도 대신 가지고 있다. 이렇게 하면 Real Subject와 Client의 코드를 수정하지 않아도 기능을 추가할 수 있다.
- Proxy가 Real Subject를 가지고 있고, Real Subject에 일을 위임해서 대신 처리하고, 부가적인 기능들은 가지고 있다.
@Primary //같은 타입의 빈이 여러가지일 때 그 중 하나를 선택하여 사용하는 에너테이션 @Service public class ProxySimpleEventService implements EventService{ @Autowired SimpleEventService simpleEventService; @Override public void createEvent(){ long begin = System.currentTimeMillis(); simpleEventService.createEvent(); System.out.println(System.currentMillis() - begin); } @Override public void publishEvent(){ long begin = System.currentTimeMillis(); simpleEventService.publishEvent(); System.out.println(System.currentTimeMillis()- begin); } @Override public void deleteEvent(){ } }
- 하지만 Proxy 클래스에서도 중복코드가 발생한다는 점과 Proxy 클래스를 만들어야 하는 비용이 발생하는 문제가 있다.
- 위의 예제에서는 Proxy를 클래스로 만들어서 사용했지만, 동적으로 Proxy 객체를 만드는 방법이 있다. 여기서 동적이란, 런타임, 즉 애플리케이션이 동작하는 중에 동적으로 어떤 객체의 Proxy 객체를 만드는 것을 말한다.
- 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용해서 여러 복잡한 문제 (중복 코드, Proxy 생성)를 해결할 수 있다. 이것이 바로 스프링 AOP 이다.
AOP
- 먼저 @AOP를 사용하기 위해 pom.xml에 의존성을 추가해준다.
- Aspect를 만들기 위해 해야할 일인 Advice, 어디에 적용할 것인가에 대한 Point Cut 두 가지를 정의해야 한다.
@Component @Aspect public class PerfAspect { @Around("execution(* com.example..*.EventService.*(..))") public Object logPerf(ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis(); Object retVal = pjp.proceed(); System.out.println(System.currentTimeMillis() - begin); return retVal; }
- Aspect 에너테이션으로 이 클래스가 Aspect 클래스임을 알려준다. @Component 에너테이션을 사용해서 빈으로 등록하는데 이는 에너테이션 기반의 스프링 IoC를 사용하기 때문에 Component Scan을 통해서 빈 등록을 하기 때문이다.
- ProceedingJoinPoint (PJP)는 Advice가 적용되는 대상이다. 즉, Advice가 적용되는 createEvent, publishEvenr와 같은 메서드 자체라고 보면 된다.
- @Around()은 Aspect의 실행 시점을 지정할 수 있는 에너테이션을 이용해서 적용할 범위를 지정해준다.
- exectuion은 Point Cut 표현식인데 이 표현식을 사용해서 어디에 적용할 지를 정의할 수 있다. 코드를 보면 “com.example 패키지 밑에 있는 모든 클래스에 적용을 하고, EventService 밑에 있는 모든 메소드에 정의해라” 라는 의미이다.
- 하지만 이렇게 할 경우, 적용하고 싶지 않은 메소드에도 적용이 될 수 있다. execution을 사용하는 대신 에너테이션을 사용하면 원하는 곳에만 적용할 수 있다.
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface PerLogging{ }
@Component @Aspect public class PerfAspect { @Around("@annotation(PerLogging)") public Object logPerf(ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis(); Object reVal = pjp.proceed(); System.out.println(System.currentTimeMillis() - begin); return reVal; } }
- @Around 에너테이션에 위와 같이 수정을 하게 되면 위에서 만든 에너테이션 파일과 연결이 된다.
- 성능을 측정하고자 하는 메소드에 @perLoggin 어노테이션을 추가해주면 해당 메소드가 실행될 때 성능 측정도 출력이 된다.
@Service public class SimpleEventService implements EventService { @PerLogging @Override public void createEvent() { System.out.println("Created an event"); } @PerLogging @Override public void publishEvent() { System.out.println("Published an event"); } @Override public void deleteEvent() { System.out.println("deleteEvent"); } }
그 외
- 이 밖에도 @Around 외에 타겟 메서드의 Aspect 실행 시점을 지정할 수 있는 에너테이션이 있다.
- @Before (이전): 어드바이스 티켓 메소드가 호출되기 전에 어드바이스 기능을 수행
- @After (이후): 타겟 메소드의 결과에 관계없이 (즉 성공, 예외 관계없이) 타겟 메소드가 완료되면 어드바이스 기능을 수행
- @AfterReturning (정상적 반환 이후): 타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
- @AfterThrowing (예외 발생 이후): 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
- @Around (메소드 실행 전후): 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출전과 후에 어드바이스 기능을 수행