백엔드 서비스 아키텍처에 대해 미리 구상했다.
최종 아키텍처는 아래와 같다. (프론트 포함)
GitHub Flow 전략 (형상관리 전략)
- Master Branch에서 개발을 시작.
- Master로 부터 Feature 단위로 Branch를 만듦.
- 기능을 세세한 개발 단위로 쪼개어 Commit을 수시로 함.
- 해당 기능이 모두 개발 완료되었다면 Push함.
- Pull Request를 통해 원격 저장소의 Master 브랜치로 Merge 요청.
- 팀원들 간의 피드백, 버그 리포팅을 통해 모든 팀원의 동의 하에 최종 Merge.
실전 프로젝트 이전, 미니 - 클론 프로젝트를 진행하며 틈틈히 공부해뒀던 JWT 인증 / 인가 방식을 채택하여 적용했다.
간략히 설명하자면 Spring Security와 JWT를 활용한 인증 / 인가 방식으로
위와 같은 방식으로 Spring Security와 JWT의 장점들을 조화롭게 녹여낸 것 같다. 😊
유저 테스트를 진행하며 받았던 피드백 중 로그인이 자주 풀려 불편하다는 의견이 있었다.
JWT의 AccessToken 만료 시간은 짧게 유지해야 탈취당해도 만료 시간 동안만 사용 가능하기에 보안상 적절하다는 것을 알고 있었고, Access Token의 만료 시간을 10분으로 설정했었다.
사실 RefreshToken은 처음 JWT를 구현했을 때 부터 만들어두긴 했었지만, 위에 직접 구상한 플로우에서 알 수 있듯이, 프론트와의 협업이 원활한 상태에서 구현이 가능했기 때문에 미루고 있었다.
해당 피드백을 받았을 때 이미 프론트와의 에러 핸들링이 완료된 상태였기 때문에, 우리는 더이상 지체하지 않고 바로 구현했다.
실제 구현 흐름은 아래와 같았다.
이를 통해 AccessToken의 만료 시간을 늘리지 않고 (보안), 로그인이 유지되도록 했다.(UX 개선)
CI / CD 구축 과정 내 고민했던 사항들..
- Github Actions 사용하는 이유
- Jenkins와 비교하여
- Github Actions 자체 클라우드가 서버가 존재하여 별도의 서버가 필요 없다.
- 모든 GitHub 이벤트에 대한 작업을 제공하여 형상관리 툴로 GitHub을 쓰는 우리에게 더욱 사용하기 용이했다.
- 도커를 선택한 이유
- Application의 개발과 배포가 편해진다.
- 개발 환경 설정이 완료된 이미지를 컨테이너로 띄우기만 하면 된다.
- Kernel을 포함하고 있지 않기 때문에 image 크기가 비교적 작다.
따라서 배포 속도가 매우 빨라지며 H/W 용량을 작게 차지한다.- Application의 독립성과 확장성이 높아진다.
- 각 도커 컨테이너는 독립된 형태로서 MSA 아키텍처를 구현하는 데 용이하다. 우리 팀은 항해 수료 후 현업에 가서도 사이드 프로젝트로 해당 서비스를 키워보기로 약속했다. :)
- 다수의 컨테이너를 운용하며 서비스의 부하를 분산시키는 (로드 밸런싱) 등의 장점을 가져갈 수 있다.
- Docker 컨테이너는 Host OS(EC2 Linux)의 시스템 자원을 관리해주는 Kernel을 공유하여 사용하기 때문에 image를 빌드할 경우 서버 환경을 구축함에 있어 차지하는 리소스 사용 공간이 작다.
- 프리티어 인스턴스에 서버를 배포한 우리 조로서는 이와같은 효율적인 리소스 사용이 가능한 도커의 장점이 매력적으로 다가왔다.
웹소켓 통신을 Stomp를 통해 편리하게 받을 수 있도록 했다.
Redis가 제공하는 기능들을 활용하고 (pub / sub 등 메세지 전송 기능), 채팅과 관련된 데이터를 저장하려고 한다. (입장 정보, 채팅 메세지 등)
결국 채팅 메세지 저장소를 MYSQL에서 Redis로 변경했고, 훨씬 원활한 환경에서 채팅할 수 있는 환경을 구축했다. 🎉🎉
HashOperations<String, Long, List<ChatMessage>>
자료구조로, 순서대로 hash key, key, hash value 형태로 저장했다.Redis를 저장소로 사용하기 위해서는 persistence를 관리해야 했는데 (사용자들이 과거 채팅 내역을 볼 수 있도록), 우리는 도커 명령을 통해 볼륨을 생성하고(호스트 볼륨) Redis를 실행할 때 해당 볼륨과 마운트하여 영속적으로 저장하는 RDB 방식으로 구현했다.
# 볼륨 리스트 확인하기
docker volume ls
# 볼륨 생성하기
docker volume create 볼륨명
# 볼륨 상세 조회하기
docker volume inspect 볼륨명
# redis 컨테이너 띄울 때 위 볼륨과 마운트한다!
우리 팀은 주어진 시간을 효율적으로 사용하기 위해 빠르게 기능을 구현하는 것을 1순위 목표로 했으며, 기능 구현 후 성능을 고려하며 리팩토링을 하기로 했다.
리팩토링 과정 중 가장 신경썼던 부분은 스프링이 지향하고, 지원해주는 싱글톤 패턴에 대한 고민이었다.
이에 대한 고민은 객체지향 코드를 작성하다보면 필수적으로 수반된다고 생각했다. 왜냐하면 객체지향적 코드를 작성한다면 하나의 클래스에 모든 변수와 메소드를 정의하는 것이 아니기 때문이다!
따라서 요청마다 메소드 내 (심각한 경우 반복문 내) 객체를 생성하는 코드가 있을 경우 극심한 메모리 소모를 초래할 것이라고 생각했고, 이를 해결하기 위해 static 메소드와 싱글톤 패턴을 적극 활용하기로 결정했다.
리팩토링 과정은 다음과 같다.
먼저 util 패키지를 생성하였고, 해당 패키지에 서비스 로직 상 공통적으로 (혹은 빈번하게) 사용되는 기능들을 관리하고자 했다.
- 이를 테면 유효성을 검토하는
Validator class
, 메일을 보내주는MailSender class
, 문자를 보내주는MessageSender class
, 두 좌표 간 거리를 구해주는DistanceCalculator
등이 있다.- 처음에는 모두 static 메소드로 구현을 했지만, 문득 스프링이 빈을 등록할 때 싱글톤 패턴을 보장한다는 내용이 떠올랐고, 바로 김영한님의 강의를 들이며 공부했던 부분들을 복습했다.
- 약 3주 간 많은 양의 코드를 작성하다 보니, 복습할 때 더욱 이해되는 부분이 많았다. 직접 겪고 난 뒤 스프링의 장점을 체감한 것 같다..😂
- 특히 @Configuration을 붙여야 CGLIB라는 친구가 바이트 코드를 조작하는 마술을 부려 (원본 객체가 아닌 복사본을 빈으로 등록해줌) 빈에 등록된 녀석을 중복하여 등록하지 않아 싱글톤 패턴을 보장해준다는 점..
- 프로젝트 코드를 짜며 마주했던 오류 중 하나다. @Bean을 등록하는데 클래스 상단에 @Configuration을 붙이지 않아 발생했던 오류가 있었다..!
- 복습을 마치고 우리는 거리를 계산하여 반환해주는 DistanceCalculator를 스프링(싱글톤) 컨테이너에 빈으로 등록하여 사용하기로 결정했다.
- @Component를 통해 앱이 실행될 경우 ComponentScanner에 의해 Bean으로 등록되는 방식을 사용했다.
- 그리고 실제 계산 값이 필요한 곳에서 private final 멤버변수로 지정하고 @RequiredArgsConstructor를 통해 DI 받아왔다.
- 이전에는 공부하고자 직접 생성자를 만들어 @Autowired를 붙여줬지만 이제는 코드를 조금 더 깔끔하게 유지하기 위해, 그리고 편리하게 사용하고자 @RequiredArgsConstructor를 사용했다.
- 이를 통해 조금 더 Spring이 지향하고 지원도 해주는 스프링(싱글톤) 컨테이너를 활용하는 올바른 방향성을 갖출 수 있었고, 맨 처음 걱정했던 메모리 소모에 대한 문제도 해결할 수 있었다. (우리 팀은 EC2 프리티어를 사용해서 메모리가 가장 민감한 사항이었다..😅)
👍