• 모든 메소드의 호출 시간을 측정하고 싶다면?
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    //Dependency Injection
    //현 클래스에서 생성하는 것이 아닌 외부에서 생성한 것을 넣어주는 형태
    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);
    }
}

join과 findMembers 메소드에 시간측정을 위한 코드를 추가하였다. 만약 이 서비스클래스에 메소드가 수십, 수백개 였다면 유지관리 및 보수가 어떨까?
-> 매우 힘듬

문제 사항

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다 (단순히 시간 측정용)
  • 시간을 측정하는 로직은 공통 관심 사항이다 (모든 메소드의 시간을 측정)
  • 시간을 측정하는 로직과 핵심 비지니스의 로직이 섞여서 유지 보수가 힘들다
  • 시간을 측정하는 로직을 별도의 공통 로직으로 빼서 만들기가 어렵다
  • 시간을 측정하는 로직을 변경하려면 각 로직마다 일일히 변경을 해야한다

이를 위해 AOP 적용

  • Aspect Oriented Programming 관점 지향 프로그래밍
Before

After

문제를 공통 관심 사항 vs 핵심 관심 사항으로 분리한다

Cross-cutting-concern VS Core-concern

적용

AOP를 위한 TimeTraceAop 클래스를 만들어준다

  • @Aspect를 달아줘야 한다 (관점지향)
  • @Component 를 달아줘서 Bean으로 등록해준다 그래야 스프링에서 관리가 가능
  • @Around 를 통해서 원하는 타켓을 정할수 있다
    • "execution(* <원하는 폴더의 파일>(..))"
    • "execution( hello.hellospring..(..))")
    • hello.hellospring 하위 모든 파일에 적용하겠다는 의미이다
  • joinPoint에는 다양한 메소드가 존재하므로 원하는 메소드를 적절히 선택하여 다양한 활용이 가능하다
    • 예를 들어 특정 조건에서는 다음 로직이 진행되지 않게 하는 것도 가능
@Aspect
@Component
public class TimeTraceAop {


    @Around("execution(* hello.hellospring..*(..))") //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");
        }
    }

}

결과

  • 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만든다
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다 (코드를 덕지덕지 붙이지 않아도 된다)
  • 변경이 필요하면 이 로직만 변경하면 된다
  • 원하는 적용 대상을 선택할 수 있다 (@Around)

스프링의 AOP 동작 방식

기존 스프링 의존 관계에서 클래스 호출 방식

helloController ----> memberService 의존하고있다
helloController에서 memberService의 메소드를 호출할때 memberService에서 호출

AOP 적용 후 의존 관계에서 클래스 호출 방식

AOP가 있다면 스프링이 호출받는 memberService의 가짜 memberService(프록시 Proxy)를 만들어 낸다. 그럼 프록시(가짜)가 불리고 끝난 이후 joinPoint.preceed()가 불리고 나서 실제 memberService를 호출한다

profile
내꿈은 숲속의잠자는공주

0개의 댓글

Powered by GraphCDN, the GraphQL CDN