네일톡톡 서비스 개발⋅운영 회고

네일톡톡 서비스 개발⋅운영 회고

Review

저는 네일톡톡에서 백앤드 개발자로 근무했습니다.

제가 합류했을 당시엔 이미 한 개의 매장이 오픈한 상태였고 추가로 홍대 매장을 준비하고 있었습니다. 수개월간의 임금체불로 인해 짧은 기간의 경험이었지만, 그 동안의 서비스를 개발하고 운영하며 겪은 상황들을 회고하고자 합니다.

미완성된 기능과 성급한 오픈

과거 이해관계가 어떻게 얽혀있는지는 정확히 알 수 없지만, 당시에는 정말 필수적인 기능만을 개발한 뒤 범계점을 오픈했다고 합니다.

그 결과 데이터에 대한 신뢰가 무너지고, 매장 직원과 고객들의 불만이 쌓이기 시작했습니다. 다른 팀의 업무에도 병목이 발생했고, 이를 해결하기 위해 개발팀은 밤낮과 주말을 가리지 않고 대응해야 했습니다.

그럼에도 문제는 계속해서 발생했고, 어느 누구도 만족하기 어려운 상황이 되었습니다.

소프트웨어가 만들어진 지 수개월밖에 지나지 않았음에도 내부에서는 “차라리 새로 만드는 것이 낫겠다”는 이야기까지 나오고 있었습니다.

개인적으로는 상당히 안타까운 상황이었습니다.

쌓여온 기술부채와 멈추지 않는 폭주 기관차

과거 급하게 오픈하기 위해 만들어진 임시 기능들, 개발자의 수동 데이터 처리, 계속해서 쌓이는 쓰레기 데이터, 불안정한 서버 등 제가 보기에도 크리티컬한 문제들이 하나씩 드러나고 있었습니다. 그런 상황에서 경영진의 추가 요구사항과 기획팀의 신규 기획까지 계속해서 쏟아지기 시작했습니다.

사람은 눈앞에 무언가가 만들어지기 시작하면 새로운 아이디어를 떠올리기 마련입니다. 그동안의 경험을 생각하면 이러한 현상 자체는 충분히 이해할 수 있습니다.

하지만 문제는 시기였습니다.

기존 시스템의 안정성을 확보해야 하는 상황에서 새로운 기능을 논의하기 위한 회의조차 개발팀에게는 상당한 비용으로 느껴졌습니다.

마치 멈추지 않는 폭주 기관차에 엔지니어들이 매달려 기차를 수리하고있는것 같다…

퇴사한 이전 개발자들, 존재하지 않는 설계 문서, 신뢰하기 어려운 데이터, 오히려 혼란을 가중시키는 주석. 저를 포함한 새롭게 투입된 개발자들 역시 이러한 상황을 이해하는 것부터 쉽지 않았습니다.

무엇이 먼저 해결되어야 할까?

개발자는 대부분 하자가 있는 기능, 시스템의 잠재적인 위험을 우선순위를 높여 중요하게 생각할것입니다. 문제는 개발자가 바라보는 위험과 비개발자가 바라보는 위험의 형태가 다르다는 것입니다.

개발자가 알고 있는 시스템의 구조적 문제와 잠재적인 장애 가능성은 사용자에게 직접 보이지 않습니다. 이걸 비개발자에게 이해할 수 있는 방식으로 설명하는 것 또한 개발자의 일입니다.

Ubiquitous Language, UML 등을 활용해 보았지만 생각보다 말처럼 쉬운 일은 아니었습니다.

생각보다 비싼 서버 운영비용

제가 합류했을 당시 AWS 인프라 비용은 월 약 660만 원 정도가 발생하고 있었습니다. EKS, EC2, S3, RDB, Redis 등을 사용하고 있었는데, 당시 서비스 규모를 고려하면 상당히 불균형한 구성이었습니다.

하루에 매장을 이용하는 사용자가 5명도 되지 않는 상황이었고, EC2는 오히려 낮은 사양으로 구성되어 빈번하게 OOM이 발생하고 있었습니다. 반면 RDB와 Redis는 서비스 규모에 비해 상대적으로 높은 사양으로 운영되고 있었습니다.

전체적으로 인프라가 서비스 규모와 맞지 않는 상태였습니다.

인프라 다이어트

이렇게 서버 비용이 많이 나올줄은 아무도 예상하지 못했었나 봅니다. 어쩌면 소통의 부재일수도 있구요. 경영진의 요구사항은 명확했습니다.

월 660만원은 과하다. 기능을 유지하면서 얼마까지 낮출 수 있는가?

인프라를 살펴보니 용도를 명확하게 파악하기 어려운 EC2 인스턴스도 있었고, 사용 목적을 확인하기 어려운 고정 공인 IP도 여러 개 존재했습니다. 정확한 비용을 장담하기는 어려웠지만, 우선 월 200만 원 이하를 목표로 잡았습니다.

만드는것 보다 걷어내는게 더 어렵다 (MSA → Monolithic)

과거 구축된 MSA 구조는 당시 서비스, 팀 규모에 비해 복잡했습니다. 서비스 간 통신은 존재했지만 로깅, 에러 핸들링, 모니터링 등 운영에 필요한 기반이 충분히 갖춰져 있지 않았습니다.

논의 끝에 우선 여러 서비스를 하나의 인스턴스에서 운영하는 방향으로 결정했습니다. 짧은 시간 안에 가장 큰 비용 절감 효과를 얻을 수 있다고 판단했기 때문입니다.

기존 비즈니스 로직을 최대한 유지하면서 서비스 간 HTTP 호출을 직접적인 Method Call로 변경하는 방식으로 진행했습니다. 당시 시스템은 규모가 크지 않았고, 여러 개발자가 동시에 작업하기에는 오히려 협업 비용이 큰 작업이었기 때문에 제가 해당 작업을 담당했습니다.

테스트를 진행한 뒤 특별한 문제가 없다고 판단했고, 그날 밤 모든 개발자가 함께 작업했습니다. EKS를 내리고 불필요한 인스턴스를 정리한 뒤, 개발 환경과 운영 환경을 각각 단일 인스턴스로 구성했습니다.

이 작업만으로도 예상 인프라 비용을 월 100만 원대로 낮출 수 있었습니다.

기술을 선택할땐 이유가 필요해

기술을 선택할 때 가장 중요한 것은 어떤 기술을 선택했는가보다 왜 그 기술을 선택했는가라고 생각합니다. 그 이유에는 팀의 상황, 가용한 자원, 재정적인 여유, 개발 일정, 요구사항, 팀의 경험과 운영 역량 등이 포함될 수 있습니다.

절대 MSA가 나쁜 아키텍처라서 Monolithic으로 변경한 것이 아닙니다. 당시의 서비스 규모와 팀의 상황, 운영 비용, 그리고 시스템이 가진 문제들을 종합적으로 고려했을 때 그 시점에서는 Monolithic 구조가 더 현실적인 선택이라고 판단한 것입니다.

그리고 이러한 판단은 시간이 지나거나 상황이 변하면 언제든 달라질 수 있습니다. 특히 프로덕트 레벨의 시스템이라면 기술 선택에 더욱 신중할 필요가 있습니다.

AI를 활용하면 많은 작업을 빠르게 수행할 수 있는 시대가 되었지만, 이미 운영 중인 시스템을 다른 구조로 마이그레이션하는 일은 여전히 쉽지 않습니다. 그리고 그 과정에서 발생하는 리스크는 결국 시스템의 최전선에 있는 개발자들이 가장 먼저 감당하게 됩니다.

따라서 기술을 선택할 때는 트랜드나 개인의 이유만으로 결정하기보다는, 지금 우리에게 필요한 것이 무엇인지, 그리고 그 선택을 감당할 수 있는지를 함께 고민해야 한다고 생각합니다.

이미지를 등록하고 저장하면 서버가 다운된다?

관리자 페이지에서 이미지를 하나 업로드하면, 이미지의 비율에 따라 36개의 이미지를 생성해 반환하는 구조였습니다. 문제는 이미지 처리와 업로드를 모두 서버가 담당하고 있었다는 것입니다.

원본 이미지 하나가 3~10MB 정도였는데, 이미지 하나를 등록할 때마다 최대 36개의 이미지가 생성되고 이 파일들이 모두 서버를 거쳐 S3에 업로드되었습니다. 결국 이미지 업로드 하나가 발생할 때마다 상당한 CPU와 메모리, 네트워크 I/O가 발생했습니다.

당시 서버에서는 이미지 업로드가 몰릴 때마다 부하가 크게 증가했고, 심한 경우 서버가 다운되는 상황까지 발생했습니다. 당황스러운건 당시의 임시 대책이었습니다.

디자이너 두 명이 동시에 작업하지 않으면 된다…

물론 임시 대책이지만 정말 부끄러운 일입니다.

모든걸 중앙 서버가 처리할 필요는 없어!

이미지 처리를 살펴보니 서버에서 수행하는 작업은 모두 클라이언트에서도 처리할 수 있었습니다. AI 를 활용해 기존 Java로 구현된 이미지 처리 로직을 분석하고 JavaScript로 옮겼습니다. (Client 로 연산 위임)

만약 제가 처음부터 직접 구현했다면 꽤 시간이 필요했을 겁니다.

업로드도 클라이언트가 처리하고 서버에겐 결과만 알려줘

이미지 처리 문제를 해결하고 나니 또 하나의 문제가 보였습니다. 이미지를 처리하는 것은 클라이언트에서 할 수 있는데, 굳이 수십 개의 이미지 데이터를 다시 서버로 전송한 뒤 서버가 S3에 업로드할 필요가 있을까?

서버는 이미지를 직접 처리하지 않더라도 단순히 이미지를 전달하는 중계 역할을 하고 있었습니다.

이 구조에서는 사용자가 늘어날수록 애플리케이션 서버의 네트워크 I/O가 불필요하게 증가합니다. 예를 들어 이미지가 포함된 리뷰를 100명이 동시에 작성한다면? 서버가 그 모든 트래픽을 받아서 다시 S3로 전달해야 합니다.

그래서 S3의 Presigned URL을 이용해 클라이언트가 S3에 직접 업로드하도록 구조를 변경했습니다. 새로운 테이블과 관련 로직을 추가하여 서버가 업로드에 필요한 Presigned URL을 발급하고, 클라이언트는 해당 URL을 이용해 S3에 직접 파일을 업로드하도록 했습니다.

이제 서버는 무거운 이미지 데이터를 전달하지 않습니다. 서버가 담당하는 것은 업로드에 필요한 메타데이터를 관리하는 것이고, 실제 파일 전송은 S3가 담당하게 되었습니다. 이를 통해 이미지 업로드로 인해 발생하던 애플리케이션 서버의 I/O 부하를 크게 줄일 수 있었습니다.

당연히 서버는 이제 클라이언트의 S3 업로드 후 API 호출에 따라 이미지 업로드가 확정되었다는 것을 알기에 S3 에 쓰레기 (Orphan) 데이터가 생길 여지가 있습니다. 사용자가 Presigned URL 을 발급받은 뒤 업로드를 완료하지 않을 수도 있고, 업로드는 완료했지만 이후 리뷰 작성이나 이미지 등록을 취소할 수도 있습니다.

이를 해결하기위해 일정 시간이 지난 미사용 이미지를 찾아 삭제하는 배치 서비스를 새로 만들었습니다.

클라이언트로 업로드 책임을 위임하면서 새로운 관리 포인트가 생겼지만, 핵심 자원인 서버의 부하를 줄이는 가치가 훨씬 더 컸다고 판단합니다. 역시 세상엔 은탄환은 없습니다.

이 코드는 여기가 어올리지 않아

만들다 만 코드, 아무도 호출하지 않는 클래스, 이름만으로는 역할을 알 수 없는 메서드, 역할과 전혀 맞지 않는 위치에 존재하는 클래스. 코드를 살펴볼수록 이런 문제들이 하나씩 보이기 시작했습니다.

개발자마다 코드를 작성하는 방식이 달랐고, 새로운 기능을 추가할 때 어디에 어떤 책임을 두어야 하는지에 대한 공통된 규칙도 없었습니다.

그래서 기준과 룰이 필요했습니다. 단순히 들여쓰기나 네이밍 같은 코딩 스타일에 대한 규칙을 이야기하는 것이 아닙니다.

새로운 개발자가 코드를 보더라도 “왜 여기에 이 코드가 있지?”라는 질문을 최소화할 수 있어야 한다.

그것이 제가 생각하는 기준이었습니다.

소프트웨어 아키텍처의 도입

이를 해결하기 위해 클린 아키텍처를 도입하고 대대적인 마이그레이션을 시작했습니다. 목적은 이 프로그램을 우리가 아닌 다른사람도 유지보수할 수 있게 만드는것 그것이 가장 중요한 목표였습니다.

기존 코드를 한 번에 모두 변경하는 것은 현실적으로 불가능했기 때문에 새로운 기능부터 구조를 적용하고, 변경이 필요한 기존 코드부터 조금씩 마이그레이션하는 방식으로 진행했습니다.

모든 개발자가 알고 있지만, 아키텍처를 도입한다고 코드가 저절로 좋아지는 것도 아니고, 오히려 작은 프로젝트에서는 불필요한 복잡성을 만들어낼 수도 있습니다. 하지만 당시 우리에게 필요했던 것은 새로운 기술이 아니라 팀이 함께 코드를 바라볼 수 있는 공통된 기준이었습니다.

좋은 코드란 단순히 동작하는 코드가 아니라, 다른 사람이 읽고 이해하고 변경할 수 있는 코드여야 한다고 생각합니다.

마치며

짧은 기간이었지만 많은 것을 경험했습니다. 성급한 오픈으로 인해 발생한 기술부채, 서비스 규모에 비해 과도했던 인프라, 서버가 모든 것을 부담하는 구조, 기준 없이 쌓여가는 코드까지.

어떤 서비스는 코드만으로 만들어지는 것이 아닙니다. 뒤에는 항상 사람과 일정, 비용, 의사결정, 조직의 상황이 함께 존재합니다.

문제를 해결하는 방법은 무궁무진하고 중요한것은

지금 우리에게 가장 적합한 선택은 무엇인가?

라고 생각합니다.

은탄환은 없으니까요.