Docker는 개발 환경을 설정하고 배포를 간소화하는 데 유용한 도구입니다. 하지만 Docker 이미지가 비대해지고 빌드 시간이 길어질 경우, 비용과 성능 측면에서 문제가 발생할 수 있습니다. 저희 팀도 Docker 개발 환경을 설정한 후, micro 서버에서 빌드를 진행했을 때 서버가 터지는 상황을 겪었습니다. 확인 결과 서버 Docker 이미지 크기가 1.468GB로 지나치게 컸습니다. 이를 해결하기 위해 이미지 크기와 빌드 시간을 최적화하여 비용과 성능을 개선하고자 경량화 작업을 진행했습니다.

Docker 이미지는 베이스 이미지의 선택에 따라 크기와 성능이 크게 달라질 수 있습니다. Node.js의 경우 다음과 같은 베이스 이미지를 사용할 수 있습니다:

  1. node:<version>
  2. node:<version>-slim
  3. node:<version>-alpine

저희 프로젝트에서 bcrypt 라이브러리를 사용하고 있어, musl 관련 추가 설정이 필요한 alpine 이미지는 빌드 시간이 더 길었습니다. 따라서 안정성과 빌드 시간을 고려해 node:18-slim을 최종적으로 선택했습니다.

멀티 스테이지 빌드는 Dockerfile에서 여러 개의 FROM 명령을 사용해, 빌드 과정에서 불필요한 파일과 의존성을 최종 이미지에 포함하지 않는 방식입니다. 빌드 환경과 실행 환경을 분리하여 최종 이미지를 경량화할 수 있습니다. 빌드 환경에서는 의존성 설치 및 소스 코드 빌드 진행 또는 불필요한 캐시 데이터 제거 (npm cache clean --force)를 진행했고 실행 환경에서는 빌드된 결과물(dist, node_modules, env 파일 등)만 복사하여 최종 이미지 생성했습니다. 결과적으로 멀티 스테이지 빌드 적용 후, 최종 이미지는 483.7MB로 줄어들어 약 87.1MB 감소를 달성했습니다. 하지만 빌드 시간이 평균 3분으로 늘어나는 문제가 있었습니다.

Docker는 레이어 캐싱을 통해 빌드 시간을 단축할 수 있습니다. 각 레이어에서 변경 사항이 없다면 기존 캐시를 재사용하지만, 한 레이어라도 변경되면 이후 모든 레이어가 다시 빌드됩니다. 따라서 변경이 잦은 레이어를 뒤로 배치했습니다. 저희 서버 이미지는 두 파일을 실행시키고 있는데 그 중 feed-crawler 파일은 리팩토링하면서 거의 변경이 없을 파일이기 때문에 Dockerfile 상단에 배치하고 server 관련 파일은 변경이 잦으므로 하단에 배치해서 캐싱을 많이 이용하여 빌드 시간을 단축하도록 했습니다. 따라서 멀티 스테이지를 이용한 방법들을 캐시를 활용해서 빌드를 했을 경우 1분이 넘어갔지만 이 방법을 통해 빌드를 했을 경우 26초 정도 걸리는 것을 확인할 수 있었습니다.

결론적으로 저희 프로젝트는 slim 베이스 이미지와 불필요한 캐시 데이터 제거를 이용하여 docker 이미지를 1.468GB에서 527.1MB로 약 64%감소시켰고 캐싱을 이용한 빌드로 빌드 시간을 약 26초 정도로 감소시켜 비용과 성능을 개선할 수 있었습니다.

결론적으로 리팩토링 기간동안 저희 팀은 목표로 했던 데이터 처리 최적화를 위해 검색, 피드 조회, 무한 스크롤 조회 쿼리와 docker 개발 환경을 최적화하였고 데이터 구조를 계층화하였습니다. 그리고 사용자 경험 강화를 위해 메인페이지 api 요청 응답 속도, 폼 데이터 지속성, Cursor 기반 페이지네이션으로 검색 기능을 개선을 하였습니다.