Skip to content

Commit e2695c6

Browse files
Merge pull request #216 from DevKor-github/mod/logging
[Refact] API 로그 저장 비동기 처리 및 최적화
2 parents 80f536d + 9013720 commit e2695c6

File tree

9 files changed

+129
-104
lines changed

9 files changed

+129
-104
lines changed
Lines changed: 51 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package devkor.ontime_back;
22

3+
import devkor.ontime_back.dto.RequestInfoDto;
34
import devkor.ontime_back.entity.ApiLog;
45
import devkor.ontime_back.repository.ApiLogRepository;
56
import devkor.ontime_back.response.GeneralException;
7+
import devkor.ontime_back.service.ApiLogService;
68
import jakarta.servlet.http.HttpServletRequest;
9+
import lombok.RequiredArgsConstructor;
710
import lombok.extern.slf4j.Slf4j;
811
import org.aspectj.lang.JoinPoint;
912
import org.aspectj.lang.ProceedingJoinPoint;
@@ -13,48 +16,36 @@
1316
import org.aspectj.lang.annotation.Pointcut;
1417
import org.aspectj.lang.reflect.MethodSignature;
1518
import org.springframework.http.ResponseEntity;
19+
import org.springframework.security.access.AccessDeniedException;
1620
import org.springframework.security.core.Authentication;
1721
import org.springframework.security.core.context.SecurityContextHolder;
1822
import org.springframework.stereotype.Component;
23+
import org.springframework.web.bind.MethodArgumentNotValidException;
1924
import org.springframework.web.bind.annotation.PathVariable;
2025
import org.springframework.web.bind.annotation.RequestBody;
2126
import org.springframework.web.context.request.RequestContextHolder;
2227
import org.springframework.web.context.request.ServletRequestAttributes;
2328

2429
import java.lang.annotation.Annotation;
30+
import java.util.Map;
2531

2632

2733
@Slf4j
2834
@Aspect
2935
@Component
36+
@RequiredArgsConstructor
3037
public class LoggingAspect {
3138

32-
private final ApiLogRepository apiLogRepository;
33-
34-
public LoggingAspect(ApiLogRepository apiLogRepository) {
35-
this.apiLogRepository = apiLogRepository;
36-
}
37-
39+
private final ApiLogService apiLogService;
40+
private static final String NO_PARAMS = "No Params";
41+
private static final String NO_BODY = "No Body";
3842

3943
@Pointcut("bean(*Controller)")
4044
private void allRequest() {}
4145

4246
@Around("allRequest()")
4347
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
44-
45-
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
46-
47-
// requestUrl
48-
String requestUrl = request.getRequestURI();
49-
// requestMethod
50-
String requestMethod = request.getMethod();
51-
// userId
52-
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
53-
String userId = (authentication != null && authentication.isAuthenticated())
54-
? authentication.getName() // 인증된 사용자의 이름 (주로 ID로 사용됨)
55-
: "Anonymous";
56-
// clientIp
57-
String clientIp = request.getRemoteAddr();
48+
RequestInfoDto requestInfoDto = extractRequestInfo();
5849

5950
// requestTime
6051
long beforeRequest = System.currentTimeMillis();
@@ -91,21 +82,14 @@ public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
9182

9283
// 정상 요청 로그 저장
9384
long timeTaken = System.currentTimeMillis() - beforeRequest;
94-
ApiLog apiLog = ApiLog.builder().
95-
requestUrl(requestUrl).
96-
requestMethod(requestMethod).
97-
userId(userId).
98-
clientIp(clientIp).
99-
responseStatus(responseStatus).
100-
takenTime(timeTaken).
101-
build();
10285

103-
apiLogRepository.save(apiLog);
86+
ApiLog apiLog = buildApiLog(requestInfoDto, responseStatus, timeTaken);
87+
apiLogService.saveLog(apiLog);
10488

10589
log.info("[Request Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, pathVariable: {}, requestBody: {}, responseStatus: {}, timeTaken: {}",
106-
requestUrl, requestMethod, userId, clientIp,
107-
pathVariable != null ? pathVariable : "No Params",
108-
requestBody != null ? requestBody : "No Body",
90+
requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(),
91+
pathVariable != null ? pathVariable : NO_PARAMS,
92+
requestBody != null ? requestBody : NO_BODY,
10993
responseStatus, timeTaken);
11094

11195
return result;
@@ -117,19 +101,7 @@ public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
117101

118102
@AfterThrowing(pointcut = "allRequest()", throwing = "ex")
119103
public void logException(JoinPoint joinPoint, Exception ex) {
120-
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
121-
122-
// requestUrl
123-
String requestUrl = request.getRequestURI();
124-
// requestMethod
125-
String requestMethod = request.getMethod();
126-
// userId
127-
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
128-
String userId = (authentication != null && authentication.isAuthenticated())
129-
? authentication.getName() // 인증된 사용자의 이름 (주로 ID로 사용됨)
130-
: "Anonymous";
131-
// clientIp
132-
String clientIp = request.getRemoteAddr();
104+
RequestInfoDto requestInfoDto = extractRequestInfo();
133105

134106
// exceptionName
135107
String exceptionName;
@@ -144,32 +116,44 @@ public void logException(JoinPoint joinPoint, Exception ex) {
144116
int responseStatus = mapExceptionToStatusCode(ex);
145117

146118
log.error("[Error Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, exception: {}, message: {}, responseStatus: {}",
147-
requestUrl, requestMethod, userId, clientIp, exceptionName, exceptionMessage, responseStatus);
148-
149-
// DB에 에러 로그 저장
150-
ApiLog errorLog = ApiLog.builder().
151-
requestUrl(requestUrl).
152-
requestMethod(requestMethod).
153-
userId(userId).
154-
clientIp(clientIp).
155-
responseStatus(responseStatus).
156-
takenTime(0).
157-
build();
158-
// 상태 코드와 시간은 예제로 설정
159-
apiLogRepository.save(errorLog);
119+
requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(), exceptionName, exceptionMessage, responseStatus);
120+
121+
ApiLog errorLog = buildApiLog(requestInfoDto, responseStatus, 0);
122+
apiLogService.saveLog(errorLog);
123+
}
124+
125+
// requestinfo 추출
126+
private RequestInfoDto extractRequestInfo() {
127+
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
128+
129+
String requestUrl = request.getRequestURI();
130+
String requestMethod = request.getMethod();
131+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
132+
String userId = (authentication != null && authentication.isAuthenticated())
133+
? authentication.getName()
134+
: "Anonymous";
135+
String clientIp = request.getRemoteAddr();
136+
137+
return new RequestInfoDto(requestUrl, requestMethod, userId, clientIp);
138+
}
139+
140+
// apilog 생성
141+
private ApiLog buildApiLog(RequestInfoDto info, int responseStatus, long timeTaken) {
142+
return ApiLog.builder()
143+
.requestUrl(info.getRequestUrl())
144+
.requestMethod(info.getRequestMethod())
145+
.userId(info.getUserId())
146+
.clientIp(info.getClientIp())
147+
.responseStatus(responseStatus)
148+
.takenTime(timeTaken)
149+
.build();
160150
}
161151

162152
private int mapExceptionToStatusCode(Exception e) {
163-
if (e instanceof IllegalArgumentException) {
164-
return 400; // Bad Request
165-
} else if (e instanceof org.springframework.security.access.AccessDeniedException) {
166-
return 403; // Forbidden
167-
} else if (e instanceof org.springframework.web.bind.MethodArgumentNotValidException) {
168-
return 422; // Unprocessable Entity
169-
} else {
170-
return 500; // Internal Server Error
153+
if (e instanceof GeneralException ge) {
154+
return ge.getErrorCode().getCode();
171155
}
156+
return 500;
172157
}
173158

174-
175159
}

ontime-back/src/main/java/devkor/ontime_back/OntimeBackApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableAsync;
56
import org.springframework.scheduling.annotation.EnableScheduling;
67

8+
@EnableAsync
79
@SpringBootApplication
810
@EnableScheduling
911
public class OntimeBackApplication {

ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
@Configuration
1616
@OpenAPIDefinition(
1717
servers = {
18-
@Server(url = "https://ontime.devkor.club", description = "Production Server"),
18+
@Server(url = "https://api.ontime.devkor.club", description = "Production Server"),
1919
@Server(url = "http://localhost:8080", description = "Local Serever")
2020
}
2121
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package devkor.ontime_back.dto;
2+
3+
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public class RequestInfoDto {
10+
private String requestUrl;
11+
private String requestMethod;
12+
private String userId;
13+
private String clientIp;
14+
}

ontime-back/src/main/java/devkor/ontime_back/response/ApiResponseForm.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
public class ApiResponseForm<T> {
99
// 제네릭 api 응답 객체
1010
private String status;
11-
private String code;
11+
private int code;
1212
private String message;
1313
private final T data;
14-
public ApiResponseForm(String status, String code, String message, T data) {
14+
public ApiResponseForm(String status, int code, String message, T data) {
1515
this.status = status; // HttpResponse의 생성자 호출 (부모 클래스의 생성자 또는 메서드를 호출, 자식 클래스는 부모 클래스의 private 필드에 직접 접근 X)
1616
this.code = code;
1717
this.message = message;
@@ -20,33 +20,33 @@ public ApiResponseForm(String status, String code, String message, T data) {
2020

2121
// 성공 응답을 위한 메서드 (message를 받는 경우)
2222
public static <T> ApiResponseForm<T> success(T data, String message) {
23-
return new ApiResponseForm<>("success", "200", message, data);
23+
return new ApiResponseForm<>("success", 200, message, data);
2424
}
2525

2626
// 성공 응답을 위한 메서드 (message를 받지 않는 경우)
2727
public static <T> ApiResponseForm<T> success(T data) {
28-
return new ApiResponseForm<>("success", "200", "OK", data);
28+
return new ApiResponseForm<>("success", 200, "OK", data);
2929
}
3030

3131
// 실패 응답을 위한 메서드
32-
public static <T> ApiResponseForm<T> fail(String code, String message) {
32+
public static <T> ApiResponseForm<T> fail(int code, String message) {
3333
return new ApiResponseForm<>("fail", code, message, null); // 실패의 경우 data는 null로 처리
3434
}
3535

36-
public static <T> ApiResponseForm<T> accessTokenEmpty(String code, String message) {
36+
public static <T> ApiResponseForm<T> accessTokenEmpty(int code, String message) {
3737
return new ApiResponseForm<>("accessTokenEmpty", code, message, null); // 실패의 경우 data는 null로 처리
3838
}
3939

40-
public static <T> ApiResponseForm<T> accessTokenInvalid(String code, String message) {
40+
public static <T> ApiResponseForm<T> accessTokenInvalid(int code, String message) {
4141
return new ApiResponseForm<>("accessTokenInvalid", code, message, null); // 실패의 경우 data는 null로 처리
4242
}
4343

44-
public static <T> ApiResponseForm<T> refreshTokenInvalid(String code, String message) {
44+
public static <T> ApiResponseForm<T> refreshTokenInvalid(int code, String message) {
4545
return new ApiResponseForm<>("refreshTokenInvalid", code, message, null); // 실패의 경우 data는 null로 처리
4646
}
4747

4848
// 오류 응답을 위한 메서드
49-
public static <T> ApiResponseForm<T> error(String code, String message) {
49+
public static <T> ApiResponseForm<T> error(int code, String message) {
5050
return new ApiResponseForm<>("error", code, message, null); // 오류의 경우 data는 null로 처리
5151
}
5252

ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,47 @@
55
public enum ErrorCode {
66

77
// HTTP 상태 코드 (4xx)
8-
BAD_REQUEST("400", "Bad Request: Invalid input or malformed request.", HttpStatus.BAD_REQUEST),
9-
UNAUTHORIZED("401", "Unauthorized: You must authenticate to access this resource.", HttpStatus.UNAUTHORIZED),
10-
FORBIDDEN("403", "Forbidden: You do not have permission to access this resource.", HttpStatus.FORBIDDEN),
11-
NOT_FOUND("404", "Not Found: The requested resource could not be found.", HttpStatus.NOT_FOUND),
8+
BAD_REQUEST(400, "Bad Request: Invalid input or malformed request.", HttpStatus.BAD_REQUEST),
9+
UNAUTHORIZED(401, "Unauthorized: You must authenticate to access this resource.", HttpStatus.UNAUTHORIZED),
10+
FORBIDDEN(403, "Forbidden: You do not have permission to access this resource.", HttpStatus.FORBIDDEN),
11+
NOT_FOUND(404, "Not Found: The requested resource could not be found.", HttpStatus.NOT_FOUND),
1212

1313
// HTTP 상태 코드 (5xx)
14-
INTERNAL_SERVER_ERROR("500", "Internal Server Error: An unexpected error occurred on the server.", HttpStatus.INTERNAL_SERVER_ERROR),
15-
BAD_GATEWAY("502", "Bad Gateway: The server received an invalid response from the upstream server.", HttpStatus.BAD_GATEWAY),
16-
SERVICE_UNAVAILABLE("503", "Service Unavailable: The server is temporarily unable to handle the request.", HttpStatus.SERVICE_UNAVAILABLE),
14+
INTERNAL_SERVER_ERROR(500, "Internal Server Error: An unexpected error occurred on the server.", HttpStatus.INTERNAL_SERVER_ERROR),
15+
BAD_GATEWAY(502, "Bad Gateway: The server received an invalid response from the upstream server.", HttpStatus.BAD_GATEWAY),
16+
SERVICE_UNAVAILABLE(503, "Service Unavailable: The server is temporarily unable to handle the request.", HttpStatus.SERVICE_UNAVAILABLE),
1717

1818
// 비즈니스 로직 오류 코드
19-
USER_NOT_FOUND("1001", "해당 ID의 사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),
20-
INVALID_INPUT("1002", "유효하지 않은 입력값입니다.", HttpStatus.BAD_REQUEST),
21-
RESOURCE_ALREADY_EXISTS("1003", "생성하려는 리소스가 이미 존재합니다.", HttpStatus.BAD_REQUEST),
22-
UNAUTHORIZED_ACCESS("1004", "해당 작업에 대한 권한이 없습니다.", HttpStatus.UNAUTHORIZED),
23-
EMAIL_ALREADY_EXIST("1005", "이미 존재하는 이메일입니다.", HttpStatus.BAD_REQUEST),
24-
NAME_ALREADY_EXIST("1006", "이미 존재하는 이름입니다.", HttpStatus.BAD_REQUEST),
25-
USER_SETTING_ALREADY_EXIST("1007", "이미 존재하는 userSettingId 입니다.", HttpStatus.BAD_REQUEST),
26-
PASSWORD_INCORRECT("1008", "기존 비밀번호가 틀렸습니다.", HttpStatus.BAD_REQUEST),
27-
SAME_PASSWORD("1009", "새 비밀번호와 기존 비밀번호가 일치합니다.", HttpStatus.BAD_REQUEST),
28-
SCHEDULE_NOT_FOUND("1010", "해당 약속이 존재하지 않습니다.", HttpStatus.BAD_REQUEST),
29-
FIREBASE("1011", "FIREBASE로 메세지를 발송하였으나 오류가 발생했습니다.(유효하지 않은 토큰 등)", HttpStatus.BAD_REQUEST),
30-
FIRST_PREPARATION_NOT_FOUND("1012", "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),
31-
NOTIFICATION_NOT_FOUND("1013", "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ),
19+
USER_NOT_FOUND(1001, "해당 ID의 사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),
20+
INVALID_INPUT(1002, "유효하지 않은 입력값입니다.", HttpStatus.BAD_REQUEST),
21+
RESOURCE_ALREADY_EXISTS(1003, "생성하려는 리소스가 이미 존재합니다.", HttpStatus.BAD_REQUEST),
22+
UNAUTHORIZED_ACCESS(1004, "해당 작업에 대한 권한이 없습니다.", HttpStatus.UNAUTHORIZED),
23+
EMAIL_ALREADY_EXIST(1005, "이미 존재하는 이메일입니다.", HttpStatus.BAD_REQUEST),
24+
NAME_ALREADY_EXIST(1006, "이미 존재하는 이름입니다.", HttpStatus.BAD_REQUEST),
25+
USER_SETTING_ALREADY_EXIST(1007, "이미 존재하는 userSettingId 입니다.", HttpStatus.BAD_REQUEST),
26+
PASSWORD_INCORRECT(1008, "기존 비밀번호가 틀렸습니다.", HttpStatus.BAD_REQUEST),
27+
SAME_PASSWORD(1009, "새 비밀번호와 기존 비밀번호가 일치합니다.", HttpStatus.BAD_REQUEST),
28+
SCHEDULE_NOT_FOUND(1010, "해당 약속이 존재하지 않습니다.", HttpStatus.BAD_REQUEST),
29+
FIREBASE(1011, "FIREBASE로 메세지를 발송하였으나 오류가 발생했습니다.(유효하지 않은 토큰 등)", HttpStatus.BAD_REQUEST),
30+
FIRST_PREPARATION_NOT_FOUND(1012, "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),
31+
NOTIFICATION_NOT_FOUND(1013, "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ),
3232

3333
// 공통 오류 메시지
34-
UNEXPECTED_ERROR("1000", "Unexpected Error: An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR),;
34+
UNEXPECTED_ERROR(1000, "Unexpected Error: An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR),;
3535

36-
private final String code;
36+
private final int code;
3737
private final String message;
3838
private final HttpStatus httpStatus;
3939

4040
// 생성자
41-
ErrorCode(String code, String message, HttpStatus httpStatus) {
41+
ErrorCode(int code, String message, HttpStatus httpStatus) {
4242
this.code = code;
4343
this.message = message;
4444
this.httpStatus = httpStatus;
4545
}
4646

4747
// 코드와 메시지를 반환하는 메서드
48-
public String getCode() {
48+
public int getCode() {
4949
return code;
5050
}
5151

ontime-back/src/main/java/devkor/ontime_back/response/GlobalExceptionHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public ResponseEntity<ApiResponseForm<Void>> handleGeneralException(GeneralExcep
2525
public ResponseEntity<ApiResponseForm<Void>> handleInvalidTokenException(InvalidTokenException ex, HttpServletRequest request) {
2626
return ResponseEntity
2727
.status(HttpStatus.UNAUTHORIZED)
28-
.body(ApiResponseForm.error("401", ex.getMessage()));
28+
.body(ApiResponseForm.error(401, ex.getMessage()));
2929
}
3030

3131
@ExceptionHandler(HttpMessageNotReadableException.class)
@@ -34,7 +34,7 @@ public ResponseEntity<ApiResponseForm<Void>> handleHttpMessageNotReadableExcepti
3434
request.getRequestURI(), request.getMethod(), (request.getUserPrincipal() != null) ? request.getUserPrincipal().getName() : "Anonymous", request.getRemoteAddr(), "HttpMessageNotReadableException", "요청 형식이 올바르지 않습니다.", 400);
3535
return ResponseEntity
3636
.status(HttpStatus.BAD_REQUEST)
37-
.body(ApiResponseForm.error("400", "요청 형식이 올바르지 않습니다."));
37+
.body(ApiResponseForm.error(400, "요청 형식이 올바르지 않습니다."));
3838
}
3939

4040
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package devkor.ontime_back.service;
2+
3+
import devkor.ontime_back.entity.ApiLog;
4+
import devkor.ontime_back.repository.ApiLogRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Service;
9+
10+
@Slf4j
11+
@RequiredArgsConstructor
12+
@Service
13+
public class ApiLogService {
14+
private final ApiLogRepository apiLogRepository;
15+
16+
@Async
17+
public void saveLog(ApiLog apiLog) {
18+
try {
19+
log.info("[Async] saveLog started on thread: {}", Thread.currentThread().getName());
20+
apiLogRepository.save(apiLog);
21+
log.info("[Async] saveLog finished on thread: {}", Thread.currentThread().getName());
22+
} catch (Exception e) {
23+
log.error("API 로그 저장 실패", e);
24+
}
25+
}
26+
}

ontime-back/src/main/java/devkor/ontime_back/service/NotificationService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.concurrent.ScheduledFuture;
2525

2626
@Slf4j
27-
@EnableAsync
2827
@Service
2928
@RequiredArgsConstructor
3029
@Transactional(readOnly = true)

0 commit comments

Comments
 (0)