본문 바로가기
Develop/Spring

[Spring] Spring AOP 이용하기

by 코딩의성지 2020. 4. 5.

하이 ~~!! 여러분 !!

 

오늘은 Spring 에서 아주 중요한 개념인 AOP (Aspect Oriented Programming) 에 대해 이야기 해보려한다.

 

딱봐도 쉽게 설명을 해줄꺼고 , 구현도 간단하게 해볼꺼다.

 

 

AOP

 

AOP는 비지니스 로직 등의 핵심 기능 (Core Concerns) 들과 로깅, 보안 , 트랜잭션 처리 등 핵심 기능을 도와주는 부가기능 (Cross-cutting Concerns) 으로 분리해서 모듈화 하는 것을 의미한다.

 

간단한 게시판이 있다고 가정해보자.

게시글을 읽고 쓰고, 답글을 읽고 쓰고, 회원가입하고 로그인하는 그런 아주 일반적인 어플리케이션이다.

 

그런데 이렇게 어플리케이션을 개발하다보면 로그도 남겨야하고 , 보안적인 이슈도 처리하는 등의 공통적인 부가기능들이 생겨나게 되는데.. 이런 것들을 모듈마다 구현해주면 코드가 굉장히 복잡해질것이다. 

 

우리는 이런 부가기능들을 핵심기능에서 분리하여 모듈화 할 수 있다.

이것이 바로 AOP(Aspect Oriented Programming) 이다.

 

일반적인 java에서 AOP를 하려면 굉장히 복잡하고 어렵다. 하지만 !! 우리가 지금 하고 있는게 무엇인가 !! 바로 Spring이다. Spring에서는 아주 쉽게 AOP를 구현할 수 있도록 Spring AOP를 제공하고 있다.

 

 

Spring AOP

Primary Concern은 우리가 흔히 아는 핵심기능이다. 다른말로는 Core Concern이라고도 한다. 여기에는 비지니스 로직만 구현된다. 

 

그리고 Cross- Cutting Concern 은 로깅, 보안 등의 기능을하는 부가 기능이다. 이건 Runtime 때 Core Concern과는 별도로 만들어진다. 그리고 Point Cut 이라는 문법이 있는데, 이건 Cross-Cutting Concern을 어디에 적용시키느냐의 위치정보가 들어있는 문법이다. Point Cut 과 Cross-Cutting Concern을 합쳐서 Aspect라고 한다. Spring에선 Adviser라고 부르기도한다. 그리고 실제 런타임 때 이를 핵심기능에 끼워넣는 걸 Weaving이라고 한다. 

 

 

Spring AOP 구현

 

자 이제.. 이론적인 공부는 다들 잘 마쳤으니. ㅎㅎ 이번에는 실제로 구현을 한번 해보도록하자.

 

일단 AOP를 구현하기 위해서는 아래 디펜던시를 pom.xml에 설정 해줘야한다.

 

1
2
3
4
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
 

 

그리고 Application 클래스에 아래의 어노테이션도 추가해주자.

 

@EnableAspectJAutoProxy

 

자 이렇게 해주면 준비 끝이다.

 

일단 로그 관련 코드를 한번 구현해보자~

 

 

LoggingAspect

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.springframework.stereotype.Component;
 
@Aspect
@Component
public class LoggingAspect {
 
    @Before("execution(* com.test.crudPjt.service.*.get*(..))")
    public void loggerBefore() {
        System.out.println("get으로 시작하는 메서드가 시작됩니다.");
    }
 
    @After("execution(* com.test.crudPjt.service.*.get*(..))")
    public void loggerAfter() {
        System.out.println("get으로 시작하는 메서드가 끝났습니다.");
    }
 
    public Object loggerAround(ProceedingJoinPoint pjp) throws Throwable {
        long beforeTimeMillis = System.currentTimeMillis();
        System.out.println("[UserController] 실행 시작 : "
                        +pjp.getSignature().getDeclaringTypeName() + "."
                        +pjp.getSignature().getName());
        Object result = pjp.proceed();
 
        long afterTimeMillis = System.currentTimeMillis() - beforeTimeMillis;
        System.out.println("[UserController] 실행 완료: " + afterTimeMillis + "밀리초 소요"
                        +pjp.getSignature().getDeclaringTypeName() + "."
                        +pjp.getSignature().getName());
 
        return result;
 
    }
 
 
}

그리 어렵지 않은 내용이다. Aspect도 여느 스프링 클래스들과 마찬가지로 POJO로 구성된다.

 

또 @Component 어노테이션을 보면 알듯이 이 Aspect는 어플리케이션이 서버에 올라갈때 같이 로딩된다.

Before은 메서드가 수행되기전, After는 메서드가 수행된후, Around는 전후 둘다 로깅을 남긴다. 

 

다른 어드바이스 들도 있다. 아래의 표를 참고해서 다음에 한번 꼭 구현해보길 권장한다.

 

종류

기능

@Before

메서드 호출 되기 이전에 어드바이스 기능 수행

@After

메서드 호출 된 이후에 어드바이스 기능 수행 ( 결과에 상관 x)

@AfterReturning

메서드가 성공적으로 리턴값 반환한 경우 어드바이스 기능 수행

@AfterThrowing

메서드 수행중 예외 발생 시 어드바이스 기능 수행

@Around

메서더 호출 전 / 후 어드바이스 기능 수행

 

Around에서 proceed라는 함수가 있는데 이걸 기준으로 전후를 나눈다.

 

그리고 각 어노테이션 뒤에 사용되는 문법은 어디로 끼워넣을지에 대한 위치정보가 들어가 있다.

나는 모든 service의 get이 들어가는 메서드에 대해 전후로 로그가 남도록 했고, 또 UserController가 기동 될때 로그가 남도록 했다. 

 

그리고 스프링 AOP 같은 경우는 메서드 호출에 대해서만 Weaving 할수 있다는 점 잘 기억하자.

 

실제로 어플리케이션을 기동하고 , PostMan 에서 get이 들어간 메서드를 호출하기 위해 URL을 날려봤다.

 

 

 

결과는 잘 나오고 ... 이제 Console 을 보면 ㅎㅎ...

이렇게 내가 남긴 로그가 잘 남는 것 알 수 있다. 로그남기는건 프로젝트가 커지면 커질 수록 중요하니 잘 ~ 기억하자.

 

다음은 보안 관련해서 Aspect를 사용하는 예도 한번 작성해보자.

 

우리가 토큰으로 인증하는 걸 저번 포스팅에서 배웠었는데 .. 이번에는 서비스의 각 메서드마다 토큰 인증이 필요한 메서드를 정해주는 걸 해볼거다.

 

토큰 인증하는 부분 기억안나시면 아래의 링크 보고 오자. 

https://devkingdom.tistory.com/113

 

[Spring] JWT 를 활용한 인증 구현

하이 ~~~ 여러분들 이전 포스팅에서 제가 Spring 에서 CRUD를 구현했었다. 이렇게 서비스를 제공하는 것도 중요하지만, 이러한 웹서비스에서는 인증을 어떻게 구현하느냐도 중요하다. 스프링에서는 Spring Securit..

devkingdom.tistory.com

 

SecurityAspect

 

먼저 바로 Aspect를 만들기 전에 아래의 어노테이션을 하나 만들어주자.

 

1
2
3
4
5
6
7
8
9
10
 
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenRequired {
}
 
 

 

 

그리고 해당 어노테이션의 기능을 SecurityAspect에 구현해주자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.stereotype.Component;
 
 
@Aspect
@Component
public class SecurityAspect {
 
    @Before("@annotation(tokenRequired)")
    public void authenticateWithToken(TokenRequired tokenRequired) {
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request =requestAttributes.getRequest();
 
        // request header에 있는 토큰 체크하기
        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token)) {
            throw new IllegalArgumentException("token is empty");
        }
        Claims claims = Jwts.parser().setSigningKey(
                DatatypeConverter.parseBase64Binary(
                        SecurityServiceImpl.SECRET_KEY))
                .parseClaimsJws(token).getBody();
        if (claims == null || claims.getSubject() == null) {
            throw new IllegalArgumentException("Token Error!! Claims are null!!");
        }
 
        System.out.println("토큰의 subject로 자체 인증해주세요.");
 
    }
 
}
 

 

기능은 간단하다. 이제 이 @TokenRequired라는 어노테이션이 딱 붙어 있으면!! 헤더에 토큰이 있는지 꺼내서 검증을 해주는 로직이 실행된다.

 

그리고 나서 메서드에 해당 어노테이션을 써주면 구현은 끝난다.

1
2
3
4
5
6
    @TokenRequired
    @GetMapping("/{userid}")
    public User getUserByUserId(@PathVariable  String userid) {
        return userService.getUserByUserId(userid);
    }
 
 

 

이것 역시 테스트해보면.. !

헤더에 토큰 없이 보내면 .. ㅎㅎ

 

이런 에러를 던지고 ! 

 

토큰을 발급받아 헤더에 넣어주면 ㅎㅎ 이렇게 잘 ~~ 수행되는걸 확인할 수 있다. ㅎㅎ

 

자~~.. 오늘은 여기까지 하도록 하겠다 ㅎㅎ 그럼 모두 즐거운 코딩하자.

반응형

댓글