public/protected/private는 불필요한 기능이다

ko생성일: 2025. 10. 22.

Car와 Vehicle 예시를 통해 접근 제한자(public/protected/private)가 인터페이스와 상속 맥락에서 인터페이스 정의 기능을 중복하는 불필요한 기능임을 설명하고, 인터페이스 기반으로 대체하는 설계를 제안한다.

진부한 OOP 예시라 미리 사과하지만: 클래스 Car가 인터페이스 Vehicle을 구현한다고 해보자.

  • 이 인터페이스를 사용하는 일반 코드는 아주 잘 동작한다:

    1. 사용자는 어떤 Vehicle에도 동작하는 제네릭 코드를 작성할 수 있고, 그 코드에서 Car를 사용할 수 있다.
    2. Car의 구현자는, 예컨대 반환 타입이 Vehicle인 함수 make_car로만 Car 인스턴스를 생성하도록 허용함으로써, 사용자가 오직 Vehicle 인터페이스를 통해서만 Car 인스턴스를 사용하도록 제한할 수 있다.
  • 하지만 이 인터페이스는 상속과는 잘 맞지 않는다:

    1. 사용자는 어떤 Vehicle이든 상속받을 수 있는 제네릭 클래스를 작성할 수 없다(더 복잡한 기능을 추가하지 않는 한).
    2. Car의 구현자는 Car의 하위 클래스들이 오직 Vehicle 인터페이스를 통해서만 Car를 사용하도록 제한할 수 없다.

우리는 항목 3은 무시하고, 항목 4에 집중하겠다. Car는 하위 클래스가 인터페이스만 사용하도록 제한할 수 없기 때문에, 어떤 하위 클래스든 Car의 내부 불변식을 위반할 수 있다.

따라서 Car는 상속을 금지해야 하거나(항목 2에서처럼 아예 Car 타입을 노출하지 않으면 항상 가능한 선택지다), 아니면 하위 클래스에도 실제로 적용될 수 있는 또 다른 방식으로 인터페이스를 정의할 필요가 있다.

하위 클래스에도 통하는 그 ‘다른’ 인터페이스 정의 방식이 바로 접근 제한자, 즉 public, protected, private이다. 멤버를 private으로 표시함으로써 Car는 하위 클래스에 영향을 미치는 일종의 인터페이스를 정의할 수 있다; 이 가짜 인터페이스는 Car의 멤버 중 public과 protected인 것만 정확히 포함한다.

하지만 이는 터무니없다. 왜 인터페이스를 정의하는 방법이 두 가지나 있어야 하는가?

Car가 특정한(일반적인) 인터페이스를 통해서만 하위 클래스가 Car를 사용하도록 강제할 수 있다면 더 낫지 않을까? 기능 상의 손실은 없다. Car는 여전히 인스턴스를 만드는 쪽과 하위 클래스를 위한 인터페이스를 따로 가질 수 있다. 단지 그 인터페이스들을, 인스턴스 생성자와 하위 클래스에 서로 다른 메커니즘을 쓰는 대신, 같은 메커니즘으로 정의하면 된다.

접근 제한자는 원래 Simula에서 발명되었다. 내가 폭넓게 조사한 바에 따르면, 당시의 발명자와 사용자들은 접근 제한자가 이미 존재하던 인터페이스 정의 수단을 중복한다는 사실을 단순히 인지하지 못했던 것 같다. 즉 가상 메서드와 서브타이핑으로, 둘을 결합하면 Simula에서 인터페이스를 정의하기에 충분했다.

중요한 점은, 상속이 없다면 인터페이스가 있는 상태에서 접근 제한자는 구현을 숨기는 추가적 힘을 전혀 제공하지 않는다. 위의 항목 2를 기억하라. 우리는 클래스의 구현 세부사항을 숨기는 데 protected/private가 필요 없다.

물론 Simula에서는 접근 제한자가 유용했다. Simula가 상속을 발명했고 광범위하게 사용했기 때문에, 기반 클래스의 구현 내부를 보호할 방법이 필요했기 때문이다.

하지만 Simula가 이미 가지고 있던 인터페이스 능력을 재사용하는 다른 수단을 쓸 수도 있었다. 예를 들어 Car의 정의 안에서, Car가 서브클래싱될 때 하위 클래스에는 기반 클래스 Vehicle에 선언된 필드만 접근 가능하다고 명시할 수 있었을 것이다.

아쉽게도, 내가 보기에는 그 가능성을 그저 몰랐던 듯하다. 그 결과 우리는 접근 제한자라는 불필요한 중복 기능을 갖게 되었고, 그것이 하위 호환성이라는 이유로 언어에서 언어로 이어졌다.

물론 앞으로도 더 나아질 수 있다. 접근 제한자를 그냥 사용하지 않으면 된다. 당신의 클래스가 서브클래싱될 수 없고(예를 들어 final이라서), 위의 항목 2처럼 그 생성도 제한하고 있다면, 접근 제한자로 애노테이트할 필요가 전혀 없다. 클래스 내부의 보호는 인터페이스를 정의함으로써 달성할 수 있고, 그래야만 한다.

정말로 비추상 클래스로부터 상속하고 싶다면... 하지 말라. 대신 합성을 사용하라. 상속은 애초에 해킹 같은 것이었다.