diff --git a/build.gradle b/build.gradle index a74543d..fcd1e64 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,9 @@ dependencies { // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // IOUtils + implementation 'commons-io:commons-io:2.7' } tasks.named('test') { diff --git a/src/main/java/bokjak/bokjakserver/BokjakserverApplication.java b/src/main/java/bokjak/bokjakserver/BokjakserverApplication.java index a28437e..bd3b7a4 100644 --- a/src/main/java/bokjak/bokjakserver/BokjakserverApplication.java +++ b/src/main/java/bokjak/bokjakserver/BokjakserverApplication.java @@ -2,6 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @@ -12,6 +13,7 @@ @EnableJpaAuditing @EnableWebSecurity @EnableScheduling +@ServletComponentScan public class BokjakserverApplication { @Bean diff --git a/src/main/java/bokjak/bokjakserver/common/exception/GlobalExceptionHandler.java b/src/main/java/bokjak/bokjakserver/common/exception/GlobalExceptionHandler.java index 8821256..0daec6d 100644 --- a/src/main/java/bokjak/bokjakserver/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/bokjak/bokjakserver/common/exception/GlobalExceptionHandler.java @@ -28,7 +28,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(BuzException.class) - public ResponseEntity handle(BuzException ex) { + public ResponseEntity handleBuzException(BuzException ex) { log.warn("{}({}) - {}", ex.getClass().getSimpleName(), ex.statusCode.getStatusCode(), ex.getMessage()); return ResponseEntity .status(ex.statusCode.getHttpCode()) @@ -36,12 +36,11 @@ public ResponseEntity handle(BuzException ex) { } @ExceptionHandler(AuthException.class) //분리 이유: SignToken - public ResponseEntity handle(AuthException ex) { + public ResponseEntity handleAuthException(AuthException ex) { log.warn("{}({}) - {}", ex.getClass().getSimpleName(), ex.statusCode.getStatusCode(), ex.getMessage()); return ResponseEntity .status(ex.statusCode.getHttpCode()) .body(ApiResponse.error(ex.statusCode.getStatusCode(), ex.data, ex.statusCode.getMessage())); - } /** @@ -51,7 +50,7 @@ public ResponseEntity handle(AuthException ex) { // AWS S3 버킷 정책에 맞지 않는 요청 @ExceptionHandler(AwsS3Exception.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) - protected ApiResponse handleAwsS3Error(final AwsS3Exception ex) { + protected ApiResponse handleAwsS3Exception(final AwsS3Exception ex) { log.warn("{} - {}", ex.getClass().getSimpleName(), ex.getMessage()); return error(ex.getAwsS3ErrorCode().getStatusCode(), ex.getMessage()); } diff --git a/src/main/java/bokjak/bokjakserver/domain/test/TestController.java b/src/main/java/bokjak/bokjakserver/domain/test/TestController.java index b0e866c..8286a5c 100644 --- a/src/main/java/bokjak/bokjakserver/domain/test/TestController.java +++ b/src/main/java/bokjak/bokjakserver/domain/test/TestController.java @@ -40,12 +40,8 @@ public ApiResponse testCreateNotificat @GetMapping("/send/email") public String testSendEmailMySelf() { - long beforeTime = System.currentTimeMillis(); User currentUser = userService.getCurrentUser(); sleepingUserService.sendMail(currentUser.getEmail()); - long afterTime = System.currentTimeMillis(); - long diffTime = afterTime-beforeTime; - System.out.println("실행 시간: " + diffTime); return "good"; } } diff --git a/src/main/java/bokjak/bokjakserver/util/CustomSliceExecutionUtils.java b/src/main/java/bokjak/bokjakserver/util/CustomSliceExecutionUtils.java index 595fc49..8b6ea93 100644 --- a/src/main/java/bokjak/bokjakserver/util/CustomSliceExecutionUtils.java +++ b/src/main/java/bokjak/bokjakserver/util/CustomSliceExecutionUtils.java @@ -10,16 +10,11 @@ public class CustomSliceExecutionUtils { public static Slice getSlice(List content, Pageable pageable) { boolean hasNext = false; - System.out.println("content.size() = " + content.size()); - System.out.println("pageable.getPageSize() = " + pageable.getPageSize()); - if (content.size() > pageable.getPageSize()) { // content.size가 최대일 경우: 항상 page size + 1 이고 다음 레코드가 있다. content.remove(pageable.getPageSize()); // limit걸 때 +1 했던 마지막 레코드를 삭제 hasNext = true; } - System.out.println("hasNext = " + hasNext); - return new SliceImpl<>(content, pageable, hasNext); } diff --git a/src/main/java/bokjak/bokjakserver/web/log/LoggerAspect.java b/src/main/java/bokjak/bokjakserver/web/log/LoggerAspect.java new file mode 100644 index 0000000..42f6d4e --- /dev/null +++ b/src/main/java/bokjak/bokjakserver/web/log/LoggerAspect.java @@ -0,0 +1,51 @@ +package bokjak.bokjakserver.web.log; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.json.simple.JSONObject; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Enumeration; +import java.util.Objects; + +@Component +@Aspect +@Slf4j +public class LoggerAspect { + @Pointcut("execution(* bokjak.bokjakserver..*Controller.*(..)) || execution(* bokjak.bokjakserver..*GlobalExceptionHandler.*(..))") + // 이런 패턴이 실행될 경우 수행 + public void loggerPointCut() { + } + + @Around("loggerPointCut()") + public Object logRequestUri(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + Object result = proceedingJoinPoint.proceed(); + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + if (Objects.nonNull(requestAttributes)) { + HttpServletRequest request = requestAttributes.getRequest(); // request 정보를 가져온다. + String controllerName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName(); + String methodName = proceedingJoinPoint.getSignature().getName(); + log.info("{}.{}: {} {} PARAM={}", controllerName, methodName, request.getMethod(), request.getRequestURI(), extractParams(request)); // param에 담긴 정보들을 한번에 로깅한다. + } + + return result; + } + + private static JSONObject extractParams(HttpServletRequest request) { // request로부터 param 추출, JSONObject로 변환 + JSONObject jsonObject = new JSONObject(); + Enumeration params = request.getParameterNames(); + while (params.hasMoreElements()) { + String param = params.nextElement(); + String replaceParam = param.replaceAll("\\.", "-"); + jsonObject.put(replaceParam, request.getParameter(param)); + } + return jsonObject; + } +} diff --git a/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapper.java b/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapper.java new file mode 100644 index 0000000..a6426c8 --- /dev/null +++ b/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapper.java @@ -0,0 +1,143 @@ +package bokjak.bokjakserver.web.log; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.entity.ContentType; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +public class ReadableRequestBodyWrapper extends HttpServletRequestWrapper { + private final Charset encoding; + private byte[] rawData; + private final Map params = new HashMap<>(); + + public ReadableRequestBodyWrapper(HttpServletRequest request) { + super(request); + this.params.putAll(request.getParameterMap()); // 오리지널 요청의 파라미터들 저장 + + String charEncoding = request.getCharacterEncoding(); // 인코딩 설정 + this.encoding = StringUtils.isBlank(charEncoding) ? StandardCharsets.UTF_8 : Charset.forName(charEncoding); + + try { + // 중요: body가 유실되지 않도록 함. getInputStream -> rawData에 저장 -> getReader() 에서 새 스트림으로 생성 + InputStream inputStream = request.getInputStream(); + this.rawData = IOUtils.toByteArray(inputStream); + + // body 파싱 + String collect = this.getReader().lines().collect(Collectors.joining(System.lineSeparator())); + if (StringUtils.isEmpty(collect)) { // body 가 없을경우 종료 + return; + } + if (request.getContentType() != null && request.getContentType().contains( + ContentType.MULTIPART_FORM_DATA.getMimeType())) { // 파일 업로드시 로깅 제외 TODO: 이 경우에도 로깅해야 하지 않나? + return; + } + + JSONParser jsonParser = new JSONParser(); + Object parse = jsonParser.parse(collect); + if (parse instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) jsonParser.parse(collect); + setParameter("requestBody", jsonArray.toJSONString()); + } else { + JSONObject jsonObject = (JSONObject) jsonParser.parse(collect); + for (Object key : jsonObject.keySet()) { + setParameter(key.toString(), jsonObject.get(key.toString()).toString().replace("\"", "\\\"")); + } + } + } catch (Exception e) { + log.error("ReadableRequestWrapper init error", e); + } + } + + @Override + public String getParameter(String name) { + String[] paramArray = getParameterValues(name); + if (paramArray != null && paramArray.length > 0) { + return paramArray[0]; + } else { + return null; + } + } + + @Override + public Map getParameterMap() { + return Collections.unmodifiableMap(params); + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(params.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + String[] result = null; + String[] dummyParamValue = params.get(name); + + if (dummyParamValue != null) { + result = new String[dummyParamValue.length]; + System.arraycopy(dummyParamValue, 0, result, 0, dummyParamValue.length); + } + return result; + } + + public void setParameter(String name, String value) { + String[] param = {value}; + setParameter(name, param); + } + + public void setParameter(String name, String[] values) { + params.put(name, values); + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData); + + return new ServletInputStream() { + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + // Do nothing + } + + public int read() { + return byteArrayInputStream.read(); + } + }; + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding)); + } +} + diff --git a/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapperFilter.java b/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapperFilter.java new file mode 100644 index 0000000..86fc0b2 --- /dev/null +++ b/src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapperFilter.java @@ -0,0 +1,28 @@ +package bokjak.bokjakserver.web.log; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +@WebFilter(urlPatterns = "/*") // 대상: 전체 URI +public class ReadableRequestBodyWrapperFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + // Do nothing + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + ReadableRequestBodyWrapper wrapper = new ReadableRequestBodyWrapper((HttpServletRequest) request); + filterChain.doFilter(wrapper, response); // 필터 체인에 Wrapper 추가 + } + + @Override + public void destroy() { + // Do nothing + } + +}