Skip to content

Refactor: 캐싱 적용#67

Open
seokjun01 wants to merge 9 commits into
feat/#20from
refactor/#62
Open

Refactor: 캐싱 적용#67
seokjun01 wants to merge 9 commits into
feat/#20from
refactor/#62

Conversation

@seokjun01

Copy link
Copy Markdown
Collaborator

📌 작업한 내용

기존 몇몇 도메인의 조회로직을 레디스를 사용하여 분리하였습니다.

레디스 선정 이유

: 레디스는 관계형 데이터가 아닌, key-value 형태로 데이터를 저장하기 때문에 단순 조회에 있어 성능 향상 기대(고속 읽기)
: 또한 일반 DB처럼 디스크에 데이터를 쓰는 구조가 아닌, 메모리에서 데이터를 처리하기 때문에 작업 처리 속도가 빠름
: 다양한 자료구조 지원 ,메모리 기반이지만 데이터의 영속적인 보존이 가능

캐싱이란?

: 한 번 조회된 데이터를 미리 특정 공간에 저장해두고, 똑같은 요청이 발생 시, 서버에 요청하지 않고 저장해 둔 데이터를 빠르게 제공해주는 것

: 조회에만 캐싱을 적용하였으므로, Look aside패턴을 사용

Look aside 패턴

캐시 조회 → MISS면 DB 조회→ 캐시에 put → 반환
(캐시 히트 : MISS 아니면 바로 데이터 반환! )

고려사항

어떤 데이터를 캐싱을 해야할까를 고민하고 알아본 결과
적용 기준 3가지를 적용하였습니다.

  1. 요청마다 동일한 데이터를 반환하는 조회에만 사용한다.
  2. 업데이트가 자주 발생하지 않는 데이터에 사용한다. 
  3. 자주 조회되는 데이터에 사용한다.

치카해 도메인들에 위 조건을 적용하면 ,, 사실 실 사용자도 없고 볼륨이 크지 않다 생각해
퀴즈 ,아이템 , 공지사항,FAQ(정적 컨텐츠), 알림 , 마이페이지 등등이 있었습니다.

위 대상 중 마이페이지 , 알림을 선정하여 캐싱을 적용하였습니다.

저는 서비스 레이어에 캐싱을 적용하였는데 ,
읽기: getSlots() → 캐시 히트 시 바로 반환
쓰기: updateSlotTime() → 캐시 비우고 DB만 변경 → 다음 getSlots()가 다시 DB 읽어 캐시에 반영

이런식으로 동작하기를 기대하였습니다.

적용 과정

로컬 터미널에 도커로 레디스를 pull 받아옴 -> 6379포트로 레디스를 띄움 -> 의존성 추가 -> config파일 생성 ->Jmeter를 사용한 서버 부하 테스트 및 응답 시간 차이 시각화

서버 부하 테스트 도구 : Jmeter

-> 처음에는 프로메테우스 + 그라파나를 사용한 데이터 수집 + 시각화를 하려했으나 ,
-> 서버에 부하를 주는 방식이 로컬에서는 어렵다고 판단하여 jmeter를 사용
-> 비교적 정확한 테스트를 위해 100명의 사용자가 한 번에 몰려 1회 조회를 무한 반복 (1분 동안 유지)
(ps. 또 대부분 어떻게 테스트 하는지가 궁금합니다.)

🖼️ 스크린샷

  • 캐싱 미적용 (다른 브랜치에서 실행)
스크린샷 2025-08-10 오전 2 03 03 - 캐싱 적용 스크린샷 2025-08-10 오전 2 03 15

평균: 281ms → 203ms (약 –28%)
중앙값: 271ms → 197ms (약 –27%)
95% 라인: 467ms → 355ms (약 –24%)
99% 라인: 580ms → 450ms (약 –22%)
최대: 914ms → 792ms (약 –13%)

대충 20% 정도 응답 시간 단축됨을 확인하였습니다.

🔗 관련 이슈

  1. @Cacheable 만 붙인다고 되는게 아니었습니다.
    missing type id '@Class' 오류가 발생하였습니다.
    찾아보니 , 스프링 캐시는 내부적으로 Object로 값을 다루기 때문에, 다시 읽을 때 원래 타입을 복원하려면 이 정보가 필요하다고 함 => 모든 타입에 @Class를 같이 JSON에 넣음으로써 해결

  2. 순환 참조의 발생(ㅠ.ㅠ)
    JPA 양방향 연관관계가 원인
    Jackson이 JSON 만들 때 필드를 타고 내려가며 직렬화:
    member → slots → member → slots → … 무한 반복
    @JsonIgnore로 순환을 끊음

  • 사실 순환참조가 발생만 하였지 정확히 왜 발생하고 뭔지 잘 몰라서 PR작성하는 김에 공부해보았습니다.
    Jackson은 객체를 JSON으로 만들 때 , 필드를 돌아가며 깊이 우선 탐색을 함

예를 들어 Member 객체를 직렬화하기 시작하면 { "id":1, "slots":[ … ] }
근데 이 안에서 한 객체를 직렬화할 때 그 객체가 들고 있는 필드 객체들도 같이 직렬화되면서
Member → slots[0] → member → slots[0] → member → … 이런 원리였다..

어노테이션을 사용해서 해결하는 방식도 있지만, 응답이나 캐시에 필요한 필드로만 구성된 DTO를 만들어 단방향으로 설계해도 된다고 합니다. 허허

(두서 없이 작성했지만 ,, 긴 글 읽어주셔서 감사드립니다 )

✅ 체크리스트

  • 로컬에서 빌드 및 테스트 완료
  • 코드 리뷰 반영 완료
  • 문서화 필요 여부 확인

@seokjun01 seokjun01 self-assigned this Aug 11, 2025

@jwnnoh jwnnoh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

고생한 흔적이 보입니당!! 저도 잘 아는건 아니지만..

  1. 도메인 별로 TTL을 다르게 설정하면 더 좋을 것 같아요
  2. 캐시 스탬피드 문제를 해결하기 위해 우선 간단하게
@Cacheable(value="notificationSlots", key="#memberId", sync=true)

를 끼얹을 수도 있겠죵..?!
3. 그리고 NotificationSlot 엔티티 자체를 캐싱하기보다는, DTO를 캐싱하는건 어떨..까요?!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants