잘 돌아가던(?) 인증, 왜 갑자기 뜯어고쳤는가
백엔드 정리를 어느정도 마치고, 프로젝트를 확장하려고 보니 앱 내부에 찰싹 달라붙어 있는 NextAuth 구조가 눈에 밟혔다. 이대로라면 나중에 새로운 서비스를 추가할 때마다 각 앱이 인증 로직을 파편화해서 들고 있거나, 앱 데이터베이스에 사용자 인증 정보가 무분별하게 섞이는 끔찍한 결말이 뻔히 보였다.
이 참에 시스템의 경계를 명확히 긋기로 했다. 단일 앱에 종속된 인증을 떼어내어 중앙 인증 서버로 분가시키는 대공사였다. 목표는 앱 도메인과 인증 도메인 간의 크로스 도메인 인증을 구현하고, 엑세스 토큰과 리프레시 토큰 구조를 철저히 정립하는 것이었다.
데이터베이스 역할 분리와 식별자 대공사
가장 먼저 데이터베이스를 쪼갰다. 인증 정보와 앱 도메인 데이터가 한곳에 뒹굴면 나중에 서비스가 확장될 때 데이터 경계가 처참하게 무너진다.
- 인증 서버 데이터베이스: 사용자 계정, 소셜 연동 키, 비밀번호를 관리하며 외부 접근을 엄격히 차단한다. 고유 해시 기반 식별자를 주로 사용한다.
- 앱 서버 데이터베이스: 시간표 앱 전용 데이터 저장소다. 시간표, 수업 정보, 앱 설정 등을 보관하며, 중앙 서버가 발급한 식별자를 매핑해 사용한다. 인증된 요청에 한해 데이터를 제공한다.
코드를 정리하던 중 등골이 서늘해지는 로직을 발견했다. 클라이언트가 API를 호출할 때 자신의 고유 아이디를 직접 보내고, 보호되어야 할 API가 그 값을 순진하게 믿고 처리하고 있었다. 악의적인 사용자가 아이디 값만 변조해서 보내면 다른 사람의 데이터를 마음대로 주무를 수 있는 엄청난 권한 우회 취약점이었다. 당장 클라이언트의 입력값을 불신하도록 뜯어고쳤다. 서버가 항상 토큰을 직접 검증한 뒤 사용자 아이디를 결정하도록 로직을 전환했다.
초기에는 이메일을 기준으로 로컬 사용자를 갱신하는 방식을 썼지만, 사용자가 이메일을 변경하는 순간 시스템에 혼란이 올 수밖에 없다. 따라서 이메일 기반 연결은 임시 호환 레이어로 남겨두고, 전역 식별자를 시스템 전체의 중심축으로 삼았다.
도커와 네트워크가 만든 환장의 트러블슈팅
서버 코드를 완성하고 깃허브 액션과 도커 허브를 거쳐 오라클 클라우드에 배포했다. 하지만 브라우저 콘솔에는 붉은색 글씨로 오리진 접근 불가 에러가 도배되었다. 코드를 백 번 넘게 확인해도 설정은 완벽했다.
범인은 배포 스크립트에 있었다. 도커 환경 변수를 주입할 때 클라이언트 오리진 주소에 따옴표가 들어간 채로 컨테이너에 들어간 것이다.
# 환경 변수 주입 시 따옴표가 포함되면 문자열 비교가 실패함
CLIENT_ORIGINS="https://app.workspace.p-e.kr"
서버는 요청이 들어온 오리진과 따옴표가 덧붙여진 환경 변수 문자열을 비교했고, 당연히 일치하지 않아 접근을 차단했다. 운영 환경 변수는 따옴표 없이 주입하는 것이 안전하며, 서버 코드 내부에서도 환경 변수의 공백이나 따옴표를 정규화해서 방어해야 한다는 뼈아픈 교훈을 얻었다. 깃허브 액션에서 시크릿을 덩어리로 관리하는 것은 편하지만, 줄바꿈이나 포맷 규칙을 엄격하게 가져가지 않으면 재앙이 된다.
네트워크 삽질은 여기서 끝이 아니었다. 도커 컨테이너 내부에서 로컬호스트를 호출하면 호스트 운영체제의 데이터베이스가 아니라 컨테이너 자신을 가리킨다. 당연히 컨테이너 안에는 데이터베이스가 없으니 연결이 거부되었다. 결국 별도의 호스트 내부 주소를 명시하여 컨테이너 밖의 호스트 데이터베이스와 통신하도록 네트워크 경로를 뚫어주었다.
인증서와 배포 파이프라인의 눈물
통신이 뚫리니 이번에는 인증서가 발목을 잡았다. 서브도메인을 새로 팠으니 기존 도메인 인증서 범위에 들어갈 리가 없었다. 브라우저는 유효하지 않은 인증서라며 경고창을 띄웠다. 서브도메인 분리 시 인증서도 같이 챙겨야 한다는 사실을 깨닫고, 새 서브도메인용 인증서를 별도로 발급받아 ngnix에 물려주고 나서야 자물쇠 아이콘에 녹색 불이 들어왔다.
배포 파이프라인도 뜯어고쳤다. 처음에는 컨테이너가 켜질 때 데이터베이스 마이그레이션을 같이 돌렸다. 하지만 배포 환경에서는 일회성 컨테이너를 먼저 띄워 마이그레이션 명령어만 실행하게 만들었다. 이 작업이 성공해야만 메인 서버 컨테이너를 교체하는 구조로 변경하여 롤백과 운영 안정성을 끌어올렸다.
프론트엔드에서도 사용자 경험을 위해 유효성 검사를 강화했다. 서버가 비밀번호 길이를 체크하기 전에 프론트에서 먼저 막아주지 않으면, 사용자는 그저 요청 데이터가 올바르지 않다는 불친절한 텍스트만 보게 된다. 프론트엔드 단에서 입력값을 철저히 검증하고, 서버의 에러 메시지도 명확하게 화면에 렌더링하도록 개선했다.
험난했던 여정의 끝, 그리고 남은 숙제
이번 개편은 단순한 기능 구현이 아니라 시스템의 경계를 재정의하는 작업이었다. 훌륭한 코드를 짜는 것보다 운영 환경과 배포 구조를 안정화하는 데 훨씬 더 많은 피땀을 흘렸다. 소셜 로그인과 넥스트오스를 떼어내고, 쿠키와 보안 정책을 맞추고, 도커와 인증서의 함정을 피하며 실제 운영 가능한 수준으로 중앙 인증 서버를 띄워냈다.
하지만 완벽한 시스템은 없다. 당장 해결해야 할 기술 부채들이 도사리고 있다.
- 잔재 데이터 청소: 시간표 데이터베이스에 쓸데없이 남아있는 비밀번호 필드 완전 영구 제거
- 식별자 체계 완성: 이메일 의존도를 버리고 전역 식별자 기반 구조를 모든 서비스에 쐐기 박기
- 소셜 로그인 보안: 접근 토큰이 콜백 주소창에 노출되는 것을 막기 위해 일회성 코드 교환 방식으로 구조 개선
- 운영 인프라 강화: 비정상 트래픽 제어, 보안 감사 로그 추적, 시크릿 키 주기적 롤링 방어막 도입
비싼 돈 주고도 못 배울 귀중한 인프라 분산 및 보안 실전 훈련을 치렀다. 이제 어떤 서비스를 가져다 붙여도 무너지지 않을 튼튼한 토대가 마련되었다.