TIL/Java | Spring Boot

[Spring Boot] AOP

yeon_zoo 2022. 1. 20. 21:45

0. AOP가 필요한 상황

만약 모든 메소드의 호출 시간을 알고 싶으면 어떻게 해야 할까?

 

MemberService에 정의된 각각의 메소드의 호출 시간을 정확히 알고 싶어졌다. 즉, 정확히 회원 가입과 회원 조회에 걸리는 시간을 찍어보고 싶은 것이다. AOP를 모르는 상황에서 이를 해결하기 위해서는 각 메소드마다 정의된 것을 조금씩 바꿔줘야 한다. 아래는 회원가입 메소드에 호출 시간을 계산하는 기능을 추가한 것이다. 

public class MemberService {
 /**
* 회원가입
*/
    public Long join(Member member) {
        long start = System.currentTimeMillis();
        try {
            validateDuplicateMember(member); //중복 회원 검증
           	memberRepository.save(member);
            return member.getId();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("join " + timeMs + "ms");
	} 
}

만약에 내 서비스가 1000개의 메소드가 있다면 위의 과정을 천 번 반복해줘야 한다. 또, 요구사항이 바뀌는 경우에는 1000번 바꿔줘야하는데, 코드의 가독성도 높지 않아서 유지보수하기가 쉽지 않다. 이런 경우의 문제점을 정리해보면 다음과 같다. 

 

  • 회원 가입과 회원 조회에 걸리는 시간을 조회하는 기능은 핵심 관심 사항이 아니다. 
  • 시간을 측정하는 로직은 공통 관심 사항이다. 
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 어렵다. 
  • 시간을 측정하는 로직을 변경할 떄 모든 로직을 찾아가면서 변경해야 한다. 

이럴 때 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)을 사용할 수 있다. AOP는 공통 관심 사항과 핵심 관심 사항을 분리하는 것이라고 보면 된다. 

 


1. AOP 적용

AOP를 적용해주기 위해서 다음과 같은 디렉토리에 aop 패키지를 만들고 TimeTraceAop 클래스를 생성했다.

TimeTraceAop는 다음과 같은 코드들을 적어주었다.

package hello.hellospring.aop;

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


@Component
//Spring bean 으로 등록을 해줘야 함 @Component(컴포넌트 스캔)을 사용하기도 하지만, 컨테이너에 직접 등록해주는 것을 선호. (정형화된 Repository 같은 게 아니기 때문에 명시해주는 게 더 좋음)
@Aspect //AOP에는 Aspect라고 적어줘야 함.
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString()); //joinPoint의 메소드들을 이용해서 원하는 것을 마음대로 조작도 가능!!
        try {
            //다음 메소드로 진행이 된다.
            return joinPoint.proceed(); //이런 조건이면 다음으로 넘어가지마! 이런 것도 설정이 가능함.
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
        }


    }
}

Aop를 적용할 클래스 위에는 @Aspect 어노테이션을 적어주어야 한다. 그리고는 ProceedingJoinPoint를 넘겨주고, 해당 클래스에 정의된 메소드들을 이용하여 사용할 기능들을 정의할 수 있다. 이 때, 해당 메소드 위에 @Around() 를 통해 적용 범위를 설정해 줄 수 있다.

@Around("execution(* hello.hellospring..*(..))")

위와 같은 코드로는 모든 코드에 적용해줬다. 이렇게 하고 해당 AOP를 스프링 빈으로 등록해줘야 한다. 지금은 우선 컴포넌트 스캔 방식으로 등록해주었다. 등록한 후에 스프링 서버를 켜고 회원 가입 기능을 실행해보면 모든 기능(Controller, Service, Repository 전체)에 시간이 얼마나 걸렸는지를 확인해 볼 수 있다. 

 

AOP 방식을 이용하면

  • 회원가입, 회원 조회 등 핵심 관심 사항과 시간을 측정하는 공통 관심사항을 분리할 수 있다. 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다. 
  • 변경이 필요하면 AOP가 적용된 클래스만 수정하면 된다.
  • 원하는 적용 대상을 선택할 수 있다.

이런 장점들이 있다. 

 

AOP가 어떻게 작동하는지 알아보면 다음과 같다. AOP를 적용하기 전에 스프링 컨테이너 내의 의존관계는 다음과 같다. 

이후에 AOP를 적용하면 이렇게 프록시에 의존관계가 먼저 생성이 된다. 따라서 memberController에서는 실제 memberService보다 먼저 AOP가 적용된 프록시를 불러온다. 

특히 예시에서 사용한 AOP의 경우에는 컨트롤러, 서비스, 리포지토리 전체에 프록시를 생성한 것으로 다음 그림과 같은 구조를 가지고 있다고 보면 된다. 

만약 DI가 불가능했다면 AOP를 사용하기도 어려웠을 텐데, DI 덕에 이런 식으로 쉽게 의존 관계를 설정할 수 있게 된 것이다. 즉, @Around() 어노테이션에 따라서 적용된 빈들은 해당 빈에 대한 의존관계를 설정할 때 프록시(가짜)랑 먼저 의존관계를 설정할 수 있게 된다. 


이렇게 해서 인프런에서 스프링 입문 강의를 들어보았다. 강의 시간 자체는 길지 않았지만, 스프링을 처음 접해보았고, 자바 자체도 완전히 익숙한 상태는 아니라서 더 시간이 오래 걸린 것 같다. 다음 강의부터는 좀 더 속도를 낼 수 있지 않을까 싶은.. 희망이 있다. 이외에도 projectlion 강의와 패스트 캠퍼스의 스프링 부트 강의를 듣고 있기 떄문에 입문 수준에서 필요한 추가 개념 사항들을 정리할 예정이다.