본문 바로가기
Backend/Spring

[Spring] AOP 사용 방법 (예제 코드)

by 볼링치는 개발자 2021. 9. 9.
반응형

흔히 스프링의 특징으로 DI, IoC, POJO, AOP 등을 대부분 언급합니다.

 

AOP는 Aspect Oriented Programming의 약자입니다. 한글로 하면 관점 지향 프로그래밍이죠.

 

하지만 이름만 들어서는 AOP에 대해 제대로 알 수가 없고 예측 조차 힘들었습니다.

 

AOP에 대한 개념적인 내용은 다음 포스팅에 작성하였으니 참고하여주세요.

https://programforlife.tistory.com/103

 

[Spring] Spring 기초

이번 포스팅에서는 인턴을 하게 된 회사에서 진행해준 신입사원 교육 중, Spring의 기초에 대해 정리해보려 합니다. Spring Boot로 프로젝트를 진행했던 경험이 있어서 Spring의 특징에 대해 어느 정도

programforlife.tistory.com

 

이번 포스팅에서는 AOP에 대한 개념적인 내용보다는 실제로 코드로 어떻게 작성하는지 정리해보려고 합니다.


AOP 시작하기

 

먼저 AOP는 스프링의 특성이기 때문에 스프링 프로젝트를 준비하고, build.gradle 파일에 의존성을 추가해줍니다.

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

먼저 AOP를 사용하기 위해서는 다음과 같은 Annotation들을 알아야 합니다.

@Aspect AOP로 정의하는 클래스를 지정함
@Pointcut AOP기능을 메소드, Annotation 등 어디에 적용시킬지 지점을 설정
지점을 설정하기 위한 수식들이 매우 많음
@Before 메소드 실행하기 이전
@After 메소드가 성공적으로 실행 후 (예외 발생 되더라도 실행 됨)
@AfterReturning 메소드가 정상적으로 종료될때
@AfterThrowing 메소드에서 예외가 발생할때
@Around Before + After 모두 제어 (에외 발생 되더라도 실행 됨)

AOP는 공통적으로 관심 있는 기능을 구현할 때 사용합니다.

 

하나의 서버에는 여러개의 메서드가 있기 마련인데, 로그를 찍는다던가, 들어오는 매개변수와 리턴되는 결과를 보고 싶다던가, 메서드 실행 시간을 알고 싶은 경우에, 모든 메서드에 해당 기능들 코드를 작성하면 코드가 길어지고 가독성이 안 좋아질 뿐 아니라, 사람이 하다 보면 실수도 하기 마련입니다.

 

예를 들어 제 서버의 컨트롤러에 get, post 메서드가 두 개가 있는데 이 두 개의 메서드에 어떤 값이 매개변수로 들어가고, 어떤 값이 리턴되는지 알고 싶으면 다음과 같이 코딩하는 게 일반적인 생각입니다.

@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
	System.out.println("Get Method가 실행됨!");
	System.out.println("Get Method {id}: " + id);
	System.out.println("Get Method {name}: " + name);

	//서비스 로직
    
	return id + " " + name;
}

@PostMapping("/post")
public User post(@RequestBody User user) {
	System.out.println("Post Method가 실행됨!");
   	System.out.println("Post Method {user}: " + user);
        
	//서비스 로직
        
	return user;
}

서비스 로직을 제외하고 메서드의 시작 부분에 어떤 메서드가 실행됐는지, 들어온 매개변수 값은 어떤 건지 출력하는 코드가 추가되었습니다.

 

이를 AOP로 분리시켜 보겠습니다.

 

컨트롤러의 매개변수를 로그에 찍어주는 ParameterAop 클래스를 다음과 같이 작성했습니다.

ParameterAop에 대한 상세한 설명은 코드에 주석으로 남겼습니다.

@Aspect
@Component
public class ParameterAop {

    //com/example/aop/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut() {}

    //cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
    @Before("cut()")
    public void before(JoinPoint joinPoint) {
		
        //실행되는 함수 이름을 가져오고 출력
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName() + "메서드 실행");

        //메서드에 들어가는 매개변수 배열을 읽어옴
        Object[] args = joinPoint.getArgs();
		
        //매개변수 배열의 종류와 값을 출력
        for(Object obj : args) {
            System.out.println("type : "+obj.getClass().getSimpleName());
            System.out.println("value : "+obj);
        }
    }

    //cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
    //@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
    @AfterReturning(value = "cut()", returning = "obj")
    public void afterReturn(JoinPoint joinPoint, Object obj) {
        System.out.println("return obj");
        System.out.println(obj);
    }
}

이렇게 AOP 클래스를 구현하고, 컨트롤러에 로그를 찍어주는 모든 코드를 삭제하고 다음과 같이 수정해 주었습니다.

@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
	//서비스 로직
	return id + " " + name;
}

@PostMapping("/post")
public User post(@RequestBody User user) {
	//서비스 로직
	return user;
}

아까보다 코드가 훨씬 간결해 지고 가독성도 높아졌습니다.

이 상태에서 GET 방식으로 request를 보내면 다음과 같이 출력이 됩니다.

POST 방식으로 request를 보내면 다음과 같이 출력이 됩니다.

AOP 클래스를 작성하여 컨트롤러에 로그를 찍는 공통적인 부분을 제거하여 코드의 가독성을 높이고, 실수를 줄이는 효과를 확인할 수 있습니다.

또한, AOP클래스를 한번만 작성하여 여러 개의 메서드에 적용할 수 있다는 장점도 있습니다.


사용자 지정 Annotation AOP

이렇게 메서드의 매개변수와 리턴 값에 대한 로깅을 남기는 AOP를 작성해보았는데, 이번에는 메서드 실행 시간에 대해 로그를 찍어주는 AOP 클래스를 작성해보겠습니다.

 

이번에는 단순히 클래스를 작성하는 것이 아닌 사용자 지정 어노테이션을 만들어서 해당 어노테이션이 붙어있는 메서드에서 AOP 기능을 수행하도록 코드를 작성해 보겠습니다.

 

먼저 com.example.aop.annotation 패키지를 생성하고 Timer라는 어노테이션 파일을 만들어 줍니다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {}

그리고 다음과 같이 TimerAop 클래스를 작성해줍니다.

@Aspect
@Component
public class TimerAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut() {}
	
    //사용자 지정 어노테이션이 붙은 메서드에도 적용!
    @Pointcut("@annotation(com.example.aop.annotation.Timer)")
    private void enableTimer() {}

    //메서드 전 후로 시간 측정 시작하고 멈추려면 Before, AfterReturning으로는 시간을 공유 할 수가 없음 Around 사용!
    @Around("cut() && enableTimer()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {

        //메서드 시작 전
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        //메서드가 실행되는 지점
        Object result = joinPoint.proceed();

        //메서드 종료 후
        stopWatch.stop();

        System.out.println("총 걸린 시간: " + stopWatch.getTotalTimeSeconds());
    }
}

 

그리고 컨트롤러에서 3초 걸리는 로직을 갖고 있다고 가정한 delete() 메서드를 작성해 줍니다.

아까 생성해준 Timer 어노테이션도 붙여서 작성해줍니다.

@Timer
@DeleteMapping("/delete")
public void delete() {

	//삭제 서비스 로직: 소요시간 3초
	try {
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

그리고 해당 API로 request를 보내면 다음과 같은 결과를 볼 수 있습니다.

시간 측정 AOP를 작성하기 이전에 작성한 메서드 이름, 리턴 값 등을 출력하는 AOP도 같이 적용되어 나왔습니다.


값을 변경하는 AOP

여태까지 스프링 AOP를 공부하면서 공통 부분으로는 로그를 찍는 부분, 실행시간을 측정하는 부분 정도로 예시들이 많았습니다.

값을 변경하는데에는 AOP를 왜, 어떻게 사용하지? 에 대한 의문점이 있었습니다.

 

값을 변경하는 AOP는 좀더 비즈니스 로직에 집중하게 해 줍니다.

들어오는 값에 대해 동일한 전처리나 가공이 필요하면 AOP로 구현할 수 있습니다.

 

Filter나 Interceptor로 변환할 수는 있지만 스프링 부트의 내장 서버인 톰캣은 Body를 한번 읽으면 다시 못 읽게 막아놨기 때문에 변환하기가 어렵습니다.

 

하지만 AOP를 적용하면, 들어오는 객체나 값은 filter와 interceptor를 지나서 객체화돼서 왔기 때문에 값을 변환해주기 비교적 쉽습니다.

 

혹은 암호화된 데이터가 들어오면 코드에서 복호화하지 않고 AOP에서 복호화 완료된 상태로 받을 수 있으며, 그 역의 작업도 가능합니다.

 

혹은 가공된 데이터를 보낼 때 특정 회원들이나 특정 서버에 보내고 싶을 때도 AOP를 사용할 수 있습니다.

 

데이터를 주고받을 때 암호화된 상태로 받는데, 코드에서 복호화하지 않고 AOP에서 복호화되어 컨트롤러 메서드로 떨어질 수 있게 AOP를 작성해 보겠습니다.

 

먼저 com.example.aop.annotation 패키지에 Decode라는 어노테이션 파일을 만들어 줍니다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {}

그리고 DecodeAop코드를 작성해줍니다.

@Aspect
@Component
public class DecodeAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut() {}

    @Pointcut("@annotation(com.example.aop.annotation.Decode)")
    private void enableDecode() {}
	
    //암호화된 데이터 들어오면 복호화!
    @Before("cut() && enableDecode()")
    public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
        
        //메서드로 들어오는 매개변수들
        Object[] args = joinPoint.getArgs();

        for(Object arg : args) {
            if(arg instanceof User)  {
            	//클래스 캐스팅
                User user = User.class.cast(arg);
                String base64Email = user.getEmail();
                //복호화
                String email = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
                user.setEmail(email);
            }
        }
    }
	
    //리턴할때는 암호화 하여 리턴
    @AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
    public void afterReturn(JoinPoint joinPoint, Object returnObj) {

        if(returnObj instanceof User) {
       		//클래스 캐스팅
            User user = User.class.cast(returnObj);
            String email = user.getEmail();
            //암호화
            String base64Email = Base64.getEncoder().encodeToString(email.getBytes());
            user.setEmail(base64Email);
        }
    }
}

그리고 ApiController에 put() 메서드를 작성하겠습니다.

@Decode
@PutMapping("/put")
public User put(@RequestBody User user) {
	//서비스 로직
	return user;
}

그럼 다음과 같은 Body를 가진 PUT request를 보내보겠습니다.

결과로 다음과 같이 request와 같은 객체가 돌아오는 것을 볼 수 있습니다.

하지만 들어오는 객체와 리턴 객체를 로그에 출력해주는 AOP에 의해서 콘솔 창을 보면, 암호화되어 들어온 이메일이 복호화된 상태로 메서드에 들어오고, 리턴되는 객체에서 이메일을 DecodeAop에 의해 암호화되는 것을 확인할 수 있습니다.

반응형

댓글