Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] #310 - log4j2를 이용한 AOP 로깅방식 도입 완료 #311

Merged
merged 15 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a16f2df
[#310] chore(build.gradle): log4j2와 aop 의존성 추가
hoonyworld Feb 15, 2025
1333d65
[#310] fix(TokenRepository): 불필요한 메서드 삭제
hoonyworld Feb 15, 2025
4e3fc67
[#310] chore(Token): 가독성을 위해 RedisHash 어노테이션 위치변경
hoonyworld Feb 15, 2025
28c2261
[#310] feat(log4j2-spring.xml): log4j2 도입 및 환경별 로깅 설정 적용
hoonyworld Feb 17, 2025
8e43b66
[#310] chore(build.gradle): common-logging 모듈 제외 로직 추가
hoonyworld Feb 17, 2025
295e714
[#310] chore(application.yml): xml 파일 경로명 명시적으로 지정
hoonyworld Feb 17, 2025
bfe2a52
[#310] feat(RepositoryConfig): JPA 및 Redis 레포지토리 분리 설정 추가
hoonyworld Feb 17, 2025
00af3c9
[#310] feat(ControllerLoggingAspect): 컨트롤러 요청/응답 및 예외 로깅을 위한 AOP 적용
hoonyworld Feb 17, 2025
d3889a9
[#310] feat(ServiceLoggingAspect): 서비스 계층의 메서드 실행 및 반환 로깅 기능 추가
hoonyworld Feb 17, 2025
f8aedbb
[#310] feat(TxAspect): 트랜잭션 로깅을 위한 TxAspect AOP 구현
hoonyworld Feb 17, 2025
598d1ba
[#310] feat(Pointcuts): AOP 포인트컷 정의를 위한 Pointcuts 클래스 추가
hoonyworld Feb 17, 2025
60d2b69
[#310] feat(BeatApplication): AOP 프록시 활성화를 위해 @EnableAspectJAutoProxy 추가
hoonyworld Feb 17, 2025
f66d6c2
[#310] chore: test 환경에서 AOP 비활성화 기능 추가
hoonyworld Feb 19, 2025
3eca382
[#310] refactor(ControllerLoggingAspect): 컨트롤러 정상 반환 로그 debug 레벨로 변경
hoonyworld Feb 20, 2025
91fb3ea
[#310] feat: 실행 시간 측정 AOP 클래스 추가
hoonyworld Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ configurations {

// Configure libraries related to QueryDSL to be required only at compile-time and add QueryDSL configuration to the compile classpath.
querydsl.extendsFrom compileClasspath

// Enable Log4j2 except for the Spring Boot Default Logging Framework (Logback)
configureEach {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
exclude group: 'commons-logging', module: 'commons-logging'
}
}

repositories {
Expand Down Expand Up @@ -94,6 +100,12 @@ dependencies {

// Prometheus
implementation 'io.micrometer:micrometer-registry-prometheus'

// log4j2
implementation 'org.springframework.boot:spring-boot-starter-log4j2'

// AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
}

jar {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/beat/BeatApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableFeignClients
@EnableScheduling
@EnableAsync
@EnableAspectJAutoProxy
@ImportAutoConfiguration({FeignAutoConfiguration.class})
public class BeatApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,4 @@
public interface TokenRepository extends CrudRepository<Token, Long> {

Optional<Token> findByRefreshToken(final String refreshToken);

Optional<Token> findById(final Long id);
}
2 changes: 1 addition & 1 deletion src/main/java/com/beat/global/auth/redis/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@RedisHash(value = "refreshToken", timeToLive = 1209600)
@Getter
@Builder
@RedisHash(value = "refreshToken", timeToLive = 1209600)
public class Token {

@Id
Expand Down
106 changes: 106 additions & 0 deletions src/main/java/com/beat/global/common/aop/ControllerLoggingAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.beat.global.common.aop;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import net.minidev.json.JSONObject;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Order(2)
@Component
@Profile("!test")
public class ControllerLoggingAspect {

private static final String REQUEST_URI = "requestURI";
private static final String CONTROLLER = "controller";
private static final String METHOD = "method";
private static final String HTTP_METHOD = "httpMethod";
private static final String LOG_TIME = "logTime";
private static final String PARAMS = "params";

/** Controller 요청 로깅 */
@Before("com.beat.global.common.aop.Pointcuts.allController()")
public void logControllerRequest(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return;

HttpServletRequest request = attributes.getRequest();
Map<String, Object> logInfo = new HashMap<>();

logInfo.put(CONTROLLER, joinPoint.getSignature().getDeclaringType().getSimpleName());
logInfo.put(METHOD, joinPoint.getSignature().getName());
logInfo.put(PARAMS, getParams(request));
logInfo.put(LOG_TIME, System.currentTimeMillis());
logInfo.put(HTTP_METHOD, request.getMethod());

try {
logInfo.put(REQUEST_URI, URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8));
} catch (Exception e) {
logInfo.put(REQUEST_URI, request.getRequestURI());
log.error("[로깅 에러] URL 디코딩 실패", e);
}

log.info("[HTTP {}] {} | {}.{}() | Params: {}",
logInfo.get(HTTP_METHOD), logInfo.get(REQUEST_URI),
logInfo.get(CONTROLLER), logInfo.get(METHOD),
logInfo.get(PARAMS));
}

/** Controller 정상 반환 로깅 */
@AfterReturning(value = "com.beat.global.common.aop.Pointcuts.allController()", returning = "result")
public void logControllerResponse(JoinPoint joinPoint, Object result) {
log.debug("[Controller 정상 반환] {}.{}() | 반환 값: {}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
result);
}

/** Controller 예외 발생 시 로깅 */
@AfterThrowing(value = "com.beat.global.common.aop.Pointcuts.allController()", throwing = "ex")
public void logControllerException(JoinPoint joinPoint, Exception ex) {
log.error("[Controller 예외 발생] {}.{}() | 예외 메시지: {}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage(), ex);
}

/** HTTP 요청 파라미터를 JSON 형태로 변환 */
private static JSONObject getParams(HttpServletRequest request) {
JSONObject jsonObject = new JSONObject();
Enumeration<String> params = request.getParameterNames();

while (params.hasMoreElements()) {
String param = params.nextElement();
String replacedParam = param.replace(".", "-");
String[] values = request.getParameterValues(param);

if (values == null || values.length == 0) {
jsonObject.put(replacedParam, ""); // 값이 없을 경우 빈 문자열 저장
} else if (values.length > 1) {
jsonObject.put(replacedParam, values); // 여러 값이 있는 경우 배열로 저장
} else {
jsonObject.put(replacedParam, values[0]); // 단일 값이면 문자열로 저장
}
}
return jsonObject;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.beat.global.common.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Component
@Order(0)
public class ExecutionTimeLoggerAspect {

private ExecutionTimeLoggerAspect() {
}

/** prod 환경에서는 서비스 계층만 실행 시간 측정 */
@Aspect
@Component
@Profile("prod")
public static class ExecutionTimeLoggerForProd {
@Around("com.beat.global.common.aop.Pointcuts.allService()")
public Object logServiceExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
return measureExecutionTime(joinPoint);
}
}

/** local/dev 환경에서는 전체 애플리케이션 로직 실행 시간 측정 */
@Aspect
@Component
@Profile({"local", "dev"})
public static class ExecutionTimeLoggerForLocalDev {
@Around("com.beat.global.common.aop.Pointcuts.allApplicationLogic()")
public Object logApplicationExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
return measureExecutionTime(joinPoint);
}
}

/** 실행 시간 측정 공통 메서드 */
private static Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long timeInMs = System.currentTimeMillis() - start;
log.info("[실행 시간] {}.{}() | time = {}ms",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
timeInMs);
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/beat/global/common/aop/Pointcuts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.beat.global.common.aop;

import org.aspectj.lang.annotation.Pointcut;

public class Pointcuts {
@Pointcut("execution(* com.beat..*Controller.*(..))")
public void allController() {}

@Pointcut("execution(* com.beat..*Service.*(..)) || execution(* com.beat..*UseCase.*(..)) || execution(* com.beat..*Facade.*(..))")
public void allService() {}

@Pointcut("execution(* com.beat..*(..))" +
" && !within(com.beat.global..*)")
public void allApplicationLogic() {}
}
60 changes: 60 additions & 0 deletions src/main/java/com/beat/global/common/aop/ServiceLoggingAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.beat.global.common.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Order(3)
@Component
@Profile("!test")
public class ServiceLoggingAspect {

/** Service 메서드 실행 전 로깅 */
@Before("com.beat.global.common.aop.Pointcuts.allService()")
public void doLog(JoinPoint joinPoint) {
log.info("[메서드 실행] {}.{}() | 인자: {}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()));
}

/** Service 정상 반환 로깅 */
@AfterReturning(value = "com.beat.global.common.aop.Pointcuts.allService()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
log.info("[Service 정상 반환] {}.{}() | 반환 값: {}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
result);
}

/** 예외 발생 시 로깅 */
@AfterThrowing(value = "com.beat.global.common.aop.Pointcuts.allService()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
log.error("[예외 발생] {}.{}() | 예외 메시지: {}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage());
}

/** 메서드 실행 후 로깅 */
@After("com.beat.global.common.aop.Pointcuts.allService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[메서드 종료] {}.{}()",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName());
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/beat/global/common/aop/TxAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.beat.global.common.aop;

import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Order(1)
@Component
@Profile("!test")
public class TxAspect {

@Around("com.beat.global.common.aop.Pointcuts.allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 정보를 추출
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();

// @Transactional 애너테이션 정보 확인 (메서드 혹은 클래스 레벨)
Transactional transactional = method.getAnnotation(Transactional.class);
if (transactional == null) {
transactional = joinPoint.getTarget().getClass().getAnnotation(Transactional.class);
}

boolean readOnly = false;
Propagation propagation = Propagation.REQUIRED;
Isolation isolation = Isolation.DEFAULT;
if (transactional != null) {
readOnly = transactional.readOnly();
propagation = transactional.propagation();
isolation = transactional.isolation();
}

// 트랜잭션 시작 로깅 (옵션 포함)
log.info("[트랜잭션 시작] {}.{}() | readOnly={} | propagation={} | isolation={}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
method.getName(), readOnly, propagation, isolation);

// 실행 시간 측정을 위한 시작 시간
long start = System.currentTimeMillis();
try {
// 실제 비즈니스 로직 실행
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("[트랜잭션 커밋] {}.{}() | 소요 시간: {}ms",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
method.getName(), elapsed);
return result;
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - start;
log.error("[트랜잭션 롤백] {}.{}() | 소요 시간: {}ms",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
method.getName(), elapsed, e);
throw e;
} finally {
// 리소스 릴리즈 로깅
log.info("[리소스 릴리즈] {}.{}()",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
method.getName());
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/beat/global/common/config/RepositoryConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.beat.global.common.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@Configuration
@EnableJpaRepositories(
basePackages = "com.beat",
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.beat\\.global\\.auth\\.jwt\\.dao\\..*"
)
)
@EnableRedisRepositories(
basePackages = "com.beat.global.auth.jwt.dao"
)
public class RepositoryConfig {
}
Loading