내용 요약
협력, 객체, 클래스
객체지향은 객체를 지향하는 것이다. 객체지향 프로그램을 작성할 때 가장 먼저 고려하는 것은 무엇인가?
클래스 기반의 객체지향 언어에 익숙한 사람이라면 가장 먼저 어떤 클래스(class)가 필요한지 고민할 것이다.
대부분의 사람들은 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 객체지향은 말 그대로 객체를 지향하는 것이다.
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
이를 위해서는 프로그래밍하는 동안 다음의 두가지에 집중해야한다.
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지를 고민하라. 클래스는 공통적인 상태와 행동을 공유하는 객체를 추상화한 것이다. 따라서 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다. 객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다. 객체는 홀로 존재하는 것이 아니다. 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다. 객체를 협력하는 공동체의 일원으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다. 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라. 훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.
도메인의 구조를 따르는 프로그램 구조
소프트웨어는 사용자가 원하는 어떤 문제를 해결하기 위해 만들어진다. 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다. 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.
클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어질 관계와 유사하게 만들어서 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.
자율적인 객체
두 가지 중요한 사실을 알아야한다.
- 객체가 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재라는 것이다.
- 객체가 스스로 판단하고 행동하는 자율적인 존재라는 것이다.
객체 지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현할 수 있게 했다.
이처럼 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 부른다.
대부분의 객체지향 프로그래밍 언어들은 상태와 행동을 캡슐화하는 것에서 한 걸음 더 나아가 외부에서의 접근을 통제할 수 있는 접근 제어(access control) 매커니즘도 할께 제공한다.
많은 프로그래밍 언어들은 접근 제어를 위해public, protected, private과 같은 접근 수정자(access modifier)를 제공한다.
캡슐화와 접근제어는 객체를 두 부분으로 나눈다. 하나는 외부에서 접근 가능한 부분으로 이를 퍼블릭 인터페이스(public interface)라고 부른다. 다른 하나는 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분으로 이를 구현(implemetation)이라고 부른다.
일반적으로 객체의 상태는 숨기고 행동만 외부에 공개해야 한다. 여러분이 사용하는 프로그래밍 언어가public이나private이라는 키워드를 제공한다면 클래스의 속성은private으로 선언해서 감추고 외부에 제공해야 하는 일부 메서드만public으로 선언해야한다.
프로그래머의 자유
프로그래머의 역할을 클래스 작성자(class creator)와 클라이언트 프로그래머(client programmer)로 구분하는 것이 유용하다. 클래스 작성자는 새로운 데이터 타입을 프로그램에 추가하고, 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
클라이언트 프로그래머의 목표는 필요한 클래스들을 엮어서 애플리케이션을 빠르고 안정적으로 구축하는 것이다. 클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 꽁꽁 숨겨야 한다. 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. 이를 구현 은닉(implementation hiding)이라고 부른다.
협력에 관한 짧은 이야기
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response)한다.
객체가 다른 객체와 상호작용을 할 수 있는 유일한 방법은 메시지를 전송(send a message)하는 것뿐이다. 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신(receive a message)했다고 이야기한다. 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다. 이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.
메시지와 메서드를 구분하는 것은 매우 중요하다. 객체지향 패러다임이 유연하고, 확장 가능하며, 재용 가능한 설계를 낳는다는 명성을 얻게 된 배경에는 메시지와 메서드를 명확하게 구분한 것도 단단히 한몫한다. 뒤에서 살펴보겠지만 메시지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발한다.
상속과 다형성
코드의 의존성과 실행 시점의 의존성은 서로 다를 수 있다.
다시 말해 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 그리고 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.
한 가지 간과해서는 안되는 사실은 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다는 것이다. 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다. 이와 같은 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여준다.
차이에 의한 프로그래밍
클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사하다고 가정해보자.
그 클래스의 코드를 가져와 약간만 추가하거나 수정해서 새로운 클래스를 만들 수 있다면 좋을 것이다.
더 좋은 방법은 그 클래스의 코드를 전혀 수정하지 않고도 재사용하는 것일 것이다. 이를 가능하게 해주는 방법이 바로 상속이다.
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.
상속과 인터페이스
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
이것은 상속을 바라보는 일반적인 인식과는 거리가 있는데 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각하기 때문이다.
인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하라. 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)=이라고 부른다.
다형성
동일한 메시지를 전송하지만 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라고 부른다.
다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있따는 사실을 기반으로 한다.
다형성은 컴파일 시간 의존성과 실행 시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행할 수 있게 한다.
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다. 따라서 다형적인 협력에 참여하는 개체들은 모두 같은 메시지를 이해할 수 있어야 한다. 다시 말해 인터페이스가 동일해야 한다는 것이다.
다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정해야한다는 공통점이 있다. 다시 말해 메시지와 메서드를 실행 시점에 바인딩한다는 것이다. 이를 지연 바인딩(lazy binding)또는 동적 바인딩(dynamic binding)이라고 부른다. 그에 반해 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩(early binding) 또는 정적 바인딩(static binding)이라고 부른다.
합성
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 할 수 있다. 또한 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다. 상속은 클래스를 통해 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨하게 결합된다. 따라서 코드 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다.
그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다. 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.
느낀점
코드를 구현할 때, 객체간의 협력을 위해 인터페이스가아닌 구현에 의존하지 않았나라는 반성을 하게 되는 부분이였다.
객체에 대한 정의가 어떤 것인지 되돌아 볼 수 있었고 막연하게 객체지향언어 니까 객체지향적으로 코드를 짜야지라고 생각했던 부분에 대해 이해하기 쉬운 코드와 변경에 유연한 코드에 대한 트레이드 오프에 대해서도 고민할 수 있었다. 앞으로 코드를 객체 간의 메시지를 통한 협력으로 구현해봐야겠다는 생각이 들었다.
현재 회사 코드에서 책을 읽으면서 개선할 수 있었던 부분이 있었나 되짚어 볼 수 있었고, 현재 진행중인 프로젝트에서도 개선점을 많이 찾을 수 있겠다 생각이 들었다.
