스프링

스프링 부트 AOP (Aspect-Oriented Programming)

 

  • AOP의 필요성 이해: 소프트웨어 개발에서 반복적으로 발생하는 부가 작업(예: 로깅, 인증/권한 관리, 성능 측정 등)을 핵심 비즈니스 로직과 분리해 유지보수성과 가독성을 높이는 방법 학습.
  • 스프링 부트에서의 AOP 활용법 숙지: 스프링 부트의 AOP 모듈을 사용해 간단한 프로젝트를 설계하고 구현.
  •  
  • 실습 중심 학습: 로깅, 실행 시간 측정 등 실제 개발 환경에서 자주 사용되는 기능을 구현하며 실습.
  • 커스텀 어노테이션 활용: AOP와 커스텀 어노테이션을 조합해 동적이고 재사용 가능한 기능 구현.

 

1.1. 개발 환경 설정

  1. 필수 소프트웨어 설치

    • IntelliJ (또는 이클립스)
    • JDK 17
    • Gradle 또는 Maven 설정 (Spring Boot 의존성 관리)
  2. 스프링 부트 프로젝트 생성

    • Spring Initializr를 사용해 프로젝트 생성: spring-boot-starter-aop는 Spring Boot 기본 제공.
  3. 디렉토리 구조

 

 

├── src/main/java
│   ├── com.example.aopdemo
│       ├── AopDemoApplication.java
│       ├── aspect (AOP 관련 클래스)
│       │   ├── LoggingAspect.java
│       │   ├── PerformanceAspect.java
│       ├── service (비즈니스 로직 계층)
│       │   ├── UserService.java
│       ├── annotations (커스텀 어노테이션 저장)
│       │   ├── TrackExecutionTime.java
│       └── controller (API 엔드포인트)
│           ├── UserController.java

 

습 환경 준비

  • Postman 또는 브라우저로 API 테스트 준비.
  • H2 Database 또는 기본 데이터 저장소(Optional).

 

 

 

1.2. AOP가 필요한 이유

  1. 코드 중복 제거

    • 예: 서비스 클래스마다 System.out.println 로깅 코드를 반복적으로 추가한다면 유지보수하기 어렵고 코드가 지저분해짐.
  2. 관심사 분리(Separation of Concerns)

    • 핵심 로직과 부가 로직(로깅, 보안 등)을 명확히 분리해 코드의 가독성과 재사용성을 높임.
  3. 실제 사례

    • 로깅: 애플리케이션의 상태와 실행 흐름을 파악하기 위해 모든 요청을 기록.
    • 성능 측정: 메소드의 실행 시간을 측정해 성능 이슈를 발견.
    • 보안: 권한 확인 후 메소드 실행 제한.

 

 

 

 

2. AOP의 기본 개념과 어노테이션 

2.1. 주요 개념

  1. Aspect

    • 부가 작업(로깅, 보안 등)을 정의하는 모듈.
    • 여러 클래스에 공통으로 적용될 로직 포함.
  2. Join Point

    • AOP에서 처리 가능한 특정 지점.
    • 스프링에서는 보통 메소드 실행 시점을 Join Point로 사용.
  3. Advice

    • Join Point에서 실행되는 실제 작업.
    • Advice의 종류:
      • @Before: 메소드 실행 전 실행.
      • @After: 메소드 실행 후 실행.
      • @AfterReturning: 메소드가 정상적으로 실행된 후 실행.
      • @AfterThrowing: 메소드 실행 중 예외 발생 시 실행.
      • @Around: 메소드 실행 전후 모두 처리 가능.

 

1. @Before: 메소드 실행 전에 로직 실행

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.aopdemo.service.*.*(..))")
    public void logBeforeMethod() {
        System.out.println("[LOG] 메소드 실행 전에 로깅 처리");
    }
}

 

설명:

  • execution(* com.example.aopdemo.service.*.*(..))
    • com.example.aopdemo.service 패키지 아래 모든 클래스의 모든 메소드 실행 전에 동작.

 

2. @After: 메소드 실행 후 로직 실행

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @After("execution(* com.example.aopdemo.service.*.*(..))")
    public void logAfterMethod() {
        System.out.println("[LOG] 메소드 실행 후 로깅 처리");
    }
}

설명:

  • 메소드가 성공적으로 실행되든 예외가 발생하든 관계없이 메소드 종료 후 실행.

 

3. @AfterReturning: 메소드가 정상적으로 실행된 후 실행

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @AfterReturning(pointcut = "execution(* com.example.aopdemo.service.*.*(..))", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("[LOG] 메소드 정상 실행 완료. 반환값: " + result);
    }
}

설명:

  • returning = "result"를 통해 메소드의 반환값을 받을 수 있음.

 

4. @AfterThrowing: 메소드 실행 중 예외 발생 시 실행

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @AfterThrowing(pointcut = "execution(* com.example.aopdemo.service.*.*(..))", throwing = "exception")
    public void logAfterThrowing(Exception exception) {
        System.out.println("[LOG] 메소드 실행 중 예외 발생: " + exception.getMessage());
    }
}

 

설명:

  • throwing = "exception"을 통해 발생한 예외 객체를 받을 수 있음.

 

5. @Around: 메소드 실행 전후 모두 처리

package com.example.aopdemo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceAspect {

    @Around("execution(* com.example.aopdemo.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("[LOG] 메소드 실행 시작: " + joinPoint.getSignature());
        
        Object result = joinPoint.proceed(); // 실제 메소드 실행
        
        long end = System.currentTimeMillis();
        System.out.println("[LOG] 메소드 실행 완료: " + joinPoint.getSignature() + " 실행 시간: " + (end - start) + "ms");
        
        return result;
    }
}

 

설명:

  • joinPoint.proceed()를 호출하면 실제 메소드를 실행.
  • 메소드 실행 전/후로 추가 로직 삽입 가능.

 

6. @Pointcut: 공통 포인트컷 정의

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    @Pointcut("execution(* com.example.aopdemo.service.*.*(..))")
    public void serviceLayer() {}

    @Pointcut("execution(* com.example.aopdemo.controller.*.*(..))")
    public void controllerLayer() {}
}

 

활용 예제:

  • 정의된 포인트컷을 다른 Advice에서 재사용 가능
package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("com.example.aopdemo.aspect.CommonPointcuts.serviceLayer()")
    public void logBeforeServiceMethods() {
        System.out.println("[LOG] 서비스 계층 메소드 실행 전");
    }
}

 

설명:

  • CommonPointcuts 클래스에 공통 포인트컷 정의.
  • 다른 Aspect에서 serviceLayer()를 호출해 재사용 가능.

전체 실행 순서 요약

  1. @Before: 메소드 실행 처리.
  2. @After: 메소드 실행 처리.
  3. @AfterReturning: 메소드가 정상적으로 실행된 후 처리.
  4. @AfterThrowing: 메소드 실행 중 예외 발생 시 처리.
  5. @Around: 메소드 실행 전후 처리.

 

  1. Pointcut

    • 특정 Join Point를 필터링하기 위한 표현식.
    • 예: execution(* com.example.service.*.*(..)): com.example.service 패키지의 모든 메소드에 적용.
  2. Weaving

    • Aspect를 실제 객체에 연결하는 과정.
    • 컴파일, 로드, 런타임 시점에서 수행 가능. 스프링은 런타임 위빙 사용.

2.2. 스프링에서의 AOP 어노테이션

  1. @Aspect: 클래스를 Aspect로 정의.
  2. @Component: 스프링에서 관리되는 빈으로 등록.
  3. @EnableAspectJAutoProxy: AOP 기능 활성화.
    • 스프링 부트에서는 자동 활성화되므로 추가 설정 불필요.

3. 간단한 AOP 예제: 로깅 (1시간)

3.1. 요구사항 정의

  • 모든 서비스 메소드 호출 시 호출 전에 로그를 출력.

3.2. 코드 구현

 

  1. Aspect 클래스: LoggingAspect

 

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.aopdemo.service.*.*(..))")
    public void logBeforeMethod() {
        System.out.println("[LOG] 메소드 실행 시작");
    }
}

 

 

2.서비스 클래스: UserService

package com.example.aopdemo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    public String getUser() {
        return "사용자 정보 반환";
    }
}

 

 

3.컨트롤러: UserController

package com.example.aopdemo.controller;

import com.example.aopdemo.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/user")
    public String getUser() {
        return userService.getUser();
    }
}

 

 

4.결과 테스트

  • /user API 호출 시 [LOG] 메소드 실행 시작 로그 출력.

 

 

 

4. @Around 어노테이션과 성능 측정 실습 

4.1. 요구사항 정의

  • 특정 메소드의 실행 시간을 측정하고 로그에 출력.

4.2. 커스텀 어노테이션 활용

  1. TrackExecutionTime 어노테이션

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackExecutionTime {
}

 

2.Aspect 클래스: PerformanceAspect

@Around("@annotation(com.example.aopdemo.annotations.TrackExecutionTime)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = joinPoint.proceed();
    long end = System.currentTimeMillis();

    System.out.println(joinPoint.getSignature() + " 실행 시간: " + (end - start) + "ms");
    return result;
}

 

3.서비스 메소드 수정

@TrackExecutionTime
public String getUser() {
    Thread.sleep(1000); // 1초 대기
    return "사용자 정보 반환";
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

부뚜막의 소금도 집어 넣어야 짜다 , 손쉽게 할 수 있는 일이나 좋은 기회가 있어도 이용하지 않으면 소용이 없다는 말.

댓글 ( 0)

댓글 남기기

작성