6 분 소요

AOP(Aspect Oriented Programming)

AOP는 OOP를 대신하는 새로운 개념이 아닌 기존의 OOP를 더욱 보완, 확장하여 OOP를 OOP답게 사용할 수 있도록 도와주는 개념이다. OOP를 통해 객체를 사용함으로 개발자들은 반복된 코드 사용을 줄일 수 있었다. 하지만 트랜잭션, 로깅, 보안 과 같은 필수적으로 반복되는 코드가 있었다. 이렇게 반복된 코드가 있을경우 수정이 힘들고 복잡성, 상호 의존성이 증가하게 되어 좋은 코드가 아니게 된다. 또한 이런 공통 관심사항들을 객체 지향 기법(상속, 위임 등)을 이용해서 여러 모듈에 효과적으로 적용하는데 한계가 있었다. 이에 대한 해결책으로 나온게 AOP이다.

img1

AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법이다. 기능을 비즈니스 로직과 공통 모듈로 구분한 후 개발자의 코드 밖에서 필요한 시점에 비즈니스 로직에 삽입하여 실행한다.

핵심관점(core concern) + 횡단관점(cross-cutting concern) 형태로 관심의 분리(Separation of Concerns) 를 실현한다.

핵심관점(core concern) : 비즈니스 로직

횡단관점(cross-cutting concern) : 트랜잭션, 로깅, 보안, 예외 등 공통 모듈

OOP에서는 공통적인 기능을 각 객체에 종단으로 입력했다면 AOP는 핵심 기능에서 중복되는 공통적인 기능을 종단간에 삽입할 수 있도록 한 것이다.

DI가 의존성 주입이라면 AOP는 기능의 주입

이러한 AOP를 통해 중복 코드 제거, 효율적인 유지보수, 높은 생산성, 재활용 극대화, 변화 수용 용이 등의 이점을 얻을 수 있다.

img4

스프링 프레임워크의 경우 설정을 통해 스프링 프레임워크 에서 작업이 이루어지고 runtime시에 주입된다.

용어

img3

● Joinpoint

Advice를 적용할 수 있는 지점이다. 메소드 호출 시점, 예외가 발생하는 시점 과 같이 애플리케이션을 실행할 때 특정 작업이 실행되는 ‘시점’을 의미한다. 스프링은 메소드 호출에 대한 Joinpoint 이용한다.

● Advice

Joinpoint에서 실행되어야 하는 코드, 공통 관심, 횡단 관점에 해당한다. 언제, 어떤 공통 관심 기능을 핵심 로직에 적용할 지를 정의 한다.

● Pointcut

Joinpoint의 부분 집합으로 실제 Advice가 적용되는 Joinpoint를 나타낸다. joinpoint를 표현하는 패턴

● Aspect

Advice, Pointcut 을 합쳐서 하나의 Aspect라고 한다.

ex) 트랜잭션 기능, 로그 기능, 보안 기능 등

● Target

비즈니스 로직을 구현하고 있는 코드, 핵심 관점에 해당한다. Advice를 받을 대상인 객체로 비즈니스 로직을 수행하는 클래스일 수도 있지만 프록시 객체가 될 수도 있다.

● Weaving

Joinpoint들을 Advice로 감싸는 과정이다.

Pointcut 표현식

● execution : 가장 정교한 Pointcut을 만들수 있고, 리턴타입 패키지경로 클래스명 메소드명(매개변수)

ex) execution(“void a.b.c.*.method()”)

● within : 타입패턴 내에 해당하는 모든 것들을 Pointcut으로 설정

● bean : bean이름으로 Pointcut

  • return 타입 지정

    표현식 설명
    * 모든 리턴타입 허용
    void 리턴타입이 void인 메소드 선택
    !void 리턴타입이 void가 아닌 메소드 선택
  • 패키지 지정

    표현식 설명
    com.gil.demo.controller com.gil.demo.controller 패키지만 선택
    com.gil.demo.controller.. com.gil.demo.controller 패키지로 시작하는 모든 패키지 선택
  • 클래스 지정

    </tr>
    표현식 설명
    MemberDTO MemberDTO 클래스만 선택
    *DTO 이름이 DTO로 끝나는 클래스 선택
    BaseObject+ 클래스 이름 뒤에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스 선택, 인터페이스 이름 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택
  • 메소드 지정

    표현식 설명
    *(..) 모든 메소드 선택
    update*(…) 메소드명이 update로 시작하는 모든 메소드 선택
  • 매개변수 지정

    표현식 설명
    (..) 모든 매개변수
    (*) 1개의 매개변수를 가지는 매소드 선택
    (com.gil.demo.dto.MemberDTO) 매개변수로 MemberDTO를 가지는 메소드만 선택. 꼭 풀패키지명을 명시해줘야 함
    (!com.gil.demo.dto.MemberDTO) 매개변수로 MemberDTO를 가지지 않는 메소드만 선택
    (Integer,…) 한개 이상의 매개변수를 가지되, 첫번째 매개변수의 타입이 Integer인 메소드만 선택
    (Integer, *) 반드시 두 개의 매개변수를 가지되, 첫번째 매개변수의 타입이 Integer인 메소드만 선택

Advice 종류

  • Before Advice 대상 객체의 메소드 호출 전에 공통 기능을 실행한다.

  • After Advice 익셉션 발생 여부에 상관없이 대상 객체의 메소드 실행 후 공통 기능을 실행한다. try - catch - finally의 finally 블록과 비슷하다.

  • After Returning Advice 대상 객체의 메소드가 익셉션 없이 정상적으로 실행된 이후에 공통 기능을 실행한다.

  • After Throwing Advice 대상 객체의 메소드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행한다.

  • Around Advice 대상 객체의 메소드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용된다.

JoinPoint 인터페이스

Advice 메소드를 의미있게 구현하기위해 클라이언트가 호출한 비즈니스 메소드 정보가 필요하다.

JoinPoint 인터페이스가 제공하는 API 이용한다.

ex) 예외가 발생했을때 예외가 발생한 메소드 이름 등 기록

  • JoinPoint 인터페이스
메소드 설명
Signature getSignature() 클라이언트가 호출한 메소드의 시그니처(리턴타입, 매개변수) 정보가 저장된 Signature 객체를 리턴
Object getTarget() 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체를 리턴
Object[] getArgs() 클라이언트가 메소드 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴
  • Signature API
메소드 설명
String getName() 클라이언트가 호출한 메소드 이름 리턴
String toLongString() 클라이언트가 호출한 비즈니스 메소드의 리턴타입, 이름, 매개변수(시그니처)를 패키지 경로까지 포함하여 리턴)
String toShortString() 클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴
String getDeclaringTypeName() 클라이언트가 호출한 메소드를 가지는 클래스 풀패키지명을 리턴

모든 Advice는 org.aspectj.lang.JoinPoint 타입의 파라미터를 어드바이스 메서드에 첫 번째 매개변수로 선언할 수 있다.

ProceedingJoinPoint 인터페이스

Around 어드바이스의 경우 JoinPoint를 상속받는 ProceedingJoinPoint 타입의 파라미터를 필수적으로 선언해야 한다.

proceed()

around 어드바이스의 경우 클라이언트 호출을 가로챈다. 만약 around 어드바이스 메소드에서 바로 return을 해버리면 비즈니스 메소드(Target)가 실행이 안된다.

따라서 around 어드바이스에서 비즈니스 메소드 호출을 책임져야 한다. 즉, around 어드바이스에서 비즈니스 메소드 호출을 가로챘기 때문에 around 어드바이스에서 비즈니스 메소드 호출을 하지 않으면 실행될 방법이 없다.

비즈니스 메소드를 호출하기 위해선 비즈니스 메소드관련 정보를 around 어드바이스 메소드가 가지고 있어야 하는데 그 정보를 Spring 컨테이너가 around 어드바이스에게 넘겨준다. 그게 ProceedingJoinPoint 객체이고 비즈니스 메소드를 진행하도록 하는게 proceed() 메소드이다. 그러므로 proceed() 메소드를 기준으로 비즈니스 메소드 수행 전, 후로 나뉜다. proceed() 호출 전이 비즈니스 메소드 호출 전이고 proceed() 호출 후가 비즈니스 메소드 호출 후 라고 생각하면 된다.

Object org.aspectj.lang.ProceedingJoinPoint.proceed() throws Throwable

proceed() 메소드 반환값이 Object 임을 알 수 있다. 여기에는 비즈니스 메소드가 실행되고 난 후의 결과 값들이 담겨 있게 된다.

예를 들어 비즈니스 메소드 기능이 select 기능이라면 그 결과 값(보통 VO형태로 담기고 이 VO가 Object에 담기게 된다.)이 Object에 담기고 비즈니스 메소드의 리턴 값이 없을 경우 Object에는 null이 담긴다.

@Aspect
public class Performance {

    @Around("execution(* com.blogcode.board.BoardService.getBoards(..))")
    public Object calculatePerformanceTime(ProceedingJoinPoint proceedingJoinPoint) {
        Object result = null;
        try {
            long start = System.currentTimeMillis();
            result = proceedingJoinPoint.proceed();
            long end = System.currentTimeMillis();

            System.out.println("수행 시간 : "+ (end - start));
        } catch (Throwable throwable) {
            System.out.println("exception! ");
        }
        return result;
    }
}

Spring AOP

Weaving

일반적으로 컴파일시와 클래스 로딩 시에 weaving하는 방식은 AspectJ 라이브러리를 추가하여 구현할때 사용된다.

  1. Compile-time Weaving

    Load-time에 대한 절차가 없어서 퍼포먼스 하락 없이 구성이 가능하다. Lombok과 같이 compile시 간섭하는 plugin들과 충돌이 발생한다.

  2. Class Load-time Weaving

    applicationContext에 로드된 객체들을 불러온 뒤, AspectJ weaver에 의해 객체들을 weaving한다. 객체들을 전부 불러온 뒤 weaving을 하기 때문에 약간의 퍼포먼스 하락이 있다.

  3. Run-time Weaving

    Spring AOP에서 사용하는 방식으로 소스코드나 클래스 정보 자체를 변경하지 않고 중간에 프록시 객체를 생성하여 AOP를 적용한다.

img2

스프링은 Aspect의 적용 대상(Target) 이 되는 객체에 대한 Proxy를 만들어 제공한다. Target 객체를 사용하는 코드는 Target에 접근하려면 Proxy를 통해서 간접적으로 접근하게 된다. Proxy에서 Aspect의 Advice(공통기능)를 실행한 후에 Target 메소드를 호출하거나 Target 메소드가 호출된 후 Advice를 실행하는 방식으로 마치 중간에 끼워넣는것 같다고 해서 Weaving이다.

Proxy

Proxy는 Target을 감싸서 요청을 대신 받아주는 랩핑 클래스이다.

Spring에서는 Proxy를 이용하여 OCP 원칙을 적용하고 있다.

Open-Close Principal(OCP) : 개방폐쇄의 원칙

‘소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.’는 프로그래밍 원칙

예제

@Component // 스프링에서 관리하는 bean
@Aspect // AOP bean
public class LogAdvice {

 // private : 외부에서 로그를 가로채지 못하도록 하기 위해
 // static final : 로그 내용이 바뀌지 않으므로
 // 로깅툴을 사용하는 이유 : sysout명령어는 IO리소스를 많이 사용하여 시스템이 느려질 수 있다, 로그를 파일로 저장하여 분석할 필요가 있다.
 private static final Logger logger = LoggerFactory.getLogger(LogAdvice.class);

 // PointCut - 실행 시점
 // @Before, @After, @Around
 // 컨트롤러, 서비스, DAO의 모든 method를 실행 전후에 logPrint method가 자동으로 실행된다.
 // .. : 하위의 모든 디렉토리를 의미
 // *(..) : * - 하위의 모든 메서드, (..) - 모든 매개변수
 @Around("execution(* com.example.spring02.controller..*Controller.*(..))"
         + " or execution(* com.example.spring02.service..*Impl.*(..))"
         + " or execution(* com.example.spring02.model..dao.*Impl.*(..))")
 public Object logPrinnt(ProceedingJoinPoint joinPoint) throws Throwable{
     // 실행 시간 체크 : 시작시간
     long start = System.currentTimeMillis();
     // 핵심로직으로 이동
     Object result = joinPoint.proceed();
     // 클래스 이름
     String type = joinPoint.getSignature().getDeclaringTypeName();
     String name = "";
     if (type.indexOf("Controller") > -1) {
         name = "Controller:";
     } else if (type.indexOf("Service") > -1) {
         name = "ServiceImpl:";
     } else if (type.indexOf("DAO") > -1) {
         name = "DAO:";
     }
     // 메서드 이름
     logger.info(name+type+"."+joinPoint.getSignature().getName()+"()");
     // 파라미터 이름
     logger.info(Arrays.toString(joinPoint.getArgs()));
     // 실행 시간 체크 : 종료시간
     long end = System.currentTimeMillis();
     // 실행 시간 체크 : 연산
     long time = end-start;
     logger.info("실행 시간:"+time);
     return result;
 }
}

태그:

카테고리:

업데이트:

댓글남기기