Aspect Oriented Programming의 약자로, 관점 지향 프로그래밍
어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다!!
예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
모든 핵심 기능에 부가기능을 추가해야 된다고 생각할 때, 핵심 기능의 수가 너무 많으면 그 핵심 기능에 하나하나 부가기능을 넣어야 하고, 이때 실수를 할 가능성과 추후에 부가기능을 유지 보수함에 있어서 어려움이 생긴다.
이때 AOP는 다른 관점(Aspect)을 가진 부가기능과 핵심 기능을 분리하여 부가기능 중심으로 설계, 구현하도록 도와준다.
OOP는 핵심 기능을 모듈화 하는 프로그래밍
AOP는 부가기능(로깅, 트랜잭션 ...)을 모듈화하는 프로그래밍
따라서 AOP는 OOP를 대체해 주는 것이 아닌 보완 해주는 프로그래밍이다.
스프링(디스패쳐 서블릿)이 핵심 기능에 접근하기 전에, 프록시 객체를 중간에 삽입하는 원리로 AOP가 동작한다.
@Component // 스프링 IoC 에 빈으로 등록
@Aspect
public class UserTimeAop {
private final UserTimeRepository userTimeRepository;
public UserTimeAop(UserTimeRepository userTimeRepository) {
this.userTimeRepository = userTimeRepository;
}
@Around("execution(public * com.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
Object output = joinPoint.proceed();
return output;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원이 없는 경우, 수행시간 기록하지 않음
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
// 로그인 회원 -> loginUser 변수
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
User loginUser = userDetails.getUser();
// 수행시간 및 DB 에 기록
UserTime userTime = userTimeRepository.findByUser(loginUser);
if (userTime != null) {
// 로그인 회원의 기록이 있으면
long totalTime = userTime.getTotalTime();
totalTime = totalTime + runTime;
userTime.updateTotalTime(totalTime);
} else {
// 로그인 회원의 기록이 없으면
userTime = new UserTime(loginUser, runTime);
}
System.out.println("[User Time] User: " + userTime.getUser().getUsername() + ", Total Time: " + userTime.getTotalTime() + " ms");
userTimeRepository.save(userTime);
}
}
}
}
@Around 어노테이션을 통해 AOP를 적용할 범위를 정해준다.
joinPoint.proceed(); 로 핵심 기능을 수행한다.
Spring AOP는 기본적으로 프록시 방식으로 동작한다. 프록시 패턴이란 어떤 객체를 사용하고자 할 때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 말한다.
그렇다면 Spring은 왜 Target 객체를 직접 참조하지 않고 프록시 객체를 사용할까?
프록시 객체 없이 Target 객체를 사용하고 있다고 생각해보자. Aspect 클래스에 정의된 부가 기능을 사용하기 위해서, 우리는 원하는 위치에서 직접 Aspect 클래스를 호출해야 한다. 이 경우 Target 클래스 안에 부가 기능을 호출하는 로직이 포함되기 때문에, AOP를 적용하지 않았을 때와 동일한 문제가 발생한다. 여러 곳에서 반복적으로 Aspect를 호출해야 하고, 그로 인해 유지보수성이 크게 떨어진다.
그래서 Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다. 이렇게 하면 Target에서 Aspect을 알 필요 없이 순수한 비즈니스 로직에 집중할 수 있다.
예를 들어 다음 코드의 logic() 메서드가 Target이라면,
public interface TargetService{
void logic();
}
@Service
public class TargetServiceImpl implements TargetService{
@Override
public void logic() {
...
}}
Proxy에서 Target 전/후에 부가 기능을 처리하고 Target을 호출한다.
@Service
public class TargetServiceProxy implements TargetService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceImpl();
...
@Override
public void logic() {
// Target 호출 이전에 처리해야하는 부가 기능
// Target 호출
targetService.logic();
// Target 호출 이후에 처리해야하는 부가 기능
}
}
사용하는 입장에서는 Target 객체를 사용하는 것처럼 Proxy 객체를 사용할 수 있다.
@Service
public class UseService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceProxy();
...
public void useLogic() {
// Target 호출하는 것처럼 부가 기능이 추가된 Proxy를 호출한다.
targetService.logic();
}
}
트랜잭션을 적용하기 위해선 트랜잭션 매니저 객체를 통해 트랜잭션을 만들어준 뒤, try, catch문으로 트랜잭션이 성공하면 commit, 실패하면 rollback을 적용해 줘야 한다.
하지만 스프링에선 @Transactional 이라는 어노테이션을 선언해 주면, 트랜잭션에 대한 프록시 객체를 자동으로 만들어주어 따로 트랜잭션에 대한 코드를 구현하지 않아도 트랜잭션이 적용된다.
클래스 생성
public class ApiRequestException extends IllegalArgumentException {
public ApiRequestException(String message) {
super(message);
}
public ApiRequestException(String message, Throwable cause) {
super(message, cause);
}
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = { ApiRequestException.class })
public ResponseEntity<Object> handleApiRequestException(ApiRequestException ex) {
ApiException apiException = new ApiException(
ex.getMessage(),
// HTTP 400 -> Client Error
HttpStatus.BAD_REQUEST
);
return new ResponseEntity<>(
apiException,
// HTTP 400 -> Client Error
HttpStatus.BAD_REQUEST
);
}
}