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

[BE-88] 지원서 칸반보드 및 지원자 페이지별 면접 기록 조회 캐싱 적용 #231 #232

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from

Conversation

Profile-exe
Copy link
Collaborator

@Profile-exe Profile-exe commented Aug 29, 2024

개요

close #231

작업사항

브라우저 캐싱

  • ETag 적용

어플리케이션 캐싱

  • @Cacheable 적용
  • @CacheEvict로 캐시 무효화 적용

변경로직

  • ETag를 응답 헤더에 넣고 If-None-Match 요청 헤더와 비교하는 ShallowEtagHeaderFilter 등록

  • CacheControl을 응답 헤더에 적용하기 위해 WebContentInterceptor 등록

  • RedisCacheManager가 존재해 이를 사용

    • 캐싱을 적용할 부분에 @Cacheable 적용
    • 캐시 무효화를 위한 부분에 @CacheEvict 적용
  • Jackson 라이브러리의 LocalDateTime 직렬화 오류 해결

    • LocalDateTimeSerializer, LocalDateTimeDeserializer 적용
  • RedisCacheManager에서 캐싱된 결과를 불러오는 경우 BoardCardResponseDto 직렬화 오류 발생하여 기본 생성자를 정의해 해결

지원서 칸반보드 조회

Board

BoardService::getBoardByColumnsIds 메서드 캐싱

캐시 저장소 이름

boardsByColumnsIds

캐시 무효화 적용

  • BoardService
    • excute
    • createWorkBoard
    • createApplicantBoard
    • relocateCard
    • delete
Columns

ColumnService::getByNavigationId 메서드 캐싱

캐시 저장소 이름

columnsByNavigationId

캐시 무효화

  • BoardService
    • createColumn
    • updateColumnLocation
Card

CardService::getByNavigationId 메서드 캐싱

캐시 저장소 이름

boardCardsByNavigationId

캐시 무효화

  • CardService

    • deleteById
      • BoardService::delete 호출
    • saveWorkCard
      • BoardService::createWorkBoard 호출
    • update
  • BoardService

    delete, createWorkBoardCardService의 메서드에서 호출하므로 무효화 적용 안해도 됨

    • excute
    • createApplicationBoard
    • createColumn
    • relocateCard
    • updateColumnLocation
  • LabelService

    Card의 label count 변경으로 인한 캐시 무효화

    • createLabel
    • deleteLabel
    • createLabelByCardId
  • CommentService

    Card의 comment count 변경으로 인한 캐시 무효화

    • saveComment
    • deleteComment

지원서 칸반보드 조회

Record

RecordService::execute 메서드 캐싱

캐시 저장소 이름

recordsByPage

캐시 무효화 적용

  • RecordService
    • createRecord
    • updateRecordUrl
    • updateRecordContents
    • updateRecord

참고사항

  • backend 브랜치에는 최신 커밋이 BE-84develop 브랜치 최신 커밋은 BE-85입니다.

  • 이번 작업을 진행할 때 backend 브랜치에서 feature 브랜치를 팠기 때문에 PR에 BE-84에 대한 변경사항이 함께 포함되었습니다. develop 브랜치에도 backend의 변경사항인 BE-84가 반영되어야 한다 생각해 이대로 PR 만들었습니다.

  • CacheManager를 구현하고 보니 RedisCacheManager가 등록되어 있어 해당 커밋은 revert 했습니다.

@Profile-exe Profile-exe added the feature✨ This issue or pull request already exists label Aug 29, 2024
@Profile-exe Profile-exe self-assigned this Aug 29, 2024
Copy link
Collaborator

@BlackBean99 BlackBean99 left a 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)
Copy link
Collaborator

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 을 쓰면 깔끔하게 할 수 있어요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Cacheable에서 keycolumnId로 적용한 상황이어야 @CacheEvictkeycolumnId를 넣어 특정 대상만 무효화할 수 있다고 알고있습니다.

그래서 캐싱 방법을 columnIdskey값으로 갖는 것이 아니라 각각의 columnId마다 @Cacheable를 적용해야할 것 같은데, BoardService::getBoardByColumnsIds 메서드에서 받은 columnIds에서 순회를 하며 각 columnIdkey로 두고 캐싱을 적용하도록 시도해보겠습니다!

image

현재는 위 이미지 처럼 boardsByColumnsIdskey값이 "1,2,3"으로 되어있는데,

image

이렇게 각기 다른 ID에 따라 달라지도록 구현해보겠습니다!

Copy link
Collaborator

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 하는 방법도 있습니다. 웹이라 저렇게 구분이 돼 있지만 앱 서버일 경우 전체 화면을 한번에 캐시하는 케이스도 있으니 알고 있으세요!

이런 시도들로 확인해보시는 자세 칭찬합니다.

Comment on lines 319 to 322
@Caching(evict = {
@CacheEvict(value = "columnsByNavigationId", allEntries = true),
@CacheEvict(value = "boardCardsByNavigationId", allEntries = true)
})
Copy link
Collaborator

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 방법을 쓰기도 합니다. (복잡하긴 함)
한번 고민해보세요.

Copy link
Collaborator Author

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를 이용

    boardLoadPortCacheManager를 주입받음

    • @Before

      • dto에서 받은 idboardLoadPort를 이용해 currentBoard, targetBoard를 가져옴

      • Board::getColumnId 메서드로 이동 전, 후 카드의 columnId를 가져와 AOP 클래스 멤버변수에 저장해둠.
        (멤버변수에 저장하는 것은 방법 2를 위한 것. 만약 방법 1만 사용한다면 멤버변수 사용 안함)

      • 방법 1 @CacheEvict와 동일.

        • columnId에 해당하는 boardsByColumnsIds 캐시 무효화
    • @After

      • 방법 2 @CachePut과 동일.

        • 저장해둔 columnId를 이용해 getBoardByColumnsId 메서드로 가져와서(구현 필요) 캐시에서 key(columnId)에 해당하는 값 변경

무효화가 된다면 특정 columnId에 해당하는 board들이 필요할 때 DB에서 조회하고 자동으로 캐싱이 될 것이므로 방법 1을 고려하고있습니다.

Comment on lines 22 to 23
builder.addFilterBefore(
new JwtTokenFilter(jwtTokenProvider), BasicAuthenticationFilter.class);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 인증은 왜 바꾼건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 앞서 말씀드린 상황(BE-84backend, BE-85develop에 merge된 상황)에서 생긴 것인데요,
제가 backend 브랜치에서 checkout을 하고 develop으로 PR을 날렸기 때문에 BE-84 PR 내용이 반영된 상황입니다!

인증 로직 변경 사유는 @LJH098 에게 연락해 확인해보겠습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#225 PR에서 반영된 내용인데, 커스텀 filter를 빈으로 등록하게 되면, default filter chian으로 등록된다고 합니다.

그래서 new 연산자를 통해 필터를 등록해 security filter chian에 등록되도록 변경한 내용입니다!

Comment on lines +20 to +28
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@CreatedDate
private LocalDateTime createdAt;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@LastModifiedDate
private LocalDateTime updatedAt;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존걸로 원래는 돌아갔는데 캐시 매니지하는경우에서 Serialize가 안되는 문제가 있나보내요? 관련 레퍼런스나 문제 리포트를 공유해주실 수 있을까요? 여기 의존도가 높아서 막 바꾸는게 위험할 수도 있어보여서요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캐싱을 위해 @Cacheable 어노테이션을 사용하니 SerializationException이 발생했습니다.

로컬에서 구동했으며 redisredis: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
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 DTO에서 의존도가 높은 모델이니 꼭 수정 후 연관된 요청에 대해 QA후 보고해주세요. 수고했어요

Base automatically changed from develop to backend August 31, 2024 15:08
@Profile-exe Profile-exe changed the base branch from backend to develop September 2, 2024 14:41
- 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`를 함께 추가
- self invocation을 방지하기 위해 `BoardCacheService` 클래스를 구현
- columnIds를 순회하며 columnId 각각을 key로 가지는 캐싱 적용

이를 통해 `@CacheEvict` 시 key 값으로 columnId를 넘겨 필요한 부분만 무효화를 적용시킬 수 있음
업무카드를 삭제할 때만 호출되는 메서드이므로 지원서 카드 댓글에 대한 캐시 무효화 로직은 불필요해 제거
기존에는 columnId 들을 리스트로 받아 key로 적용했기에 Ids를 사용

현재는 columnId마다 key로 구분하므로 Id로 변경
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature✨ This issue or pull request already exists
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BE-88] 지원서 칸반보드 및 지원자 페이지별 면접 기록 조회 캐싱 적용
2 participants