요즘 혼자 인증용 서버를 하나 만들고 있는데
로그를 좀 더 멋지게 처리할 수 있는 방법이 있을까 해서 찾아보았다.
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 가 뜨고
이런 식으로 로그도 뜨는 걸 확인 가능하다.
이렇게 사용하면 굳이 하나하나 로그를 찍지 않아도 대부분의 로그를 자동으로 찍을 수 있고
정말 필요한 때만 명시하여 로그를 써줄 수 있기 때문에 매우 편리하다.
^_^여러분도 로그 하나하나 쓰지 마시고 편리하게 이용하세요잉~! 아따 좋으다~!
'개발공부 개발새발 > Spring' 카테고리의 다른 글
Spring Security) permitAll() 의 비밀... (0) | 2023.01.21 |
---|---|
Spring Security ) JWT와 Session 인증 중에 무엇을 사용해야 좋을까? (0) | 2023.01.06 |
Spring) 궁금한 것들 목록...(고민해결안됨) (0) | 2023.01.05 |
Spring, Boot) maven 버전 선택 꿀팁 (0) | 2022.11.24 |
Spring) PasswordEncoder를 사용하기 위한 준비 (0) | 2022.11.16 |