본문 바로가기
개발공부 개발새발/Spring

Spring Boot 3.x ) 로그를 멋지게 처리하는 법

by 휴일이 2024. 12. 6.

요즘 혼자 인증용 서버를 하나 만들고 있는데

로그를 좀 더 멋지게 처리할 수 있는 방법이 있을까 해서 찾아보았다.

 

AOP 와 핸들러를 이용하면

실패/성공 로그 모두 멋지게 처리할 수 있다는 사실 !

 

 

 

일단 중요한 "실패 로그" 부터 어떻게 처리하는지 살펴보자!

 

 

실패 로그

예외 상황 발생 시, throws 이전에 로그를 찍는 것보다 더 좋은 방법이 있다.

그건 `@ControllerAdvice` 에서 실행하면 되는데, 일단 예외 수정부터 필요하다.

 

DefaultException

import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.util.Map;

@Getter
public class DefaultException extends RuntimeException {
    protected HttpStatus httpStatus;
    protected String message;
    protected Map<String, Object> data;

    public DefaultException(HttpStatus status, String message, Map<String, Object> data) {
        super(message);
        this.message = message;
        this.httpStatus = status;
        this.data = data;
    }

}

일단 RuntimeException 을 상속받아 모든 사용자 정의 예외가 공통적으로 상속받을 공용 예외 객체를 만들어준다.

 

 

ApiErrorResponse

@Builder
public record ApiErrorResponse(
        Integer code,
        HttpStatus status,
        String message,
        LocalDateTime timestamp
) {

    public static ApiErrorResponse of(DefaultException e) {
        return ApiErrorResponse.builder()
                .message(e.message)
                .code(e.httpStatus.value())
                .status(e.httpStatus)
                .timestamp(LocalDateTime.now())
                .build();
    }
}

그리고 예외 발생 시, 어떤 정보가 필요한지 고심하여 예외 발생 시 클라이언트에게 응답해줄 응답 객체를 하나 만들었다.

응답 코드와 status, 메시지 등이 적혀있음!

 

 

GlobalExceptionHandler

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DefaultException.class)
    public ResponseEntity<ApiErrorResponse> globalException(DefaultException e) {
        log.error("[Error] : {} - {} - {}",
                e.httpStatus,
                e.message,
                e.data);

        return ResponseEntity.status(e.httpStatus)
                .body(ApiErrorResponse.of(e));
    }
}

 

그리고 대망의 핸들러!

핸들러에서는 공통 예외를 처리할 수 있는데,

아까 만들어두었던 DefaultException 클래스에서 나오는 모든 예외를 이 핸들러로 처리하도록 한다.

로그에는 예외 status 와 메시지, 참고할만한 데이터를 넣어두도록 했으며, 응답은 내가 원하는 status 로 보내도록 한다.

 

 

 

자, 이제 사용자 정의 예외를 작성해볼 시간이다! (두구두구)

 

MemberException (사용자 정의 예외)

import me.holiday.common.exception.DefaultException;
import org.springframework.http.HttpStatus;

import java.util.Map;

public class MemberException extends DefaultException {
    public MemberException(HttpStatus status, String message, Map<String, Object> data) {
        super(status, message, data);
    }
}

 

나는 멤버 도메인 관련 인증이 되지 않았다면(또는 기타 등등) 예외를 발생 시키는 MemberException 클래스를 하나 만들었다.

여기에서는 HttpStatus, message(응답, 로깅 메시지), 혹시 응답에 필요한 데이터가 있다면 그 데이터도 받도록 한다.

 

 

 

자, 이제 에러 상황을 만들어보자.

    public SignInRes signIn(SignInReq dto) {
        Member member = findByUsername(dto.username())
                .orElseThrow(() -> new MemberException(
                        HttpStatus.NOT_FOUND,
                        "로그인 실패",
                        Map.of("username", dto.username())
                ));

        // 비밀 번호 검증
        member.validPwd(dto.password(), passwordEncoder);

        return new SignInRes("accessToken", "refreshToken");
    }

유저가 로그인을 했는데 유저 아이디가 없다면 예외를 발생시키는 로직이다.

 

 

잘 보면 MemeberException 을 발생시킬때,

404 Not Found, 로그인 실패(응답 메시지), username 을 Map 으로 보내주는 것이 보인다.

 

 

 

 

MemberException 은 DefaultException 을 상속받았기 때문에 이 핸들러가 처리할 수 있음.

에러 로그와 ResponseEntity 로 알맞은 응답을 보내준다.

 

 

 

그러면 어떻게 되냐?

 

 

 

클라이언트 응답

 

 

에러 로그

 

 

 

이렇게 로그를 따로 찍지 않아도 이쁘게 로그가 나온다.

 

 

 

그런데 성공 로그를 찍고 싶다면?

스프링의 AOP 기능을 사용할 수 있는 @Ascept 애노테이션을 이용하면 된다!

 

 

성공 로그

@LogExecution("로그인 요청")
public SignInRes signIn(SignInReq dto) {
    Member member = findByUsername(dto.username())
            .orElseThrow(() -> new MemberException(
                    HttpStatus.NOT_FOUND,
                    "로그인 실패",
                    Map.of("username", dto.username())
            ));

    // 비밀 번호 검증
    member.validPwd(dto.password(), passwordEncoder);

    return new SignInRes("accessToken", "refreshToken");
}

 

사실 내 로그인 서비스에는 @LogExecution 이라는 사용자 정의 애노테이션이 붙어 있다.

이 애노테이션을 사용하면, Service 가 성공했을 때도 다른 처리 없이 로그를 찍을 수 있다.

 

 

@LogExecution

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
    String message() default "";
}

 

로그가 필요한 메서드에 달아주는 애노테이션을 하나 만들어준다.

메서드 단에만 붙이기 때문에 메서드에만 붙일 수 있게 만들어줬고, message 에서 메시지를 받아 보낸다.

 

 

LogAspect

package me.holiday.common.annotation.log;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class LogAspect {
    @Pointcut("@annotation(logExecution)")
    public void logPointcut(LogExecution logExecution) {}

    @AfterReturning(pointcut = "logPointcut(logExecution)", returning = "result")
    public void logAfterExecution(JoinPoint joinPoint, LogExecution logExecution, Object result) {
        String methodName = joinPoint.getSignature().getName();
        String logMessage = logExecution.message();

        log.info("[{}] 성공 - {}, {}",
                methodName,
                logMessage,
                result);
    }
}

 

이 클래스가 중요한데, 애노테이션이 달렸을 때 어떤 행위를 하는지 정의해준다.

메서드 이름과 로그 메시지를 받아 성공 로그를 작성해준다.

Object 는 만약 성공 시에 어떤 객체를 반환할 경우, 응답 객체 안에 뭐가 들었는지 알려준다.

 

이 두 클래스만 만들어두면 간단하게 성공 로그를 처리할 수 있다.

 

 

성공 시에 200 OK 가 뜨고

 

 

이런 식으로 로그도 뜨는 걸 확인 가능하다.

 

 

 

 

이렇게 사용하면 굳이 하나하나 로그를 찍지 않아도 대부분의 로그를 자동으로 찍을 수 있고

정말 필요한 때만 명시하여 로그를 써줄 수 있기 때문에 매우 편리하다.

^_^여러분도 로그 하나하나 쓰지 마시고 편리하게 이용하세요잉~! 아따 좋으다~!

 

728x90