Fascination
article thumbnail

[Spring Intro] Section 07. AOP

강의: 김영한의 스프링 입문

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/


# 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를 할 수 있다는 것이 장점이다

 

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!