서버를 만들지 않겠다는 선택은 왜 자연스러웠는가

처음 Readium을 설계할 때 가장 먼저 세운 전제는 단순했다. 가능하면 서버를 만들지 않는다는 선택이었다. 이 선택은 기술적인 도전이라기보다 오히려 불필요한 복잡성을 제거하려는 의도에 가까웠다. 독서 기록이라는 문제를 바라보면, 대부분의 데이터는 사용자 개인에게 귀속된다. 어떤 책을 읽었는지, 언제 읽었는지, 어디까지 읽었는지는 외부와 공유될 필요가 없는 정보다. 이 관점에서 보면 서버는 기능을 확장하는 요소가 아니라, 오히려 관리해야 할 부담을 늘리는 존재처럼 보인다. 인증, 데이터 저장, 동기화, 백업, 보안까지 고려해야 하는 순간, 문제의 범위는 빠르게 커진다. 그래서 처음에는 이 모든 것을 의도적으로 배제하려 했다. 설계의 출발점 자체가 “서버를 만들지 않는다”였기 때문이다.

이 판단은 단순한 이상론이 아니라, 실제로 구현 가능한 구조 위에 서 있었다. 모바일 디바이스의 성능은 이미 충분히 강력했고, 로컬 데이터베이스를 활용하면 대부분의 상태를 기기 내부에서 관리할 수 있었다. 특히 SQLite 기반 구조는 안정성과 성능 면에서 이미 검증된 선택지였다. 네트워크가 없어도 동작하는 앱은 사용자 경험 측면에서도 분명한 장점이 있다. 연결이 끊겨도 기록은 남고, 입력은 막히지 않으며, 앱은 항상 동일하게 동작한다. 이런 특성은 독서라는 행위와도 잘 맞는다. 독서는 언제 어디서든 이루어지며, 네트워크 상태에 의존하지 않는 경험이 더 자연스럽다. 이 모든 조건이 맞아떨어지면서 “서버를 제거하는 것이 맞다”는 판단은 점점 더 확신으로 굳어졌다.

문제는 이 판단이 틀렸기 때문이 아니라, 너무 자연스러웠기 때문에 의심되지 않았다는 점이다. 서버를 만들지 않는 것이 아니라, 만들 필요가 없다고 믿게 되는 순간이 있었다. 이 차이는 작아 보이지만, 이후 설계가 붕괴되는 지점에서 결정적인 역할을 한다. 당시에는 이 선택이 오히려 더 정교한 설계라고 생각했다. 불필요한 레이어를 제거하고, 데이터의 소유권을 명확히 하며, 시스템을 단순하게 유지하는 방향이었기 때문이다. 그러나 이 단순함은 특정 조건이 유지될 때만 유효하다. 그 조건이 무엇인지 정확히 정의하지 않은 상태에서, “서버는 필요 없다”는 결론만 남겨두는 순간 설계는 이미 취약해진다.

이 시점에서 중요한 것은 무엇을 만들었는가가 아니라, 무엇을 제외했는가다. 서버를 제거한다는 선택은 단순히 컴포넌트를 줄이는 것이 아니라, 시스템이 해결해야 할 문제의 범위를 재정의하는 행위다. 하지만 그 재정의가 완전하지 않았다. 어떤 문제는 사라졌지만, 어떤 문제는 아직 등장하지 않았을 뿐이었다. 그리고 그 문제는 곧 나타나게 된다.

로컬 퍼스트 아키텍처가 실제로 해결한 것들

서버를 제거하는 대신 선택한 구조는 로컬 퍼스트 아키텍처였다. 이 구조는 단순히 데이터를 로컬에 저장하는 수준이 아니라, 시스템의 중심을 디바이스 내부로 이동시키는 방식이다. Readium의 핵심 데이터 구조는 전부 로컬 데이터베이스에 존재한다. 독서 상태, 세션 기록, 타임라인 이벤트까지 모두 기기 안에서 생성되고 저장된다. 네트워크는 선택적인 요소일 뿐, 필수적인 구성 요소가 아니다. 이 구조는 단순하지만 강력하다. 앱은 항상 동일한 상태를 유지하고, 외부 환경에 의해 기능이 제한되지 않는다.

이 구조가 실제로 효과적이라는 점은 구현 과정에서 반복적으로 확인되었다. 독서 세션을 시작하고 종료하는 과정은 네트워크 요청 없이도 완전히 처리된다. 사용자가 책을 읽다가 앱을 종료하더라도, 다음에 다시 열었을 때 상태는 그대로 복원된다. 타임라인 역시 서버와의 동기화 없이 로컬 이벤트만으로 구성된다. 이 모든 흐름은 지연 없이 즉시 반영되며, 사용자 입장에서는 훨씬 자연스러운 경험으로 이어진다. 네트워크 요청을 기다릴 필요가 없기 때문에 인터랙션은 끊기지 않는다. 이 시점에서는 로컬 퍼스트 전략이 단순한 선택이 아니라, 올바른 해답처럼 보였다.

특히 상태와 이벤트를 로컬에서 모두 관리한다는 점은 설계적으로도 중요한 의미를 가진다. 서버가 없는 구조에서는 데이터의 일관성을 유지하기 위해 복잡한 동기화 로직이 필요하지 않다. 충돌을 해결할 필요도 없고, 네트워크 실패를 고려한 재시도 전략도 필요 없다. 시스템은 훨씬 단순해지고, 문제의 범위도 줄어든다. 이 단순함은 개발 속도를 높이고, 유지보수 부담을 줄인다. 그래서 이 구조는 점점 더 강한 확신을 만들어냈다. “이 앱은 서버 없이도 충분히 완성될 수 있다”는 믿음이 자연스럽게 형성되었다.

하지만 이 구조가 해결한 것과 해결하지 못한 것을 구분하지 못한 순간, 문제가 시작된다. 로컬 퍼스트는 “데이터를 어디에 저장할 것인가”에 대한 답은 제공하지만, “데이터를 어디에서 가져올 것인가”에 대한 답은 제공하지 않는다. 이 차이는 초기에는 거의 드러나지 않는다. 앱이 이미 알고 있는 데이터만으로 동작할 때는 아무 문제가 없기 때문이다. 그러나 사용자가 새로운 데이터를 입력하려는 순간, 이 구조는 한계를 드러내기 시작한다.

이 시점에서 설계는 아직 무너지지 않았다. 오히려 완성에 가까워 보였다. 하지만 그 완성은 특정 조건 위에 서 있었다. 그 조건이 무엇인지 명확하게 정의되지 않았다는 점이 문제였다. 그리고 그 조건은 아주 단순한 사용자 행동 하나로 깨지게 된다.

첫 번째 균열: ‘책을 선택해야 한다’는 문제

독서 기록을 남기기 위해서는 반드시 선행되어야 하는 단계가 있다. 바로 어떤 책을 읽고 있는지를 선택하는 과정이다. 이 단계는 너무 당연해서 처음에는 설계의 일부로 인식되지 않는다. 사용자는 검색창에 책 제목을 입력하고, 목록에서 하나를 선택한다. 표지 이미지가 보이고, 저자와 출판사 정보가 함께 표시된다. 이 경험은 이미 수많은 서비스에서 반복되어 왔기 때문에, 별도의 고민 없이 받아들여진다. 문제는 이 익숙한 경험이 로컬 데이터만으로는 만들어질 수 없다는 점이다.

앱이 처음 실행되는 시점에는 아무런 책 데이터도 존재하지 않는다. 사용자가 입력하는 제목을 기준으로 어떤 결과를 보여주려면, 그에 해당하는 데이터가 어딘가에 있어야 한다. 그러나 로컬 데이터베이스에는 그 정보가 없다. 사용자가 이전에 읽은 책이라면 일부 정보가 남아 있을 수 있지만, 새로운 책을 검색하는 순간 이 구조는 아무런 도움을 주지 못한다. 이 지점에서 처음으로 설계의 공백이 드러난다. 로컬 퍼스트 구조는 이미 존재하는 데이터를 다루는 데에는 강력하지만, 존재하지 않는 데이터를 생성하거나 조회하는 데에는 아무런 답을 주지 않는다.

이 문제는 단순히 기능 하나를 추가하면 해결되는 수준이 아니다. 책 메타데이터는 방대한 양의 정보를 포함하고 있으며, 지속적으로 업데이트된다. 수백만 권의 책 정보를 직접 수집하고 관리하는 것은 개인 프로젝트의 범위를 벗어난다. 즉, 이 문제는 “어떻게 구현할 것인가”가 아니라 “누가 이 데이터를 가지고 있는가”의 문제다. 그리고 그 답은 명확하다. 이미 외부에 존재한다.

이 순간 설계의 초점이 바뀐다. 지금까지는 “어떻게 기록할 것인가”가 중심이었다면, 이제는 “어떤 데이터를 기반으로 기록을 시작할 것인가”가 중심이 된다. 이 변화는 작아 보이지만, 시스템의 경계를 바꾼다. 내부 데이터만으로 닫혀 있던 구조가, 외부 데이터에 의존하는 구조로 전환되는 지점이다. 이 전환이 바로 첫 번째 균열이다.

이 균열은 즉시 시스템을 무너뜨리지는 않는다. 오히려 자연스럽게 다음 선택을 유도한다. 외부 API를 사용하면 된다. 이미 존재하는 데이터베이스를 활용하면 문제는 간단히 해결된다. 이 판단은 틀리지 않다. 하지만 이 선택이 또 다른 문제를 만들어낸다는 사실은, 그 다음 단계에서야 드러나게 된다.

외부 Book API라는 현실과 마주하는 순간

책 메타데이터 문제를 인식한 이후의 선택지는 생각보다 단순했다. 이미 존재하는 데이터를 활용하는 것이 가장 현실적인 방법이었기 때문이다. Kakao Books API나 Google Books API 같은 서비스들은 방대한 책 데이터베이스를 이미 구축해 두고 있으며, 간단한 검색 요청만으로도 표지 이미지, 저자, 출판사, ISBN과 같은 정보를 반환해준다. 이 API들은 단순한 편의 기능이 아니라, 사실상 “데이터를 소유하지 않는 서비스가 선택할 수 있는 유일한 해법”에 가깝다. 개인이 직접 데이터를 수집하고 정제하며 업데이트하는 구조는 유지 비용 자체가 감당되지 않는다. 그래서 외부 API를 사용하는 것은 선택이 아니라 필수에 가까운 결정이었다.

이 시점에서는 여전히 설계가 크게 흔들리지 않는다. 로컬 퍼스트 구조는 그대로 유지되고, 외부 API는 단지 “데이터를 가져오는 도구”로만 추가된다. 모바일 앱에서 직접 API를 호출하고, 결과를 받아 화면에 표시하면 된다. 이 방식은 구현도 간단하고, 기존 구조와도 크게 충돌하지 않는다. 앱은 여전히 로컬 데이터 중심으로 동작하고, 외부 API는 단지 초기 입력을 돕는 역할만 수행한다. 이 단계에서는 오히려 전체 구조가 더 완성된 것처럼 보인다. 부족했던 퍼즐 조각이 하나 채워진 느낌에 가깝다.

하지만 이 판단에는 중요한 전제가 숨어 있다. 외부 API를 사용하는 구조가 안전하고 지속 가능하다는 가정이다. 이 가정은 겉으로 보기에는 아무 문제가 없어 보인다. 대부분의 샘플 코드나 튜토리얼도 모바일에서 직접 API를 호출하는 방식을 사용한다. 개발 초기에는 이 방식이 가장 빠르고 직관적이다. 실제로 구현도 어렵지 않다. HTTP 요청을 보내고 JSON 응답을 파싱하면 끝이다. 이 단순함이 오히려 문제를 가린다. 구조적인 위험이 존재함에도 불구하고, 구현이 쉽다는 이유로 그 위험이 보이지 않게 된다.

이 지점에서 설계는 아직 붕괴되지 않았다. 오히려 더 자연스럽게 이어지고 있었다. 로컬 퍼스트 구조 위에 외부 API를 얹는 방식은 직관적이고, 개발 속도도 빠르다. 하지만 이 구조는 하나의 전제를 전혀 검증하지 않은 상태로 받아들인다. 바로 “클라이언트에서 외부 API를 직접 호출해도 괜찮은가”라는 질문이다. 이 질문은 곧 문제의 중심으로 떠오르게 된다.

모바일에서 API를 직접 호출하는 구조의 문제

모바일 앱에서 외부 API를 직접 호출하는 구조는 처음에는 아무 문제도 없어 보인다. API 키를 발급받고, 요청 헤더에 포함시켜 호출하면 정상적으로 데이터가 반환된다. 하지만 이 구조는 기본적으로 하나의 취약점을 내포하고 있다. API 키가 클라이언트에 포함된다는 점이다. 이 키는 단순한 문자열이지만, 서비스 전체의 접근 권한을 의미한다. 그리고 이 문자열은 생각보다 쉽게 노출된다. APK 파일을 분석하거나, 네트워크 트래픽을 캡처하는 것만으로도 키를 추출하는 것은 어렵지 않다.

이 문제는 단순히 보안 취약점이라는 수준에서 끝나지 않는다. API 키가 노출된다는 것은, 더 이상 요청의 출처를 통제할 수 없다는 의미다. 누군가 이 키를 가져가면, 그 사람은 앱을 거치지 않고도 직접 API를 호출할 수 있다. 요청 횟수가 증가하고, 사용량 제한에 도달하면 API 제공자가 키를 차단할 수도 있다. 심한 경우 서비스 자체가 중단될 수도 있다. 즉, 이 구조는 “언젠가는 반드시 깨지는 구조”다. 단순히 가능성이 있는 문제가 아니라, 시간 문제에 가깝다.

더 중요한 것은 이 문제가 기술적인 해결책으로 막을 수 있는 종류가 아니라는 점이다. 코드 난독화나 네트워크 암호화는 근본적인 해결책이 되지 않는다. 클라이언트에 포함된 정보는 결국 외부로 드러날 수밖에 없다. 이 구조에서 API 키는 “보호할 수 없는 자산”이다. 그리고 보호할 수 없는 자산에 시스템의 핵심을 의존하는 것은 설계적으로 잘못된 선택이다. 이 시점에서 더 이상 “구현 방식의 문제”로 볼 수 없게 된다. 구조 자체가 틀렸다는 결론에 도달하게 된다.

이 문제를 인식하는 순간, 이전까지 자연스럽게 이어지던 설계 흐름이 멈춘다. 로컬 퍼스트 구조 위에 외부 API를 얹는 방식은 더 이상 유지될 수 없다. 데이터는 외부에서 가져와야 하지만, 그 경로는 클라이언트가 될 수 없다. 이 모순은 단순한 수정으로 해결되지 않는다. 설계 자체를 다시 정의해야 하는 지점에 도달하게 된다.

설계의 붕괴: 서버를 제거하는 것이 아니라 재정의해야 했다

처음에는 서버를 만들지 않는 것이 목표였다. 하지만 이 목표는 외부 API라는 현실과 충돌하면서 유지될 수 없게 되었다. 문제는 서버를 추가해야 한다는 사실이 아니라, “서버를 어떤 역할로 정의할 것인가”였다. 기존의 백엔드 시스템을 그대로 도입하는 것은 과도한 선택이었다. 사용자 데이터를 저장하고, 비즈니스 로직을 처리하는 전통적인 서버는 이 구조에 맞지 않는다. Readium의 핵심 데이터는 여전히 로컬에 있어야 하고, 서버는 그 구조를 침범해서는 안 된다.

이 지점에서 설계는 방향을 바꾼다. 서버를 제거하는 것이 아니라, 서버의 역할을 최소한으로 제한하는 방향으로 재정의한다. 서버는 상태를 가지지 않는다. 데이터베이스도 없다. 사용자 정보를 저장하지도 않는다. 대신 하나의 역할만 수행한다. 외부 API를 대신 호출하고, 그 결과를 안전하게 전달하는 것. 이 역할은 작아 보이지만, 구조적으로는 매우 중요한 의미를 가진다. 클라이언트와 외부 세계 사이에 경계를 만드는 역할이기 때문이다.

이 서버는 단순한 프록시가 아니다. 요청을 중계하는 역할을 넘어서, 시스템의 경계를 정의한다. API 키는 더 이상 클라이언트에 존재하지 않고, 서버 내부에서만 관리된다. 클라이언트는 특정 API에 직접 의존하지 않고, 서버가 제공하는 인터페이스만을 사용한다. 이 구조는 단순히 보안을 강화하는 것을 넘어서, 전체 시스템의 결합도를 낮춘다. 외부 API가 변경되더라도 클라이언트는 영향을 받지 않는다.

이 변화는 “서버를 추가했다”는 수준의 수정이 아니다. 설계의 중심이 바뀐 것이다. 처음에는 서버를 제거하는 것이 목표였다면, 이제는 경계를 어떻게 설정할 것인가가 핵심이 된다. 서버는 더 이상 시스템의 중심이 아니라, 경계를 유지하기 위한 최소한의 장치가 된다. 이 전환이 이루어지는 순간, 로컬 퍼스트 전략은 오히려 더 안정적인 형태로 재정의된다. 서버를 없애는 것이 아니라, 서버의 영향 범위를 통제하는 것이 진짜 목표였다는 사실이 드러난다.

보호 계층으로서의 Backend 구조

서버의 역할을 최소화하는 방향으로 재정의한 이후, 구조는 비교적 명확하게 정리되기 시작한다. 모바일 앱은 더 이상 외부 Book API를 직접 호출하지 않는다. 대신 하나의 고정된 엔드포인트를 가진 Readium Backend를 호출한다. 서버는 이 요청을 받아 내부적으로 외부 API를 호출하고, 그 결과를 가공한 뒤 다시 클라이언트에 전달한다. 겉으로 보면 단순한 중계처럼 보이지만, 실제로는 시스템의 책임 분리가 이 지점에서 발생한다. 클라이언트는 더 이상 외부 API의 존재를 알 필요가 없고, 서버는 외부 API의 변화를 내부에서 흡수한다.

이 구조는 단순히 보안을 강화하는 것 이상의 효과를 만든다. 클라이언트와 외부 API 사이의 직접적인 연결이 끊기면서, 시스템의 결합도가 크게 낮아진다. 예를 들어, 특정 API의 응답 구조가 변경되거나 서비스가 중단되더라도, 클라이언트 코드는 수정할 필요가 없다. 서버 내부에서만 대응하면 된다. 또한 여러 API를 조합하는 구조로 확장할 수 있는 기반도 자연스럽게 마련된다. 처음에는 하나의 API만 사용하더라도, 이후에는 여러 API를 순차적으로 호출하거나 결과를 병합하는 방식으로 발전시킬 수 있다. 이 모든 변화는 클라이언트에게는 투명하게 처리된다.

이 구조를 코드 수준으로 단순화하면 다음과 같은 흐름으로 표현할 수 있다.

Client → Readium Backend → External APIs

이 표현은 단순하지만, 설계적으로는 중요한 의미를 담고 있다. 모든 외부 요청은 반드시 Backend를 거쳐야 한다는 제약이 생기고, 이 제약이 곧 시스템의 안정성을 만든다. API 키는 서버 내부에만 존재하고, 클라이언트는 이를 전혀 알 수 없다. 요청의 형태도 서버에서 통제되기 때문에, 외부 API 사용 방식 역시 일관성을 유지할 수 있다. 결국 이 Backend는 단순한 중계 레이어가 아니라, 보안과 추상화를 동시에 담당하는 보호 계층으로 자리 잡게 된다.

이 시점에서 서버는 더 이상 “없애야 할 대상”이 아니다. 오히려 시스템을 안정적으로 유지하기 위해 반드시 필요한 구조적 요소가 된다. 하지만 그 역할은 여전히 제한되어 있다. 데이터는 저장하지 않고, 상태도 가지지 않는다. 오직 경계를 유지하는 역할만 수행한다. 이 제한이 있기 때문에, 서버는 커지지 않고 복잡해지지도 않는다. 그리고 이 점이 이후의 선택들에도 영향을 미치게 된다.

왜 Express가 아니라 Fastify였는가

서버의 역할이 명확해지면서, 기술 스택 선택 역시 자연스럽게 좁혀진다. 이 서버는 복잡한 도메인 로직을 처리하지 않는다. 데이터베이스도 없고, 상태를 관리하지도 않는다. 요청을 받아 외부 API를 호출하고, 결과를 반환하는 것이 전부다. 이 단순함은 프레임워크 선택 기준을 바꾼다. 일반적인 백엔드 시스템에서는 생태계나 확장성이 중요할 수 있지만, 여기서는 가벼움과 예측 가능한 구조가 더 중요하다.

Node.js 환경에서 가장 널리 사용되는 프레임워크는 여전히 Express다. 오랜 시간 동안 사실상의 표준처럼 자리 잡았고, 자료와 예제가 풍부하다. 하지만 Express는 유연한 만큼 구조를 강제하지 않는다. 작은 프로젝트에서는 이 유연함이 오히려 일관성을 해칠 수 있다. 특히 요청과 응답의 형태를 명확하게 정의하지 않으면, 코드가 점점 느슨해지고 유지보수가 어려워진다. Readium Backend는 규모는 작지만, 구조는 명확해야 했다.

이 지점에서 Fastify가 더 적합한 선택으로 보였다. Fastify는 schema 기반으로 요청과 응답을 정의할 수 있고, 이 schema를 기반으로 validation과 serialization이 자동으로 이루어진다. 이는 단순히 편의 기능이 아니라, 서버의 인터페이스를 명확하게 고정하는 역할을 한다. 클라이언트와 서버 사이의 계약이 코드 수준에서 드러나기 때문에, 변경이 발생했을 때 영향 범위를 쉽게 파악할 수 있다. 또한 내부적으로 JSON serialization 성능이 매우 빠르기 때문에, 단순한 요청 처리에서도 불필요한 오버헤드가 줄어든다.

TypeScript와의 결합도 중요한 요소였다. schema를 정의하면 요청과 응답 타입이 자동으로 추론되기 때문에, 별도의 타입 정의를 반복할 필요가 없다. 작은 서버라고 해서 타입 안정성이 중요하지 않은 것은 아니다. 오히려 구조가 단순할수록, 타입이 깨지는 순간 전체 흐름이 쉽게 무너진다. Fastify는 이런 부분을 자연스럽게 보완해준다. 결과적으로 이 선택은 “새로운 기술을 쓰고 싶어서”가 아니라, 현재 구조에 가장 맞는 도구를 선택한 결과였다.

이 선택은 이후 서버가 확장될 때도 영향을 준다. 구조가 명확하게 정의되어 있기 때문에, 새로운 기능을 추가하더라도 기존 인터페이스를 쉽게 유지할 수 있다. 그리고 이 일관성은 곧 시스템 전체의 안정성으로 이어진다. 결국 프레임워크 선택은 단순한 기술 취향이 아니라, 설계의 연장선에 있는 결정이었다.

서버를 운영하지 않기 위한 선택: Cloud Run

서버를 구성한 이후에 남는 문제는 “이 서버를 어떻게 운영할 것인가”였다. 서버를 하나 두는 순간, 운영이라는 새로운 책임이 생긴다. VM을 직접 관리하면 운영 체제 업데이트, 보안 패치, 로그 관리, 스케일링까지 신경 써야 한다. 이 모든 작업은 애플리케이션 개발과는 별개의 영역이지만, 실제로는 지속적으로 시간을 요구한다. 개인 프로젝트에서는 이 부담이 빠르게 누적된다. 서버를 유지하기 위한 비용이 기능 개발보다 커지는 순간, 프로젝트는 자연스럽게 멈추게 된다.

이 문제를 피하기 위해 선택한 것이 Cloud Run이었다. Cloud Run은 컨테이너 이미지를 기반으로 동작하는 서버리스 플랫폼이다. 개발자는 Docker 이미지를 하나 만들어 업로드하면 되고, 나머지 실행과 스케일링은 플랫폼이 자동으로 처리한다. 이 구조의 핵심은 요청 기반 실행이다. 요청이 들어올 때만 컨테이너가 실행되고, 요청이 없으면 아무 자원도 사용하지 않는다. 즉, 서버가 존재하지만 동시에 존재하지 않는 상태를 유지할 수 있다.

이 방식은 Readium Backend의 특성과 잘 맞는다. 트래픽이 많지 않은 상황에서는 비용이 거의 발생하지 않고, 필요할 때만 리소스를 사용한다. 또한 별도의 서버 관리가 필요 없기 때문에, 개발자는 애플리케이션 코드에만 집중할 수 있다. 이는 단순히 편리함의 문제가 아니라, 프로젝트의 지속 가능성과 직접적으로 연결된다. 운영 부담이 줄어들수록, 기능 개발과 구조 개선에 더 많은 시간을 사용할 수 있다.

이 선택 역시 하나의 설계 결정이다. 서버를 두되, 서버를 운영하지 않겠다는 선택이다. 물리적인 서버는 존재하지만, 관리 대상에서는 제외된다. 이 방식은 로컬 퍼스트 전략과도 충돌하지 않는다. 서버는 여전히 최소한의 역할만 수행하고, 인프라 역시 최소한으로 유지된다. 결국 Cloud Run은 단순한 배포 환경이 아니라, 서버의 존재를 최소화하기 위한 또 하나의 구조적 선택이었다.

인증이라는 두 번째 경계: Firebase Authentication

서버를 도입한 이후에도 구조는 아직 완성되지 않았다. 외부 API 키를 보호하는 문제는 해결되었지만, 또 다른 질문이 자연스럽게 따라온다. 이 서버는 누가 사용할 수 있는가라는 문제다. 만약 아무런 제약 없이 API를 공개한다면, 이 서버는 단순한 보호 계층이 아니라 외부 API 호출을 대신해주는 공개 프록시로 변하게 된다. 누군가가 이 엔드포인트를 발견하고 임의로 요청을 보내기 시작하면, 서버는 의도하지 않은 트래픽을 처리하게 된다. 결국 이 문제는 다시 처음과 같은 결과로 이어진다. API 사용량이 증가하고, 제한에 도달하면 서비스 전체가 영향을 받는다.

이 지점에서 서버는 단순히 “요청을 대신 전달하는 존재”를 넘어, 요청의 출처를 판단하는 역할까지 가져야 한다. 즉, 서버는 이제 두 가지 경계를 동시에 담당하게 된다. 하나는 외부 API를 보호하는 경계이고, 다른 하나는 서버 자체를 보호하는 경계다. 이 두 번째 경계를 만들기 위해 선택한 것이 Firebase Authentication이었다. 모바일 앱은 Firebase를 통해 사용자 인증을 수행하고, 인증이 완료되면 ID Token을 발급받는다. 이 토큰은 단순한 문자열이 아니라, 특정 사용자와 세션을 식별할 수 있는 서명된 데이터다.

서버는 요청을 받을 때 이 토큰을 함께 전달받고, 이를 검증한 뒤에만 요청을 처리한다. 이 과정은 단순히 로그인 상태를 확인하는 수준이 아니다. 요청이 실제 앱에서 발생했는지, 유효한 사용자 세션에 의해 생성되었는지를 판단하는 기준이 된다. 이 구조를 통해 서버는 더 이상 누구에게나 열려 있는 엔드포인트가 아니라, 제한된 범위에서만 접근 가능한 인터페이스가 된다.

이 선택은 구조적으로 중요한 의미를 가진다. 서버는 단순한 보호 계층에서, 보안과 요청 제어를 동시에 수행하는 게이트웨이로 역할이 확장된다. 외부 API를 보호하는 것만으로는 충분하지 않았고, 서버 자체를 보호해야 전체 시스템이 안정적으로 유지된다. 이 두 번째 경계가 추가되면서, Backend는 비로소 독립적인 시스템으로서의 형태를 갖추게 된다.

이 서버는 왜 작지만 중요한가

이렇게 만들어진 Readium Backend는 규모만 보면 매우 작다. 데이터베이스도 없고, 복잡한 도메인 로직도 없다. 처리하는 요청의 종류도 제한적이다. 하지만 이 서버가 담당하는 역할을 구조적으로 바라보면, 그 중요성은 단순한 크기와는 전혀 다른 차원에 있다. 이 서버는 시스템의 중심이 아니라, 시스템의 경계를 정의하는 역할을 수행한다. 그리고 이 경계는 전체 구조의 안정성을 결정한다.

클라이언트는 더 이상 외부 API와 직접 연결되지 않는다. 모든 요청은 서버를 통해 전달되며, 서버는 이를 검증하고 가공한다. 이 과정에서 보안, 요청 제어, API 추상화가 동시에 이루어진다. 외부 API가 변경되더라도 클라이언트는 영향을 받지 않고, 인증되지 않은 요청은 서버에서 차단된다. 또한 여러 API를 조합하거나 교체하는 것도 서버 내부에서만 처리하면 된다. 이 모든 변화가 클라이언트에게는 투명하게 유지된다.

이 구조를 전체 시스템 관점에서 보면, 이 서버는 전통적인 의미의 백엔드라기보다 게이트웨이에 가까운 역할을 한다. 데이터는 여전히 로컬에 있고, 서버는 데이터를 소유하지 않는다. 대신 외부와의 연결 지점을 통제하고, 시스템 내부로 들어오는 모든 요청을 관리한다. 이 역할은 눈에 잘 드러나지 않지만, 구조적으로는 가장 중요한 부분 중 하나다.

결국 이 서버는 크기가 아니라 역할로 정의된다. 작기 때문에 단순한 것이 아니라, 역할을 제한했기 때문에 작게 유지될 수 있다. 그리고 이 제한이야말로 구조를 안정적으로 만드는 핵심 요소다. 서버가 커지지 않도록 설계하는 것이 아니라, 서버가 커질 필요가 없도록 역할을 정의하는 것이 더 중요하다는 사실이 이 시점에서 드러난다.

설계의 재정의: 서버를 없애는 것이 아니라 경계를 줄이는 것이다

처음 Readium을 설계할 때의 목표는 분명했다. 가능한 한 서버를 만들지 않는 것. 이 목표는 일정 부분 유지되었지만, 완전히 그대로 실현되지는 않았다. 대신 전혀 다른 형태로 재정의되었다. 서버를 제거하는 것이 아니라, 서버의 역할을 극도로 제한하는 방향으로 구조가 바뀌었다. 이 변화는 단순한 타협이 아니라, 설계에 대한 이해가 깊어지면서 자연스럽게 도출된 결과였다.

서버를 완전히 제거하려는 시도는 이상적으로 보이지만, 실제 시스템에서는 항상 외부와의 경계가 필요하다. 데이터는 내부에 있을 수 있지만, 모든 입력과 출력까지 내부에만 머무를 수는 없다. 외부 데이터에 의존하는 순간, 그 경계를 어떻게 설정할 것인가가 더 중요한 문제가 된다. Readium에서 서버는 바로 그 경계를 담당하는 최소한의 장치로 남게 되었다. 더 많은 역할을 추가하는 것도 가능했지만, 의도적으로 그렇게 하지 않았다.

이 선택은 결과적으로 로컬 퍼스트 전략을 더 강하게 만든다. 서버가 없는 것이 중요한 것이 아니라, 서버가 핵심 데이터를 침범하지 않는 것이 중요하다. 데이터의 소유권은 여전히 클라이언트에 있고, 서버는 그 외부에서 발생하는 문제를 처리하는 역할만 수행한다. 이 구조는 단순하면서도 확장 가능하다. 필요하다면 서버의 역할을 조금씩 늘릴 수 있지만, 기본적인 경계는 유지된다.

이 지점에서 처음의 질문으로 돌아오게 된다. 서버는 필요한가라는 질문이다. 이제 이 질문에 대한 답은 단순하지 않다. 서버는 필요할 수도 있고, 필요하지 않을 수도 있다. 중요한 것은 존재 여부가 아니라, 어떤 역할로 존재하는가다. Readium Backend는 그 질문에 대한 하나의 답이다. 서버를 없애는 것이 아니라, 서버의 의미를 다시 정의하는 방식이다.

그리고 이 구조 위에서 또 다른 문제가 등장한다. 하나의 API에 의존하는 구조는 과연 충분히 안정적인가라는 질문이다. 검색이 실패하는 경우는 어떻게 처리해야 하는가, 서로 다른 데이터 소스를 어떻게 조합할 것인가라는 문제가 남아 있다. 다음 글에서는 이 문제를 해결하기 위해 도입한 Provider와 Fallback 전략에 대해 더 자세히 이야기해보려 한다.

다음 단계: 하나의 API로 충분한가

지금까지의 구조를 기준으로 보면, Readium Backend는 이미 충분히 안정적인 형태를 갖춘 것처럼 보인다. 외부 API 키는 서버 내부에 안전하게 보호되고, 클라이언트는 더 이상 외부 서비스에 직접 의존하지 않는다. 인증을 통해 요청의 출처도 통제되고, 서버는 최소한의 역할만 수행하면서도 전체 시스템의 경계를 유지한다. 이 상태만 놓고 보면 설계는 한 번 정리된 것처럼 느껴진다. 실제로 단일 Book API를 사용하는 상황에서는 큰 문제 없이 동작한다. 검색 요청을 보내면 결과가 반환되고, 사용자는 그 중 하나를 선택해 독서 기록을 시작할 수 있다.

하지만 이 구조는 하나의 전제를 여전히 그대로 유지하고 있다. 특정 외부 API가 항상 올바른 결과를 반환한다는 가정이다. 이 가정은 처음에는 거의 의심되지 않는다. 대부분의 검색은 정상적으로 동작하고, 일반적인 책 제목에 대해서는 충분히 정확한 결과를 제공한다. 그러나 사용자가 조금만 다른 형태의 입력을 시도하면 상황이 달라진다. 책 제목이 부분적으로만 입력되거나, 오탈자가 포함되거나, 특정 언어나 지역에 따라 데이터가 부족한 경우에는 검색 결과가 비어 있거나 부정확하게 반환되는 경우가 발생한다. 이 문제는 단순한 예외 케이스가 아니라, 실제 사용자 경험을 지속적으로 흔드는 요소가 된다.

이 시점에서 다시 하나의 질문이 등장한다. 하나의 API에 의존하는 구조가 과연 충분히 안정적인가라는 질문이다. 외부 API는 우리가 통제할 수 없는 시스템이다. 응답 형식이 바뀔 수도 있고, 서비스가 일시적으로 중단될 수도 있으며, 특정 데이터가 누락될 수도 있다. 지금까지는 서버를 통해 이 의존성을 “숨기는 것”까지는 해결했지만, 의존성 자체를 줄인 것은 아니다. 즉, 구조는 안전해졌지만 여전히 하나의 단일 실패 지점에 묶여 있는 상태다.

이 문제는 단순히 API를 하나 더 추가하면 해결되는 문제가 아니다. 여러 API를 사용하는 순간, 어떤 순서로 호출할 것인지, 결과를 어떻게 통합할 것인지, 실패했을 때 어떤 전략으로 대체할 것인지에 대한 새로운 설계가 필요해진다. 즉, 단일 API 구조에서는 고려하지 않았던 “검색 전략”이라는 개념이 등장한다. 이 전략은 단순한 구현 문제가 아니라, 사용자 경험과 직접적으로 연결된다. 어떤 결과를 먼저 보여줄 것인지, 실패를 어떻게 처리할 것인지에 따라 앱의 완성도가 달라지기 때문이다.

결국 이 지점에서 구조는 다시 한 번 확장된다. 서버는 더 이상 단순히 요청을 전달하는 계층이 아니라, 여러 데이터 소스를 조합하고 선택하는 역할까지 맡게 된다. 하지만 이 확장은 이전과 같은 방식으로 이루어지지 않는다. 서버의 역할을 무작정 늘리는 것이 아니라, 기존의 “보호 계층”이라는 개념을 유지한 채 그 위에 새로운 전략을 얹는 방식으로 진행된다.

다음 글에서는 바로 이 지점을 다룬다. 여러 Book API를 어떻게 하나의 검색 경험으로 통합할 것인지, 그리고 실패를 전제로 한 구조를 어떻게 설계할 것인지에 대한 이야기다. Provider라는 추상화와 Fallback 전략을 통해, 단일 API 의존에서 벗어나는 과정을 구체적으로 살펴보게 될 것이다.