[Spring Intro] Section 07. AOP
강의: 김영한의 스프링 입문
# AOP가 필요한 상황
서비스 운영 중 프로그램에 문제가 있는 것 같아 모든 method의 호출 시간을 측정하고자 한다
우리 서비스에는 현재 1000개의 메소드가 있기 때문에 시작과 끝에 시간 측정 로직을 모두 심어야 한다
초 단위로 측정하는 코드를 모두 작성했는데 측정이 잘 되지 않아 밀리세컨드 단위로 바꾸게 되었다
이런 상황에서 모든 메소드의 호출 시간을 다시 바꾸기에는 너무 비효율적이다
따라서 현재는 AOP를 사용한다
AOP는 다음과 같은 상황에서 필요하다
1. 모든 메소드의 호출 시간을 측정하고 싶을 때
2. 회원 가입 시간, 회원 조회 시간을 측정하고 싶을 때
1. MemberService 회원 조회 시간 측정 추가
- src/main/java/hello.hellospring/service/MemberService.java에 다음과 같이 시간 측정 로직을 추가해 준다
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import jakarta.transaction.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 {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("join " + timeMs + "ms");
}
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> { // 값이 존재한다면
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
// 전체 회원 조회
public List<Member> findMembers() {
long start = System.currentTimeMillis();
try {
return memberRepository.findAll();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("findMembers = " + timeMs + "ms");
}
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
- 수정된 코드를 바탕으로 통합 테스트를 진행해 보면 다음과 같이 시간이 측정되는 것을 확인할 수 있다
2. 문제점
- 회원 가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다
- 시간을 측정하는 로직은 공통 관심 사항이다
- 시간을 측정하는 로직과 핵심 비지니스 로직이 섞여서 유지보수가 어렵다
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다
- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다
- 결론: AOP를 도입해야 한다
# AOP 적용
1. AOP란?
- AOP: Aspect Oriented Programming
- 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리
2. 시간 측정 AOP 등록
- src/main/java/hello.hellospring에 aop 패키지를 생성하고 그 내부에 TimeTraceAop.java를 다음과 같이 작성한다
- 이전에 Memberservice에 추가했던 시간측정 로직은 모두 지운다
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;
@Aspect
@Component // 여기에 바로 컴포넌트로 등록하지 않고 기존의 SpringConfig에 TimeTraceAop를 등록하는 방법도 있다
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");
}
}
}
- 만약 TimeTraceAop를 @Component로 등록하지 않았다면 아래 코드와 같이 SpringConfig.java를 수정하여 스프링 빈으로 등록될 수 있게 한다
주의할 점은 TimeTraceAop에서 @Around코드를 수정하여 자기 자신에 대한 클래스는 제외해야 한다는 것이다
그렇지 않으면 순환참조가 발생하며 프로젝트 빌드 시 에러가 발생한다
@Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
참고: https://www.inflearn.com/questions/48156/aop-timetraceaop-%EB%A5%BC-component-%EB%A1%9C-%EC%84%A0%EC%96%B8-vs-springconfig%EC%97%90-bean%EC%9C%BC%EB%A1%9C-%EB%93%B1%EB%A1%9D
- 수정하여 실행하면 다음과 같이 모든 method에 시간 측정이 적용된다
3. 해결
- 회원가입, 회원 조회 등 핵심 관심 사항과 시간을 측정하는 공통 관심 사항을 분리한다
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다
- 핵심 관심 사항을 깔끔하게 유지할 수 있다
- 변경이 필요하면 이 로직만 변경하면 된다
- 원하는 적용 대상을 선택할 수 있다
4. 스프링의 AOP 동작 방식 설명
- AOP 적용 전 의존관계 → memberController가 memberService에 의존하고 있다
- AOP 적용 후 의존관계 → AOP를 어디에 적용할지 지정하면 스프링은 프록시(가짜 memberService)를 생성한다
스프링 컨테이너에 스프링 빈을 등록할 때 가짜 스프링 빈을 앞에 세워두고, 가짜 스프링 빈(프록시)를 통해 AOP를 모두 실행한 뒤 joinPoint.proceed()하면 실제 memberService를 호출한다
- MemberController 생성자 부분에 왼쪽과 같은 코드를 추가한 후 실행해 보면 오른쪽 사진의 박스 안에 보이는 콘솔 내용을 통해 실제 Proxy가 주입되었음을 확인할 수 있다
EnhancedBySpringCGLIB: 현재 프로젝트에서는 memberService를 복제해서 코드를 조작하는 기술로 사용되었다
- AOP 적용 전 전체 그림
- AOP 적용 후 전체 그림 → 컨테이너에서 스프링 빈을 관리하면서 가짜를 만들어 DI를 할 수 있다는 것이 장점이다
'Study > Backend Note' 카테고리의 다른 글
[Spring Boot] Custom Error Handling을 해보자! (2) | 2023.11.28 |
---|---|
[SOPT] 파이썬과 Mysql 그리고 구글스프레드 시트를 연동하여 기획이랑 소통하는 법 (0) | 2023.07.21 |
[Spring Intro] Section 06. 스프링 DB 접근 기술 (0) | 2023.03.06 |
[Spring Intro] Section 05. 회원 관리 예제 - 웹 MVC 개발 (0) | 2023.03.04 |
[Spring Intro] Section 04. 스프링 빈과 의존관계 (0) | 2023.02.28 |