AOP 활용할 부분을 생각해보기
Spring Boot 프로젝트에서 AOP를 적용하는 부분들이 보통 어떤 부분들인지 궁금했다. 어디에서 사용해야겠다 라는 것 자체가 시나리오를 고려하는 과정인데, 어떤 시나리오에 적합할 지 감이 안왔다. 통상적으로 AOP를 적용하는 사례들을 조사했다.
1.
로깅(Logging): 가장 일반적으로 사용되는 AOP 시나리오 중 하나는 로깅으로 애플리케이션의 여러 부분에서 로그를 일관되게 남기려는 경우 AOP를 사용 할 수 있다.
•
@Before, @After, 또는 @Around Advice를 사용하여 메서드 호출 전후에 로그를 남길 수 있다. 실행 상태등을 알 수 있다.
•
메서드 파라미터와 반환값을 로그에 포함시킬 수 있다. 정상적인 데이터가 이동되는지 확인 할 수 있다.
2.
트랜잭션 관리(Transaction Management): 트랜잭션을 관리하기 위해 AOP를 사용할 수 있다. 예를 들어, 특정 메서드 또는 클래스에 트랜잭션 관리 로직을 적용하여 데이터베이스 트랜잭션을 시작하고 커밋 또는 롤백할 수 있다.
•
@Around Advice를 사용하여 트랜잭션 시작과 종료 로직을 구현할 수 있다.
•
Spring의 @Transactional 어노테이션과 함께 사용하여 트랜잭션을 설정할 수 있다.
3.
인증 및 권한 부여(Authentication and Authorization): AOP를 사용하여 인증 및 권한 부여 관련 로직을 처리할 수 있다. 예를 들어, 특정 메서드가 실행되기 전에 사용자 인증을 확인하고 권한을 검사할 수 있다.
•
@Before Advice를 사용하여 인증 및 권한 검사를 수행할 수 있다.
•
Spring Security와 연계하여 보다 고급 인증 및 권한 부여 기능을 구현할 수 있다.
4.
캐싱(Caching): AOP를 사용하여 메서드 호출 결과를 캐싱할 수 있다. 이를 통해 동일한 메서드 호출의 반복 작업을 줄이고 성능을 개선할 수 있다.
•
@Around Advice를 사용하여 메서드 호출 전에 캐시를 확인하고, 결과를 캐시에 저장할 수 있다.
•
Spring의 @Cacheable, @CachePut, @CacheEvict와 함께 사용하여 캐싱을 구현할 수 있다.
5.
예외 처리(Exception Handling): AOP를 사용하여 예외 처리 로직을 중앙에서 관리할 수 있다. 특히, 여러 메서드에서 공통으로 발생하는 예외를 처리하는 데 유용하다.
•
@AfterThrowing Advice를 사용하여 예외가 발생할 때 공통 로직을 수행할 수 있다.
•
예외 처리, 로깅 또는 사용자 지정 에러 메시지 생성을 수행할 수 있다.
6.
메서드 실행 시간 측정(Measuring Method Execution Time): AOP를 사용하여 메서드의 실행 시간을 측정하고 성능 통계를 수집할 수 있다.
•
@Around Advice를 사용하여 메서드 실행 전과 후의 시간을 측정할 수 있다.
•
성능 통계 데이터를 수집하여 로그 또는 모니터링 시스템에 기록할 수 있다.
위 케이스를 적용하기 전 테스트로 중복 코드제거 해보기
컨벤션을 맞춘다치고 주요 기능인 MenuService, OrderService, ReviewService, StoreService 클래스에 클래스 내 중복을 제거하는 메소드를 만들어 두었었다. 하지만 이 메소드 자체가 위 4개의 서비스 클래스마다 중복되기 때문에 AOP를 통해 모듈화 한다면 효과적으로 중복을 제거 할 수 있다 판단했다.
@Service
@RequiredArgsConstructor
public class ReviewService {
...
// 중복 코드 제거를 위한 메소드
private ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto(ex.getMessage(), 400), HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto("서비스 요청 중 오류가 발생했습니다.", 500), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
...
}
Java
복사
[1] 의존성 추가
Spring AOP를 사용하기 위해서는 의존성을 추가해야 한다. build.gradle 에 해당 의존성을 추가해준다.
dependencies {
//SpringBoot AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
//THYMELEAF
implementation
...
}
Java
복사
[2] AOP 프록시 활성화(AOP를 앱에 활성화)
Spring Boot에서 AOP를 활성화하려면 @EnableAspectJAutoProxy 어노테이션을 사용하여 AOP 프록시를 활성화해야 해당 프로젝트에서 AOP가 실행 될 수 있는 환경이 제공된다.
@EnableJpaAuditing
@SpringBootApplication
@EnableAspectJAutoProxy //<-- AOP활성화
public class SpringCafeserviceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCafeserviceApplication.class, args);
}
}
Java
복사
Aspect 클래스 작성
AOP를 사용하여 적용할 로직을 담은 Aspect 클래스를 작성해야 한다.
이 클래스에는
어디, 에서 AOP가 작동할지를 정의하는 포인트컷
언제, 수행될 어드바이스(Advice) 메서드가 포함된다.
내 첫번째 목표는 위 중복제거 메소드 자체가 또 서비스마다 중복되는 것을 제거하기 위함으로 동일한 로직을 담은 ServiceExceptionHandlerAspect 클래스를 생성해보았다.
@Aspect // [1] 관점(Aspect) 정의 부분 - 이 클래스가 Aspect클래스임을 알림
@Component // [2] 스프링 빈으로 등록 - IoC컨테이너에 Bean으로 등록
public class ServiceExceptionHandlerAspect {
// [3] AOP 포인트컷 정의 - 실행 위치
@Pointcut("execution(* com.sparta.springcafeservice..*Service.*(..))") // com.sparta.springcafeservice내 하위 *Service라는 파일들은 모두 이 AOP가 적용
public void targetAllServiceMethods(){
}
// [4] AOP 어드바이스 정의 - 포인트컷에서 실제 동작하는 코드(구현체)
/*
Around=메서드 호출 전/후/예외 발생 시점
Before=메서드 호출 전
After=메서드 호출 후
AfterReturning - 정상적 반환 이후
AfterThrowing - 예외 발생 이후
*/
// [4-1] Service 클래스에서 반환을 통일시키는 부가기능
@Around("targetAllServiceMethods() && args(action)")
public ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto(ex.getMessage(), 400), HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto("서비스 요청 중 오류가 발생했습니다.", 500), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Java
복사
이에 따른 중복 메소드 부분 리팩토링
이제 이 Aspect를 통해 각종 Service 메소드들의 반환이 연결된다. 이 말은 기존 Service 클래스에 중복을 제거하기 위해 선언되었던 메소드들은 사용할 필요가 없으며, 그 메소드를 호출했던 각 서비스들의 연결 부분 또한 필요가 없다는 것이다.
이에 따라 *Service.java 내 handleServiceRequest() 메소드를 지워준다(왜냐하면 이제부턴 Aspect가 관리하니까)
또한 기존 메소드는 내부 클래스에서 정의되었던 반환을 관리하던handleServiceRequest() 메소드를 호출하도록 정의되어있었기 때문에 기본형태로 다시 돌려주었다.
아래는 AOP적용 전 기존 코드이며,
// 선택 리뷰 삭제
@Transactional
public ResponseEntity<StatusResponseDto> deleteReview(Long id, User user) {
return handleServiceRequest(() -> {
Review review = checkReviewExist(id);
validateUserAuthority(user.getId(), review.getUser());
reviewRepository.delete(review);
return new StatusResponseDto("리뷰가 삭제되었습니다.", 200);
});
}
...
// 중복 코드 제거를 위한 메소드
private ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto(ex.getMessage(), 400), HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto("서비스 요청 중 오류가 발생했습니다.", 500), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Java
복사
아래는 Aspect를 통해 중복 코드 제거를 모듈화시켜서 기본형태로 돌아온 코드이다.
// 선택 리뷰 삭제
@Transactional
public StatusResponseDto deleteReview(Long id, User user) {
Review review = checkReviewExist(id);
validateUserAuthority(user.getId(), review.getUser());
reviewRepository.delete(review);
return new StatusResponseDto("리뷰가 삭제되었습니다.", 200);
}
Java
복사
이 과정에서 생긴 트러블슈팅
처음에 Aspect 클래스에서 어드바이스 정의 부분은 다음과 같았다.
// [3] AOP 포인트컷 정의 - 실행 위치
@Pointcut("execution(* com.sparta.springcafeservice..*Service.*(..))") // com.sparta.springcafeservice내 하위 *Service라는 파일들은 모두 이 AOP가 적용
public void targetAllServiceMethods(){
}
// [4-1] Service 클래스에서 반환을 통일시키는 부가기능
@Around("targetAllServiceMethods()")
public ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
...
Java
복사
이와 같이 어드바이스의 포인트컷(실행위치)는 단순히 위에 정의한 위치를 가져다 사용했지만, 다음과 같은 오류를 나타냈다.
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-09-24T14:44:17.628+09:00 ERROR 68076 --- [ main] o.s.boot.SpringApplication : Application run failed
Caused by: org.springframework.beans.factory
.BeanCreationException:
Error creating bean with
name 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration':
error at ::0 formal unbound in pointcut
Java
복사
"Unbound pointcut parameter 'action'" 오류는 포인트컷 표현식에서 action 매개변수를 참조하는데 해당 매개변수가 정의되지 않았을 때 발생하는 오류였다.
action 매개변수?
args(action) 부분은 action 매개변수를 인식하도록 하는 것으로 Spring AOP가 action 매개변수를 캡처하고 전달할 수 있도록 설정해줘야 한다.
쉽게 말하면, Service에 구현된 실제 메소드에 전달되고 사용되는 매개변수들
”public StatusResponseDto deleteReview(Long id, User user) {”
이 Aspect에 구현된 모듈화된 메소드에도 인지되어야하고 사용, 연결되어야 하기 때문이다.
따라서 어드바이스 어노테이션에서 포인트컷에 추가된 action 매개변수 표현은 다음과 같다.
// [4-1] Service 클래스에서 반환을 통일시키는 부가기능
@Around("targetAllServiceMethods() && args(action)")
public ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto(ex.getMessage(), 400), HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto("서비스 요청 중 오류가 발생했습니다.", 500), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Java
복사
참조자료