이제 스프링의 꽃! AOP 에 대해서 공부해보겠습니다!
이번 강의까지해서 스프링에 대해서 전체적으로 맛보기는 다 끝날 것 같습니다. 아마 다음부터는 강의 들어면서 정리도 하고 실제로 만들어보기도 하고 천천히 해보려고 합니다!
1. AOP 없이 시간 측정 메서드 1000개 만들기!
AOP 에 대해서 설명하기 위해 한 가지 예시를 들어볼까 한다.
제가 담당하는 서비스의 메서드는 총 1000개 정도 입니다.
PM 님이 제게 오셔서 말씀하셨습니다. "오늘까지 모든 메서드의 호출 시간을 측정할 수 있도록 만들고 적용해놓거라"
그러자 "오늘까지요? 대충 1000개인데..날밤 새야하는데요...ㅠㅠ" 라고 감히 이야기드립니다.
PM님은 "그렇다. 빠르게 하거라." 라고 말하셨습니다.
저는 결국악덕 상사님의 말씀을 받들어메서드 1000개 만들기(수정하기)를 시작합니다.
1) MemberService 시간 측정 추가하기
- try ~ finally : 여기서 finally 쪽은 try 가 끝난 후 항상 오는 부분 즉, try가 실행 된 후 실행된다.
- try ~ catch : 여기서 catch 쪽은 try 실행 된 후 예외상황(Exception) 이 발생했을때 실행되는 구문.
import HJproject.Hellospring.domain.Member;
import HJproject.Hellospring.repository.MemberRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@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 ~ finally 에서 finally 부분은 항상 오는 나오도록 하는 부분
// try ~ catch 에서 catch 는 특정 상황에서 나오도록 하는 부분
try{
// 회원 가입 시 동일한 ID 로는 가입 불가
checkDuplicateID(member); // 중복 회원 검증
memberRepository.save(member); // memberRepository 에 member 저장
return member.getCode(); // 저장 후 저장된 회원의 Code 번호 반환
}finally {
long finish = System.currentTimeMillis(); // 끝나는 시간 측정
long timeMS = finish - start;
System.out.println("join time = "+ timeMS + "ms");
}
}
private void checkDuplicateID(Member member) { // 메서드 추출 단축키 컨트롤 + 쉬프트 + M
long start = System.currentTimeMillis();
try{
memberRepository.findById(member.getId())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 ID 입니다");
});
}finally {
long finish = System.currentTimeMillis();
long timeMS = finish - start;
System.out.println("checkDuplicateID = "+ 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("findMemberALL = "+ timeMS + "ms");
}
}
public Optional<Member> findOne(Long memberCode){
return memberRepository.findByCode(memberCode);
}
}
2) 결과 확인하기
- 아래 사진의 위 부분이 처음 실행한 결과이고, 아래 부분이 2번째 실행한 결과이다.
- 맨 처음 실행하면 시간이 조금 더 오래걸리고 2번째부터는 시간이 줄어든다.
- 이는 처음 실행 시 불러들이고 읽어오는 데이터가 더 많기 때문!!
3) 아직도 998개 - AOP가 없을때 문제점
아니다! 우리는 2개의 메서드밖에 수정하지 않았고, 998개의 메서드를 더 수정해야한다. 솔직히 핵심 사항이 아닌 부분인데 반해 너무나 많은 시간을 들여 수정해야한다. AOP를 사용하지 않을 때 문제가 너무 많다.
- 회원가입, 회원 조회 시간을 측정하는 기능은 핵심 관심 사항이 아니다 => 시간을 측정하는 로직은 공통 관심 사항
- 핵심 관심 사항은 회원 가입 기능, 회원 조회 기능!
- 시간을 측정하는 로직과 핵심 비지니스 로직이 섞여서 유지보수가 어렵다.
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다
- 시간을 측정하는 로직을 변경할때는 모든 로직을 찾아가면서 변경해야한다.
그러면 우리는 정말 밤을 새서 998개를 손으로 일일히 고쳐야할까...?
2. AOP 사용해서 시간 측정 메서드 만들기
이번에는 AOP를 사용한다는 가정하에 위와 똑같은 상황을 마주했다.
PM 님이 제게 오셔서 말씀하셨습니다. "오늘까지 모든 메서드의 호출 시간을 측정할 수 있도록 만들고 적용해놓거라"
그러자 "퇴근 시간까지만 만들면 될까요? 빨리 끝내면 칼퇴가 가능하겠습니까..?" 라고 감히 이야기드립니다.
PM님은 "그렇다. 빠르게 하거라." 라고 말하셨습니다.
저는 쿨하신 PM님의 아량에 감사드리며 메서드 호출 시간 기능 만들기를 시작합니다.
이번에는 상황이 살짝 달라졌다. 분명 똑같은 업무가 주어졌을 때 AOP를 사용해 칼퇴를 가능하게 만들 것이다.
1) TimetraceAOP class 작성
- @Aspect : AOP 를 사용하기 위한 어노테이션
- @Around : AOP 적용 시점을 정해주기 위한 어노테이션
- ("execution( [ ~~~~~~~~~~~~~~~~~~] ")
- [ ~~~~~~~~~~~~~~~~~~] 위치에 AOP 적용 타켓을 정해줌 -> 접근제어자 패키지.클래스 경로 메소드 마라미터 가 들어간다.
- joinpoint : 크라이언트가 호출하는 모든 비즈니스 메소드.
- AOP 어노테이션에 대한 보다 자세한 설명은 3. AOP 란 무엇인가? 에서 설명하겠다.
package HJproject.Hellospring.Aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect // AOP 사용하기 위한 어노테이션
@Component
// Configration 에 Bean 으로 등록하거나 @Component 어노테이션 사용
// 현재는 @Component 사용
public class TimeTraceAop {
@Around("execution(* HJproject.Hellospring..*(..))")
// AOP를 적용할 클래스들을 정하는(타게팅하는) 어노테이션
// 따라서 excution(* 접근제어자 전체 HJproject 패키지. 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");
}
}
}
2) 결과 확인하기
AOP 에 눈을 뜬 나는 AOP 클래스 하나만 만들고 적용시킴으로써 칼퇴에 성공할 수 있었다!! 만세!
3. AOP(Aspect Oriented Programming)
1) What is AoP?
사실 이 쯤되면 대충 눈치 챈 사람도 있을 것이다. AOP 란 결국 '관점 지향 프로그래밍' 이라는 의미이다. 즉 비즈니스 관점에 따라 프로그래밍하고 결합하라는 의미이다. 이를 위에 내가 만났던 상황을 통해 설명하겠다.
내가 마주했던 상황은 핵심 비지니스 메서드들 - 회원 가입, 회원 조회 - 에 추가할 공통된 관심 사항 - 시간 측정 메서드 - 를 적용해야하는 것이었다. 즉 여기서 가장 중요한 것은 회원 가입, 회원 조회 이며 사실상 시간 측정 메서드는 부가적인 기능에 지나지 않는다. 그리고 우리는 이런 부가적인 기능을 핵심 비스니스 메서드에서 떼내어서 따로 메서드를 만들었고, 필요한 부분에만 적용 한 것이다. 이것이 바로 AOP의 알파이자 오메가이다.
핵심 비즈니스 로직과 공통된 로직을 분리하여 관리하고 필요할 때 결합하는 것이 바로 AOP 이다. 즉 아래 그림처럼 공통적용이 가능한 로직들을 빼내고(Aspect X Y Z) 각 핵심 로직 별로(class A B C) 적용시키는것이다.
우리의 시간 측정 로직도 마찬가지이다. AoP 적용 전과 후는 아래와 같은 그림처럼 생길 것이다.
2) AoP 동작 방식
AoP 는 프록시라는 기능을 사용한다. 아마 네트워크를 공부한 사람은 어디선가 한번쯤은 들어봤을 법한 단어이다. 사실 프록시가 무엇인가에 대해서도 장황하게 설명이 가능하나...일단은 간단하게만 설명하도록 하겠다.
프록시(Proxy) 란 프로토콜에 있어서 대리 응답 등에서 사용하는 개념이다. 즉 보안상의 문제로 직접 통신을 주고 받을 수 없는 사이에서 프록시를 이용해서 중계를 하는 개념이다. 일종의 클라이언트와 서버를 잇는 일종의 중계 서버 라고 보면 될 것 같다. 비슷한 개념이 하나 더 있는데, VPN이다. 다만 VPN과는 살짝 결이 다르다.
AoP의 동작이 바로 이런 프록시를 이용한 방법이다. 이를 확인하기 위해서 MemberController 코드를 살짝 고쳐서 확인해보자. 다른 부분은 고치지 않고, memberService DI 될때 getClass 를 이용해서 프록시의 동작을 확인해보겠다.
@Controller
// 컨트롤러는 그냥 그대로
public class MemberController {
private final memberService memberService;
@Autowired
// Bean 으로 설정되었던 memberService 를 넣어줌. 의존성 주입
public MemberController(memberService memberService) {
this.memberService = memberService;
System.out.println("memberService = " + memberService.getClass()); // AoP 프록시 동작 확인하기
}
또 비교를 위해서 HelloSpringApplication 쪽도 println 문을 하나 추가하겠다.
@SpringBootApplication
public class HelloSpringApplication {
public static void main(String[] args) {
SpringApplication.run(HelloSpringApplication.class, args);
System.out.println("Hellospring : " + HelloSpringApplication.class);
}
}
이제 spring을 실행하면 아래와 같이 memberService 에서는 proxy 가 적용된 모습을, 그리고 HellospringApplication 에서는 이반적으로 실행되는 모습을 확인 할 수 있다.
그림으로 나타내보면 아래와 같은 모양일 것이다.
3) AoP 용어 및 어노테이션
- 조인 포인트 JoinPoint : 클라이언트가 호출하는 모든 비즈니스 메소드. 조인포인트 중에서 포인트 컷되기 때문에 포인트컷의 후보로 생각할수 있다.
- 포인트 컷 Pointcut : 특정 조건에 의해 필터링된 조인포인트, 수많은 조인 포인트 중에서 특정 메서드에서만 공통 기능을 수행시키기 위해 사용한다. => 표현식 : 리턴타입 - 패키지경로 - 클래스명 - 메서드명(매개변수)
- 보통 execution 과 함께 표현식을 사용한다.
- within : 패키지 내 모든 메서드에 적용할 때 사용 => 표현식은 pointcut 과 비슷?
- 어드바이스 Advice : 공통 관심 사항에 해당하는 공통 기능의 코드, 독립된 클래스의 메서드로 작성한다.
동작 시점 | 설명 |
Before | 메서드 실행 전에 동작 |
After | 메서드 실행 후에 동작 |
After-returning | 메서드가 정상적으로 싱행된 후에 동작 |
After-throwing | 예외가 발생한 후에 동작 |
Around | 메서드 호출 이전, 이후, 예외상황 등 모든 시점에서 동작 |
- 위빙 Weaving : 포인트컷으로 지정한 핵심 관심 메서드가 호출될 때, 어드바이스에 해당하는 관심 메서드가 삽입되는 과정을 의미한다. 즉 이를 통해 핵심 비지니스 메서드를 수정하지 않고 공통 관심 로직에 해당하는 기능을 추가하거나 변경이 가능하다.
- 에스팩트 Aspect : 공통 관심사(부가 기능) Class 임을 알려주는 어노테이션. 이후 Bean 으로 따로 등록해주는 작업이 필요하다. 위에서처럼 @Component 를 사용해도 무방하다.
- Bean(bean id) : 해당 bean id 를 갖는 bean 의 모든 메서드에 적용한다는 의미 -> @Pointcut(bean(car)) : car 라는 bean id 를 갖는 bean에게 적용한다는 의미.
- 이 부분은 앞으로 스프링과 AoP에 대해서 공부하면서 더 정리해놓을 예정입니다. 더 자세하게 정리한 내용은 아래 참고 블로그 보시며 좋을 것 같습니다.
참고 블로그
https://engkimbs.tistory.com/746
https://galid1.tistory.com/498
https://jeong-pro.tistory.com/171
https://sjh836.tistory.com/157
- 프록시 & VPN 차이점과 공통점
https://dany-it.tistory.com/115
'Java - Spring &&n SpringBoot' 카테고리의 다른 글
spring - 로그인 기능 구현하기 (2) 세션으로 로그인 하기 (0) | 2022.01.18 |
---|---|
spring - 로그인 기능 구현하기 (1) 쿠키 활용, 쿠키의 취약점 (0) | 2022.01.16 |
Spring - DB 연동(2) : JPA, Spring Data JPA (0) | 2021.12.25 |
Spring - DB 연동(1) : H2 DB, 순수 JDBC, JdbcTemplate (0) | 2021.12.21 |
Spring - 회원 관리 페이지 만들기 - 홈 화면 추가, 등록, 조회 (0) | 2021.12.18 |
댓글