T_era
템플릿 메서드 패턴과 AOP 로깅 구현: 패턴 적용의 적절성에 대한 고찰 본문
1. 템플릿 메서드 패턴이란?
템플릿 메서드 패턴(Template Method Pattern)은 행위(Behavioral) 디자인 패턴 중 하나로, 알고리즘의 뼈대(구조)를 정의하고, 특정 단계의 구현은 하위 클래스에게 맡기는 패턴입니다.
핵심 특징:
- 추상 클래스를 통해 알고리즘의 뼈대를 작성
- 구현체를 통해 구체적인 알고리즘 단계를 구현
- 상속을 통한 코드 재사용과 확장성 제공
2. AOP 로깅에서 템플릿 메서드 패턴을 적용한 이유
AOP를 통해 로깅을 구현할 때 다음과 같은 문제에 직면했습니다:
- 중복 코드 문제: 각 로깅 시점마다 새로운 Aspect를 만들면 중복되는 내용이 많음
- ID 추출 로직의 다양성:
- User: JWT 토큰에서 사용자 ID 추출
- Post: 요청/응답 객체에서 postId 추출
- 기타 도메인: 각각 다른 방식으로 ID 추출 필요
대안으로 고려한 패턴들:
- 전략 패턴 (Strategy Pattern)
- SpEL (Spring Expression Language)
- 템플릿 메서드 패턴
3. 구현된 코드 구조
3.1 추상 클래스: AbstractLoggingAspect
public abstract class AbstractLoggingAspect {
public final Object executeLogging(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 요청 로깅
String requestId = extractEntityId(targetClass, method, args, null, "request", serviceType);
// 로그 메시지 생성 및 DB 저장
Object result;
try {
// 2. 메서드 실행
result = joinPoint.proceed();
// 3. 응답 로깅
String responseId = extractEntityId(targetClass, method, args, result, "response", serviceType);
// 응답 메시지 생성 및 DB 저장
} catch (Exception e) {
// 4. 에러 로깅
String errorId = extractEntityId(targetClass, method, args, null, "error", serviceType);
// 에러 메시지 생성 및 DB 저장
throw e;
}
return result;
}
// 추상 메서드: ID 추출 로직을 하위 클래스에서 구현
protected abstract String extractEntityId(
Class<?> targetClass,
Method method,
Object[] args,
Object result,
String logType,
String serviceType
);
}
3.2 User 로깅 구현체
@Component
@Aspect
public class UserLoggingAspect extends AbstractLoggingAspect {
@Around("com.example.demo.global.log.LoggingPointcut.userLogging()")
public Object userLogging(ProceedingJoinPoint joinPoint) throws Throwable {
return executeLogging(joinPoint);
}
@Override
protected String extractEntityId(
Class<?> targetClass,
Method method,
Object[] args,
Object result,
String logType,
String serviceType
) {
if ("USER".equals(serviceType)) {
// JWT 토큰에서 사용자 ID 추출
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast)
.map(ServletRequestAttributes::getRequest)
.map(request -> request.getHeader(Const.AUTHORIZATION_HEADER))
.map(tokenValue -> {
try {
String substringToken = jwtUtil.substringToken(tokenValue);
return jwtUtil.getUserId(substringToken).toString();
} catch (Exception e) {
return "INVALID_TOKEN";
}
})
.orElse("NO_TOKEN");
}
return "N/A";
}
}
3.3 Post 로깅 구현체
@Component
@Aspect
public class PostLoggingAspect extends AbstractLoggingAspect {
@Around("com.example.demo.global.log.LoggingPointcut.postLogging()")
public Object postLogging(ProceedingJoinPoint joinPoint) throws Throwable {
return executeLogging(joinPoint);
}
@Override
protected String extractEntityId(
Class<?> targetClass,
Method method,
Object[] args,
Object result,
String logType,
String serviceType
) {
if ("POST".equals(serviceType)) {
try {
if(result == null) {
// 요청에서 postId 추출
for (Object arg : args) {
if (arg != null && arg.toString().contains("postId")) {
return "POST_" + arg;
}
}
} else {
// 응답에서 postId 추출
ResponseEntity<?> responseEntity = (ResponseEntity<?>) result;
Object body = responseEntity.getBody();
if (body instanceof Map<?, ?> bodyMap) {
Object data = bodyMap.get("data");
if (data != null) {
Field postIdField = data.getClass().getDeclaredField("postId");
postIdField.setAccessible(true);
Object postId = postIdField.get(data);
return "POST_" + postId.toString();
}
}
}
return "POST_NEW";
} catch (Exception e) {
return "POST_ERROR";
}
}
return "N/A";
}
}
4. 템플릿 메서드 패턴 적용의 문제점
4.1 패턴의 의도와 실제 구현의 괴리
템플릿 메서드 패턴의 본래 의도:
- 알고리즘의 뼈대를 명확히 정의
- 하위 클래스만 보고도 전체 알고리즘의 흐름을 이해할 수 있어야 함
- 각 단계의 역할과 책임이 명확해야 함
현재 구현의 문제점:
// 이 코드만 보고는 왜 ID를 추출하는지 알 수 없음
@Override
protected String extractEntityId(...) {
// 복잡한 ID 추출 로직
// JWT 토큰 파싱, 리플렉션 사용 등
}
4.2 코드 가독성과 유지보수성 저하
- 의도 파악의 어려움:
extractEntityId메서드만 보고는 이 메서드가 로깅을 위한 ID 추출이라는 것을 알기 어려움 - 책임 분산: ID 추출 로직이 로깅 Aspect에 포함되어 있어 단일 책임 원칙 위반
- 확장성 제한: 새로운 도메인마다 새로운 Aspect 클래스를 만들어야 함
5. 더 나은 대안들
5.1 전략 패턴 (Strategy Pattern)
public interface EntityIdExtractor {
String extractId(
Class<?> targetClass,
Method method,
Object[] args,
Object result,
String logType,
String serviceType
);
}
@Component
public class UserIdExtractor implements EntityIdExtractor {
@Override
public String extractId(...) {
// JWT 토큰에서 사용자 ID 추출 로직
}
}
@Component
public class PostIdExtractor implements EntityIdExtractor {
@Override
public String extractId(...) {
// Post ID 추출 로직
}
}
5.2 SpEL (Spring Expression Language)
@UserLogging(entityIdExpression = "#request.getHeader('Authorization')")
public ResponseEntity<?> createUser(@RequestBody CreateUserRequestDto request) {
// 메서드 구현
}
@PostLogging(entityIdExpression = "#result.body.data.postId")
public ResponseEntity<?> getPost(@PathVariable Long postId) {
// 메서드 구현
}
5.3 함수형 인터페이스 활용
@FunctionalInterface
public interface IdExtractor {
String extract(ProceedingJoinPoint joinPoint, Object result);
}
public class LoggingAspect {
private final Map<String, IdExtractor> extractors = Map.of(
"USER", this::extractUserId,
"POST", this::extractPostId
);
}
6. 결론 및 교훈
6.1 템플릿 메서드 패턴 적용의 적절성
적합하지 않은 이유:
- 의도 전달의 실패: 하위 클래스만으로는 전체 알고리즘의 목적을 파악하기 어려움
- 책임 분산: 로깅과 ID 추출이라는 서로 다른 책임이 혼재
- 확장성 부족: 새로운 도메인마다 새로운 클래스 생성 필요
6.2 디자인 패턴 선택 시 고려사항
- 패턴의 본래 의도 파악: 패턴이 해결하려는 문제와 현재 상황이 일치하는지 확인
- 코드 가독성: 다른 개발자가 코드를 보고 의도를 쉽게 파악할 수 있는지 검토
- 유지보수성: 향후 변경사항에 대한 대응이 용이한지 고려
- 단일 책임 원칙: 각 클래스와 메서드의 책임이 명확한지 확인
6.3 개선 방향
현재 구현에서는 전략 패턴이나 SpEL을 활용하는 것이 더 적절할 것으로 판단됩니다. 추후에 기능 개선을 통해 전략 패턴을 적용해보고 기능의 의도와 목적이 명확해 진다면 앞으로도 전략 패턴을 사용하게 될 것 같습니다.
핵심 교훈:
디자인 패턴은 도구일 뿐이며, 상황에 맞는 적절한 패턴을 선택하는 것이 중요합니다. 패턴을 적용할 때는 항상 "이 패턴이 코드의 의도를 명확하게 전달하는가?"를 자문해보아야 합니다.
Q: "그냥 구현하면 되는 거 아닌가? joinPoint에서 가져오면 되잖아?"
A: 좋은 방법 입니다. 하지만 여기서 문제는 ID를 가져오는 방식이 도메인마다 다르다는 점이기 때문에 사용할 수 없는 문제가 있습니다. User의 경우 JWT 토큰에서 사용자 ID를 추출해야 하고, Post의 경우 요청/응답 객체에서 postId를 찾아야 합니다. 단순히 joinPoint에서 가져오는 것으로는 해결되지 않는 복잡한 로직이 필요했기 때문에 방법을 찾아보게 되었습니다.
Q: "왜 템플릿 메서드 패턴을 선택했나요? 다른 패턴은 안 되나요?"
A: 처음에는 중복 코드를 줄이고 싶어서 템플릿 메서드 패턴을 선택했습니다. 하지만 구현해보니 패턴의 의도와 실제 사용 목적이 맞지 않는다는 것을 알게되었고, 템플릿 메서드 패턴은 알고리즘의 뼈대를 정의하는 것이 목적인데, 우리는 단순히 ID 추출 방식만 다르게 하고 싶었던 것을 늦게 깨닳았습니다.
Q: "이 코드만 보고는 왜 ID를 추출하는지 알 수 없다고 하셨는데, 그게 왜 문제인가요?"
A: 템플릿 메서드 패턴의 핵심은 하위 클래스만 보고도 전체 알고리즘의 흐름을 이해할 수 있어야 한다는 것입니다. 하지만 extractEntityId 메서드만 보면 무엇을 위한 ID 추출인지 알기 어려워요. 메서드명만으로는 의도가 명확하지 않죠.
Q: "그럼 어떤 패턴이 더 적절했을까요?"
A: 전략 패턴이나 SpEL이 더 적절했을 것 같습니다. 전략 패턴은 ID 추출 전략을 명확하게 분리할 수 있고, SpEL은 어노테이션만으로 ID 추출 로직을 명시할 수 있어서 코드의 의도가 더 명확해집니다. 하지만 개인적인 기호로 전략 패턴을 적용해보고 의도와 사용 방식이 적합하다면 SpEL사용은 피할 것 같습니다. 왜냐하면 SpEL은 문자열을 직접적으로 사용해야 하기 때문에 컴파일 단계에서 문제 파악히 힘들거든요
Q: "이런 실패 경험을 굳이 작성한 이유가 무엇인가요? 성공하고 좋은 방법만 올리는게 좋지 않나요?"
A: 디자인 패턴은 도구일 뿐이기 때문에 어떠한 기술스택을 선택할 때와 마찬가지로 패턴을 적용할 때는 항상 "이 패턴이 코드의 의도를 명확하게 전달하는가?", "이 패턴이 현재 상황에 정말 적합한가?"를 자문해보아야 해요. 패턴에 얽매이기보다는 문제를 해결하는 데 가장 적합한 방법을 선택하는 것이 중요합니다. 그래서 실패경험도 작성하면서 좀 더 고민을 많이해보고 싶었습니다.
'Programing > Spring' 카테고리의 다른 글
| 헬퍼(Helper)와 유틸(Utility)의 차이, 그리고 실전 코드 예시 (0) | 2025.06.25 |
|---|---|
| Spring API와 Postman으로 네트워크 오버헤드 실험하기 (0) | 2025.06.24 |
| AOP로 로깅을 할 때 동적으로 값을 조율할 땐 어떤 방법을 사용할까 (1) | 2025.06.22 |
| WebFlux와 일반 route의 차이 (0) | 2025.06.16 |
| api gateway yml 설정 (0) | 2025.06.16 |