[섹션 7. AOP] AOP가 필요한 상황 & AOP 적용/ 인프런 김영한 스프링 입문
이번 시간에는 AOP에 대하여 필요한 상황을 예시로 들고, 그것을 적용해보는 시간을 가졌다.
AOP가 필요한 상황이 예시로 주어졌는데, 만약에 모든 메소드의 호출 시간을 측정하고 싶다면?
메소드가 1,000개가 있다고 가정할 때, 일일이 시간을 측정하는 로직을 넣어야 하는가?
그래서 먼저 메소드에 직접 시간측정 로직을 넣는다고 해보자.
스프링 컨테이너에서 helloController, memberService, memberRepository 등..
클래스들을 들어가서 각 메소드에 초단위 시간 측정 로직을 시작과 끝 사이에 넣었다.
그런데, 초단위 로직에서 밀리세컨드 단위로 수정하고 싶으면 또 전부 다 들어가서 수정해야 하는가?
난감한 상황;;ㅋㅋㅋㅋ
어쨌든 직접 구현을 시작해보자~~!
회원가입 메소드에서 start 시간을 찍고, finish 시간을 찍고 나서,
finish - start 시간을 구하면 소요 시간이 나오고,
println으로 콘솔에 로그를 찍는 코드를 넣고 테스트를 돌려봤다.
그러면, 위와 같이 회원가입하는데 걸린 시간을 구할 수 있다.
근데, 이런거를 1000개를 반복해서 해야된다고...?ㅋㅋㅋㅋ
이렇게 시간을 측정하는 기능은 핵심 관심 사항이 아니고, 공통 관심 사항(cross-cutting concern)이라고 한다.
공통적으로 여러 메소드에 시간을 측정하는 공통 기능이기 때문이다.
핵심 비즈니스 로직이 핵심 관심 사항(core concern)이라고 한다.
위의 코드 사진을 보듯이, 핵심 관심 사항과 공통 관심 사항이 섞여있기 때문에 유지보수가 어려움ㅜㅜ
이런 문제를 해결하는 기술을 AOP(Aspect Oriented Programming) 라고 하며, 관점 지향 프로그래밍 이라고도 한다.
AOP는 공통 관심 사항이랑 핵심 관심 사항을 분리하는 기술을 말한다.
AOP는 공통 관심 사항인 시간 측정 로직을 따로 빼서, 원하는 곳에 공통 관심 사항을 적용하는 기술이다.
- AOP 구현
aop패키지를 생성하고, TimeTraceAop 클래스를 생성해준다.
AOP를 구현하기 위해 Aspect 어노테이션을 추가해준다.
그리고 예외가 발생할것을 대비해 메소드에 throws Throwable을 추가해서 예외를 던져준다.
jointPoint.proceed를 넣어주면, 다음 메소드로 진행이 된다.
jointPoint.toString을 넣어주면, 여기안에 어떤 메소드를 콜했는지 이름을 얻을 수 있다.
(아래의 콘솔 로그사진에서 확인 가능)
그리고 Around 어노테이션을 추가해서, AOP를 어디에 적용할지 타겟팅을 해줄 수 있게 한다.
Around는 정해진 문법이 있는데,
위의 사진처럼 hello.hellospring 패키지명을 넣고,
*부분은 클래스명을 넣는 부분인데, *는 클래스 전체를 선택하겠다 를 의미한다.
그리고 (..)는 파라미터 타입
이런식으로 원하는 조건을 넣을 수 있다.
그러니까, 입력된 코드는 hello.hellospring 패키지 하위에는 다 적용해라 라는 뜻이다.
joinPoint의 역할은 호출이 될 때마다, 중간에서 인터셉팅을 하고,
proceed를 입력해서 진행할지, 혹은 진행하길 원치 않으면 다른 메소드를 쓴다든지,
이런식으로 풀 수 있는 기술이 AOP임.
다른 방법으로는 위와 같이 스프링 빈에 추가해서 하는 방법도 있다.
@Component 어노테이션을 달아서 컴포넌트 스캔을 해도 되지만,
스프링 빈에 등록해서 쓰는것을 더 선호한다.
리포지토리 이런것들과 다르게, AOP는 정형화 된것이 아니기 때문에,
스프링 빈에 등록하면, SpringConfig에서 직접 보고 AOP 등록되어서 쓰이는구나~~ 인지하기 쉽기 때문에,
스프링 빈에 등록하는게 낫다.
하지만 실습은 컴포넌트 스캔으로 진행했다..ㅋㅋㅋㅋ
- AOP 테스트
테스트를 위해서 서버를 실행하고 회원 목록을 들어가본다.
그리고 콘솔창을 확인해보면, AOP가 정상적으로 적용된것을 확인할 수 있다!
execution들을 확인하면, 컨트롤러, 서비스, 리포지토리 순으로 들어간 것을 확인할 수 있고,
Hibernate가 뿌려준 쿼리가 보이고, 그 이후로 END로 나오는 과정까지 볼 수 있다.
이렇게 전체적으로 보면서 어디서 병목 현상이 발생하는지 파악할 수 있다.
이렇게 AOP를 통해서 핵심 관심 사항과 공통 관심 사항을 분리했기 때문에,
유지보수가 용이하고, 또한 원하는 적용 대상을 선택할 수 있게 된다.
@Around("execution(* hello.hellospring.service..*(..))")
이런식으로 문법을 적용하면 AOP가 service 패키지의 하위 폴더만 적용되게 된다.
- AOP 동작 방식
그림은 구글링을 통해 찾은 그림ㅋㅋㅋㅋ
helloController에서 memberService 호출할 때,
AOP를 적용하고, Around를 통해 어디에 적용할지 지정을 하면,
해당 메소드가 AOP가 있으면, 가짜 memberService, 즉 프록시 memberService를 만들어내서,
그림과 같이 가짜 스프링 빈을 먼저 호출하도록 앞에 세워둔다.
이 가짜 스프링 빈이 끝나면, joinPoint.proceed()를 실행하고 나서, 실제 memberService가 호출된다.
다시 말해, helloController가 호출하는건 진짜 memberService가 아닌,
프록시라는 기술로 생성된 가짜 memberService 인 것이다.
이 기술을 여러 클래스에 적용했을 때는,
스프링 컨테이너에서
프록시 helloController를 호출하고, 다음에 실제 helloController,
다음 순서인 프록시 memberSerivce를 호출하고, 실제 memberService,
프록시 memberRepository를 호출하고, 실제 memberRepository를 호출하는 순서로 진행된다.
AOP가 가능한 이유는 DI가 가능하기 때문이다.
컨테이너에서 스프링 빈을 관리하면, 프록시를 통해 가짜를 만들어서 DI를 실제 클래스에 해주면 된다.
예를 들어, helloController에서 memberService를 직접 new로 생성해서 한다면,
주입을 못하기 때문에 AOP가 불가능할 것이다.
이렇게 주입이 가능하기 때문에 AOP가 가능한 원리이다.
이 프록시 기술을 직접 보기 위해서 println을 찍어본다.
콘솔에 찍힌 로그를 보면,
MemberService로 끝나는게 아닌 뒤에 추가로 CGLIB, 즉, CG 라이브러리.
이 memberService를 가지고, 복제를 해서 코드를 조작하는 기술이다.
이렇게 프록시가 생성한 가짜 서비스 객체를 확인할 수 있다~~!