클래스는 코드가 모이는 단위이면서 변경의 단위이기도 하다. 그래서 클래스를 어떻게 구성하느냐에 따라 코드를 읽는 흐름과 수정할 때의 위험이 달라진다. 이 장을 관통하는 핵심은 단순하다. 클래스는 작아야 하고, 하나의 책임에 집중해야 한다. 함수의 크기를 줄 수로 재듯이, 클래스의 크기는 “책임의 수”로 잰다.

클래스의 구성

자바 클래스는 보통 위에서 아래로 추상화 수준이 내려가도록 배치한다. 정적 공개 상수, 정적 비공개 변수, 비공개 인스턴스 변수 순으로 변수를 두고, 공개 메서드 아래에는 그 메서드가 사용하는 비공개 메서드를 가깝게 둔다. 이렇게 배치하면 독자는 공개 동작을 먼저 보고, 필요한 만큼만 세부 구현으로 내려갈 수 있다. 이는 신문 기사처럼 위에서 아래로 읽히는 함수 배치(3장)를 클래스 수준으로 확장한 것이다.

캡슐화는 기본적으로 내부 구현을 숨기는 방향을 택한다. 변수와 유틸리티 함수는 가능하면 공개하지 않는 편이 좋다. 판단 기준: 테스트나 같은 패키지의 협력이 필요한 경우처럼 이유가 분명하다면 protected나 package-private으로 제한적으로 열어둘 수 있다. 함정: 테스트를 위해 무심코 public으로 열면 캡슐화가 조금씩 무너진다. 열더라도 최소한의 가시성을 택한다.

작은 클래스와 단일 책임

클래스 이름이 모호해지거나 Processor, Manager, Super 같은 단어에 기대게 된다면 여러 책임이 섞였을 가능성이 크다. 클래스를 설명할 때 “그리고”, “또는”, “하지만”이 자주 필요하다면 나눌 시점이다.

단일 책임 원칙(SRP)은 클래스가 변경될 이유를 하나만 가져야 한다는 원칙이다. 예를 들어 SuperDashboard가 GUI 컴포넌트도 관리하고 소프트웨어 버전 정보도 추적한다면, 이 클래스는 서로 무관한 두 가지 이유로 변경된다. 아래 스텝 플레이어는 이 God class에서 버전 관리 책임을 뽑아내는 과정을 세 단계로 보여준다.

Refactoring Step 원본 — SuperDashboard 가 GUI 와 버전을 함께 관리
1/3
public class SuperDashboard extends JFrame implements MetaDataUser {    public Component getLastFocusedComponent() { ... }    public void setLastFocused(Component c) { ... }    public int getMajorVersionNumber() { ... }    public int getMinorVersionNumber() { ... }    public int getBuildNumber() { ... }    // 변경 이유가 둘: GUI 포커스 정책 / 버전 표기 정책}

판단 기준: “이 클래스가 바뀔 이유를 한 문장으로 말할 수 있는가”가 SRP의 리트머스다. 두 문장이 필요하면 두 클래스다. 함정: 작은 클래스가 많아지면 전체 구조를 보기 어렵다고 느낄 수 있다. 하지만 큰 클래스 하나에 여러 책임이 얽혀 있는 편이 훨씬 위험하다. 도구 상자에 큰 서랍 하나로 모든 부품을 쏟아 넣는 것과, 이름 붙은 작은 서랍 여러 개로 정리하는 것의 차이다. 시스템은 작고 목적이 분명한 클래스가 “많이” 모여 돌아가는 편이 건강하다.

응집도

응집도는 클래스의 메서드와 변수가 얼마나 밀접하게 연결되어 있는지를 말한다. 각 메서드가 클래스의 인스턴스 변수를 많이 사용할수록 응집도가 높다. 반대로 어떤 메서드는 이 변수만 쓰고 다른 메서드는 전혀 다른 변수만 쓴다면, 서로 다른 책임이 한 클래스에 섞였다는 신호다.

// 응집도 낮음 — 두 변수 묶음이 서로 겹치지 않는다
public class Report {
    private List<Row> rows;      // aggregate() 만 사용
    private PrintStream out;     // render() 만 사용
    public Total aggregate() { ... }  // rows 만 만짐
    public void render() { ... }      // out 만 만짐
}

큰 함수를 작은 함수로 나누다 보면, 몇몇 함수가 특정 변수 묶음에만 의존하는 모습이 드러난다. 이때 억지로 한 클래스에 붙들어 두기보다 새 클래스로 분리하면 구조가 선명해진다. 판단 기준: 인스턴스 변수를 두 그룹으로 나눌 수 있고 각 그룹이 서로 다른 메서드 집합에만 쓰인다면, 그 지점이 클래스를 가르는 자연스러운 절단면이다. 함정: 큰 함수에서 지역 변수 몇 개를 필드로 승격했을 때 응집도가 뚝 떨어지는 일이 흔하다. 이건 “클래스를 쪼개라”는 신호이지, 필드 승격이 틀렸다는 뜻이 아니다.

flowchart TD
    A["클래스가 커진다"] --> B{"변경 이유가<br/>둘 이상인가?"}
    B -->|예| S["SRP: 책임별로 분리"]
    B -->|아니오| C{"변수 묶음이<br/>메서드 그룹마다<br/>따로 노는가?"}
    C -->|예| S2["응집도 회복:<br/>새 클래스로 추출"]
    C -->|아니오| K["유지 — 이미 응집도 높음"]

변경하기 쉬운 클래스

깨끗한 시스템은 변경이 생겼을 때 영향을 받는 범위가 작다. 요구사항은 계속 바뀌므로, 클래스가 구체적인 구현에 직접 매달릴수록 변경 비용이 커진다. 인터페이스와 추상화를 사용하면 세부 구현의 변화가 바깥으로 새어나가지 않게 만들 수 있다.

예를 들어 포트폴리오가 주가를 계산할 때 변동이 잦은 외부 API에 직접 의존하면 테스트도 어렵고 변경에도 취약하다. 대신 필요한 동작을 인터페이스로 정의하고, 그 인터페이스에 의존한다.

// 추상화에 의존 — 구체 구현이 바뀌어도 Portfolio 는 흔들리지 않는다
public interface StockExchange {
    Money currentPrice(String symbol);
}
 
public class Portfolio {
    private StockExchange exchange;              // 구체 클래스가 아니라 인터페이스에 의존
    public Portfolio(StockExchange exchange) {   // 생성자로 주입받는다
        this.exchange = exchange;
    }
    public Money value() { ... }                 // exchange.currentPrice(...) 사용
}
 
// 테스트 — 항상 100달러를 반환하는 가짜 거래소를 주입
public class PortfolioTest {
    @Test public void portfolioValueSumsHoldings() {
        StockExchange fixed = symbol -> Money.dollars(100); // 통제된 값
        Portfolio p = new Portfolio(fixed);
        p.add(5, "AAPL");
        assertEquals(Money.dollars(500), p.value());
    }
}

Portfolio는 구체적인 TokyoStockExchange가 아니라 StockExchange에 의존한다. 그래서 테스트에서는 통제된 값을 반환하는 가짜 거래소를 넣을 수 있고, 운영에서는 실제 거래소 구현을 넣을 수 있다. 판단 기준: 테스트하기 어려운 클래스는 대개 결합이 지나친 클래스다. 테스트 가능성은 좋은 설계의 대리 지표다. 이 구조는 결합도를 낮추고 테스트 가능성을 높이며, 고수준 정책(Portfolio)이 저수준 구현(거래소 API)에 휘둘리지 않게 하므로 DIP(의존성 역전 원칙)에 가까워진다.

마무리

클래스를 작게 나누는 목적은 단순히 파일 수를 늘리는 데 있지 않다. 책임을 분명히 하고, 변경 이유를 좁히고, 읽는 사람이 한 번에 이해할 수 있는 단위로 코드를 정리하기 위해서다.

  • 클래스 크기는 줄이 아니라 책임의 수로 잰다. 변경 이유가 하나면 SRP를 지킨 것이다.
  • 응집도가 낮아진 클래스(변수 묶음이 따로 노는 클래스)는 쪼개라는 신호다.
  • 추상화에 의존하면 세부 구현의 변화가 새어나가지 않는다 — DIP는 테스트 가능성으로 드러난다.

클래스가 커지고 이름이 흐려진다면, 책임을 다시 살펴봐야 한다.

다음장으로 11장