실전 프로젝트 사용 전략 / 기술 정리 (WIL_항해 9, 10주차 회고)

김형준·2022년 7월 17일
0

TIL&WIL

목록 보기
43/45
post-thumbnail

🔗 실전 프로젝트 BE GitHub 링크


1. 개발 전 수립한 전략 및 설계

실전 프로젝트의 개발을 시작하기 전에 전체적 구조의 설계와 협업 시 지켜야 할 약속들을 미리 정의했다.


1) 백엔드 서비스 아키텍처 구상

백엔드 서비스 아키텍처에 대해 미리 구상했다.

최종 아키텍처는 아래와 같다. (프론트 포함)


2) ERD 설계

  • 개발에 앞서 ERD를 설계했고, 이에 따라 Entity Class와 Repository를 (화면 공유를 통해 모든 팀원의 동의하에 ) 구현한 베이스 프로젝트를 푸시했고, 이를 클론하여 작업을 시작했다.
  • 처음에는 아래와 같이 채팅 관련 Entity도 MYSQL로 관리하기로 했고, 아래와 같이 ERD를 설계했다.

  • 그러나 채팅을 구현하며 채팅메시지는 Redis로 관리하기로 결정했고 아래와 같이 수정사항을 반영해두었다.

  • (굳이 ERD에 다루지 않아도 되지만 전체적인 Entity를 정리하기 위해 Non-Persistence 객체로 같이 그려두었다.)

3) 코드 컨벤션 수립

  • 코드 컨벤션을 수립하여 통일성을 부여했고 협업 시 서로의 코드를 이해하기 수월하며 유지 보수 관점에서도 모두가 모두의 코드를 수정할 수 있을 정도를 목표로 잡았다.

4) 형상관리 전략 수립

  • GitHub Flow 전략 (형상관리 전략)

    • 🔗 Git Flow 학습 및 정리한 글
    • 처음에 현업에서 보편적으로 사용되는 Git-Flow 전략에 대해 공부하고, 이를 적용하려 했으나 우리 백엔드 팀은 3명으로 구성된 소규모 팀이었기에 배보다 배꼽이 더 클 것 같다는 생각이 들었다.
    • 따라서 조금 더 간소화된 GitHub-Flow 전략을 채택했다.
    • 이를 통해 아래와 같은 방식으로 정리할 수 있다.
    1. Master Branch에서 개발을 시작.
    2. Master로 부터 Feature 단위로 Branch를 만듦.
    3. 기능을 세세한 개발 단위로 쪼개어 Commit을 수시로 함.
    4. 해당 기능이 모두 개발 완료되었다면 Push함.
    5. Pull Request를 통해 원격 저장소의 Master 브랜치로 Merge 요청.
    6. 팀원들 간의 피드백, 버그 리포팅을 통해 모든 팀원의 동의 하에 최종 Merge.

2. 개발 과정에서 사용한 기술


1) JWT를 이용한 인증 / 인가 방식 구현

🔗 JWT 파헤치기 1편
🔗 JWT 파헤치기 2편

  • 실전 프로젝트 이전, 미니 - 클론 프로젝트를 진행하며 틈틈히 공부해뒀던 JWT 인증 / 인가 방식을 채택하여 적용했다.

  • 간략히 설명하자면 Spring Security와 JWT를 활용한 인증 / 인가 방식으로

    • Spring Security가 제공하는 WebSecurityConfigurerAdapter를 통해 간편히 Config를 설정할 수 있었다.
      • 미리 정의한 JwtSecurityConfig 파일을 apply()하여 미리 구현한 TokenProvider를 등록했다.
      • OncePerRequestFilter를 상속받은 JwtFilter를 구현하여 각 요청마다 헤더에 담긴 Authorization 토큰을 통해 유효성을 판단하도록 했다. (위 JwtSecurityConfig에 addFilterBefore(JwtFilter, UsernamePasswordAuthenticationFilter.class)를 명시하여 필터 체인에 넣은 방식이다.)
      • TokenProvider에선 받아온 토큰을 통해 Spring이 제공하는 User 객체를 생성하여 principal로 사용했으며 이를 Authentication에 담아 SecurityContext에 담아 해당 쓰레드 내 인증 정보를 사용할 수 있도록 만들었다.
      • 위 방식은 SecurityContextHolder에 의해 가능한 것이며, 우리는 디폴트로 제공되는 LocalThread 전략을 따르고 있다. (각 쓰레드 마다 인증정보를 저장하고 본 요청에서는 저장한 인증정보를 다시 SecurityContextHolder.getAuthentication을 다운캐스팅하고 추출하여 사용하고 있다.)
    • antMatchers().permitAll()을 통해 회원가입, 로그인 등 인증 정보 체크가 이뤄지지 않는 곳을 명시해줬다.
    • 프론트 협업 시 가장 중요한 CorsConfigurationSource를 등록해줬다.
  • 위와 같은 방식으로 Spring Security와 JWT의 장점들을 조화롭게 녹여낸 것 같다. 😊


1-1) Refresh Token 활용 (보안, UX 개선)

  • 유저 테스트를 진행하며 받았던 피드백 중 로그인이 자주 풀려 불편하다는 의견이 있었다.

  • JWT의 AccessToken 만료 시간은 짧게 유지해야 탈취당해도 만료 시간 동안만 사용 가능하기에 보안상 적절하다는 것을 알고 있었고, Access Token의 만료 시간을 10분으로 설정했었다.

    • 사실 RefreshToken은 처음 JWT를 구현했을 때 부터 만들어두긴 했었지만, 위에 직접 구상한 플로우에서 알 수 있듯이, 프론트와의 협업이 원활한 상태에서 구현이 가능했기 때문에 미루고 있었다.

    • 해당 피드백을 받았을 때 이미 프론트와의 에러 핸들링이 완료된 상태였기 때문에, 우리는 더이상 지체하지 않고 바로 구현했다.

    • 실제 구현 흐름은 아래와 같았다.

      • 인증 관련 에러(401)가 발생하면 대부분 만료기간이 지난 케이스였고, 프론트는 다시 서버에 AccessToken과 RefreshToken을 보낸다.
      • 서버에서는 AccessToken을 통해 사용자의 정보를 알아내고, 이를 통해 DB에 저장된 RefreshToken을 가져와 프론트가 보낸 것과 비교한다.
      • 일치한다면 JWT를 재발급하고 다시 프론트에 토큰 정보를 넘겼다.
      • 프론트는 다시 받은 토큰 정보를 브라우저의 SessionStorage에 저장함으로써 로그인이 유지되도록 했다.
  • 이를 통해 AccessToken의 만료 시간을 늘리지 않고 (보안), 로그인이 유지되도록 했다.(UX 개선)


2) GitHub Actions + Docker로 CI/CD 구축

🔗 학습하며 정리했던 CI / CD 게시글 링크

  • 아무래도 CI/CD 파이프라인 구축은 생소한 개념이었기 때문에 미루게 되었는데,
  • 프론트와 통합 시 수동적으로 서버에 배포하는 것의 비효율성을 몸소 경험했다. (빌드, 클라우드 서버에 업로드, 클라우드 서버에서 실행의 과정을 몇십번 씩 수작업..😂)
  • 따라서 먼저 구축해두는 것이 가장 합리적일 것으로 판단되어 바로 팀원과 머리를 맞대며 구축했다.

CI / CD 구축 과정 내 고민했던 사항들..

  • Github Actions 사용하는 이유
    • Jenkins와 비교하여
      • Github Actions 자체 클라우드가 서버가 존재하여 별도의 서버가 필요 없다.
      • 모든 GitHub 이벤트에 대한 작업을 제공하여 형상관리 툴로 GitHub을 쓰는 우리에게 더욱 사용하기 용이했다.
  • 도커를 선택한 이유
    • Application의 개발과 배포가 편해진다.
      • 개발 환경 설정이 완료된 이미지를 컨테이너로 띄우기만 하면 된다.
      • Kernel을 포함하고 있지 않기 때문에 image 크기가 비교적 작다.
        따라서 배포 속도가 매우 빨라지며 H/W 용량을 작게 차지한다.
    • Application의 독립성과 확장성이 높아진다.
      • 각 도커 컨테이너는 독립된 형태로서 MSA 아키텍처를 구현하는 데 용이하다. 우리 팀은 항해 수료 후 현업에 가서도 사이드 프로젝트로 해당 서비스를 키워보기로 약속했다. :)
      • 다수의 컨테이너를 운용하며 서비스의 부하를 분산시키는 (로드 밸런싱) 등의 장점을 가져갈 수 있다.
    • Docker 컨테이너는 Host OS(EC2 Linux)의 시스템 자원을 관리해주는 Kernel을 공유하여 사용하기 때문에 image를 빌드할 경우 서버 환경을 구축함에 있어 차지하는 리소스 사용 공간이 작다.
      • 프리티어 인스턴스에 서버를 배포한 우리 조로서는 이와같은 효율적인 리소스 사용이 가능한 도커의 장점이 매력적으로 다가왔다.


3) WebSocket을 통한 채팅 서비스 구현 (진행중 완료!)

  • 웹소켓 통신을 Stomp를 통해 편리하게 받을 수 있도록 했다.

  • Redis가 제공하는 기능들을 활용하고 (pub / sub 등 메세지 전송 기능), 채팅과 관련된 데이터를 저장하려고 한다. (입장 정보, 채팅 메세지 등)

    • Stomp의 특징인 MessageBroker, Pub/Sub구조, CONNECT, DISCONNET, SUBSCRIBE와 같은 명령을 사용한 메시지 구분을 통해 StompHandler를 구현했으며 prsend() 오버라이드 메소드를 통해 각 상황 별 검증 및 기능 코드를 추가하여 관리했다.

현재 채팅 메세지를 MYSQL에 저장하는 방식으로 구현까지 완료되었으나.. TPS를 고려하면 채팅 메세지가 조회되고 저장되는 쿼리들이 많이 발생할 경우 서버에 걸릴 부하가 걱정되어 Redis에 저장하는 방식으로 개선하기로 합의했고, 다음주 월요일 (7/18) 부터 공부 후 적용해볼 예정이다.

  • 결국 채팅 메세지 저장소를 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 컨테이너 띄울 때 위 볼륨과 마운트한다!

  • 위와 같이 DockerHub 에서 pull 받은 Redis의 Cli에 접속하여 저장된 정보들을 관리할 수 있다.
    • 또한 Redis 컨테이너를 stop하고 다시 시작해도 로컬의 호스트 볼륨과 연결하기에 기존의 데이터가 잘 보존되어있음을 확인했다.
  • 추가적으로 외부 저장소에 스냅샷을 저장하여 백업 관리도 가능하다고 하여 추후 도전 사항에 넣어두었다.

4) 싱글톤 패턴 활용 (리팩토링)

  • 우리 팀은 주어진 시간을 효율적으로 사용하기 위해 빠르게 기능을 구현하는 것을 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 프리티어를 사용해서 메모리가 가장 민감한 사항이었다..😅)
  • 아래는 우리의 디렉토리 구조 :)


profile
BackEnd Developer

1개의 댓글

comment-user-thumbnail
2022년 7월 17일

👍

답글 달기