개발기록

Spring AOP 내부 호출 문제 해결 방법 본문

Spring

Spring AOP 내부 호출 문제 해결 방법

Danuvibe 2024. 9. 10. 16:04

Spring AOP(Aspect-Oriented Programming)는 횡단 관심사를 효과적으로 분리할 수 있는 강력한 기능을 제공합니다. 하지만 내부 메서드 호출(self-invocation) 시 AOP가 적용되지 않는 문제가 있습니다. 이 글에서는 이 문제의 원인과 다양한 해결 방법을 예제 코드와 함께 상세히 살펴보겠습니다.

## 문제 상황

먼저, 문제가 발생하는 상황을 코드로 살펴보겠습니다.

```java
@Service
public class UserService {
    
    @Transactional
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
        // 사용자 생성 로직
    }

    public void registerUser(String username) {
        System.out.println("Registering user");
        this.createUser(username);  // 내부 호출
    }
}

@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.example.UserService.*(..))")
    public void logMethodCall(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }
}
```

이 경우, `createUser` 메서드를 직접 호출할 때는 AOP가 적용되어 로깅이 수행되지만, `registerUser` 메서드 내에서 `createUser`를 호출할 때는 AOP가 적용되지 않습니다.

## 해결 방법

### 1. 자기 자신을 주입받아 사용

Spring의 `@Autowired` 어노테이션을 사용하여 자기 자신을 주입받아 사용하는 방법입니다.

```java
@Service
public class UserService {
    
    @Autowired
    private UserService self;

    @Transactional
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }

    public void registerUser(String username) {
        System.out.println("Registering user");
        self.createUser(username);  // 프록시를 통한 호출
    }
}
```

이 방법은 간단하지만, 순환 참조 문제가 발생할 수 있으므로 주의가 필요합니다.

### 2. 별도의 내부 클래스로 분리

관련 메서드를 별도의 내부 클래스로 분리하고, 이를 빈으로 등록하여 사용하는 방법입니다.

```java
@Service
public class UserService {
    
    @Autowired
    private UserCreator userCreator;

    public void registerUser(String username) {
        System.out.println("Registering user");
        userCreator.createUser(username);
    }

    @Component
    public static class UserCreator {
        @Transactional
        public void createUser(String username) {
            System.out.println("Creating user: " + username);
        }
    }
}
```

이 방법은 코드를 더 모듈화할 수 있지만, 클래스 구조가 복잡해질 수 있습니다.

### 3. ApplicationContext를 이용해 프록시 객체 가져오기

Spring의 `ApplicationContext`를 사용하여 현재 객체의 프록시를 가져와 사용하는 방법입니다.

```java
@Service
public class UserService implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Transactional
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }

    public void registerUser(String username) {
        System.out.println("Registering user");
        UserService proxy = applicationContext.getBean(UserService.class);
        proxy.createUser(username);
    }
}
```

이 방법은 `ApplicationContext`에 대한 의존성이 생기지만, 프록시를 직접 가져와 사용할 수 있습니다.

### 4. AspectJ를 사용한 컴파일 타임 위빙

AspectJ를 사용하여 컴파일 시점에 AOP를 적용하는 방법입니다. 이 방법을 사용하려면 프로젝트 설정을 변경해야 합니다.

```java
@Service
@Aspect
public class UserService {
    
    @Transactional
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }

    public void registerUser(String username) {
        System.out.println("Registering user");
        this.createUser(username);  // AspectJ를 사용하면 이 호출에도 AOP가 적용됩니다.
    }

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

AspectJ를 사용하려면 `build.gradle` 또는 `pom.xml`에 AspectJ 관련 설정을 추가해야 합니다.

## 결론

Spring AOP의 내부 호출 문제는 프록시 기반 AOP의 한계로 인해 발생합니다. 위에서 소개한 방법들은 각각 장단점이 있으므로, 프로젝트의 요구사항과 구조에 맞는 방법을 선택하여 사용하는 것이 좋습니다.


어떤 방법을 선택하든, AOP의 동작 원리를 이해하고 적절히 사용하는 것이 중요합니다. 또한, 단위 테스트를 통해 AOP가 의도대로 동작하는지 반드시 확인해야 합니다.

Comments