[Spring Boot] AOP - Aspect Oriented Programming
목표
1. AOP 개념과 주요 용어
2. @Aspect 어노테이션
내용
1. Aspect Oriented Programming의 개념과 주요 용어를 파악한다.
2. 포인트 컷 문법을 이해하고 구현한다.
3. @Aspect 어노테이션을 이용해 다양한 Advice를 구현할 수 있다.
참고 내용
객체지향 기본원칙을 적용해서 핵심기능과 부가기능을 분리해서 모듈화 하는 것은 매우 어렵다. AOP 방법으로 상당 부분 해결할 수 있다.
스프링 AOP는 매우 강력한 기능이고 실제로 여러 프로젝트에서 거의 필수적으로 사용되고 있으며 적극적으로 사용함으로써 전체적인 애플리케이션의 품질을 높일 수 있다.
AOP의 개요와 용어
AOP (Aspect Oriented Programming)
■ 핵심기능과 부가기능
- 업무(Biz) 로직을 포함하는 기능을 핵심 기능 (Core Concerns)
- 핵심기능을 도와주는 부가적인 기능(로깅, 보안 등)
- 부가기능 (Cross-cutting Concerns)
- 핵심기능에서 부가기능을 분리해서 모듈화 하는 프로그래밍 기법 - AOP
로그를 남기거나, 핵심기능을 사용할 때 인증, 인가를 한다던가, 비즈니스 로직의 트랜잭션 처리를 하는 작업은 모든 핵심기능에 공통적으로 사용되는 기능으로 볼 수 있다. AOP는 비즈니스 로직을 나타내는 핵심기능을 작성할 때마다 공통적으로 작성하는 부가기능(로깅, 보안, 트랜잭션)을 분리해서 프로그래밍하는 기법이다. Java에서 AOP를 사용하기 위해 AspectJ라는 것을 사용한다고 하는데, 문법이 조금은 달라 더 편하게 사용할 수 있는 spring-aop를 사용하는 것을 권장한다고 한다.
핵심기능과 부가기능
핵심기능 관점에서 봤을 때 부가기능은 게시판 대상 서비스의 getBoards() 메서드나 유저 대상 서비스의 getUsers() 메서드의 호출되는 시점 앞, 뒤로 before(), after()라는 공통 메서드를 사용해 부가기능을 처리하는 것이다. 이러한 before, after를 Cross-Cutting Concerns이라 할 수 있다.
AOP 용어 정리
Point-Cut은 어디에 끼워 넣는지를 나타내는 문법이다.
Point-Cut과 Cross-Cutting Concern을 합쳐서 Aspect, spring-aop에서는 Advisor라고도 한다. (Aspect가 일반적)
Runtime 시에 Aspect를 끼워 넣는 행위를 weaving이라고 한다.
Spring Boot AOP 설정 3가지
1. Spring Boot Starter AOP 라이브러리 설정
디펜던시에 추가 (gradle 기준)
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
implementation 'org.springframework.boot:spring-boot-starter-aop:2.6.4'
2. @EnableAspectJAutoProxy 적용
main 메서드를 갖는 Application 클래스에 어노테이션을 추가한다.
@EnableAspectJAutoProxy
@SpringBootApplication
public class ExApplication {
public static void main(String[] args) {
SpringApplication.run(ExApplication.class, args);
}
}
3. 공통기능 정의와 공통기능 사용될 시점 정의 (Advice or Aspect)
- @Aspect, @Component라는 어노테이션을 함께 붙여준다.
- Advice 어노테이션 @Before, @After, @Around 등의 어노테이션을 메서드 단위에 붙여준다.
- Advice 어노테이션에는 Point-cut 문법으로 부가기능을 적용할 대상을 설정한다.
- 부가기능의 적용 대상은 메서드 단위이다.
- 실제 부가기능을 수행하는 메서드를 Advice라고 부르기도 한다.
- Advice 어노테이션, Point-cut 문법, Advice 메서드를 통틀어 Aspect라고 한다.
UserStudyController의 모든 메서드를 호출하기 전에 testAOP 메서드를 호출하는 부가기능이다. 즉, Aspect이다.
AOP 보완 및 부연 설명
Aspect 대상 클래스의 Advice 메서드(setLog, logger)에 point-cut 문법으로 aop의 대상 메서드를 규정한다. point-cut에 설정된 위치(weaving 할 위치)의 메서드가 호출될 때 Aspect는 @Before, @Around과 같은 Advice 어노테이션을 기준으로 Aspect의 weaving 시점을 나타낸다. 실제 부가 기능으로는 Aspect의 어노테이션이 설정된 Advice 메서드(setLog, logger)가 동작하게 된다.
어노테이션 @Around가 세팅된 Advice 메서드를 보면 Parameter로 ProceedingJoinPoint가 인자로 사용된다. 중간에 Object result = pjp.proceed(); 를 기준으로 위 코드는 Before 시점에 수행하고, 아래 코드는 After 시점에 수행한다. return result를 했을 때 @Around로 동작한다고 한다.
※ AspectJ와 달리 spring-aop에서는 메서드 단위의 설정이 가능하고, 핵심 기능 메서드가 사용될 때 weaving 시점을 정할 수 있다.
Advice 종류
@Before (이전)
어드바이스 타깃 메서드가 호출되기 전에 어드바이스 기능을 수행
@After (이후)
타깃 메서드의 결과에 관계없이(즉 성공, 예외 관계없이) 타깃 메서드가 완료되면 어드바이스 기능을 수행
@AfterReturning (정상적 반환 이후)
타깃 메서드가 성공적으로 결괏값을 반환 후에 어드바이스 기능을 수행
@AfterThrowing (예외 발생 이후)
타깃 메서드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
Spring Boot에서 @AfterThrowing을 사용해서 aop 예외처리 기능으로도 사용한다.
@Around (메서드 실행 전후)
어드바이스가 타깃 메서드를 감싸서 타깃 메서드 호출 전과 후에 어드바이스 기능을 수행
스프링 AOP 구조
@Before("포인트 컷 표현식")
public void 어드바이스메소드() {
....
}
Point-cut 문법 설명 - 포인트 컷 표현식
@Around("execution (* com.project.blog.service.UserService.*(..))")
@Around : 어드바이스 어노테이션
execution : 포인트 컷 지정자
첫 번째 *은 return 타입을 나타낸다. *은 모든 리턴 타입을 대상으로 한다.
com.project.blog.service는 타깃이 되는 패키지 구조를 나타내고 그 뒤로는 클래스와 메서드의 위치를 가리킨다. 현재 위 표현식으로 볼 때 UserService 클래스의 모든 메서드에 @Around를 지정한다.
인자(argument) 타입(..)은 현재 모든 타입 인자를 허용한다.
UserService 클래스의 모든 메서드(*)에 @Around Advice 메서드를 적용한다.
@Before("execution(* com.project.blog.service.*.get*(..))") // point-cut
해당 패키지 위치의 모든 클래스에 get으로 시작하는 모든 메서드에 대해서 포인트 컷 @Before 시점에 Advice 메서드를 weaving 시킨다.
어노테이션을 이용한 AOP
Point-cut으로 aop 설정하는 방식이 아닌, 어노테이션을 만들어서 aop를 설정하는 방법도 있다. 특정한 메서드에 대해서만 aop를 적용하고자 할 때 point-cut으로 설정하기 난해한 경우에 사용하기 용이하다.
■ 어노테이션 작성
package com.project.blog.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TokenRequired {
}
TokenRequired 어노테이션을 생성한다.
■ 토큰을 이용한 어드바이스
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(tokenRequired)")
public void tokenRequiredWithAnnotation(TokenRequired tokenRequired)
throws Throwable {
System.out.println("Before tokenRequiredWithAnnotation");
}
}
인자에 설정된 변수명(tokenRequired)을 Advice 어노테이션의 경로에 @annotation(tokenRequired)로 세팅해주면,
위에서 생성한 어노테이션(@TokenRequired)을 사용한 핵심 기능의 메서드를 호출할 때 weaving 하는 Aspect가 세팅된다.
■ 어노테이션을 이용한 Advice 적용
접속 URL이 http://localhost:8080/으로 들어오는 경우에 @TokenRequired를 걸어두었으니, 위에서 설정한 aop로 로그를 출력하는 것을 확인할 수 있을 것이다.
http://localhost:8080/ 호출 후 로그 확인
JWT인증을 AOP로 적용하기(응용 편)
Advice
실제 토큰이 인증되면 메서드를 실행하고, 그렇지 않으면 해당 메서드를 실행하지 않고 예외를 던지는 기능
package com.project.blog.advice;
import com.project.blog.annotation.TokenRequired;
import com.project.blog.service.SecurityServiceImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
@Aspect
@Component
public class SecurityAspect {
// JWT인증을 AOP로 적용하기
// TokenRequired 어노테이션이 필요한 경우의 Advice
@Before("@annotation(tokenRequired)")
public void tokenRequiredWithAnnotation(TokenRequired tokenRequired)
throws Throwable {
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// checks for token in request header
String tokenInheader = request.getHeader("token");
if(StringUtils.isEmpty(tokenInheader)) {
throw new IllegalArgumentException("Empty token");
}
Claims claime = Jwts.parser().setSigningKey(DatatypeConverter
.parseBase64Binary(SecurityServiceImpl.secretKey))
.parseClaimsJws(tokenInheader).getBody();
if(claime == null || claime.getSubject() == null) {
throw new IllegalArgumentException("Token Error : Claim is null");
}
if(!claime.getSubject().equalsIgnoreCase("kim")) {
throw new IllegalArgumentException("Subject doesn't match in the token");
}
//System.out.println("Before tokenRequiredWithAnnotation");
}
}
위에서 만든 코드(SecurityAspect)를 수정하여서 @TokenRequired 어노테이션을 사용하는 메서드에 대해서 위 해당하는 Aspect를 실행한다. 예외가 발생하면 예외를 던지고, 정상적인 경우 호출하는 핵심기능이 실행된다.
token 없는 요청
이전에 만든 토큰 생성 코드로 토큰을 생성한다.
header에 token을 담아서 @TokenRequired 어노테이션을 사용하는 메서드를 호출하면 다음과 같이 정상적인 return 메시지를 받을 수 있다.
위에서는 "http://localhost:8080/" 요청에 대한 Controller의 메서드에 걸어두었고 해당 return 메시지는 내가 세팅한 것이므로 정상적으로 출력된 것을 확인할 수 있다.
Advice 종류 정리 (Advice Annotation)
- @Before - 어드바이스 타깃 메서드가 호출되기 전에 어드바이스 기능을 수행
- @After - 타깃 메서드의 결과에 관계없이(즉 성공, 예외 관계없이) 타깃 메서드가 완료되면 어드바이스 기능을 수행
- @AfterReturning - 타깃 메서드가 성공적으로 결괏값을 반환 후에 어드바이스 기능을 수행
- @AfterThrowing - 타깃 메서드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
- @Around - 어드바이스가 타깃 메서드를 감싸서 타깃 메서드 호출 전과 후에 어드바이스 기능을 수행
정리
- AOP는 핵심기능에서 부가기능을 분리해서 모듈화 하는 프로그래밍 기법을 말한다.
- AOP는 핵심기능(Core Concerns)과 부가기능 (Cross Cutting Concerns)을 분리하여 개발할 수 있게 해 준다.
- 부가기능에 해당하는 코드인 어드바이스와 적용한 위치를 문법적으로 표현한 포인트 컷을 합친 개념이 Aspect이다.
- 스프링 AOP는 메서드 조인 포인트만 지원하여 간단하게 쉽게 AOP를 구현할 수 있다.
- 포인트 컷의 문법을 이용하여 원하는 위치에 부가기능 어드바이스를 추가할 수 있다.
- Advice는 타깃 객체 이전과 이후 그리고 이전/이후 모두에 적용 가능하다.
도움이 되셨다면 광고 꾹!
감사합니다.^^
'java, spring' 카테고리의 다른 글
JPA 맛보기 (0) | 2022.06.10 |
---|---|
[Spring Boot] 로그와 예외처리 (0) | 2022.03.19 |
[Spring Boot] RESTful 웹서비스 테스트 (0) | 2022.03.17 |
[Spring Boot] REST API, CRUD 구현 (0) | 2022.03.17 |
[SpringBoot] API Security and JWT (0) | 2022.03.15 |