다시 AOP로 돌아왔다.
@Aspect
@Component
@Log4j2
public class CacheControlAspect {
@Around("@annotation(com.portal.util.annotation.CacheControl)")
public Object handleCacheControl(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("handleCacheControl method invoked ????????????");
// HTTP 응답 가져오기
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attrs.getRequest();
HttpServletResponse response = attrs.getResponse();
// 메서드와 어노테이션 가져오기
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
CacheControl cacheControl = method.getAnnotation(CacheControl.class);
log.info("Handling cache control for method: {}", method.getName());
// 메서드 실행
Object result = joinPoint.proceed();
if (cacheControl != null && result instanceof ActionResult) {
ActionResult actionResult = (ActionResult) result;
// ActionResult의 데이터 사용해 ETag 생성
String eTag = generateETag(actionResult);
log.info("Generated ETag: {}", eTag);
// If-None-Match 헤더 확인
String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
log.info("If-None-Match header: {}", ifNoneMatch);
if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
log.debug("ETag matches. Returning 304 Not Modified");
// ETag가 일치하면 304 Not Modified 반환
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return null;
}
// ETag와 Cache-Control 헤더 추가
response.setHeader(HttpHeaders.ETAG, eTag);
log.info("Setting ETag header: {}", eTag);
String cacheControlHeader = "max-age=" + cacheControl.maxAge();
log.info("Setting Cache-Control header: {}", cacheControlHeader);
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + cacheControl.maxAge());
} else{
log.info("Cache control not applied. Result is not ActionResult or CacheControl is null **********");
}
return result;
}
private String generateETag(ActionResult actionResult) {
log.info("ActionResult content: {}", actionResult);
StringBuilder content = new StringBuilder();
content.append(actionResult.getResultCd());
content.append(actionResult.getResultMessage());
content.append(actionResult.getResultMessageCd());
Map<String, Object> data = actionResult.getData();
if (data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
content.append(entry.getKey()).append(entry.getValue());
}
}
String eTag = DigestUtils.md5DigestAsHex(content.toString().getBytes(StandardCharsets.UTF_8));
log.info("Generated ETag from ActionResult: {}", eTag);
return eTag;
}
}
/**
* 메서드 실행 전:
* @Around 어노테이션을 통해 @CacheControl이 적용된 메서드가 호출될 때마다 handleCacheControl 메서드가 실행.
* RequestContextHolder를 사용하여 현재 요청의 HttpServletResponse 객체를 가져옴. 이를 통해 응답 헤더를 설정 가능
* 메서드에 적용된 @CacheControl 어노테이션을 읽어 maxAge 값을 확인.
*
* 메서드 실행 후:
* 원래의 메서드를 실행 (joinPoint.proceed())하고 그 결과를 반환.
* 이 과정에서 캐시 관련 헤더가 응답에 설정.
*
* 결론
* 이 AOP 모듈은 메서드의 캐시 관련 설정을 담당하며, 비즈니스 로직과 분리되어 있음. 캐시 헤더 설정과 같은 공통 관심사를 분리하여 재사용 가능하게 만듬.
* Pointcut:
*
* @CacheControl 어노테이션이 적용된 메서드에 대해서만 이 AOP 로직이 적용됨. 이를 통해 필요한 경우에만 캐시 설정을 적용할 수 있음.
*
*/
지피티와 함께 이렇게 주절주절 써봤다.
이땐 api 통신에 되던 캐싱도 안됐다. 모든 응답이 200이었다..
저기서 ETag 부분을 삭제했더니 또 캐싱은 됐다. 뭐가 문제고 뭐 때문에 되고 뭐 때문에 또 안되는지 하나도 몰랐다.
import java.util.Base64;
@Aspect
@Component
@Log4j2
public class CacheControlAspect {
@Around("@annotation(com.portal.util.annotation.CacheControl)")
public Object handleCacheControl(ProceedingJoinPoint joinPoint) throws Throwable {
// HTTP 요청 및 응답 가져오기
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attrs.getRequest();
HttpServletResponse response = attrs.getResponse();
// 메서드와 어노테이션 가져오기
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
CacheControl cacheControl = method.getAnnotation(CacheControl.class);
log.info("&&&& Handling cache control for method: {}", method.getName());
// 메서드 실행
Object result = joinPoint.proceed();
if (cacheControl != null && result instanceof ActionResult) {
ActionResult actionResult = (ActionResult) result;
// 응답 데이터를 JSON으로 정규화하여 ETag 생성
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); // JSON 필드 순서 고정
String normalizedJson = mapper.writeValueAsString(actionResult.getData()); // 데이터 부분만 해시 생성에 사용
log.info("&&&& $$$$ data = " + normalizedJson);
// 해시 생성 (SHA-256 사용)
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(normalizedJson.getBytes(StandardCharsets.UTF_8));
String eTag = Base64.getEncoder().encodeToString(hash);
log.info("&&&& Generated ETag: {}", eTag);
// If-None-Match 헤더 확인
String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
log.info("&&&& If-None-Match header: {}", ifNoneMatch);
// ETag가 일치하면 304 Not Modified 반환
if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
log.debug("&&&& ETag matches. Returning 304 Not Modified");
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return null; // 본문 없이 304 응답 반환
}
// ETag와 Cache-Control 헤더 설정
response.setHeader(HttpHeaders.ETAG, eTag);
log.info("&&&& Setting ETag header: {}", eTag);
String cacheControlHeader = "max-age=" + cacheControl.maxAge();
log.info("&&&& Setting Cache-Control header: {}", cacheControlHeader);
response.setHeader(HttpHeaders.CACHE_CONTROL, cacheControlHeader);
return actionResult; // 200 응답 시 본문 포함
} else {
log.info("&&&& Cache control not applied. Result is not ActionResult or CacheControl is null");
}
return result;
}
}
일단 모든 응답이 캐싱되지 않았던 문제는
컨트롤러에 달아주었던
@CacheControl(maxAge = 60)
이 놈이 문제였다.
maxAge를 주면 그 시간동안 캐싱되어서 데이터 변경이 되어도 알빠노 무시되고 캐싱되었기 때문에 아무리 요청해도 돌아오는 응답은 200 뿐이었다.
그래서
@CacheControl(maxAge = 0)
했더니 304로 캐싱이 잘 됐다.
그리고 데이터베이스에서 데이터를 하드코딩해서 변경하면 응답이 달라져서 ETag도 달라졌고,
그 땐 200으로 필요한 응답도 잘 돌아왔다.
아 다 끝났다 ㅋ
하고선 필요없는 이랍시고 로깅 지우고 뭐 지우고 하고 다시 테스트 해봤더니 또 안됐다.
콘솔에 요청마다
Generated ETag: abcdefg12345
If-None-Match header: null
Setting ETag header: abcdefg12345
라고 찍혔다.
분명 같은 요청에 대해 같은 ETag는 생성되는데 ... 대체 왜 ...
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + cacheControl.maxAge());
이 부분을 지운게 문제였다.
이 부분을 지웠더니 헤더에 캐시 컨트롤 한다고 알리지 않게 되어버린 것이었던 것이다....
어쩐지 아예 메서드를 안타버리더라...
// ETag와 Cache-Control 헤더 설정
response.setHeader(HttpHeaders.ETAG, eTag);
response.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=0");
log.info("Setting ETag header: {}", eTag);
요로코롬 헤더를 두개 다 넣어줘야 ETag도 넘어가고 나 캐시 할 거임~도 잘넘어간다.
아무튼 그래서
최종적으로
@Aspect
@Component
@Log4j2
public class CacheControlAspect {
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object handleCacheControl(ProceedingJoinPoint joinPoint) throws Throwable {
// HTTP 요청 및 응답 가져오기
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attrs.getRequest();
HttpServletResponse response = attrs.getResponse();
// 메서드와 어노테이션 가져오기
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
CacheControl cacheControl = method.getAnnotation(CacheControl.class);
log.info("Handling cache control for method: {}", method.getName());
// 메서드 실행
Object result = joinPoint.proceed();
ActionResult actionResult = (ActionResult) result;
// 응답 데이터를 JSON으로 정규화하여 ETag 생성
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); // JSON 필드 순서 고정
String normalizedJson = mapper.writeValueAsString(actionResult.getData()); // 데이터 부분만 해시 생성에 사용
log.info("Response body Data = " + normalizedJson);
// 해시 생성 (SHA-256 사용)
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(normalizedJson.getBytes(StandardCharsets.UTF_8));
String eTag = Base64.getEncoder().encodeToString(hash);
log.info("Generated ETag: {}", eTag);
// If-None-Match 헤더 확인
String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
log.info("If-None-Match header: {}", ifNoneMatch);
// ETag가 일치하면 304 Not Modified 반환
if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
log.debug("ETag matches. Returning 304 Not Modified");
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return null; // 본문 없이 304 응답 반환
}
// ETag와 Cache-Control 헤더 설정
response.setHeader(HttpHeaders.ETAG, eTag);
response.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=0");
log.info("Setting ETag header: {}", eTag);
return actionResult; // 200 응답 시 본문 포함
}
이렇게 작성해주었다.
조회에만 캐싱하면 될 것 같아서 일단 모든 @GetMapping 어노테이션에 캐싱되도록 설정했다.
첫 요청 이후로 304 응답이 오는 것을 확인했다.
우와 신기하다~ 하고서 서버 끄려다가 디비에 데이터 안바꿔본 걸 깜빡하고 디비에 값도 변경해주었는데
데이터 바뀐 응답은 ETag도 잘 바뀐다 ~~ 아싸~
'Spring Boot' 카테고리의 다른 글
JSON_ARRAYAGG & @JsonRawValue (1) | 2024.09.24 |
---|---|
AOP를 활용한 브라우저 캐시_4 정적 리소스 캐싱 명시 (0) | 2024.09.11 |
AOP를 활용한 브라우저 캐시_1_전략 전택 (1) | 2024.09.10 |
AOP를 활용한 브라우저 캐시_2_ETag & 인터셉터 (실패) (0) | 2024.09.10 |
Controller Annotation (0) | 2024.09.04 |