본문 바로가기
Back-end/JAVA | Spring

6_AOP, 프록시 기초 개념

by 카랑현석 2025. 3. 28.

참고 자료

이해를 위해 인프런_김영한님의 스프링 입문 강의를 참고하여 쉬운 예시로 작성하였습니다.

 

Summary

AOP는 끼어들기(새치기) 기술이다.

AOP를 사용하려면 스프링 컨테이너에 스프링 빈을 올려놓아야 사용할 수 있다. (DI를 사용하는 이유 중 하나)

프록시는 가상(가짜)의 분신 메서드이다.

AOP가 필요한 상황

팀장님이 요즘 시스템이 느린데, 어디서 느린지 확인을 해봐야겠다며 1000개가 되는 모든 메서드의 호출 시간을 각각 알아오라고 했다.

 

그래서 나는 아래와 같이 메서드의 모든 로직을 다 try, finally를 붙이고 ms 단위로 시간을 측정하는 로직을 붙여주었다.

// 회원 가입
     public Long join(Member member) {
         long start = System.currentTimeMillis();
 
         try {
         	// 같은 이름이 있는 중복 회원 X
             Optional<Member> result = memberRepository.findByName(member.getName());
             validateDuplicateMember(result);
             memberRepository.save(member);
             return member.getId();
         } finally {
             long finish = System.currentTimeMillis();
             long timeMs = finish-start;
             System.out.println("join = " + timeMs + "ms");
         }
     }

 

그런데 갑자기 상사가 초(second) 단위로 보고 싶다고 하신다.

1000개가 되는 것을 모두 다 또 수정해야 하나..? 멘붕에 빠져버렸다.

 

시간을 재는 로직을 메서드로 하기도 버거운게, 어디서 시작하고 어디서 끝나는지가 다 달라서 메서드화 할 수가 없다!ㅠㅠ

 

코드도 지저분해지고, 1000개를 한꺼번에 수정하기가 어렵다.

뭔가 한 번에 관리할 수 없을까?

 

AOP

[현재 상황 정리]

시간을 측정하는 로직 = 공통 관심 사항

회원가입, 회원 조회에 시간을 측정하는 기능(주 로직) = 핵심 관심 사항

 

위 코드의 예시처럼 공통 관심 사항(시간 측정 로직)과 핵심 관심 사항(중복 회원 처리, 데이터 삽입)이 섞여 있어 코드가 더러워진다.(=유지 보수가 어렵다.)

시간을 측정하는 로직을 변경하면 1000개의 메서드를 다 찾아가서 수정해주어야 한다.

 

시간을 측정하는 로직을 공통의 로직으로 만들기 어렵다. (메서드마다 각각 시작 위치와 종료 위치가 달라서 메서드화 할 수도 없는 상황이다.)

 

[AOP으로 해결하기]

AOP(Aspect Oriented Programming) : 공통 관심 사항과 핵심 관심 사항을 분리하여 해결한다.

(좌측) 기존의 문제점 - 공통 관심 사항과 핵심 관심 사항이 섞여 있다. (우측) 공통 관심 사항과 핵심 관심 사항을 분리하였다.

 

이렇게 하면 원하는 곳에 공통 관심 사항을 적용할 수 있도록 하는 기술이 AOP 이라고 한다.

 

우리가 원하는 방식을 직관적으로 생각해보자.

 

1) 메서드를 실행했을 때 처음에 시작 시간을 저장하고

2) 메서드 로직을 다 실행한 다음

3) 메서드가 끝나기 전에 종료 시간을 저장해서 (종료 시간 - 시작 시간) 을 하면 각 메서드의 실행 시간이다!

 

이걸 그대로 할 수 있도록 만들어주는 것이 바로 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 // bean 등록
@Aspect // AOP 사용을 위해서 사용하는 어노테이션
public class TimeTraceAop {
    // execution(* ......) - 모든 반환 타입
    // hello.hellospring.. - hello.hellospring 패키지와 그 하위 패키지들
    // *(..) - 메서드 이름과 파라미터 제한 없음
    @Around("execution(* hello.hellospring..*(..))") // @Around는 적용 범위를 정할 수 있는 어노테이션, hello.hellospring 패키지와 그 하위에는 다 AOP 적용
    // AOP에서 실제로 공통 기능을 실행하는 메서드
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { // ProceedingJoinPoint는 지금 실행하려는 메서드에 대한 정보를 담고 있음
        long start = System.currentTimeMillis();  // 1)번 : 시작
        System.out.println("START: " + joinPoint.toString()); // 1)번 : 시작
        try {
            Object result = joinPoint.proceed(); // 2)번 : 해당하는 비즈니스 메서드 실행 (ex. join() 메서드)
            return result;
        } finally {
            long finish = System.currentTimeMillis(); // 3)번 : 끝 시간 저장
            long timeMs = finish - start; // 3)번 : 끝 시간 - 시작 시간 = 한 메서드에 걸린 시간
            System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

위 코드는 AOP를 적용한 코드로 hello.hellospring 패키지와 그 하위에는 모두 이 것이 적용된다.

정확히는 프록시(가상의 분신 메서드)가 작동하는 것인데 이는 바로 밑에서 설명하겠다.

 

작동 순서는 아래와 같다.

일단, hello.hellospring 하위에 있는 메서드가 join() 가 있다고 가정해보자.

0) main을 실행하면 AOP가 먼저 작동하여 TimeTraceAop 메서드가 실행된다.

1) 시작 시간을 재고

2) joinPoint.proceed() 에서 해당하는 비즈니스 메서드인 join() 메서드를 실행한다.

3) 다 실행되고 나면 다시 AOP 메서드인 TimeTraceAop 메서드로 돌아와서 끝 시간을 저장하고, (끝 시간 - 시작 시간)을 계산한다.


작동 순서를 보면 개략적으로 이해는 가는데, 몇 가지 의문점이 들 것이다.

 

❓main을 실행하는데 내가 호출하지도 않은 AOP 메서드(TimeTraceAop)가 왜 실행이 될까?

일단 Spring은 @Service, @Repository, @Component, @Aspect 이런 것들을 전부 "빈(bean)"으로 등록한다.

 

그런데 Spring이 join 함수가 포함되어 있는 MemberService와 같은 클래스를 빈으로 만들 때 실제 객체를 바로 주는 것이 아니다. AOP가 끼어든 프록시(가상 분신) 객체를 대신 만들어서 전달해준다.

 

예시를 들자면,

MemberService memberService = new MemberService(memberRepository);

위 코드처럼 이렇게 직접 new로 만드는 것이 아니라 Spring에서 아래처럼 처리한다.

// MemberService는 join() 메서드가 담겨 있는 클래스
MemberService memberService = new 프록시(MemberService) {
    // join()을 호출하면 먼저 AOP 코드(TimeTraceAop)를 실행하고,
    // 그 다음 원래 MemberService.join()을 실행함
};

 

즉, 프록시가상의 분신 메서드와 같은 것이다.

 

정리하자면 main 메서드가 호출되었을 때 과정은 아래와 같다.

 

1) main() → SpringApplication.run() 실행

2) Spring이 전체 프로젝트를 스캔해서 빈들을 다 생성 (@Controller, @Repository, @Service, @Component, @Aspect, @Bean 등등)

3) TimeTraceAop에 @Aspect 가 붙어 있어서 AOP로 등록됨

4) MemberService 같은 서비스들을 만들 때 AOP 프록시를 씌운 버전(MemberService 가짜 분신 메서드)으로 생성

5) join() 메서드를 호출하면 TimeTraceAop.execute() 가 먼저 실행됨

 

이제 다른 예시로 적용한 아래 그림도 이해가 갈 것이다.

출처 : 인프런 김영한_스프링 입문 강의