Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

🛠️ [refactor] 강의 오픈시 수강생 정보 Redis에 캐싱하기 #62

Closed
10 tasks done
binary-ho opened this issue Aug 20, 2023 · 0 comments
Closed
10 tasks done
Labels
Refactor 리팩토링

Comments

@binary-ho
Copy link
Owner

binary-ho commented Aug 20, 2023

🛠️ 리팩토링이 필요한 부분

학생이 출석 시도시, 수강 신청 데이터가 많으면 데이터를 가져오는데에 엄청난 시간이 소요되는 것을 확인했다,
(1000건 동시요청시 10만건에선 아예 600건 이상 타임아웃)
그래서 인덱싱을 통해 해결을 시도하니 약 6.5초만에 전부 성공했다 -> #57

그러나, 6.5초도 여전히 유저 입장에선 여전히 느리다고 생각되어, 강의를 열 때 수강생 정보를 캐싱을 해보기로 했다.

계획

1. 캐싱 대상

  1. 열린 강의 정보 (hash 이용해 강의 id를 key로, 강의 정보들을 field와 value로 저장)
  2. 수강생이 출석 가능한 강의 목록 (key : 학생 id, value : 열린 강의 id 목록 set)

2. 개선된 출석 과정

  1. 수강생들은 자신이 승인된 강의 중, 출석 가능한 강의가 있는지 캐시 먼저 확인한다.
  2. 있는 경우 캐싱된 열린 강의 정보에서 정보를 가져온다.
  3. 없는 경우 DB에서 직접 데이터를 가져온다.
  4. 출석이 가능한 상태에서 새로 수강 승인되는 학생의 경우 승인 이후 바로 캐싱된다.

3. 수업 정보는 같은 트랜잭션, 학생 정보는 비동기 자식 트랜잭션 (+Event를 통한 구현)

설계 과정에서 여러가지 방법을 고민했는데, 결국 구현중 Event를 발행해서 캐싱을 따로 처리하기로 결정했다. 그 이유는 다음과 같다

  1. 강사가 수업을 열 때 기대한 행위와 응답은 "수업을 여는 행위" 자체에 대한 것이여한다.
    "열린 수업", "수강생"을 저장하는 작업은 수업을 여는 행위를 요청하는 강사 쪽에선 관심사가 아니다.
  2. 따라서, "수업을 여는 행위"와는 따로 처리하고, 수업이 열리면 빠르게 그에 대한 응답을 반환해 주고 싶다.
  3. 아예 별개의 트랜잭션을 이용해 캐싱 자체가 실패하더라도 영향을 받지 않았으면 좋겠다.
  4. 하지만, "수업을 여는 행위"의 트랜잭션이 실패한다면 캐싱은 진행되지 않았으면 좋겠다.

이 조건을 모두 만족시키는 간단한 방법이 Event를 사용하는 것이라고 생각되었다.

처음엔 Event를 사용하지 않고 단순 서비스로 구현했고, @Async@Transactional(propagation = NESTED로 해결할 수 있을 줄 알았다.

하지만, 테스트 해본 결과 예상과는 다르게 부모 트랜잭션이 실패해도 자식 트랜잭션이 실패하지 않았다.

이벤트를 사용한다면 TransactionalListner의 Phase 설정을 After Commit으로 두면서 편리하게 커밋시에만 저장되면서도 비동기적으로 작동하게 만들 수 있다.

종합하면 내 목표는 아래와 같이 이룰 수 있다.

  1. @Async를 통해 수업을 여는 행위에 대한 응답을 바로 돌려주고, 캐싱은 비동기적으로 처리
  2. Transaction의 Require New 옵션을 통해 캐싱을 위한 아예 다른 트랜잭션을 생성
  3. Phase 설정을 통해, "수업을 여는 행위"가 성공했을 때만 캐싱하도록 구현

4. 유의할 점

  1. Max Memory는 5MB로 설정한다 -> Memory를 너무 많이 사용하는 것을 막기 위함으로 비즈니스를 생각하면 5MB는 닿을 수 없을 만큼 크다. 학생 데이터를 보수적으로 set으로 저장하는 경우 약 200 초반의 바이트가 필요하다.
    1만명의 학생이 있다고 했을 때, 2MB가 최대이다. 그리고 이 1만명의 학생이 동시에 가입한다고 해도, 이메일 데이터가 100 초반대이므로, 2MB에 닿기 어렵다.
    결론적으로 5MB의 공간이 있다면, 1만명의 학생이 동시에 가입하면서, 즉시 수업 정보를 캐싱할 수 있다.
    현재 1000명 동시 출석이 목표인 시점에서 아주 충분한 값이다. (현재 EC2 스펙은 1GiB)
  2. eviction 정책은 volitle ttl : 앱에서 다양한 종류의 캐싱을 하지만, 가장 큰 용량을 차지하는 것은 이 수강생 정보이다. 문제는 수강생 정보는 사용되지 않은 데이터 일수록 오히려 사용 확률이 높다는 점이다 이는 기존의 캐싱 정책들과 반대이다
    왜냐하면 한번 출석한 학생은 오히려 또 출석할 일이 없고, 출석하지 않은 학생이 새로 출석할 가능성이 높기 떄문이다. 그래서 레디스에 이 상황에 100% 맞는 적절한 정책이 없다. (보통은 자주 쓰이는 데이터를 더 살려 놓는다)
    그나마 적절해 보이는 것이 volitle-ttl이다. (정책이 아예 없는 경우엔 새로운 저장이 안 된다.) 그래서 volitle-ttl을 선택했다.
    문제는 앱에서 캐싱하는 다양한 데이터 중에 캐싱된 데이터가 수강생 데이터가 제일 사라져도 문제가 없는 데이터란 것이고, 다른 데이터들은 사라지는 경우 문제가 된다.

    이제까지 사라져선 안 되는 데이터들은 제한 시간이 실제 비즈니스적으로도 존재하는 데이터들이여서 비즈니스적인 제한 시간과 redis exprire time을 똑같이 맞췄다. 이젠, 이 데이터들의 exprire time을 2배로 늘리고, 따로 유효 시간을 저장하는 방식을 고려해봤다.
    그러니까 위험을 대비해 저장 용량을 늘리게 되는 것이다.
    이 방법은 기존의 일반적인 캐싱과 같은 상황에선 비싼 메모리 자원을 더 소모하는 것임으로 문제가 되겠지만, 우리 서비스에선 이런 방식으로 추가 메모리를 사용해 저장하는 자료가 적음으로 고려할만해 보인다. 만약 용량이 그리 넉넉하지 않았더라면, 유효시간을 대폭 줄이는 방법으로 바꿨을 것이다. -> 또다른 이슈가 필요하다
  3. 강의를 OPEN 중에 새로운 학생이 승인될 수 있다 (출석 가능한 학생이 동적으로 생겨날 수 있음)
    -> 강사가 학생을 승인했을 때, 강의가 이미 열려있다면 레디스에 캐싱한다.
  4. redis transaction support : redis template에서 관련 설정을 true로 설정해야 /@transactional 을 붙인 메서드에서 RedisTemplate를 사용할 때, 우리가 평소에 DB를 사용할 때와 같이 동작한다. (하나의 트랜잭션으로 묶이고, 롤백시 롤백 등)

리팩토링 작업 브랜치

feature/student-caching

☑ Refactoring TODO

  • 레디스 설정 적용
  • Open된 강의 정보인 OpenLecture 구현
  • 출석 번호를 저장하던 AttendanceNumber Repository 제거
  • 수강생 정보를 저장하는 LectureStudentRepository 구현
  • 수업을 열 때, OpenLecture 정보와 수강생 정보를 저장하는 기능 구현
  • 출석 가능한 강의를 가져오는 과정에 캐시 데이터를 먼저 확인 하는 과정 추가
  • 학생이 출석을 시도할 때도 캐싱을 먼저 확인하는 과정 추가
  • 학생을 승인해줄 때, 열려있는 경우 캐싱하는 과정 추가
  • 전과정 테스트 추가
  • Event를 발행하여 처리하는 방식으로 변경
@binary-ho binary-ho added the Refactor 리팩토링 label Aug 20, 2023
binary-ho added a commit that referenced this issue Aug 20, 2023
binary-ho added a commit that referenced this issue Aug 22, 2023
binary-ho added a commit that referenced this issue Aug 23, 2023
* feat : 현재 열린 강의를 표현하는 DTO OpenLecture와 OpenLectureRepository 구현  (#62)

* feat : OpenLecture를 캐싱하는 OpenLectureRedisRepository를 구현하고 OpenLecture 정적 팩터리 메서드 구현 (#62)

* chore : OPEN_LECTURE_KEY_PREFIX 추가 (#62)

* refactor : AttendanceNumberRepository를 제거하고 AttendanceService에 OpenLectureRepository 적용  (#62)

* refactor : OpenLectureFieldKeys를 OpenLectureRedisRepository의 Inner class로 변경  (#62)

* test : FakeOpenLectureRepository를 구현해 AttendanceServiceTest에 적용 (#62)

* chore : OpenLectureRedisRepository 형변환 부분 변경 (#62)

* feat : 수강생을 저장하는 LectureStudentRepository 와 이미 저장된 수강생의 Redis Data Type에 따른 Read/Write 전략 구현 (#62)

* feat : RedisTemplate이 @transactional에 포함되도록 설정 (#62)

* refactor : Cache 목적의 save를 모두 Cache로 이름을 변경하고, 수강생이란 단어를 LectureStudent에서 Attendee로 이름 변경 (#62)

* feat : AttendeeCacheEvent를 이용해 Attendee를 비동기적으로 캐싱하는 AttendanceService 구현 (#62)

* refactor : OpenLectureService를 구현하여, AttendanceNumber가 필요할 때, Repository가 아닌 Service로 요청하도록 변경 (#62)

* feat : 학생의 수강신청을 승인할 때, 이미 열려있는 강의라면, 학생 정보를 캐싱하는 기능 구현 (#62)

* chore : 불필요한 LectureStudentRepository 삭제 (#62)

* feat : 강의를 열 때, 강의 정보와 학생 정보를 캐싱하는 기능 구현 (#62)

* chore : OpenLecture 생성자 변경 (#62)

* refactor : Lecture의 Approved Enrollment를 가져오는 메서드 쿼리 변경  (#62)

* feat : 학생이 열린 수업 정보를 가져오기 전에 캐싱된 데이터가 있는지 먼저 확인하고, 있는 경우 캐싱된 결과를 이용하는 기능 구현 (#62)

* test : CacheRepository들의 Fake 객체를 구현해 TestContainer에 적용 (#62)

* test : 기존 테스트에 OpenLecture와 Attendee의 캐싱을 위해 변경된 내용들을 적용 (#62)

* chore : Lecture, OpenLecture, AttendeeCacheEvent를 Lecture Domain 패키지로 이동 (#62)

* test : AttendeeCacheService와 OpenLectureService 단순 호출 테스트 작성 (#62)

* test : 캐싱을 저장하고 확인하는 캐싱 관련 기능 Test Code 작성 (#62)

* chore : OpenLectureService와 AttendanceService의 메서드 이름을 적절하게 변경 (#62)

* refactor : AttendeeCacheRedisRepository의 자료형 변경으로 인해 발생하는 예외 catch (#62)

* refactor : OpenLectureRedisCacheRepository에서 저장 key를 id를 이용해 만들도록 변경 (#62)
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Refactor 리팩토링
Projects
None yet
Development

No branches or pull requests

1 participant