Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.loopon.global.domain.ErrorCode;
import com.loopon.global.domain.dto.CommonResponse;
import com.loopon.global.domain.dto.CommonResponse.ValidationErrorDetail;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
Expand All @@ -25,36 +25,32 @@
public class GlobalExceptionAdvice {

@ExceptionHandler(BusinessException.class)
public ResponseEntity<CommonResponse<Void>> handleBusinessException(BusinessException ex) {
log.warn("Business Exception: [{}] {}", ex.getErrorCode().getCode(), ex.getMessage());
public ResponseEntity<CommonResponse<Void>> handleBusinessException(BusinessException ex, HttpServletRequest request) {
log.warn("[BUS] {} {} | {}: {}", request.getMethod(), request.getRequestURI(), ex.getErrorCode().getCode(), ex.getMessage());
return ResponseEntity
.status(ex.getErrorCode().getStatus())
.body(CommonResponse.onFailure(ex.getErrorCode()));
}

@ExceptionHandler({
BindException.class,
MethodArgumentNotValidException.class
})
public ResponseEntity<CommonResponse<List<ValidationErrorDetail>>> handleValidationException(BindException ex) {
log.warn("Validation Error: {}", ex.getBindingResult().getFieldError() != null
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
public ResponseEntity<CommonResponse<List<CommonResponse.ValidationErrorDetail>>> handleValidationException(BindException ex, HttpServletRequest request) {
String errorMessage = ex.getBindingResult().getFieldError() != null
? ex.getBindingResult().getFieldError().getDefaultMessage()
: "Unknown Validation Error");
: "Unknown Validation Error";

log.warn("[VAL] {} {} | Msg: {}", request.getMethod(), request.getRequestURI(), errorMessage);

return ResponseEntity
.status(ErrorCode.INVALID_INPUT_VALUE.getStatus())
.body(CommonResponse.onFailure(
ErrorCode.INVALID_INPUT_VALUE,
ex.getBindingResult()
));
.body(CommonResponse.onFailure(ErrorCode.INVALID_INPUT_VALUE, ex.getBindingResult()));
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<CommonResponse<List<ValidationErrorDetail>>> handleConstraintViolationException(ConstraintViolationException ex) {
log.warn("Constraint Violation: {}", ex.getMessage());
public ResponseEntity<CommonResponse<List<CommonResponse.ValidationErrorDetail>>> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
log.warn("[VAL] {} {} | Msg: {}", request.getMethod(), request.getRequestURI(), ex.getMessage());

List<ValidationErrorDetail> details = ex.getConstraintViolations().stream()
.map(violation -> new ValidationErrorDetail(
List<CommonResponse.ValidationErrorDetail> details = ex.getConstraintViolations().stream()
.map(violation -> new CommonResponse.ValidationErrorDetail(
violation.getPropertyPath().toString(),
violation.getMessage()
))
Expand All @@ -72,8 +68,8 @@ public ResponseEntity<CommonResponse<List<ValidationErrorDetail>>> handleConstra
HttpRequestMethodNotSupportedException.class,
IllegalArgumentException.class
})
public ResponseEntity<CommonResponse<Void>> handleBadRequest(Exception ex) {
log.warn("Client Error: [{}] {}", ex.getClass().getSimpleName(), ex.getMessage());
public ResponseEntity<CommonResponse<Void>> handleBadRequest(Exception ex, HttpServletRequest request) {
log.warn("[BAD] {} {} | {}: {}", request.getMethod(), request.getRequestURI(), ex.getClass().getSimpleName(), ex.getMessage());

ErrorCode errorCode = switch (ex) {
case NoHandlerFoundException ignored -> ErrorCode.NOT_FOUND;
Expand All @@ -88,32 +84,25 @@ public ResponseEntity<CommonResponse<Void>> handleBadRequest(Exception ex) {
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<CommonResponse<Void>> handleAuthenticationException(AuthenticationException ex) {
log.warn("Authentication Exception: {}", ex.getMessage());
public ResponseEntity<CommonResponse<Void>> handleAuthenticationException(AuthenticationException ex, HttpServletRequest request) {
log.warn("[AUTH] {} {} | {}", request.getMethod(), request.getRequestURI(), ex.getMessage());
return ResponseEntity
.status(ErrorCode.UNAUTHORIZED.getStatus())
.body(CommonResponse.onFailure(ErrorCode.UNAUTHORIZED));
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<CommonResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
log.warn("Access Denied Exception: {}", ex.getMessage());
public ResponseEntity<CommonResponse<Void>> handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest request) {
log.warn("[DENY] {} {} | {}", request.getMethod(), request.getRequestURI(), ex.getMessage());
return ResponseEntity
.status(ErrorCode.FORBIDDEN.getStatus())
.body(CommonResponse.onFailure(ErrorCode.FORBIDDEN));
}

@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<CommonResponse<Void>> handleAuthorizationException(AuthorizationException ex) {
log.warn("Authorization Custom Exception: {}", ex.getMessage());
return ResponseEntity
.status(ex.getErrorCode().getStatus())
.body(CommonResponse.onFailure(ex.getErrorCode()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<CommonResponse<Void>> handleException(Exception ex) {
log.error("Unhandled Exception: ", ex);
public ResponseEntity<CommonResponse<Void>> handleException(Exception ex, HttpServletRequest request) {
log.error("[ERR] {} {} | Exception: ", request.getMethod(), request.getRequestURI(), ex);

return ResponseEntity
.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
.body(CommonResponse.onFailure(ErrorCode.INTERNAL_SERVER_ERROR));
Expand Down
132 changes: 34 additions & 98 deletions src/main/java/com/loopon/global/log/LogAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,134 +5,70 @@
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;

@Component
@Aspect
@Slf4j
public class LogAspect {

private static final String[] SENSITIVE_KEYWORDS = {
"password", "pw", "secret",
"token", "access", "refresh",
"auth", "cred",
"key", "pin", "card",
"ssn", "social"
};

@Pointcut("execution(* com.loopon..*Controller.*(..))")
public void controllerMethods() {
@Pointcut("execution(* com.loopon.*Controller.*(..))")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix: controller() pointcut is now execution(* com.loopon.*Controller.*(..)), which will not match controllers under subpackages like com.loopon.user.presentation.FriendController (and other ...presentation.*Controller). This effectively disables controller logging. Use a recursive package pattern (e.g., com.loopon..*Controller.*(..)), or otherwise align the pointcut to the actual controller package structure.

Suggested change
@Pointcut("execution(* com.loopon.*Controller.*(..))")
@Pointcut("execution(* com.loopon..*Controller.*(..))")

Copilot uses AI. Check for mistakes.
public void controller() {
}

@Pointcut("execution(* com.loopon..*Service.*(..))")
public void serviceMethods() {
public void service() {
}

@Around("controllerMethods() || serviceMethods()")
@Around("controller() || service()")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
MethodInfo methodInfo = null;
long startTime = System.currentTimeMillis();
Object result = null;

try {
methodInfo = extractMethodInfo(joinPoint);

stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();

logSuccess(methodInfo, result, stopWatch);
result = joinPoint.proceed();
return result;

} catch (Throwable e) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}

logFailure(methodInfo, e, stopWatch);
throw e;
} finally {
long totalTime = System.currentTimeMillis() - startTime;
MethodSignature signature = (MethodSignature) joinPoint.getSignature();

log.info("[{}] {}.{} | Args: {} | Ret: {} | {}ms",
getLayer(signature), // 1. 레이어 (API / SVC)
className(signature), // 2. 클래스명
methodName(signature), // 3. 메서드명
formatArgs(joinPoint.getArgs()), // 4. 파라미터 (단순화)
formatResult(result), // 5. 결과값 (길이 제한)
totalTime // 6. 소요시간
);
Comment on lines 27 to +45
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix: Exception cases are no longer logged as errors. Because logging happens only in finally with log.info(...), a thrown exception will produce an INFO log with Ret: void and no exception details/stacktrace, which is a regression in observability. Capture the thrown exception (e.g., catch (Throwable t)), log at WARN/ERROR with details, then rethrow; keep the finally for timing/MDC cleanup if needed.

Copilot uses AI. Check for mistakes.
}
}

private void logSuccess(MethodInfo methodInfo, Object result, StopWatch stopWatch) {
log.info("{} | {}({}) -> {} [{}ms]",
methodInfo.className(),
methodInfo.methodName(),
methodInfo.params(),
formatResult(result),
stopWatch.getTotalTimeMillis());
}

private void logFailure(MethodInfo info, Throwable e, StopWatch stopWatch) {
String className = (info != null) ? info.className() : "UnknownClass";
String methodName = (info != null) ? info.methodName() : "UnknownMethod";
Map<String, Object> params = (info != null) ? info.params() : Map.of();

log.error("{} | {}({}) -> [EXCEPTION] {}: {} [{}ms]",
className,
methodName,
params,
e.getClass().getSimpleName(),
e.getMessage(),
stopWatch.getTotalTimeMillis()
);
}

private MethodInfo extractMethodInfo(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
private String getLayer(MethodSignature signature) {
String className = signature.getDeclaringType().getSimpleName();
String methodName = signature.getName();
Map<String, Object> params = extractParams(joinPoint);

return new MethodInfo(className, methodName, params);
if (className.endsWith("Controller")) return "API";
if (className.endsWith("Service")) return "SVC";
return "ETC";
}

private Map<String, Object> extractParams(ProceedingJoinPoint joinPoint) {
CodeSignature signature = (CodeSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
Map<String, Object> params = new HashMap<>();

if (paramNames == null) return params;

for (int i = 0; i < paramNames.length; i++) {
String key = paramNames[i];
Object value = args[i];
if (isSensitive(key)) {
params.put(key, "****");
} else {
params.put(key, value);
}
}
return params;
private String className(MethodSignature signature) {
return signature.getDeclaringType().getSimpleName();
}

private boolean isSensitive(String key) {
if (key == null) return false;

String lowerKey = key.toLowerCase();
private String methodName(MethodSignature signature) {
return signature.getName();
}

for (String keyword : SENSITIVE_KEYWORDS) {
if (lowerKey.contains(keyword)) {
return true;
}
}
return false;
private String formatArgs(Object[] args) {
if (args == null || args.length == 0) return "[]";
return Arrays.toString(args);
}
Comment on lines +64 to 67
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix: The aspect now logs arguments via Arrays.toString(args) without any masking. This can leak credentials/tokens/passwords in controller/service logs (the previous masking logic was removed). Reintroduce sensitive-data masking (by parameter name and/or by DTO field annotations) or avoid logging full arguments for authentication-related endpoints.

Copilot uses AI. Check for mistakes.

private String formatResult(Object result) {
if (result == null) return "void";
String resultStr = result.toString();
if (resultStr.length() > 100) {
return resultStr.substring(0, 100) + "...";
}
return resultStr;
String s = result.toString();
return s.length() > 100 ? s.substring(0, 100) + "..." : s;
}

private record MethodInfo(String className, String methodName, Map<String, Object> params) {}
}
20 changes: 12 additions & 8 deletions src/main/java/com/loopon/global/log/MdcLoggingFilter.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
package com.loopon.global.log;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcLoggingFilter implements Filter {
public class MdcLoggingFilter extends OncePerRequestFilter {

private static final String REQUEST_ID = "request_id";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestId = UUID.randomUUID().toString().substring(0, 8);
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestId = request.getHeader("X-Request-ID");
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString().substring(0, 8);
}

MDC.put(REQUEST_ID, requestId);
response.setHeader("X-Request-ID", requestId);

try {
chain.doFilter(request, response);
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class FriendController implements FriendApiDocs {
//내 친구 목록 조회 API
@Override
@GetMapping
public ResponseEntity<CommonResponse<SliceResponse<FriendResponse>>> getMyFriend(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(sort = "nickname", direction = Sort.Direction.ASC) Pageable pageable) {
public ResponseEntity<CommonResponse<SliceResponse<FriendResponse>>> getMyFriend(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable) {
Long me = principalDetails.getUserId();
SliceResponse<FriendResponse> res = friendService.getMyFriends(me, pageable);
return ResponseEntity.ok(CommonResponse.onSuccess(res));
Expand Down
Loading
Loading