백엔드 개발 포트폴리오 | 토론 진행 서비스

내일배움캠프 4기 웹 개발 과정 Spring 트랙 수료생 최종 프로젝트 ‘우리 모두 철학자’를 소개합니다.
Aug 17, 2023
백엔드 개발 포트폴리오 | 토론 진행 서비스

💎 프로젝트 소개

 

궁금한건 못참아!

일상에서 비롯된 우리들의 호기심을 자극하는 요소들을 바탕으로 토론을 진행할 수 있도록 도와주는 서비스입니다.

🛠️ Architecture ( 아키텍쳐 ) & Service Flow ( 서비스 플로우 )

 
                                                                 Service Architecture ( 서비스 아키텍쳐 )
Service Architecture ( 서비스 아키텍쳐 )
 
                                                                         Service flow ( 서비스 플로우 )
Service flow ( 서비스 플로우 )

🛠️ 기술적 의사 결정에 따른 주요 기술

“ 기술의 선택은 객관적인 근거를 기반으로 적절한 Trade off을 지양합니다 “

                                                                                  서비스 최종 적용 기술
서비스 최종 적용 기술

CI / CD 구축

이번 프로젝트에서는 CI / CD 환경을 구성해서 빌드와 배포의 자동화를 적용해보자 했습니다. 이러는 회의 과정에서 나온 여러 의견에서 나온 기술들은 다음과 같았습니다.
  • GitHub Action ( 깃허브 액션 )
  • Jenkins ( 젠킨스 )
  • Bamboo
먼저 깃허브 액션과 젠킨스를 비교 결과는 다음과 같습니다.
Jenkins ( 젠킨스 )
GitHub Action ( 깃허브 액션 )
서버가 필요하나요?
추가적인 서버 설치 필요
깃허브 클라우드가 있어서 별도의 설치가 필요 없음
시간적 측면
동기 방식의 배포 ( 시간 소요 많음)
비 동기 식 배포
추가적인 리소스
호환성 이슈로 인한 도커의 필수 사용
모든 환경과 호환
레퍼런스
많음
젠킨스에 비해 적음
캐싱 관련
캐싱 플러그인 사용 가능
자체 캐싱 메커니즘 작성 ( 추가 작업 필요)
난이도 및 시간
추가적인 작업이 많이 필요하며 난이도가 깃허브 액션과 비교했을 때, 더 높다고 판단
젠킨스에 비해 낮은 난이도와 현 상황에서 가장 큰 효율을 낼 것이라 판단해서 깃 허브 액션 채택
Bamboo 같은 경우엔 쉽고 직관적인 UI를 가지고 있으나, 추가적인 비용이 발생하고, 복잡한 개념을 이해하려면 약간의 시간이 필요하다. 금전적으로 추가적 비용이 발생하기에 가장 먼저 제외 그래서 저희 서비스에서는 깃허브 액션을 채택했습니다.
 

로그인 방식의 선택 ( JWT vs Session )

사용자의 정보는 중요합니다. 그래서 Spring Security와 같은 라이브러리를 이용해서 사용자의 개인 정보를 방어할 수 있어야 하는데, 로그인 방식을 선택하는 중 JWT 방식과 Session의 선택지가 있었습니다.
Session vs JWT
JWT ( Token )
세션 ( Session )
자원의 크기
세션에 비해 크다
토큰에 비해 작음
안정성
웹 브라우저의 스토리지나 쿠키 등에 저장하기에 노출될 가능성이 크다. 사용자의 정보를 담아서 토큰을 제작해야하고, 탈취가 된다면 막을 방법이 없다.
서버측에서 저장 관리하니까 상대적으로 상태를 유지하기 유리하다. 탈취되더라도 세션 ID만 지워버리거나 유저의 정보를 담아서 전송할 필요가 없다.
확장성
토큰 발행을 시간으로 하기에, 유효하면 어디에서든 사용이 가능하며 추가 서버 없이 인증 및 인가가 필요 없음
서버에 과부하가 걸리는 것을 생각해서 서버를 여러 대 둬야하는데, 각 서버마다 세션의 정보를 가지고 있어야 한다. 그래서 세션 정보가 없는 다른 서버에 접속할 때 마다 계속 로그인을 해야하는 번거로움이 발생
그래서 저희 서비스에서는 JWT 토큰을 이용해서 로그인 기능을 구현했으며, 보안성을 조금 더 강화하기 위해 짧은 시간의 Access Token과, 토큰 재발행을 위해 RefreshToken을 사용하기로 선택했습니다.

Redis의 사용과 목적

 
앞서 언급한 로그인 방식의 Next Step으로 선정한 기술인 “Redis” JWT 인증 및 인가 방식의 채택으로 인한 Refresh Token의 보관이 필요해짐에 따라 어느 장소에 RT를 저장할 지에 관한 리스트 업
 
  • RDB를 이용한 컬럼 핸들링 (Hibernate)
  • Redis
  • EhCache
다름과 같은 선택지에서 RDB 컬럼을 통한 핸들링의 경우 데이터 수명 관리를 위한 스케쥴러 구현 등의 추가 로직이 발생할 것이며, Access Token의 발행시간이 짧음에 따라서 계속 Refresh Token을 확인하게 될텐데, 계속 DB에 접근한다는 점에서 효율적이지 못할 것이라는 생각 서버 어플리케이션과 라이프사이클을 같이하며 Spring에서 간단하게 사용할 수 있는 EhCaches key-value로 구성되어 있으며 In-Memory 방식으로 빠르게 관리 가능한 데이터 스토리지인 Redis는 웃헌 빠른 액세스 속도로 로그인 시 병목현상이 발생하지 않는다는 점과, 가장 큰 장점인 빠른 액세스 속도와 휘발성이라는 특징으로 인해서 많은 개발자들이 Redis에 Refresh Token을 저장해서 사용하는 점과 로직 최적화와 다양한 데이터 구조를 담을 수 있다는 장점에서 Redis를 선택하였습니다.

SSE Emitter

notion image

배포 방식

프로젝트를 맨 처음 기획하고 제작함에 있어 최종 목표는 “배포” 어떠한 방식을 사용해서 배포를 진행할 것이냐에 대한 여러 고민과 그 속에서 나온 아키텍쳐 Docker를 이용해서 어떤 환경이든 일괄성 유지 및 컨테이너 기술을 이용해서 더욱 다양한 경험을 할 수있다고 생각했으며, AWS EC2를 사용함으로써 하드웨어에 선 투자할 필요없이 다양한 레퍼런스들을 활용해서 빠르게 애플리케이션을 개발, 배포가 가능하다는 점 또한 내가 추가적으로 관리하지 않고 AWS의 설정을 이용해 쉽고 편하게 프로젝트를 관리할 수있다는 점에서 배포 방식을 선택

Image Resize

자바 진영에서 제공해주는 이미지 관련 라이브러리 - java.awt.Graphics2D - image.getScaledInstance() - Imagescalr - Thumbnailator - Marvin ☝️ 저는 이번 이미지 리사이징에서 Marvin 라이브러리를 사용했습니다. 그 이유는 Marvin 라이브러리가 다른 라이브러리 대비 시간이 오래 걸리지 않으며, 원본 대비 결과물이 많이 깨지지 않는 다는 점에서 Marvin 라이브러리를 선택 ✌️ 또한 AWS CloudFront와 Lamba@Edge를 이용해서 리사이징 가능하지만, 함수 배포 및 코드 작성, 관리 포인트가 많이 늘어날 것으로 예상되어서 Spring boot 내부에서 리사이징을 적용했습니다. 현 시점에서 가장 빠르게 적용이 가능하고, 추가적인 작업이 발생하지 않을 것으로 예상해서 서비스 내부에서 Image Resize 적용 그러나 뚜렷한 한계점과 트러블슈팅 발생
 

🛠️ 트러블 슈팅


 
Image Resizing ( 이미지 리사이징 ) 을 이용한 성능 개선과 한계점
🧐 왜 이미지 리사이징이 필요할까요?
                                                                        페이지 로딩 시간에 따른 반송률
페이지 로딩 시간에 따른 반송률
웹에서 가장 많은 리소스를 차지하는 이미지!
웹에서 가장 많은 리소스를 차지하는 이미지!
이러한 경험과 조사에서 시작된 이미지 리사이징! 어떻게 하면 웹 페이지 속도를 개선하고 사용자에게 속도와 관련해서 만족감을 줄 수 있을까?
페이지 속도를 개선하는 방안에는 여러 가지가 있습니다. 1. 이미지 리사이징 2. 코드의 리펙토링과 압축 3. 브라우저 캐싱 활성화 4. 호스팅 업그레이드
 
이러한 여러 대안에서 가장 쉽게 접근이 가능하고 다양한 경험이 가능하다고 판단한 이미지 리사이징의 선택 👌
또한 서비스에서 채택한 방식 Soft Delete Soft Delete 방식은 추후 데이터를 사용할 일이 있거나, 신중하게 데이터를 삭제하기 위해서 데이터를 바로 삭제하지 않고, 조건에 따라서 특정 상태값으로 변환 후 데이터를 처리하는 방식! 이 방식의 가장 큰 단점 데이터의 적재” Soft Delete 방식을 채택함으로써 삭제 처리 된 데이터는 일정 기간동안 사라지지않고 계속 적재될 것인데, 이러한 데이터들이 많아지면 한정된 리소스에서 낭비가 심할 것이라고 판단했고, 이럴수록 데이터의 압축이 필요하다고 느꼈습니다.
  • 한정된 DB 용량 ( AWS 프리티어 )
  • 이미지 크기의 일관성
  • 페이지 렌더링 속도 개선
 
이미지 리사이징을 함으로서 얻을 수 있는 장점은 다음과 같습니다. 1. 기존의 이미지 사이즈를 설정값에 맞춰서 줄임으로써 용량을 많이 줄일 수 있습니다. ( 한정된 DB 용량의 극복) 2. 일괄된 이미지 크기를 통해 최적의 사용자 경험을 제공할 수 있으며, 또한 클라이언트 단에서 번거롭게 데이터를 핸들링 할 필요가 없습니다. ( 페이지 로딩 시간을 개선함으로서 사용자의 편의성 개선 및 백엔드에서 이미지를 리사이징해서 저장함으로써 프론트에서 추가적인 작업이 불 필요 ) 3. 웹 페이지 바이트를 절약함으로써 사이트의 성능을 개선할 수 있다. ( 페이지 로딩 속도 개선 )
                                                           리사이징 전 이미지 해상도와 파일 용량
리사이징 전 이미지 해상도와 파일 용량
                                                            리사이징 후 S3 파일서버에서 파일 저장 확인
리사이징 후 S3 파일서버에서 파일 저장 확인
                                                        리사이징 후 해상도와 파일 용량
리사이징 후 해상도와 파일 용량
 
원본(2.2M) 파일 대비 파일크기(55.2KB) 약 75% 감소를 확인할 수 있었습니다. 파일의 크기에 따라 줄어드는 비율은 제각각이겠지만, 파일의 용량이 줄어든다에 중점을 둔다면 한정된 리소스에서 많은 장점으로 작용할 것입니다.
notion image
Java 진영에서는 다양한 이미지 리사이징 라이브러리를 제공하는데, 제공하는 라이브러리는 다음과 같습니다. - java.awt.Graphics2D - image.getScaledInstance() - Imagescalr - Thumbnailator - Marvin ☝️ 우선 저는 이번 이미지 리사이징에서 Marvin 라이브러리를 사용했습니다. 그 이유는 Marvin 라이브러리가 다른 라이브러리 대비 시간이 오래 걸리지 않으며, 원본 대비 결과물이 많이 깨지지 않는 다는 점에서 Marvin 라이브러리를 택했습니다. ✌️ 또한 AWS CloudFront와 Lamba@Edge를 이용해서 리사이징 가능하지만, 함수 배포 및 코드 작성, 관리 포인트가 많이 늘어날 것으로 예상되어서 Spring boot 내부에서 리사이징을 적용했습니다. 한계점
  1. 이미지 리사이징 시 여전히 속도가 느리다. 해당 서비스에서 구현한 로직은 받아온 MultipartFiles 에 대해서 리사이즈를 진행하게 되는데, 이 때 BufferedImage를 MarvinImage 객체로 만들며, 이 객체를 클론 후에 Scale 객체에 의해서 이미지 파일의 리사이징을 진행하고, 생성된 BufferedImage를 다시 Multipartfile로 변환하는 일련의 작업들이 생각보다 시간이 너무 소요됐습니다. 서버의 부하를 줄이고 DB 리소스를 줄여서 사용자의 만족도를 개선하고자 하는 긍정적인 측면에서 시작되었지만, DB 리소스는 줄이지만 서버의 부하를 줄이지 못했다로 귀결됩니다. 게시글 작성시 속도를 측정하면 현저히 느려짐을 육안으로도 확인할 수 있었습니다. ( 이미지가 조금 크기가 있다면 시간이 오래걸립니다. test 조건 : image size : 2.2 mb )
                                    동일한 환경에서 기존의 이미지 업로드  vs 리사이징 후 이미지 업로드


이에 대한 제가 생각한 대안책은 다음과 같습니다.

AWS Lambda, AWS CloufFront, S3 pre-signed URL , 서버 분리 등을 이용하자. 입니다.

대안책에 관한 이유는 먼저 저희 서비스 로직의 흐름을 보면 다음과 같습니다.
동일한 환경에서 기존의 이미지 업로드 vs 리사이징 후 이미지 업로드 이에 대한 제가 생각한 대안책은 다음과 같습니다. AWS Lambda, AWS CloufFront, S3 pre-signed URL , 서버 분리 등을 이용하자. 입니다. 대안책에 관한 이유는 먼저 저희 서비스 로직의 흐름을 보면 다음과 같습니다.
notion image
클라이언트가 서버에게 업로드를 요청, 서버에서는 이미지 리사이징 및 각종 처리를 거쳐 S3에 이미지를 저장하고 URL을 클라이언트에 반환하는 과정을 거치게 됩니다. 이와 같은 과정에서 첫 번째로 서버가 필요하냐에 관한 의문점입니다. 중간에 서버를 둔다면, 먼저 서버에 부하가 생길 위험이 있으며 대역폭 증가와 같은 문제가 발생할 여지가 생깁니다. 그래서 파일 업로드 때문에 서버가 다운될 위험이 존재합니다 (서버에 대한 부담이 증가) 그럼 어떻게 해결할 것인가요?
notion image
그럼 사용자가 직접 S3에 접근해서 파일을 올리면 되는거 아니야? 라는 의문을 가질 수 있는데, 이 때 사용자는 웹 서버를 거치지 않아서 추가적인 인증과 관련된 작업이 필요합니다. 그래서 사용자는 S3에 접근할 권할을 갖게 되는데, 만약에 이 권한을 특수한 경로에 의해서 악의적으로 탈취당한다면?? 또한 문제가 생길 것입니다. 이러한 한계점을 극복한 것이 AWS에서 제공하는 pre-signed URL인데 JWT와 비슷하게 만료 시간을 부여해서 URL의 권한을 검증하게 됩니다. 어느정도 악의적인 루트에 의한 방어는 되지만 여전히 만료시간이 지나기 전까진 몇번이든 사용할 수 있다는 단점이 존재합니다. 이렇게 서버를 거치지 않고 클라이언트에서 바로 S3로 업로드 함으로써 서버에서는 추가적인 작업이 이루어지지 않아서 상대적으로 하나의 기능에만 집중할 수 있는 장점이 생깁니다. 이때는 AWS Lambda와 같은 서벌리스 서버를 사용하거나, 별도의 서버를 직접관리해도 된다. ( 운영서버와 이미지 서버를 분리 ) 이미지 전처리와 업로드는 서버에 많은 부하를 주기에, 이 때 트래픽이 몰리게 된다면 그 이후는 상상하기도 싫다. 또 이 과정에서 최소한의 이미지 용량만큼 메모리를 차지하게 되는데 이러한 작업을 AWS Lambda와 같은 서비스를 사용해서 구성하는 쪽이 유리하다. 그래서 적절하게 AWS S3 pre-signed URL, AWS Lambda의 조합을 사용해서 구성하는 쪽이 좋아 보인다. 물론 이도 제 생각이고, 결국 개발자는 적절한 선택을 해야합니다!
 
  1. 해상도가 5000 X n 이상일경우 java.lang.OutOfMemoryError: java heap space 에러 발생 테스트 데이터는 총 3개입니다. 1) 2.2 MB ( 4000 x 3000 ) 2) 4.8 MB ( 4096 x 2692 ) 3) 1.3 MB ( 5248 x 4000 ) 서비스 내부에서 이미지 파일 갯수 제한과 용량 제한을 두고 있습니다. 그러나 만약에 어렇게 해상도가 크거나 파일의 크기가 큰 데이터가 들어오면 어떤가?에서 출발한 테스트에서 결과는 다음과 같습니다. 3번을 제외한 1, 2번은 테스트를 통과했지만, 3번 파일 업로드 시 java.lang.outofmemoryerror: java heap space 에러가 발생했습니다. Spring 프로젝트의 heap 최소와 최대 공간을 늘려주었지만 여전히 해결하지 못했습니다. 해상도의 가로폭이 5000이 넘은 데이터가 들어온다면, 추가적인 예외처리를 진행하는 방식과 Scale 객체로 리사이즈를 진행하지 말고 Graphics 객체로 코드를 수정 후 추가적인 테스트가 필요합니다.
 
Mention 조회 기능의 성능 개선
notion image
쓰레드 종료 시점의 스케쥴링 및 쿼리 최적화
notion image
 

Share article
Subscribe to our newsletter

내일배움캠프 블로그