02. 객체지향 프로그래밍

객체지향 프로그래밍을 향해

협력, 객체, 클래스

객체지향은 말 그대로 '객체를 지향'하는 것이다. 대부분의 사람들은 클래스를 결정한 후 클래스에 어떤 속성과 메서드가 필요한지 고민한다.

안타깝게도 이 방식은 객체지향의 본질과는 거리가 멀다. 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.

객체지향 프로그래밍을 위해서 아래와 같은 두 가지에 집중해야 한다.

  1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민해야 한다.

  2. 객체를 독립적인 존재가 아닌 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다.

도메인의 구조를 따르는 프로그램 구조

도메인(domain)이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 뜻한다.

객체지향 패러다임이 강력한 이유는 요구사항 분석을 하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 '추상화 기법'을 사용할 수 있기 때문이다.

일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다. 클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어서 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.

클래스 구현하기

도메인 개념들의 구조를 반영하는 적절한 클래스 구조를 만들었다면, 남은 일은 적절한 프로그래밍 언어로 이 구조를 구현하는 것이다.

클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다. 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분은 감출지를 결정하는 것이다.

클래스의 내부와 외부를 구분지음으로써 경계를 명확하게 하며, 객체의 자율성을 보장할 수 있다. 또한, 이는 프로그래머에게 자유를 준다!

자율적인 객체

  • 객체는 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재다.

  • 객체가 스스로 판단하고 행동하는 자율적인 존재다.

데이터와 기능을 객체 내부로 함께 묶는 '캡슐화'는 접근 제어(access control) 메커니즘을 제공하며 public, protected, private 과 같은 접근 수정자(access modifier)를 제공한다.

객체가 자율적인 존재로 되기 위해서는 외부의 간섭을 최소화해야한다. 객체가 어떤 상태에 놓여 있는지, 어떤 생각을 하고 있는지 알아서는 안 되며, 결정에 직접적으로 개입하려고 해서도 안된다!

접근 가능한 부분은 퍼블릭 인터페이스(public interface)라고 부르며, 외부에서 접근 불가능하고 오직 내부에서만 접근 가능한 부분을 구현(implementation)이라고 부른다.

프로그래머의 자유

프로그래머를 아래와 같이 구분해보자.

  • 클래스 작성자(class creator)

  • 클라이언트 프로그래머(client programmer)

클라이언트 프로그래머의 목표는 애플리케이션을 구축하는 것이며, 클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨겨야 한다.

이렇게 함으로써 클라이언트 프로그래머에 대한 영향은 걱정하지 않고 내부 구현을 마음대로 변경할 수 있다. 이를 구현 은닉(implementation hiding)이라고 부른다.

설계가 필요한 이유는 변경을 관리하기 위해서라는 것을 기억하자. 객체지향 언어는 객체 사이의 의존성을 적절하게 관리함으로써 변경에 대한 파급효과를 제어할 수 있는 다양한 방법을 제공한다.

협력에 관한 짧은 이야기

객체의 내부 상태는 외부에서 접근하지 못하도록 감춰야 한다. 대신 퍼블릭 인터페이스를 통해 내부 상태에 접근할 수 있도록 허용한다.

  • 객체가 다른 객체와 상호작용을 할 수 있는 유일한 방법은 메시지를 전송(send a message)하는 것뿐이다.

  • 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신(receive a message)했다고 한다.

이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.

여기서 메시지와 메서드는 다르다! 다형성(polymorphism)의 개념으로 설명할 수 있어야 한다.

상속과 다형성

의존성의 개념을 살펴보고 상속과 다형성을 이용해 특정한 조건을 선택적으로 실행하는 방법을 알아보자.

컴파일 시간의 의존성과 실행 시간 의존성

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다! 즉, 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 그리고 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.

다만, 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다. 코드를 이해하기 위해서 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다.

이와 같은 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여준다.

훌륭한 객체지향 설계자로 성장하기 위해 항상 유연성과 가독성 사이에서 고민해야 한다. 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다. 이것이 객체지향 설계가 어려우면서 매력적인 이유다.

상속과 인터페이스

상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각하듯 일반적인 인식과는 거리가 있다.

인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하라. 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.

자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다.

다형성

위에서 언급했듯이, 메시지와 메서드는 다르다. '다형성' 측면에서 봐보자.

동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이러한 특징을 다형성(polymorphism)이라고 부른다.

다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 말한다.

  • 실행 시점에 메시지와 메서드 바인딩: 지연 바인딩(lazy binding), 동적 바인딩(dynamic binding)

  • 컴파일 시점에 메시지와 메서드 바인딩: 초기 바인딩(early binding), 정적 바인딩(static binding)

추상화와 유연성

추상화의 힘

  1. 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.

  2. 추상화를 이용하면 설계가 좀 더 유연해진다.

즉, 추상화를 사용하면 세부적인 내용은 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.

추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다. 예를 들어, 할인 정책이나 할인 조건의 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따르게 된다.

이처럼 재사용 가능한 설계의 기본을 이루는 디자인 패턴과 프레임워크 모두 추상화를 이요해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있는 것이다.

유연한 설계

  • 컨텍스트 독립성(context independency)

  • 추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.

Last updated

Was this helpful?