Study

[STUDY] 스프링 입문 기록(5) - 코드로 배우는 스프링부트, 웹 MVC, DB 접근 기술

jeonniu 2023. 3. 11. 17:05

06. AOP

 

 

 

AOP가 필요한 상황

  • 모든 메소드의 호출 시간을 측정하고 싶다면?
  • 공통 관심 사항 cross-cutting concern) vs 핵심 관심 사항(core concern)
  • 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?

 

 

 

 

 

 

MemberService 회원 조회 시간 측정 추가

 

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

// 회원 서비스 핵심 비즈니스 로직 구현
// 보통 비즈니스에 의존적으로 용어를 선택
// test 코드 만들려면 ctrl+shift+t 단축키


// JPA를 쓰려면 join 들어올 때 모든 데이터 변경이 트랜젝션 안에서 실행되어야 함
// 그래서 @Transactional 애노테이션 필수
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    // DI(Dependency Injection)- 내가 직접 new 하지 않고, 외부에서 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");
        }
    }

    // window- Shift+Ctrl+Alt+T :  자동화 리팩토링
    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");
        }
    }


    // test code에서 회원ID 값을 찾아주는 메서드
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }

}

 

 

 

문제

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

 

 

 

 

AOP 적용

 

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

 

 

이전 그림은 시간 측정 로직을 매서드 당 하나씩 붙여줬다면,

AOP 방법은 시간 측정 로직을 하나에 모아놓고 원하는 곳에 적용시키는 방법이다.

 

 

 

 

 

 

시간 측정 AOP 등록

 

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 {

            // hello.hellospring 하위 패키지에 모두 적용시켜라.
            // 시간을 측정하고 싶은 특정 위치 설정을 여기서 해주면 된다.
            @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");
        }
    }
}

 

변경사항이 필요하면 위의 로직만 변경하면 된다.

 

 

해결

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

 

 

 

 

스프링의 AOP 동작 방식 설명

 

AOP적용 전 의존 관계

컨트롤러(memberController)가 서비스(memberService)를 의존하고 있다.

 

 

 

 

AOP 적용 후 의존 관계

 

 

스프링은 AOP가 있으면 프록시라고 하는 가짜 스프링 빈(memberService)을 만들어낸다.

 

AOP가 있으면 스프링 컨테이너는 스프링 빈을 등록할 때 진짜 스프링 빈이 아닌 가짜 스프링 빈을 앞에 세워놓는다.

가짜 스프링 빈이 joinPoint.proceed() 끝나면 그 때 실제 memberService 호출을 해준다.

 

따라서 AOP 적용 시에 memberController가 호출하는 것은 실제 memberService가 아닌 프록시라는 기술로 발생하는 가짜 memberService이다.

 

 

 

 

AOP 적용 전 전체 그림

 

 

 

 

타겟을 다 설정한 후의 그림은 다음과 같다.

 

AOP 적용 후 전체 그림

 

 

 

 

실제 Proxy가 주입되는지 콘솔에 출력해서 확인

 

 

 

 

 

 

 

참고: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (https://inf.run/PWvM)