Go의 Layered 설계 방식

2 weeks ago 9

  • Go 언어는 패키지 간 순환 참조를 엄격히 금지하기 때문에, 이는 자연스럽게 계층적 설계(layered design) 를 유도함
  • 이 글은 Go 프로젝트를 의무적으로 갖게 되는 계층 구조를 설명하고, 그 위에 별도의 아키텍처를 강제하지 않아도 충분히 유효하다고 주장함
  • 순환 의존이 발생할 경우, 이를 해결하기 위한 구체적이고 실용적인 리팩터링 전략을 단계별로 제시함
  • 각 패키지는 독립적으로 의미 있는 기능 단위를 가지도록 설계되어, 테스트, 유지보수, 마이크로서비스 분리에도 유리함
  • 결과적으로 이 방식은 실제 코드 설계에서 흔히 발생하는 "바나나를 원했는데 정글을 들고 온다"는 문제를 방지함

Go에서의 레이어드 설계 접근법

기본 원칙

  • Go는 패키지 간 순환 참조 금지
  • 모든 Go 프로그램의 import 관계는 유향 비순환 그래프(DAG) 를 구성해야 함
  • 이 구조는 선택이 아닌 언어 레벨에서 강제되는 설계 규칙

패키지 레이어링의 자동 형성

  • 외부 패키지를 제외한 프로젝트 내부 패키지들은 참조 깊이에 따라 자동으로 레이어링 가능
  • 아래 그림처럼 최하단에는 metrics, logging, 공용 자료구조 등 핵심 유틸리티 패키지가 위치
  • 이후 상위 패키지들이 점점 기능을 조합하면서 위로 쌓이는 구조를 형성

이 설계 방식의 특성

  • 레이어는 계층적 추상화가 아닌 참조 방향성에 기반
  • 하나의 패키지는 다수 하위 레벨 패키지를 참조 가능
  • MVC, 헥사고날 아키텍처 등 기존 설계 방식도 이 구조 위에 "적용" 가능함
    → 단, Go의 구조적 제약을 반드시 고려해야 함

순환 참조 해결 전략

순환 참조 발생 시 아래 순서대로 리팩터링을 시도:

1. 기능 이동

  • 가장 추천되는 방식
  • 순환을 유발하는 기능을 정확히 분석해, 논리적으로 적절한 위치로 옮김
  • 자주 쓰이진 않지만 개념적 명확성을 가장 많이 향상시킴

2. 공용 기능을 별도 패키지로 분리

  • 양쪽에서 공통으로 쓰는 타입이나 함수(Username 등)를 제3 패키지로 이동
  • 패키지가 작아 보일지라도 과감히 분리
    → 시간이 지나며 해당 패키지가 커질 가능성이 높음

3. 상위 조합 패키지 생성

  • 순환하는 두 패키지를 조합하는 제3 패키지를 생성
  • 예: Category, BlogPost 양방향 의존을 상위 패키지로 분리
    → 하위 패키지는 dumb struct로 유지, 실제 기능은 상위 패키지에서 조합

4. 인터페이스 도입

  • 구조체나 함수가 필요한 메서드만 가진 인터페이스로 의존성 치환
  • 불필요한 의존성 제거 및 테스트 편의성 확보
  • 단, 지나치게 사용하면 오히려 설계가 복잡해질 수 있음

5. 복사(Copy)

  • 의존 대상이 매우 작을 경우, 간단히 복사해서 사용
  • DRY 위반처럼 보일 수 있지만 실제로는 설계 명확화에 도움되는 경우 많음

6. 하나의 패키지로 합치기

  • 위 방법이 모두 불가능하면 두 패키지를 병합
  • 너무 큰 패키지가 되지 않는다면 수용 가능
    → 단, 무조건적인 병합은 지양하고 신중히 결정

이 설계 방식의 실용적 장점

  • 각 패키지는 스스로 의미 있는 기능 단위를 가지며 독립 테스트 가능
  • 패키지 내 참조가 제한되어, 전체 코드 이해 없이도 개별 패키지 이해가 가능
  • 의도하지 않은 전체 의존성 연결(=정글 문제)을 피하고, 필요한 것만 사용하는 코드 작성 유도
  • 마이크로서비스 분리 시에도 손쉽게 추출 가능
    → 대부분의 종속성이 명확히 정의되어 있음

결론

  • Go의 패키지 설계 제약은 귀찮은 제약이 아닌 좋은 설계 유도 장치
  • 특별한 아키텍처 없이도 패키지 간 참조 구조만으로도 견고한 설계 구현 가능
  • 순환 참조에 대한 정교한 분석과 리팩터링 전략은 Go뿐만 아니라 타 언어에도 유효

Read Entire Article