Spring

[Spring] Tech Spec 작성부터 리팩토링 과정

가든잉 2025. 6. 29. 10:00

이전에도 리팩토링에 대한 회고를 한 번 쓴 적이 있었다. 하지만 지금 돌이켜보면 대부분 결과론적인 이야기였다. 어떤 결과가 있었고, 그로 인해 어떤 성과가 있었는지만 말했지, 왜 그렇게 결정했는지, 어떤 대안을 고민했는지, 그 선택이 어떤 트레이드오프를 동반했는지에 대한 기록은 없었다. 그래서 이번에는 내가 리팩토링을 하며 Tech Spec을 작성하게 된 이유와, 그 과정에서 어떤 기술적 배경과 판단이 있었고 어떻게 선택했는지를 자세히 남겨두고자 한다.

 

1. 왜 Tech Spec을 쓰게 되었는가?

새로운 프로젝트에 투입되었을 때, 너무나 명확하게 보였다. 구조는 무너져 있었고, 코드와 데이터는 얽히고설켜 있었고, DAO, Controller, Service의 경계는 없었으며, 무엇보다 이 시스템이 어떻게 돌아가는지 아무도 설명해 줄 수 없었다. 따라서 리팩토링을 빠르게 결정하게 되었다.

 

하지만 문제의 범위가 워낙 다양하고 복잡했기 때문에, 어디서부터 손대야 할지 쉽게 판단하기 어려웠다. 한 번에 모든 것을 고치려고 하면 끝없이 범위가 커지고, 오히려 중요한 목표를 놓치게 될 것 같았다. 그래서 이번에는 처음부터 내가 바꾸고자 하는 핵심 목표를 문서로 명확히 정의하고, 리팩토링 과정에서 다른 길로 새지 않도록 기준점을 만들어두고 싶었다. 필요 이상의 변경을 하지 않고, 내가 해결해야 할 문제에만 집중하기 위해 Tech Spec을 작성하기로 했다.

Tech Spec의 필요성

많은 블로그에서는 Tech Spec이 두 명 이상의 팀에서 커뮤니케이션 비용을 줄이는 데 효과적이라고 이야기한다. 하지만 개인적으로 혼자라도 Tech Spec을 작성해 보는 것이 좋다고 생각한다. 작성하는 과정에서 내가 내린 결정을 되짚고, 다양한 대안을 검토하고, 의사결정을 구체적으로 설명할 수 있기 때문이다. 결국 이 과정은 개발 속도를 늦추는 것이 아니라 오히려 명확한 목표를 만들어 시간을 단축시켰다. 또한 체계가 잡혀있지 않은 환경에 문서화 문화를 도입하는 계기가 될 것이라 생각했다.

 

실제로 나의 경우엔 정말 다양한 문제가 있는 상황이라 정신이 없었지만 테크스펙을 작성하면서 문제를 정의하고 목표를 설정하며 문서화해두면서, 다른 길로 새지 않고 정확하게 원하는 목표에 도달할 수 있었다. 처음 테크스펙을 작성하며 고민하는 시간이 소요되긴 했지만, 결과적으로는 오히려 시행착오를 줄이고 문제를 명확히 바라볼 수 있게 도와줬다.

 

2. 내 Tech Spec 문서의 구성

Tech Spec을 본격적으로 쓰기 위해, 구글링을 많이 해봤다. 실제로 뱅크샐러드나 당근마켓 같은 여러 회사들의 기술 블로그를 보며, 대략적으로 어떤 형식으로 작성하는지를 배웠다. 물론 거기엔 현재 내 상황과 맞지 않는 항목들도 있었기 때문에 나에게 우선적으로 필요한 것만으로 커스터마이징 한 나만의 템플릿을 만들었다.

 

나는 이정도 요소들을 포함시켜 템플릿을 만들었다.

  • Summary: 전반적인 요약
  • Background: 개발을 하게 된 전반적인 배경
  • Goal / Non-Goal: 무엇을 개선하려는가, 목표가 아닌 것
  • Architecture Decision: 선택한 기술/패턴과 그 근거, 대안과 비교
  • Milestones: 일정 및 단계별 주요 작업
  • Considerations: 설계나 구현 시 주의해야 할 점, 리스크, 제약사항

이 정도면 내가 기록하는데 충분할 것 같았다. (물론 나중에 더 체계적으로 진행하고 싶으면, 예상 리스크나, 기대 반응 등 다양한 요소를 넣어도 될 것 같다.) 이렇게 내가 꼭 필요한 내용들을 고민해 보고 작성 과정에서 지피티에게 템플릿을 만들어 달라했다. 지피티가 귀염뽀짝하게 잘 만들어주니 문서를 작성할 땐 활용해 보는 것도 좋은 선택인 것 같다..~

 

3. 실제 문제 해결의 배경과 목표

나의 Background는 유지보수성과 확장성이 낮은 구조를 개선해야 하는 상황이었다. 기존에 나뉜 Controller, Service, DAO의 경계가 불분명해 로직을 파악하거나 변경하는 데 큰 어려움이 있었다. 더불어 모든 수정이 큰 비용을 수반했고, 문제를 해결해도 같은 문제가 다른 곳에서 반복되었다. 또한 데이터 접근 방식이 전부 JDBC에 의존하고 있어 코드 중복과 실수 가능성이 높았고, 테스트나 리팩토링이 쉽지 않았다. 이러한 문제를 해결하기 위해 계층형 아키텍처를 재정립하고, 도메인 모델과 영속성 계층을 분리하며, 계층별 책임을 명확히 하고자 했으며, JPA를 활용해 반복적인 개발 비용을 줄이고 생산성을 높이고자 했다.

 

Goal은 명확했다.

  • 구조를 정리해 유지보수성을 높인다.
  • 코드와 데이터의 일관성을 확보한다.
  • 앞으로의 기능 확장이 수월하게 만든다.

물론 Non-Goal도 분명히 해야 했다.

  • 모든 기능을 새로 개발하거나 API 응답 형식을 대폭 변경하지 않는다.
  • 기존 비즈니스 로직의 근본적인 변경도 시도하지 않는다.

테크스펙을 작성하면서 생각보다 좋았던 것은 Non-Goal인데, 이처럼 Non-Goal을 정의해 두면, 범위를 넘어서는 리팩토링으로 리소스를 소모하지 않고, 정확히 필요한 부분에 집중할 수 있다. 실제로 이걸 정확히 정리해 둬서 덕분에 명확하게 변경사항만 바꾸고 그 외의 부가적인 개발을 하는데 시간이 소요될 일이 없어서 빠르게 개발할 수 있게 됐다.

 

 

4. 그래서 어떻게 변경했는가?

1) 변경된 레이어드 아키텍처 구조

사실 기존 구조자체는 3-Tier 아키텍처였다. Controller, Service, DAO로 구분되어 있긴 했지만, 각 계층이 제 역할을 하지 못하고 서로의 책임이 뒤섞여 있었다.

 

실제로 아래는 코드의 일부를 가져왔다.

 

Controller는 Provider, Service, JwtService까지 모두 직접 의존하고 있었다. 이 구조만 봐도 어떤 책임이 어디에 있어야 하는지가 모호해진다. ClipController 내부에서 JWT 유효성 검사, 비즈니스 분기, 응답 조립까지 모든 작업이 뒤섞여 있었다.

 

또, Service 계층에서는 단순히 DAO에 존재 여부를 묻고 insert/update하는 로직만 담당하거나, Provider에 다시 처리를 위임하는 식이었다. 결국 Service와 Provider의 경계가 흐려졌고, 어디서 어떤 책임을 지는지가 분명하지 않았다.

 

Provider 계층도 애매했다. 이름만 보면 Query Service(조회 전용) 역할을 할 것 같지만, 실제로는 단순히 DAO를 한 번 더 포장하는 정도였다. Provider에서 하는 일은 대부분 DAO에 쿼리를 위임한 뒤 결과를 그대로 돌려주는 것이었다. 이러면 Service에서 Provider를 호출하나 DAO를 호출하나 크게 다를 바 없었고, 계층이 하나 더 생겼다는 점에서 오히려 복잡도만 높아졌다.

 

DAO는 더 큰 문제를 가지고 있었다. SQL 쿼리 작성과 조회뿐 아니라, 조회 결과를 DTO로 매핑하거나, 할인율 계산 같은 비즈니스 로직도 함께 처리하고 있었다. 예를 들어 getCodies() 메서드 안에서 코디 리스트를 조회하고, 거기서 또 하위 Product 데이터를 조립하며, 할인율을 계산해서 DTO에 담는다. 이 작업이 전부 DAO에 몰려있었고, 테스트나 재사용이 사실상 불가능했다.

 

이처럼 각 계층의 역할이 나눠진 것이 아닌 단순 클래스 분리에 가까웠다.. 더불어 결국 각 계층의 의존 관계도 꼬였다. Controller는 Service뿐 아니라 Provider, JwtService를 모두 의존했고, Provider와 Service는 둘 다 DAO를 의존하고 있었다. 심지어 인증/인가 처리도 Controller, Service, Provider에 중복으로 흩어져 있었다. 이런 구조에서는 작은 기능 하나를 수정할 때도 어디를 고쳐야 할지 확신이 들지 않았다.

 

이런 구조의 문제는 명확했다.

  • 로직이 여러 레이어에 흩어져 있어 어디서 어떤 처리를 하는지 알기 어렵다.
  • 같은 기능을 여러 계층에서 중복으로 구현하게 된다.
  • 테스트 코드 작성이 사실상 불가능하다.
  • 로직 수정 시 어떤 계층에 손대야 할지 명확하지 않다.

그래서 이 문제를 해결하기 위해 나는 계층 간 책임을 분명하게 나누고, 의존 관계를 단순화하는 쪽으로 방향을 잡았다.

 

이 다이어그램은 내가 개선한 아키텍처이며, 각 계층은 단일 책임 원칙(SRP)을 따르도록 분리했다.

  • Controller : 오직 API 입출력과 요청 흐름만 처리한다.  Controller는 Service에 요청을 위임하고 결과만 반환한다.
  • Service: 트랜잭션 관리와 비즈니스 로직만 담당한다. 단순 데이터 조회/저장 호출도 Service에서만 이루어지며, 이전에 Provider가 하던 “DAO 래핑” 계층은 제거하거나 Service에 통합했다.
  • Repository: JPA Repository 인터페이스를 통해 데이터 접근을 추상화한다. 필요한 커스텀 쿼리는 RepositoryImpl에서 담당하며, 표준 CRUD는 JpaRepository에서 위임받는다.
  • Domain: Entity가 아닌 순수 비즈니스 객체이다. 비즈니스 로직과 상태를 표현하며, Service와 Repository 사이에서 도메인 모델로서 동작한다.
  • Entity: JPA 영속성 관리에만 집중하는 객체. 데이터베이스 테이블에 대응된다.

이 구조에서는 각 계층이 오직 하위 계층에만 의존하며, 의존 역전 원칙을 지키기 위해 Repository도 인터페이스로 정의해두었다.

 

사실 3-Tier 구조에서는 Service가 비즈니스 로직만 잘 처리하고, 나머지가 각자의 역할만 정확히 수행한다면 기본적인 기능을 만드는 데에는 굳이 레이어를 많이 둘 필요는 없다고 생각한다. 레이어를 하나 더 둔다고 해서 자동으로 책임이 분리되는 게 아니기 때문에 오히려 명확한 기준 없이 계층만 늘리면, 그 레이어가 단순히 DAO를 한 번 더 감싸는 역할에 그쳐 버리고, 복잡성만 증가할 것이다. 기존의 Provider 계층이 그 전형적인 예였다. 그래서 나는 추가 레이어를 최소화하고, 각 계층의 역할을 정확히 규정하는 것에 더 집중했다.

 

결국 이번에 크게 변화한 점은, Domain을 엔티티와 분리하고 Repository를 별도로 추상화한 것이었다. 이전에는 DAO에서 곧바로 Entity나 DTO를 다루면서 비즈니스 로직까지 처리했지만, 지금은 Repository가 오직 데이터 영속성 처리에만 집중하고, Domain이 순수 비즈니스 개념과 상태를 담는 역할을 맡았다. 이렇게 계층을 나누면 로직의 위치가 명확해진다.

 

그리고 JpaRepository를 활용해 표준 CRUD를 빠르게 처리할 수 있도록 했지만, 사실 JPA는 어디까지나 기술적인 선택지일 뿐이다. 언제든지 MyBatis, QueryDSL, 또는 아예 다른 데이터 접근 기술로 교체될 수 있다. 그래서 Repository를 인터페이스로 추상화하고, JPA 관련 구현체는 RepositoryImpl에 따로 두었다. 이렇게 하면 영속성 계층만 교체해도 나머지 비즈니스 로직과 API 코드는 그대로 유지할 수 있다.

 

(사실 이 부분은 이전에 책을 읽고 정리해 둔 블로그 내용과도 연결된다. 그때도 객체의 역할을 분리하고, 의존성을 어떻게 관리할지에 대해 고민했던 적이 있었다. 하지만 책으로만 배울 때는 “이론적으로 그렇구나” 하고 넘어가기 쉽다. 이번에는 그걸 실제 프로젝트에 직접 적용하면서, 단순히 계층을 나누는 것이 아니라 어떤 책임을 어디에 둘 것인지, 어떤 계층이 무엇을 몰라야 하는지를 구체적으로 정의해보고 싶었다.)

 

2) 왜 JDBC를 버렸나?

사실 처음에는 기존 JDBC를 유지하는 방안을 가장 먼저 고려했다. JDBC는 단순하고, 자바 생태계에서 가장 오래된 표준이기도 하다. 실제로 이전 코드에서는 JdbcTemplate을 중심으로 쿼리를 작성하고, ResultSet으로 데이터를 꺼내서 DTO에 매핑하는 방식으로 모든 데이터 처리를 수행하고 있었다.

 

물론 JDBC에도 장점이 있다.

  • 직관적이다. SQL이 그대로 노출되기 때문에 쿼리를 어떻게 작성하는지 명확히 알 수 있고, 성능을 미세하게 튜닝하기도 수월하다.
  • 학습 난이도가 낮다. ORM을 처음 접하는 사람에게는 JDBC가 더 이해하기 쉽다.
  • 불필요한 추상화가 없다. 프레임워크가 숨기는 동작이 없어서, 모든 데이터 흐름이 개발자 눈에 보인다.

하지만 이번 프로젝트에서 JDBC를 그대로 유지하기에는 단점이 너무 뚜렷했다.

  • 동일한 CRUD 로직의 중복: insert, update, select 같은 반복 작업이 계속해서 중복됐다. 사실상 DAO 메서드마다 PreparedStatement를 열고, 파라미터를 채우고, ResultSet을 DTO로 수동 매핑해야 했다.
  • 실수 가능성이 높았다. 파라미터 개수나 순서를 맞추지 못해 발생하는 버그가 잦았고, Connection close를 깜빡하면 리소스 누수가 생겼다.
  • 비즈니스 로직과 데이터 로직의 혼재: DAO에서 데이터를 꺼내 DTO를 조립하면서 동시에 할인율 계산, 필드 가공 등을 처리했다. 이 과정이 점점 복잡해져 테스트가 어려워졌다.
  • 유지보수 비용: 테이블 구조나 컬럼이 바뀌면, 쿼리문뿐 아니라 ResultSet 매핑 로직까지 일일이 고쳐야 했다.

따라서 JDBC가 무조건적으로 좋지 않다고 생각하진 않지만 바꿔야겠다 생각했다. JDBC를 대체할 대안으로는 MyBatis, QueryDSL, JOOQ, JPA 등.. 이 있을 것이다. 여러 대안을 비교했지만, 이번에 JPA를 선택한 이유는 솔직히 대단한 이유가 있진 않았다.

가장 큰 이유는 내가 JPA에 익숙했기 때문이었다. 이전 프로젝트에서도 JPA를 활용해 봤기 때문에, 이번에도 빠른 시일 안에 개발을 마치고 안정적인 결과물을 낼 수 있었다. 그리고 이 선택이 가능했던 이유는 사실 위에서 이야기한 아키텍처 구조와도 연결된다.

Repository를 인터페이스로 추상화하고, Service와 Domain이 JPA에 전혀 의존하지 않는 구조를 만들어두었기 때문에, 지금은 JPA를 쓰더라도 나중에 MyBatis나 QueryDSL, 혹은 다른 데이터 접근 기술로 교체하는 것이 어렵지 않다.

어차피 영속성 계층은 기술에 불과하다. 언제든 바뀔 수 있다. 내가 지금 무엇을 선택하든, 그 결정이 시스템 전체를 흔들지 않도록 아키텍처 자체를 유연하게 가져가는 것이 더 중요하다고 판단했다. 결국 JPA를 선택한 것은 “내가 가장 빠르게 쓸 수 있고, 표준 CRUD의 생산성을 확보할 수 있었다”는 현실적인 이유가 컸고, 그 선택을 뒷받침해 준 건 앞서 만든 책임이 분리되고 의존성이 느슨한 계층 구조였다.

 

3) 도메인 주도 설계인줄 알았지만 아니었던 과정(단지 용어를 명확하게 한)

기존에도 나는 나름대로 도메인 주도 개발을 하고 있다고 생각했다. 하지만 돌아보면, 실제로는 도메인을 나누기 모호한 부분들이 많았고, 그래서 데이터 주도 개발에 훨씬 가까웠다. 실제로 개발을 진행할 때도 ERD나 테이블 중심으로 모델을 먼저 정의하고, 그 모델이 곧바로 코드에 반영되는 식이었다. 그 과정에서 "도메인"이라는 개념은 단순히 DB 컬럼 집합과 크게 다르지 않았고, 그 안에서 어떤 책임과 의미를 갖는지 충분히 고민하지 못했다.

이번 리팩토링에서는 도메인을 더 분명하게 정의해보고 싶었다. 특히 플랫폼이 인스타그램, 유튜브 등으로 확장되고, 서비스가 커지면서 "코디", "셀럽", "상품" 같은 개념이 애매하게 섞여 있었다. 기획자, 디자이너, 프론트엔드와 대화를 할 때도 각자 다른 언어를 쓰고 있었고, 그로 인해 혼란이 자주 생겼다. 이 문제를 해결하기 위해, 처음부터 도메인 언어를 명확히 하고, 각 도메인 객체의 역할과 책임을 구분하려고 시도했다.

 

물론 도메인 주도 설계(DDD)가 정답은 아니다. (실제로 이걸 한 번 해보고 나니 더더욱 느꼈다..) TDD(테스트 주도 개발), BDD(행위 주도 개발) 같은 방법론도 있지만, 이번 프로젝트에서는 "서비스의 핵심 개념을 통일된 언어로 표현하고, 그 언어를 코드와 데이터에 일관되게 녹이는 것"에 초점을 맞췄다. 그렇게 해야 앞으로도 서비스가 커져도 같은 개념이 계속 유지될 수 있다고 생각했다.

 

이렇게 도메인을 분리하는 시도에는 여러 장점이 있었다.

  • 기획자, 디자이너, 개발자가 같은 언어로 대화할 수 있다.
  • 코드가 데이터베이스 스키마에 종속되지 않고, 비즈니스 개념에 맞게 설계된다.
  • 설계와 구현이 더 일관되고 예측 가능해진다.

 

하지만 현실은?

하지만 DDD를 시도하면서 가장 크게 느낀 점은, 처음 설계와 정리가 생각보다 훨씬 더디다는 것이었다. 무엇보다 "이게 정말 이 서비스의 핵심 도메인이 맞는가?"에 대한 의문이 계속 생겼다.

사실 시간이 지날수록, 결국 데이터 모델과 도메인 모델이 점점 닮아가는 모습을 확인할 수 있었다. 책에서 이야기하는 것처럼, 마치 도메인 전문가가 따로 존재해서 개념과 용어를 명확히 잡아주는 이상적인 환경이 아니라면, 혼자서 도메인을 정의하고 유지하는 일은 쉽지 않았다. (요즘은 개발자에게 이런 역량이 어느 정도 필요시 되지만, 실제로 해보니 여전히 어렵다는 생각이 든다.)

더불어 우리 서비스 자체가 아직 초기 단계라, 도메인으로 복잡하게 구분할 만큼의 비즈니스 요구사항이 충분히 쌓여있지 않았다는 점도 있었다. 그렇다 보니 도메인을 나누고 이름을 붙여도, 실제로는 데이터 모델과 크게 다르지 않은 상태로 머무르게 된 부분이 아쉽다.

 

이런 고민은 Entity와 도메인을 분리하는 과정에서도 그대로 이어졌다. 사실 이렇게 Entity와 Domain을 구분한 이유는, 위에서 Repository를 인터페이스로 분리했던 이유와 본질적으로 같았다. 영속성 계층은 언제든 기술이 바뀔 수 있다. 그런데 도메인과 엔티티를 동일시해 버리면, 결국 다시 JPA나 특정 기술에 종속될 수밖에 없다고 판단했다. 따라서 Repository를 교체하거나 다른 기술로 전환하더라도 비즈니스 로직과 서비스 코드에 영향이 가지 않도록 하고 싶었다. 그래서 Entity를 영속성 전용으로, Domain을 비즈니스 로직 전용으로 철저히 분리했다. 하지만 막상 해보니, 파일의 양이 방대해지고 매핑 로직도 많아져 구현 시간이 오래 걸리는 단점이 있었다.

 

처음에는 테이블을 설계하며 "이제 용어를 좀 더 명확하게 해야겠다"는 목표로 도메인을 구분했지만, 되돌아보면 그냥 테이블 명과 컬럼 이름을 조금 더 구체적으로 짓고 그것을 도메인이라고 치부한 측면이 컸던 것 같다. 따라서 굳이 도메인 주도 개발이라는 이름을 붙일 필요는 없었을 것 같기도 하다. 단순히 엔티티를 명확히 설계하고, 비즈니스 로직과 JPA 엔티티를 분리하는 것만으로도 충분히 코드의 책임을 구분할 수 있었다. 그런데 당시에는 “비즈니스 로직을 Domain으로 빼면 그게 곧 도메인 주도 개발이다!”라고 생각해 무턱대고 구조를 나누었고, 그게 오히려 부담이 되고 복잡성을 키운 면이 있었다. 결국 중요한 건 어떤 방법론을 따르는가가 아니라, 왜 그 책임을 분리하는지, 그게 정말 이 프로젝트에 필요한 일인지를 더 깊이 고민하는 것이었는데, 그 점이 부족했던 것 같다.

 

4) 도메인 언어 통일과 더불어 ERD 구조 개편

 

실제로 기존에는 ERD도 존재하지 않았다. 위에 보이는 다이어그램은 DataGrip으로 DB에 직접 연결해 테이블 스키마를 추출한 것이다.

 

우리 서비스는 콘텐츠(코디, 상품 등)를 보여주는 시스템이었는데, 처음에는 유튜브만 지원하는 구조였다. 내가 투입되었을 때는 인스타그램으로 기능을 확장하는 시점이었고, 이때부터 기존 설계의 한계가 명확히 드러나기 시작했다.

아마 초기에 이 서비스가 여러 플랫폼으로 확장될 것이라고 예측하지 못했던 것 같다. 그래서 유튜브 채널과 인플루언서를 사실상 동일 개념으로 보고 Celeb 테이블 하나에 통합해 두었고, 유튜브 관련 데이터가 전부 이 테이블에 담겨 있었다.

 

그런데 인스타그램으로 콘텐츠를 확장하기 시작하자, 새로운 테이블들이 무리하게 _Insta 식으로 붙여지면서 점점 혼란이 커졌다. 예를 들어 유튜브에서만 쓰던 칼럼과 인스타그램에서만 필요한 칼럼이 뒤섞였고, 콘텐츠가 비슷하면서도 다른 성격이라 테이블을 분리해야 하는데도 구분하지 못한 채 그대로 운영됐다. 결국 도메인과 테이블 네이밍 모두 뒤죽박죽이 되어, 협업 시에도 “어떤 데이터가 어디에 저장되는지” 매번 설명해야 했다.

 

이런 상황을 개선하기 위해, 확장 가능성을 고려한 ERD 재설계를 진행했다.

저 Celeb 테이블만 보더라도 처음에는 모든 정보를 그냥 Celeb 테이블에 몰아넣을까 라는 생각도 했었다. 하지만 실제 기획 단계에서 한 인플루언서가 여러 플랫폼 아이디(유튜브, 인스타그램 등)를 가질 수 있다는 니즈가 있었고, 이 요구사항이 앞으로 더 늘어날 수 있었다. 그래서 아래처럼 1:N 구조로 설계를 바꿨다.

 

이 사진처럼 하면 하나의 셀럽이 여러 플랫폼 프로필을 가질 수 있고, 각각의 데이터가 혼재되지 않으며, 테이블을 더 확장하더라도 큰 구조 변경 없이 이어갈 수 있다. 실제 기획 요구사항에서는 이름이나 프로필 이미지는 동일하게 유지됐기 때문에, 공통으로 자주 사용되는 필드만 Celeb 테이블에 두고, 각 플랫폼별로만 필요한 정보는 별도 테이블에 분리했다.

 

이 과정은 사실 위에 이야기했던 “도메인 언어를 명확하게 하겠다”는 목표와도 연결된다. 테이블 이름과 도메인 네이밍을 다시 검토하면서, 단순히 데이터 컬럼의 나열이 아니라 비즈니스 개념에 가까운 이름으로 재정비했다. 덕분에 코드와 DB 모두에서 용어가 일관되게 맞춰졌고, 이후 프론트, 기획자와의 협업에서도 용어 혼선을 줄일 수 있었다.

 

추가로 ERD 변경 로직 중에 가격에 대한 부분도 개선했다. 기존에는 Price 테이블에 regularPrice, price, discount를 모두 저장하고 있었고, sale 테이블에도 유사하게 가격 정보가 중복되었다. 이 구조에서는 단일 상품의 가격 정보가 여러 테이블에 퍼져 있었고, 할인율 또한 저장된 값을 그대로 사용하는 방식이었다. 그 결과 정가나 할인가 중 하나만 갱신되어도 데이터 간 불일치가 발생했고, 데이터 불일치가 진행될 경우 잘못된 할인율을 가지게 되어 문제가 자주 발생했다.

 

이를 해결하기 위해 가격 관련 테이블을 통합하고, 할인율 필드를 제거했으며, 정가와 할인가만 저장하도록 변경했다. 이후 할인율은 Domain 계층에서 조회 시에 실시간으로 계산하게 했다. 이렇게 하면 데이터 중복과 불일치를 없앨 수 있었고, 가격 변경에도 항상 최신 할인율을 보장할 수 있게 됐다. 

 

이 외에도 Cody, Codies와 같은 기존의 모호한 테이블들과 필드 타입, 필드 명까지  좀 더 명확하게 같이 변경했다. 

 

5) GlobalException

기존에는 전역 예외 처리 체계가 존재하지 않았다.그래서 코드 곳곳에서 try-catch 블록이 반복적으로 등장했고, 계층별로 동일한 예외를 중복 처리하거나, 심지어 어떤 곳에서는 아예 처리하지 않고 런타임까지 전파되기도 했다.

예를 들어 Service에서 특정 조건이 맞지 않을 때 단순히 return null을 반환하고, Controller에서 null을 감지해 다시 에러 응답을 생성하는 식으로 예외 처리가 이루어졌다. 그러다 보니 다음과 같은 문제가 반복됐다.

 

  • 어떤 계층에서 어떤 상황에 예외가 발생하는지 한눈에 파악하기 어렵다.
  • Controller마다 try-catch 로직이 중복되고, 코드의 양이 불필요하게 많아진다.
  • 일관되지 않은 HTTP 응답 포맷이 생성된다.
  • 특정 계층에서는 Exception이 그대로 터져버려 500 에러가 빈번하게 발생한다.

 

이를 해결하기 위해 @RestControllerAdvice를 이용해 GlobalExceptionHandler를 만들었다.

더불어 목표는 다음과 같았다.

  • 예외 응답 형식을 통일해 프론트엔드와 협업을 수월하게 한다.
  • 중복된 try-catch 코드를 제거한다.
  • 로직과 예외 처리를 분리해 가독성을 높인다.

도메인별 주요 예외를 BaseException으로 통합하고, 각 예외에 ErrorCode를 부여해 상태코드와 메시지를 관리했다. 예상치 못한 RuntimeException도 별도 처리해 안정성을 확보했다.

{
  "code": "PRODUCT_NOT_FOUND",
  "message": "해당 상품을 찾을 수 없습니다."
}

다음과 같이 일관된 포맷으로 변환한 뒤불필요한 예외 처리 코드가 줄어 로직이 깔끔해졌고, 협업도 쉬워졌으며, 로그 추적도 쉽게 할 수 있게 됐다.

 

 

마무리

사실 이번에는 단순히 기술적인 문제를 고치는 것 뿐만 아니라, 구조 자체를 어떻게 가져가야 할지를 고민하고 설계하는 과정이었기에 더 어렵게 느껴졌다. 그래도 다행인 건 그때 당시 도메인 주도 설계나 실용주의 책을 읽으면서 많은 도움을 받았던 것 같다. 

한 번에 변경 규모를 크게 잡았던 터라, 도메인 주도 설계처럼 체계적으로 완결 짓지는 못한 부분도 있었던 것 같다. 테크 스펙을 작성하며 기준을 세웠음에도, 도메인 언어나 테이블 구조를 명확하게 하려던 시도가 결과적으로 DDD라고 치부된 면이 조금은 아쉽다.

그래도 이번 경험 덕분에, 테크 스펙을 작성하는 일에 조금 더 익숙해졌고, 앞으로는 이런 문서화와 설계 과정에서 범위를 더 명확히 좁히고, 중요한 것에 더 집중하는 연습을 해야겠다는 생각이 들었다. 물론 덕분에 기존보다 훨씬 명확한 기준과 의사결정을 바탕으로 리팩토링을 진행할 수 있었고, 이를 통해 협업할 때도 훨씬 수월하게 소통할 수 있었다는 점은 큰 성과라고 생각한다.

 

지금도 새롭게 하고 있는 게 있는데... 나중에 다시 회고하고 돌아와야겠다...~