MyRSS 제작기 - HJ-Rich/2022-MyRSS GitHub Wiki
Welcome to the 2022-MyRSS wiki!
@Table(indexes = @Index(name = "idx_subscribe_member_id_rss_id", columnList = "memberId, rssId", unique = true))
@Entity
public class Subscribe {
- 인덱스, 외래키는 성능에 큰 영향을 미치는 요소이므로 명시적으로 드러내는 것이 필요하다고 판단하여 진행
- 5차 요구사항에서 진행했던 인덱스 설정을 애플리케이션 레벨에 명시하도록 구현
- 5차 요구사항 중 인덱스 설정 관련 진행한 내용 wiki
- https://github.com/HJ-Rich/2022-MyRSS/pull/66
- 서브모듈에 보안이 필요한 설정값이 있고, 이 서브모듈은 프라이빗 리포지토리에서 관리
- 해당 보안 설정값 없이도 clone 후 즉시 기동 가능하도록 프로파일 리팩터링
- 이를 위해 Optional 설정 임포트, EmbeddedRedis, H2 를 적용.
- 다만 Github OAuth를 위한 Client_id, Client_Secret은 대체가 불가하여 로그인 기능은 Clone후 즉시 테스트 불가
- https://github.com/HJ-Rich/2022-MyRSS/pull/63
- 공통 피드
- 모든 사용자가 동일한 데이터를 사용함 - Shared Cache
- 1시간에 한 번 업데이트 됨
- CacheControl의 maxAge 설정을 통해 브라우저가 아예 요청을 보내지 않게 처리해버리자
- 단, FixedDelay로 fetching중인데, 다음 fetching까지 남은 시간을 maxAge시간에 주입시켜야 한다
- WAS 레벨에서도 eh캐싱 등을 적용해서 1시간에 한 번만 DB호출이 일어나게 할 수 있다.
- fetching이 수행될 때 Evict를 수행시키면 될 듯 하다
- 여기까지하면 브라우저단 캐싱, 애플리케이션단 캐싱은 완료다.
- Nginx 캐싱이나 Redis를 통한 캐싱은 우선순위가 다른 작업에 밀리는 듯 하다.
- 사용자별 구독, 북마크 피드
- 개인화된 캐시이므로 Private Cache
- 공통 피드에 적용했던 것과 유사한 컨셉인데 evict 처리를 추가로 고민해야 한다
- 구독 또는 북마크 정보가 추가 혹은 삭제 되어 변경이 일어날 경우에 대한 처리가 필요하다
- 브라우저단 캐싱의 경우 maxAge와 더불어 no-cache 설정을 사용해서 매번 재사용 전 변경 여부를 검증하도록 해야 한다
- 애플리케이션단 캐싱의 경우, 개인화된 데이터에 대한 변경이 일어났을 때 evict 처리를 해줘야 한다.
- 이러면.. 변경이 일어날 때마다 새로 읽어야하는데.. evict를 이벤트 처리해서 미리 캐시를 생성해둬야할까..?
- 블루 그린 배포 선택
- 이중화는 진행하지 않음
- Github Webhook Trigger -> Jenkins build, test, Jar 전송, shell 실행 -> shell 파일 내에서 블루 그린 배포 수행
- 현재 실행중인 포트가 A, B 중 어느 것인지 식별. 실행중이지 않은 포트를 신규 배포 포트로 설정
- 신규 배포 포트로 jar파일을 실행한 뒤 20초 대기 후 헬스 체크 수행.
- 헬스 체크 실패 시 프로세스 중단, 성공시 nginx에 include 될 파일을 수정후 nginx reload
- 초당 10번 요청을 전송하도록 구성해둔 채로 배포를 수행했을 때, 총 2건이 정상응답되지 않았음.
- reload가 이루어지는 찰나에 정상 응답이 수행되지 않는 것으로 예상.
- 주 1회 배포, 배포 마다 0.5초 다운타임으로 가정했을 때, 연 30초 미만 이므로 six nine으로 충분한 가용성으로 판단.
- MemberRepository에 사용되는 메서드는 로그인 시 조회 및 저장으로 이루어짐
- Github Provider Id로 조회해서 존재하면 로그인처리, 없으면 저장 후 로그인처리 로직
- Provider Id에 대한 인덱스 적용 전과 후 비교 진행
- 천만건 데이터 기준, 4.2초에서 0.08초로 단축 (약 50배 성능 차이)
- 인덱스 유무에 따른 천만건 삽입 시 소요시간은 195초에서 211초로 증가.
- 인덱스 설정으로 인한 저장 성능 저하는 사실상 유의미 하지 않다고 판단
- 천만건 데이터가 존재하는 상황에서 인덱스를 최초 설정시 38초 소요.
- 인덱스 설정은 애플리케이션 규모가 커지기 전에 선제적으로 적용하는 것이 필요하다는 학습
- Infra2 인스턴스에 MySQL 추가 설치 및 Replication 구성
- WAS에 Multi DataSource 설정은 아직 적용하지 않음
- log파일들이 존재하는 서버에는 Promtail을 설치. Agent로서 Loki 서버로 log 전송
- 로그를 적재할 서버 역할을 할 Loki 설치.
- Grafana에서 DataSource로 Loki 추가. job 별로, 파일명 별로 로그를 필터링하여 쿼리, 카운팅 등 할 수 있음
- Grafana Alert 를 사용하여 지정 시간 내 특정 로그 카운팅이 설정 숫자 이상일 경우 Slack 알림 오도록 구성
-
드디어 1.0.0 릴리즈 완료.
-
구독 추가 기능 구현 관련
- 문제 해결의 시작은
구독 관리 화면
과구독 화면
의 분리- 기존: 구독 화면에서 바로 구독을 추가, 모든 새로은 포스팅들을 응답 받아 재렌더링
- 변경: 구독 화면에서는 구독 관리 화면으로 이동하는 버튼만 제공. 처리 성공시 추가된 채널의 기본 정보만 응답 받아 재랜더링
- 구독 추가 시 1번의 AJAX, 라이브러리를 통한 1번의 문자열 파싱, n번의 INSERT 문이 실행된 뒤 응답이 나간다.
- AJAX 및 파싱이 정상 완료됐다면 정상적인 RSS 주소라고 할 수 있다.
- 여기까지만 수행되었다면 정상 처리됐다고 고객에겐 응답을 즉시 나가는 것도 괜찮을 듯 하다.
- n번의 INSERT를 bulk insert로 개선하거나, 비동기적으로 처리하는 식으로 개선할 수 있을 듯 하다.
- RSS 변환 기능과 첫 테스트 코드
- 그동안 비즈니스 복잡도가 현저하게 낮고 MVP출시가 시급해서 테스트 코드 없이 진행
- 그러나 사용자 편의성을 위해 티스토리, velog, 브런치, medium의 경우 특정 블로그의 어느 URL을 주더라도 자동 변환처리를 해주고 싶었음
- 티스토리, velog, medium은 패턴이 일정해서 정규식을 통해 블로그 주인 아이디만 추출한 뒤 패턴에 주입하는 식으로 RSS 주소를 얻을 수 있음
- 브런치는 RSS 주소 패턴은 있으나, PathVariable로 사용자 아이디를 사용하지 않고 랜덤 문자열을 사용함
- 어쩔 수 없이 브런치 주인 ID를 정규식으로 추출한뒤, 해당 브런치로 AJAX를 보낸 뒤, JSOUP 라이브러리로 RSS 주소가 담긴 요소를 추출해내야만 했다.
- 로직이 복잡하기도 하고 다양한 테스트 케이스에 대해 정상 대응됨이 확인되어야 했기에 TDD로 진행
- 구현하기엔 공수가 꽤 들었지만, 사용성 개선 측면에선 상당한 개선이라 뿌듯함
- 사용자 피드백 수집 시작.
- 어느정도 누적되면 취합해서 리스팅한 뒤 우선순위를 매겨 ISSUE에 등록한 뒤 처리해봐야겠다.
여기까지 구현되면 MVP 완료.
피드를 공유하며 글쓰기, 댓글 기능도 필요하지만 가장 핵심적인 기능에 대한 구현은 완료이다.
주변에 공개해서 피드백을 받고 앞으로 구현 방향에 대해 다시 영점을 잡아보자.
-
RSS 신규 등록 시, DB에 없었던 RSS면 AJAX, FEED저장 작업이 필요함
- 이때, AJAX도 그렇고, 피드 파싱, 최소 5개 이상 DB 인서트도 해야하니, 응답시간이 걱정됨
-
DB에 있었던 RSS면 findBy만 해서 바로 끝날 수 있음
-
사용자는 Subscribe 화면에서 해당 작업을 진행했기에, 추가를 했을 때, 추가한 RSS의 피드들이 현재 보고 있는 화면에 즉시 반영되는 것을 선호할 것으로 예상됨
- 기존 저장되어있던 거는 findBy해서 있었다면 최근 피드들 더 긁어와서 응답해주면 프론트에서 시간순으로 정렬해버리면 끝임
- 그러나 없었다면… 시간이 너무 오래걸림.
- 이 지점에 대한 고민이었음…
-
사용자를 아예 블락시켜버리고, AJAX, 파싱, 인서트, 응답까지 한번에 해주면 확실하게 프론트 재랜더링이 가능함
- 다만 당연하게도 실패에 대한 문제가 심각하고.. 응답시간도 문제다
-
블락시키지 않는다면, 응답은 빨리 줄 수 있겠지만… 진행하겠다고..
- 처리 결과를 어떻게 줘야하나…
- 프론트에서 그냥 딜레이주고 다시 AJAX를 한다??
- 성능적으로도 그렇고.. 신뢰할 수 없는 시스템이다… 어떻게 해야할까
어느쪽 길을 가야할지 고민이어서 네오께 이 부분을 여쭤봄
- 양쪽 다 연결된 부분이 있다
- 블락시키는 방법으로 구현하더라도, 그 과정에서 고려해야할 지점이 매우 많다
- 그 지점에서의 고민들이 넌블락으로 옮겨가더라도 다시 재사용될 것이다
- 가령 병렬 처리 후 결과 취합이나 배치 인서트 등이 그것이다.
- 한 번 구현해보고 시간을 측정하고, 사용자로서 직접 그 시간을 경험해보라
- 어느정도 시간을 목표로 하는지, 현재 시간이 얼마나 걸리는지, 직접 체감했을 때 그 시간이 긴지 짧은지 이대로 괜찮은지 괜찮지 않은지 느껴봐야 한다
- 한쪽으로 우선 구현해본 뒤 다시 이야기해보자.
- 역시 최고시다 네오….
- 시원해졌다…!!!!
- 일단 블락으로 구현하면서 학습하고, 그 다음 네오께 공유드리고, 넌블락이 가능할지 고려해보자
- 최초 서비스 구상할 때, 내가 구독한 RSS에 대해 매주 1회 메일링 서비스를 제공하고 싶었다.
- 가령, 우형, 네이버를 구독했다면, 우형과 네이버의 지난 1주간 작성된 포스팅을 요약해서 메일로 전송해주는 것이다.
- 그러면 서비스에 들어오지 않더라도 메일로 간편하게 매주 1회 요약해서 받아보고, 필요한것만 가서 보면 되지 않을까?
- 추가적으로 메일에 이어 Slack Webhook URL을 받아서 알림을 보내주면 어떨까? 좀 더 빨리 받아보고 싶은 사람도 있지 않을까?
- 스케일 아웃 가능한 알림 서비스는 어떻게 설계해야할지 학습하고 고민해본 결과를 그림으로 그려봤다.
- 검색 페이지에 대한 임시 페이지 처리
- 페이지 이동, 로그인 시 스피너 처리
- 로그인/비로그인에 대한 버튼 분기처리
- 북마크 페이지 및 북마크 추가 구현
- 북마크 상태관리 및 북마크 처리 성공 시 아이콘 재랜더링 구현
- 프로필 페이지 구현. 아이디, 닉네임, 프로필 이미지, 로그아웃 버튼 나타남.
- 미션 및 기타 일정으로 인해 MyRSS에 공수를 할당할 수 있는 날이 매우 적어짐
- 특정 기능이 구현된 것에 대해서만 기록해야할 듯
- 부하 테스트 및 설정 최적화를 위한 학습
- 쓰레드 풀 관련 학습 및 포스팅
- JMeter 관련 학습
-
자바 성능 튜닝 이야기
,자바 개발자와 시스템 운영자를 위한 트러블 슈팅 이야기
발췌독
- 프로젝트 코드 작성은 계속 못하넹.. ;ㅅ;
- 피드 디자인 개선 및 목업 네브바 구현
- Web Share API를 이용한 공유하기 구현
큰 틀에서 구현 완료
- loading, hasNext, pageNumber, feeds 등의 상태관리에 대해
- axios로 가져온 피드들은 useState로 의도대로 관리되었음
- 그러나 loading, hasNext, pageNumber 들에 대한 useState가 적용되지 않는 이슈 발생
- 적용되지 않는 변수들에 대해 useState없이 let 선언한 순수 자바스크립트 flag값으로 사용하는 것으로 일단 해결
- 백엔드 API 관련 변경
- 피드 조회 시, 다음 페이지 존재 여부, 다음 페이지 번호를 함께 응답하도록 개선
- 피드 응답값에 Description 추가
- 피드 컴포넌트 디자인 개선
- 좋아요, 공유하기 기능은 미구현 상태
- 제목, 날짜, 요약, 클릭시 링크이동 구현
- icon 제공, 밑줄 제거, 색감 개선, Description 길이 줄이기 먼저 진행 예정
- favicon, title 적용
- IAM 사용자 생성 및 On-Premise 방식으로 AWS CloudWatch 를 구성해봄
- 대시보드로 CPU 사용률, 네트워크I/O, 로그 스트림을 시각화함
- 다만 프리 티어에서 제공하는 월간 메트릭 수가 적어 과금 이슈 발생
- CloudWatch는 구성 방법 및 제공하는 기능이 무엇인지 파악하는 정도에서 마무리
- 오픈 소스 기반의 Grafana를 구축하여 사용하기로
- 로그백을 이용해 로컬, 개발, 운영 환경별 로그 출력을 설정
- 로컬은 콘솔, 개발과 운영은 비동기 방식으로 로그 레벨별 파일로 분리해서 출력하게 했다.
- 스프링부트에 기본적으로 제공되는 로그백 출력 패턴을 이용해서 평소에 자주 보던 로그 패턴과 동일하게 유지했다.
- 로컬과 개발은 DEBUG 이상, 운영은 INFO 레벨 이상을 로깅하도록 했다.
- RestDocs를 이용한 API 문서 자동화
- build.gradle 파일에 설정 추가
- DocumentationTest 작성
- snippet을 조합하는 adoc 작성
- adoc 문서들을 통합해주는 adoc 작성
- index.html 파일 생성 확인
- 피드를 보여주는 View에서 카드 형태로 보여주되, 제목과 더불어 내용의 일부를 함께 보여주는 것도 좋을 것 같았다.
- 이 용도에 맞는 엘리먼트가 있다. RSS 규격상 Description이다.
- 그러나 atom, rss 여부에 따라, RSS를 제공하는 사이트에 따라 상황이 다양하다.
- 다행히 ROME 라이브러리가 정말 많은 일을 해주지만.. Description이 없는 경우도 있다.
- 임시적으로 Description이 없지만 Contents를 제공해주는 경우, Contents 중 앞 부분 중 일부를 잘라서 담기로 했다.
- Description, Contents 모두 없을 경우 아쉽지만 빈 문자열을 일단 담아주기로 했다.
- 티스토리 블로그, Naver D2, 우아한형제들 기술블로그 이렇게 세 RSS를 등록함.
- 피드 컨트롤러 구현. PageRequest 를 적용하여 기본 페이징 처리 구현.
- RSS에 추천 여부를 설정. 추천 true인 RSS는 디렉터's 초이스. 비로그인 상태의 기본 피드에 활용됨.
- 기본 피드 View 작업 시작
- RSS 기능 관련
- RSS가 자동으로 변경감지하여 알림을 전송해주는 매직인 줄 알았지만.. 현재까지 확인한 바로는 거는 아닌 듯 하다
- 다행히도 RSS URL 만 전달하면 파싱해주는 훌륭한 라이브러리가 존재. Rome
- RSS 공식 문서를 확인하여 element들 중 필수값이 무엇이 있는지, 사용할만한 값이 무엇이 있는지 확인
- atom 방식과 rss 방식이 있음을 확인, 감사하게도 Rome 라이브러리가 모두 지원
- RSS와 Feed 관련
- RSS 주소와 RSS 주소로 fetch하여 parsing한 Feed 두 가지를 도메인 객체로 정의
- 1시간 간격으로 등록된 RSS 주소를 모두 가져와서 새로운 피드를 확인하여 DB에 저장하는 기능까지 구현
- 다만, 변경감지 처리는 없이, link가 중복되지 않는 Feed들을 저장하도록 구현
- Feed 탐지 관련 개선 가능성
- Feed 를 DB에 저장할 때, fetch한 item하나를 통으로 해싱한 값을 같이 저장한다면?
- 추후 item이 업데이트 되었는지도 해싱하여 검증한 뒤, 변경되었다면 업데이트 처리를 해줄 수 있을 듯 하다.
- 다만 현재 MyRSS 서비스에서 구상하는 바는 제목과 링크 정도만 제공할 예정이기에..
- 제목의 변경 여부 정도만 직접 검증하는 게 오히려 나을 듯 하다.
- 해싱을 이런 식으로 활용할 수도 있겠구나 하는 새로운 컨셉을 얻었다.
- 서버에서 Set-Cookie 응답을 했음에도 프론트에서 Cookie가 할당되지 않는 이슈 발견
- Set-Cookie가 되지 않으니 쿠키값을 바디로 응답해서 프론트에서 직접 쿠키에 주입시키는 방법에 대해 시도해봄
- 그러나 Redis에 저장되는 SessionID와 Set-Coookie에 응답되는 값이 다름을 확인
- 디버깅을 통해 SessionID가 응답시점엔 base64로 인코딩되어 Set-Cookie로 응답되고 있음을 확인
- 따라서 추후 프론트에서 전달한 쿠키값을 base64로 디코딩해서 세션 존재 여부를 검증하는 방법을 검토했음
- 근본 원인을 확인함
- 개발, 운영에선 프론트와 백엔드가 Same-Origin에 있지만, 로컬에선 3000과, 8080포트로 분리되어 있어서 Same-Origin이 아님
- 그로인해, CORS설정 뿐만 아니라 추가적으로 credential에 대한 옵션을 더해줘야 함을 확인
- 프론트에서 요청을 전송 시에도, 백엔드에서 CORS설정 시에도 credential에 대한 설정을 'include'로 추가하여 해결
- Spring Session을 이용한 Github 로그인 구현 시도
- 인프라1 인스턴스에 Redis 설치
- Spring Session 라이브러리를 이용해, HttpServletRequest.getSession 시, Redis에 30분 TTL로 세션 저장 확인
- React Router Dom 을 이용한 Github OAuth 구성 시작
- 프론트 로컬, 개발, 운영 환경별 프론트 환경변수 구성
- npm start 명령어 시점과, npm run build 시점에 사용되는 .env 파일의 우선순위가 다름을 확인
- CRA 공식문서 중 일부
- .env.development 를 npm start에서 우선순위로 하여 로컬 개발시에 사용
- .env.production 을 npm run build 하여 운영 배포시에 사용
- 개발 배포시에는 npm run build 시점에 직접 전달하여 .env.production 에 정의된 값을 오버라이드 처리
- Jenkins를 이용한 프론트 배포를 위해 NodeJS 플러그인 설치 및 배포 구성
- 오라클 클라우드 프리티어 계정으로 인스턴스 4개 생성 및 구성 설계
- 인스턴스 1 운영 - BE, FE 운영 배포
- 인스턴스 2 개발 - BE, FE 개발 배포
- 인스턴스 3 인프라1 - Nginx, MySQL
- 인스턴스 4 인프라2 - Jenkins, SonarQube 등
- myrss.ga 도메인 확보
- Nginx 리버스 프록시 구성 및 SSL 설정 완료
- MySQL 구성 완료
- Jenkins 구성 완료
- 메인 프로젝트 및 서브모듈 프로젝트 리포지토리 생성
- 메인 프로젝트 리포지토리에 backend, frontend 기본 프로젝트 생성
- Slack 워크스페이스 생성
- Slack, Notion 연동
- Slack, Github 리포지토리 연동
- 전체 조회 - 기술 블로그들을 RSS로 모아서 보여주자
- 구독 조회 - 내가 구독한 RSS만 모아서 보여주자
- RSS 추가 - 전체 조회에서 나오지 않는 블로그들도 RSS로 추가할 수 있게 해주자
- 메일링 - 내가 구독한 RSS들을 일주일 단위로 메일 보애줄까?
- Slack 서비스 - 내가 구독한 RSS가 신규로 올라오면 등록한 Slack 채널로 보내줄까?
- 피드 쓰기 - 공유하고 싶은 RSS 피드를 공유하면서 본인의 생각을 추가해서 피드로 작성하기
- 소셜 - 팔로우, DM