Skip to content

Commit

Permalink
docs : 인덱싱 캐싱 과정을 더욱 자세히 설명하고 PR 링크 첨부 (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
binary-ho authored Nov 17, 2023
1 parent 44a08dc commit 2a39544
Showing 1 changed file with 34 additions and 8 deletions.
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

<br>

### 1.3 유저와 소통하기 위해 노력합니다.
### 1.3 유저의 입장에서 생각하기 위해 노력합니다.

<br>

Expand All @@ -62,16 +62,42 @@


## 3.1 백엔드
백엔드 애플리케이션은 유연한 프로그램을 위해, java 11과 Spring Boot 프레임워크를 이용해 만들었습니다. <br>
백엔드 애플리케이션은 유연한 프로그램을 위해, **java 11과 Spring Boot 프레임워크를** 활용해 만들었습니다. <br>

- **트래픽이 몰리는 출석 과정 전반을 인덱싱과 캐싱을 통해 개선했습니다.** <br> **10만 건 데이터 1,000건 동시 조회 : 30초 타임아웃 → 6.4872초 → 0.1434초** <br> <br> ▪️ 문제 상황 : 수강 신청 데이터 10만건, 1000명의 학생이 동시에 자신이 출석 가능한 수업 조회시, 650건 이상의 요청이 30초 타임아웃으로 실패 <br>
▪️ 인덱싱 적용 : 6.487초로 응답 시간 개선. <br>
카디널리티가 높은 컬럼에 인덱싱을 적용하여 6.4872초 만에 모든 요청이 성공. 그러나 6.4872초도 사용자 입장에선 느린 응답일 것으로 생각되어, 다른 인덱싱 방법을 고려. 커버링 인덱스나 복합 인덱스를 고려했으나, 인덱싱이 차지하는 용량에 비해 큰 응답 시간 개선이 이루어지진 않았음. 결국 DB I/O를 줄이기 위해 캐싱을 도입하게 됨. <br> <br>
▪️ 캐싱 적용 : 0.1434초로 응답 시간 개선. <br>
수강생들의 조회 요청이 몰리는 상황 직전에 수강생들의 정보를 캐싱하여 0.1434초로 응답 시간을 개선. 강의 데이터는 Hash 자료형으로 저장하고, 수강생 정보는 하나의 Redis Collection에 수강생 정보를 모두 저장하는 경우, 삭제시 Redis 스레드가 오랜 시간 점거될 수도 있으므로, 개별 String 형태로 저장. <br> 수강생 캐싱은 오직 수업을 여는 것이 성공했을 때만 캐싱되어야 하며, 강사는 캐싱 때문에 응답을 늦게 받아선 안 된다. 그리고 캐싱 실패로 인해 강의를 여는 행위가 실패할 필요는 없다. 따라서, 캐싱은 이벤트를 발행해 개별 트랜잭션에서 비동기적으로 수행하게 만들었다.
- 학생들의 출석 체크 과정에서 수강 신청 정보를 가져오는 과정의 소요 시간을 인덱싱을 통해 개선했습니다. 수강 신청 정보 테이블의 외래키인 학생 id에 인덱스를 적용했습니다. 이후, 자바 동시성 컬렉션을 이용해 10만 건의 데이터 중 1000건을 동시에 조회하는 테스트를 진행했더니, 기존엔 600건 이상 실패하던 작업을 인덱스 적용 이후 6.4872초 만에 처리할 수 있게 되었습니다. <br> <br>
### 1. **트래픽이 몰리는 출석 과정 전반을 인덱싱과 캐싱을 통해 개선했습니다.**
<br> **10만 건 데이터 1,000건 동시 조회 : 30초 타임아웃 → 6.4872초 → 0.1434초** <br> <br> ▪️ **문제 상황** : 직접 강의를 진행하며 서비스를 통해 출석을 받아본 경험에 따르면, 학생들은 강사가 출석이 가능함을 알리는 순간 거의 동시에 출석 화면으로 이동했습니다.
저희는 서비스를 더 많은 동아리와 공동체에 무료로 나누는 것이 목표였습니다. 거의 동시에 대부분의 인원이 출석을 시도하는 모습을 보며, 더 많은 공동체가 저희 서비스를 활용하게 된다면, 트래픽이 몰리는 문제가 발생할 것으로 생각되었습니다. <br> <br>
이에 자바 동시성 컬렉션을 사용해 RDS에 직접 쿼리하여 동시 출석 인원이 많은 상황을 테스트해 보았습니다.
수강 신청 데이터가 10만 건일 때, 1,000명의 학생이 현재 출석할 수 있는 수업을 동시에 조회하는 경우, 650건 이상의 요청이 30초 타임아웃으로 실패하는 문제가 발생했습니다 <br>


▪️ **인덱싱 적용 : 6.487초로 응답 시간 개선.** <br>
응답 지연이 가장 오래 발생하는 부분은 DB에서 데이터를 가져올 때라고 판단하여, 수강 신청 데이터에서 카디널리티가 높은 “학생 id” 컬럼에 단일 인덱싱을 적용하고, JSQL을 통해 JPA로 인한 불필요한 Join을 제거. 이후, 6.4872초 만에 모든 요청이 성공. 처음 인덱싱에 대한 지식이 부족해 복합인덱싱을 고려했으나, 학생의 “수강신청 승인 여부”는 대부분 동일한 값을 가졌기 때문에, 단일 인덱싱의 용량에 비해 1.5배 용량이 늘어나는 데에 비해, 시간 개선은 거의 없었기 때문에 선택지에서 제외.

개선된 6.4872초도 사용자 입장에선 느린 응답일 것으로 생각되어, 커버링 인덱스를 고려했으나, 가져와야 할 데이터들이 너무 많았기 때문에 인덱스 용량에 비해 큰 응답 시간 개선이 이루어지진 않을 것으로 판단. 결국 DB I/O를 줄이기 위해 캐싱을 도입 <br>
- 이슈 : [인덱싱을 통해 출석 과정에서 학생이 강의 정보를 가져오는 시간 단축](https://github.com/binary-ho/imhere-server/issues/57)
- Pull Request : [인덱싱을 통해 출석 과정에서 학생이 강의 정보를 가져오는 시간 단축](https://github.com/binary-ho/imhere-server/pull/58)

<br> <br>

▪️ **캐싱 적용 : 0.1434초로 응답 시간 개선.** <br>

**수강생들의 조회 요청이 몰리는 상황 직전에 수강생들의 정보를 미리 캐싱하여 (prefetching) 0.1434초로 응답 시간을 개선. 지연 감소율 97.79%** <br>
결국 학생들이 출석을 시도할 때는 강사가 수업을 OPEN한 이후이기 때문에, 강사가 강의를 OPEN할 때, 미리 수강생 정보와 강의 정보를 캐싱 <br> <br>

▪️ **수업 정보 캐싱은 수업 OPEN과 같은 트랜잭션, 학생 정보 캐싱은 Event를 활용해 개별 트랜잭션에서 비동기 호출** <br>
수업 정보 캐싱과 수강생 정보 캐싱은 복잡한 요구조건이 있었습니다.
1. 수업 정보 캐싱이 실패하는 경우엔, 수업도 열리지 않길 바랐지만, 수강생 정보의 캐싱 실패는 수업 OPEN에 영향을 미쳐선 안 된다.
2. 강사가 수업 열기 버튼을 누를 때 기대하는 것은 단순히 수업이 OPEN 되는 것이기 때문에, 수강생의 캐싱으로 인해 강사가 응답을 늦게 받는다는 건 불편한 경험이라고 생각하여, 수업 열기와 수강생 정보 캐싱 은 분리된 작업이길 바란다.
<br>

이러한 요구 조건을 모두 만족하기 위해 **수업 정보 캐싱은 수업 열기 API와 같은 트랜잭션에서 진행하고, 학생 정보 캐싱은 별도 트랜잭션에서 After Commit Phase에 비동기적으로 수행했습니다.** <br> 수강생 정보는 Redis Collection 크기가 커지는 것이 우려되어 개별 String에 저장했습니다. (Collection의 크기가 크면 삭제 시 스레드가 오랜 시간 점거됨) <br>
- Pull Request : [강의 오픈시 수강생 정보 Redis에 캐싱하기](https://github.com/binary-ho/imhere-server/issues/62)

<br>

- 유연한 설계와, 회귀 방지를 위한 테스트 작성을 위해 의존성 역전을 적극적으로 활용했습니다. RedisTemplate과 JavaMailSender를 사용하거나, 토큰 생성을 위해 SECRET을 사용하는 객체들을 인터페이스로 추상화하였습니다. 구현체는 빈으로 관리하고, 호출하는 쪽에서는 추상화된 인터페이스를 의존하도록 구현하였습니다. 테스트 시엔 인터페이스를 구현한 Fake 객체를 활용했습니다.
- PR opened, synchronize, closed시 자동화된 빌드 테스트 도입
- 서버가 접속 불가 상태가 되는 문제를 해결하고, 알림을 받기 위해 모니터링과 알림을 도입했습니다. 다운시 디스코드로 알람이 옵니다.
- 학생과 강사의 사용 가능한 기능을 분리하기 위해 스프링 시큐리티를 통해 인증 기능을 구현했습니다. 서버를 stateless 하게 유지하면서도 회원이 편리하게 이용할 수 있도록 토큰을 활용해 인가 기능을 구현했습니다.
- 문제 상황 추적을 위해 로깅을 진행하였습니다. 처음엔 일반 출력문을 사용하다가, 파일로 보관하기 위해 로깅 프레임워크를 도입했습니다. 스프링 부트가 기본으로 제공하는 logback 대신 log4j2를 사용하였습니다. 로그는 날짜를 기준으로 파일에 저장되며, 지정 용량을 초과하는 경우 동일한 날짜라도 별도의 파일에 보관됩니다.
Expand Down

0 comments on commit 2a39544

Please sign in to comment.