우리가 모노리포를 유지하는 방식, 그리고 왜 DLL 경계가 더 중요한가

제가 운영하는 회사는 기본적으로 Monorepo를 지향합니다. 그래서 폴더 구조 역시 "코드를 예쁘게 정리하자"가 아니라, 의존성 관리와 재사용을 어떻게 할 것인가를 기준으로 설계되어 있습니다.

많은 분들이 "기능 단위냐, 도메인 단위냐"를 고민하시는데, 저는 조금 다른 관점에서 접근합니다.

핵심은 폴더가 아니라 프로젝트(.csproj) 단위, 즉 DLL 단위의 분리입니다.

Level 0 - 제품(Product) 단위 분리

가장 먼저, "이 코드는 어떤 제품에 속하는가?"를 명확히 나눕니다.

  • Academy: POCU 아카데미 관련 코드
  • ProctoredExamService: 온라인 시험 감독 서비스
  • Engine: 여러 제품이 공통으로 사용하는 코드 모음 (사실상 내부 미들웨어)

이 단계에서 이미 제품 경계가 명확해집니다. 코드가 어디 소속인지 애매하면, 그 자체로 구조가 흔들리기 시작합니다.

Level 1 - 프로젝트(.csproj) 단위

여기서 가장 중요합니다.

각 제품 안에는 여러 개의 .csproj가 존재합니다.

예를 들어:

  • Academy.Services
  • Academy.Buildfarm
  • Shop (최종 사용자가 보는 웹 앱)

Level 1은 단순 폴더가 아닙니다. 곧 DLL 경계입니다.

제가 Level 1을 나누는 진짜 이유

기능 분리? 도메인 분리? 아닙니다.

공통 코드의 올바른 dependency 관리와 접근 제어를 위해서입니다.

운영 방식

1) 앱 전용 코드

특정 앱에서만 사용하는 코드는 그냥 그 프로젝트 안에 둡니다. 그 안의 폴더 구조는 팀 합의에 따라 자유롭게 구성합니다.

체감상 폴더 구조가 개발 효율에 미치는 영향은 10% 정도입니다.

현실에서는 보통 이렇게 탐색합니다.

  • Go to Definition
  • Search All References
  • 전체 검색 (Ctrl + Shift + F)
  • 빌드 에러로 역추적

이게 훨씬 빠르기 때문입니다.

2) 공유 코드

두 개 이상의 앱에서 동시에 필요한 코드가 생기면 Academy.Libs 같은 공통 라이브러리에 넣습니다.

네임스페이스는 폴더 구조에 의해 자동 결정됩니다.

예:

Academy.Libs/Services/Order/OrderService.cs
-> namespace Academy.Services.Order

그리고 나중에 이 폴더를 통째로 Academy.Services.csproj로 독립시켜도 namespace는 그대로 유지됩니다.

의존성(csproj 참조)만 다시 연결하면 끝입니다.

이게 굉장히 중요합니다. 빠르게 넣고, 필요할 때 바로 빼서 독립시킬 수 있어야 합니다.

3) 더 세분화가 필요할 때

공유 코드가 커지면 별도 라이브러리로 분리합니다.

예:

  • Academy.Entities: ORM 엔티티 + query extensions
  • Academy.Services: 공통 서비스 로직

여기서 중요한 건 접근 제어 전략입니다.

접근 제어와 협업 규칙

Academy.EntitiesAcademy.Services의 많은 클래스는 internal입니다.

필요한 경우에만 InternalsVisibleTo로 특정 프로젝트에 접근 권한을 줍니다.

왜 이렇게까지 하냐면,

  • ORM 엔티티
  • 핵심 서비스 로직
  • 성능에 직결되는 코드

이 영역은 junior가 자유롭게 수정하면 사고가 날 확률이 높기 때문입니다.

실제 협업 규칙

  • Shop 프로젝트
    • 누구나 수정 가능
    • 리뷰 없이 main 머지 가능
  • Academy.Services / Academy.Entities
    • senior 이상만 수정 가능
    • junior는 senior 리뷰 없이는 머지 불가

폴더 구조가 개발 효율에 주는 영향이 10% 정도라면, 권한 제어 체계는 그보다 훨씬 큰 영향을 줍니다.

진짜 생산성과 안정성을 만드는 건 여기라고 생각합니다.

NuGet 크기 문제로 분리하기도 합니다

예를 들어 clang 툴셋을 Academy.Libs 안에 넣으면 모든 앱 배포 파일이 수백 MB씩 커질 수 있습니다.

그래서 별도 DLL로 분리합니다.

이 역시 "기능 단위냐 도메인이냐"의 문제가 아니라 배포 전략과 의존성 관리의 문제입니다.

Level 2 - 프로젝트 내부 폴더

이 단계는 유연합니다.

  • Services
  • Models
  • Entities
  • TransferData

저는 보통 이렇게 둡니다.

  • DTO는 TransferData
  • ViewModel은 Models
  • DB 엔티티는 Entities

하지만 이 구조는 자주 바뀝니다.

  • 기능이 늘 때
  • 개념이 재정의될 때
  • 제가 실수했을 때

C# IDE는 네임스페이스와 참조를 자동으로 고쳐주기 때문에 파일 이동은 부담이 거의 없습니다.

옮기고 컴파일 돌리면 바로 확인 가능합니다.

실제 탐색은 어떻게 하느냐

현실은 이렇습니다.

  • 90%는 IDE 탐색
  • 10%만 폴더 계층을 따라 내려감

그래서 제 철학은 이렇습니다.

폴더 구조는 관리 편의를 위한 도구일 뿐이다.
진짜 중요한 건 프로젝트 레벨의 의존성 관리와 접근 제어다.

정리

  1. Level 0, 1은 반드시 체계적으로 잡아야 합니다
  2. Level 1은 기능/도메인 구분이 아니라 DLL 경계입니다
  3. Level 2 이하는 유연하게 바꿔도 됩니다
  4. IDE 탐색 + 컴파일이 유지보수의 핵심입니다
  5. 중복 제거와 권한 제어가 실제 생산성과 안정성을 만듭니다

한 줄로 요약하면 이렇습니다.

빠르게 넣고, 필요할 때 바로 빼서 독립시킬 수 있는 구조.
그리고 그 위에 강력한 권한 제어를 얹는다.

제가 운영하는 회사의 모노리포 운영 방식은 이런 방향입니다.

img

제대로 대우받는 개발자 | 부족한 컴공지식 배우기 | MIT급 컴공인강

최저임금으로 고통받는 일회성 프로그래머는 그만! POCU 아카데미가 올해 연봉협상을 책임지겠습니다!