Published on

회고록 백엔드 서버의 복잡성 관리

Authors
  • avatar
    Name
    Jihwan Seong
    Twitter

TL;DR

  • 정규화는 비일관성과 데이터 중복을 완화한다.
  • ERD와 정규화를 적용하여 백엔드의 복잡성을 관리한다.
  • DB의 복잡성은 서비스 로직의 복잡성에도 영향을 미친다.

본론

문제 정의 (Problem)

  • 어떤 문제가 발생했는가? (객관적 사실 중심, 정량/정성 데이터 포함)
  • 문제의 영향을 받는 주체(사용자, 시스템, 비즈니스)는 누구인가?

저는 이전 토이 프로젝트에서 프론트 서버와 백엔드 서버를 별도로 나눠서 진행했었는데요. 백엔드 서버에서 서비스 로직의 복잡성을 관리하기 어려운 문제가 있었습니다. 여기서 복잡성이란, 프로젝트 참여자가 프로젝트를 이해할 수 없는 정도를 의미하는데요. 복잡성이 높은 경우에는 다음과 같은 문제가 발생합니다.

복잡성으로 인한 문제

  1. 문제(버그,이슈,확장 등등)를 이해할 수 없는 경향이 높다.
  2. 변경 사항의 영향 범위를 예측할 수 없는 경향이 높다.
  3. 하위 시스템들이 복잡하게 얽혀서 독립적으로 하위 시스템을 이해할 수 없는 경향이 높다.

DB 테이블 수정 및 관리 그리고 백엔드 로직 변경에 시간이 오래 걸린다는 문제가 있었습니다. 제가 겪은은 문제는 다음과 같습니다.

직면한 문제

  • DB 테이블에 대한 서비스 클래스를 추상화가 어렵다.
    • 클래스가 해결해야할 문제를 이해하기 어렵다는 의미입니다.
    • 클래스가 추상화해야할 서브 시스템을 이해하기 어렵다는 의미입니다.
  • HTTP API 추가 및 변경시, 서비스 로직 수정 범위가 예상보다 넓다.
    • 개발자가 변경 사항의 영향 범위를 예측하기 어려운 것을 의미합니다.
  • DB 테이블 변경시, 마이그레이션 작업 비용이 크다
    • 마이그레이션 작업 비용이 크다는 것은 다음을 의미합니다.
      • 마이그레이션이 해결해야할 문제를 이해하기 어려움
      • 마이그레이션 영향 범위를 알 기 어려움
      • 마이그레이션 타겟 서브시스템을 이해기 어려움

새로운 웹 프로젝트에서는 이전에 경험한 문제를 해결하고자하였습니다. DB와 ORM에 대한 지식만으로는 서비스 로직에 내재된 복잡성을 관리하기 어려웠습니다. DB에 관한 지식은 DBMS를 통해 DB 자체를 관리하는 것에 집중되어 있고, ORM은 DB 클라이언트에 대한 코드 레벨의 인터페이스 제공이 중심이기 때문에, DB에 저장된 데이터와는 연관이 매우 적기 때문입니다.

여러 영역을 분석한 결과, 결론적으로 DB 데이터를 분석한 결과 원인은 데이터의 비일관성과 데이터 중복 때문이라고 판단했습니다.

비일관성 및 데이터 중복 문제점

  1. 서비스 클래스 추상화를 어렵게 만듭니다.
  • 하나의 테이블에 다양한 도메인의 존재는 서비스 클래스 간의 강한 결합을 초래하므로 도메인을 정의하기 어려워집니다..
  • 데이터 중복은 동일한 기능을 가진 인터페이스 중복을 초래하므로 다양한 클래스에 책임이 겹칩니다.
  1. 서비스 로직 수정 범위를 예측하기 어렵게 만듭니다.
  • 중복 데이터간의 정합성 유지를 위해서 HTTP API 요청의 검증 로직이 복잡해집니다.
  • 서비스 로직에서 다양한 다양한 서비스 클래스에 접근하기 때문에, 영향받는 클래스가 많아집니다.
  • 데이터의 비일관성에 의한 서비스 로직의 일관성이 로직의 영향범위를 예측할 수 없게 만듭니다.
  1. DB 마이그레이션이 작업량이 증가합니다.
  • 테이블간에 중복된 데이터를 모두 고려하여 마이그레이션 작업을 진행해야합니다.
  • 비일관적인 테이블 구조와 데이터가 해결해야할 문제를 이해하기 어렵게 만듭니다.

목표 설정 (Goal)

  • 어떤 상태를 달성해야 문제가 해결되었다고 판단할 수 있는가?
  • 성공 기준(지표, 조건)을 명확히 수립.

새로운 프로젝트에서 서비스 로직의 복잡성을 관리하기 위해서 목표를 세웠습니다.

목표

  1. 설계 및 구현 시간을 늘리지 말아야함
  2. 비일관성 및 데이터 중복 완화하기
  3. 백엔드 복잡성 관리하기

방법 탐색 결과 다양한 접근법이 존재했습니다. 따라서 가장 중요한 시간 자원을 아끼기 위해서 설계 및 구현 시간을 늘리지 않는 것을을 우선적으로 고려하였습니다. 2번째 목표는 첫 프로젝트의 문제 원인을 해결하기 위함입니다. 제가 판단한 문제 원인은 DB 테이블 비일관성과 데이터 중복이었기 때문에 반드시 해당 문제를 해결해야한다고 생각했습니다. 3번째 목표는 실제로 제가 직면한 문제 해결입니다. 비일관성과 데이터 중복을 해결했지만 복잡성이 증가할 수도 있기 때문에 목표로 추가하였습니다.

행동 및 실행 (Act)

  • 실제로 무엇을 했는가? (객관적, 구체적 기록)

방법 선택 저는 ERD을 사용하여 설계하고 DB 정규화 방법을 적용하기로 하였습니다. ERD는 이전에 경험적이 있어서 익숙하고, 시각적 효과를 제공하므로 러닝커브가 낮다고 판단했습니다. DB 정규화 방법은 비일관성과 데이터 중복 완화에 사용되므로 2번째 목표에 부합하며, 전통적인 방식이고 학습자료가 많아서 러닝커브가 낮다고 생각했습니다.

ERD와 DB 정규화는 모두 러닝커브는 낮다고 판단했지만, DB 설계 시간이 증가되어 전체 설계 및 구현 시간이 증가될 수 있습니다. 하지만 저는 DB 설계 시간 증가가 결론적으로 구현 시간을 줄여줄 것이므로 전체 시간 비용이 늘어나지 않는다고 판단했습니다. DB 설계 과정에서 논리적 오류를 발견하고 해결하는 것이 구현 단계에서 수정하는 비용보다 매우 저렴합니다. 구현 도중에는 설계를 변경하는 것은 이미 걸어간 거리를 다시 되돌아가 가야하기 때문입니다. 또한 DB 설계 과정은 도메인에 대한 이해를 높여주므로 구현 시간을 더욱 단축할 수 있을 것이라 생각했습니다.

정규화는 여러 단계가 있으므로 어느 단계 까지 적용할지 선택해야합니다. 각 단계를 진행할 수록 DB 데이터의 비일관성과 데이터 중복을 줄일 수 있지만, 테이블이 작은 테이블로 쪼개져 DB 구조가 복잡해지고, JOIN 연산이 증가하여 성능 저하로 이어질 수 있는 문제가 있습니다.

저는 3단계(3NF)까지 적용하기로 결정하였습니다. 일반적으로 3단계 또는 3.5단계(BCNF)까지 적용하기 때문입니다. 설계 시간과 러닝 커브를 고려하여 3단계까지 적용하기로 결정하였습니다. 3단계 적용 후 유동적으로 변경할 수 있는데요. DB 마이그레이션을 통해 역정규화 과정을 거쳐서 2단계로 다운그레이드하거나 정규화 과정을 거쳐서 3.5단계로 업그레이드할 수 있기 때문입니다.

정규화로 인한 DB 구조 복잡성 증가와 성능 저하 문제는 충분히 감당할 만하고 해결할 수 있는 리스크라고 판단하였습니다.

DB 구조 복잡성 증가는 SQL 쿼리 난이도 증가, DB 관리 난이도 증가로 이어질 수 있습니다. SQL 쿼리 난이도는 추후 쿼리 최적화 역량 강화 경험으로 이어질 수 판단하여 감당할만 하고, DB 관리 난이도 증가 또한 DB 마이그레이션 역량 강화 경험으로 이어질 수 있다고 판단하여 감당할만 하다고 판단했습니다.

성능 저하 문제는 현재 상황에서 감당할만 합니다. 실 사용자가 없는 서비스이고, 규모가 크지 않기 때문에 조인 연산이 10개 이하로 발생할 것이기 때문입니다. 추후 성능 저하 문제를 해결해야한다면, DB 내적 그리고 DB 외적으로 해결할 수 있다고 판단하였습니다. DB 내적으로는 인덱스 튜닝 및 수직적 및 수평적 파티셔닝을 통해 해결할 수 있습니다. 인덱스 튜닝을 통해서 조인 연산에서 타겟 튜플을 빠르게 탐색할 수 있도록 할수 있습니다. 수직적 파티셔닝을 통해 열(컬럼) 개수를 줄여서 조인 연산을 줄일 수 있습니다. 수평적 파티셔닝을 통해서 행 개수를 줄여 조인 연산에서 타겟 튜플을 빠르게 탐색할 수 있습니다. DB 외적으로는 Scale up, DB 파라미터 설정,쿼리 캐싱, 읽기 쓰기 분산이 있습니다. Scale up과 DB 파라미터 설정은 DB 엔진에 더 많은 자원을 할당하여 성능 향상을 이룰 수 있습니다. 쿼리 캐싱은 조인 연산이 대부분 발생하는 읽기 결과를 캐싱하므로써 읽기 성능을 높일 수 있습니다. 읽기 쓰기 분산은 조인 연산 대부분이 발생하는 읽기 전용 DB 서버와 쓰기 전용 DB 서버를 분리하여 트래픽을 분산시킬 수 있습니다.

방법 적용

ERD 툴을 사용하였고, 작은 규모에서 DB 설계를 진행하며 점진적으로 DB 규모를 키워나갔습니다.

  • 1NF
  1. 테이블에서 비 원자값을 갖는 속성을 원자값으로 쪼개기.
    • 이때, 원자값을 갖는 속성의 데이터는 복제한 레코드(행)가 쪼개진 원자값 개수만큼 추가된다.
  2. 유사한 데이터를 저장하는 복수의 속성을 하나의 속성으로 표현하기.
    • 2번과 마찬가지로, 복수의 속성 개수 만큼 레코드가 추가된다.
  • 2NF
  1. 기본키에 full functional dependency 하지 않은 속성들은 각각 별도의 새로운 테이블로 분리하기
  2. 새로 생성된 테이블도 1NF와 2NF 정규화를 적용하기
  3. 새로운 테이블들의 외래키를 참조하기
  • 3NF
  1. 기본키에 직접적으로 의존하지 않는 속성들은 각각 별도의 새로운 테이블로 분리하기
    • Transitive FD 를 통해 기본키에 의존하는 간접적으로 의존하는 속성들이 해당됨
  2. 새로 생성된 테이블에 정규화 1NF,2NF,3NF를 적용하기

최종 DB 설계계는 링크를 참조해주세요

검증 (Validation)

  • 문제 해결 여부를 어떻게 검증했는가?
  • 객관적 검증 지표, 실험, 사용자 피드백 포함.

실제 검증은 DB 설계 완료 후 프로젝트를 진행하면서 목표가 달성되었는지 평가하였습니다.

목표

  1. 설계 및 구현 시간을 늘리지 말아야함
  2. 비일관성 및 데이터 중복 완화하기
  3. 백엔드 복잡성 관리하기

설계 및 구현 시간을 늘리지 말아야함 검증증

설계 및 구현 시간은 제 예상 스케줄과 실제 스케줄을 비교하여 검증했습니다.

DB 설계에 ERD와 정규화를 진행한 결과 설계 시간이 예상보다 오래 걸렸습니다. 정규화에 익숙하지 않는 점과 설계 단계에서의 오류 수정 작업 때문입니다. 하지만 개발에서는 예상보다 빠르게 진행할 수 있었습니다. 설계 단계에서 도메인에 대한 이해 증가와 논리적 결함 사전 해결이 구현 단계에서 구현에 집중할 수 있게 해주었기 때문입니다. 이전 프로젝트에서는 구현 단계에서 설계의 결함을 수정하는 빈도가 높아서 구현 작업 도중 설계를 다시 되돌아가는 일이 빈번했습니다.

구현 단계에서 설계를 수정해야하는 경우가 있었습니다. 설계 단계에서 발견하지 못한 결함 또는 서비스 로직 스펙 변경 때문입니다. 이러한 경우에는 DB 마이그레이션을 통해서 DB를 수정하였는데요. 설계 단계에서의 도메인 이해 증가 덕분에 문제점을 빠르게 탐색하고 해결책을 신속하게 선택할 수 있었습니다. 테이블 컬럼 추가와 새로운 테이블로 쪼개는 작업 등의 DB 확장 작업은 빠르게 DB 마이그레이션을 적용할 수 있었습니다.

하지만 DB 마이그레이션 작업 난이도가 증가했습니다. 기존 테이블에 분산된 데이터를 새로운 테이블로 집중시키는 작업, 테이블 간에 Many-To-Many 관계 형성을 중계하는 Junction Table 생성하는 작업 등은 난이도가 높았습니다. 처음에는 해결하는데 시간이 걸렸지만, SQL 그리고 DB 설계에 대한 지식을 쌓아나감으로써 시간을 단축할 수 있었습니다. 복잡성 증가로 인한 문제는 설계 및 구현 비용을 예측할 수 없지만, 높은 난이도 문제는 지식 학습과 경험을 통해 설계 및 구현 시간을 예측할 수 있습니다.

비일관성 및 데이터 중복 검증 비일관성 및 데이터 중복 완화는 DB 설계 후, 프로젝트를 진행하며 비일관성 및 데이터 중복으로 인한 문제가 어떻게 변화하였는지를 기준으로 검증하였습니다.

  1. 클래스 추상화가 쉬어졌습니다.
  • 하나의 테이블에 하나의 도메인이 존재하므로 서비스 클래스가 필요한 기능만 갖으므로 도메인을 정의하기 쉽습니다.
  • 데이터가 테이블에 집중되므로, 클래스마다 고유한 기능을 가진 인터페이스를 제공하여 책임이 덜 겹칩니다.
  1. 서비스 로직 수정 범위를 예측하기 쉬어졌습니다.
  • 서비스 로직단의 검증 로직이 간략해졌습니다.
    • 데이터 일관성이 증가하여 DB에서 제약 조건 관리가 쉬어진 결과, DB 데이터 정합성이 높아졌습니다.
    • 테이블에 분산된 중복된 데이터가 하나의 테이블로 집약되므로 데이터 검증 조건과 범위가 줄어들었습니다.
  • 서비스 로직에서 접근해야할 서비스 클래스가 줄어들었습니다.
  1. DB 마이그레이션 작업량은 줄었지만 작업의 난이도가 증가했습니다.
  • 중복된 데이터가 사라져 작업량이 줄어들었습니다.
  • 일관적인 테이블 구조와 데이터가 작업의 복잡도를 완화했습니다.
  • DB 마이그레이션시 제약 조건, 관계형 테이블을 고려하고, 복잡한 SQL 또는 조인 연산을 사용하므로 난이도가 증가했습니다.
    • 충분히 감수할만한 난이도라고 판단했습니다. 데이터 중복과 비일관성으로 인해 인지하기 어렵고 영향 범위를 예측할 수 없는 문제보다는 학습과 경험을 통해 해결할 수 있는 문제가 복잡성을 관리에 더 적합하다고 판단했습니다.

백엔드 복잡성 관리 검증

백엔드 서버의 코드 레벨과 DB 서버의 테이블 레벨에서 복잡성을 평가하였습니다. ERD와 정규화 적용의 영향 범위에 반드시 포함되는 영역이라고 판단했기 때문입니다.

  1. SQL문

테이블 간의 조인이 증가하여 SQL문이 복잡해졌습니다. 하지만 관리할 수 있는 문제입니다. 코드 레벨에서는 typeORM 인터페이스를 사용하면 직접 SQL문을 사용하지 않고, 조인 연산 및 복잡한 쿼리를 코드로 표현할 수 있습니다. DB 마이그레이션 처럼 직접 SQL을 사용해야하는 경우에는 작업 난이도가 높아지고 문제를 이해하기 어려워 질수 있지만, SQL문 학습과 경험을 통해 해결할 수 있는 문제라고 판단했습니다. typeORM 인터페이스와 SQL 학습을 통해서 문제를 분석하기 쉽게 만들고, 변경사항의 영향 범위를 예측할 수 있게 만들며, SQL문 서브시스템을 독립적으로 이해할 수 있게 만들수 있다고 판단하였습니다.

  1. DB 테이블

테이블간의 관계 증가와 테이블 개수 증가 때문에 테이블 구조가 복잡해졌습니다. 하지만 관리할 수 있는 문제입니다. 테이블 간의 관계는 외래키 참조, Junction Table을 통해서 테이블 레벨 내에서 관계를 명시하여 복잡성을 관리할 수 있다고 판단했습니다.

테이블 증가는 서비스 클래스 증가를 초래했지만 서비스 클래스의 복잡성을 관리하기 쉬어졌습니다. 서비스 클래스의 책임이 명확해지고 추상화를 통해 가독성이 증가한 덕분입니다. 책임이 명확해진 덕분에 문제가 발생한 영역을 규정하기 쉬어졌고, 변경 사항의 영향 범위를 예상하기 쉬어졌기 때문입니다.

  1. 데이터

데이터 일관성 덕분에 DB 데이터에 대한 Race Condition, 무결성 손상 그리고 정합성 깨짐을 유발하는 상황을 인지하기 쉬어졌습니다. 예전 프로젝트에서는 데이터 이상 현상을 예측하기 어려웠기 때문에, 문제 발생시 트래킹이 어려웠고 안정성 높은 방식으로 핸들링하기 힘들었습니다. 하지만 현재 프로젝트에서는 문제 트래킹이 쉬워졌고, 예외 상황 발생 지점을 서비스 로직에서 예측하기 쉽고 제약조건을 통해 관리하기 쉬으므로 안정성 높게 핸들링할 수 있게 되었습니다.

  1. 서비스 로직

서비스 로직의 동작이 논리적으로 분할되기 쉬어지고, 기능 책임이 명확해졌기 때문에 복잡성 관리가 쉬어졌습니다. 문제 발생시 서비스 로직이 복합적으로 동작해도 작업 단위마다 변경하는 데이터 또는 상태가 정해진 덕분에 문제를 쉽게 트래킹하고 이해하기 쉬어졌습니다. 서비스 로직의 영향 범위를 논리적 예측할 수 있습니다. DB 테이블과 SQL과 독립적으로 서비스 로직 서브시스템을 이해하기 쉬어졌습니다.

결과 (Outcome)

  • 어떤 결과가 나왔는가? (정량/정성 데이터로 표현)
  • 목표와 얼마나 일치했는가?

결과적으로 3가지 목적 중에서 첫번째를 제외한 목적을 달성했습니다. 첫 번째 목적은 달성하지 못했지만 시사할 점을 통해 깨달은 바가 있었습니다.

  1. 전체적인 설계 및 구현 시간은 증가했습니다.

제 예상과 달리 전체 시간은 증가했습니다. 다만 시사할 만한 점은 초기 단계에서 설계 및 구현 시간이 증가했지만, 이후에는 줄어든다는 점이었습니다. 이전에 언급했던 것 처럼 초기에는 정규화 및 ERD 적응과 높은 난이도의 DB 마이그레이션 작업으로 설계 및 구현 시간이 증가했습니다. 하지만 이후에 기능 추가 또는 변경 사항으로 인한 설계 및 구현 시간은 이전 프로젝트에서 체감한 시간 보다 줄어들었습니다.

저는 프로젝트에서 백엔드 뿐만 아니라, 프론트 서버와 배포도 담당하게 되어 병렬적으로 작업을 진행했습니다. 작업을 병렬로 진행하면 작업 대상을 다시 이해할 시간이 필요한데요. 이러한 과정에서 이전에 발견하지 못한 결함을 발견하여 수정하는 경우가 있습니다. 하지만 이번 프로젝트에서는 백엔드 서버에서 이러한 경향이 적었습니다. 그 결과로 기능 추가 또는 변경 사항 구현으로 인한 시간 비용이 이전 프로젝트보다 줄었습니다.

  1. 비일관성 및 데이터 중복이 완화되었습니다.

DB 마이그레이션 작업의 난이도가 증가한 점을 제외하고는 대부분의 문제가 긍정적으로 변화했습니다.

  1. 백엔드 복잡성을 관리할 수 있습니다.

4가지 영역의 복잡성을 관리할 수 있습니다. SQL문과 DB 테이블의 복잡성이 증가하지만, 학습과 경험을 통해서 관리할 수 있습니다. 또한 상대적으로 데이터와 서비스 로직의 복잡성이 낮아진 덕분에 전체적으로 복잡성 관리가 쉬어졌습니다.

인사이트 및 학습 (Insights & Lessons)

  • 이번 과정에서 배운 점은 무엇인가?
  • 어떤 원인이 문제를 일으켰고, 어떻게 예방할 수 있는가?

제가 서브 시스템 의존성과 설계의 중요성을 깨달았다면, 이전 프로젝트에서 겪은 문제를 30%이상 줄일 수 있었다고 생각합니다.

  1. 서브시스템 의존성

서브 시스템인 백엔드 서버가 DB 서버에 의존한다는 사실을 알 수 있었습니다. 서비스 로직의 복잡성이 DB 설계 및 관리에서 매우 큰 영향을 받는다는 사실을 깨달았습니다. 그 이유는 서비스 로직이 DB 자원(데이터,테이블 등)의 인터페이스이기 때문이라고 생각합니다. 개발자는 요구사항에 맞게 DB 자원에 접근하고 변경하기위해서 서비스 로직을 정의합니다. 개발자가 DB 자원을 이해하기 어렵다면, 서비스 로직을 정의하기 어렵기 때문입니다.

  1. 설계의 중요성

설계는 미래의 비용을 절감하기 위한 방법임을 다시 한번 깨달았습니다. 이전 프로젝트에서는 코드 레벨에서 DB 설계 및 관리를 진행하는 동시에 서비스 로직도 함께 구현하였습니다. 결과적으로 설계의 결함을 구현단계에서 수정하게되어, 이미 쌓인 결과물들을 전체적으로 변경하는데 매우 큰 비용이 발생하였습니다. 설계 단계에서 논리적 결함을 줄이면 줄일수록 구현 단계에서의 수정비용은 급격히 감소하게 됩니다.

실무에서는 설계가 변경되거나 스케줄 때문에 설계의 결함 발견이 어려운 경우가 있습니다. 이러한 상황때문에 설계에 시간을 덜 쓰자는 주장이 힘을 얻기도 합니다. 하지만 그렇다 하더라도, 현재 정의된 설계를 최대한 검토해야 한다고 생각하게 되었습니다. 비용을 줄일 수 있는 방법이 분명하게 존재하는데, 하지 않을 이유가 없기 때문입니다.

향후 계획 (Next Step)

  • 같은 문제가 재발하지 않도록 어떤 개선/예방 조치를 취할 것인가?
  • 추가로 탐색하거나 실험할 부분은 무엇인가?

오픈소스 프로젝트 기여를 통해서 저의 인사이트와 복잡성 관리를 실무에 적용해보려고 합니다.