[Java] 객체지향의 핵심을 이해하려면, 동적 바인딩부터 봐야 한다

thezz9·2025년 4월 6일
3

– 런타임에 결정되는 구조를 이해해야 객체지향의 핵심이 보인다 –


객체지향 이해를 위한 출발점, 다형성과 동적 바인딩

Java나 Kotlin 같은 객체지향 언어에서 왜 인터페이스나 추상 클래스를 자주 쓸까?

바로 다형성(polymorphism) 때문이다.

다형성이란, 하나의 추상 타입(예: 인터페이스 또는 부모 클래스)으로 여러 구현체(Dog, Cat 등)를 다룰 수 있는 성질이다.

즉, 코드는 상위 타입(예: Animal)에만 의존하면서도, 실제 동작은 하위 클래스에 따라 다양하게 바뀔 수 있다.

Animal animal = new Dog();
animal.speak(); // "멍멍"

animal.speak()가 어떤 메서드를 호출할지는 컴파일 시점이 아닌 런타임에 결정된다.
즉, 실제 객체인 Dog의 메서드가 동적으로 호출되며, 이를 동적 바인딩(dynamic binding) 이라고 한다.

이러한 구조 덕분에 클라이언트는 구체 클래스가 아닌 상위 타입만 알고 있어도, 실제 동작은 구현체가 알아서 처리한다.

여기서 중요한 점은, 이렇게 객체의 내부 구현을 감추고, 외부에서는 추상 타입을 통해 일관되게 접근하도록 동작하는 구조가 객체지향의 또 다른 핵심 개념인 캡슐화(encapsulation) 와 깊이 연결된다는 것이다.

캡슐화란, 객체 내부의 구현 세부사항을 외부로부터 숨기고
외부에는 필요한 인터페이스만 노출시키는 것을 말한다.

다형성은 추상 타입을 통해 메시지를 전달하고, 이로써 내부 구현을 감추는 방식으로 캡슐화를 실현한다.

void makeAnimalSpeak(Animal animal) {
    animal.speak();
}

makeAnimalSpeak 메서드는 DogCat이든 상관없이 Animal만 알면 된다.
구체적인 구현은 감춰진 채, 외부에선 통일된 인터페이스로 접근할 수 있다.

결국 객체지향의 4대 특징(추상화, 상속, 다형성, 캡슐화)은 서로 분리된 개념이 아니라,
강력한 캡슐화를 실현하기 위한 도구들이다.


컴파일 의존성과 런타임 의존성의 차이 이해

앞에서 본 동적 바인딩은 메서드 호출을 런타임에 결정하게 해준다.
그런데 여기서 더 중요한 포인트가 하나 있다.

바로 "컴파일 타임에 우리가 어떤 타입에 의존하고 있는가?" 하는 점이다.

Animal animal = new Dog();
  • 컴파일 시점에는 Animal만 알고 있다.
  • 실제 실행(런타임) 시점에는 동적 바인딩에 의해 Dog가 메모리에 올라간다.
    • 컴파일 의존성: Animal
    • 런타임 의존성: Dog

즉, 컴파일 타임에는 추상 타입만 의존하고, 런타임에 실제 구현체가 주입된다.
이 차이가 왜 중요하고 어떤 효과를 불러 일으킬까?

컴파일 시점에는 의존성을 느슨하게 가져가되,
런타임 시점에는 실제 구현체를 주입받음으로써 유연한 시스템을 만들 수 있게 된다.


변경에 강한 구조는 느슨한 결합에서 나온다

위에서 말한 유연한 시스템을 가진 구조는 변경에 강한 구조라는 말이고
이는 곧 느슨한 결합(Loose Coupling) 으로 이어진다.
이것에 대해 한 호흡으로 정리를 해보자면,

객체지향 프로그래밍은 객체 간 협력으로 시스템을 완성해나간다.
이 과정에서 의존성은 반드시 생길 수밖에 없지만, 의존성은 변경을 전파시키는 주범이 된다.
그래서 우리는 변경의 전파를 최소화하는 것에 집중해야 한다.

변경의 전파를 줄이려면, 자주 바뀌는 것에 의존하면 안 된다.

즉, 클라이언트 객체는 구체적인 클래스가 아니라 변경 가능성이 낮은 추상 타입에 의존해야 한다.
이 추상 타입은 상위 계층으로 캡슐화할 수 있으며, 이를 가능하게 해주는 게
인터페이스와 추상 클래스, 그리고 타입 계층이다.

  • 타입 계층은 추상화를 구성하는 문법적 도구이자,

  • 다형성의 기반이며,

  • 캡슐화의 핵심 도구다.

결국 이 글에서 설명하고 있는 모든 것들이 강력한 캡슐화를 위한 것이고, 강력한 캡슐화는 곧 변경에 유연한 시스템이라는 말이기 때문에 객체지향 프로그래밍의 핵심은 캡슐화로 귀결된다고 볼 수 있다.


💡 동적 바인딩의 실제 메커니즘 (JVM 관점)

JVM은 오버라이딩된 메서드를 호출할 때 가상 메서드 테이블(Virtual Method Table) 을 사용한다.

  • 클래스가 로딩되면, JVM은 각 클래스별로 메서드 테이블을 구성한다.
  • 오버라이딩된 메서드는 해당 구현체의 메서드 테이블을 따라 호출된다.
  • 이 덕분에 런타임에 메서드 호출이 결정되는 유연성이 확보된다.

이 구조 덕분에 우리는 인터페이스를 통해 여러 구현체를 주입하고,
런타임에 어떤 동작을 할지 자유롭게 바꿀 수 있다.


동적 바인딩을 사용할 때 주의할 점

동적 바인딩은 코드의 유연성과 확장성을 높여주는 강력한 도구지만, 모든 기술이 그렇듯 사용할 때 고려해야 할 몇 가지 주의점이 있다.

호출 흐름이 복잡해질 수 있다

메서드 호출이 런타임에 결정되기 때문에, 코드만 봐서는 실제로 어떤 구현이 실행되는지 파악하기 어렵다.
IDE나 디버거의 도움 없이 추적하려면 전체 타입 계층을 파악해야 해서, 디버깅이나 흐름 이해에 시간이 더 걸릴 수 있다.

리플렉션, 프록시와 결합되면 추적이 더 어려워진다

동적 바인딩은 프록시, AOP, DI 프레임워크 같은 런타임 기반 기술과 자주 함께 사용된다.
이럴 경우 메서드 호출이 실제 구현까지 도달하는 과정이 한 단계 더 늘어나며,
디버깅 시 실제 호출 지점을 추적하는 데 혼란을 줄 수 있다.

책임을 명확히 분리해야 한다

동적 바인딩은 추상화 위에 동작하기 때문에, 과도한 추상화는 오히려 구조를 흐리게 만든다.
책임이 명확히 나뉘지 않은 상태에서 여러 계층을 만들면, 무슨 일이 어디서 일어나는지 알기 어려운 코드가 된다.
"유연하게 만들기 위해 추상화를 도입한다"는 목표가, 오히려 가독성과 유지보수성을 해치는 결과가 될 수 있다.


마무리

정리해보면,

  • 동적 바인딩은 실행 시점에 메서드를 결정하는 객체지향 언어의 핵심 메커니즘이다.
  • 이를 통해 우리는 인터페이스 기반의 느슨한 결합을 설계할 수 있다.
  • 컴파일 타임에는 추상화에 의존, 런타임에는 실제 구현체로 동작을 유연하게 바꿀 수 있고,
  • 이 유연함은 SOLID 원칙 중 OCP(변경에 닫히고 확장에 열림), DIP(의존성 역전), DI(의존성 주입) 같은 객체지향 설계의 핵심 원칙들을 가능하게 만든다.

유연함을 가능하게 만드는 핵심이 바로 동적 바인딩, 그리고 그걸 중심으로 하는 다형성이고,
이 모든 것들을 따라가다 보면, 마지막엔 캡슐화로 귀결될 수 밖에 없다.
결국 객체지향의 진짜 핵심은 "캡슐화"라고 할 수 있다.

profile
개발 취준생

0개의 댓글