📌 Spring에서 AOP의 관점으로 '메서드의 실행 속도 측정 방식을 개선'하는 과정을 학습하고 정리한 내용입니다.
AOP은 로직에 관점을 부여하는 프로그래밍(Aspect Oriented Programming)입니다. 어떤 로직이 핵심 기능을 수행하며, 어떤 로직은 부가적인 기능을 수행하는지 관점을 부여해 분리하는 것입니다. 부가적인 로직은 공통으로 사용되는 경우와 그렇지 않은 경우로 나눠지기도 합니다.
예를 들어, Service에 회원 목록을 조회하는 findAll()
가 있습니다. 그리고 findAll()
이 실행될 때마다 메서드의 실행 속도를 측정하는 currentTimeMillis()
가 있습니다. 이 때 관점에 따라 핵심 로직을 findAll()
로, 부가적인 로직은 currentTimeMillis()
로 분류 할 수 있습니다.
AOP의 아이디어는 관점에 따라 분류가 가능한 로직을 모듈화하여 따로 관리하되, 공통적으로 사용이 가능한 로직은 재사용이 가능한 형태로 만들자는 의도에서 시작되었습니다.
- 모든 메소드의 호출 시간을 측정하고 싶다.
- 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)을 나누고 싶다.
기존의 시간 측정 방식은 System.currentTimeMillis()
를 이용하여 시작 시점과 종료 시점의 시간 차이를 이용했습니다. 측정하고자 하는 로직의 시작과 끝에 측정 기준점을 일일히 셋팅해야 하므로, 측정 항목이 늘어날 수록 반복되는 코드 사용도 비례합니다. 또한, 시간 측정 로직과 비즈니스 로직이 혼재한다는 문제도 있습니다.
이러한 방식은 이후 시간 측정 로직의 변경이 필요할 경우, 모든 로직을 찾아다니며 하나하나 변경해야 하기 때문에 유지보수의 비효율을 초래합니다.
AOP(Aspect Oriented Programming)의 등장 배경에는 공통 관심 사항(cross-cutting concern)과 핵심 관심 사항(core concern)의 분리'라는 아이디어가 있었습니다.
공통 관심 사항에 해당하는 로직을 모듈로 분리하고, 자동화된 세팅 방법을 제공하므로 기존의 측정 방식이 가졌던 단점을 모두 해결했습니다.
AOP를 이용하면 아래 구조도와 같이 공통적으로 사용해야 하는 로직
과 회원 가입/회원 조회 등과 같은 핵심 로직
이 분리된 상태에서 함께 사용할 수 있습니다. 모듈화되어 있으므로 코드의 수정이 편리하며, @Around를 이용하여 간단하게 적용 대상을 설정할 수 있습니다.
AOP 방식은 2가지 방법으로 구현할 수 있습니다. TimeTraceAop
클래스에 @Component
를 추가하여 컴포넌트 스캔을 이용하거나, Config
파일을 이용하여 스프링 빈에 직접 등록할 수 있습니다.
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))") // 패키지 하위에 모두 적용
public Object execut(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("Start: " + joinPoint.toString());
try {
return joinPoint.proceed(); // 다음 로직으로 넘어간다.
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("End: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
@Component
: 컴포넌트 스캔의 대상임을 명시합니다.
@Around("execution(* hello.hellospring..*(..))")
: hellospring 패키지 하위에 모두 적용합니다.
로컬 서버를 띄우고 메서드가 호출되도록 기능을 조작해 보면 아래와 같이 TimeTraceAop
클래스에 구현된 로직이 모든 메서드마다 실행되는 것을 확인할 수 있습니다.
이렇게 메서드가 실행되는 속도를 실시간으로 모니터링 할 수 있기 때문에 병목이 발생하는 지점을 쉽게 파악할 수 있습니다.
(*참고: Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_
는 JPA가 자동을 만드는 쿼리문이다.)
Controller
, Service
, Repository
와 같이 정형화된 패턴이 아닐 경우, Config
에 등록하여 사용하는 것을 권장합니다.
정형화 된 파일이 아니라면 동료 개발자가 스프링 빈으로 관리되고 있는 파일을 인지하지 못할 수 있습니다. Config
파일을 이용하면 그 자체로 스프링 빈에 등록되어 사용중임을 드러낼 수 있다는 장점이 있습니다.
Config
등록 시 아래와 같이 @Bean
을 이용하여 TimeTraceAop
객체를 등록합니다.
@Bean
public TimeTraceAop timeTraceAop() {
return new TimeTraceAop();
}
AOP가 @Component
방식 또는 Config
파일을 통해 스프링 빈으로 등록되면, 기존의 Controller
가 Service
를 의존 했던 관계 사이에 유사 Service
객체가 생성됩니다.
유사 Service
는 프록시를 이용해 만들어집니다. 스프링 컨테이너가 최초 실행될 때 Controller는 프록시로 만들어진 유사 Service
객체를 먼저 실행합니다. 프록시가 대리, 대신이라는 의미를 지닌 것 처럼, 실제 Service가 동작하기 앞서 필요한 기능을 실행하고, 내부 로직을 따라 joinPoint.proceed()
가 실행되면, 그 때 실제 Service
가 동작합니다.
프록시로 만들어진 유사 Service
의 실체를 확인해 볼 수 있는 방법이 있습니다. Controller
의 생성자가 Service
를 주입받을 때 getClass()
를 이용하여, 클래스 정보를 출력하면 아래와 같이 프록시로 만들어진 유사 Service
객체의 정보를 확인할 수 있습니다.
출력된 클래스 정보 중 보이는 CGLIB
가 Service
를 복제하여 유사 Service
를 만들고, 코드를 조작하는 기술에 해당합니다.