인삿말
안녕하세요, 오랜만에 인사드리네요. 하입타운 대표 공태현입니다.
하입타운은 어느덧 누적 사용자 수 15,000명을 넘기고,
누적 티켓 거래액도 10억 원을 훌쩍 넘어서며 시장 안에서 조금씩 자리를 잡아가고 있습니다.
제품이 성장해가면서 많은 분들의 사랑을 받고 있다는 점은 참으로 기쁘고 감사한 일입니다.
하지만 그만큼, 저희 제품을 사용하는 사람들의 삶과 경험에 직접적인 영향을 미친다는 사실을 날이 갈수록 무겁게 실감하고 있습니다. 이제는 단순히 기능을 개발하고 배포하는 것을 넘어, 시스템의 안정성과 신뢰성에 대한 더 큰 책임을 져야 하는 단계에 왔다는 것을 느낍니다.
왜 이 글을 쓰게 되었나
특히 최근 몇 달 동안 새로운 기능을 개발하면서, 예전에는 오류가 나도 알려줄 사람이 없었지만, 지금은 15,000명의 사용자들이 직접 사용하는 서비스가 된 만큼 작은 오류 하나에도 문의가 쏟아지고, 피드백이 실시간으로 쌓이고 있습니다. 예전에는 사용자가 불편을 참고 넘어가는 경우도 많았지만, 이제는 기대 수준이 훨씬 높아졌고, 그만큼 시스템의 견고함이 곧 서비스에 대한 신뢰로 직결되고 있습니다.
성장통이 남긴 흔적
또한 사용자가 늘어나면서, 트래픽이 몰릴 때마다 그동안 미뤄왔던 코드의 문제점들이 하나둘씩 드러나고 있습니다. 무엇보다 하나를 고치면 다른 곳에서 오류가 터지는 상황, 소위 말하는 스파게티 코드 현상이 팀 전체에 부담이 되고 있다는 점이 가장 심각한 문제로 다가왔습니다.
그래서 저는 대표로서 이 문제를 결코 가볍게 넘기지 않기로 했습니다.
기술 부채를 해결하는 일을 2025년 5월, 하입타운 개발팀의 최우선 과제로 삼았습니다.
그 첫 번째 단계로, 오늘부터 SOLID 원칙을 깊게 공부하고, 이를 하입타운의 개발 문화로 정착시킬 수 있도록 정리해나가려 합니다.
이 글은 그 첫 번째 기록입니다.
SOLID 원칙 한눈에 보기
이번에 적용할 SOLID 원칙은,
소프트웨어를 더 안정적이고, 확장 가능하고, 유지보수하기 쉽게 만드는 5가지 핵심 설계 원칙입니다.
코드가 커지더라도 망가지지 않고, 예상 가능한 방향으로 자라게 만들어 주는 “뼈대” 같은 규칙이라고 생각하면 됩니다.
아주 쉽게 요약하면 다음과 같습니다.
원칙 | 요약 | 한 줄로 이해하기 |
S: 단일 책임 원칙
(Single Responsibility Principle) | 하나의 클래스는 하나의 일만 해야 한다. | “한 사람이 한 가지 일만 해라.” |
O: 개방-폐쇄 원칙
(Open/Closed Principle) | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다. | “새로운 걸 추가할 수는 있어야 하지만, 기존 코드는 건드리지 말자.” |
L: 리스코프 치환 원칙
(Liskov Substitution Principle) | 자식 클래스는 부모 클래스를 대체해도 문제 없어야 한다. | “자식이 부모인 척해도 괜찮아야 한다.” |
I: 인터페이스 분리 원칙
(Interface Segregation Principle) | 쓸데없는 기능은 강요하지 말자. | “필요한 것만 쓰게 인터페이스를 잘게 쪼개라.” |
D: 의존관계 역전 원칙 (Dependency Inversion Principle) | 세부사항이 아니라 추상화에 의존하자. | “구체적인 것보다 ’약속(추상화)’에 의존하라.” |
SOLID를 지키면 어떤 점이 좋아질까?
•
클래스를 더 읽기 쉽게 만들 수 있습니다.
•
수정할 때 다른 코드에 피해가 적어집니다.
•
새로운 기능을 추가할 때 덜 두렵습니다.
•
버그를 찾고 수정하는 시간이 줄어듭니다.
특히 우리가 지금 겪고 있는 “수정하면 다른 데서 오류 터지는” 문제를 근본적으로 줄이는 데 큰 도움이 될 거라 기대하고 있습니다.
현재 하입타운 코드베이스 문제 분석
현재 하입타운의 코드베이스에서 가장 큰 문제는,
티켓 예매 관련 비즈니스 로직을 담당하는 ApplyViewModel이 지나치게 방대해졌다는 점입니다.
초기에 이 뷰 모델은 담당하는 기능이 몇 가지 되지 않아, 코드 길이도 적당했고 복잡도도 낮았습니다.
하지만 시간이 지나면서 기능이 추가되고, 제품이 성장함에 따라 ApplyViewModel은 어느새 800줄이 넘는 거대한 클래스가 되어버렸습니다.
더 심각한 문제는,
하나의 뷰 모델 안에 너무 많은 서로 다른 책임의 로직이 혼재되어 있다는 것입니다.
•
티켓을 예매하는 로직
•
티켓을 선택하거나 해제하는 로직
•
엔트리(신청권) 데이터를 생성하는 로직
•
서버와 통신하는 네트워크 요청 로직
•
결제 준비 및 결제 완료 처리 로직
이 모든 것들이 ApplyViewModel 안에 얽혀 있습니다.
어떤 함수는 티켓을 다루고, 어떤 함수는 결제를 다루고, 어떤 함수는 엔트리를 관리하는데,
서로 의존성과 흐름이 얽혀 있어 하나를 수정하면 다른 부분에서 오류가 발생하는 일이 자주 생기고 있습니다.
문제를 정리하면
•
SRP(단일 책임 원칙) 위반: 하나의 클래스가 여러 종류의 책임을 동시에 지고 있음
•
유지보수성 악화: 코드를 수정하거나 확장할 때 어디를 건드려야 할지 명확하지 않음
•
테스트 어려움: 단위 테스트를 짜기가 매우 어렵고, 자연스럽게 테스트 커버리지가 낮아짐
•
버그 발생 확률 증가: 의도치 않은 사이드 이펙트가 쉽게 발생함
•
개발 속도 저하: 신규 기능 추가나 기존 기능 수정이 점점 더 부담스러워짐
그래서 우리는 이번 SOLID 원칙 적용을 통해,
ApplyViewModel을 작은 책임 단위로 나누고,
각각의 로직을 서비스 레이어로 분리하여
더 깔끔하고 견고한 코드 구조를 만들어 가려 합니다.
SOLID 원칙 적용을 통한 MVVM 리팩터링 계획
현재 하입타운의 ApplyViewModel 리팩터링은
•
*SOLID 원칙을 적용해 책임 분리(Separation of Concerns)**를 가장 먼저 진행했습니다.
그 결과, 다음과 같은 구조로 나누었습니다.
1. TicketService
•
티켓 선택/해제 기능 담당
•
티켓 수량 증가(증분) 및 감소(감분) 기능 담당
•
티켓의 선택 상태, 수량, 유효성 관리
요약:
“사용자가 어떤 티켓을 몇 장 선택했는지”를 책임지는 서비스입니다.
2. EntryService
•
신청(Entry) 데이터 생성 및 서버와 통신하는 기능 담당
•
서버 통신을 위해 필요한 모델을 구성하고, 데이터 정리
•
현재 선택된 티켓들로부터 신청 가능한지 검증하는 로직 포함
요약:
“선택된 티켓들을 실제 구매 신청 데이터로 만드는” 과정을 책임지는 서비스입니다.
3. PaymentService
•
결제 관련 전체 흐름을 담당
•
토스페이먼트(TossPayments) 결제 위젯 호출
•
결제 승인 요청을 포함한 결제 관련 API 호출
•
결제 성공/실패에 따른 후속 처리 로직 관리
요약:
“결제 프로세스 전체를 관리하고, 외부 결제 서비스와 연결하는” 책임을 갖는 서비스입니다.
구조를 나누면서 적용한 SOLID 원칙
적용 원칙 | 어떻게 적용했나 |
SRP (단일 책임) | 각 서비스가 명확한 하나의 역할만 담당 |
DIP (의존관계 역전) | ViewModel은 서비스의 추상 타입만 의존 |
OCP (개방-폐쇄) | 새로운 결제 수단 추가 시 PaymentService만 확장 |
ISP (인터페이스 분리) | Ticket, Entry, Payment 역할을 각각 분리하여 필요 없는 메서드 강요 없음 |
현재 리팩터링 진행 현황
•
TicketService, EntryService, PaymentService의 기본 골격은 완성했습니다.
•
ApplyViewModel에서는 이제 직접 로직을 처리하지 않고, 세 가지 서비스에 작업을 위임(delegation)하는 구조로 리팩터링이 진행 중입니다.
•
리팩터링하면서 각 서비스별 단위 테스트를 추가하고, 기능별로 명확히 나누어 테스트 커버리지도 함께 개선하고 있습니다.
이제 앞으로는
각 서비스별 책임을 더욱 명확히 다듬고,
ViewModel 자체는 최대한 “상태 관리”와 “UI와의 연결”에만 집중하는 가벼운 구조로 리뉴얼할 계획입니다.
앞으로의 로드맵
이번 리팩터링은 단순히 ApplyViewModel 하나를 정리하는 데서 끝나지 않습니다.
앞으로 하입타운의 코드베이스 전체에 걸쳐 핵심 기능을 담당하는 주요 ViewModel을 대상으로
Service + Repository 패턴을 일관되게 적용할 계획입니다.
구체적인 방향
1.
핵심 뷰 모델부터 정리
•
티켓 예매, 결제, 이벤트 신청 등 사용자 경험에 직접 영향을 미치는 뷰 모델부터
•
비즈니스 로직을 서비스 레이어로 분리
2.
Repository 패턴 본격 도입
•
서버 통신, 로컬 저장소 접근 등 외부 의존성을 깔끔하게 분리
•
ViewModel과 Service는 Repository를 통해 데이터 접근
3.
SOLID 원칙 상시 점검
•
새로운 서비스나 뷰 모델을 작성할 때 항상 SOLID 원칙을 적용했는지 점검
•
책임이 혼재되지 않도록 코드 리뷰 시 체크리스트 도입
4.
테스트 가능한 구조 유지
•
모든 서비스와 레포지토리는 인터페이스를 기반으로 설계
•
목(mock)을 통한 단위 테스트가 가능한 구조 확보
최우선 목표
“치명적인 오류만큼은 절대 발생하지 않는 시스템을 만든다.”
지금 단계에서 가장 중요한 것은
모든 기능을 완벽하게 만들려고 욕심내는 것이 아니라,
사용자가 믿고 쓸 수 있는 최소한의 안정성을 확보하는 것입니다.
그 출발점이 바로
•
기능별 책임을 명확히 나누고
•
SOLID 원칙에 기반해 코드를 설계하며
•
오류가 생겨도 빠르게 찾고 고칠 수 있는 구조를 만드는 것이라고 생각합니다.
맺음말
하입타운은 문화와 경험을 연결하고, 사용자가 공연장으로 나갈 기대를
현실로 만들어내는 서비스를 만들고 있습니다.
그 과정에서 기술의 안정성은 단순한 선택이 아니라 책임이라고 생각합니다.
이번 SOLID 원칙 적용과 코드 리팩터링은 단순히 예쁜 코드를 짜기 위한 작업이 아닙니다.
15,000명이 넘는 사용자에게,
그리고 앞으로 더 많아질 하입타운의 유저들에게
믿을 수 있는 경험을 제공하기 위한 가장 기본적인 준비라고 생각합니다.
아직 부족한 점도 많고, 앞으로도 분명히 새로운 문제들이 생기겠지만
이번 과정을 시작으로
“어떤 기능이 추가돼도 치명적인 오류 없이 움직이는 시스템”을 만드는 토대를 쌓아가려고 합니다.
처음 이 글을 쓰기 시작할 때 다짐했던 것처럼,
하입타운은 제품이 커지는 만큼 코드도 함께 성숙해가는 서비스를 만들겠습니다.
그리고 이 과정을 팀원들과 함께 공유하고, 또 나중에 새롭게 합류할 사람들과도 함께 나눠
“좋은 문화 위에서 좋은 제품이 만들어진다” 는 믿음을 실제로 증명해 보이겠습니다.
긴 글 읽어주셔서 감사합니다.