하위 태스크 1
횡단 관심사 문제 분석
중복 코드 위치 파악 및 문제점 문서화
UserService 클래스의 구현에 두 가지 문제가 있다. 첫째로 메서드가 단일 책임 원칙을 지키지 않는다. createUser와 updateUser는 로깅과 사용자 관리 작업을 동시에 수행한다. 이는 메서드가 하는 일이 많다는 점을 시사한다. 둘째로 로깅을 위한 코드가 거의 동일하며 여러 메서드에 걸쳐 중복된다는 점이다. 이는 클래스가 비대해질수록 유지보수성을 떨어뜨린다.
public class UserService {
UserRepository userRepository;
public void createUser(User user) {
// 로깅 코드 중복
long start = System.currentTimeMillis();
System.out.println("메서드 시작: createUser");
userRepository.save(user);
// 로깅 코드 중복
long end = System.currentTimeMillis();
System.out.println("메서드 종료: createUser, 실행 시간: " + (end - start) + "ms");
}
public void updateUser(User user) {
// 로깅 코드 중복
long start = System.currentTimeMillis();
System.out.println("메서드 시작: updateUser");
userRepository.update(user);
// 로깅 코드 중복
long end = System.currentTimeMillis();
System.out.println("메서드 종료: updateUser, 실행 시간: " + (end - start) + "ms");
}
}하위 태스크 2
AOP 의존성 추가
build.gradle에 Spring AOP 의존성 추가
build.gradle에 implementation 'org.springframework.boot:spring-boot-starter-aop'를 추가한다.
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-aop'
// ...
}IntelliJ IDEA의 Gradle 패널에서 ‘모든 Gradle 프로젝트 동기화’를 실행한다.
하위 태스크 3 ~ 5
LoggingAspect 클래스 생성
@Aspect를 사용한 로깅 Aspect 구현
@Before Advice 구현
메서드 실행 전 로깅 구현
Pointcut 표현식 작성
특정 패키지/메서드를 대상으로 하는 Pointcut 작성
LoggingAspect 클래스를 구현한다. AspectJ 포인트컷 표현식을 사용했다. 해당 표현식은 com.example.demo 패키지와 하위 패키지에 있는 모든 클래스의 모든 메서드가 실행되는 경우를 의미한다.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.demo..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("메서드 실행 전: " + joinPoint.getSignature().getName());
}
}스프링 부트 애플리케이션을 시동한 결과는 다음과 같다.

하위 태스크 6 ~ 8
기존 로깅 코드 제거
서비스 클래스에서 중복 로깅 코드 제거
PerformanceAspect 클래스 생성
성능 측정을 위한 Aspect 구현
@Around Advice 구현
메서드 실행 시간 측정 구현
성능 측정을 위한 PerformanceAspect 클래스를 구현한다. ProceedingJoinPoin.proceed 메서드를 호출하여 원본 메서드를 실행할 수 있다.
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.demo..*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println(joinPoint.getSignature() + "실행 시간: " + (end - start) + "ms");
return result;
}
}스프링 부트 실행 결과는 다음과 같다.

UserService 클래스에서 LoggingAspect·PerformanceAspect와 중복되는 코드를 제거한다. UserService 클래스의 메서드가 간결해지고 단일 책임 원칙에 따라 하나의 일만 수행하게 되었다.
public class UserService {
UserRepository userRepository;
public void createUser(User user) {
userRepository.save(user);
}
public void updateUser(User user) {
userRepository.update(user);
}
}다시 스프링 부트를 실행한 결과는 다음과 같다.

하위 태스크 9 ~ 10
커스텀 어노테이션 생성
@LogExecution 어노테이션 생성
어노테이션 기반 Pointcut
커스텀 어노테이션을 활용한 선택적 AOP 적용
@LogExecution 애노테이션을 선언한다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
}LogExecutionAspect 클래스를 생성하여 @LogExecution을 구현한다.
@Aspect
@Component
public class LogExecutionAspect {
@Around("@annotation(LogExecution)")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[정보] 메서드 실행 전: " + joinPoint.getSignature().getName());
var object = joinPoint.proceed();
System.out.println("[정보] 메서드 실행 후: " + joinPoint.getSignature().getName());
return object;
}
}UserService 클래스의 updateUser 메서드에 @LogExecution 애노테이션을 적용한다.
// ...
@LogExecution
public void updateUser(User user) {
userRepository.update(user);
}
// ...스프링 부트 실행 결과는 다음과 같다.

애너테이션이 적용된 updateUser 메서드만 로깅된 것을 볼 수 있다.