AOP가 필요한 상황과 AOP 적용

myeonji·2022년 2월 6일
0

Spring

목록 보기
8/22

> AOP가 필요한 상황

  • 모든 메소드의 호출 시간을 측정하고 싶을 때
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
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");
        }

    }

ms로 시간을 조회하는 코드를 추가해보았다.
지금은 join 메소드 하나에만 추가를 했지만 모든 메소드의 호출 시간을 알기 위해서는 하나하나 메소드마다 코드를 작성해야 한다.

public Long join(Member member) {
		validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

이 메소드의 핵심 기능은 위와 같다. 하지만 시간을 측정하는 기능을 넣어서 굉장히 관리가 어려워 보인다.

📌 문제

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.
  • 시간을 측정하는 로직은 공통 관심 사항이다.
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
  • 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.

> AOP 적용

  • AOP : Aspect Oriented Programming
  • 공통 관심 사항 vs 핵심 관심 사항 분리

위와 같았던 구조가

이처럼 바뀐 것이다!

1. @Component로 스프링 빈에 등록

< TimeTraceAop >

// 스프링 빈에 등록 -> SpringConfig에 timeTraceAop
// @Component으로 스프링 빈에 등록해도 됨
@Aspect // AOP 로 쓰기 위해 등록
@Component
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))") // hellospring 패키지 하위에 전부 적용
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            // 변수 인라인 화 (Ctrl + Shift + Alt + T)
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString() +" " + timeMs + "ms");
        }
    }
}

TimeTraceAop를 만들었으니 이제 MemberService의 join 메소드 안에 복잡하게 구현했던 것들은 지워도 된다.

위의 TimeTraceAop는 @Component를 이용해서 스프링 빈에 등록하였는데,

2. SpringConfig에서 스프링 빈으로 등록

	@Bean // TimeTraceAop를 스프링 빈에 등록
    public TimeTraceAop timeTraceAop()
    {
        return new TimeTraceAop();
    }

SpringConfig에서 위처럼 등록할 수도 있다.

✔ 하지만 SpringConfig에 @Bean으로 등록하기 위해서는 추가할 점이 있다.

    @Around("execution(* hello.hellospring..*(..))") // hellospring 패키지 하위에 전부 적용

TimeTraceAop의 AOP 대상을 지정하는 @Around 코드를 보면 SpringConfig의 timeTraceAop() 메서드도 AOP로 처리를 하게 된다.
그런데 여기서 이 timeTraceAop()는 바로 자기 자신인 TimeTraceAop를 생성하는 코드이다. 따라서 순환참조 문제가 발생한다.

@Component 스캔을 사용할 때는 AOP의 대상이 되는 이런 코드(timeTraceAop 같은) 자체가 없기 때문에 문제가 발생하지 않은 것이다.

따라서 AOP 설정 클래스를 직접 빈으로 등록할 때는 아래처럼 AOP 대상에서 SpringConfig를 빼주면 된다.

	@Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
    //@Around("execution(* hello.hellospring..*(..))") // hellospring 패키지 하위에 전부 적용

🎉 해결

  • 회원가입, 회원 조회 등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
  • 변경이 필요하면 이 로직만 변경하면 된다.
  • 원하는 적용 대상을 선택할 수 있다.

원하는 적용 대상을 선택하는 코드는

    @Around("execution(* hello.hellospring..*(..))") // hellospring 패키지 하위에 전부 적용

이 부분인데, 지금은 전부 적용하였지만 응용하여 사용하면 된다.

< AOP 적용 전 의존관계 >

< AOP 적용 후 의존관계 >

스프링은 가짜 memberService를 만든다. 이를 프록시라고 한다.
스프링 컨테이너에 스프링 빈을 등록할 때 진짜 스프링 빈이 아닌 가짜 스프링 빈을 앞세운다.
가짜 스프링 빈이 joinPoint.proceed()를 끝나면 그때 진짜 스프링 빈이 호출된다.
즉, memberController가 호출하는 것은 가짜 memberService 인 것이다.

< AOP 적용 전 전체그림 >

< AOP 적용 후 전체그림 >

AOP 타겟을 전부 다 설정했기 때문에 전체 그림이 위처럼 된다.

직접 확인해보자!!
< MemberController >

 	@Autowired 
    public MemberController(MemberService memberService) {

        this.memberService = memberService;
        
        System.out.println("memberService = " + memberService.getClass()); // 프록시 조회해보기
    }

memberService = class hello.hellospring.service.MemberService$$EnhancerBySpringCGLIB$$7ce27c0f

MemberService 뒤에 $$로 뭐가 막 붙어있다! 바로 프록시다!!
간단히 말하면 MemberService를 복제해서 조작한 기술이다.

이게 다 AOP가 적용되어 일어나는 일임을 명심하자!

0개의 댓글