[스프링 입문] Section08. AOP

Euiyeon Park·2025년 6월 17일
0

갓영한 스프링🍀

목록 보기
11/12
post-thumbnail

🍀 AOP 핵심 용어

용어설명
Aspect부가기능(Advice)을 모아놓은 클래스
AdviceAspect에서 실제로 수행할 동작을 정의한 메서드(execute())
JoinPointAdvice가 적용될 수 있는 실행 지점
메서드 실행, 객체 생성 등 다양한 지점이 될 수 있음
PointcutAdvice가 적용될 메서드나 클래스의 범위를 지정하는 표현식
"execution(* hello.hello-spring..*(..))" 특정 패키지의 모드 메서드 지정

26. AOP가 필요한 상황

AOP(Aspect Oriented Programming)란?

  • 관점 지향 프로그래밍
  • 어떤 로직을 기준으로 핵심 관점, 부가 관점을 분리관점을 기준으로 각각 모듈화
    • 유지보수성과 재사용성을 높임
  • 핵심 관심 사항(Core concern) : 핵심 비지니스 로직
  • 공통 관심 사항(Cross-cutting concern) : 로깅, 파일 입출력, DB연결 등 부가기능
    • 로깅
    • 트랜잭션 관리
    • 성능 모니터링
    • 보안처리

⏰ 만약 각 메서드의 실행 시간을 측정하고 싶다면? - 시간 측정 로직을 모두 추가

📂 MemberService
모든 메서드에 시간 측정 로직을 추가

@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 {
            validateDuplicatedMember(member);
            memberRepository.save(member);
            return member.getId();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("join() = " + timeMs + "ms");
        }
    }

    // 중복 회원 확인 
    private void validateDuplicatedMember(Member member) {
        long start = System.currentTimeMillis();
        try {
            memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("validateDuplicatedMember() = " + timeMs + "ms");
        }
    }

    // 전체 회원 조회
    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> findMember(Long memberId){
        long start = System.currentTimeMillis();
        try {
            return memberRepository.findById(memberId);
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("findMember() = " + timeMs + "ms");
        }
    }
}
validateDuplicatedMember() = 4ms
join() = 7ms
findMembers() = 11ms

💥 문제점

  • 시간을 측정하는 기능은 핵심 관심 사항❌
  • 시간을 측정하는 로직은 공통 관심사항 - 부가 기능
  • 시간 측정 로직과 핵심 비지니스 로직이 섞여 유지보수가 어려움
  • 시간 측정 로직을 별도의 공통 로직으로 만들기 어려움
  • 시간 측정 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 함

27. AOP 적용

  • 핵심 관심사항(회원가입, 회원조회 등)과 공통 관심사항(시간 측정)을 분리
  • 변경이 필요하면 공통 관심사항만 변경
  • 원하는 적용 대상 선택 가능 - @Around

📂 TimeTraceAop

@Aspect
@Component  // 또는 SpringConfig에 @Bean으로 등록(선호하는 방법)
public class TimeTraceAop {

    @Around("execution(* hello.hello_spring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        long start = System.currentTimeMillis();
        // joinPoint.toString() : 어떤 메서드를 호출하는지 얻어옴
        System.out.println("START: " + joinPoint.toString());
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}
START: execution(String hello.hello_spring.controller.MemberController.list(Model))
START: execution(List hello.hello_spring.service.MemberService.findMembers())
START: execution(List org.springframework.data.repository.ListCrudRepository.findAll())
Hibernate: select m1_0.id,m1_0.name from member m1_0
END: execution(List org.springframework.data.repository.ListCrudRepository.findAll()) 4ms
END: execution(List hello.hello_spring.service.MemberService.findMembers()) 4ms
END: execution(String hello.hello_spring.controller.MemberController.list(Model)) 9ms

핵심 로직

@Around("execution(* hello.hello_spring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    ...
}
  • @Around: 해당 메서드가 지정된 포인트컷의 전후에 실행될 Advice임을 명시
  • "execution(* hello.hello_spring..*(..))": hello.hello_spring 패키지 이하의 모든 메서드에 적용하겠다는 의미(join point)
Object result = joinPoint.proceed();
return result;
  • 실제 타겟 메서드 실행
  • proceed()프록시된 대상 메서드를 실행하며, 예외를 던질 수 있어 throws Throwable이 필요

🍀 AOP 관련 어노테이션

어노테이션설명
@Aspect해당 클래스가 AOP의 관심사(Aspect)를 정의하는 클래스임을 명시
@Before핵심 로직 실행 전 실행될 Advice 정의
@After핵심 로직 실행 후 실행될 Advice 정의
@AfterReturning핵심 로직이 정상적으로 반환된 후 실행될 Advice 정의
@AfterThrowing예외가 발생했을 때 실행될 Advice 정의
@Around핵심 로직의 전후를 모두 감싸는 Advice (가장 강력한 형태)

🍀 AOP가 동작하기 위한 기타 설정 어노테이션

어노테이션설명
@EnableAspectJAutoProxyAOP 프록시를 활성화하는 어노테이션
보통 @Configuration 클래스에 선언
@Component@Aspect 클래스에 필수
스프링 빈으로 등록되기 위해 필요

AOP 적용 전 의존 관계

AOP 적용 후 의존 관계

  • AOP가 적용되면 스프링이 올라와서 스프링 컨테이너에 빈을 등록할 때,
    진짜 스프링 빈 말고 가짜 스프링 빈(프록시)을 앞에 세워둠
  • joinPoint.proceed()를 실행하면 내부적으로 진짜 스프링 빈을 호출
    처음에 memberController가 호출하는 건 프록시 memberService를 호출하는 것

AOP 적용 전 전체 그림

AOP 적용 후 전체 그림

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

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
        System.out.println("memberService = " + memberService.getClass());
    }
memberService = class hello.hello_spring.service.MemberService$$SpringCGLIB$$0

🍀 CGLIB(Code Generation Library)

  • 자바 동적 프록시를 생성하기 위한 바이트코드 생성 라이브러리
  • 인터페이스가 없는 구체 클래스를 프록시 - 클래스 기반 프록시 생성
profile
"개발자는 해결사이자 발견자이다✨" - Michael C. Feathers

0개의 댓글