5장에서 우리는 급여 보고서의 업무 규칙에서 생성 책임을 뜯어내 바깥으로 밀어냈다. SalaryReporter가 더 이상 new MySqlEmployeeDatabase()를 쓰지 않게 되자, 그 new는 사라진 것이 아니라 어딘가로 이사했다. 그 종착지가 Main이었다. 이 장은 그 Main을 정면으로 마주한다 — 모든 것이 결국 흘러 들어가는 그곳, 가장 구체적이고 가장 더러운 코드가 모여 사는 그곳. 마틴의 명제는 도발적이다. Main은 시스템에서 가장 낮은 수준의 모듈이며, 궁극의 세부사항이다.

Main은 궁극의 세부사항이다

동심원 다이어그램(22장)에서 우리는 고수준 정책을 안쪽에, 저수준 세부를 바깥쪽에 두었다. 그렇다면 Main은 어디에 있는가. 가장 바깥, 그것도 바깥 원의 바깥에 있다. Main보다 더 바깥은 없다. 어떤 컴포넌트도 Main에 의존하지 않고, Main은 거의 모든 것에 의존한다. 화살표를 그려 보면 Main에서 나가는 화살표는 무수히 많은데 Main으로 들어오는 화살표는 하나도 없다.

이것이 의미하는 바는 분명하다. Main은 시스템에서 가장 의존성이 낮은 축에 놓인 모듈, 즉 아무도 재사용하지 않고 아무도 이름을 알 필요 없는 코드다. 세부사항의 정의가 바로 이것이었다 — 무엇에도 의존받지 않고, 언제든 갈아 끼울 수 있으며, 핵심 정책이 그 존재를 몰라도 되는 것. Main은 그 정의를 가장 완벽하게 만족한다. 웹이 세부사항이고 DB가 세부사항이라면, Main은 그 세부사항들을 실제로 손에 쥐고 조립하는 세부사항 중의 세부사항이다.

그래서 마틴은 Main을 “가장 낮은 수준의 정책”이라고도 부른다. 얼핏 모순처럼 들린다 — 정책은 고수준이라고 하지 않았나. 그러나 Main도 하나의 정책이긴 하다. 다만 시스템의 초기 상태를 어떻게 세울지, 어떤 구현을 어떤 슬롯에 꽂을지를 결정하는, 가장 밑바닥의 초기화 정책이다. 헌법이 아니라 시행 세칙에 가깝다. 그것도 딱 한 번, 부팅 순간에만 유효한 세칙.

판단 기준: 어떤 모듈이 세부사항인지 알고 싶으면 “이 모듈을 아무도 import하지 않아도 시스템이 성립하는가”를 물어라. Main은 누구도 참조하지 않으므로 가장 순수한 세부사항이다. 함정: Main을 “시작점이니까 가장 중요한 코드”로 여기기 쉽다 — 중요도와 의존성 수준은 다르다. 진입점이라는 사실이 곧 고수준을 뜻하지는 않는다.

진입점은 조립 라인이다

Main이 실제로 하는 일은 놀랄 만큼 단순하다. 구체적인 부품을 만들어 상위 컴포넌트에 건넨 다음, 제어를 넘기고 물러난다. 5장의 마지막 스텝을 다시 보면 Main의 전형이 거기 있다 — new MySqlEmployeeDatabase()로 게이트웨이를 만들고, new ConsolePrinter()로 프레젠터를 만들고, 그 둘을 SalaryReporter의 생성자에 주입한 뒤 report()를 부르는 것. 그게 전부였다.

조금 더 현실적인 규모로 그려 보면 이렇다.

public class Main {
    public static void main(String[] args) {
        // 1) 설정을 읽는다 — 어떤 부품을 꽂을지 여기서만 안다
        Config config = Config.load(args);
 
        // 2) 구체 구현을 생성한다 — 시스템에서 유일하게 new가 허락된 곳
        EmployeeGateway gateway   = new MySqlEmployeeDatabase(config.dbUrl());
        ReportPresenter presenter = config.web()
                                    ? new HtmlPresenter()
                                    : new ConsolePrinter();
 
        // 3) 상위 컴포넌트에 주입하고 제어를 넘긴다
        SalaryReporter reporter = new SalaryReporter(gateway, presenter);
        reporter.report();
        // 4) 물러난다. 이후의 흐름에서 Main은 두 번 다시 등장하지 않는다
    }
}

여기서 눈여겨볼 것은 Main만이 구체 클래스의 이름 — MySqlEmployeeDatabase, HtmlPresenter, ConsolePrinter — 을 알고 있다는 사실이다. 나머지 시스템 전체는 인터페이스(EmployeeGateway, ReportPresenter)만 안다. 구현을 어떤 슬롯에 꽂을지 아는 코드는 시스템에 딱 하나 있어야 하고, 그게 Main이다. 이것이 우리가 의존성 주입(dependency injection)이라 부르는 것의 알맹이다 — 거창한 프레임워크가 아니라, **“객체를 쓰는 쪽이 그 객체를 만들지 않고 바깥에서 건네받는다”**는 배치. Main은 그 “바깥”의 이름이다.

config.web()에 따라 HtmlPresenterConsolePrinter 중 하나를 고르는 저 한 줄이 곧 전략(strategy)의 선택이다. 어떤 전략을 쓸지 정하는 결정은 업무 규칙 안으로 새어 들어가면 안 된다. 업무 규칙은 ReportPresenter 하나만 알아야지, 그것이 웹인지 콘솔인지 알아서는 안 된다. 그 분기 하나를 Main이 삼켜 준 덕에 SalaryReporter는 출력 매체의 존재 자체를 모르는 채로 남는다.

판단 기준: 시스템 전체에서 new로 구체 구현을 만드는 코드가 Main(또는 그것이 위임한 소수의 팩토리)에만 모여 있는지 확인하라. 업무 규칙 한복판에서 구체 클래스를 new하고 있다면, 그 new는 아직 이사하지 못한 것이다. 함정: “의존성 주입 = 프레임워크(스프링 등)“라고 오해하기 쉽다 — 주입의 본질은 컨테이너가 아니라 생성 위치를 바깥으로 밀어낸 배치다. 프레임워크 없이 Main의 생성자 호출 몇 줄로도 완전한 주입이 성립한다.

더러움을 한곳에 격리하는 플러그인

Main의 진짜 존재 이유는 여기서 드러난다. 시스템을 짜다 보면 어쩔 수 없이 지저분한 코드가 생긴다 — 환경 변수를 읽고, 커넥션 문자열을 조립하고, 스레드 풀을 만들고, 프레임워크를 초기화하고, if로 실행 모드를 가르는 일들. 이 코드들은 우아하게 만들 방법이 없다. 세상의 구체적인 사정(어떤 DB, 어떤 포트, 어떤 플래그)을 정면으로 받아내야 하기 때문이다.

이 더러움을 어디에 둘 것인가. 시스템 곳곳에 흩뿌리면 업무 규칙 사이사이에 구성 코드가 끼어들어 핵심이 오염된다. 마틴의 답은 격리다 — 이 지저분한 초기 구성을 전부 Main이라는 한 컴포넌트로 몰아넣고, 시스템의 나머지를 깨끗하게 유지한다. Main은 시스템의 쓰레기 하치장이 아니라, 더러움을 자발적으로 떠안아 다른 곳을 정갈하게 지키는 방파제다.

그리고 이렇게 격리하고 나면 Main 자체가 플러그인이 된다. 개발용 Main과 운영용 Main을 따로 두거나, 테스트에서는 가짜 게이트웨이를 꽂는 TestMain을 두는 식이다. 애플리케이션의 본체 — 업무 규칙과 유스케이스 — 는 그대로 둔 채, Main만 갈아 끼워 전혀 다른 구성으로 부팅할 수 있다.

// 운영 구성
public class ProdMain {
    public static void main(String[] args) {
        new SalaryReporter(
            new MySqlEmployeeDatabase(env("DB_URL")),
            new HtmlPresenter()
        ).report();
    }
}
 
// 테스트/데모 구성 — 업무 규칙 코드는 한 줄도 건드리지 않았다
public class DemoMain {
    public static void main(String[] args) {
        new SalaryReporter(
            new InMemoryEmployeeDatabase(),   // 가짜 데이터
            new ConsolePrinter()
        ).report();
    }
}

같은 SalaryReporter가 한쪽에서는 MySQL과 HTML로, 다른 쪽에서는 인메모리와 콘솔로 돈다. 업무 규칙은 자신이 어느 구성으로 실행되는지조차 모른다. Main을 플러그인으로 다루면, 애플리케이션을 컴파일된 라이브러리로 취급하고 그 위에 서로 다른 부팅 껍데기를 씌우는 일이 가능해진다.

판단 기준: 구성·초기화 코드가 지저분해질 때, 그것을 감추려 애쓰지 말고 Main으로 몰아넣어 격리하라 — 더러움은 없앨 수 없으니 한곳에 가두는 것이 최선이다. 함정: Main을 깨끗하게 만들겠다고 초기화 로직을 업무 규칙 쪽으로 분산시키면, 방파제를 허물어 더러운 물을 온 시스템에 들이는 셈이다. Main은 더러워도 되는, 더러우라고 있는 자리다.

Main과 애플리케이션은 컴파일 단위가 다르다

Main이 플러그인이라는 말에는 물리적 함의가 따라온다. 진짜 플러그인이 되려면 Main과 애플리케이션 본체가 별개의 컴파일·배포 단위여야 한다. 애플리케이션은 인터페이스만 노출하는 라이브러리(.jar, .dll)로 빌드되고, Main은 그 라이브러리를 가져다 구체 구현을 꽂는 별도의 실행 모듈로 빌드된다.

이 경계가 실제로 서 있는지 검증하는 방법은 화살표를 세는 것이다. 애플리케이션 라이브러리는 Main의 이름을 몰라야 한다 — SalaryReporterProdMainimport하는 순간 방향이 뒤집혀 둘은 한 덩어리로 묶인다. 의존성은 오직 Main → 애플리케이션 한 방향으로만 흘러야 하고, 그래야 애플리케이션을 다시 빌드하지 않고도 Main만 교체할 수 있다.

판단 기준: Main을 통째로 삭제하고 다른 Main으로 대체할 때 애플리케이션 라이브러리를 재컴파일할 필요가 없다면, 경계가 제대로 선 것이다. 함정: 폴더만 나누고 여전히 애플리케이션이 Main의 유틸리티를 참조하고 있으면(예: Main에 둔 공용 헬퍼를 업무 규칙이 갖다 쓰면), 물리적으로는 갈라도 의존성은 한 몸이다 — 5장에서 본 “폴더는 갈라도 import가 남아 있으면 한 덩어리”라는 함정이 여기서 그대로 반복된다.

정리

Main은 시스템에서 가장 흔히 과소평가되는 컴포넌트다. 진입점이라는 이유로 특별 대우를 받거나, 반대로 아무렇게나 짜도 되는 잡동사니 취급을 받는다. 마틴이 이 짧은 장에서 바로잡으려는 것은 그 둘 다다 — Main은 특별히 고귀하지도, 특별히 하찮지도 않다. 그것은 명확한 역할을 가진 하나의 세부사항 컴포넌트다.

  • Main은 시스템에서 가장 낮은 수준의 모듈이자 궁극의 세부사항이다. 아무도 그것에 의존하지 않고, 그것은 거의 모든 것에 의존한다.
  • Main의 일은 구체 구현(팩토리·전략·전역 구성)을 생성해 상위 컴포넌트에 주입하고, 제어를 넘긴 뒤 물러나는 것이다. 시스템에서 new가 허락되는 거의 유일한 자리다.
  • Main은 지저분한 초기 구성을 자기 안에 격리해 나머지 시스템을 깨끗하게 지키는 방파제다. 더러움은 없앨 수 없으니 한곳에 가둔다.
  • Main을 애플리케이션과 별도의 컴파일·배포 단위로 두면 Main 자체가 플러그인이 된다 — 운영·개발·테스트마다 다른 껍데기를 씌워 같은 업무 규칙을 다르게 부팅할 수 있다.

Main을 이렇게 이해하고 나면, 시스템을 “하나의 애플리케이션”이 아니라 “구성 가능한 부품들과 그것을 조립하는 얇은 껍데기”로 보게 된다. 다음 장은 이 조립의 시선을 더 큰 단위 — 서비스 — 로 끌고 올라간다. 서비스 경계가 과연 아키텍처 경계를 저절로 주는지를 따져 물으면서.

다음장으로 27장