1. AOP가 필요한 상황
백엔드 구현에서의 관심사항은 두 가지 사항으로 나뉜다.
공통 관심사항(cross-cutting concern)과 핵심 관심사항(core concern)으로 나뉘는데, 회원 가입, 회원 조회와 같은 비즈니스 로직과 같은 경우가 핵심 관심사항이고, 이러한 기능 외에 회원 가입이나 회원 조회의 기능을 실행하는데 걸리는 시간을 측정하는 것과 같은 기능은 공통 관심 사항으로 분류된다.
이렇게 모든 메서드에 대한 호출 시간을 측정하고 싶을 때, 우리는 기존 모든 메서드의 시작과 끝에 시간을 측정하는 코드를 추가하여 이를 구현해야 한다.
회원 가입과 회원 조회의 기능의 호출 시간을 구하기 위해서는 회원 서비스에 다음과 같은 코드를 작성해야 한다.
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
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");
}
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers(){
long start = System.currentTimeMillis();
try {
return memberRepository.findAll();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println(timeMs);
}
}
}
메소드 시작 전 start로 시간을 받고, finally에서 (회원가입 또는 회원 조회에 실패하더라도 시간을 잴 수 있게) 메소드가 끝난 시점 시간을 재서 두 시간의 차로 호출에 걸리는 시간을 측정하였다.
통합 테스트를 수행하여 동작을 확인할 수 있다.
테스트가 정상적으로 수행되고, 호출 시간 또한 출력되는 것을 확인할 수 있다.
웹 애플리케이션에서 직접 회원을 추가/조회하여 findMembers() 메소드의 호출 시간 또한 출력해 볼 수 있다.
위와 같은 방법으로 메소드에 코드를 추가하여 호출 시간을 구할 수 있지만, 여기에서 문제가 발생한다.
해당 기능은 공통 관심 사항으로, 이러한 핵심 관심사항이 아닌 코드와 비즈니스 로직 코드가 섞여 유지 보수가 어려워질 뿐만 아니라, 호출 시간을 구하는 코드를 추가한 것처럼 모든 로직을 찾아가며 코드를 추가, 변경하는데 시간이 많이 걸린다는 단점이 있다.
이러한 로직은 별도의 공통 로직으로 만들기도 매우 어려워 비즈니스 로직과 관련없는 개발에서 불필요한 시간 소모가 발생한다.
이러한 상황에서 AOP의 필요성을 느끼게 된다.
2. AOP 적용
AOP란 Aspect Oriendted Programming이란 의미로 이전 1번에서 말한 것처럼 시간을 측정하는 로직과 같은 공통 관심 사항(cross-cutting concern)과 비즈니스 로직인 핵심 관심 사항(core concern)으로 분리하여 개발하는 기술이다.
우리가 위에서 각 메소드의 시간을 측정하는 코드를 작성할 때 각각의 메소드에 코드를 입력하는 방식으로 개발하였다면, AOP를 사용하면 시간을 측정하는 코드를 따로 분리하여 개발할 수 있다.
다음은 1번에서 말했던 방법을 사용했을 때 스프링 컨테이너의 모습이다.
컨트롤러, 서비스, 레포지토리에서 각각 메소드를 호출할 때 시간 측정을 해야 하므로 시간 측정 로직이 각 과정에 작성되어야 했다.
이제 AOP를 사용했을 때의 컨테이너 모습이다.
시간 측정 로직(공통 관심 사항)을 분리하여 로직을 적용하기를 원하는 곳에만 적용할 수 있다.
이제 AOP를 사용하여 직접 로직을 분리 해본다.
먼저 이전에 작성했던 멤버 서비스의 시간 측정 로직을 지우고 프로젝트 아래에 aop폴더를 생성하고 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
@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());
try {
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString()+ " " + timeMs +
"ms");
}
}
}
execute()
메소드 의 내용 : 기존에 작성했던 시간을 측정하는 코드이다.
try문 안에 joinPoint.proceed()를 반환하는 것만 다른 점이다.
@Around애노테이션을 사용해 우리가 시간 측정 로직을 어떤 부분에 적용하고 싶은지 명시하고, 회원 가입, 회원 목록 조회 등의 메소드가 호출될 때마다 ProceedingJoinPoint 객체인 joinPoint가 넘겨오면서 execute()메소드가 호출된다.
해당 메소드가 호출되면 일단 시작 시간을 재고 출력을 하고, try문 안의 joinPoint.proceed()가 호출될 때 우리가 실행하길 원하는 로직이 수행된다. (회원가입, 회원 목록 조회)
메소드가 종료되면 finally에서 종료 시간을 재고, 시간을 측정해 출력한 뒤 execute()메소드가 종료된다.
이 메소드가 AOP 기능을 할 수 있게 하기 위해 @Aspect 애노테이션을 사용한다.
AOP 또한 스프링 빈으로 등록되어야 하기 때문에 컴포넌트 스캔을 위해 @Component로 스프링 빈을 등록한다.
(또는 Config 파일에서 직접 @Bean을 통해 스프링 빈으로 등록할 수 있으며, 실무에서는 이러한 방법을 더 선호한다. Config 파일의 확인을 통해 어떤 AOP가 적용되어 있는지 확인이 가능하기 때문이다.)
이제 웹 애플리케이션을 실행해서 시간 측정이 정상적으로 동작하는지 로그를 확인 해본다.
회원 가입 후 회원 목록으로 가입된 회원 확인까지 한 후의 로그이다.
컨트롤러, 서비스, 레포지토리 각각의 메소드가 호출될 때마다 시간 측정 메소드가 호출되어 START와 END를 출력한 것을 확인할 수 있다.
이렇게 AOP를 사용하여 시간 측정 로직을 분리했을 때 다음과 같은 문제가 해결되었다.
- 핵심 관심사항과 공통 관심사항을 분리하여 시간을 측정하는 로직을 별도의 공통 로직으로 만들 수 있었고 이로 인해 핵심 관심사항을 깔끔하게 유지할 수 있었다.
- 만약, 이러한 공통 관심사항 로직의 변경이 필요하면 이전에는 각 메소드에 추가했던 시간 측정 코드를 전부 찾아가 수정했다.
- 이제는 해당 AOP 로직 하나만 수정하여 변경할 수 있다.
- 또한, @Around를 사용하여 간편하게 원하는 적용 대상을 선택할 수 있다.
- 예를 들어 위 예시에서는 해당 프로젝트의 모든 메소드에 위 AOP를 적용했지만 만약 서비스의 메소드에만 이를 적용하고 싶다면 ( hello.hellospring.service.. (..)) 와 같이 이를 명시하여 적용 대상을 선택할 수 있다.
스프링 컨테이너에서의 의존관계를 확인하여 AOP의 동작 방식을 확인 해본다.
AOP 적용 전 의존관계이다.
회원 컨트롤러와 회원 서비스는 각 스프링 빈으로 컨테이너에 등록되어있고, 회원 컨트롤러는 회원 서비스를 의존하고 있다.
다음은 AOP 적용 후 의존관계이다.
실제로 AOP를 적용하면 스프링은 회원 서비스가 스프링 빈으로 등록될 때 프록시(Proxy) 회원 서비스(가짜 회원 서비스)를 앞에 같이 세운다. 회원 컨트롤러에서 회원 서비스가 호출될 때 실제 회원 서비스가 아닌 프록시 회원 서비스가 호출되고, joinPoint.proceed()
가 호출되면 실제 회원 서비스의 메소드들이 동작한다.
(참고로 이러한 방식은 회원 컨트롤러에서 회원 서비스를 주입할 때 실제 회원 서비스의 코드를 복제하여 이러한 방식으로 동작할 수 있도록 스프링에서 조작하기 때문에 가능하다.)
AOP를 적용하기 전후의 전체 모습을 확인해보자.
@Around를 통해 프로젝트 전체 메소드에 AOP가 적용되었기 때문에 컨테이너에 스프링 빈으로 등록되는 것들을 모두 프록시와 같이 등록된 것을 확인할 수 있다.
이러한 방식을 스프링에서는 프록시(Proxy) 방식의 AOP라고 하며, 실제 코드를 통해 proxy가 주입되는지 출력으로 확인해보자.
간단히 기존의 MemberController의 생성자에 다음과 같은 코드를 추가하고 실행을 통해 확인해보자.
// 생성자 주입 (최근 많이 사용)
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
System.out.println("memberService = "+memberService.getClass());
}
콘솔 창을 확인하면 memberService = class hello.hellospring.service.MemberService$$~
과 같은 추가적인 것을 출력하는 것을 확인할 수 있다.
이는 실제로 스프링 컨테이너에서 프록시 서비스를 만들어 이를 주입한 것임을 알 수 있다.
참고 자료
'공부 및 활동 > 스프링 강의 정리' 카테고리의 다른 글
[스프링 입문] 6. 스프링 DB 접근 기술 2 (0) | 2021.09.27 |
---|---|
[스프링 입문] 6. 스프링 DB 접근 기술 1 (0) | 2021.09.27 |
[스프링 입문] 5. 회원 관리 예제 - 웹 MVC 개발 (0) | 2021.09.26 |
[스프링 입문] 4. 스프링 빈과 의존관계 (0) | 2021.09.26 |
(회원 관리 예제) 회원 서비스 테스트 (0) | 2021.09.26 |
댓글