diff --git a/build.gradle b/build.gradle index c98d8bd..3b9a14f 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,13 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // Redis 체크 + implementation 'io.lettuce:lettuce-core' + + // AOP + implementation 'org.springframework.boot:spring-boot-starter-aop' + + } tasks.named('test') { diff --git a/src/main/java/DiffLens/back_end/domain/members/controller/AuthController.java b/src/main/java/DiffLens/back_end/domain/members/controller/AuthController.java index e5645ba..40bc4eb 100644 --- a/src/main/java/DiffLens/back_end/domain/members/controller/AuthController.java +++ b/src/main/java/DiffLens/back_end/domain/members/controller/AuthController.java @@ -2,7 +2,7 @@ import DiffLens.back_end.domain.members.dto.auth.AuthRequestDTO; import DiffLens.back_end.domain.members.dto.auth.AuthResponseDTO; -import DiffLens.back_end.domain.members.service.AuthService; +import DiffLens.back_end.domain.members.service.auth.AuthService; import DiffLens.back_end.global.responses.exception.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/DiffLens/back_end/domain/members/service/AuthService.java b/src/main/java/DiffLens/back_end/domain/members/service/auth/AuthService.java similarity index 97% rename from src/main/java/DiffLens/back_end/domain/members/service/AuthService.java rename to src/main/java/DiffLens/back_end/domain/members/service/auth/AuthService.java index 8fd9ffd..9f822ac 100644 --- a/src/main/java/DiffLens/back_end/domain/members/service/AuthService.java +++ b/src/main/java/DiffLens/back_end/domain/members/service/auth/AuthService.java @@ -1,4 +1,4 @@ -package DiffLens.back_end.domain.members.service; +package DiffLens.back_end.domain.members.service.auth; import DiffLens.back_end.domain.members.auth.StrategyFactory; import DiffLens.back_end.domain.members.auth.strategy.interfaces.AuthStrategy; diff --git a/src/main/java/DiffLens/back_end/domain/members/service/auth/CurrentUserService.java b/src/main/java/DiffLens/back_end/domain/members/service/auth/CurrentUserService.java new file mode 100644 index 0000000..5ea2604 --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/members/service/auth/CurrentUserService.java @@ -0,0 +1,41 @@ +package DiffLens.back_end.domain.members.service.auth; + +import DiffLens.back_end.domain.members.entity.Member; +import DiffLens.back_end.global.responses.code.status.error.AuthStatus; +import DiffLens.back_end.global.responses.exception.handler.ErrorHandler; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class CurrentUserService { + + public Member getCurrentUser() { + var authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new ErrorHandler(AuthStatus.AUTHENTICATION_FAILED); + } + + var principal = authentication.getPrincipal(); + if (principal == null) { + throw new ErrorHandler(AuthStatus.AUTHENTICATION_FAILED); + } + + if (!(principal instanceof Member)) { + throw new ErrorHandler(AuthStatus.AUTHENTICATION_FAILED); + } + + return (Member) principal; + } + + public Long getCurrentUserId() { + Member user = getCurrentUser(); + return user != null ? user.getId() : null; + } + + public String getCurrentUserEmail() { + Member user = getCurrentUser(); + return user != null ? user.getEmail() : null; + } + + +} diff --git a/src/main/java/DiffLens/back_end/global/aop/ApiRequestLogAspect.java b/src/main/java/DiffLens/back_end/global/aop/ApiRequestLogAspect.java new file mode 100644 index 0000000..4578d7a --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/aop/ApiRequestLogAspect.java @@ -0,0 +1,60 @@ +package DiffLens.back_end.global.aop; + +import DiffLens.back_end.domain.members.service.auth.CurrentUserService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class ApiRequestLogAspect { + + private final CurrentUserService authService; + + /** + * 전/후 처리 모두 가능 + * CommonPointcut.restControllerEndpoints() Pointcut 지정하여 + * API 호출 시 호출 전후로 로그 출력하도록 하는 Aspect + */ + @Around("CommonPointCut.restControllerEndpoints()") + public Object logApiRequest(ProceedingJoinPoint jp) throws Throwable { + long start = System.currentTimeMillis(); + + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletRequest request = attrs.getRequest(); + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + + Object currentUser = null; + try { currentUser = authService.getCurrentUser(); } catch (Exception ignored) {} + + String methodName = jp.getSignature().getName(); + String args = jp.getArgs() != null ? String.join(", ", java.util.Arrays.stream(jp.getArgs()) + .map(String::valueOf).toArray(String[]::new)) : ""; + + String userInfo = currentUser != null ? "[User: " + currentUser + "]" : "[User: Anonymous]"; + String requestInfo = "[" + httpMethod + ": " + uri + " - " + methodName + "(" + args + ")]"; + + log.info("⏳ [API 호출 시작] {} {}", userInfo, requestInfo); + + try { + Object result = jp.proceed(); + long end = System.currentTimeMillis(); + log.info("✅ [API 호출 종료] {} {} - 실행시간: {}ms", userInfo, requestInfo, (end - start)); + return result; + } catch (Throwable ex) { + long end = System.currentTimeMillis(); + log.error("❌ [API 호출 예외] {} {} - 실행시간: {}ms - 예외: {}", userInfo, requestInfo, (end - start), ex.getMessage()); + throw ex; // 예외를 다시 던져서 컨트롤러에게 전달 + } + } + +} \ No newline at end of file diff --git a/src/main/java/DiffLens/back_end/global/aop/CommonPointCut.java b/src/main/java/DiffLens/back_end/global/aop/CommonPointCut.java new file mode 100644 index 0000000..973148a --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/aop/CommonPointCut.java @@ -0,0 +1,10 @@ +package DiffLens.back_end.global.aop; + +import org.aspectj.lang.annotation.Pointcut; + +public class CommonPointCut { + + // API 호출 시 로그 찍도록 @RestController 어노테이션에 PointCut 설정 + @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") + public void restControllerEndpoints() {} +} diff --git a/src/main/java/DiffLens/back_end/global/redis/RedisHealthChecker.java b/src/main/java/DiffLens/back_end/global/redis/RedisHealthChecker.java new file mode 100644 index 0000000..6bceb34 --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/redis/RedisHealthChecker.java @@ -0,0 +1,34 @@ +package DiffLens.back_end.global.redis; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisHealthChecker { + + private final RedisTemplate redisTemplate; + + @PostConstruct + public void checkRedisConnection() { + log.info("[Connection Check] Redis 연결 체크..."); + try { + String ping = redisTemplate.getConnectionFactory() != null ? redisTemplate.getConnectionFactory() + .getConnection() + .ping() : "null"; + if(!"PONG".equals(ping)) { + log.error("[Connection Check] Redis 연결 실패 ❌ - 응답값이 'PONG' 이 아님"); + throw new IllegalStateException("Redis 연결 실패: PING 응답 이상. 응답=" + ping); + } + }catch (Exception e) { + log.error("[Connection Check] Redis 연결 실패 ❌ - {}", e.getMessage()); + throw new IllegalStateException("Redis 연결 실패: " + e.getMessage(), e); + } + log.info("[Connection Check] Redis 연결 성공 ✅"); + } + +} diff --git a/src/main/java/DiffLens/back_end/global/responses/code/status/error/AuthStatus.java b/src/main/java/DiffLens/back_end/global/responses/code/status/error/AuthStatus.java index 43f7832..372d9db 100644 --- a/src/main/java/DiffLens/back_end/global/responses/code/status/error/AuthStatus.java +++ b/src/main/java/DiffLens/back_end/global/responses/code/status/error/AuthStatus.java @@ -10,6 +10,8 @@ @AllArgsConstructor public enum AuthStatus implements BaseErrorCode { + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "AUTH400", "인증에 실패했습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH401", "존재하지 않는 사용자입니다."), ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "AUTH402", "이미 존재하는 사용자입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "AUTH403", "올바르지 않은 비밀번호입니다."), @@ -22,7 +24,7 @@ public enum AuthStatus implements BaseErrorCode { SOCIAL_USERINFO_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH504", "소셜 사용자 정보를 가져오지 못했습니다."), SOCIAL_USERINFO_NETWORK_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "AUTH505", "소셜 사용자 정보 요청 중 네트워크 오류가 발생했습니다."), - SOCIAL_USERINFO_UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH506", "소셜 사용자 정보 처리 중 오류가 발생했습니다.") + SOCIAL_USERINFO_UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH506", "소셜 사용자 정보 처리 중 오류가 발생했습니다."), ; diff --git a/src/main/java/DiffLens/back_end/global/responses/exception/ExceptionAdvice.java b/src/main/java/DiffLens/back_end/global/responses/exception/ExceptionAdvice.java index da63e60..4956776 100644 --- a/src/main/java/DiffLens/back_end/global/responses/exception/ExceptionAdvice.java +++ b/src/main/java/DiffLens/back_end/global/responses/exception/ExceptionAdvice.java @@ -106,7 +106,7 @@ public ResponseEntity handleMethodArgumentNotValid( }); return handleExceptionInternalArgs( - e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, errors); + e, HttpHeaders.EMPTY, ErrorStatus.valueOf("BAD_REQUEST"), request, errors); } @ExceptionHandler diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index d874c8a..73eb48f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -20,6 +20,7 @@ spring: redis: host: ${REDIS_HOSTNAME} port: ${REDIS_PORT} + timeout: 2000ms diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 732a2e6..5972db9 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -22,6 +22,7 @@ spring: redis: host: localhost port: 6381 + timeout: 2000ms