AOP가 필요한 상황
소프트웨어 개발 과정에서 특정 기능을 모든 메소드에 반복적으로 적용해야 하는 상황이 발생할 때가 있다. 예를 들어, 모든 메소드의 호출 시간을 측정하고 싶다면 어떻게 해야 할까? 아래는 호출 시간을 측정하는 코드를 메소드에 직접 추가한 예제다:
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");
}
}
/**
* 전체 회원 조회
*/
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");
}
}
위 코드는 문제점이 많다.
- 핵심 로직과 부가 로직의 혼합
회원 가입과 회원 조회의 시간 측정 기능은 핵심 관심 사항이 아니다. 그러나 코드가 섞여 있어 가독성이 떨어진다. - 유지보수의 어려움
시간을 측정하는 로직을 수정하거나 확장하려면 모든 메소드를 찾아서 수정해야 한다. - 재사용성 부족
시간 측정 로직을 별도의 공통 로직으로 만들기 어렵다.
AOP를 활용한 문제 해결
위 문제는 AOP(Aspect Oriented Programming)를 적용하면 해결할 수 있다. AOP는 공통 관심 사항(cross-cutting concern)과 핵심 관심 사항(core concern)을 분리하는 프로그래밍 패러다임이다. 이를 통해 코드를 더 깔끔하고 유지보수하기 쉽게 만들 수 있다.
아래는 AOP를 적용한 시간 측정 로직의 예제다:
@Component
@Aspect
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");
}
}
}
위 코드에서 핵심은 @Around 어노테이션이다. 이 어노테이션은 특정 메소드 실행 전후에 공통 로직을 실행하도록 한다. 위의 execution(* hello.hellospring..*(..))은 hello.hellospring 패키지와 하위 패키지의 모든 메소드를 대상으로 한다.
AOP 적용의 이점
- 핵심 로직과 공통 로직의 분리
시간 측정 로직을 핵심 비즈니스 로직과 분리해 코드가 깔끔해진다. - 유지보수의 용이성
시간 측정 로직을 한 곳에서 관리할 수 있어 유지보수가 간편하다. - 재사용성 향상
AOP를 사용하면 동일한 공통 로직을 다양한 곳에 쉽게 적용할 수 있다. - 적용 대상의 선택적 설정
원하는 메소드나 패키지에만 공통 로직을 적용할 수 있다.
AOP 적용 전후의 동작 방식: 스프링 컨테이너의 변화
- AOP 적용 전 의존관계
위 그림이 AOP를 적용하기 전 스프링 컨테이너의 구조를 보여준다.
- 구성 요소
- memberController, memberService, memberRepository는 각각 Controller, Service, Repository 역할을 하는 실제 객체이다.
- 요청이 들어오면 컨트롤러(memberController)가 이를 받아 서비스(memberService)를 호출하고, 서비스는 리포지토리(memberRepository)와 상호작용한다.
- 동작 방식
- 각 계층 간에는 핵심 로직만 존재하며, 시간 측정이나 기타 공통 로직을 직접 추가해야 한다.
- 이 방식에서는 공통 관심 사항이 모든 계층의 코드에 분산되며, 중복 코드가 발생할 가능성이 높다.
- AOP 적용 후 의존관계
위 그림은 AOP를 적용한 뒤의 스프링 컨테이너 구조를 보여준다.
- 구조 변화
- 기존의 실제 객체(컨트롤러, 서비스, 리포지토리) 외에 프록시(Proxy) 객체가 추가되었다.
- 프록시는 실제 객체를 감싸는 래퍼(wrapper) 역할을 하며, 메소드 호출 전후에 공통 로직을 삽입한다.
- 실제 비즈니스 로직은 여전히 원래의 객체(memberService, memberRepository)에서 수행된다.
- AOP의 역할
- 프록시 객체는 @Aspect로 정의한 시간 측정 로직을 적용한다.
- 호출 흐름:
- 클라이언트가 memberController를 호출하면, 스프링 컨테이너는 프록시 memberController를 먼저 호출한다.
- 프록시가 시간을 측정하거나 로그를 남긴 뒤 실제 객체를 호출한다.
- 실제 로직 수행이 끝나면 다시 프록시로 돌아와 후처리 로직을 수행한다.
끝.
'Study > SpringBoot' 카테고리의 다른 글
Spring Been 객체와 프록시 패턴 (0) | 2025.03.10 |
---|---|
Spring Boot 주요 모듈 및 MVC 아키텍처 (1) | 2025.03.06 |
스프링 빈과 의존관계 (0) | 2024.11.23 |
스프링 DB 접근 기술 개요 (2) | 2024.11.14 |
간단한 회원 관리 예제로 웹 애플리케이션 계층 구조를 이해하기 (0) | 2024.10.31 |