
AOP
Aspect-Oriented Programming, 관점지향 프로그래밍
관심사를 분리하여 활용하는 프로그래밍 방식
횡단 관심사 (크로스-커팅 코너스)
여러 곳에서 반복되는 부가 기능 (로깅, 트랜잭션, 보안, 성능 측정 등등..)
핵심 관심사 (코어 코너스)
실제 비즈니스 로직
관점에 대해 4가지 정의를 할 수 있다
관점 (Aspect)
@Aspect 를 사용하여 관점 분리
결합점 (JointPoint) → 부가 기능이 적용될 수 있는 지점
호출 시점
실행 시점
예외 발생 시점
어드바이스 (Advice) → 언제 부가 기능을 실행할 지
@Before → 메서드 실행전
@After → 실행 후 (예외 미상관)
@AfterReturning → 메서드 정상 종료 후
@AfterThrowing → 예외 발생 후
@Around → 메서드 실행 전후 (가장 강력)
포인트컷 (PointCut) → 어디에 적용할 지
execution(* com.example.service.*.*(..)) → service 패키지의 모든 메서드
@annotation(com.example.Logging) → @Logging 어노테이션이 붙은 메서드
within → 클래스/패키지 별로 적용할 때
Advice 종류 예시
Before
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.service.*.*(..))")
public void checkSecurity(JoinPoint joinPoint) {
System.out.println("보안 체크 시작");
User currentUser = SecurityContext.getCurrentUser();
if (currentUser == null) {
throw new UnauthorizedException("로그인이 필요합니다");
}
System.out.println("보안 체크 완료: " + currentUser.getName());
// 실행되는 메서드가 마저 실행 됨
}
}
// 실행 순서:
// 1. checkSecurity() 실행 (보안 체크)
// 2. 실제 메서드 실행AfterReturning (정상 종료 후)
@Aspect
@Component
public class AuditAspect {
@AfterReturning(
pointcut = "execution(* com.example.service.UserService.save(..))",
returning = "result" // -> 결과를 받아와서 활용
)
public void logAfterSave(JoinPoint joinPoint, Object result) {
System.out.println("사용자 저장 성공!");
System.out.println("저장된 사용자: " + result);
// 감사 로그 기록
auditLogger.log("USER_SAVED", result);
}
}
// 실행 순서:
// 1. save() 메서드 실행
// 2. 정상 종료되면 logAfterSave() 실행AfterThrowing (예외 발생 후)
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex" //-> 실행 중인 메서드의 예외처리 에러를 받아옴
)
public void handleException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.err.println("예외 발생!");
System.err.println("메서드: " + methodName);
System.err.println("예외: " + ex.getMessage());
// 알림 발송
notificationService.sendAlert(
"메서드 " + methodName + "에서 예외 발생: " + ex.getMessage()
);
}
}
// 실행 순서:
// 1. 메서드 실행
// 2. 예외 발생하면 handleException() 실행
// 3. 예외는 계속 전파됨After (무조건 후실행)
@Aspect
@Component
public class ResourceAspect {
@After("execution(* com.example.service.*.*(..))")
public void cleanup(JoinPoint joinPoint) {
System.out.println("리소스 정리 시작");
// 성공하든 실패하든 무조건 실행
resourceManager.cleanup();
System.out.println("리소스 정리 완료");
}
}
// 실행 순서:
// 1. 메서드 실행
// 2. 성공/실패 상관없이 cleanup() 실행Around (메서드의 실행 전체 시점 제어)
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
// ========== 메서드 실행 전 ==========
String methodName = joinPoint.getSignature().getName();
System.out.println("시작: " + methodName);
long startTime = System.currentTimeMillis();
Object result = null;
try {
// ========== 메서드 실행 ==========
result = joinPoint.proceed(); // 이 호출이 실제 메서드 실행!
} catch (Exception e) {
System.err.println("예외 발생: " + e.getMessage());
throw e;
} finally {
// ========== 메서드 실행 후 (무조건) ==========
long endTime = System.currentTimeMillis();
System.out.println("종료: " + methodName);
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
}
return result;
}
}
// @Around는 메서드 실행을 완전히 제어 가능!
// - 실행 전후 처리
// - 예외 처리
// - 결과 변경
// - 실행 자체를 막을 수도 있음PointCut 표현식
execution
execution(접근제어자 리턴타입 패키지.클래스.메서드(파라미터))
// 모든 public 메서드
execution(public * *(..))
// set으로 시작하는 모든 메서드
execution(* set*(..))
// service 패키지의 모든 메서드
execution(* com.example.service.*.*(..))
// service 하위 모든 패키지
execution(* com.example.service..*.*(..))
// Service로 끝나는 클래스의 모든 메서드
execution(* com.example..*Service.*(..))
// 파라미터가 Long 1개인 메서드
execution(* com.example.service.*.*(Long))
// 파라미터가 없는 메서드
execution(* com.example.service.*.*())@annotation
// 커스텀 어노테이션을 우선 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceCheck {
}
// 관점 작성
// Aspect
@Aspect
@Component
public class CustomAspect {
// @Logging 붙은 메서드에만 적용
@Around("@annotation(com.example.annotation.Logging)")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("로그 시작");
Object result = joinPoint.proceed();
System.out.println("로그 종료");
return result;
}
// @PerformanceCheck 붙은 메서드에만 적용
@Around("@annotation(com.example.annotation.PerformanceCheck)")
public Object checkPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("실행 시간: " + (end - start) + "ms");
return result;
}
}
// 사용
@Service
public class UserService {
@Logging // 이 메서드만 로깅
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
@PerformanceCheck // 이 메서드만 성능 측정
public void complexOperation() {
// ...
}
// 어노테이션 없으면 AOP 적용 안 됨
public void simpleOperation() {
// ...
}
}within (클래스/패키지 기반)
@Aspect
@Component
public class PackageAspect {
// service 패키지의 모든 메서드
@Before("within(com.example.service.*)")
public void beforeService() {
System.out.println("Service 호출");
}
// UserService의 모든 메서드
@Before("within(com.example.service.UserService)")
public void beforeUserService() {
System.out.println("UserService 호출");
}
}JointPoint
Aspect 메서드의 파라미터로 JointPoint 를 받고 있다
JointPoint 는 AOP가 적용된 메서드의 실행 시점 정보를 가진 객체이다
메서드 이름, 파라미터 등의 정보를 제공 하는 실행 시점의 정보가 담겨 있다
@Slf4j
@Aspect
@Component
public class TestAspect {
@Before("execution(* kyj.practice.demo.*.service.*.*(..))")
public void testPrintText(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
log.info("AOP TEST : {}", signature.getName());
Object[] objects = joinPoint.getArgs();
log.info("AOP TEST2 : {}", objects);
Object target = joinPoint.getTarget();
log.info("AOP TEST3 : {}", target);
Object target2 = joinPoint.getThis();
log.info("AOP TEST4 : {}", target2);
}
}joinPoint.getSignature()
메서드의 정보에 대해
getName() → 메서드 이름
getDeclaringTypeName() → 선언한 클래스의 전체 이름 (아티팩트 명 까지 다)
getDeclaringTypeName().getSimpleName() → 클래스명만
jointPoint.getArgs()
파라미터 값에 대해
파라미터 값들을 받아오므로 Object[] 형식을 지님
jointPoint.getTarget()
실행 메서드의 대상 객체에 대해 (실제 객체를 받아오는 것)
getClass().getName() 을 더 할 경우 대상 객체의 클래스 이름을 받아옴
jointPoint.getThis()
대상 객체의 프록시 객체를 받아옴
jointPoint.getKind()
실행되는 JointPoint 종류를 받아옴
메서드가 실행되는 것이면 method-excution 이라는 String을 얻음
ProceedingJointPoint
@Around에서만 사용가능한 JointPoint 로, 메서드의 실행 여부 까지도 제어 가능
이 때는 proceed() 를 통해 실행을 제어 해주어야한다
@Aspect
@Component
@Slf4j
public class AroundAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// JoinPoint의 모든 메서드 사용 가능
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("메서드 시작: {}", methodName);
log.info("파라미터: {}", Arrays.toString(args));
long startTime = System.currentTimeMillis();
// 실제 메서드 실행 **
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
log.info("메서드 종료: {}", methodName);
log.info("실행 시간: {}ms", executionTime);
log.info("반환값: {}", result);
return result;
}
}
//------------------------------------------
// 파라미터의 수정이 일어난 경우
@Around("execution(* com.example.service.*.*(..))")
public Object modifyAndProceed(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 파라미터 수정
if (args.length > 0 && args[0] instanceof String) {
args[0] = ((String) args[0]).toUpperCase();
}
// ⭐ 수정된 파라미터로 실행
Object result = joinPoint.proceed(args);
return result;
}적용 예시
AOP 적용전
@Service
public class UserService {
public User findById(Long id) {
// ========== 부가 기능 시작 ==========
System.out.println("=== 메서드 시작: findById ===");
long startTime = System.currentTimeMillis();
// ========== 핵심 로직 ==========
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
// ========== 부가 기능 종료 ==========
long endTime = System.currentTimeMillis();
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("=== 메서드 종료: findById ===");
return user;
}
public void save(User user) {
// ========== 부가 기능 시작 ==========
System.out.println("=== 메서드 시작: save ===");
long startTime = System.currentTimeMillis();
// ========== 핵심 로직 ==========
userRepository.save(user);
// ========== 부가 기능 종료 ==========
long endTime = System.currentTimeMillis();
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("=== 메서드 종료: save ===");
}
public void delete(Long id) {
// ========== 부가 기능 시작 ==========
System.out.println("=== 메서드 시작: delete ===");
long startTime = System.currentTimeMillis();
// ========== 핵심 로직 ==========
userRepository.deleteById(id);
// ========== 부가 기능 종료 ==========
long endTime = System.currentTimeMillis();
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("=== 메서드 종료: delete ===");
}
}메서드의 소요 시간을 로깅하기 위해 계속 같은 코드 반복 중
AOP 적용 후
// 우선 핵심 로직만 남김
@Service
public class UserService {
private final UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
public void save(User user) {
userRepository.save(user);
}
public void delete(Long id) {
userRepository.deleteById(id);
}
}
// 부가 기능 분리
@Aspect // 관점 어노테이션
@Component
public class LoggingAspect {
// UserService의 모든 메서드에 적용
@Around("execution(* com.example.service.UserService.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// ========== 메서드 실행 전 ==========
String methodName = joinPoint.getSignature().getName();
System.out.println("=== 메서드 시작: " + methodName + " ===");
long startTime = System.currentTimeMillis();
// ========== 실제 메서드 실행 ==========
Object result = joinPoint.proceed();
// ========== 메서드 실행 후 ==========
long endTime = System.currentTimeMillis();
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("=== 메서드 종료: " + methodName + " ===");
return result;
}
}로깅 예시
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("execution(* com.example.controller.*.*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
// 요청 정보
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("▶ API 호출: {}.{}", className, methodName);
log.info("▶ 파라미터: {}", Arrays.toString(args));
// 실행
Object result = joinPoint.proceed();
// 응답 정보
log.info("◀ 응답: {}", result);
return result;
}
}
// 출력:
// ▶ API 호출: UserController.createUser
// ▶ 파라미터: [UserRequest(name=홍길동, email=hong@email.com)]
// ◀ 응답: UserResponse(id=1, name=홍길동)성능 모니터링
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 성능 기록
log.info("⏱ {}: {}ms", methodName, executionTime);
// 느린 쿼리 알림 (1초 이상)
if (executionTime > 1000) {
log.warn("느린 메서드 감지: {} ({}ms)", methodName, executionTime);
alertService.sendSlowMethodAlert(methodName, executionTime);
}
}
}
}권한 체크
// 커스텀 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String value(); // 필요한 권한
}
// Aspect
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(requireRole)")
public void checkRole(RequireRole requireRole) {
String requiredRole = requireRole.value();
User currentUser = SecurityContext.getCurrentUser();
if (currentUser == null) {
throw new UnauthorizedException("로그인이 필요합니다");
}
if (!currentUser.hasRole(requiredRole)) {
throw new AccessDeniedException(
"권한이 없습니다. 필요한 권한: " + requiredRole
);
}
log.info("권한 체크 성공: {} ({})", currentUser.getName(), requiredRole);
}
}
// 사용
@Service
public class AdminService {
@RequireRole("ADMIN")
public void deleteUser(Long userId) {
// ADMIN 권한 있어야만 실행됨
userRepository.deleteById(userId);
}
@RequireRole("MANAGER")
public void updateProduct(Product product) {
// MANAGER 권한 있어야만 실행됨
productRepository.save(product);
}
}파라미터 검증
@Aspect
@Component
@Slf4j
public class ParameterValidationAspect {
@Before("execution(* com.example.service.*.*(..))")
public void validateParameters(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 파라미터 null 체크
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
log.error("메서드 {}: {}번째 파라미터가 null입니다", methodName, i);
throw new IllegalArgumentException(
String.format("%s의 %d번째 파라미터가 null입니다", methodName, i)
);
}
}
// 특정 타입 검증
for (Object arg : args) {
if (arg instanceof String) {
String strArg = (String) arg;
if (strArg.isEmpty()) {
log.error("메서드 {}: 빈 문자열이 전달되었습니다", methodName);
throw new IllegalArgumentException("빈 문자열은 허용되지 않습니다");
}
}
}
}
}