56. AOP

송민경's avatar
Apr 02, 2024
56. AOP

1. AOP(Aspect-Oriented Programming)

  • 관점 지향 프로그래밍
  • 소프트웨어 개발에서 각각의 비즈니스 로직이나 기능 이외의 공통 관심 사항을 분리
→ 모듈화하는 기법
코드의 가독성과 유지 보수성을 높일 수 있음
  • 리플렉션 (메서드 분석 가능)
  • 주로 클래스와 메서드의 단위로 관심사의 분리
  • 횡단 관심사(cross-cutting concerns)
보안, 로깅, 트랜잭션 관리 등과 같이 여러 모듈에서 반복적으로 발생하는 부분
여전히 여러 모듈에 걸쳐 분산
  • 횡단 관심사를 '관점'이라 불리는 모듈로 모듈화하는 프로그래밍 패러다임
  • 보통 메서드 호출이나 객체 생성과 같은 지점에 Advice를 적용하여 특정 관심사를 분리
  • 관점(Aspect) → 코드의 여러 지점(Join Point)에서 공통적으로 발생하는 작업(Advice)을 적용
- Aspect(관점) : 횡단 관심사를 모듈화한 것
- Join Point(조인 포인트) : 조인 포인트는 프로그램 실행 중 관점이 적용될 수 있는 특정한 지
- Advice(어드바이스 ): 조인 포인트에서 실행되는 코드
관점의 횡단 관심사를 수행하는 실제 코드 블록

2. Aspect

  • 생성 : 로깅 기능(Advice)을 포함하는 Aspect를 정의(클래스 생성)
  • 정의 : 메서드 실행 전후에 로그를 남기는 코드 작성(수행할 메서드 생성)
  • 설정 : 로깅 기능을 적용할 메서드를 결정
ex) 모든 public 메서드에 로깅을 적용하려면 해당하는 패턴을 Pointcut에 지정(깃발에 별칭주기)
  • 적용 : 설정한 Pointcut에 따라 실행 전후에 로그를 남기는 Advice가 실행(PointCut(별칭) 적용)

3. 인터셉트(Interception)

  • 메서드 호출이나 객체 생성과 같은 작업을 가로채는 기술
  • 메서드 실행 전에 실행
  • 주로 프레임워크나 라이브러리의 훅(hook) 또는 프록시를 통해 구현
  • AOP의 구현 기술 중 하나로 사용 가능
 

4. 리플렉션(Reflection)

  • 런타임에 클래스의 메타데이터를 검사하고 조작하는 기능
  • 주로 클래스, 필드, 메서드 등의 정보를 동적으로 액세스하고 조작하는 데 사용
  • AOP나 인터셉트와 달리 보다 일반적인 용도로 사용
  • 스프링부트에서는 DispatcherServlet을 우리가 제어할 수 없음 → AOP 제공
깃발(어노테이션)을 원하는 위치에 설정
그 깃발이 있는 코드가 실행될 때 수행할 공통기능을 적용
 

5. AOP 적용 방법 3가지

첫번째 방법

  • 깃발(어노테이션)을 만들기
    • package shop.mtcoding.aopstudy.config.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 메소드에 적용할 어노테이션을 정의하기 위한 어노테이션입니다. @Target(ElementType.METHOD) //런타임 시에도 어노테이션 정보를 유지하기 위한 어노테이션 @Retention(RetentionPolicy.RUNTIME) public @interface Hello { }
  • 깃발을 PointCut(별칭)으로 등록하기
  • Advice를 만들기 (메서드 행위)
  • PointCut을 Advice에 적용하기
    • package shop.mtcoding.aopstudy.config.advice; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class HelloAdvice { // 깃발에 별칭주기 @Pointcut("@annotation(shop.mtcoding.aopstudy.config.annotation.Hello)") public void hello(){} // 매개변수에 접근해서 값을 찾는 것을 가능 - 값을 주입하려면 @Around 사용해야함 @Before("hello()") public void helloAdvice(JoinPoint jp) throws Throwable { Object[] args = jp.getArgs(); for (Object arg : args) { if(arg instanceof String){ String username = (String) arg; System.out.println(username+"님 안녕"); } } } }
  • HelloController
package shop.mtcoding.aopstudy.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import shop.mtcoding.aopstudy.config.annotation.Hello; @RestController public class HelloController { @GetMapping("/hello/v1") public String v1(){ return "v1"; } // http://localhost:8080/hello/v2?username=ssar @Hello @GetMapping("/hello/v2") public String v2(String username){ System.out.println("username : 값 변경? : "+username); return "v2"; } }
notion image
notion image
 
 

두번째 방법

  • 이미 만들어져 있는 깃발을 PointCut(별칭)으로 등록하기
  • Advice를 만들기 (메서드 행위)
  • PointCut을 Advice에 적용하기
  • postMapping과 putMapping에 관한 매개변수에 대한 값을 파싱
→ 맵에 던져주고 가공해서 MyValidationException의 위임
package shop.mtcoding.aopstudy.config.advice; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import shop.mtcoding.aopstudy.config.exception.MyValidationException; import java.util.HashMap; import java.util.Map; // Aspect로 동작하기 위한 클래스임을 선언하는 어노테이션 @Aspect // Spring의 컴포넌트로 등록되도록 하는 어노테이션 @Component public class ValidAdvice { // Pointcut을 선언 @PostMapping 어노테이션이 지정된 메소드를 대상으로 함 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void postMapping() { } // @PutMapping 어노테이션이 지정된 메소드를 대상으로 함 @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") public void putMapping() { } // 메소드의 매개변수 중에 BindingResult가 있는 경우, 해당 객체에 에러가 있는지 검사 // 에러가 있으면 에러 정보를 Map에 담아 오류를 던짐 @Before("postMapping() || putMapping()") public void validationAdvice(JoinPoint jp) throws Throwable { Object[] args = jp.getArgs(); for (Object arg : args) { if (arg instanceof BindingResult) { BindingResult bindingResult = (BindingResult) arg; if (bindingResult.hasErrors()) { Map<String, String> errorMap = new HashMap<>(); // BindingResult에 포함된 모든 FieldError를 순회하면서 에러 정보를 Map에 담기 for (FieldError error : bindingResult.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } // 발생한 에러 정보를 담은 Map과 함께 MyValidationException을 throw throw new MyValidationException(errorMap); } } } }
  • 컨트롤러
@PostMapping("/valid") public String join(@Valid JoinInDto joinInDto, BindingResult bindingResult){ return "ok"; }
  • JoinDTO
package shop.mtcoding.aopstudy.dto; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Getter @Setter public class JoinInDto { @NotNull //NULL 안됨 private String username; @NotEmpty // 값이 비어 있으면 안됨 private String password; @NotEmpty // 값이 비어 있으면 안됨. @Size(min = 4, max = 10) // 4자 이상 10자 이하 private String email; }
  • MyValidationException
package shop.mtcoding.aopstudy.config.exception; import lombok.Getter; import java.util.Map; @Getter public class MyValidationException extends RuntimeException { private Map<String, String> erroMap; public MyValidationException(Map<String, String> erroMap) { this.erroMap = erroMap; } }
 
notion image
 

세번째 방법

  • 깃발을 생성하지 않고, 특정한 패턴이 수행될 때 정의된 advice를 실행하기
  • Spring Framework에서는 XML을 이용하여 AOP를 설정하는 방법
  • Spring Boot에서는 주로 어노테이션 기반의 AOP를 사용
execution expression을 이용하여 pointcut을 등록 가능
  • 직접적으로 execution expression을 이용하여 pointcut을 등록하려면 여러가지 설정이 필요
Spring Boot에서는 AspectJ를 사용하여 간편하게 pointcut을 등록할 수 있는 기능을 제공
execution expression(메서드 Pointcut 지점 설정 시 사용 때)
// AspectJ의 포인트컷 표현식 사용 -> 메서드 실행 시점을 지정 // execution 키워드는 메서드 실행, 특정 메서드를 대상으로 포인트컷을 정의 가능 // 여기서 *는 모든 리턴 타입을 나타냅니다. // com.example.service.MyService는 패키지명과 클래스명을 지정, MyService 클래스를 가리킴 // save(..)는 메서드명과 메서드 시그니처, 이는 save 메서드를 가리킴 // 괄호 안에(메서드의 매개변수 타입이 들어가야됨) ..을 사용하여 모든 매개변수 타입을 허용 @Pointcut("execution(* com.example.service.MyService.save(..))") public void saveMethod() {}
  • AspectJ는 Java와 비슷한 문법
execution expression을 이용하여 pointcut을 등록하기
 
메서드 매개변수가 여러개 일 때
@Pointcut("execution(* com.example.service.MyService.save(java.lang.String, java.lang.Integer))") public void saveMethodWithStringAndIntegerParams() {}
execution expression(컨트롤러 Pointcut 지점 설정 시 사용 때)
@Pointcut("execution(* com.example.controller.UserController.*(..))") public void userControllerMethods() {}
 
Share article

vosw1