티스토리 뷰

Spring

[SPRING] 로깅에 AOP 적용해보기

chaewonni 2024. 6. 1. 13:41

1. 기본 로깅의 한계

  • 중복된 로깅 코드로 인한 문제점
    //PostController
    
    @PostMapping("/posts")
    public ResponseEntity<SuccessStatusResponse> createPost(
            @RequestHeader Long memberId,
            @RequestHeader Long blogId,
            @Valid @RequestBody PostCreateRequest postCreateRequest) {

        log.info("Entering createPost: memberId={}, blogId={}, title={}, content={}",
                memberId, blogId, postCreateRequest.title(), postCreateRequest.content());

        ResponseEntity<SuccessStatusResponse> response = ResponseEntity.status(HttpStatus.CREATED)
                .header("Location", postService.create(memberId, blogId, postCreateRequest))
                .body(SuccessStatusResponse.of(SuccessMessage.POST_CREATE_SUCCESS));

        log.info("Exiting createPost: responseStatus={}, responseBody={}",
                response.getStatusCode(), response.getBody());

        return response;
    }
    
    
        @GetMapping("/posts/{postId}")
    public ResponseEntity<SuccessStatusResponse<PostFindDto>> findPost(@PathVariable Long postId) {
    
        log.info("Entering findPost: postId={}", postId);

        ResponseEntity<SuccessStatusResponse<PostFindDto>> response = ResponseEntity.status(HttpStatus.OK)
                .body(SuccessStatusResponse.of(
                        SuccessMessage.POST_FIND_SUCCESS,
                        postService.findPostById(postId)));

        log.info("Exiting findPost: postId={}, responseStatus={}, responseBody={}",
                postId, response.getStatusCode(), response.getBody());

        return response;
    }

메서드의 실행 과정에서 입력된 파라미터와 반환된 결과를 로깅하면,

콘솔에 이런식으로 출력된다.

++ 눈이 침침한 분들을 위해 (확대버전)

그러나

각 컨트롤러 메서드에 로그 문을 직접 삽입하는 방식은 코드 중복을 발생시킨다.

현재 **createPost**와 findPost 메서드 모두에서 메서드의 시작과 종료 시점에 로그를 기록하는 코드가 반복적으로 사용되고 있는데, 각 메서드에 로그 관련 코드를 추가하면 코드의 가독성과 관리가 어려워진다.

  • 유지보수의 어려움

로깅 로직이 비즈니스 로직과 혼재되어 있을 경우, 로깅 요구사항이 변경될 때마다 관련된 모든 메서드를 수정해야 한다.

예를 들어, 로그 포맷을 변경하거나 추가적인 정보를 로그에 포함시키고자 할 때, 모든 컨트롤러 메서드를 일일이 찾아 수정해야 하는 번거로움이 있다.

따라서 우리는 이러한 반복되는 작업을 분리하여 전처리 혹은 후처리가 가능하도록 AOP를 적용해보겠슴!!!

2. AOP란?

출처:  Incheol’s TECH BLOG

AOP(Aspect-Oriented Programming)는 공통 관심사를 추상화하여 중앙에 보관하고, 필요한 곳에 동적으로 삽입하여 적용하는 기술이다.

이 접근 방식을 통해 핵심 로직과 부가 기능을 분리할 수 있으며, 이로 인해 각 기능은 단일 책임 원칙을 유지하며 독립적인 코드로 구성된다.

이러한 구조는 프로그램의 가독성과 유지보수성을 크게 향상시킨다.

특히, 로깅과 같은 반복적이고 일반적인 작업을 처리하는 데 AOP는 매우 유용하다.

로깅을 AOP를 통해 구현하면, 메소드 호출의 시작과 종료 시점에 자동으로 로그를 기록하도록 설정할 수 있다.

이로써 개발자는 로그 관리가 필요한 모든 메소드에 직접 로그 코드를 삽입하지 않아도 되므로, 코드의 중복을 줄이고, 핵심 비즈니스 로직에 더 집중할 수 있다.

또한, 로깅 설정을 변경하거나 업그레이드할 때, 중앙 집중화된 로깅 설정만 수정하면 되기 때문에 변경 관리가 수월해진다.

3. AOP 적용

1) build.gradle에 의존성 추가

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

2) AOP 클래스 작성

//LogAop

// Lombok 어노테이션을 사용하여 로거 인스턴스를 자동으로 생성
@Slf4j
// 이 클래스를 Aspect로 선언하여 AOP 기능을 구현
@Aspect
@Component
public class LogAop {

		// Pointcut 정의: org.sopt.practice.controller 패키지 내의 모든 클래스와 모든 메소드를 대상으로 함
    @Pointcut("execution(* org.sopt.practice.controller.*.*(..))")
    public void controllerPointcut() {}

		// Before 어드바이스: 메소드 실행 전에 실행되며, 지정된 Pointcut에 해당하는 메소드에서 작동
    @Before("controllerPointcut()")
    public void logMethodCall(JoinPoint joinPoint) {
        Method method = getMethod(joinPoint);
        // 메소드 진입 시 메소드 이름을 로깅
        log.info("===== Entering method: {} =====", method.getName());
        // 메소드 파라미터를 로깅
        logParameters(joinPoint);
    }

    // AfterReturning 어드바이스: 메소드가 값을 반환한 후 실행되며, 지정된 Pointcut에 해당하는 메소드에서 작동
    @AfterReturning(value = "controllerPointcut()", returning = "returnObj")
    public void logMethodReturn(JoinPoint joinPoint, Object returnObj) {
        Method method = getMethod(joinPoint);
        // 메소드 반환 시 메소드 이름을 로깅
        log.info("===== Returning method: {} =====", method.getName());
        // 반환된 값의 타입과 값을 로깅
        logReturnValue(returnObj);
    }

    // JoinPoint에서 메소드 정보를 추출하는 메소드
    private Method getMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod();
    }

    // 메소드 파라미터를 로깅하는 메소드
    private void logParameters(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        if (args.length == 0) {
            log.info("No parameters");
        } else {
            for (int i = 0; i < args.length; i++) {
                log.info("Parameter name: {}, type: {}, value: {}",
                        parameterNames[i],
                        args[i].getClass().getSimpleName(),
                        args[i]);
            }
        }
    }

    // 반환된 객체를 로깅하는 메소드
    private void logReturnValue(Object returnObj) {
        if (returnObj != null) {
            log.info("Return type: {}, value: {}", returnObj.getClass().getSimpleName(), returnObj);
        } else {
            log.info("Return value is null");
        }
    }
}

@Aspect

  • @Aspect는 AOP 설정 클래스를 정의할 때 사용하는 어노테이션이다. 이 어노테이션을 사용함으로써 클래스가 Aspect, 즉 관점을 정의하는 컴포넌트임을 나타낸다.

Pointcut

  • Pointcut은 Aspect의 적용 위치를 결정하는 표현식이다. 이를 통해 특정 조건(패키지, 클래스, 메소드, 어노테이션 단위 등)에 따라 Aspect가 적용될 JoinPoint를 필터링한다. 즉, Pointcut은 Aspect가 적용될 수 있는 잠재적인 지점들 중에서 실제 적용될 지점을 선택하는 필터 역할을 한다.

JoinPoint

  • JoinPoint는 프로그램 실행 중 Aspect가 적용될 수 있는 실제 지점이다. 메소드 호출, 예외 발생, 필드 접근 등 다양한 실행 지점이 이에 해당한다. JoinPoint는 런타임에 메소드 정보를 포착하며, Pointcut에 의해 선별된 특정 지점에 대한 정보를 제공한다.

어드바이스 타입

@Before

  • 지정된 JoinPoint(대부분 메소드 호출) 전에 실행된다. 이 어드바이스는 메소드 실행 전에 필요한 준비 작업을 수행할 수 있게 한다.

@AfterReturning

  • 메소드가 예외 없이 성공적으로 결과를 반환한 후 실행된다. 이 어드바이스는 메소드의 반환 값을 처리하거나, 필요한 후처리를 수행할 때 사용된다.

3) 테스트 ㄱㄱ

    //PostController
    
    @PostMapping("/posts")
    public ResponseEntity<SuccessStatusResponse> createPost(
            @RequestHeader Long memberId,
            @RequestHeader Long blogId,
            @Valid @RequestBody PostCreateRequest postCreateRequest) {
        return ResponseEntity.status(HttpStatus.CREATED).header(
                "Location",
                postService.create(memberId, blogId, postCreateRequest))
                .body(SuccessStatusResponse.of(SuccessMessage.POST_CREATE_SUCCESS));
    }

    @GetMapping("/posts/{postId}")
    public ResponseEntity<SuccessStatusResponse<PostFindDto>> findPost(@PathVariable Long postId) {
        return ResponseEntity.status(HttpStatus.OK).body(SuccessStatusResponse.of(
                SuccessMessage.POST_FIND_SUCCESS,
                postService.findPostById(postId)));
    }

log를 제거한 원래의 controller로 다시 돌려주고 (AOP 클래스로 이미 로깅 적용 되어있기 때문에),

 

먼저 @PostMapping("/posts")

메서드 호출하면..!!

로그가 잘 찍혔다. ㅎ (중간 SQL 쿼리 로그는 생략)

 

@GetMapping("/posts/{postId}")

메서드 호출하면..!!

뒤에가 좀 잘렸지만 로그가 잘 찍혔다. ㅎ (중간 SQL 쿼리 로그는 생략)

++ 눈이 침침한 분들을 위해(확대버전)

뒤에가 좀 잘리긴 하지만 이런식으로 로그가 잘 찍힌답니당

자 이를 통해 AOP를 사용하여 로깅을 구현하면 코드의 중복을 제거하고, 비즈니스 로직에서 로깅 코드를 분리하여 전체 코드의 깔끔함과 유지보수성을 향상시킬 수 있음을 알게 되었습니다!!!!!!

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함