tl;dr
- 제일 하단에 내용을 요약해둔 KPT를 작성해두었다.
여러 팀 프로젝트를 진행하면서 백엔드 개발과 데브옵스를 구성한 경험이 있으나 실제로 클라우드 서비스를 직접 활용해 본 경험은 없었고, 또 스스로 백엔드 개발에 기여했던 경험이 많이 부족하다고 생각했다. 그래서 이번 기회에 프로젝트를 제대로 개발해보고, 배포까지 수행해보기로 했다.
설계 및 개발
gist, pastebin과 같은 서비스를 만들어보기로 했다. 여기에 임베딩 기능, 코드 하이라이트 기능을 추가로 만들어보기로 결정했다.
이미 privatebin, microbean과 같은 오픈소스가 존재하나, 임베드 기능을 제공하고 있지는 않아 만들어보기로 했다.
백엔드 개발
백엔드 개발은 스프링부트를 활용해보기로 했다. 새로이 익히고 활용할 기능은 많이 없으나, 클라우드 배포를 새로 익히는 만큼 가장 잘 다룰 수 있는 도구를 채택하기로 했다. 아래는 백엔드를 개발하면서 느꼈던 고민과 문제 등을 작성했다.
Spring WebFlux
스프링부트를 비동기 프레임워크로 탈바꿈시켜주는 프레임워크이다.
DB 단에서도 R2DBC를 활용해야 하는 등의 제약이 있는 것으로 알고 있고, 제대로 알지 못하는 부분이 많아 함부로 사용하지 않기로 했다. 또한 후술할 클라우드 서비스인 GCP Run을 활용하여 배포를 진행할 것이므로, 리액티브 스택을 추가할 이유가 없다고 판단하여 적용하지 않았다.
DTO와 Mapper
컨트롤러가 받는 데이터와 반환하는 데이터를 DTO로 구성했다.
서비스가 컨트롤러의 DTO를 파라미터로 받고, 또 반환하는 형태로 구성했다. 다만 서비스가 DTO를 입력받고, 또 반환하는 것이 올바른 구현이 맞는지 의문스러웠다. 구현 당시 컨트롤러에 로직을 지나치게 포함하지 않으려는 의도였으나, 현재는 DTO 수정 시 서비스 단을 함께 수정해야 하는 문제가 있음을 파악했다.
서비스 단에서 DTO와 엔티티 사이를 변환해 줄 매퍼가 필요하다고 느끼고 관련 내용들을 찾아보던 중 매퍼 라이브러리들을 알게 되었다. 당시 발견한 매퍼 라이브러리들 중 `Mapstruct`라고 불리는 라이브러리가 괜찮은 퍼포먼스를 보여주고 있음을 확인하였고, 이를 활용하여 DTO와 엔티티 사이를 변환해주는 매퍼를 작성했다.
다만 DTO와 엔티티는 항상 1:1 대응하지 않고, 경우에 따라 DTO에 추가 데이터, 또는 더 적은 데이터를 요구했다. 또한 매퍼 작성 시 데이터가 정상적으로 변환이 되는지 확인하는 과정이 필요했고, 매퍼 코드 수정 시 항상 재컴파일 작업이 필요했다. 그래서 매퍼를 직접 작성하는 비용이, 잘못된 매퍼로 발생한 문제를 파악하고 해결하는 비용보다 훨씬 싸다고 판단하여, 작성했던 Mapstruct 코드를 모두 지우고 lombok의 Builder를 활용하여 객체 변환을 진행하는 코드를 작성했다.
테스트
스프링부트에는 레이어 별 테스트 기능을 제공하고 있다.
당시 컨트롤러 레이어의 테스트 코드를 작성했으나, 내부 로직 자체가 단순해서 불필요한 테스트였다고 판단했다. 로직이 복잡하거나, 신뢰성을 확보해야 하는 객체 등에 한해서 테스트 코드를 작성하는 것이 좋았을 것이라고 생각한다.
ProblemDetails
스프링 프레임워크에서 rfc9457 규격에 맞게 지원하고 있는 오류 응답이다. ControllerAdvice에서 ResponseEntityExceptionHandler를 상속하여 스프링 프레임워크 내부의 모든 예외들에 대해 ProblemDetails를 처리하는 로직을 활용할 수 있다.
당시 해당 기능을 활용하기 위해 관련 내용을 찾아보았는데, 오류에 관한 정보를 제공하는 uri를 제공해주어야 하고, 별도의 에러코드를 제공하기 위해서는 결국 ProblemDetails를 상속하는 임의의 객체를 작성할 필요가 있었다. 그러기 위해서 들여야 하는 품이 꽤 크다고 생각하였고, 정말로 필요한 기능은 아니라고 판단하여 활용하지 않기로 결정했다.
프론트엔드 개발
리액트 프레임워크를 활용해서 개발을 진행했다. 당시 관련 내용을 공부한 경험이 있어 무난하게 개발 진행이 가능했다. 공식 문서에서 create-react-app 대신 create-next-app 또는 create-react-router를 사용할 것을 권장하고 있어 create-react-router를 활용하여 개발을 진행했다.
fetch와 enum
개발을 진행하던 도중, 백엔드로부터 fetch를 통해 가져온 데이터를 인터페이스에 맞게 변환해주는 과정이 필요했다. 이 때, enum 속성으로 변환해야 하는 필드가 존재했는데, 해당 로직을 구현하는 부분이 번거롭다고 느꼈다. 직접 객체를 새로 생성해주거나, enum 객체를 순회하여 알맞은 enum을 반환하는 코드를 작성하여도 되나, 이를 직접 관리하는 경우 추후 데이터 구조 변경 시마다 직접 코드를 관리해주는 책임이 필요했다.
이 문제를 해결하기 위해 커뮤니티에서 해당 내용에 대해 질문하였고, zod라는 validation 라이브러리를 새로이 알게 되었다. 해당 라이브러리에서 enum을 검증하는 방법을 알아보면서 타입스크립트에서 enum을 사용하는 것을 권장하지 않는다는 사실을 새로이 알게 되었다. 해당 라이브러리의 parse 메소드를 통해 데이터 검증 및 파싱이 가능하여 이를 채용하기로 했다.
배포
Dockerfile
클라우드에 백엔드부터 배포를 진행하기로 했다. 우선 백엔드를 도커라이징하기로 결정했고, 최대한 가볍게 만들기 위해 google/distroless를 활용하여 이미지를 만들었다. 이는 리눅스에서 프로그램을 실행하기 위한 최소한의 기능만을 남긴 이미지로, alpine 이미지의 절반 수준의 크기를 가지고 있다.
graalvm을 활용하여 이미지를 빌드하는 것도 고려했으나, java17 버전 활용을 위해선 오라클 측에서 직접 설치하는 과정이 필요하였고, 이 과정이 번거롭다고 느껴 활용하지 않았다.
GCP
배포할 클라우드 플랫폼은 GCP로 결정하게 되었다.
GCP에는 GCP Run이라는 서비스가 존재하는데, 사용 시 잠깐 컨테이너 서버가 실행되고 이후 꺼지는 형식의 서비스이다. GCP Run Function과 유사하나, 함수만 실행 가능한 function과는 달리 컨테이너 자체를 등록하고 실행하는 작업이 가능하다. 그래서 스프링부트와 같은 서버들을 등록하고 사용이 가능하고, 실행 시마다 서버가 실행되는 방식이므로 저렴하게 서버를 사용할 수 있다.
Compute Engine, App Engine과 같은 기능도 후보에 있었으나, 요금 등의 면에서 Run을 활용하는 것이 훨씬 유리하다고 판단했다.
추후 AWS에도 App Runner라는 이름으로 비슷한 기능이 존재한다는 것을 알게 되었는데, 해당 서비스에선 사용 가능한 언어가 제한적이고 Secret 등을 따로 설정하는 것이 불가능하다고 들었다.
데이터베이스
Spring Cloud GCP를 활용하면 GCP에서 제공하는 기능들을 손쉽게 활용할 수 있다. 해당 기능은 초기에 스프링 팀과 구글에서 협업하여 개발하다가 추후 구글이 온전히 맡고 있는 것으로 알고 있다.
원래대로라면 GCP 내 리소스에 접근하기 위해 ADC 설정이 필요하나, GCP에서 제공하는 서버 내부에서 ADC 관련 정보가 환경변수 등으로 제공되고 있고, Spring Cloud GCP를 사용하면 내부적으로 해당 정보들을 불러오는 과정을 자동으로 수행하는 것으로 알고 있다.
해당 프레임워크에는 GCP SQL과 Firestore를 사용할 수 있는 기능을 제공하고 있다. 비용을 아끼기 위해선 Firestore를 사용하는 것이 옳으나, 당시 미리 작성해 둔 엔티티 및 리포지토리 코드가 존재했고, Firestore를 사용하기 위해 이를 수정하는 작업이 필요했다. 그래서 빠른 배포를 위해, 그리고 타 SQL 서버와의 호환성을 위해 GCP SQL을 사용하기로 결정했다.
VPC
SQL 서버 생성을 시도하던 중 VPC 설정이 가능하다는 것을 알게 되었다. GCP Run 측에서 내부 네트워크를 통해 SQL에 접근하기 위해선 Connector를 활용해야 했으나, 현재는 VPC Egress 등을 통해 직접 접근이 가능하다고 한다. GCP Run 인스턴스 생성 시, 연결할 GCP SQL 서버를 지정할 수 있었고, Spring Cloud GCP가 어느 정도 해당 작업을 수행해 줄 것이라고 판단하여 그대로 진행하기로 하였다.
Secret Manager
스프링부트 내부적으로 JWT 관리 작업을 위해 키 값을 세팅해주는 작업이 필요했는데, 컨테이너 내부에 키 값에 대한 정보가 들어가면 보안 문제가 발생할 것이 우려되었다. 이를 해결할 방안을 찾아보던 중 Secret Manager라는 것을 알게 되었고, GCP Run 등의 환경변수로 값을 주입할 수 있다는 것을 알게 되었다.
백엔드 코드 수정하기
GCP 배포를 위해 어떤 기능들이 필요한지 어느 정도 확인이 되었고, 이를 활용하기 위해 스프링부트 설정을 수정하는 작업을 진행했다.
우선 Spring Cloud GCP를 사용하기 위해 스프링부트의 버전을 다운그레이드했다. 2025년 6월 기준 가장 최근의 라이브러리가 스프링부트 3.4.x 버전까지 지원하고 있는데, 현재 프로젝트가 3.5.0 버전을 사용하고 있었다. 다운그레이드 이후 테스트 코드를 돌려보면서 발생한 문제가 없음을 확인했다.
이후 application.properties 등에서 JWT 관련 정보를 환경변수로 받도록 수정하고, 관련 내용들을 README에 작성해두었다. 이후 수정된 정보에 맞추어 테스트가 정상적으로 작동하도록 수정하는 작업을 거쳤다. main의 application.properties를 사용하는 contextLoad 테스트에서는 application-test.properties를 사용하도록 하여 GCP를 거치지 않고 H2를 사용하도록 수정했다.
배포 시도
이후 배포를 시도하면서 여러 문제를 맞닥뜨렸다.
GCP Run 인스턴스를 생성하기 위해서 GCP registry에 이미지를 등록하는 작업이 필요했고, 이를 위해서 gcloud 명령어를 활용해야 했다. 해당 문서의 도움을 받아 진행했다. 우선 프로젝트에서 레지스트리 기능을 활성화하고 레포지토리를 구성했다. 이후 `gcloud auth login`, `gcloud auth configure-docker asia-northeast3.pkg.dev` 명령어를 통해 인증을 수행하고, 레지스트리 규격에 맞게 태그를 달고 이미지를 푸시하는 과정을 거쳤다.
이후 SQL 서버를 생성하고, Secret Manager에 키 값을 설정하고, Run 인스턴스 생성을 수행했다. Run 인스턴스 생성 시 Secret Manager 접근 권한이 없어 값을 불러오지 못했다는 오류를 확인하고 해당 서비스 계정에 권한을 부여해주었다.
이후에도 Run 인스턴스 생성에 실패했음을 확인했다. 실패 로그를 읽어보면서, SQL 서버 연결 작업은 정상적으로 수행되었으나 Dialect 문제로 종료되었음을 확인하여 application.properties에 해당 설정을 진행하였고, 이후에도 SQL 서버에 입력한 DB 테이블이 없어 종료되었음을 확인하여 SQL 서버에 테이블을 추가해주었다. 이후 정상적으로 인스턴스가 생성되었음을 확인하였다.
이후 Postman 등을 통해 접근제어 없이 공개되어 있는 API에 요청을 보내고 응답을 받으면서, 문제 없이 서버가 작동하고 있는 것을 확인했다.
중간 회고
그동안 간단한 백엔드 서비스를 클라우드에 배포하는 것을 고려할 때마다 비용, 개발 측면에서 Lambda, Function 등을 활용하는 것이 유리하다고 느꼈고, 역량 향상에 도움이 되지 않을 것이라고 생각하여 늘 유야무야 됐는데, 이번에 배포까지 끝까지 작업하면서 경험을 쌓을 기회가 되었다.
서비스는 배포하는 것까지가 MVP임을 느꼈다. 이번에 백엔드 기능을 많이 만들어두고 나서 배포를 시도했는데, 문제가 발생했을 때 이를 파악하고 해결하기까지의 과정이 많을 것이라는 점이 예측이 되어서 심적으로 부담이 컸다. 조금씩 만들면서 배포하는 것이 훨씬 좋았을 것 같다. 실제로 배포 중 문제가 자주 발생했으나, 문제가 치명적이지 않고 전부 해결 가능한 문제여서 다행히 배포에 성공했다. 그 밖에도 코드를 미리 작성해버려서 firestore 등으로 마이그레이션 작업을 수행하기가 애매해지는 등 선택의 폭이 좁아지기도 했다.
WebFlux 등을 활용해보지 못한 건 아쉽긴 했다. 정말 대규모 트래픽을 관리해야 한다면 해당 작업을 수행하지 않을까 싶다. 배포한 Cloud Run 서비스를 확인해보니, 각 트래픽마다 인스턴스를 사용할 것이라는 생각과는 달리, 인스턴스 하나에 감당 가능한 트래픽을 계속 처리하는 방식이어서, 성능 향상을 위해 이를 적용해도 좋을 것 같다.
이제 배포 자동화를 위한 간단한 CI/CD 구성, 프론트엔드 배포, DNS 설정 등의 작업을 염두에 두고 있고, 추후 테라폼 작성, CDN 설정 등등의 작업을 고려하고 있다. 서비스 자체에 집중하지 못하고 기술적인 부분에 집중하고 있다는 느낌도 들어서, DNS 이후에는 서비스 자체를 개선하는 작업을 수행할 것 같다.
KPT
- Keep
- 백엔드를 개발했다.
- GCP 기능들을 익히고, 서비스를 배포했다.
- Problem
- 개발 이후 첫 배포까지 시간이 지나치게 오래 걸렸다.
- 배포한 백엔드에 대한 테스트가 이루어지지 않았다.
- 프론트엔드 코드가 완성되지 않았다.
- 회고 주기가 너무 길다.
- CORS 등의 시큐리티 관련 설정이 진행되지 않았다.
- Try
- CI/CD를 구성하여 배포를 자동화할 것이다.
- 프론트엔드를 최대한 간단하게 구성하고 배포할 것이다.
- JWT 기반 로그인 기능이 정상적으로 작동하는지 확인할 것이다.
- DNS를 설정할 것이다.
참고
'기록 > 회고' 카테고리의 다른 글
| Golang 기반 툴 개발부터 오픈소스에 기여하기까지 (2) | 2025.06.13 |
|---|