이 포스트는 김영한 이사님의 스프링 입문 강의를 듣고 작성하였습니다.
이번엔 AOP에 대해서 간단하게 알아보고자 한다. 강의에서는 실제 예시를 위주로 설명을 하기 때문에 개념적인 부분은 이 글을 참고하자. 이 부분도 다룰게 많을 것 같아 추후에 따로 다뤄보려고 한다.
우리가 알고 있는대로 메소드의 호출 시간을 측정하는 로직을 넣으려면 다음과 같이 해야될 것이다.
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");
}
}
회원 가입 로직의 메소드 호출 시간을 알아보기 위해서 시작 시간과 끝 시간을 측정해서 그 두개의 값을 빼서 계산한 것이다. 이렇게 수정하고 테스트를 돌려보면 다음과 같이 시간이 측정되서 로그로 출력될 것이다.
MemberService안에 존재하는 findMembers나 findOne과 같은 메소드에 대해서도 시간 측정을 하려면 위와 동일하게 코드를 짜면 된다.
하지만 위와 같은 메소드가 적은 경우가 아닌 측정할 메소드가 경우에 따라 수백개, 수천개가 된다면 어떻게 될까?
위와 같은 방법을 사용하려면 수많은 메소드들을 다 뜯어서 시작시간과 종료시간을 측정하는 로직을 넣어야 될 것이다.
문제는 여기서 발생한다.
문제점
- 회원가입, 회원 조회에 시간을 측정하는 기능은
핵심 관심 사항이 아니다.- 시간을 측정하는 로직은
공통 관심 사항이다.- 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.
즉, 위와 같이 구현을 할 경우 핵심 관심사항(core concern) 과 공통 관심 사항 (cross-cutting concern)이 뒤엉켜 코드가 뒤죽박죽 될수 있다는 것이다.
이를 극복하기 위해서 어떻게 해야할까??
AOP에 대한 사전적 정의는 다음과 같다.
AOP
컴퓨팅에서 관점 지향 프로그래밍(aspect-oriented programming, AOP)은 횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다.
위키백과
쉽게 말해서 공통 관심사항과 핵심 관심사항을 분리하여 모듈성을 증가시키는 프로그래밍 페러다임이라고 생각할 수 있을 것이다.
위 문제에서 핵심 관심사항은 멤버를 생성하고 조회하는 기능을 말할테고 시간측정은 공통 관심사항이 되는 것이다.
package memberpractice.memberpractice.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TimeTraceAop {
@Around("execution(* memberpractice.memberpractice..*(..)) && !target(memberpractice.memberpractice.SpringConfig)\"")
public Object execute(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");
}
}
}
AOP를 사용하려면 @Aspect어노테이션을 붙여주어야 한다. 그리고 @Around 어노테이션을 통해서 AOP를 적용시킬 범위를 지정해주어야 한다. excution안에는 범위가 들어가있는데 위와 같이 할 경우 패키지의 하위에 모두 적용된다. 코드 내부에는 우리가 앞서 만들었던 시간 측정 로직이 들어가 있다.
AOP도 마찬가지로 스프링 빈으로 등록해주어야 한다. 이때 AOP의 경우 정형화 된 것이 아닌 조금 특수한 성격을 띄기 때문에 컴포넌트 스캔 방식 보다는 명시적으로 @Configuration밑에 직접 등록하는 것이 좋다.
package memberpractice.memberpractice;
import memberpractice.memberpractice.aop.TimeTraceAop;
import memberpractice.memberpractice.repository.*;
import memberpractice.memberpractice.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository);
}
@Bean
public TimeTraceAop timeTraceAop(){
return new TimeTraceAop();
}
// @Bean
// public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
// return new JpaMemberRepository(em);
// }
}
참고
AOP를 스프링에 직접 등록할 때 주의할 사항으로 탐색 범위에서SpringConfig는 빼고 진행하여야 한다. 왜냐하면 탐색 대상에SpringConfig도 포함된다면 의존관계가 생성되는 과정에서순환참조가 발생하기 때문이다.
서버를 실행하고 동작을 수행했을 때 다음과 같이 우리가 TimeTraceAop에서 구현한 형태로 모든 메소드의 소요시간이 로그로 찍히는 것을 확인할 수 있다. 아래 화면은 회원 가입 후 조회를 하였을 때 발생한 로그이다.

해결
- 회원가입, 회원 조회등
핵심 관심사항과 시간을 측정하는공통 관심사항을 분리한다.- 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
핵심 관심 사항을 깔끔하게 유지할 수 있다.- 변경이 필요하면 이 로직만 변경하면 된다.
- 원하는 적용 대상을 선택할 수 있다.
이렇게 해서 핵심 관심사항을 건들 필요없이 공통 관심사항인 시간 측정을 AOP를 통해 적용시킬 수 있었다.
위의 그림을 살펴보면 이해할 수 있듯 AOP를 적용하면 memberController는 실제 memberService가 아닌 프록시로 인해 생겨난 가짜 memberService를 호출하게 된다. 그 후 가짜 memberService가 동작이 끝나고 나면 joinPoint.proceed()메소드를 통해 진짜 memberService를 호출해준다.
실제로 클래스를 찍어보면 memberService가 찍히는 것이 아닌 뒤에 CGLIB라는 라이브러리가 만들어준 가짜 memberService가 찍히는 것을 확인할 수 있다. 이 부분에 대해서는 추후에 자세히 다뤄볼 예정이다.
사실 이러한 점도 스프링의 큰 장점 중 하나라고 할 수 있는데 우리가 new연산을 통해서 객체를 생성하고 DI를 한다면 이렇게 AOP를 간단하게 하진 못했을 것이다. 그러나 스프링 빈으로 등록된 객체를 DI하여 사용한다면 이러한 AOP도 우리가 큰 힘을 들이지 않아도 쉽게 구현할 수 있게 되는 것이다.
이번 포스팅을 끝으로 김영한님의 스프링 입문 강의에 대한 정리를 끝마쳤다. 스프링에 대해서는 아에 제로베이스인 상태로 들었었는데 상당히 이해도 잘되고 특히 우리가 스프링에 대해 공부할 때 어떤 방향성을 가지고 공부해야할지 잡을 수 있도록 잘 도와주신 것 같다.
다음엔 JPA활용편으로 바로 실습부터 진행하며 스프링에 대해서 익숙해져보고자 한다.