본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

02 CHAPTER 객체지향 프로그래밍

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

협력, 객체, 클래스

객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.

 

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

클래스 : 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것.

그래서 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야한다.

 

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

-> 설계를 유연하고 확장 가능하게 한다.




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

도메인 : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야.

 

도메인의 개념과 관계를 반영하도록 프로그램을 구조화해야 하기 때문에 클래스의 구조는 도메인의 구조와 유사한 형태를 띠어야 한다.




클래스 구현하기

외부에서는 객체의 속성에 직접 접근할 수 없도록 막고 적절한 public 메서드를 통해서만 내부 상태를 변경할 수 있게 해야 한다.

 

클래스의 내부와 외부를 구분해야 하는 이유.

1. 경계의 명확성이 객체의 자율성 보장.

2. 프로그래머에게 구현의 자유를 제공.

 

1. 자율적인 객체.

1) 객체는 상태와 행동을 함께 가지는 복합적인 존재이다.

2) 객체는 스스로 판단하고 행동하는 자율적인 존재이다.

 

객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다.

외부에서는 객체가 어떤 상태에 놓여 있는지, 어떤 생각을 하고 있는지 알아서는 안 되며, 결정에 직접적으로 개입하려고 해서도 안된다.

객체에게 원하는 것을 요청하고 객체가 스스로의 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.

 

일반적으로 객체의 상태는 숨기고 행동만 외부에 공개해야 한다.



2. 프로그래머의 자유.

클래스 작성자 : 새로운 데이터 타입을 프로그램에 추가.

클라이언트 프로그래머 : 클래스 작성자가 추가한 데이터 타입을 사용하여 필요한 클래스들을 엮어서 어플리케이션을 빠르고 안정적으로 구축.

 

‘구현은닉’의 효과 : 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다.



클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 하는 이유.

1) 클라이언트 프로그래머가 알아야 할 지식의 양이 줄어든다. (구현에 배경 지식 불필요.)

2) 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어진다.



설계가 필요한 이유는 변경을 관리하기 위해서이다.

 

 

변경을 관리할 수 있는 기법중에서 가장 대표적인 것이 접근 제어이다.

 

 

의미를 좀 더 명시적으로 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현해야한다.

그 개념이 비록 하나의 인스턴스 변수만 포함하더라도 개념을 명시적으로 표현하는 것전체적인 설계의 명확성과 유연성을 높이는 첫걸음이다.




템플릿 메서드 패턴

부모 클래스에서 기본적인 알고리즘의 흐름을 구현하고 중간이 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴.







생성자의 파라미터 목록을 이용해 초기화에 필요한 정보를 전달하도록 강제하면 올바른 상태를 가진 객체의 생성을 보장할 수 있다.





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

두 클래스 사이에 의존성이 존재.

1) 다른 클래스에 접근할 수 있는 경로를 가질 경우.

2) 다른 클래스의 객체의 메서드를 호출하는 경우.



코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.

다시 말해 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.

쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.

 

한 가지 간과해서는 안 되는 사실은 코드의 의존성과 실행 시점의 의존성이 서로 다르면 다를수록 코드를 이해하기 어려워진다.

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

 

현재 인스턴스가 어떤 객체에 의존하고 있는지 알 수 있으려면 인스턴스를 생성하는 부분에서 전달되는 객체가 어떤 인스턴스인지 확인한 후에만 의존성의 대상이 무엇인지를 알 수 있다.

 

설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하라.





차이에 의한 프로그래밍

상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법.

상속은 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있는 간편한 방법을 제공한다.

상속을 사용하면 코드 중복을 제거하고 여러 클래스 사이에서 동일한 코드를 공유할 수 있게 된다.

부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.




 

상속과 인터페이스

상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.

이것은 상속을 바라보는 일반적인 인식과는 거리가 있는데 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각한다.

 

인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.

 

자식 클래스는 상속을 통해 부모의 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용할 수 있다.

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





다형성

동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.

이를 다형성이라고 부른다.

 

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

따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야한다.

동일한 인터페이스를 물려받았기 때문에 이를 보장할 수 있다.

두 클래스의 인터페이스를 통일하기 위해 사용한 구현 방법이 상속이다.

 

다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.

메시지와 메서드를 실행 시점에 바인딩한다.

이를 지연 바인딩 또는 동적 바인딩이라고 부른다.

전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 또는 정적 바인딩이라고 부른다.

 

객체지향이 컴파일 시점의 의존성과 실행 시점의 의존성을 분리하고, 하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유가 바로 지연 바인딩이라는 메커니즘을 사용하기 때문이다.



구현 상속과 인터페이스 상속

상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.

인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

 

 

유연한 설계

예외 케이스가 발생하여 일관성 있던 협력 방식이 무너지게 되면 문제가 된다.

책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다.

항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택해야한다.

 

예를 들어 할인 요금이 적용이 안되는 경우 조건문으로 걸러주는게 아니라,

할인요금이 적용되지 않는 클래스를 하나 추가해서 할인요금을 계산할 책임을 원래 맡았던 클래스 계층에 유지하는 게 좋은 방법이다.

이렇게 하면 기존의 코드를 수정하지 않고 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능이 확장했다.




코드 재사용

코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법이다.




상속

상속은 두 가지 관점에서 설계에 안 좋은 영향을 미친다.

 

1. 가장 큰 문제점은 캡슐화를 위반한다는 것이다.

부모 클래스의 구현이 자식 클래스에 노출되기 때문에 캡슐화가 약화된다.

캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들어 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다.

결과적으로 상속을 과도하게 사용한 코드는 변경하기도 어려워진다.

 

2. 설계가 유연하지 않다.

상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.




합성

인스턴스 변수로 관계를 연결하는 방법.

실행 시점에 객체의 종류를 간단하게 변경가능하다.

 

상속이 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는 데 비해 상속은 인터페이스를 통해 약하게 결합된다.

인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.

 

합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.

1. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 할 수 있다.

2. 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.

 

상속클래스를 통해 강하게 결합되는 데 비해 합성메시지를 통해 느슨하게 결합된다.




코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용하는 수밖에 없다.






대부분의 사람들은 객체지향 프로그래밍 과정을 클래스 안에 속성과 메서드를 채워넣는 작업이나 상속을 이용해 코드를 재사용하는 방법 정도로 생각한다.

프로그래밍 관점에 너무 치우쳐서 객체지향을 바라볼 경우 객체지향의 본질을 놓치기 쉽다.

객체지향에서 가장 중요한 것은 애플리케이션의 기능을 구현하기 위해 협력에 참여하는 객체들 사이의 상호작용이다.

객체들은 협력에 참여하기위해 역할을 부여받고 역할에 적합한 책임을 수행한다.