Code States/회고

Final Project - 마라톤 대회 결과 기록 시스템, 마라마라톤!

ki1111m2 2023. 6. 28. 12:44

프로젝트 개요

  • 개인 사용자와 대회 주최자를 위한 마라톤 대회 결과 기록 시스템
  • 유저 데이터를 저장하고 있는 데이터베이스는 다른 데이터베이스와 분리되어 있음
  • 기록 데이터를 기반으로 사용자별 점수를 기록하는 시스템은 데이터 유실을 막기 위해 SQS를 사용하여 느슨하게 결합됨
  • 각 유저는 로그인 기능을 통해 유저 타입에 따른 토큰을 발급받으며, 해당 토큰을 기반으로 메인 서버에 대한 CRUD 요청이 동작함

 


요구사항 분석

인프라 요구사항

  • 시스템 전반에 가용성, 내결함성, 확장성, 보안성이 고려된 서비스들이 포함되어야 합니다.
  • 하나 이상의 컴퓨팅 유닛에 대한 CI/CD 파이프라인이 구성되어야합니다.
    • Github Action을 이용하여 CI/CD 파이프라인 구성
  • 유저 데이터를 저장하고 있는 유저 데이터베이스는 다른 데이터베이스와 분리되어있어야 합니다.
    • 2개의 데이터베이스로 구성
    • user_db: 유저 정보, 점수
    • main_db: 마라톤 대회 정보, 공식 기록, 비공식 기록, 참가자
  • 기록 데이터를 기반으로 사용자별 점수를 기록하는 시스템은 데이터 유실을 막기 위해 느슨하게 결합되어야합니다.
    • Lambda, SQS 사용
    • 공식 기록 테이블에 점수가 추가되는 것을 트리거로 람다 함수 작동 -> SQS에 메세지 전달
    • SQS를 트리거로 람다 함수 작동 -> 점수 추가
  • 시스템 메트릭 또는 저장된 데이터에 대한 하나 이상의 시각화된 모니터링 시스템이 구축되어야합니다.
    • Grafana, Cloudwatch 이용
    • 별도 컴퓨팅 유닛 이용하여 가동

 

 

기능 요구사항

  • 개인 사용자와 대회주최자는 로그인 기능을 통해 토큰을 발급받을 수 있습니다.
    • jwt를 이용하여 구현
    • 로그인시 GET 요청 body에 login_id, password 값 넣어서 DB에 일치하는 정보가 있다면 토큰 발급
    • 토큰에는 user_id, user_type 포함
    • user_type에 따라 CRUD 동작 분리
  • 인증된 개인 사용자는 자신의 비공식 기록을 입력 및 조회할 수 있습니다.
    • 조회: GET
    • 입력: POST
  • 인증된 개인 사용자는 특정 대회에 참가 신청을 할 수 있습니다.
    • 참가 신청: POST
  • 대회 주최자는 대회 참가자를 조회할 수 있습니다.
    • 조회: GET
  • 대회 주최자는 대회 참가자들에 대한 공식 기록을 입력 및 조회할 수 있습니다.
    • 조회: GET
    • 입력: POST
  • 대회 주최자에 의해 입력된 공식 기록에 따라 해당 참가자의 point 데이터에 점수가 추가됩니다.
    • Lambda, SQS 이용하여 구현
    • 공식 기록 입력시 point 데이터에 점수가 추가되도록 자동화
  • 개인 사용자는 점수를 확인할 수 있습니다.
    • 점수 확인: GET

API 명세서.pdf
0.07MB


 

데이터베이스

유저 데이터베이스와 다른 데이터베이스를 분리하라는 요구사항에 맞춰 2개의 데이터베이스로 구성했다.

USER DB는 AWS RDS에서 MySQL을 사용했고, user 테이블과 point 테이블을 만들었다. point 테이블에는 점수 요청이 들어오는 대로 쌓이도록 구성했고, 유저별 총 점수는 user 테이블의 total_point에 합산되어 저장되도록 했다. 로그인시 user 테이블에서 login_id와 password를 확인하고 user_id와 user_type을 반환한다. user_type은 불린값으로 True일시 대회 주최자, False일시 개인 사용자로 분류했다.

MAIN DB는 AWS DynamoDB를 사용했고, official_marathon, official_record, non_official_marathon, player 테이블을 만들었다. 각 테이블에 대한 요청은 토큰에 따라 다르게 작동한다.

 


기본 인프라 구성

기본 CRUD 구현은 다른 팀원들이 맡았고, 그 사이 인프라 구성을 시작했다. (세부과정)

메인 서버는 ECS를 통해 가동하고, express로 구현하기로 결정했다. express로 기본 서버를 만들고 도커 컨테이너로 동작하는지 테스트를 진행하고, 정상 작동하는 것을 확인한 후 이미지를 AWS ECR에 Push했다. 이미지를 이용하여 태스크 정의와 서비스를 생성하고 태스크가 정상작동하는 것을 확인했다.

ECS는 Auto Scailing 기능을 이용하여 확장성을 확보했고, Application Load Balancer를 붙여서 가용성과 내결함성을 확보했다. 

ECS 사용 이유

컨테이너화된 애플리케이션을 배포, 운영, 확장하기 위해 선택했다. ECS는 컨테이너를 실행하는 노드 플릿 유지 관리 및 확장에 용이하다. 또한 컨테이너의 실행을 오케스트레이션 할 수 있으며 로그 수집, 모니터링 등 다양한 컨테이너 관리 기능을 가지고 있다.
Application Load Balancer 사용 이유

ECS 클러스터 내의 여러 컨테이너 인스턴스에 대한 트래픽을 분산시킬 수 있는 로드 밸런싱 기능이 있다. 이를 통해 애플리케이션에 대한 부하를 균형적으로 분산하여 안정성과 확장성을 향상시킬 수 있다. 요청의 경로, 호스트, 헤더 등을 기반으로 트래픽을 다른 컨테이너로 동적 라우딩할 수 있다. 또한 SSL/TLS 종료 처리 및 서비스 검사 기능을 제공한다

 


CI/CD 파이프라인 구성 및 자동화 구현

프로젝트에서 사용한 컴퓨팅 유닛은 Lambda, EC2, ECS이다. EC2의 경우, 모니터링 시스템에 사용할 예정이라 항시 가동하기로 결정했고, Lambda와 ECS에 대해 CI/CD 파이프라인을 구성하기로 했다. 또한 프론트엔드에 사용될 S3 버킷에 대해서도 구성하였다. S3 버킷에 정적 웹 호스팅 파일 업로드, Lambda 함수 배포, ECR 이미지 Push, 태스크 정의 생성, ECS 서비스 업데이트를 자동화하기 위해 Github Action을 사용했다. (Lambda 자동화 세부과정) (ECS 자동화 세부과정)

 


VPC 생성 및 서브넷 구현

EC2와 ECS는 퍼블릭 서브넷에, 데이터 보안을 위해 RDS는 프라이빗 서브넷에 위치시키기로 했다. (세부과정)

프로젝트를 위한 새로운 VPC를 생성한다. ECS 서비스는 2개 이상의 가용영역이 존재해야 생성되므로 가용 영역과 퍼블릭 서브넷 수, 프라이빗 서브넷 수는 각각 2개씩으로 지정한다. 리소스들을 새로운 VPC에 생성한다. RDS의 경우, 프라이빗 서브넷으로 지정한다. 프라이빗 서브넷에 위치한 RDS에는 바로 접근할 수 없으므로 EC2 인스턴스(모니터링을 위한 ec2를 활용했다.)를 통해 접근하여 세팅했다. RDS에 접근해야 하는 point_increase_lambda에 프라이빗 서브넷에 접근하기 위한 권한을 추가한다.

 


jwt 토큰을 사용한 로그인 구현 및 CRUD 분리

로그인 시스템을 구현하는 데는 jwt를 사용하기로 결정했다. (로그인 구현 세부과정)

user 서버에 로그인 요청 시 GET 메서드에 body 값으로 login_id와 password를 전달한다. user 서버는 user db에서 login_id와 password가 일치하는 사용자가 있는지 확인하고, 존재한다면 토큰을 발급한다. 토큰에는 해당 유저의 user_id와 user_type이 담긴다. 발급된 토큰은 api 요청에 따른 응답으로 사용자에게 반환된다.

main 서버에 api 요청 시 헤더에 토큰을 입력한다. main 서버에서는 토큰을 디코딩하여 user_id와 user_type을 사용한다.

 

기존에 작성한 CRUD를 기능 요구사항에 맞춰 수정했다. (CRUD 분리 세부과정)

마라톤, 공식 기록에 대해서는 user_type이 1(True)인 경우에만 작동하도록 변경했고, 비공식 기록, 참가 신청에 대해서는 user_type이 0(False)인 경우에만 작동하도록 변경했다. user_type이 일치하지 않는 경우, 제한된 접근 안내 메세지가 반환되도록 했다.

 


CloudFront & Route 53 연결

HTTPS 연결을 통해 보안을 강화했다. (세부과정)

메인 서버의 경우, 클라우드 프론트는 사용하지 않고 Route 53만 연결했다. ECS 서비스에 연결된 alb에 https 연결을 허용할 443번 리스너를 생성했다. 그 후 route 53 레코드 생성시 alb 주소를 연결하였다. ( user.mrmrthon.click / server.mrmrthon.click )

프론트엔드의 경우, 클라우드 프론트에 S3 버킷을 연결하였다. 그 후 route 53 레코드 생성시 클라우드 프론트 주소를 연결하였다. ( www.mrmrthon.click )

모니터링 시스템의 경우, ec2의 퍼블릭 주소를 Route 53에 연결했다. ( monitor.mrmrthon.click )

Cloudfront 사용 이유

HTTPS를 통해 컨텐츠 전송을 암호화하여 데이터의 보안성을 유지할 수 있다. 정적 컨텐츠를 캐시하고 동적 컨텐츠를 가까운 엣지 로케이션에서 캐싱하여 성능을 최적화기 때문에 빠르고 안정적인 컨텐츠 전송이 가능하다. 필요에 따라 자동으로 스케일링 된다.
Route 53 사용 이유

도메인 이름 등록 및 관리를 제공한다. DNS 조회 요청을 처리하고, 도메인 이름을 IP 주소로 매핑하여 인터넷에서 액세스할 수 있도록 한다.

 


모니터링 시스템 구현

EC2에 도커를 이용하여 Grafana를 가동하기로 했다. (세부과정)

EC2를 생성한 후 ssh로 접속해 도커를 설치한다. 컨테이너로 Grafana를 실행하면 ec2의 퍼블릭 주소로 그라파나에 접근할 수 있게 된다. 데이터 소스로 클라우드 워치를 지정하여 클라우드 워치의 성능 지표를 가져올 수 있도록 한다. 각 소스별로 모니터링 화면을 볼 수 있다. 대시보드를 커스텀하여 우리 팀에게 필요한 모니터링 지표를 시각화했다.

 

추출한 성능 지표는 다음과 같다. 

ECS: CPU Utilization, Memory Utilization

RDS: CPU Utilization

Lambda: Duration

SQS: 들어온 메시지 수, 나간 메시지 수, 처리 중인 메시지 수

DLQ: 보유한 메시지 수

DynamoDB: Read Capacity Utilization, Write Capacity Utilization

Cloudwatch 사용 이유

리소스의 상태, 성능 지표, 로그, 이벤트 등 실시간 모니터링이 가능하다. 설정한 조건에 따라 경고 및 알림을 제공하는 기능이 있다. 본 모니터링 시스템에서는 필요한 성능 지표들만 추출하여 모니터링 시스템에 사용했다.
Grafana 사용 이유

차트, 그래프, 테이블 등 다양한 시각화 옵션을 제공한다. Promatheus, Cloudwatch 등 다양한 데이터 소스를 지원한다. 메트릭 및 이벤트를 실시간으로 모니터링 할 수 있다.

 


트러블슈팅

  • Secrets Manager를 이용한 환경변수 사용시 특정 환경변수 값을 불러오는 것이 아닌 시크릿매니저에 입력된 모든 값을 불러옴
더보기

 

태스크 정의에서 시크릿 매니저의 arn을 입력할 때 뒷부분에 변수명을 붙여줘야 함

해결

  • PUT DELETE 요청처럼 특정 id를 찾는 쿼리를 사용할시 Data not found 상황 발생
더보기

바디로 들어온 값을 int로 인식하지 못하는 것 같았다

id에 할당한 값을 parseInt를 통해 int로 변환해준 후 해결

  • [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
더보기

서버가 2개 이상의 응답을 보내려할때 발생
if문 밖의 res.json 문을 지워 해결

  • JsonWebTokenError: invalid token
더보기

console.log(token)을 통해 토큰값을 확인한 결과, 제대로 들어간 것을 확인
그러나 Invalid Token Error 발생

https://stackoverflow.com/questions/48606341/jwt-gives-jsonwebtokenerror-invalid-token

토큰 앞의 Bearer를 제거한 후 해결했다는 글을 발견

split(' ')을 통해 토큰값만 불러온 후 해결

 


마치며

약 2주간 진행한 파이널 프로젝트가 끝났다! 👏

지금까지 했던 프로젝트 3개를 종합하는 느낌이었다. 전에는 프로젝트 1개에 4-5일씩 걸렸는데, 프로젝트 3개를 합친 분량의 이번 프로젝트도 5일만에 구현을 끝냈다. 😊 당시에는 트러블 슈팅도 엄청나게 많았는데 이번엔 오타 등의 자잘한 트러블 슈팅만 있었다. 전보다 구글링 실력이 늘어난건지 전처럼 한 문제를 가지고 하루 종일 고민한 이슈는 없었던 것 같다. 이제는 구글링 방법이나 문제 해결에 접근하는 방법을 좀 더 깨달았다.

지금까지 했던 프로젝트는 팀원들 다같이 진행했었는데, 이번 프로젝트는 본격적으로 역할을 분배해서 진행했다. 그렇다보니 lambda 함수 작성이나 IaC 구현 부분에는 손대지 못해서 아쉽다. 프로젝트는 종료되었지만 개인적으로 IaC는 다시 진행해봐야겠다.

자동화 부분은 혼자 맡아서 진행했는데 확실히 이해도가 올라간 것 같다. 지금까지는 작동하게 하는 것에 중점을 뒀는데 이번엔 workflow yml 파일의 단계 하나하나를 이해하고 수정하려고 노력했다.

jwt는 정말 처음 접해본 거였는데 어쩌다보니 혼자 맡게되어.. 홀로 고군분투 했다. 이틀간 붙들고 노력한 결과 동작 과정이나 사용법은 어느정도 이해한 것 같다. 원하는 값만 커스텀해서 사용하기에 좋은 것 같았다.

어쩌다보니 팀장도 맡게 되고 거의 모든 문서 작업과 발표까지 맡게 됐는데 팀원분들 모두 열심히 노력하고 도와주셔서 끝까지 해낼 수 있었다. 가이드라인 없이 프로젝트를 진행해본 것은 처음이었는데 그래도 잘 끝낸 것 같아서 매우 뿌듯하다. 이번 프로젝트는 2팀이었지만.. 내 마음 속 우리는 10팀이다. 최강 10팀 ! ❤