WEB/Spring

Spring Boot AOP 적용 방법(Log, Transaction)

대전집주인 2024. 9. 3. 11:08
728x90
SMALL

AOP  VS OOP

객체 지향 프로그래밍(OOP)
OOP의 주요 개념:
클래스(Class) : 객체의 설계도. 속성과 메서드를 정의하는 틀입니다.
객체(Object) : 클래스를 통해 생성된 실체로, 실제로 동작하는 프로그램의 단위입니다.
상속(Inheritance) : 기존 클래스를 확장하여 새로운 클래스를 만드는 기능입니다.
다형성(Polymorphism) : 동일한 이름의 메서드가 다른 동작을 수행할 수 있게 하는 기능입니다.
캡슐화(Encapsulation) : 객체의 속성과 메서드를 외부에서 직접 접근하지 못하도록 숨기는 기능입니다.
추상화(Abstraction) : 복잡한 시스템을 단순화하여 핵심 기능만 노출하는 기능입니다.

OOP의 장점:
코드 재사용 : 상속과 다형성 등을 통해 코드의 재사용이 용이합니다.
모듈화 : 각 클래스가 독립적인 역할을 가지며, 모듈 단위로 개발과 테스트가 가능합니다.
유지보수성 : 캡슐화 덕분에 내부 구현을 변경하더라도 외부에 미치는 영향이 적습니다.

OOP의 한계:
횡단 관심사(Cross-Cutting Concerns) 처리의 어려움 : 로깅, 보안, 트랜잭션 관리 등과 같은 기능은 여러 클래스나 모듈에 걸쳐 사용되며, 이를 처리하기 위해서는 중복된 코드가 발생할 수 있습니다. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다.

관점 지향 프로그래밍(AOP)
AOP의 주요 개념:
애스펙트(Aspect) : 횡단 관심사를 모듈화한 것. 예를 들어, 로깅 기능을 애스펙트로 정의하여 여러 클래스에서 공통으로 사용할 수 있습니다.

조인 포인트(Join Point) : 애스펙트가 적용될 수 있는 실행 지점. 메서드 호출, 예외 발생 등이 이에 해당합니다.
포인트컷(Pointcut) : 조인 포인트 중에서 실제로 애스펙트를 적용할 지점을 정의한 것.
어드바이스(Advice) : 애스펙트에서 실행되는 실제 코드. 메서드 실행 전후 또는 예외 발생 시에 동작할 수 있습니다.
위빙(Weaving) : 애스펙트를 실제 코드에 적용하는 과정입니다. 이 과정은 컴파일 타임, 로드 타임, 또는 런타임에 이루어질 수 있습니다.

AOP의 장점:
횡단 관심사의 모듈화: 로깅, 보안, 트랜잭션 관리 등과 같은 공통 기능을 하나의 애스펙트로 정의하여 여러 클래스에 쉽게 적용할 수 있습니다.

코드 간소화 : 핵심 비즈니스 로직에서 공통 관심사를 분리하여 코드를 간결하게 유지할 수 있습니다.
유지보수성 : 공통 관심사를 애스펙트로 모듈화하면, 이를 관리하고 수정하는 것이 훨씬 쉬워집니다.

AOP의 한계:
복잡성 증가 : AOP는 기존 OOP와 함께 사용되므로, 시스템의 복잡성을 증가시킬 수 있습니다.디버깅의 어려움: 애스펙트가 적용된 코드의 흐름을 추적하기 어려워 디버깅이 복잡해질 수 있습니다.


언제 OOP와 AOP를 사용해야 할까?
OOP는 애플리케이션의 주요 비즈니스 로직과 구조를 정의하는 데 사용됩니다. 클래스와 객체를 통해 시스템의 기본 동작을 모델링하고 설계하는 데 적합합니다.

AOP는 여러 모듈에 걸쳐 있는 공통 기능(로깅, 보안, 트랜잭션 관리 등)을 효율적으로 처리해야 할 때 사용됩니다.

OOP로 해결하기 어려운 횡단 관심사를 분리하여 코드의 복잡성을 줄이고 유지보수성을 높이는 데 도움을 줍니다.

 

 

OOP와 AOP의 비교

목적 객체를 중심으로 시스템을 모델링 횡단 관심사를 분리하여 모듈화
핵심 개념 클래스, 객체, 상속, 다형성, 캡슐화 애스펙트, 조인 포인트, 포인트컷, 어드바이스, 위빙
주요 사용 사례 비즈니스 로직 구현 로깅, 보안, 트랜잭션 관리, 예외 처리
코드 재사용 상속과 다형성을 통해 코드 재사용 횡단 관심사를 애스펙트로 모듈화하여 코드 재사용
유지보수성 객체 단위로 코드가 분리되어 있어 유지보수성이 좋음 공통 기능이 모듈화되어 있어 횡단 관심사의 유지보수가 용이
적용 방식 비즈니스 로직의 구현에 직접 사용 기존 코드에 애스펙트를 추가하여 적용

 

AOP Annotaion

1. @Aspect
설명: @Aspect 애노테이션은 이 클래스가 "애스펙트(Aspect)"임을 나타냅니다. 애스펙트는 횡단 관심사(cross-cutting concerns), 즉 여러 클래스나 모듈에서 공통적으로 사용되는 기능(예: 로깅, 보안, 트랜잭션 관리 등)을 모듈화한 것입니다.


사용법: 이 애노테이션을 사용함으로써 Spring AOP가 이 클래스를 애스펙트로 인식하고, 지정된 포인트컷(Pointcut)과 어드바이스(Advice)에 따라 동작하게 됩니다.


2. @Component
설명: @Component 애노테이션은 이 클래스가 Spring의 컴포넌트 스캔에 의해 자동으로 빈(bean)으로 등록되도록 합니다. 이 클래스를 애스펙트로 사용하려면 빈으로 등록되어야 하므로 필수적으로 사용됩니다.


사용법: 이 애노테이션 덕분에 Spring은 이 클래스를 애플리케이션 컨텍스트에 등록하고 관리할 수 있습니다.


3. @Before
설명: @Before 애노테이션은 특정 메서드가 호출되기 전에 실행되는 어드바이스를 정의합니다.


사용법: 이 애노테이션을 사용한 logBefore 메서드는 com.msaProjectMenu01.menu.controller 패키지 내의 모든 메서드가 실행되기 전에 동작합니다. 포인트컷 표현식 "execution(* com.msaProjectMenu01.menu.controller.*.*(..))"에 의해 어떤 메서드가 대상이 되는지 정의됩니다.


4. @After
설명: @After 애노테이션은 특정 메서드가 실행된 후에 실행되는 어드바이스를 정의합니다.


사용법: 이 애노테이션을 사용한 logAfter 메서드는 com.msaProjectMenu01.menu.controller 패키지 내의 모든 메서드가 실행된 후에 동작합니다.


5. @AfterReturning
설명: @AfterReturning 애노테이션은 메서드가 정상적으로 실행된 후, 즉 예외가 발생하지 않고 반환된 후에 실행되는 어드바이스를 정의합니다.


사용법: 이 애노테이션을 사용한 logAfterReturning 메서드는 지정된 메서드가 반환하는 결과를 로깅합니다. returning 속성을 통해 반환된 값을 받을 변수명을 지정할 수 있습니다.


6. @AfterThrowing
설명: @AfterThrowing 애노테이션은 메서드 실행 중 예외가 발생했을 때 실행되는 어드바이스를 정의합니다.


사용법: 이 애노테이션을 사용한 logAfterThrowing 메서드는 메서드 실행 중 발생한 예외를 로깅합니다. throwing 속성을 통해 발생한 예외 객체를 받을 변수명을 지정할 수 있습니다.


7. @Around
설명: @Around 애노테이션은 메서드 실행 전후 또는 예외 발생 시에 실행되는 어드바이스를 정의합니다. 이 어드바이스는 가장 유연하며, 메서드 실행 전과 후의 작업을 모두 처리할 수 있습니다.


사용법: logAround 메서드는 메서드 실행 전과 후에 각각 로깅을 하고, 메서드를 실제로 실행하기 위해 joinPoint.proceed()를 호출합니다.


logExecutionTime 메서드는 메서드 실행 시간을 측정하고 로깅하는 역할을 합니다. 이 메서드 역시 joinPoint.proceed()를 통해 메서드를 실행한 후, 소요 시간을 계산합니다.


@Around 어드바이스는 메서드의 반환 값을 조작할 수도 있으며, 필요에 따라 메서드의 실행을 제어할 수 있습니다.

 

8. 포인트컷 표현식
포인트컷 표현식: "execution(* com.msaProjectMenu01.menu.controller.*.*(..))"와 같은 표현식은 AOP에서 메서드의 실행 지점을 지정합니다. execution(* 패키지.클래스.메서드(..)) 형식을 사용하여 특정 메서드나 클래스에서 어드바이스를 적용할 지점을 선택할 수 있습니다.

 

EX)

  • "execution(* com.msaProjectMenu01.menu.controller.*.*(..))": 이 표현식은 com.msaProjectMenu01.menu.controller 패키지 내의 모든 클래스의 모든 메서드에 어드바이스를 적용한다는 의미입니다.
  • "execution(* com.menu.service..*(..))": 이 표현식은 com.menu.service 패키지 및 하위 패키지 내의 모든 메서드에 어드바이스를 적용합니다.

 

AOP 적용방법

저는 Spring Boot 3.3.3버전과, Gradle을 이용하였습니다.

 

1) 의존성 주입

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

 

2) 패키지 구성

Spring Boot의 경우 application 디렉토리 포함한 경로가 Component Scan 경로이기에 위와 같이 해야만 been 객체로 컨테이너로 올라가 사용할수가 있습니다.

저의 경우 MsaProjectMenu01Application.java 같은 경로 core/aop 폴더안에 LoggingAspect.java, TransactionAspect.java 파일 두개를 생성하였습니다.

 

 

3) LoggingAspect

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    /**
     * Before: 대상 “메서드”가 실행되기 전에 Advice를 실행합니다.
     *
     * @param joinPoint
     */
    @Before("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
    	logger.info("Before: " + joinPoint.getSignature().getName());
    }

    /**
     * After : 대상 “메서드”가 실행된 후에 Advice를 실행합니다.
     *
     * @param joinPoint
     */
    @After("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
    	logger.info("After: " + joinPoint.getSignature().getName());
    }

    /**
     * AfterReturning: 대상 “메서드”가 정상적으로 실행되고 반환된 후에 Advice를 실행합니다.
     *
     * @param joinPoint
     * @param result
     */
    @AfterReturning(pointcut = "execution(* com.msaProjectMenu01.menu.controller.*.*(..))", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
    	logger.info("AfterReturning: " + joinPoint.getSignature().getName() + " result: " + result);
    }

    /**
     * AfterThrowing: 대상 “메서드에서 예외가 발생”했을 때 Advice를 실행합니다.
     *
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "execution(* com.msaProjectMenu01.menu.controller.*.*(..))", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
    	logger.info("예외 발생: " + joinPoint.getSignature().getName() + " exception: " + e.getMessage());
    }

    /**
     * Around : 대상 “메서드” 실행 전, 후 또는 예외 발생 시에 Advice를 실행합니다.
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    	logger.info("실행 메소드: " + joinPoint.getSignature().getName());
        Object result = joinPoint.proceed();
        logger.info("실행 후 메소드: " + joinPoint.getSignature().getName());
        return result;
    }
    
    /**
     * Around advice to calculate the execution time of methods in the specified packages.
     *
     * @param joinPoint
     * @return Object (result of the method execution)
     * @throws Throwable
     */
    @Around("execution(* com.msaProjectMenu01.menu.controller..*(..)) || execution(* com.menu.service..*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        Object proceed = joinPoint.proceed(); // Execute the method

        long executionTime = System.currentTimeMillis() - startTime;

        logger.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);

        return proceed;
    }

}

 

4) TransactionAspect

@Aspect
@Component
public class TransactionAspect {

    private static final Logger logger = LoggerFactory.getLogger(TransactionAspect.class);

    private final TransactionTemplate transactionTemplate;

    public TransactionAspect(TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    @Around("execution(* com.msaProjectMenu01.menu.service..*(..))")  // 서비스 계층의 모든 메서드에 적용
    public Object applyTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        return transactionTemplate.execute(status -> {
            try {
                logger.info("Transaction started for method: {}", joinPoint.getSignature());
                Object result = joinPoint.proceed(); // 메서드 실행
                logger.info("Transaction successful for method: {}", joinPoint.getSignature());
                return result;
            } catch (Throwable throwable) {
                logger.error("Transaction failed for method: {}", joinPoint.getSignature(), throwable);
                status.setRollbackOnly();
                throw new RuntimeException(throwable);
            }
        });
    }
    
    @Before("execution(* com.msaProjectMenu01.menu.service..*(..))")
    public void beforeTransaction() {
        logger.info("Transaction started");
    }
}

 

5) JoinPoint 메서드

메서드 설명
getArgs() 대상 메서드의 인자 목록을 반환합니다.
getSignature() 대상 메서드의 정보를 반환합니다.
getSourceLocation() 대상 메서드가 선언된 위치를 반환합니다.
getKind() Advice의 종류를 반환합니다.
getStaticPart() Advice가 실행될 JoinPoint의 정적 정보를 반환합니다.
getThis() 대상 객체를 반환합니다.
getTarget() 대상 객체를 반환합니다.
toString() JoinPoint의 정보를 문자열로 반환합니다.
toShortString() JoinPoint의 간단한 정보를 문자열로 반환합니다.
toLongString() JoinPoint의 자세한 정보를 문자열로 반환합니다.

 

실행

9000/api/board api를 호출

 

아래 이미지를 보면 로그와 트랜잭션이 정상적으로 사용되는걸 볼 수 있다.

728x90
LIST