백엔드 개발자로서 좋은 포트폴리오기도 하지만 제가 사랑하는 저의 축구 동아리가 더 성장하고 원활하게 진행되었으면 하는 마음이 더 크게 담겨 있는 프로젝트입니다, 이 프로젝트 덕분에 부원을 두배로 늘릴 수 있었고 경기들도 더 원활하게 진행되고 있습니다
- 기존에 모든 회원 경고, 회비 납부, 출석 여부등을 엑셀로 수기 기록하고 있었음
- 이로 인하여 약 60명 정도 이상의 인원을 동아리가 감당하기 힘들어짐
- 매 기수마다 200개가 넘는 지원서가 들어오지만 매번 죄송하게 20명 이상 뽑기가 어려웠음
- 인스타그램 디엠으로 꼭 함께하고 싶다는 많은 연락들을 본 뒤 자동화 함으로써 동아리 규모를 키우기로 결정
- 기존 부원들도 출석, 회비 납부, 패널티에 관한 인식이 줄어들고 전체적인 탄력이 늘어지게 됨
- 이로 인하여 약 60명 정도 이상의 인원을 동아리가 감당하기 힘들어짐
- 모든 회원 경고, 회비 납부, 출석 여부등을 홈페이지에서 확인하고 관리할 수 있게 됨
- 회원 경고는 경기가 끝나는 시간에 회원의 출석, 납부 여부에 따라 일괄적으로 경고를 부여하도록 배치 처리
- 회비 납부는 부원이 회비를 납부 한 뒤 확인 요청을 하면 관리자가 입금내역을 확인하여 회비 납부 확인 처리함
- 출석 여부는 Naver map api를 연동하여 매 경기 장소의 위,경도를 저장하고 출석하는 사용자의 현재 위경도와 비교하여 400m내에서만 출석이 가능하도록 개발
- 위 내용처럼 모두 자동화 된 후의 성과
- 홈페이지 베타 테스트 기수 (3기) 부터 인원을 100명까지 증원하였고 문제 없이 운영되어짐
- 4기부터 100% 도입하여 동아리 전체 부원을 약 150명까지 증원할 예정
- 운영진들의 역할에 대한 부담이 적어졌고 자동화된 패널티 처리로 긴장하게 되어 동아리의 전체적인 탄력이 살아남
- 회원 관리
- 회원가입
- 로그인
- JWT와 Spring Security의 필터를 이용한 로그인 방식
- 프로젝트 특성상 체류시간이 길지 않을것으로 예상되어 재발급 토큰은 사용하지 않음
- 회원 경기 출석 현황 (어드민)
- 관리자가 차트를 이용하여 전체 경기 참여 비율과 출석 현황 비율을 확인함
- 백엔드에서 데이터를 보내주면 프론트에서 HighCharts 라이브러리를 이용해 파싱
- 관리자가 차트를 이용하여 전체 경기 참여 비율과 출석 현황 비율을 확인함
- 경고 부여 시스템
- 매주 일요일 경기가 있는지 확인하고 있다면 경기 종료시간에 실행되는 일회용 동적 스케줄러를 생성
- 생성된 일회용 스케줄러는 해당 경기의 참여 멤버들을 조회 및 출석 & 납부 현황에 맞게 경고 부여
- 매주 일요일 경기가 있는지 확인하고 있다면 경기 종료시간에 실행되는 일회용 동적 스케줄러를 생성
- 경기 관리
- 경기 생성, 수정, 삭제, 조회 (어드민)
- 경기 생성 시 주소를 기반으로 naver map api에서 위,경도를 조회하여 같이 저장함
- 출석 확인 (사용자)
- 현재 요청자의 위경도와 경기장의 위경도를 비교하여 400m 내에서 출석되도록 개발
- 회비 납부 요청 (사용자)
- 회비 납부 후 관리자에게 회비 납부 확인 요청을 함, 회비 납부 확인 대기 상태가 됨
- 회비 납부 확인 (어드민)
- 대기중인 회비 납부 확인 요청들을 승인시켜줌, 회비 납부 완료 상태가 됨
- 참여 인원 추가, 삭제, 조회 (어드민)
- 경기에 참여하는 인원들을 추가, 삭제, 조회 하는 기능
- N:N 관계가 되는 테이블을 N:1 1:N의 사이 역할을 해주는 Member_Event 테이블로 풀어나감
- 경기 생성, 수정, 삭제, 조회 (어드민)
![Screenshot 2024-03-07 at 10 08 44 PM](https://private-user-images.githubusercontent.com/54535550/310887948-47e099cf-8b9e-4cd5-aabd-7ddb1cd89f7e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkwOTEzODAsIm5iZiI6MTczOTA5MTA4MCwicGF0aCI6Ii81NDUzNTU1MC8zMTA4ODc5NDgtNDdlMDk5Y2YtOGI5ZS00Y2Q1LWFhYmQtN2RkYjFjZDg5ZjdlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA5VDA4NTEyMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTAwYWI4N2I0NDgxMWY2MDYwYjhmZWI5YzAyZGNhNjlkNmNkMGRhNTNmMmUzZjZlYWUwMzEwMDUyODkyNDg3NTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.4HLYEBLtZIOq6D4YVI-RI12UjeFdmkSAZruuOse7Ebk)
- Vultr 클라우드 사용
- 기존에 AWS로 배포하였지만 최소 1년 이상 서비스가 지속되어야 한다는 이유 때문에 비용이 저렴한 vultr로 이전
- 스프링 서버 1vCpu, Ram 1GB 두개
- DB 서버 1vCpu, Ram 1GB 한개
- Pinpoint 서버 2vCpu, Ram 8GB 한개
- 프로메테우스 & 그라파나 서버 1vCpu, Ram 1GB 한개
- 로드밸런싱 서버 1vCpu, Ram 1GB 한개
- !! 로드밸런서와 DB 서버는 vultr에서 제공하는 sass 형태가 아닌 직접 우분투 인스턴스에 올려서 구성
- AWS를 사용하게 되면 편한 sass를 많이 이용할것으로 예상되어 이번 프로젝트는 직접 모두 구성하고 싶었습니다
- 비용도 직접 설정하고 구성하는것이 훨씬 저렴했습니다
- Vultr Firewall 사용
- 방화벽 개발 경험을 통해 각 인스턴스들은 예상된 출발지 IP로부터 최소 필요한 포트 연결만 허용하도록 구성하였습니다
- 프론트 PHP 서버는 VPC 안에 구성하지 않았습니다, 백엔드 도메인을 통해 요청합니다.
- Nginx를 이용해서 직접 로드밸런서 인스턴스를 구성하여 엔드포인트로 사용
- Sectigo DV급 SSL 인증서를 발급받아서 도메인에 적용했습니다.
- 세션 의존적인 설계가 아니여서 베이직한 Round-Robin 방식으로 부하분산 처리했습니다.
- 추후 진행된 성능 테스트에서 로드밸런싱 후 스케일 아웃된 만큼의 TPS 개선이 있었습니다.
- 백엔드 서버는 최소 사양 인스턴스 두개로 구성
- 성능 테스트 시 4개까지 사용했지만 불필요하다고 생각되어 2개로 줄이게 되었습니다
- 최종 도커로 배포되는 방식이여서 스케일 아웃시 도커만 pull 받아 실행하면 되도록 세팅하였습니다.
- DB 서버는 최소 사양 인스턴스 한개로 구성
- Pinpoint로 트랜잭션을 분석할때 병목지점이 DB가 아닌 웹서버인것으로 확인하여 DB는 한개만 사용했습니다
- Replication DB 구성 등 여러 개선 방법들을 고려해봤지만 비용투자가 더 이상 필요 없다고 판단되었습니다
- Pinpoint 서버는 2vCpu, Ram 8GB 한개로 구성
- 최대한 낮은 사양으로 타협하고 싶었지만 일정 사양 이하의 인스턴스에서는 여러 하드웨어 자원적 문제가 발생했습니다
- 구성하던 도중 pinpoint-docker의 간단한 스크립트 문법 오류를 발견하여 PR을 보냈고 다음날에 merge 되었습니다
- 자세한 트랜잭션 추적 기능으로 추후 성능 개선의 방향성을 잡는것에 많은 도움이 되었습니다.
- 프로메테우스 & 그라파나 서버는 최소 사양 인스턴스 한개로 구성
- Spring Boot 메트릭 모니터링으로 http 요청들과 JVM에 대한 모니터링을 하였습니다
- Mysql 메트릭 모니터링으로 QPS나 SlowQuery 등을 모니터링 하였습니다.
- 시스템 자원 메트릭 모니터링으로 자원 사용량을 모니터링 하였습니다.
- 자원 사용량 및 슬로우쿼리 발생등 이상 상황시 slack webhook으로 알림을 보내도록 설정하였습니다.
실제 사용자들이 사용하는 보편적인 시나리오로 성능 테스트를 진행하였습니다.
- 200명의 유저가 동시에 회원가입 후 로그인 요청을 합니다.
- 로그인 요청 후 발급받은 JWT를 이용해 간단한 경기 목록 조회 및 경기 상세 조회를 합니다
![Screenshot 2024-02-20 at 10 03 41 PM](https://private-user-images.githubusercontent.com/54535550/311128745-821cde0b-ce17-4c2f-8010-7849b2e6e4cd.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkwOTEzODAsIm5iZiI6MTczOTA5MTA4MCwicGF0aCI6Ii81NDUzNTU1MC8zMTExMjg3NDUtODIxY2RlMGItY2UxNy00YzJmLTgwMTAtNzg0OWIyZTZlNGNkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA5VDA4NTEyMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThlNDIyYmU5ZmU2ODA5ZGUxOTVlMDljYjU4NjJiMWNlOWE4YjE3ZjIyYzBiMGVkOGE2MjcyM2U0ODdmYWM0OTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.FXA4bXpYvcXQUKWCM4uMBM3aDFqegikaxtteKkgjFpE)
병목 구간을 찾지 위해서 Naver의 PinPoint APM을 사용하였다, http 요청 트랜잭션을 분석했을때는 크게 병목 구간이 없었다. 그래서 전체적인 flow에 집중하였고 단순 많은 가상 유저수에 비해 서버 하드웨어 성능이 빈약함으로 인해 웹 어플리케이션 서버에서 병목 지점이 발생하고 있다고 판단하였다.
별도의 nginx 인스턴스를 개설하여 LoadBalancer로 사용했다, 요청은 Round-Robin 방식으로 분배하였다.
왼쪽이 로드밸런싱 전 TPS 측정 결과이고 오른쪽이 두대로 로드밸런싱 한 후 TPS를 측정한 결과다.
TPS 개선 정도는 스케일 아웃한 인스턴스 갯수에 정비례 하다는 사실을 확인할 수 있었다.
제일 기억에 남았던 이슈는 회원가입 중복 확인에 대한 동시성 문제였습니다
https://archanwriteup.tistory.com/entry/슬축생-프로젝트-10-회원가입-아이디-중복-확인시-동시성-문제-해결
nGrinder로 성능 테스트를 진행하던 중 의도치 않게 중복 가입 요청들이 생성되었는데 가입이 되어버렸습니다.
원인은 회원 중복 확인시 데이터베이스에 select 쿼리로 회원 id의 존재 여부를 확인하는데 별 다른 동시성 처리가 없었기 때문에 동시에 select 쿼리를 실행하고 없다는 결과를 받을 시 회원가입이 되었다는것이 문제였습니다.
처음에는 비관적 락을 이용하여 동시성 문제를 해결하였지만 이로 인해 중복회원 가입 시 API 응답 지연이 발생하여 회원 아이디에 유니크를 설정해주고 유니크 예외를 핸들링하는 방식으로 처리했습니다. 사실 처음부터 유니크 제약조건을 걸어야하는건 당연했는데 급하게 진행되느라 놓친것 같습니다..