[Spring] AOP, Aspect-Oriented Programming

dyomi·2024년 8월 10일
0

AOP란, 관점 지향 프로그래밍이라고도 하며, 관점을 기준으로 묶어서 개발하는 방식을 의미한다.

예를 들면, 데이터를 저장하는 기능에서 데이터를 저장하는 비즈니스 로직 사이사이에 로깅이나 트랜잭션과 같은 부가 기능을 추가해야 할 수 있다.

이때 부가 기능은 보통 핵심 기능과 달리 동일한 기능을 반복적으로 수행할 확률이 높다. 그래서 이런 부가 기능을 하나의 공통 로직으로 모듈화해서 삽입하는 방식을 AOP 라고 한다.

디자인 패턴 중 프록시 패턴을 활용한 것이라고 볼 수 있다.

AOP의 목적은 모듈화를 통해서 재사용 가능한 구성을 만드는 것이고, 이 또한 개발자가 비즈니스 로직 구현에만 집중할 수 있도록 도와준다.

AOP를 활용하기 전

아래 코드는 AOP를 사용하지 않고 로깅을 직접 메서드에 추가하는 경우이다.

public class UserService {

    public void createUser(String username) {
        // 로깅 기능
        System.out.println("Creating user: " + username);

        // 실제 비즈니스 로직
        // 사용자 생성 로직 (예: 데이터베이스에 사용자 정보 저장)
        System.out.println("User " + username + " created successfully!");
    }

    public void deleteUser(String username) {
        // 로깅 기능
        System.out.println("Deleting user: " + username);

        // 실제 비즈니스 로직
        // 사용자 삭제 로직 (예: 데이터베이스에서 사용자 정보 삭제)
        System.out.println("User " + username + " deleted successfully!");
    }
}

문제점

  1. 중복 코드: 로깅 코드('System.out.println')가 각 메서드에 중복되어 있다.
  2. 유지보수 어려움: 로깅 형식이나 메시지를 변경하려면 모든 메서드를 수정해야 한다.
  3. 핵심 로직과 부가 기능이 혼재: 비즈니스 로직과 부가 기능이 함께 있어서 가독성이 떨어진다.

AOP를 활용한 코드

아래는 AOP를 사용하여 로깅을 분리한 코드이다.

  1. 비즈니스 로직
public class UserService {

    public void createUser(String username) {
        // 실제 비즈니스 로직
        System.out.println("User " + username + " created successfully!");
    }

    public void deleteUser(String username) {
        // 실제 비즈니스 로직
        System.out.println("User " + username + " deleted successfully!");
    }
}
  1. 로깅 관련 정의
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 포인트컷: UserService 클래스의 모든 메서드에 적용
    @Before("execution(* UserService.*(..))")
    public void logBeforeMethod(JoinPoint joinPoint) {
        // 메서드 실행 전 로깅
        System.out.println("Executing method: " + joinPoint.getSignature().getName());
        Object[] args = joinPoint.getArgs();
        if (args.length > 0) {
            System.out.println("Arguments: " + Arrays.toString(args));
        }
    }
}

장점

  1. 중복 코드 제거: 로깅 코드가 'LoggingAspect'에 모듈화되어, 중복이 제거된다.
  2. 유지보수성 향상: 로깅 로직을 변경해야 할 경우, 'LoggingAspect'만 수정하면 된다.
  3. 관심사 분리: 핵심 비즈니스 로직('UserService')과 부가 기능(로깅)이 분리되어 코드의 가독성과 유지보수성이 향상된다.

AOP의 핵심 개념

1. 관점(Aspect)

특정 횡단 관심사를 모듈화한 것. 예를 들어 로깅 기능이 하나의 관점이 될 수 있습니다.

2. 조인 포인트(Join Point)

횡단 관심사가 실행될 수 있는 특정 지점. 예를 들어 메서드 호출, 객체 생성 등이 조인 포인트가 될 수 있습니다.

3. 어드바이스(Advice)

실제로 수행되는 코드. 조인 포인트에서 실행될 코드 블록입니다. 어드바이스는 주로 메서드 호출 전후, 예외 발생 시점 등에 실행됩니다.

4. 포인트컷(Pointcut)

어드바이스가 적용될 조인 포인트를 지정하는 표현식입니다. 어떤 메서드에 로깅을 적용할지 결정하는 역할을 합니다.

5. 위빙(Weaving)

어드바이스를 실제 코드에 적용하는 과정입니다. 컴파일 시, 로드 시, 런타임 시에 위빙이 이루어질 수 있습니다.



🌟 추가 질문

📍 AOP를 구현할 때 사용되는 어노테이션은 어떤 것이 있는가?

@Aspect: 클래스가 하나 이상의 관점을 정의하는 Aspect임을 명시

@Aspect
@Component
public class LoggingAspect {
    // Aspect 정의
}

@Pointcut: 포인트컷을 정의하는 데 사용되며, 특정 조인 포인트를 지정한다. 포인트컷은 어드바이스가 적용될 지점을 결정한다.

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

@Before: 지정된 포인트컷 이전에 어드바이스를 실행하도록 정의한다. 메서드 호출 전이나 특정 이벤트 전에 로직을 실행할 때 사용된다.

@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
}

@After: 지정된 포인트컷 이후에 어드바이스를 실행하도록 정의한다. 메서드 호출 후에 실행된다.

@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
}

@AfterRefurning
메서드가 정상적으로 종료된 후(예외 없이) 어드바이스를 실행한다. 메서드의 반환값을 참조할 수 있다.

@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("Method returned: " + result);
}

@AfterThrowing
메서드 실행 중 예외가 발생했을 때 어드바이스를 실행한다.

@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
    System.out.println("Exception in method: " + joinPoint.getSignature().getName());
    System.out.println("Exception: " + error);
}

@Around
지정된 포인트컷의 전후로 어드바이스를 실행한다. 메서드 실행 전, 후, 또는 실행 자체를 제어할 수 있는 가장 강력한 어노테이션이다.

@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
    Object result = joinPoint.proceed();  // 실제 메서드 실행
    System.out.println("After method: " + joinPoint.getSignature().getName());
    return result;
}

@DeclareParents
기존 클래스에 인터페이스를 구현하게 하거나 속성을 추가할 때 사용된다. 주로 클래스에 새로운 기능을 추가하는데 사용된다.

@DeclareParents(value="com.example.service.*+", defaultImpl=DefaultFeatureImpl.class)
public static FeatureInterface mixin;

📍 AOP로 구현할 수 있는 기능으로는 어떤것들이 있을까?

AOP를 활용하여 구현할 수 있는 기능들은 주로 애플리케이션 전반에 걸쳐 공통적으로 필요하지만, 핵심 비즈니스 로직과는 분리되어야 하는 관심사들이다.

주로 로깅이나 트랜잭션 관리, 보안 (인증인가와 같이 메서드나 클래스에 접근 권한을 제어하는 기능), 캐싱, 공통 예외 처리 등 다양하게 존재한다.


📍 AOP는 프록시 패턴을 사용했다. 프록시 패턴은 어떤 것일까?

프록시 단어 자체를 번역하면 '대리'라는 뜻이다.

즉, 특정 객체에 대한 접근을 제어하기 위해 그 객체의 대리자 역할을 하는 프록시 객체를 제공하는 패턴을 의미한다.

프록시는 원래 객체에 대한 인터페이스를 제공하면서, 그 객체에 대한 접근을 통제하거나 추가적인 기능을 수행할 수 있다.

아래는 프록시 패턴을 사용한 간단한 예제 코드이다.

// Subject 인터페이스
public interface Service {
    void request();
}

// RealSubject 클래스 (실제 서비스)
public class RealService implements Service {
    @Override
    public void request() {
        System.out.println("Executing real service request...");
    }
}

// Proxy 클래스
public class ProxyService implements Service {
    private RealService realService;

    @Override
    public void request() {
        if (realService == null) {
            realService = new RealService();  // 실제 서비스 객체를 생성
        }
        System.out.println("Logging: Proxy request");
        realService.request();  // 실제 서비스의 메서드를 호출
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        Service service = new ProxyService();
        service.request();  // 프록시를 통해 실제 서비스에 접근
    }
}

예를 들어, 로깅을 위해 AOP를 사용할 떄, 클라이언트는 원래 객체 대신 프록시 객체를 호출하게 되며, 이 프록시 객체는 실제 메서드를 호출하기 전에 로깅 작업을 수행하고, 그 후에 원래 객체의 메서드를 실행하게 된다.

이로써 핵심 비즈니스 로직과 부가 기능을 분리하면서도, 클라이언트 코드에는 이러한 분리를 인식하기 못하게 할 수 있다.

profile
기록하는 습관

0개의 댓글