-
Notifications
You must be signed in to change notification settings - Fork 8
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
[BE-88] 지원서 칸반보드 및 지원자 페이지별 면접 기록 조회 캐싱 적용 #231 #232
base: develop
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋아요는 캐싱 전략이 안보이네요? 그 부분은 나름 1차 합격을 결정하는 중요시 여기는 요소기 때문에 다시 한번 보세요.
제가 바빠서 하지 못했던 캐시 부분을 들여다 봐주셔서 고맙습니다. 처음 보는 코드에 이렇게 적용해보기 어려웠을 텐데 고생했어요.
추가적으로 조언드릴 점은 Caching으로 떡칠돼 있는 코드가 나중에 기능이 추가될때 인수인계를 어렵게 만들기도 하고 곧 바로 대응하기도 어려울 거에요. AoP로 특정 메소드들을 묶어서 캐시를 공통적으로 처리할 수 있도록 충분히 개선해볼 수 있을 것 같아요. 예를 들면
@AfterReturning("@annotation(invalidateCache)")
public void evictCache(JoinPoint joinPoint, InvalidateCache invalidateCache) {
String cacheName = invalidateCache.cacheName();
String key = invalidateCache.key();
Cache cache = cacheManager.getCache(cacheName);
if (!key.isEmpty()) {
cache.evict(key);
} else {
cache.clear();
}
}
대충 이런식으로요. 더 클린하게 짜볼 수 있도록 같이 고민해봅시다.
public void execute(Board board) { | ||
boardRecordPort.save(board); | ||
} | ||
|
||
@Override | ||
@CacheEvict(value = "boardsByColumnsIds", allEntries = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 delete하게 될때마다 전체를 allEntries를 날려버리면 조금 비효율적일 수도 있습니다. 삭제한 대상만을 무효화 시키는 방법이 더좋을 수도 있습니다. 지원서는 기본적으로 캐시 데이터가 클텐데 그때마다 전체 refresh 하면 에코노 인프라 자체가 좋지 못해서 부하가 있을 수도 있어요. 이렇게 하시면 레디스 모니터링 해서 한번 꼭 개발서버에서 테스트 해보세요.
관련해서 추천할 내용은
@CacheEvict(value = "boardsByColumnsIds", key = "#columnId")
이런식으로 SpEL 을 쓰면 깔끔하게 할 수 있어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Cacheable
에서 key
를columnId
로 적용한 상황이어야 @CacheEvict
의 key
에 columnId
를 넣어 특정 대상만 무효화할 수 있다고 알고있습니다.
그래서 캐싱 방법을 columnIds
를 key
값으로 갖는 것이 아니라 각각의 columnId
마다 @Cacheable
를 적용해야할 것 같은데, BoardService::getBoardByColumnsIds
메서드에서 받은 columnIds
에서 순회를 하며 각 columnId
를 key
로 두고 캐싱을 적용하도록 시도해보겠습니다!
현재는 위 이미지 처럼 boardsByColumnsIds
의 key
값이 "1,2,3"
으로 되어있는데,
이렇게 각기 다른 ID
에 따라 달라지도록 구현해보겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실제로 실무 전사 플랫폼같은 경우에는 자주 메인 화면 전체를 캐싱해두는 경우가 있습니다. 리스트 1,2,3처럼 할 수도 있지만 화면 전체를 Dto로 묶어서 Cache 하는 방법도 있습니다. 웹이라 저렇게 구분이 돼 있지만 앱 서버일 경우 전체 화면을 한번에 캐시하는 케이스도 있으니 알고 있으세요!
이런 시도들로 확인해보시는 자세 칭찬합니다.
@Caching(evict = { | ||
@CacheEvict(value = "columnsByNavigationId", allEntries = true), | ||
@CacheEvict(value = "boardCardsByNavigationId", allEntries = true) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nested Annotation을 도입한건 간만에 신선한 방법이네요 칭찬드립니다 따봉
여기서 쓸 수 있는 다른 방법은 update연산이기 때문에 성능과 캐시 일관성을 유지시키는 고민을 좀해봐야 합니다. 특히 실제 이 서비스에서 가장 많이 이루어지는게 카드 위치를 옮기는 것이거든요. 1초에 많으면 여러 사람이 하면 5번의 캐시를 무효화시켜야 할 수도 있을 것 같은데, 이렇게 되면 캐시가 꼬일 수도 있을 것 같아요.
@CachePut(value = "boardCache", key = "#updateDto.id") 이렇게 put으로 대체도 가능하다는 점.
그래서 동시성 캐시 무효화 문제를 해결하기 위해서 종종 Cache Event Driven Invalidation 방법을 쓰기도 합니다. (복잡하긴 함)
한번 고민해보세요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 이 부분은 @CachePut
을 이용해서 캐시에 해당 변경사항을 바로 반영하는게 이상적이라 생각합니다!
특히 이 메서드는 column
의 위치 변경이므로 카드 위치를 변경하는 relocateCard
메서드에 해당 부분을 적용하고 싶은데요,
@CachePut
은 반환하는 값을 캐시에 저장하기 때문에 List<Board>
를 반환하도록 relocateCard
의 명세를 변경해야 하는 문제가 있어 다음과 같은 방식을 생각해보았습니다.
boardsByColumnsIds
캐시 저장소가 코멘트 드린 내용대로 columnId
마다 캐싱되게 변경된 경우에 고려한 구현 방법입니다!
-
AOP
를 이용boardLoadPort
와CacheManager
를 주입받음-
@Before
-
dto
에서 받은id
는boardLoadPort
를 이용해currentBoard
,targetBoard
를 가져옴 -
Board::getColumnId
메서드로 이동 전, 후 카드의columnId
를 가져와 AOP 클래스 멤버변수에 저장해둠.
(멤버변수에 저장하는 것은 방법 2를 위한 것. 만약 방법 1만 사용한다면 멤버변수 사용 안함) -
방법 1
@CacheEvict
와 동일.columnId
에 해당하는boardsByColumnsIds
캐시 무효화
-
-
@After
-
방법 2
@CachePut
과 동일.- 저장해둔
columnId
를 이용해getBoardByColumnsId
메서드로 가져와서(구현 필요) 캐시에서key(columnId)
에 해당하는 값 변경
- 저장해둔
-
-
무효화가 된다면 특정 columnId
에 해당하는 board
들이 필요할 때 DB
에서 조회하고 자동으로 캐싱이 될 것이므로 방법 1
을 고려하고있습니다.
builder.addFilterBefore( | ||
new JwtTokenFilter(jwtTokenProvider), BasicAuthenticationFilter.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 인증은 왜 바꾼건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JsonSerialize(using = LocalDateTimeSerializer.class) | ||
@JsonDeserialize(using = LocalDateTimeDeserializer.class) | ||
@CreatedDate | ||
private LocalDateTime createdAt; | ||
|
||
@JsonSerialize(using = LocalDateTimeSerializer.class) | ||
@JsonDeserialize(using = LocalDateTimeDeserializer.class) | ||
@LastModifiedDate | ||
private LocalDateTime updatedAt; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기존걸로 원래는 돌아갔는데 캐시 매니지하는경우에서 Serialize가 안되는 문제가 있나보내요? 관련 레퍼런스나 문제 리포트를 공유해주실 수 있을까요? 여기 의존도가 높아서 막 바꾸는게 위험할 수도 있어보여서요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
캐싱을 위해 @Cacheable
어노테이션을 사용하니 SerializationException
이 발생했습니다.
로컬에서 구동했으며 redis
는 redis:latest
이미지를 docker
에서 띄워서 실행중입니다.
Java
버전은 17.0.12
입니다.
저와 동일한 이슈의 블로그 글을 보고
직렬화/역직렬화 시 사용되는 클래스를 지정하는 용도로 이해하고 해당 어노테이션을 적용했습니다.
Redis
에 저장된 직렬화 내용 일부를 첨부하겠습니다.
{
"@class": "com.econovation.recruitdomain.domains.board.domain.Board",
"createdAt": [2024, 8, 31, 8, 34, 2],
"updatedAt": [2024, 8, 31, 8, 34, 2],
"id": 2,
"nextBoardId": null,
"cardType": "INVISIBLE",
"cardId": null,
"columnId": 2,
"navigationId": 1
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여러 DTO에서 의존도가 높은 모델이니 꼭 수정 후 연관된 요청에 대해 QA후 보고해주세요. 수고했어요
4a6acb5
to
3f5c811
Compare
- ETag 사용을 위해 `Cache-Control`헤더에 `no-cache`, `must-revalidate` 적용
- 응답을 받아 고유한 ETag를 생성하고 요청의 If-None-Match와 비교하여 값이 같다면 304 응답 반환 - 해당 필터를 로컬에서 사용하려면 `@Profile`에 `local` 추가하기
- 사용할 캐시 저장소를 등록
- `BoardService::getBoardByColumnsIds` 응답 캐싱
- `ColumnService::getByNavigationId` 응답 캐싱
- `CardService::getByNavigationId` 응답 캐싱
- `RecordService::execute` 응답 캐싱
InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default 위와 같은 오류가 발생했고, `LocalDateTimeSerializer`, `LocalDateTimeDeserializer`를 적용해 오류 해결
- 기본 생성자가 없어 Jackson 라이브러리에서 `InvalidDefinitionException` 예외를 발생시킴 - 기본생성자를 `@@NoArgsConstructor`로 넣어주고 `@Builder` 어노테이션을 사용하기 위해 `@AllArgsConstructor`를 함께 추가
This reverts commit 8f01543.
- self invocation을 방지하기 위해 `BoardCacheService` 클래스를 구현 - columnIds를 순회하며 columnId 각각을 key로 가지는 캐싱 적용 이를 통해 `@CacheEvict` 시 key 값으로 columnId를 넘겨 필요한 부분만 무효화를 적용시킬 수 있음
업무카드를 삭제할 때만 호출되는 메서드이므로 지원서 카드 댓글에 대한 캐시 무효화 로직은 불필요해 제거
기존에는 columnId 들을 리스트로 받아 key로 적용했기에 Ids를 사용 현재는 columnId마다 key로 구분하므로 Id로 변경
39c9912
to
e4ab6c2
Compare
e4ab6c2
to
ad88883
Compare
개요
close #231
작업사항
브라우저 캐싱
ETag
적용어플리케이션 캐싱
@Cacheable
적용@CacheEvict
로 캐시 무효화 적용변경로직
ETag
를 응답 헤더에 넣고If-None-Match
요청 헤더와 비교하는ShallowEtagHeaderFilter
등록CacheControl
을 응답 헤더에 적용하기 위해WebContentInterceptor
등록RedisCacheManager
가 존재해 이를 사용@Cacheable
적용@CacheEvict
적용Jackson 라이브러리의
LocalDateTime
직렬화 오류 해결LocalDateTimeSerializer
,LocalDateTimeDeserializer
적용RedisCacheManager
에서 캐싱된 결과를 불러오는 경우BoardCardResponseDto
직렬화 오류 발생하여 기본 생성자를 정의해 해결지원서 칸반보드 조회
Board
BoardService::getBoardByColumnsIds
메서드 캐싱캐시 저장소 이름
캐시 무효화 적용
BoardService
excute
createWorkBoard
createApplicantBoard
relocateCard
delete
Columns
ColumnService::getByNavigationId
메서드 캐싱캐시 저장소 이름
캐시 무효화
BoardService
createColumn
updateColumnLocation
Card
CardService::getByNavigationId
메서드 캐싱캐시 저장소 이름
캐시 무효화
CardService
deleteById
BoardService::delete
호출saveWorkCard
BoardService::createWorkBoard
호출update
BoardService
excute
createApplicationBoard
createColumn
relocateCard
updateColumnLocation
LabelService
createLabel
deleteLabel
createLabelByCardId
CommentService
saveComment
deleteComment
지원서 칸반보드 조회
Record
RecordService::execute
메서드 캐싱캐시 저장소 이름
캐시 무효화 적용
RecordService
createRecord
updateRecordUrl
updateRecordContents
updateRecord
참고사항
backend
브랜치에는 최신 커밋이 BE-84고develop
브랜치 최신 커밋은 BE-85입니다.이번 작업을 진행할 때
backend
브랜치에서feature
브랜치를 팠기 때문에 PR에BE-84
에 대한 변경사항이 함께 포함되었습니다.develop
브랜치에도backend
의 변경사항인BE-84
가 반영되어야 한다 생각해 이대로 PR 만들었습니다.CacheManager
를 구현하고 보니RedisCacheManager
가 등록되어 있어 해당 커밋은 revert 했습니다.