AOP를 활용한 로그 저장 시스템

개발하는 구황작물·2023년 10월 1일
1

회사 프로젝트

목록 보기
4/8
post-custom-banner

본 글은 AOP에 대해 어느정도 이해를 했다는 전제 하에 적은 글입니다.

회사 프로젝트 진행 중 로그 저장 및 조회 기능을 만들어야 했다.

처음에는 logService 하나를 만든 후 로그를 저장해야 하는 곳에 일일이 DI를 하자고 지시가 내려왔으나

이걸 언제 다 붙이고 다니기도 귀찮고 코드 섞이는 것도 보기 싫어서

AOP랑 인터셉터 둘 중 하나를 쓰자고 제안했다.

참고 : 회사 코드를 유출할 수는 없어서 거의 새로운 코드로 다시 재 작성했습니다.

AOP

AOP는 어떤 로직을 기준으로 핵심 관점, 부가적인 관점으로 나누어 보고, 그 관점을 기준으로 각각 모듈화 하여 프로그래밍 한다는 것을 뜻한다.

여러 비즈니스 로직에 위치한 부가적인 기능(보안, 로깅....)은 비즈니스 로직 사이사이에 사용되어 코드 중복을 많이 발생시킨다.

이를 해결하기 위해 Aspect로 모둘화하고, 비즈니스 로직에서 분리하여 소스 코드 중복을 줄이고, 필요할 때마다 가져다 쓰게 하는 기술이다.

AOP vs Interceptor

Interceptor를 간략하게 설명하자면

Interceptor란 컨트롤러에 들어오는 요청 HttpRequest와 컨트롤러가 응답하는 HttpResponse를 가로채는 역할을 합니다.

...라고 설명되어 있다.

처음에는 Interceptor를 사용하려 했으나

클라이언트가 생각보다 로그를 자세하게 남기기를 원해서

컨트롤러에만 적용할 수 있는 Interceptor 대신 더 세밀하게 작업할 수 있는 AOP를 활용하기로 하였다.

코드 적용

  1. Dependency
implementation 'org.springframework.boot:spring-boot-starter-aop'

build.gradle에 이거 하나 추가해주면 된다.

  1. @EnableAspeceJAutoProxy 부착
@EnableAspectJAutoProxy //aop
@SpringBootApplication
public class XXApplication {

	public static void main(String[] args) {
		SpringApplication.run(XXApplication.class, args);
	}
}

AOP사용을 위해 @EnableAspectJAutoProxy를 Application.java에 부착해준다.

  1. PointCut

Pointcut를 간략히 설명하자면 AOP에서 Advice를 타깃의 코드 중 어디에 적용할지를 뜻하는 AOP 용어이다.

기본적으로 아래와 같이 사용이 된다.

@Around("execution(* com.example.domain.AService(..))")
    public void exampleAdvice(ProceedingJoinPoint joinPoint) {
    ...
    }

그러나 Pointcut가 생각보다 많은 곳에 적용을 해야 되어 도메인별로 포인트컷을 모아놓고 사용하기로 결정하였다.

public class APointcuts {
    @Pointcut("execution(* com.example.domain.AService.createA(..))")
    public void createA() {}

    @Pointcut("execution(* com.example.domain.AService.deleteAById(..))")
    public void deleteA() {}

    @Pointcut("execution(* com.example.domain.AService.updateA(..))")
    public void updateA() {}
}
  1. Advice에 Pointcut부착
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AServiceLogAspectj {
    private final LogService logService;

    @AfterReturning(value = "com.example.global.log.aop.pointcut.APointcuts.createA()", returning = "returnObj")
    public void createA(JoinPoint joinPoint, Object returnObj) {
    	Long a_Id = (Long) returnObj;
        String message = "A 저장됨, id : " + a_id;
        
        LogModel logModel = LogModel.builder()
        .message(message)
        .build();
        logService.save(logModel);
    }
    
    @AfterReturning(value = "com.example.global.log.aop.pointcut.APointcuts.deleteA()", returning = "returnObj")
    public void deleteA(JoinPoint joinPoint, Object returnObj) {
        // AService.updateA()의 파라미터 추출
        Object[] args = joinPoint.getArgs();
        Long a_id = (Long) args[0];
        
        
        String message = "A 삭제됨, id : " + a_id;
        
        LogModel logModel = LogModel.builder()
        .message(message)
        .build();
        logService.save(logModel);
    }
    
    @AfterReturning(value = "com.example.global.log.aop.pointcut.APointcuts.updateA()", returning = "returnObj")
    public void updateA(JoinPoint joinPoint, Object returnObj) {
        Object[] args = joinPoint.getArgs();
        Long a_id = (Long) args[0];
        ARequestDto a = (ARequestDto) args[1];
        
        String message = "A 수정됨, " + "name : " + a.getName + ", title : " + a.getTitle() + ", id : " + a_id;
        
        LogModel logModel = LogModel.builder()
        .message(message)
        .build();
        logService.save(logModel);
        
    }

}
profile
어쩌다보니 개발하게 된 구황작물
post-custom-banner

0개의 댓글