[kernel360] final Project 회고
이번엔 따끈따끈한했던 final Project 회고....
2023.11.27(월) ~ 2024.03.29(금), 16주
프로젝트를 설계하고 개발하고 배포하고, 기능적인 그리고 운영상의 수정 요구사항들에 대한 업데이트를 하고 다시 배포하는 과정을 모두 경험하는 기간이다. 이 전과정에서 개발을 위한 환경을 설정하고 협업 도구들을 활용하여 협업을 이루어 나간다.
JDON
어떠한 기술스택 으로 어떠한 강의를 학습해야하는지 고민하는 모든 개발자들을 위한, 개발자들의 네트워킹을 위한 서비스. JDON
기획의도
최근 기술적으로 고려되어야 할 이슈와 이를 해결하기 위해 사용하는 기술을 간접 경험하기 위해서 선택했던 방법의 하나가 채용 공고에 작성된 요구사항을 확인하는 것이었다. 효율적인 학습법이라고 생각하였고, 다른 개발자 동료도 같은 이유로 학습 목표 설정에 대해 고민하는 것을 알게 되어 이들에게 도움이 될 수 있는 서비스를 만들고자 기획하게 되었다.
기능
- 인기 기술스택 기반 추천
- 원티드에서 수집한 JD를 기반으로 사용자에게 인기 기술스택을 추천합니다.
- 인프런 강의 연동
- 인프런에서 각 기술스택에 대한 실용적인 인기 강의를 수집하여 제공합니다.
- JD 조회와 리뷰 서비스
- JD 목록을 제공하고, JD 게시글에 리뷰 서비스를 제공합니다.
- 개발자 커뮤니티
- JDON 회원을 위한 커피챗 서비스를 제공하여, 경험 공유와 네트워킹을 도모합니다.
Github
JDON 기술 스택
- Java 17
- Spring Boot 3.X 버전을 사용하기로 결정하면서 Spring Boot 3.X의 요구 조건인 17 이상의 자바버전이 필요했다. 17이상의 자바 버전들 중에서 LTS 버전이냐를 우선적으로 고려했다. LTS버전은 장기간동안 지속적으로 보안패치와 성능 개선을 받을 수 있기 때문에 장기적 관점에서의 프로젝트의 안정성을 생각했을 때 가장 우선적으로 고려할 사항이었다. 17버전과 21버전을 고려했을 때 굳이 17버전을 고집할 필요가 없고 지원기간이 더 길기도 한 21버전을 선택했었다.
그런데 프로젝트를 빌드했을 때 gradle이 JDK 21 버전을 아직 정상적으로 지원하지 않는 이슈가 발생하여서 결과적으로 17버전으로 다운그레이드함...
현재 시점으로 프로젝트를 회고하면서 다시 시도해보았을 때 이 레퍼런스를 따라서 인텔리제이 내의 Gradle의 JVM 버전과 프로젝트의 SDK 버전까지 21로 맞춰주면 Java 21로 빌드 가능하다.
- Spring Boot 3.X 버전을 사용하기로 결정하면서 Spring Boot 3.X의 요구 조건인 17 이상의 자바버전이 필요했다. 17이상의 자바 버전들 중에서 LTS 버전이냐를 우선적으로 고려했다. LTS버전은 장기간동안 지속적으로 보안패치와 성능 개선을 받을 수 있기 때문에 장기적 관점에서의 프로젝트의 안정성을 생각했을 때 가장 우선적으로 고려할 사항이었다. 17버전과 21버전을 고려했을 때 굳이 17버전을 고집할 필요가 없고 지원기간이 더 길기도 한 21버전을 선택했었다.
- Spring Boot 3.2
- Spring Boot 2.7 버전은 2023년 11월 이후에 지원이 종료되어서 계속적으로 지원을 받을 수 있으면서 안정화 버전이 나온 3.2 버전 선택
- MySQL 8.0, Spring Data JPA 3.2, QueryDSL 5.0
팀 문화
팀 JDON이 구성된 후 우리가 가장 중요하게 목표했던 점은 내가 하지 않은 일에 대해서도 빠르게 파악할 수 있도록 하자.는 것이었다. (aka. 너의 코드도 나의 것마냥 알고 있고 싶다.)
1. 컨벤션 만들기
기본적으로 커밋, 브랜치부터 패키지, 코드 컨벤션까지 다양한 부분에 대해서 함께 규칙을 정하고 지키기 위해서 노력했다. 특히 코드 컨벤션은 어떤 로직을 어떤 계층에 작성할 것인지, 특정 동작에 대한 메서드명을 사용할 것인지 자세하게 정하려고 했다. 이렇게까지 자세하게 정의하려고 했던 이유는 팀 내 코드에 통일성을 가져서 시스템적으로는 각 계층의 책임과 역할을 지킬 수 있도록 하고 협업의 측면으로는 다른 사람의 코드 파악 속도가 빨라질 것이라고 기대했기 때문이었다. 그리고 실제로 시간이 지날수록 이런 효과를 느꼈다.
2. 서로의 업무를 동기화하여 팀 내 이슈를 항상 파악하자
이를 위해서 2~3일에 한번씩 정기 스프린트 회의를 가지고 업무 상황을 파악하고 다음 마일스톤을 세워서 JDON 내의 각 기능이 어떻게 진행되고 있는지 공유했다. 다음으로 발생하는 이슈에 대해서는 Github Issue를 적극 활용하여 팀내에 공유했다.
3. 코드를 적극적으로 설명하고 적극적으로 리뷰하는 문화를 형성하자
내가 한 업무에 대해서 팀원들에게 PR-Template에 맞춰서 추가된 부분과 그 이유와 결과를 정확하게 공유하고 팀원들은 코드를 읽고 적극적으로 의견을 제시하여 내가 알지 못하는 로직이 서비스에 붙어있다는 생각이 들지 않도록 노력했다.
내가 JDON에서 한 일
▷ Session, OAuth를 활용한 로그인 구현
JDON을 기획하면서 auth 단계에서 사용자의 접근성을 높이고 비밀번호를 관리하는 리소스를 줄이기 위해서 OAuth 로그인 도입을 결정했다. 그리고 인가를 위해서 세션과 JWT 중 어떤 방식을 선택할 것인지 고민했는데 다음의 이유들로 세션을 사용하기로 했다.
1. JDON의 API 들 중 인가가 필요한 API의 비율은?
JDON은 단순 기술 스택 검색이나, JD 확인 서비스 외에는 전부 로그인된 계정을 기반으로 서비스를 제공한다. 이 말은 곧 대부분의 API가 인가를 거쳐야한다는 말인데... 그렇다고 토큰에 사용자의 중요한 정보를 무조건 담을 수도 없는 노릇이니 만일 토큰을 사용한다면 사용자의 정보를 가져오기 위해서 DB조회를 계속할 것으로 예상되었다. 또한 토큰을 사용한다면 토큰의 유효성 검증을 위한 연산도 매번 진행될 것이다. 그렇다면 세션을 사용하여 서버의 메모리나, 혹은 레디스를 사용하여 DB의 부하를 줄이고, 좀 더 빠른 인가처리를 할 수 있지 않을까?
2. 토큰의 보안성을 생각하다보니 세션과 별다를게 없어진 것같다.
토큰은 클라이언트가 저장하고 있기 때문에 만일 토큰이 털리면 해당 토큰이 만료되기 전까지는 속수무책으로 피해를 입을 수 밖에 없다. 서버에 토큰을 저장하지 않아 서버에서 토큰을 삭제하거나 무효화할 수 없기 때문이다. 따라서 이런 문제를 해결하기 위해서 흔히 Refresh Token을 두고 RTR 방식을 적용하여 결과적으로 토큰을 저장해두고 관리한다!!!!!
또한 로그아웃, 혹은 회원탈퇴했을 때를 고려해봐도 토큰만의 만료기한을 따로 가졌기 때문에 이 토큰을 더 이상 허용하지 않기 위해서는 어딘가에 저장해두고 로그인을 할 때마다 혹시 로그아웃, 회원탈퇴를 한 토큰은 아닌지 검증해야한다.
그럼 세션이랑 뭐가 달라...?
3. 다양한 서브 도메인을 가진 서비스로 성장할 것인가?
세션의 가장 큰 단점이 확장성에 용이하지 않다는 점이다. 토큰은 서버가 저장하고 관리하지 않기 때문에 서버를 스케일 아웃하던, 아님 신규 서비스에 대해 계정을 같이 사용하던 확장 가능하지만 세션은 그렇지 않다. 냉정하게 생각해봤을 때 서버를 스케일 아웃할 수는 있어도 신규 서비스에 대한 고민을 하는 것은 굳이 싶다. 흠... 규모가 커졌을 때, 스케일 아웃된 서버에 대해 세션을 어떻게 관리할 것인지가 마지막 고민인데.....
4. 세션의 단점은 이제 거의 극복 가능하다.
이제는 세션의 가장 큰 단점인 `확장성에 불리`라는 문제를 해결할 수 있는 방법들이 존재한다.
- Session Clustering
- Sticky Session
- Session을 Database에 저장 ( ex. Redis, .. )
그렇다면, (유저의 로그인 상태에 대한 제어 하나는 확실히 할 수 있는) 세션을 쓰는 것이 맞을 것 같다. 고 결정했다.
이제 인증, 인가 로직을 짤거에요. Spring Security로
Spring Security는 스프링 기반 애플리케이션의 보안을 담당하는 스프링의 하위 프레임워크로 보안 관련 옵션을 많이 제공하고 있다. CSRF 공격, session fixation 공격을 방어하고 요청 헤더도 보안 처리를 해주기 때문에 개발자가 보안 관련해서 신경써야 하는 부분을 줄여준다. 기본적으로 보안에 대한 신뢰할 수 있는 처리를 해주는 Spring Security를 안쓸 이유는 없었다.
OAuth2 로그인 구현하기, 다중 로그인 막기, 세션 유효시간 정하기, 로그아웃시 세션과 세션을 담은 쿠키 관리하기 였다.
JDON의 auth를 구현할 때 적용한 방식을 토대로 작성한 oauth2-client를 이용한 OAuth 로그인 구현 가이드
SecurityFilter로 다중 로그인 막기, 세션 유효시간 정하기, 로그아웃시 세션과 세션을 담은 쿠키 관리하기 (To Be Continue...)
JDON 회원가입 로직 (feat. AES, HMAC)
사용자가 OAuth2 인증을 성공하면 email(unique)을 기반으로 존재하는 회원인지 확인한다. 존재하지 않는 이메일이면 회원가입하도록 해야한다. 이때!!! 우리의 로직상 회원가입하려면 OAuth2 Provider로부터 받은 사용자의 정보(email)를 일부 사용해야한다. 하지만 OAuth2 인증에 대한 요청과 회원가입을 위한 요청은 별개의 요청이며 HTTP는 stateless하기 때문에 이 두 요청 사이에 정보를 전달해줄 수 있는 뭔가의 로직을 구현해야했다.
결론적으로 서버에서 OAuth2 Provider로부터 받은 사용자의 정보를 어떻게 안전하게, 프론트에 전달하냐는 것이었는데 내가 할 수 있는 것은 redirect로 정보를 꽂는 것이었고 그렇다면 query parameter를 써야했다.
그래서 사용자의 정보를 암호화하기로 했다.
우선 사용자 정보 암호화에는 고민할 것도 없이 대칭키 알고리즘을 결정했다. 이유는 비대칭키 암호화 알고리즘에 비해서 키 사이즈가 작고, 알고리즘 구조가 간단해서 연산 속도가 훨씬 빠르기 때문이다. (키 관리만 잘한다면...) 서버의 부하를 최대한 줄이고 싶었다.
AES (Advanced Encryption Standard, 고급 암호화 표준) 알고리즘
블록 암호를 사용하여 암호화 및 복호화를 진행하는 대표적인 대칭키 암호화 알고리즘으로 기존 DES 암호화 강도가 약해지면서 새롭게 표준 알고리즘으로 채택되었다. 높은 안전성과 효율성, 유연성, 알고리즘 단순성의 장점을 가진다.
하지만 암호화만 하면 끝일까?ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ
Case 1 | 공격자가 암호화된 데이터를 가로채서 변경할 수 있다. 수신자는 데이터가 변경되었는지 알 수 없으므로, 변조된 데이터를 복호화하여 사용할 위험이 있다. |
Case 2 | 공격자가 이전에 전송된 암호화된 데이터를 재전송할 수 있다. HMAC이 없다면, 수신자는 이 데이터를 유효한 새로운 데이터로 인식할 수 있다. |
Case 3 | 공격자가 자신을 신뢰할 수 있는 송신자로 위장하여 데이터를 전송할 수 있다. HMAC이 없다면, 수신자는 이 데이터를 신뢰할 수 없는 출처에서 왔음을 알 수 없다. |
그래서 이것도 함께 사용했다.
HMAC (Hash-based Message Authentication Code)
HMAC은 메시지의 무결성을 확인하고 인증할 수 있는 방법이다. HMAC은 해시 함수와 비밀 키를 사용하여 메시지의 해시 값을 생성하는데 이 해시 값은 메시지와 함께 전송되어 수신자가 메시지의 무결성을 검증할 수 있게 한다. HMAC을 사용하면 메시지가 전송 도중 변경되지 않았음을 보장할 수 있다.
이 두가지 방법 모두 키 관리가 핵심이기 때문에... 키 관리에 대해서 열심히 고려해야한다...ㅎㅎ..ㅎㅎ.ㅎ.ㅎ.ㅎ.ㅎ.ㅎ.ㅎ
JDON 로그인 로직
사용자가 OAuth2 인증을 성공하면
1. email(unique)을 기반으로 존재하는 회원인지 확인한다.
2. 존재하는 email임이 확인되면 로그인한 OAuth Provider와 DB에 등록된 OAuth Provider를 검증하여 회원 검증을 마무리한다.
3. 회원 검증에 성공하면 마지막 로그인 날짜를 업데이트하고 세션에 사용자의 id와 마지막 로그인 날짜, 권한을 저장하고 세션 ID를 쿠키에 담아 전달한다.
▷ CI / CD 파이프라인 구축
Github Action과 서버 인스턴스에 작성한 Shell Script를 통해서 배포했다. 오픈한 PR에 대해서 test를 할 수 있도록 구성한 CI 플로우가 존재하고 PR이 develop에 머지되면 수동 트리거를 이용해서 test code 실행 후에 서버 인스턴스에 있는 배포 shell script를 동작시키도록 했다. shell script는 깃헙 코드를 받아서 build 후 프로그램을 실행시키는데 JDON은 기능에 따라 API 서버와 batch 서버가 따로 있기 때문에 이 중에서 배포시키고 싶은 서버만 골라서 실행시킬 수 있도록 수동 트리거를 커스텀했다.
왜 수동 트리거를 사용했는지 궁금하시다면...ㅎㅎ
두가지 이유가 있었는데 첫번째는 PR이 main이나 develop으로 병합될 때 Github Action의 Deploy job이 돌도록 CD 구성하였으나 해당 시점에서 Secrets를 읽어오지 못해 CD에 실패하는 이슈가 발생했었다. 원인은 Github Action 정책상 forked Repository에서 발생한 workflow는 Secrets가 전달되지 않기 때문에 CD에 실패하게된 것으로 추측하고 있다........
두번째는 마침 개발 단계가 마지막에 접어들었고, 팀 내 작업 상황상 아직 개발 서버에 배포하지 않기 위해서 승인 완료 후에도 병합하지않고 대기 중인 PR이 다수 존재했기 때문에 수동 Trigger를 사용하기로 최종 결정했다.
JDON Architecture
JDON Trouble Shooting
▷ 납득이 가는 멀티모듈 구조 도입기
▷ 납득이 가는 Layered Architecture 도입기
▷ Cookie, you know?
▷ 내가 너를 뜯어봐야했던 이유.. | Series 1. OAuth2 Client 편
▷ 내가 너를 뜯어봐야했던 이유.. | Series 2. Request Header 편
▷ CSRF, but never used...
▷ Gtihub Action, 아 왜 PR만 Secret Key 못 읽는데.....
▷ 커버링 인덱스를 활용하여 페이지네이션 조회 속도 개선하기
▷ Redisson의 pub/sub 분산락을 사용하여 동시성 문제 해결하기
JDON 개선할 점은 여전히 있다.
▷ CSRF 적용하기
▷ Deploy 방식 뜯어고치고 싶어요
CD 플로우를 완성하지 못한 것에 대해 아쉬움이 너무 크다. CD 실패의 원인으로 fork Repo에서 발생한 workflow에 Secrets를 주지 못한다는 정책이 있었지만, 내가 생각해봤을 때 규모있는 회사에서는 레포를 포크떠서 사용할 것 같고, trunk based develop 방식을 쓰는 글들을 심심치않게 봤었는데 해당 정책대로라면 이렇게 사용하는 회사에서는 모두 우리와 같은 이슈를 겪었어야 할 것 같지만 아무리 구글링을 해도 같은 이슈를 흔히 찾아볼 수 없었기 때문에 아직 내 눈에 보이지 않는 어딘가에 구멍이 있었을거라고 생각된다.
또한 shell script를 이용해서 깃 코드를 받아서 빌드한 다음 서버를 내리고 새로 빌드한 서버로 다시 띄우고 있는데 이렇게 했을 때 단순히 Github Action이 성공한 것을 서버가 성공적으로 실행되고 있다고 보기 어렵기 때문에 Github Action의 성공이 서버 실행 성공임을 보장하는 flow로 리팩터링해야한다.