AOP(관점 지향 프로그래밍)

KDG: First things first!·2024년 9월 10일
0

Spring

목록 보기
1/5




AOP(관점 지향 프로그래밍)란?

AOP : 소프트웨어 개발 패러다임 중 하나로, 핵심 로직과 공통적이고 부가적인 기능(횡단 관심사, cross-cutting concerns)을 분리하여 코드의 모듈성을 향상시키는 방법이다. AOP를 사용하면 비즈니스 로직에 영향을 주지 않고도 로깅, 트랜잭션 관리, 보안, 예외 처리와 같은 공통 기능을 중복 없이 재사용 가능하게 만들 수 있다.

그렇다면 AOP는 어떤 상황에서 필요한 것일까???

예를 들어 회사에서 특정 프로그램의 메서드 실행시간을 측정하라고 지시가 내려온 상황이라고 가정해보자.

// 측정 시작
    long startTime = System.currentTimeMillis();

    try {
        
        // (수업 생성) 메서드 로직 내용 //
       
    } finally {
        // 측정 완료
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        log.info("::: ExecutionTime: {}ms", executionTime);
    }

위와 같이 메서드 시작과 끝에 System.currentTimeMillis()을 붙여주면 손쉽게 메서드의 실행시간을 측정할 수 있다. 하지만 해당 방식에는 다음과 같은 문제들이 있다.

  • 시간 측정 로직을 모든 메서드에 일일이 붙여줘야 해서 매우 번거롭고 비효율적이다.
  • 차후에 시간 측정 단위가 변경되거나 시간 측정에 새로운 기능을 추가해야 하는 경우 적용된 모든 메서드를 일일이 수정해야 되서 마찬가지로 매우 비효율적이다.

이러한 문제점을 방지하기 위한 해결책이 바로 AOP 적용을 통한 횡단 관심사 분리이다.


AOP를 한 마디로 정의하면 핵심기능과 횡단 관심사(부가적이고 중복되는 기능)을 분리해서 관리하는 것이다.




AOP(관점 지향 프로그래밍) 주요 개념


1. 어드바이스(Advice)

어드바이스(Advice) : 어드바이스는 실제로 실행되는 횡단관심사(부가 기능) 코드를 의미한다.

어드바이스의 종류로는 로깅 처리, 인증 및 인가 처리, 트랜잭션 처리 등의 부가기능이 있다.



2. 포인트컷(Pointcut)

포인트컷(Pointcut) : 포인트컷은 AOP에서 어드바이스를 적용할 구체적인 범위를 선택하는 규칙이다.

해당 범위는 특정 패키지 내부의 모든 메서드가 될 수도 있고, 특정 클래스의 모든 메서드가, 그리고 가장 자주 쓰이는 특정 애노테이션이 달린 메서드가 될 수도 있다.

포인트컷을 통해 사용자가 어드바이스를 어느 범위에 적용할지 정확하게 지정하는 것이 가능하다.



3. 타겟(Target)

타겟(Target) : 타겟은 AOP에서 어드바이스가 적용되는 객체이다.

예를 들어 특정 클래스가 포인트컷으로 설정되면 해당 클래스 내의 모든 메서드가 어드바이스에 적용된다.

execution(int createCourse(int, int)) // 반환타입 int, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(int, int)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(..)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수 상관없음
execution(* *(..)) // 반환타입 상관없음, 메서드이름 상관없음, 매개변수 상관없음


4. 조인포인트(JoinPoint)

조인포인트(Target) : 조인포인트는 어드바이스(횡단관심사)가 적용되는 실행 지점을 의미한다.

특정 클래스를 타겟으로 지정하면 해당 클래스 내부의 메서드들이 조인포인트가 된다.



5. 에스펙트(Aspect)

에스펙트(Aspect) : 에스팩트는 어드바이스(횡단관심사)와 포인트컷(횡단 관심사를 어디에 적용할지) 를 하나로 묶은 모듈이다.



6. 용어 정리

  1. 어드바이스 는 반복되는 횡단관심사를 정의해 놓은곳이다.(ex. 트랜잭션, 로깅, 인증 및 인가 등)

  2. 그 횡단관심사를 어느 범위에 적용할지 선택하는 것이 포인트컷 이다.
    (ex 패키지 내부, 클래스 내부, 특정 애노테이션 등)

  3. 그리고 그 범위 안에서 선택받은 객체들 혹은 대상 객체가 타겟 이 되는 것이다.

  4. 그 타겟 내에서 어드바이스 실제로 실행되는 시점을 조인포인트 라고 한다.

  5. 이러한 어드바이스포인트컷을 하나로 묶어 모듈화하여 특정 기능을 여러 클래스에 공통적으로 적용될 수 있는 모듈이 에스팩트이다.




AOP 적용 코드

1. Aspect 생성


@Slf4j
@Aspect
/**
 * 애스팩트: 어드바이스와 포인트컷을 하나로 묶은 모듈
 */
public class AspectPractice {
    // 포인트컷
    // 어드바이스 위치
}

클래스 레벨에 @Aspect를 붙이면 Aspect로 적용된다.
@Aspect는 AOP에서 적용할 어드바이스가 정의된 클래스임을 나타낸다.



@Configuration
public class WebConfig {

    @Bean
    public AspectPractice getAspectPracticeAop() {
        return new AspectPractice();
    }

}

싱글톤을 보장해주는 @Configuration이 포함된 설정 클래스인 WebConfig에 위에서 Aspect로 정의한 클래스 AsepectPractice를 Bean으로 등록한다.



2. 포인트 컷(패키지 범위) 생성


    /**
     * 포인트컷: 서비스 패키지 기반
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() {}

@Pointcut 애노테이션을 붙이고 execution 포인트컷 표현식을 정의하여 포인트 컷의 구체적인 범위를 지정한다.



3. 어드바이스의 종류



1. @Before

/**
     * 어드바이스: @Before
     * 메서드 실행 전에 수행되는 로직을 처리할때 사용합니다.
     */
    @Before("serviceLayer()")
    public void beforeMethod(){
        log.info("::: BEFORE 실행 :::");
    }

@Before("(@Pointcut 적용된 메서드 이름)")는 Spring AOP에서 "메서드 실행 전에" 특정 로직을 실행하는 어드바이스(Advice)를 정의할 때 사용하는 애노테이션이다. 즉, 지정된 포인트컷에 해당하는 메서드가 실행되기 바로 전에 부가 기능을 적용하는 것을 의미한다.


쉽게 말해, @Before를 붙이면 포인트 컷에 포함되는 메서드들(@Before의 () 안에 정의된 메서드의 @Pointcut("execution...") 범위 안에 포함되는 메서드들)이 실행되기 전에 해당 메서드가 수행된다.



2. @AfterReturning


 /**
     * 어드바이스: @AfterReturning
     * 메서드가 정상적으로 반환된 후에 실행됩니다.
     * 예외가 발생하지 않고 정상적으로 결과값을 반환했을때만 동작합니다.
     * @param result 성공시 반환값
     */
   @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void afterReturningMethod(Object result) {
       // result 에 대한 작업을 수행할 수 있겠죠?
       log.info("::: AFTER RETURNING :::");
    }

@AfterReturning(pointcut = "@Pointcut 적용된 메서드 이름", rfeturning = "메서드 성공 시 리턴 값 변수 이름")

@AfterReturning가 붙은 메서드는 메서드의 결과값을 파라미터로 받아 사용하면 된다.



3. @AfterThrowing


    /**
     * 어드바이스: @AfterThrowing
     * 메서드 실행 중 예외가 발생했을 때만 실행됩니다.
     * @param joinPoint 조인포인트
     * @param ex 발생한예외 객체
     */
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        // 서비스에서 발생한 예외 ex.getMessage() <- 예외 조작 가능
        log.info("::: AFTER THROWING :::");
    }

@AfterThrowing(pointcut = "@Aspect가 적용된 메서드 이름(AOP 적용 범위), throwing = "발생한 예외를 전달받을 파라미터 이름")

(여기서 메서드의 파라미터로 들어간 JoinPoint는 AOP에서 실제로 예외가 발생한 타겟 메서드를 가리킨다.)



4. @After



/**
     * 어드바이스: @After
     * 메서드가 정상적으로 실행되던 예외가 발생하던 메서드가 완료된 후에 항상 실행됩니다.
     */
    @After("serviceLayer()")
    public void afterMethod(JoinPoint joinPoint) {
        log.info("::: AFTER :::");
    }

@Atfer는 try-catch문에서 무조건 실행되는 finally 역할을 한다고 생각하면 이해하기 쉽다.



5. @Around


/**
     * 어드바이스: 가장 강력한 어드바이저, 전체 흐름을 제어할 수 있습니다.
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("serviceLayer()")  // 어노테이션 기반으로 포인트컷 설정
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("::: BEFORE :::");
        try {
            Object result = joinPoint.proceed(); //  타겟 메서드 실제 실행
            System.out.println(result);
            log.info("::: AFTER RETURNING :::");
            return result;

        } catch (Exception e) {
            log.info("::: AFTER THROWING :::");
            throw e;

        } finally {
            log.info("::: AFTER :::");
        }
    }



@Around의 특징

  • 메서드 실행 전후의 로직을 제어할 수 있음.
  • 타겟 메서드를 직접 실행하거나 실행하지 않을 수도 있음.
  • 메서드의 반환값을 조작할 수 있음.
  • 메서드 실행 중 발생하는 예외를 처리할 수 있음.

ProceedingJoinPoint : @Around에서 사용하는 파라미터로, 타겟 메서드를 실행할 수 있는 기능을 제공한다.

proceed() 메서드를 호출하면 타겟 메서드가 실행된다.


proceed(): 타겟 메서드를 호출하는 메서드이다. 이 호출이 없으면 실제 타겟 메서드는 실행되지 않는다.

타겟 메서드를 실행하지 않도록 제어하거나, 반환값을 수정하거나 예외 처리 등을 할 수 있다.



6. 정리



4. 어드바이스의 적용


1. 패키지 범위 기반 어드바이스 적용

 /**
     * 포인트컷: 서비스 패키지 기반
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() {}

    
    /**
     * 어드바이스: 패키지 범위 기반
     */
    @Around("serviceLayer()")
    public Object advicePackage(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            return joinPoint.proceed();

        } finally {
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("::: ExecutionTime: {}ms", executionTime);
        }
    }

@Pointcut으로 패키지 기반으로 어드바이스 적용 범위를 설정한 메서드를 정의하고 해당 메서드를 사용하여 어드바이스 애노테이션을 사용한다.



2. 어노테이션 범위 기반 어드바이스 적용


[애노테이션 사용자 생성]

/**
 * @Target(ElementType.METHOD)
 * 이 어노테이션이 적용될 수 있는 위치를 지정합니다. 
 * 이 경우, 메서드에만 적용될 수 있음을 의미합니다.
 *
 * @Retention(RetentionPolicy.RUNTIME)
 * 이 어노테이션이 런타임에 유지되도록 지정합니다. 
 * 즉, 이 어노테이션은 실행 시점까지 존재하며 리플렉션을 통해 접근할 수 있습니다.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackTime {
}


[Aspect 생성]

@Slf4j
@Aspect
/**
 * 애스팩트: 어드바이스와 포인트컷을 하나로 묶은 모듈
 */
public class AspectPractice {
    // 포인트컷
    
    // 어드바이스 위치
}


[포인트컷 생성]


@Slf4j
@Aspect
public class AspectPractice {
 
--------포인트컷 예시들---------------------------------------------
    
    /**
	* 포인트컷 1: 어토네이션 기반
	*/
 	@Pointcut("@annotation(com.standard.sparta.aop.annotation.TrackTime)")
 	private void trackTimeAnnotation() {}
    
    
    /**
     * 포인트컷 2: 서비스(Service) 패키지 기반
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() {
    }
    
    
    // 어드바이스 위치
}




[어드바이스 적용]



@Slf4j
@Aspect
public class AspectPractice {
 
 --------포인트컷 예시들--------------------------------------------
    
    /**
	* 포인트컷 1: 어토네이션 기반
	*/
 	@Pointcut("@annotation(com.standard.sparta.aop.annotation.TrackTime)")
 	private void trackTimeAnnotation() {}
    
    
    /**
     * 포인트컷 2: 서비스(Service) 패키지 기반
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() {
    }
    
    
    
    
--------어드바이스 예시들--------------------------------------------
    
    /**
     * 어드바이스 1: 어노테이션 범위 기반
     */
    @Around("trackTimeAnnotation()")
    public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
        //측정 시작
        long startTime = System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            // 측정 완료
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("::: ExecutionTime: {}ms", executionTime);
        }
    }
    
    
    
     /**
     * 어드바이스 2: 패키지 범위 기반
     */
    
    @Around("serviceLayer()")  // 어노테이션 기반으로 포인트컷 설정
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("::: BEFORE :::");
        try {
            Object result = joinPoint.proceed(); //  타겟 메서드 실제 실행
            System.out.println(result);
            log.info("::: AFTER RETURNING :::");
            return result;

        } catch (Exception e) {
            log.info("::: AFTER THROWING :::");
            throw e;

        } finally {
            log.info("::: AFTER :::");
        }
    }

}





	



5. AOP 동작 원리

스프링에서는 객체를 빈으로 등록하면 빈후처리기를 거쳐야 한다. 하지만 이 때 빈후처리기가 빈으로 등록되는객체를 검사하여 해당 객체에 AOP 설정이 적용되어 있음을 감지하면, 해당 객체를 Proxy 객체로 감싼 후(해당 객체를 상속받아 메서드를 호출하고 AOP 처리하는 프록시 객체를 생성) 프록시객체를 스프링 컨테이너에 등록한다.



등록된 Proxy 객체는 중간에 삽입되어 실제 대상 객체에 대한 클라이언트의 호출을 가로챈다.

이로 인해 클라이언트 호출시 실제 대상 객체가 아니라 Proxy 객체가 호출된다는 사실을 기억해야 한다!

profile
알고리즘, 자료구조 블로그: https://gyun97.github.io/

0개의 댓글