SSAFY에서의 마지막 프로젝트를 마무리하며 회고를 해보도록 합니다.
프로젝트 정보
프로젝트 명 : 덥잉 (dub-eng)
따라읽기만 하던 영어는 이제 그만! ✋ (Practice makes Perfect)
덥잉 서비스는 영상을 직접 더빙을 해봄으로써 재미있게 영어의 강세 습득 및 스크립트를 외우면서 자연스럽게 회화 스킬을 얻을 수 있습니다. 영화나 드라마속 다양한 배우들과 함께 대사를 주고 받으면서 여러분도 스타가 되어보세요.
기간 : 6주 (23.04 ~ 23.05)
인원 : 7명(BackEnd 3, FrontEnd 3, Infra 1)
주 역할 : FullStack(?) Developer (기여도 30%)
FullStack Developer??
원래 주 역할은 DevOps
이번 프로젝트에서의 주 역할은 DevOps 였습니다. 프로젝트 시작과 동시에 쿠버네티스에 대해서 공부했습니다. 하지만 팀의 기획에 비해 인프라에 투자할 수 있는 시간은 매우 부족했고, 실제 배포를 하고 서비스 실 사용자를 받아보고 싶었기에 개발을 빠르게 진행해야했습니다. 그렇게 쿠버네티스의 우선순위를 2순위로 미뤄두고 개발 파트를 맡아 진행하였습니다.
첫 번째, BackEnd에 기여를 하다
1. 카카오 소셜로그인 기능을 구현할 회원 API 개발
지난 프로젝트에서 했던 카카오 소셜로그인 기능에 이어서 카카오 로그인 과정에서 인가코드를 API 서버로 응답받는 흐름을 지난 번에서는 클라이언트로 받는 과정을 선택했습니다.
하지만, 카카오 API 문서를 참고했을 때, 시퀀스 다이어그램은 아래 그림과 같았습니다.
여기서 Service Server는 덥잉 서비스의 백엔드 서버를 의미하며 Kakao API 요청은 서버단에서 이루어지는 것을 확인할 수 있었습니다. 동의 화면 출력 이후에 인가코드를 받는 부분을 백엔드 서버로 받아 그 다음 과정을 수행했습니다.
@GetMapping("/kakao/callback")
public void getAuthCode(@RequestParam String code, HttpServletResponse response, RedirectAttributes attributes) throws IOException {
}
JWT 토큰 관리
카카오 Auth 서버로 부터 발급받은 JWT 토큰을 클라이언트로 전송하고 보관하는 부분에서 고민을 많이 했습니다.
RefreshToken은 다양한 공격에 대비해 쿠키에 HTTP Only, Secure 설정을 통해 안전하게 보관및 전달해야 하고 AccessToken은 그에 비해 덜 안전하게 보관해도 되기에 쿠키로 보관하는 방법을 사용하였습니다.
ATK 유효성 검사 부분에 대한 수많은 중복코드
ATK의 토큰 만료를 대비해 RTK를 이용하여 ATK 유효성 검사 후 재발급 받는 과정에서 수 많은 중복 코드가 존재하게 되었습니다.
//토큰 유효성 검사
// 토큰을 파싱하는 경우 Exception을 처리해 RTK로 재발급받는 과정
try{
authService.parseToken(accessToken);
}catch(Exception e){
accessToken = authService.reissueATK(refreshToken);
}
이 부분에 대해 추후 Filter를 적용해 개선해 나갈 예정입니다.
추가로 JWT 토큰 보관방법, 공격에 대한 대비에 대한 내용은 따로 포스팅을 할 예정입니다.
2. 사용자 더빙 녹음 파일 저장 API 개발
사용자의 스크립트 별 녹음파일을 서버 볼륨에 임시 저장하기 위하여 컨테이너 내의 별도에 공간에 녹음 파일을 저장해야만 했습니다.
기존의 하나의 EC2 환경에서는 단순히 docker의 volume 연결을 통해 컨테이너와 로컬 환경과의 볼륨 연결이 쉽게 되었지만 쿠버네티스 환경에서는 쉽지 않았습니다.
AWS의 별도의 파일 시스템과 kubernetes의 PV를 구축하면서 문제를 해결하였는데 그 부분은 아래의 쿠버네티스 운영 환경 구축에서 설명하겠습니다.
Redis를 통한 사용자 녹음 임시 저장 파일 경로 저장
하나의 영상에 대한 임시 저장파일이 여러개 존재했기 때문에 Redis 자료구조는 List를 사용했습니다. 하나의 키에 여러개의 녹음 파일 경로를 저장할 수 있었고 최종 녹음 본 저장시에 RDB를 거치지 않고 Redis에서 사용자 녹음 파일의 경로를 조회가능합니다.
두 번째, FrontEnd에 기여를 하다
인원은 7명이지만 FrontEnd의 인원은 부족한 상태였고, 이전 프로젝트에서 React를 통한 FrontEnd 개발을 맡아 진행했었기에 React와 TypeScript에 대한 경험이 있었습니다.
Next.js는 처음이였지만 개발이 React와 크게 달라지지 않기 때문에 회원 API 개발이 끝나고 바로 Front 작업에 들어갔습니다. 회원쪽 API를 담당했기 때문에 회원 관련 화면과 로직을 담당하였습니다.
- 로그인 화면
- 회원가입 화면
- 마이페이지
하지만 Next.js의 특징인 SSR (서버 사이드 렌더링)의 특징을 잘 활용해서 개발을 하지는 못하였고 추후 리팩토링하는 과정에서 개선해 나갈 예정입니다.
운영 서버에 쿠버네티스 도입
실제 운영 환경에는 Amazon EKS를 이용한 쿠버네티스 환경을 구축했습니다.
구축 과정에서 AWS의 다양한 서비스 (EKS, ALB, EC2, EFS, ECR, ACM) 를 활용하였습니다. 실제로 Amazon EKS를 이용하면 컨트롤 플레인을 직접 구성하지 않고서 k8s를 손쉽게 사용할 수 있도록 편리성을 제공하기 때문에 On-premise 환경에서 보다 구축하기가 수월했습니다.
AWS EKS 구축 과정
- Bastion 서버를 생성합니다. (public으로 공개된 EC2 인스턴스)
- Bastion 서버에서 aws cli 명령을 통해 EKS 클러스터를 생성합니다.
- Worker Node 2개로 구성된 node-group을 생성합니다.
- ALB(Application Load Balancer)로 외부에 로드밸런서를 두어 요청을 로드밸런싱합니다.
- AWS ECR(Elastic Container Registry)로 각 API 서버의 이미지를 태그별로 업로드하고 관리합니다.
- Jenkins를 이용한 CI/CD 진행합니다.
PV(Persistence Volume)을 통한 파일 저장 볼륨 생성
또한 AWS EKS 환경에서는 EFS(Elastic File System)이나 EBS(Elastic Block Storage)를 PV로 사용해야 했고, 여러 파드에서 참조가능한 EFS를 이용하여 PV를 구축하였습니다.
Volume 참조가 필요한 서버(파일 저장 API 서버, 더빙 영상 저장 API)에 PVC(PV Claim)를 통해 연결을 하여 Volume 사용이 필요한 파드에 모두 연결할 수 있었습니다.
개발 외적인 기여
OpenAPI 2.x → OpenAPI 3.x (Swagger)
덥잉 서비스의 개발 환경은 아래와 같았습니다.
- localhost
- Test Server
- Production Server
실제로 개발 2주차에 AWS EKS를 이용한 쿠버네티스 구현을 완료한 상태에서 다양한 환경에서 API 테스트가 필요한 상황이였고 기존 OpenAPI 2.x의 경우 Localhost 외의 테스트 서버에 Front 팀의 테스트가 어려운 상태였습니다.
2.0에서는 API EndPoint URL을 3가지(host, basePath, schemes)로 정의했었습니다.
이러한 스펙 하에서는 하나의 endpoint URL만 정의할 수 있었습니다.
"host": "dub-eng.com",
"basePath": "/v1",
"schemes": [
"http"
]
Swagger 3.x에서는 요청을 보낼 서버를 선택할 수 있는 기능인 Multiple Servers가 추가되었고 멀티 URL을 지원합니다.
"servers": [
{
"url": "https://{username}.gigantic-server.com:{port}/{basePath}",
"description": "The production API server",
"variables": {
"username": {
"default": "demo",
"description": "this value is assigned by the service provider, in this example `gigantic-server.com`"
},
"port": {
"enum": [
"8443",
"443"
],
"default": "8443"
},
"basePath": {
"default": "v2"
}
}
}
]