읽은 기록

The_Object-Oriented_Thought_Process(객체지향 사고 프로세스)

객체지향 책으로 추천 받은 책이다.

얇게 알고 있다면 한번 읽어서 기반을 탄탄하게 하기 좋은 책으로 이후에 오브젝트객체지향의 사실과 오해를 읽기 전에 먼저 읽으면 좋은 책이다.

주위분들에게 객체지향 책으로 추천..!

1. 객체지향의 개념 소개

이 장에선 객체지향의 개념을 소개하는 파트이다.

개념은 끊임없이 진화한다. 그리고 이런 진화적 측면이 개념에 초점을 맞추기 아주 적합하다.

개념은 일관성을 보이더라도 늘 새롭게 해석할 수 있으므로 아주 흥미로운 토론 거리가 될 수 있다.

1.1. 기본 개념

역사적으로 객체지향 언어는 캡슐화, 상속 및 다형성으로 정의되어 있다.

고전적인 객체지향

따라서 한 언어가 이러한 특성을 따르지 않으면 객체지향언어가 아닌 것으로 간주한다.

저자는 세가지 개념에서 합성이라는 개념을 추가하여 총 4가지 개념을 말한다.

  • 캡슐화(encapsulation)
  • 상속(inheritance)
  • 다형성(polymorphism)
  • 합성(composition)

생각했던 대로 바로 등장하는 상속의 문제점..

다음 장에서 다룰 예정이지만 많은 개발자들은 최대한 상속을 피하려 한다.

여기서도 등장하는 생각이지만 is a의 개념.. 좋은 코드 나쁜 코드에서도 논쟁이였지만 상속에 대한 말은 아직도 다양하게 나오는 것 같다.

1.2. 객체와 레거시 시스템

  • 레거시 시스템(legacy system): 구형,기존 시스템이라고 불림

구조적 프로그래밍과 객체지향적 프로그래밍은 서로 경쟁관계(대립)의 관계가 아니다.

즉, 객체지향 코드는 구조적 코드를 대체하기 위한 것이 아니다.

객체지향적이지 않은 많은 레거시 시스템이 작업을 제대로 수행하고 있는데 굳이 변경하거나 교체해서 위험을 무릎쓸 이유가 있을까?

즉 프로그램이 잘 동작만 한다면 객체지향이든 절차지향이든 크게 상관이 없지만 완전히 새로 개발해야하는 상황이라면 객체지향 기술을 반드시 고려해야 한다.

어떤 경우에는 그렇게 할 수 밖에 없다.

최근 웹앱에서 처리에 대한 내용이 증가하면서 객체지향이 더욱 각광받고 있다.

따라서 대부분의 시스템이 새로 개발되고 있는 상황에서 레거시는 크게 문제가 되지 않는다.

문제가 있더라도 레거시를 객체 래퍼로 둘러싸는 경향이 있다.

  • 객체 래퍼

다른 코드를 둘러싸는 객체지향 코드이다.

대부분의 객체지향을 선호하게 되면서 전송을 객체로 처리하는 방식으로 변경되았다.

1.3. 객체지향 프로그래밍과 절차적 프로그래밍

객체지향의 장점을 생각하기 이전에 “객체란 무엇인가?”

이 질문은 단순하면서 복잡하다..

하지만 대부분의 사람들이 객체(물체)라는 관점에서 생각하며 산다는 관점에선 간단하다.

로버트 마틴은 사람들이 객체라는 관점에서 생각한다. 이라는 말을 마게팅 담당자가 지어낸 문구라는 의견이 있다.

사람을 볼 때 사람을 일종의 객체(object)로 간주하게 된다.

그리고 객체는 속성과 행위라는 두 가지 성분으로 정의된다.

사람에게는 눈 색, 나이, 키등과 같은 속성(attributes)이 있다.

또한 걷기, 말하기, 호흡하기 등 행위(behaviors)를 한다.

객체에 대한 기본적인 정의에 따르면 객체란 데이터행위라는 양면(both)을 포함하는 엔터티(entity)다.

양면이라는 단어는 객체지향 프로그래밍과 다른 프로그래밍 방법론과 주요한 차이점이다.

  • 절차적 프로그래밍은 클래스라는 개념이 없기 때문에 다른 함수나 절차와 구별된다.

객체지향의 이점이 명확했지만 쉽게 도입하지 못한 이유는 절차적 프로그래밍이 잘 동작했기 때문이다.

절차지향은 데이터에 대한 접근을 제어하거나 예측하기가 어렵다는 매우매우 큰 단점이 있는 반면 객체지향방식에서는 데이터와 행위를 객체로 결합함으로써 이러한 데이터 문제를 해결할 수 있다.

  • 적절한 설계: 제대로 설계된 객체지향 모델이라면 전역 데이터가 전혀 없을 것이라고 말할 수 있다.. 이로 인해 객체지향 시스템은 데이터 무결성이 달성된다.

객체는 자료구조나 정수, 문자열과 같은 기본 데이터 형식 이상의 것이다.

객체에는 속성을 나타내는데 사용되는 정수 및 문자열과 같은 엔터티가 들어 있지만, 행위를 나타내는 메서드도 들어 있다.

객체에서 메서드는 데이터에 대한 연산이나 그 밖의 연산을 수행하는데 사용된다.

더 중요한 것은 객체의 멤버(속성 멤버와 메서드 멤버)에 대한 접근을 제어할 수 있다는 점이다.

이것은 속성과 메서드에서 모두 일부 멤버가 다른 객체에 대해 숨겨질 수 있다는 점이다.

  • 데이터 은닉: 객체지향 용어에서는 데이터를 속성이라고 하고 행위를 메서드라고 한다. 속성이나 메서드에 접근하지 못하게 제한하는 일을 데이터 은닉이라고 한다.

객체지향 용어로는 캡슐화라고 부른다.

Math클래스와 myObject클래스가 있다고 할 때 Math클래스의 멤버 변수 myInt1, myInt2을 합산한 결과에 접근하고 싶다면 myObject클래스는 Math의 sum메서드를 호출하여 값을 반환받는다.

여기서 잘 봐야하는 점이 myObject는 sum메서드의 동작방식을 알 필요가 없다.(알수없다.)

즉 이러한 설계를 가져가면 필요에 의해 Math에서 sum함수를 변경하게 되면 myObject를 수정할 필요가 없어진다.

책임을 온전하게 해당 객체가 담당하는 것이 핵심..

일반적으로 객체는 다른 객체의 내부 데이터를 조작해서는 안 된다.

일반적으로 큰 객체를 만들어서 작업하는 것 보다 작은 객체로 만들어 특정 작업만 담당하게 하는 것이 좋다.

1.4. 절차적 개발에서 객체지향적 개발로 옯겨 타기

1.4.1. 절차적 프로그래밍

절차적 프로그래밍은 일반적으로 시스템의 데이터를 다루는 연산과 데이터를 분리한다.

1.5. 객체지향 프로그래밍

객체지향 프로그래밍은 데이터와 연산을 하나의 객체로 묶는다. (캡슐화)

1.6. 객체란 정확히 무엇일까?

객체란 객체지향 프로그램의 빌딩 블록이다.

객체지향 기술을 사용하는 프로그램은 기본적으로 객체들의 모음인 것이다.

1.6.1. 객체의 데이터

객체 내에 저장된 데이터를 속성(attribute)이라고 한다.

1.6.2. 객체의 행위

객체가 수행할 수 있는 행위를 메서드(method)라고 한다.

  • C#의 프로퍼티..

예제에서 나오는 프로퍼티보다 최근에는 자동구현 프로퍼티기능을 더 많이 사용하는 것 같다.

  • 게터와 세터: 게터와 세터는 속성에 대한 접근을 제어하는 메서드다.

간혹 게터와 세터가 왜 필요한지 이해하기 어려운 경우가 있다.

예를 들어, 속성이 음수가 될 수 없는 경우 게터와 세터를 사용하여 이를 제어할 수 있다.

그 밖에도 객체지향의 정신을 그대로 가져갈려면 데이터를 직접 조작하면 안 되므로, 게터와 세터로 객체 데이터에 대한 접근 권한을 제어해야 한다.

우리는 메서드의 인터페이스만 보여주고 있을 뿐, 메서드의 구현을 보여주고 있지 않다는 점에 유념해야 한다.

  • 메서드의 이름
  • 메서드에 전달된 매개변수
  • 메서드의 반환 형식

사용자가 메서드를 효과적으로 사용하기 위해 알아야 하는 요소

클래스란 객체를 만드는 탬플릿이다.

객체가 생성되면 우리는 객체가 인스턴스화되었다고 말한다.

1.7. 클래스란 정확히 무엇을 일컫는 말인가?

클래스는 객체를 만드는 설계도다.

Person클래스

public class Person
{
    private string name;
    private string address;

    public string getName()
    {
        return name;
    }

    public void setName(string name)
    {
        this.name = name;
    }

    public string getAddress()
    {
        return address;
    }

    public void setAddress(string address)
    {
        this.address = address;
    }
}
  • 속성: name, address가 속성이다.
  • 메서드: getName, setName, getAddress, setAddress

1.7.1. 메시지

메시지는 객체 간의 소통 매커니즘이다.

메시지는 객체가 다른 객체에게 요청을 보내는 것이다.

조금 거창해 보이지만 객체에서 다른 객체의 메서드를 호출하는 것이다.

1.8. 시각화 도구로 클래스 다이어그램 사용하기

클래스 다이어그램중 가장 많이 사용되는 UML클래스가 있다.

그렇다고 UML클래스 다이어그램을 사용해야만 한다는 것은 아니다.

꼭 사용할 필요는 없으며 직관적으로 이해할 수 있는 다이어그램을 사용하면 된다.

1.9. 캡슐화 및 데이터 은닉

객체를 사용할 때의 주요 이점 중 하나는, 모든 속성과 행위를 객체에 나타낼 필요가 없다는 점이다.

좋은 객체지향 설계에서 객체는 여타 객체와 상호 작용하는 데 필요한 인터페이스만 타나내야 한다.

객체 사용과 관련이 없는 세부 사항을 그 밖의 객체들이 알 수 없도록 감추어야 한다.

캡슐화는 객체속에 속성뿐만 아니라 행위도 함께 들어 있다는 사실에 기반하여 정의할 수 있는 개념이다.

1.9.1. 인터페이스

인터페이스는 객체가 다른 객체와 소통하는 방법을 정의한다.

  • 비공개 데이터: 데이터의 은닉이 제대로 작동하려면 모든 속성을 private으로 선언해야 한다. 따라서 속성은 인터페이스의 일부가 아니다. public메서드들만이 클래스 인터페이스의 일부이다.

속성을 public으로 하게 되면 데이터의 은닉 개념이 깨진다.

1.9.2. 구현부

공개 속성과 공개 메서드만 인터페이스로 간주한다.

사용자는 내부 구현부중 어떤 부분도 볼 수 없으며, 클래스 인터페이스를 통해서만 그 밖의 객체와 소통할 수 있다.

따라서 비공개로 정의된 게 어떤 것이든지 간에 사용자는 접근할 수 없으며, 비공개로 정의된 것들은 클래스의 내부 구현부의 일부로 간주된다.

1.9.3. 인터페이스/구현부 패러다임의 실제 예

usb 포트, 전기 단자와 같은 예

1.9.4. 인터페이스/구현부 패러다임의 모델

예제에서는 계산부분을 private로 선언하고, public으로 선언된 메서드를 통해서만 계산을 할 수 있도록 하였다.

이렇게 한번 더 둘러싼 이유는 이후에 계산부분만 수정이 되면 프로그램의 동작과정이 문제가 없고 모듈화의 기초가 되기 때문이다.

계산출력부분을 델리게이트로 두거나 인터페이스로 연결해두어서 다양한 계산에도 대응이 가능하다.

1.10. 상속

상속을 통해 클래스는 그 밖의 클래스들이 지닌 속성과 메서드를 물려받을 수 있다.

이를 통해 새로 만들려고 하는 클래스가 아닌 그 밖의 클래스들이 공통으로 지닌 속성과 행위를 추상화해 새 클래스를 만들 수 있다.

  • 행위: 오늘날에는 행위를 인터페이스 안에 기술해 두는 경향이 있으며, 속성들을 상속하는게 일반적인 용례라는 점에 주목할 만하다.

슈퍼클래스, 서브클래스, 기반클래스, 파생클래스, 부모클래스, 자식클래스 등으로 불린다.

1.10.1. 추상화

상속 트리는 상당히 커질 수 있다.

만약 포유류 클래스가 완성이 되고 이를 상속받는 강아지, 고양이, 사자 등등을 쉽게 추가할 수 있다.

코드의 중복도 줄고 좋아보인다.. 하지만

고양이의 경우 종에 따라 좀 더 추사화되어야 할 수 있다.

추상의 개념은 사람마다 주관적이므로 펭귄의 경우 새로 분류하는지(새 추상클래스의 경우 날 수 있다)

한가지 부모를 상속하면 단일 상속, 두개 이상의 부모를 상속하면 다중 상속이라고 한다.

1.10.2. is - a의 관계

도형의 경우 사각형은 도형이고, 원도 도형이다 즉, cicle is a shape이다.

이러한 명확한 관계를 is a관계라고 한다.

1.11. 다형성

다형성은 문자 그대로 다양한 현상을 의미하는 그리스어다.

다형성은 상속과 밀접하게 관련되어 있지만 종종 객체지향 기술의 가장 강력한 점 중에 하나로 여겨진다.

동적바인딩에 대한 이야기.. 상속받은 인터페이스를 오버라이딩하여 각 자식 클래스마다의 인터페이스를 가져가는 것

1.12. 합성

객체에 다른 객체가 들어있다고 생각하는 게 자연스럽다.

컴퓨터에는 그래픽카드와 키보드 및 드라이버가 들어있다.

컴퓨터도 객체로 간주할 수 있지만, 그 안에 담긴 그래픽카드, 키보드, 드라이버도 객체로 간주할 수 있다.

좀 더 직관적이게 컴퓨터를 열어 그래픽카드, 키보드, 드라이버를 꺼내보자.

이런 식으로 객체를 종종 그 밖의 객체들을 사용해 구축하거나 합성할 수 있는데, 이것이 합성이라는 개념이다.

1.12.1. 추상화

상속과 마찬가지로 합성은 객체를 만드는 매커니즘을 제공한다.

실제로 필자는 클래스를 그 밖의 클래스를 사용해 작성하는 방법으로 상속과 합성 두 가지를 모두 사용한다.

자동차와 엔진의 관계를 생각해본다면 합성의 이점이 명확해진다.

car has a engine이다.

1.12.2. Has-a의 관계

상속은 is-a의 관계를 표현하는데 적합하다.

합성은 has-a의 관계를 표현하는데 적합하다.

합성을 다른 말로 구성으로도 많이 사용되는 듯 하다

1.13. 느낀점

과연 객체지향언어를 사용하는 사람들 중 고전적인 객체지향이 아닌 책에서 말하는 객체지향을 공부한 사람이 몇이나 될까..?

나는 1년간 객체지향언어를 사용했다고 말은 할 수 있지만 개념도 제대로 이해하지 못한 채 사용했던 것 같다..

2. 객체라는 관점에서 생각하는 방법

객체지향의 설계의 기본단위는 클래스다.

객체지향 설계의 최종 결과는 강력하고 기능적인 객체 모델, 즉 완전한 시스템이다.

인생에는 정답이 없다.

일반적으로 동일한 문제에 대한 해결 방법이 여러 가지다.

따라서 객체지향 솔루션을 설계하려고 할 때 처음부터 완벽하게 설계하려고 에쓸 필요는 없다.

언제나 항상 개선할 부분이 있을 것이다.

우리가 해야하는 일은 브레인스토밍을 통한 생각하는 과정이 다른 방향까지 다른 방향으로까지 나아가게 하는 것이다.

생각

최근에는 객체지향을 사용하여 설계할 때 정답이 없다는 것을 많이 느끼고 있다.  

많은 좋은 개발 서적에서 시작하는 말은 정답은 없다라는 말로 시작하는 부분이 인상적이다.

아직 개발의 역사가 100년정도 밖에 되지 않아서 일까.. 발전이 너무 빨라서일까 프로그래밍은 수학적인 능력만을 요구하는게 아닌 다양한 능력이 요구되는 `기예`에 가깝다.

사실 객체지향뿐만 아니라 설계과정에서는 특정 프로그래밍언어를 고려하지 마라.

업무에 있어서 첫 번째 순서는 문제를 식별하고 해결하는 것이다.

문제를 분석하다 보면 필요한 기술이 보일 것이고 해당 기수에 맞는 솔루션을 고려해야 한다.

앞서 다룬 구조적 프로그래밍과 객체지향적 프로그래밍 모두 공존한다..

실제로 작성중인 객체지향 코드만 봐도 구조적 구문을 사용중이다..

하지만 다른 예로 객체지향 설계로 전환하기 위해선 다른 종류의 투자가 필요하다.

객체지향 언어로 옮겨 탈 생각이라면 무엇보다 먼저 객체지향의 개념을 익히고 객체지향 방식으로 생각하는 과정을 익히는 일에 투자해야 한다.

이렇게 시작하지 않는다면 프로젝트가 객체지향적이지 못하거나 완전히 객체지향 감각 상실적인 망작이 될 것이다.

객체지향 사고 방식을 이해하기 위해 세 가지 중요한 사항을 다룬다.

  1. 인터페이스와 구현부의 차이점을 아는 것
  2. 더 추상적으로 생각하기
  3. 사용자에게 가능한 한 인터페이스를 적게 제공하기

2.1. 인터페이스와 구현부의 차이점을 아는 것

객체지향 설계를 구축하는 비결 중 하나는 인터페이스(interface)와 구현부(implementation)의 차이점을 이해하는 것이다.

클래스를 설계할 때 사용자가 알아야 할 사항, 그리고 아마도 더 중요한 사용자가 몰라야 할 사항을 잘 구분해 둬야 한다는 점이다.

캡슐화 시에 고유한 데이터를 은닉하는 메커니즘이란 필수적이지 않은 데이터를 사용자로부터 숨기는 수단이다.

자동차를 기준으로 인터페이스는 운전대, 가속 페달, 브레이크같은 부품에 포함되어 있다.

기본적으로 우리가 보지 못하는 구현부는 일반 운전자들에게 거의 관심이 없다.

그러나 모든 운전자는 일반적인 인터페이스인 운전대를 사용하는 방법을 알고 있다.

자동차에 표준 운전대를 설치함으로써 제조업체는 표적 시장을 이루고 있는 고객들이 시스템을 사용할 수 있다고 확신할 수 있다.

그러나 제조업체가 운전대 대신에 조이스틱을 설치하기로 결정한 경우에 대부분의 운전자에게는 장애물이 될 뿐만 아니라 자동차도 팔리지 않게 된다.

반면, 성능과 감성이 변하지 않는 이상 평균적인 운전자는 제조업체가 자동차의 엔진을 바꿨는지 여부를 알지 못한다.

엔진또한 해당 인터페이스 소속이기 때문에 다른 엔진과 교체될 수 있다.

엔진은 구현의 일부이며, 운전대는 인터페이스의 일부이다.

구현부를 변경해도 운전자에게 영향을 미치지 않아야 하지만, 인터페이스는 변경될 수 있다.

운전자는 비슷한 방식으로 작동하더라도 핸들에 대한 미적 변화를 알아차릴 수 있을 것이다.

인터페이스와 구현부라는 두 부분만으로 클래스를 설계해야 클래스가 제대로 합성이 된다.

2.1.1. 인터페이스

최종 사용자에게 제공되는 서비스들에 맞춰 인터페이스가 합성된다.

최종 서비스에게 필요한 서비스, 바로 그것만 제공하는 게 최선이다.

물론, 사용자에게 필요한 서비스라는 게 의견에 따라 달라질 수 있다.

한 방에 열명이 있게 한 다음에 그들이 각자가 독립적인 설계를 하도록 요청하면 완전히 다른 열 가지 설계를 받을 수 있는데, 이럴지라도 아무런 문제는 없다.

그러나 일반적으로 클래스에 대한 인터페이스에는 사용자가 알아야 할 내용만 포함헤야 한다.

사용자는 토스터가 인터페이스(이 경우에는 전기 콘센트)에 연결되는 방법과 토스터자체가 작동하는 방법만 알아야 한다.

사용자 식별: 클래스를 설계할 때 가장 중요한 고려 사항은 클래스의 시청자, 즉 클래스를 사용하는 사람을 식별하는 것이다.

2.1.2. 구현부

구현부의 상세 내용(세부사항)은 사용자에게 드러나지 않는다.

구현부에 관한 한 가지 목표를 명심해야 한다.

구현부를 변경할지라도 사용자는 자신의 코드를 변경하지 않아도 되게 해야 한다.

다소 혼란스러워 보일 수 있지만, 이것이야말로 설계 시의 핵심 목표이다.

좋은 인터페이스: 인터페이스가 올바르게 설계된 경우 구현부가 변경되어도 사용자 코드를 변경하지 않아도 된다.

핸드폰의 전화기능을 예로 들어보자.

핸드폰 마다 전화를 거는 인터페이스는 간단하다.

공급자가 소프트웨어를 업데이터 하더라도 전화를 거는 방법은 바뀌지 않는다.

구현부를 어떤 식을 변경했든지 간에 상관없이 인터페이스가 동일하게 유지된다.

  • 기본적으로 고객은 사용하던 인터페이스가 익숙하기 때문에 변경을 싫어한다.

토스트기의 예제로 돌아가서 토스트의 전기 콘센트를 사용하여 전류를 공급받으면 문제없이 작동한다.

토스트키는 해당 인터페이스를 준수하여 제작되었기 때문에 문제가 없다.

마찬가지로 전류를 공급해주는 발전소또한 전류 공급 인터페이스가 존재한다.

해당 인터페이스를 준수하여 전류를 공급 해야 하는데 만약 1번 공장에선 교류를 2번은 직류를 생산한다면 문제가 발생한다.

따라서 사용자든 구현부든 모두 인터페이스를 준수해야 한다는 것이다.

2.1.3. 인터페이스/구현부 예제

간단한 예제로 DatabaseReader클래스를 만들어 본다.

앞서 이야기 했지만 설계를 할 때는 사용자에 맞게 설계를 진행해야 한다.

요구조건을 보고 그에 맞는 설계를 가져간다.

인터페이스는 프로그래머가 사용할 API인 셈이다.

각 요구 사항에 맞는 기능을 담당할 메서드가 필요하다.

여기서 몇 가지 생각해야 하는 부분이 있다.

  1. 이 클래스를 효과적으로 사용하기 위해서 프로그래머가 구현 세부사항을 알아야 하는가?
  2. 데이터베이스의 내부에 있는 코드가 데이터베이스를 어떻게 여는지를 알아야 하는가?
  3. 특정 레코드에 물리적으로 어떻게 위치하는지를 내부 데이터베이스 코드가 판별하는 방법을 알아야 하는가?
  4. 더 많은 레코드가 남아 있는지 여부를 내부 데이터베이스가 코드가 판별하는 방법을 알아야 하는가?

아무리 생각해도 답변은 NO다.

우리는 이 정보를 알 필요도 없고 알아서는 안된다.

그저 적절한 반환값을 얻고 작업이 올바르게 수행된다는 점만 신경쓰면 된다.

최소화한 인터페이스: 극단적인 경우도 있지만 최소화한 인터페이스를 결정하는 한가지 방법은 인터페이스를 제공하지 않는 것이다. 물론 그런 클래스는 쓸모없지만 이후 협상을 통해 인터페이스를 추가하는 것이다.

2.2. 인터페이스 설계 시 추상적으로 생각해 보기

객체지향 프로그래밍의 주요 장점 중 하나는 클래스를 재상용할 수 있다는 점이다.

일반적으로 재사용 가능한 클래스는 구상적이라기보다는 오히려 더 추상적인 인터페이스를 갖는 경향이 있다.

구상 인터페이스(concrete interfaces)는 매우 구체적인 경향이 있지만, 추상 인터페이스(abstract interfaces)는 더 일반적이다.

그러니 단순히 아주 추상적인 인터페이스가 매우 구상적인 인터페이스보다 더 유용하다고 말하는게 아니라(종종 그러기는 함), 항상 그렇지는 않다고 말하는 것이다.

전혀 재사용할 수 없지만, 매우 유용하고 구상적인 클래스를 작성할 수는 있다.

그러나 우리는 현재 설계 작업을 하고 있으며, 객체지향이 우리에게 제공하는 면을 이용하고 싶다.

따라서 우리는 재사용 가능한 추상 클래스를 설계하는 것을 목표로 삼아야 한다.

택시를 예로 들어 우회전, 좌회전, 정지등과 같이 별도의 인터페이스를 가지는 것 보다 공항으로 가주세요 같은 인터페이스를 갖는 것이 휠씬 더 유용하다.

사용자는 도착하기만 하면 그만이기 때문

실제로 호텔에서 나와 택시를 탄다면 택시기사는 “어디로 갈까요?”라고 물을 것이고 우리는 “공항으로 가주세요”라고 답할 것이다.

물론 도시에 공항이 하나만 있다면 위처럼 말하거나 여러개라면 인천공항등의 Type이 붙을 것이다.

따라서 “공항으로 가주세요”같은 추상 인터페이스는 일반적으로 시카고나 뉴욕같은 도시별로 구현방식을 다르게 하여 재사용 가능하기 때문에 더 유용하다.

2.3. 가능한 한 사용자에게 인터페이스 적게 제공하기

클래스를 설계할 때는 사용자에게 클래스의 내부 작업에 대해서 될 수 있으면 언제든지 최소한의 내용만 알려주는게 일반적인 규칙이다.

  • 사용자에게 꼭 필요한 것만 제공하자.

이것은 클래스에 될수록 적은 인터페이스가 있음을 의미한다. 클래스 설계를 시작할 때 최소한의 인터페이스로 시작하자.

클래스 설계는 반복적이므로 우리는 최소한의 인터페이스 세트로는 충분하지 않을 수 있음을 곧 알게 될 것이다.

  • 사용자가 필요로 하는 것보다 더 많은 인터페이스를 제공하는 것보다 실제로 필요할 때 제공하는 편이 바람직하다.

때때로 사용자가 특정 인터페이스에 접근하는 일이 큰 문제가 된다.

예를 들어, 모든 사용자에게 급여 정보를 제공하는 인터페이스는 필요하지 않다. 알아야 하는 사용자만 알면 된다.

  • 우선, 하드웨어 예제를 사용해 소프트웨어 예제를 이해해 보자.

모니터와 키보드 없이 사용자에게 컴퓨터 상자를 전달하는 일을 상상해보자

이런 경우라면 컴퓨터는 사용되지 않을 것이다.

PC에 대한 최소한의 인터페이스 세트를 사용자에게 제공해야 한다.

그러나 이 최소 세트로는 충분하지 않으며, 곧 인터페이스를 추가해야 할 것이다.

  • 공개 인터페이스를 지정하여 사용자가 바로 접근할 수 있는 대상을 정의한다.

우리가 처음에는 인터페이스를 비공개로 지정해 클래스 전체를 사용자로부터 은닉했다고 할지라도, 프로그래머가 이 클래스를 사용하기 시작하면서 일부 메서드를 공개로 지정해야겠다는 압박을 받게 되면 그런 메서드를이 공개 인터페이스가 되는 것이다.

  • 정보 시스템의 관점에서 클래스를 설계하기보다는 사용자의 관점에서 클래스를 설계해야 한다.

클래스 설계자가 특정 기술 모델에 맞도록 클래스를 설계하는 경우가 너무 흔하다.

설계자가 사용자의 관점에서 보려고 애를 쓰더라도 여전히 기술자의 관점에서 벗어나기는 힘들며, 이러한 상황에서는 클래스가 기술적인 관점에서 작동하도록 설계되므로 사용자는 클래스를 사용하기 쉽지 않게 된다.

  • 요구사항과 설계를 다루는 클래스를 설계할 때 개발자뿐만 아니라 실제로 사용하는 사람들과 함께 설계해야 한다.

시스템 프로토 타입을 만들 때 이러한 클래스를 개선하고 보강할 여지가 크다.

생각

대략적으로 알고 있던 내용을 전체적으로 살펴주니 이해가 잘된다.

그 중에서 경험적인 부분이 있어서 적어본다.

1. 사용자에게 필요 이상의 API를 노출하지 말아야 한다는 부분

초보 개발자들이 많이 하는 실수 중 하나가 클래스를 설계하고 이후 다른 사용자가 해당 클래스의 인터페이스를 사용할 때 필요 이상의 API를 오픈하는 문제이다.

왜 그런것인지 내가 왜 그랬는지 생각을 해보니 가장 많이 접하는 API가 바로 라이브러리 메서드들이다.

해당 클래스들은 필요 이상의 예상된 기능들을 많이 넣어두는데 이걸 보고 클래스를 설계할 때도 좀 더 편하게 쓰라고 필요 이상의 인터페이스를 오픈하는 것 같다.

우리가 만드는 클래스와 해당 라이브러리의 성격은 전혀 다르며 자동차를 제작중인 경우에 사용되는 부품정도를 라이브러리라고 볼 수 있을 것 같다..

2. 클래스 설계자가 특정 기술에 맞춰 클래스를 설계하는 경우이다.

앞에서 두번 정도 말했지만 사용자에게 맞춰서 맞는 방법으로 제작해야 하지만 공부나 체험의 명목으로 설계가 되게 되면 원하는 결과물이 나오기 쉽지 않다.  

과거 프로그래밍 패턴을 공부하던 중 해당 패턴을 실제 프로젝트에 적용하고 싶어서 억지로 같은 틀을 끼워넣은 저이 있다.  

지금은 좋은 반면교사이지만, 그 때는 그게 정답인줄 알았다.  

위에서 말하는 정답은 없다와 100명이 설계하면 전부 다른 결과물이 나오는 것 처럼 그 때 설계하고 이후에 필요하다면 기술을 도입하는 방식이 적절하다.

2.3.1. 누가 사용자인지 결정하기

앞서 다룬 택시 예제에서는 사용자를 실제로 시스템을 사용하는 사용자라고 결정했다.

조금 더 생각해본다면 “사용자는 누구인가?”라는 점을 집중하게 된다.

첫 번째는 아마 고객일 것이다.

확실히 고객도 사용자이긴 하지만 택시기사도 마찬가지로 고객에게 서비스를 성공적으로 제공할 수 있어야 한다.

다시 말해서 공짜로 공항까지 태워주세요라고 말하는 고객에게 인터페이스를 제공할 일은 의심할 여지없이 택시 기사와 어울리지 않을 것이다.

따라서 현실에 맞게 사용해 볼 만한 인터페이스를 구축하려면 고객과 택시기사 모두 사용자이다.

요컨대, 택시 객체에 메세지를 보내는 모든 객체는 사용자로 간주된다.

2.3.2. 객체의 행위

사용자를 식별했다면 객체의 행위를 결정해야 한다.

각 객체의 목적과 올바르게 수행해야 할 작업을 모든 사용자의 관점에서 식별하자.

많은 초기 선택지는 최종 공개 인터페이스 선발에서 살아남지 못할 것 이다.

2.3.3. 환경에 따른 제약 조건

길버트, 맥거티의 유명 저서에서 종종 객체가 할 수 있는 작업에 제약을 가한다고 지적한다.

실제로 환경에 따른 제약은 거의 한가지 요인으로 컴퓨터 하드웨어가 소프트웨어의 기능을 제한할 수 있다는 말이다.

택시가 공항으로 가는 길에 도로가 없는 것

2.3.4. 공개 인터페이스 식별

사용자, 객체의 행위, 환경애 대한 모든 정보가 수집되면 각 사용자 객체의 공개 인터페이스를 결정해야 한다.

택시 객체를 어떻게 사용할지 생각해 보자

  • 택시에 탄다.
  • 택시 기사에게 가고 싶은 곳을 말한다.
  • 택시 기사에게 요금을 지불한다.
  • 택시 기사에게 팁을 준다.
  • 택시에서 내린다.

택시 객체를 사용하려면?

  • 갈 곳이 있다.
  • 택시를 부른다.
  • 택시 기사에게 돈을 지불한다.

이 내용을 기반으로 Cabble클래스를 만들어보자.

나열된 인터페이스를 정리하거나 수정하는 일은 반복과정이고 필수적이다.

많은 객체지향 교과서에서 각 인터페이스 모델은 하나의 행위만 수행하라고 권한다.

이것은 우리가 설계를 어떻게 추상화하고 싶은지에 대한 질문으로 돌아가게 된다.

enterTaxi() 인터페이스에 요금을 지불하는 로직이 들어가 있기를 바라지 않을 것이다.

해당 로직이 들어가 있으면 논리적이지 않고 클래스 사용자가 요금을 택시기사에게 지불하기 위해서는 어떤 일이 이뤄져야 하는지를 말할 길이 전혀 없게 된다.

2.4. 구현부 식별

공개 인터페이스를 선택했다면 그다음은 구현부를 식별하는 것이다.

클래스가 설계되고 클래스가 올바르게 다루는 데 필요한 모든 메서드가 준비되었다면 클래스의 작동 방법을 구체적으로 생각해 볼 차례다.

기술적으로 보면, 공개 인터페이스가 아닌 것은 모두 구현부로 간주할 수 있다.

다시 말해서 사용자는 구현부에 속하는 메서드를 전혀 볼 수 없다는 말인데, 이러한 것들로는 메서드의 시그니처도 포함되며, 마찬가지로 사용자는 이러한 메서드 내부의 실제 코드를 볼 수 없다는 말이기도 하다.

클래스에는 클래스 내부에서만 사용하는 비공개 메서드들이 있을 수 있다.

사용자는 비공개 메서드를 볼 수 없고 접근할 수도 없으며 비공개 메서드는 구현부의 일부로 간주된다.

예를 들어, 클래스에 ChangePassword()라는 메서드가 있다고 가정해 보자.

같은 클래스 안에 비밀번호를 암호화하는 비공개 메서드도 있을 수 있다.

이런 경우에 비공개 메서드는 사용자에게 숨겨지고 changePassword() 메서드를 통해서만 호출된다.

구현부는 사용자에게 완벽하게 감춰져 있어야 한다.

공개 메서드 내의 코드 또한 사용자가 볼 수 없기 때문에 구현부에 해당한다.

이는 이론적으로 구현부로 간주되는 모든 것이 사용자 클래스에 인터페이스하는 방식에 영향을 미치지 않으면서 변경될 수 있음을 의미한다.

2.5. 느낀점

객체지향 방식으로 일을 한다는 건 과학이라기보다는 예술에 가깝다.

조금은 객체지향에 대한 강박이 사라진 것 같다..

자꾸 정답을 찾으려고 노력하는 스트레스가 있었는데 해결된 기분,,

3. 그 밖의 객체지향 개념들

3.1. 생성자

생성자는 구조적 프로그래머에게 새로운 개념일 수 있다.

객체지향적이지 않은 언엉에서 일반적으로 생성자가 되지 않지만 C/C++같은 언어의 구조체는 생성자가 포함된다.

자바와 C#과 같은 일부 객체지향언어에서 생성자란 클래스의 이름과 같은 이름을 지닌 메서드를 말한다.

객체지향 언어에서 생성자는 반환값이 없다.

3.1.1. 생성자는 언제 호출되는가

새로운 객체가 생성될 때 가장 먼저 일어나는 일 중 하나는 생성자 호출이다.

Cabbie myCabbie = new Cabbie();

new 키워드는 택시 기사 클래스의 새 인스턴스를 만들고 이에 필요한 메모리를 할당한다.

그런 다음 생성자가 호출되어 매개변수 목록의 인수를 전달한다.

따라서 new Cabbie()코드는 Cabbie 객체(정확하게 말하면 Cabbie 클래스)를 인스턴스화하고 생성자인 Cabbie메서드를 호출한다.

3.1.2. 생성자의 내부는 어떨까?

아마도 생성자의 가장 중요한 기능은 new키워드가 발견될 때 할당된 메모리를 초기화하는 일일 것이다.

즉, 생성자에 포함된 코드는 새로 생성된 객체를 초기의 안정적이고 안전한 상태가 되게 정해 주어야 한다.

Count라는 속성을 가진 객체가 있는 경우 생성자에서 Count를 0으로 초기화 해야 한다.

속성초기화
구조적 프로그래밍에서는 Init으로 쓰인다. 시스템 기본값에 의존하지 말 것

3.1.3. 객체지향의 3단계

클래스를 정의한 코드 그대로일 때 이 클래스의 일종의 부류체(class)라고 부른다면 이 부류체는 생성자(constructor)에 의해서 구성체(construct)가 되고, 이 구성체는 다시 사례체(instance)가 되어 활용된다.

즉 객체지향 프로그래밍을 할 때 부류체, 구성체, 사례체로 이어지는 3단계를 거친다고 할 수 있다.

우리가 보통 객체를 설계한다고 할 때는 부류체(class)를 정의한다는 말이며, 객체를 생성한다고 할 때는 구성체(construct)가 된다는 말이며, 인스턴스를 만든다고 할 때 사례체(instance)가 된다는 말이다.

우리가 부류체를 말할 때 붕어 빵 틀에 비유를 하는데 사실 붕어 빵 틀에 대한 설계도라고 하는 것이 더 적합하다.

여기서 나온 구성체야 말로 진짜 붕어 빵 틀이다.

3.1.4. 기본 생성자

클래스를 작성하여 생성자를 두지 않더라도 컴파일, 활용이 가능한 이유는 클래스는 명시적으로 생성자를 두지 않아도 기본 생성자가 제공되기 때문이다.

생성자를 직접 작성하던지 않던지 간에 항상 생성자는 한 개 이상 있다는 점을 이해해야 한다.

기본생성자가 취하는 유일한 작업은 슈퍼클래스의 생성자를 호출하는 것이다.

public Cabbie()
{
    super();
}

컴파일러가 생성한 바이트 코드를 역으로 컴파일 해보면 이 코드가 표시된다.

기본 생성자가 있다고 해서 충분하다고 생각할 수 있지만 대부분 경우에 어떤 메모리 초기화가 수행되어야 한다.

상황에 관계없이 클래스에 항상 생성자를 한 개 이상 두는 게 좋은 프로그래밍 습관이다.

3.1.5. 다중 생성자 사용

많은 경우에 여러 방식으로 객체를 생성할 수 있다.

이는 메서드 오버로딩을 통하여 가능하다.

public Cabbie()
{
    super();
}

public Cabbie(String name)
{
    super();
    this.name = name;
}

3.1.6. 메서드 오버로딩

메서드의 시그니처가 매번 다르면 오버로딩을 통해 프로그래머는 동일한 메서드 이름을 계속해서 사용할 수 있다.

시그니처는 메서드 이름과 매개변수 목록으로 합성된다.

// 시그니처
public String GetRecord(int key)

// 시그니처 = GetRecord + (int key)
// 시그니처 = 메서드 이름 + 매개변수 목록

시그니처는 언어에 따라 반환 형식을 시그니처의 일부로 보기도 하고 그렇지 않기도 한다.
자바 및 C#에서는 반환 형식을 시그니처의 일부로 보지 않는다.

즉, 시그니처를 서로 다르게 하면 생성자별로 다른 객체를 생성할 수 있다.

물론 같은 생성자를 사용하더라도 본질은 다른 객체이긴 하다

이 기능은 사용 가능한 정보의 양을 미리 알 수 없는 경우에 아주 유용하다.

3.1.7. 슈퍼클래스가 생성되는 방법

상속을 사용할 때는 부모 클래스가 어떻게 구성(construct)되어 있는지를 알아야 한다.

상속은 부모에 관한 모든 것을 상속받는다.

상속을 사용할 땐, 부모의 모든 데이터와 행위를 철저히 알고 있어야 한다.

속성의 상속은 아주 분명하다.

그러나 생성자가 어떻게 상속되는지는 분명하지 않다.

  • 생성자 안에서 클래스의 슈퍼클래스의 생성자가 호출된다.
    • 명시적으로 호출하지 않으면 기본 생성자가 자동으로 호출된다.
    • 바이트 코드에서 해당 생성자 코드 부분을 볼 수 있다.
  • 객체의 각 클래스 속성이 초기화된다.
    • 이들은 클래스 정의의 일부인 속성이며, 생성자 내의 속성이나 그 밖의 메서드가 아니다.
  • 생성자의 나머지 코드가 실행된다.

3.1.8. 생성자의 설계도

클래스를 설계할 때는 모든 속성을 초기화하는 편이 바람직하다.

일부 언어에서는 컴파일러가 초기화를 제공하지만 그것을 맹신하면 안된다.

자바는 초기화 이전에 속성을 사용할 수 없다.

생성자는 애플리케이션이 안정적인 상태인지 확인하는 데 사용된다.

설계하는 동안에 모든 속성의 안정 상태가 무엇인지를 식별한 다음에, 생성자에서 이 안정 상태로 초기화하는 편이 바람직하다.

3.2. 오류 처리

클래스를 처음부터 아주 완벽하게 작성하기는 아주 힘들다.

전부는 아니더라도 대부분의 상황에서 문제가 생길 것이다.

문제가 발생할 때에 대한 대비를 미리 해 두지 않으면 재난적인 상황이 벌어질 수 있다.

여러가지 오류처리 방법이 있다.

문제를 바로 수정하거나, 오류억제로 문제를 무시하거나, 런타임 도중 우아한 방법을 통해 오류를 퇴출하는 것.

  • 문제를 무시하는 건 좋은 생각이 아니다.
  • 잠재적인 문제를 확인하고 문제를 발견하면 프로그램이 중단되도록 하자.
  • 잠재적인 문제를 확인하고 실수를 파악한 후 문제를 해결하자
  • 예외를 던진다.

3.2.1. 문제를 무시하지 않기

모든 애플리케이션의 주요 지침을 들자면 애플리케이션이 중단되지 않아야 한다는 점을 들 수 있다.

오류를 처리하지 않으면 결국 애플리케이션이 비정상적으로 종료되거나 불안정한 상태로 간주될 수 있는 상태로 계속 진행한다.

후자의 경우 잘못된 부분이 있다는 사실도 모를 수 있어서 더욱 나쁜 상황이다.

게임 도중에 문제가 발생하여 강제 종료되는 경우

3.2.2. 문제를 점검하고 애플리케이션을 중단하기

잠재적인 문제를 감지함으로써 애플리케이션을 중단할 때는 애플리케이션에 문제를 나타내는 메시지를 표시할 수 있다.

이 경우 애플리케이션이 정상적으로 종료될 것이고 사용자는 문제를 파악할 수 있다.

문제를 무시하는 방법보다 더 나은 처리 방법이긴 하지만 가장 좋은 방법은 아니다.

게임 도중에 서버점검 메세지가 뜨고 종료되는 경우

3.2.3. 문제를 점검해 복구하기

잠재적인 문제를 확인하고 실수를 포착해 복구해 보려는 방식은, 단순히 문제를 확인하고 애플리케이션을 중단시켜 버리는 방식보다 훨씬 뛰어난 해법이다.

서버의 데이터를 받아오기 실패했을 때 다시 받아오는 시도의 경우

3.2.4. 예외 던지기

대부분의 객체지향 언어는 예외라는 특징을 제공한다.

가장 기본적인 의미에서 예외란 시스템 내에서 발생하는 예기치 않은 이벤트다.

try {
    // 예외가 발생할 수 있는 코드
} catch (Exception e) {
    // 예외를 처리하는 코드
}

try블록 내에서 예외가 발생하면 catch 블록이 예외를 처리하고, 블록이 실행되는 동안 예외가 발생하면 다음과 같은 일이 일어난다.

  1. try 블록의 실행이 종료된다.
  2. catch절은 위반으로 인한 예외를 처리하는 데 적합한 catch절을 찾는다.
  3. catch절 중 어느 것도 문제가 되는 예외를 처리하지 않으면 다음 차례에 해당하는 부모 수준의 try블록으로 전달된다.
  4. 일치하는 catch절이 있다면 catch절이 실행된다.
  5. 그런 다음 try블록의 다음 차례로 나오는 명령문으로 살행이 재개된다.

방어 코드(bulletproof code): 여기에 설명된 방법을 조합해 프로그램을 가능한 사용자에게서 강력히 보호하는 게 좋다.

3.3. 범위의 중요성

클래스 한 개로부터 여러 객체를 인스턴스화할 수 있다.

이러한 각 객체에는 고유한 ID와 상태가 있다.

이게 핵심이다.

각 객체는 개별적으로 생성되며, 자체 메모리에 할당된다.(디버깅으로 해당 ID값을 볼 수 있음)

그러나 일부 속성 및 메서드는 올바르게 선언된 경우에 동일한 클래스에서 인스턴스화된 모든 객체가 공유할 수 있으므로 이러한 클래스 속성 및 메서드에 할당된 메모리를 공유할 수 있다.

공유 메서드: 생성자는 클래스의 인스턴스가 공유하는 메서드의 좋은 예이다.

메서드는 객체의 행위를 나타낸다.

객체의 상태는 속성으로 표현한다.

속성의 종류는 세 가지다.

  • 지역적인 속성
  • 객체의 속성
  • 클래스의 속성

3.3.1. 지역적인 속성

특정 메서드를 사용해 지역적인 속성들을 지닐 수 있다.

public class Number{

    public Method1(){
        int count;
    }

    public Method2(){
    
    }
}

method1 메서드에는 count라는 지역 변수가 있다.

이 정수형 변수에는 method1 내부에서만 접근이 가능하다.

method2 메서드는 정수형 변수 count가 존재하는지도 모른다.

이 시점에서 매우 중요한 개념인 범위(Space)를 소개한다.

속성은 특정 범위 안에 들어 있게 된다.

이 경우에 정수형 변수인 count는 method1이라는 범위 안에 들어 있게 된다.

클래스 자체에도 범위가 있고 메서드 마다의 각 범위가 있다.

3.3.2. 객체의 속성

많은 설계 상황에서 동일한 객체 내의 여러 메서드들이 속성을 공유해야 할 때가 있다.

한 객체에 멤버 변수, 즉 인스턴스 변수로 설정된 count가 존재한다면 이는 해당 클래스에 존재하는 메서드에서 참조가 가능하다.

여러 객체를 생성한다면 각각 객체마다 고유한 메모리를 할당받아 count라는 변수를 각각 가지게 된다.

이런 경우 각 객체의 속한 count는 해당 객체의 속성이라고 정의한다.

중복된 식별자라도 범위가 다르면 다른 속성이 된다.

객체의 속성 this로 호출

3.3.3. 클래스의 속성

동일한 클래스를 가지고 만든 두 개 이상의 객체들끼리 서로 속성을 공유할 수 있다.

정정 형식(Static)으로 선언한다.

static을 사용하면 클래스에서 인스턴스화된 모든 객체에 대해 단일 메모리 속성이 할당된다.

따라서 클래스의 모든 객체는 count를 가리키는 동일한 메모리 위치를 사용하게 된다.

정적 속성은 사이드 이펙트를 유발할 수 있기 때문에 잘 사용되지 않는다.

3.4. 연산자 오버로딩

일부 객체지향 언어를 사용하면 연산자를 오버로딩할 수 있다.

오버로딩은 강력한 메커니즘이지만 코드를 읽는 사람이나 유지보수하는 사람들에게 혼동을 줄 수 있다.

3.5. 다중 상속

다중 상속은 클래스 설계의 더 강력하면서도 난해한 측면 중 하나이다.

다중 상속이라는 말을 통해 알 수 있듯이, 다중 상속을 통해 어떤 한 가지 클래스를 자기 자신이 아닌 그 밖의 클래스들 중 두 개 이상으로부터 상속을 받을 수 있다.

실제로도 다중 상속이 좋은 방안인 것 처럼 보이지만, 다른 책에서도 다루듯이 많은 문제점이 있다.

다중 상속은 연산자 오버로딩과 마찬가지로 강력한 기능이다.

어떤 문제는 필수로 필요로하고 세련되게 해결이 가능하다.

하지만 복잡성이 크게 늘어나게 된다.

자바와 닷넷은 이를 생각하여 지원하지 않는다.

행위 상속과 구현부 상속

인터페이스는 행위 상속 메커니즘이며, 추상 클래스는 구현부를 상속하는 데 사용된다.

인터페이스가 담당하는 언어 구성소가 행위적 인터페이스를 제공하면서도 구현부를 제공하지 않는 반면에, 추상 클래스들은 인터페이스와 구현부를 둘 다 제공할 수 있다.

3.6. 객체 연산

우리가 복잡한 자료구조나 복잡한 객체를 다뤄야 한다면, 프로그래밍에서 가장 기본적인 연산자들일지라도 그중에 어떤 것들은 점점 더 복잡해져야 한다.

예를 들어, 기본 데이터 형식을 복사하거나 비교하는 과정은 무척이나 간단하다.

그러나 비교하는 일은 그리 간단하지 않다.

  • 클래스와 참조

복잡한 자료구조 및 객체의 문제점은 참조가 포함될 수 있다는 점이다.

단순히 참조만 복사한다면 참조 대상인 자료구조나 객체는 복사되지 않는다.

같은 맥락에서 객체를 비교할 때 포인터를 그 밖의 포인터와 단순하게 비교만 한다면, 실상은 포인터가 가리키는 것을 비교하는 게 아니라 참조를 비교하는 셈이 되고만다.


객체들을 대상으로 비교나 복사를 수행할 때는 문제가 발생한다.

특히, 포인터를 따라 가는지 여부에 따라 질문할 내용이 달라진다.

그럼에도 객체를 복사할 수 있는 방법이 있어야 한다.

복사는 간단해 보이지만 유효한 복사를 하려면 참조 트리들을 따라가야하는 복잡함이 있다.

  • 전체 복사 vs 단순 복사

모든 참조를 따라가 참조된 모든 객체에 대해 새 사본을 작성할 때 전체 복사가 이뤄진다.

DeepCopy

한 차례의 전체 복사에 많은 수준이 관련될 수 있다.(부담되는 작업일 수 있음)

단순 복사는 참조를 복사할 뿐 수준별로 처리하지 않는다.

ShallowCopy


이런 문제는 객체들을 서로 비교할 때도 나타난다.

객체를 비교하기 위해선 복사와 마찬가지로 참조 트리를 따라가야 한다.

3.7. 결론

클래스 설계와 같은 상위 수준의 객체지향 개념을 다뤘다.

3.7.1 느낀점

대부분 알고 있던 내용이지만 한번 점검하는 느낌이라 나쁘지 않았다..

4. 클래스 해부하기

클래스를 무작정 처음부터 설계하면 안되는 이유는 어떤 클래스도 고립된 섬 같은게 아니기 때문이다.

객체가 인스턴스화되면 객체는 거의 항상 그 밖의 객체와 상호 작용을 한다.

객체는 다른 객체의 일부이거나 상속 계층의 일부일 수도 있다.

4.1. 클래스의 이름

클래스의 이름은 매우 중요하다.

다른 개발 서적(clean code, effective java)에서도 클래스의 이름은 매우 중요하다고 강조한다.

클래스가 하는 일과 이름이 더 큰 시스템 내에서 상호 작용하는 방법에 대한 정보를 제공하기 때문에 이름을 잘 지어야 한다.

using System;

namespace ConsoleApplication1
{
    class TestPerson
    {
        public static void Main()
        {
            Cabbie joe = new Cabbie("Joe", "1234");

            Console.WriteLine(joe.Name);
            Console.ReadLine();
        }
 }
    
    public class Cabbie 
    {
 
     private string _Name;
     private Cab _Cab;

     public Cabbie() {

         _Name = null;
           _Cab = null;

        }

        public Cabbie(string name, string serialNumber) {

         _Name = name;
         _Cab = new Cab(serialNumber);

     }

        //Methods  
        public String Name
        {
            get { return _Name; }
            set { _Name = value; }
        }

    }

    public class Cab 
    {
 
     public Cab (string serialNumber) {

         SerialNumber = serialNumber;

     }
        
        //The property is public to get, but private to set
        public string SerialNumber { get; private set; }
        
    }

}

4.1.1. 주석

사용된 주석은 구문과 관계없이 함수를 이해하는 일종의 알림판이다.

주석에 대한 생각은 CleanCode, GoodCode, BadCode와 크게 다르지 않아서 생략한다.

잘짜여진 코드는 주석이 필요없다. 일종의 변명이다.

4.2. 속성

객체에 대한 정보를 저장하는 속성은 객체의 상태를 나타낸다.

  • 가능한 많은 데이터를 숨기기

모든 속성을 최소한으로 유지하는 설계를 가져가고 메서드 인터페이스를 통해 소통한다.

MyCab이라는 속성은 그 밖의 객체를 가리키기 위한 참조이다.

  • 참조 전달

Cab객체가 그 밖의 객체에 의해 생성되었을 수 있다.

따라서 참조는 Cabbie객체로 전달된다.

그러나 이 예를 위해 Cab은 Cabbie 객체 내에 만들어진다.

결과적으로 우리는 실제로 Cab객체의 속을 들여다볼 생각이 없는 것이다.

private Cab _Cab; 이 시점에는 Cab객체에 대한 참조만 생성된다는 점에 집중하자.

이 정의문만으로 메모리가 할당되지 않는다.

4byte의 레퍼런스 변수 메모리만 할당됨

4.3. 생성자

public Cabbie() {
         _Name = null;
           _Cab = null;
        }

        public Cabbie(string name, string serialNumber) {

         _Name = name;
         _Cab = new Cab(serialNumber);
     }

첫 번째 생성자는 Default 생성자이다.

이 생성자는 시스템에서 제공하는 Default 생성자가 아니고 기본 생성자라고 부르는 이유는 인수가 없는 생성자이기 때문이다.

사실상 컴파일러의 기본 생성자를 가지고 오버라이딩한 것이다.

앞 장에서 말한 것 처럼 시스템의 기본 생성자에 의존하지 말 것

다중 생성자를 이상적인 방법으로 여겨지지 않는 경우가 있다.

4.4. 접근자

대부분의 속성의 경우 private으로 정의된다.

그렇다면 객체와 상호작용하지 않고 격리된 객체를 만드는 것이 올바른 일인가?

아니다.

때로는 격리된 객체가 유용할 수 있지만 객체들과 상호작용하기 위해선 접근자의 수준을 잘 결정해야 한다.

객체 속성에 접근해야 하는 경우가 종종 있지만 그런 일을 객체가 직접 수행할 필요는 없다.

만약 객체 A가 객체 B를 제어할 권한이 없는데도 객체 B의 속성을 검사하거나 변경하는 기능을 갖게 되기를 바라지 않는다.

여기에는 몇 가지 이유가 있는데 가장 중요한 이유는 데이터 무결성과 효율적인 디버깅으로 귀결된다.

이러한 측면에서 get, set함수와 프로퍼티가 잘 활용된다.

이러한 무결성을 get,set함수에 잘 처리된 로직을 추가하면 더욱 단단해진다.

실제로 각 객체에 대한 비정적 메서드의 실제 사본은 존재하지 않는다.

각 객체는 동일한 물리적 코드를 가리킨다.

그러나 개념적 수준에서 볼 때는 객체가 완전히 독립적이며, 고유한 속성과 방법을 갖는 것으로 생각할 수 있다.

4.5. 공개 인터페이스 메서드

생성자와 접근자 메서드는 모두 공개(public)으로 선언되어 있으니, 이 둘은 모두 공개 인터페이스의 일부다.

이것들은 클래스 생성에 특별히 중요하다.

공개 인터페이스는 매우 추상적인 경향이 있으며, 구현부는 아주 구상적이다.

4.6. 비공개 메서드

클래스의 메서드들을 그 밖의 클래스로부터 숨기는 편이 더 일반적이다.

이러한 비공개 메서드는 공개 인터페이스가 아닌 구현부의 일부로 사용된다.

중요한 점은 비공개 메서드는 구현부의 일부이며, 그 밖의 클래스에서는 접근할 수 없다는 것이 핵심이다.

4.7. 결론

클래스가 어떻게 구축되는지 이해하고 기본 개념을 설명했다.

4.7.1. 느낀점

마찬가지로 기본적인 객체지향 내용이다.

5. 클래스 설계 지침

객체지향 프로그래밍이란 단일 엔터티인 데이터와 행위를 캡슐화해 완전한 패키지인 클래스를 만든다는 아이디어에 따른 것이다.

따라서 클래스는 택시와 같은 논리적 구성요소를 나타내야 한다.

이번 장에선 클래스 설계와 관련하여 몇 가지 제안을 한다..!

5.1. 현실 세계 시스템 모델링

객체지향 프로그래밍의 주요 목표 중 하나는 사람들이 실제로 생각하는 방식과 비슷한 방식으로 현실 세계의 시스템을 모델링하는 것이다.

클래스 설계라는 것은 이러한 모델을 작성하기 위해 동원하는 객체지향적인 방법이다.

객체지향 방식은, 데이터와 행위가 논리적으로 별개의 객체가 되게 하는 구조적 방식이나 하향식방식을 사용하는 대신에, 데이터와 행위가 서로 상호작용할 수 있게 한 객체 안에 두어 캡슐화한다.

이렇게 사고하게 되면 문제를 사건의 연속이라고 생각하거나 별로로 존재하는 데이터 파일에서 작동하는 루틴이라고 생각하지 않는다.

이 사고 방식의 우아함은 클래스가 문자 그대로 현실의 객체를 모델링한다는 점과, 이렇게 모델링해 만든 객체가 그 밖의 현실 객체와 상호 작용하는 방식에서 나온다.

이러한 상호 작용은 현실 객체간의 상호 작용과 비슷한 방식으로 발생한다.

앞의 예제와 같이 실제 행위를 나타내는 택시 기사와 택시를 생각해서 모델링한다면 Cab객체와 Cabbie 객체는 데이터와 행위를 캡슐화한 다음에 공개 인터페이스를 통해 서로 상호 작용한다.

여기서 말하는 이 사고가 정말 중요하다. 객체지향적 시선을 가지는게 어렵지만 한가지를 객체지향 클래스로 구현한다면 상속구조나 인터페이스, 캡슐화 수준을 고려해보는 연습을 하면 도움이 된다..

객체지향 프로그래밍이 처음으로 대중화되었을 때 구조적 프로그래밍을 하던 많던 많은 프로그래머는 객체지향 프로그래밍으로 전환하기 어려웠다.

구조적 방식으로 프로그래밍을 하던 프로그래머가 저지른 주요 실수 중 하나는 실제로 구조적인 모델을 바탕으로 삼아 함수나 서브루틴을 만듦으로써 결과적으로 행위는 있지만 클래스 데이터가 없는 클래스를 만들었다는 점이다.

이런 방식은 캡슐화의 힘을 사용하지 않았기 때문에 생긴 것으로 바람직하지 않다.

실제로 많은 프로그래머가 객체지향언어를 시작하며 배워온 구조적 프로그래밍 방식을 객체지향언어에 적용하면서 많이 발생하는 문제점이다.

이런 방식은 현재는 부분적으로 이뤄진다.

현재 빈약한 도메인 모델이라고 할 수 있는 DTO와 뷰를 생성하기에 충분한 데이터가 있는 뷰 모델의 형태을 사용해 개발하는 방식이 많이 도입되었다.

데이터를 다루는 일과 행위들에 더 초점이 맞춰져 왔는데, 이는 인터페이스를 통해 처리된다는 말이기도 한다.

행위를 단일 응답 인터페이스로 캡슐화한 다음에 인터페이스에 코딩하면 코드는 더욱 유연해지고 모듈화되며, 유지보수를 하기가 훨씬 쉬워진다.

5.2. 공개 인터페이스 식별

클래스를 설계할 때 가장 중요한 문제는 공개 인터페이스를 최소한으로 유지하는 것이다.

클래스를 만들기 위한 목적은 유용하고 간결한 것을 제공하는 데 있다.

잘 설계된 객체의 인터페이스는 클라이언트가 원하는 서비스를 설명한다

5.2.1. 최소 공개 인터페이스

최소 공개 인터페이스(minimum public interface)를 제공하면 클래스가 가장 간결해진다.

목표는 사용자에게 작업을 올바르게 수행할 수 있는 정확한 인터페이스를 제공하는 것이다.

공개 인터페이스가 불완전한 경우에(즉, 행위가 누락된 경우) 사용자는 전체 작업을 수행할 수 없다.

공개 인터페이스를 적절하게 제한해 두지 않으면(즉, 사용자가 불필요하거나 위험한 행위에 접근할 수 있는 경우라면) 문제가 발생해 디버깅이 필요할 수 있으며, 시스템 무결성 및 보안 문제가 생길 수 있다.

클래스를 만드는 일은 일종의 업무 계획이므로 설계 과정을 이루는 모든 단계와 마찬가지로 사용자는 테스트 시작 단계부터 테스트의 모든 단계에 걸쳐 반드시 설계에 참여해야 한다.

  • 인터페이스 확장
    • 클래스의 공개 인터페이스가 특정 애플리케이션에 충분하지 않더라도 객체 기술을 사용해 이 인터페이스를 확장하고 조정할 수 있다.
    • 요약하자면, 올바르게 설계된 경우라면 기존 클래스를 활용하는 확장 인터페이스가 되도록 새 클래스를 만들 수 있다.
    • 개발자가 상속을 사용해 행위를 추가하기보다 인터페이스를 사용해 행위를 추가해야 하는 이유다.

5.2.2. 구현부 숨기기

공개 인터페이스를 식별하는 일은 클래스 사용자가 중심이 되게 설계해야 하는 문제인 반면에, 구현부는 사용자가 전혀 관여하지 않도록 해야 한다.

여기서 사용자란, 코드의 실제 동작 과정을 생각하면 된다. 예를 들어 택시 회사의 택시 기사는 택시 객체를 내부 구성으로 가지고 있고 택시의 최소 인터페이스만 사용하여 운전을 한다. 내부 엔진, 기관등을 알 필요가 없다.

구현부가 사용자에게 필요한 서비스를 제공해야 하는 것은 맞지만, 이러한 서비스가 실제로 어떻게 수행되는지를 사용자에게 투명하게 내비쳐서는 안된다.

사용자에게 영향을 미치지 않고 구현부가 변경될 수 있는 경우라야 클래스가 최대로 유용해진다.

이 부분에서 생각해낼 수 있는 부분이 구현부가 변경된다면 해당 행위에 대한 일관성이 보장되는가? 라는 의문이 있을 수 있는데 이는 테스트코드를 생각하면 쉽게 해결된다.

여기서 나온 개념이 TDD라고 생각되는데 도메인에 맞는 테스트 코드를 미리 만들고 기능을 구현한다면 이후에 구현부가 변경되고 해당 인터페이스를 활용하여 테스트를 통과하면 무결성이 보장된다.

다시 말해 행위를 바꿀 수 있게 하는 가장 좋은 방법은 인터페이스와 컴포지션을 사용하는 것이다.

  • 고객 vs 사용자
    • 떼로는 소프트웨어를 실제로 사용하는 사람을 사용자가 아닌 고객으로 부르기도 한다.
    • 같은 맥락에서 조직의 일부인 사용자를 내부 고객이라고 부를 수 있다.
    • 사소해 보이는 개념이라고 생각할 수 있지만, 모든 최종 사용자를 실제 고객으로 생각하는 편이 좋다.

5.3. 튼튼한 생성자나 소멸자가 되게 설계하기

클래스를 설계할 때 가장 중요한 설계 문제 중 하나는 클래스 생성 방법이다.

무엇보다 생성자는 객체를 초기의 안전한 상태에 두어야 한다.

여기에는 속성 초기화 및 메모리 관리와 같은 문제가 포함된다.

또한, 객체가 기본 조건에 따라 올바르게 생성되어 있는지를 확인해야 한다.

일반적으로 이 기본적인 상황을 처리할 생성자를 제공하는 것이 좋다.

소멸자를 포함하는 대부분의 언어(GC가 있는 언어는 따로 두지 않고 clear정도의 함수로 메모리 관리)는 적절한 정리 함수를 넣어두는 것이 적당하다.

특정한 시점에 메모리를 해제하는 등의 행위..

5.3.1. 생성자 주입

의존성 주입의 한 예로 생성자를 통해 의존성을 주입하여 디커플링을 도모하는 방법이다.

서비스 클래스를 클래스 내에 두는 것이 아니라 객체를 생성할 때 생성자에 주입한다는 것을 의미한다.

예를 들어 택시 기사는 자신의 먼혀 객체, 무선 정보 객체를 얻을 수 있으며, 택시를 시동하는 키는 생성자를 통해 객체로 전달된다.

이렇게 되면 택시기사는 일반적인 객체가 아닌 생성자에 따라 다른 성격을 가질 수 있다.

5.4. 클래스에 대한 오류 처리 설계

생성자를 설계할 때와 마찬가지로 클래스가 오류를 처리하는 방법을 설계하는 일도 무척 중요하다.

5.4.1. 클래스 문서화 및 주석 사용

대부분의 개발자는 코드를 철저히 문서화해야 한다는 점을 알고 있지만, 일반적으로 이런 일에 시간을 내고 싶어하지 않는다.

그러나 문서화를 잘 하는 좋은 습관이 없이는 훌룡하게 설계하기는 사실상 불가능하다.

좋은 설계의 가장 중요한 측면 중에 하나는, 클래스 설계이든지 아니면 그 밖의 어떤 설계이든지 간에, 이러한 설계 과정을 신중하게 문서화해야 한다는 점이다.

5.4.2. 협동할 수 있는 객체로 만들기

홀로 쓰이는 클래스가 더의 없다는 점을 확실히 말할 수 있다.

클래스가 한 번만 사용되지 않는 한, 다른 클래스와 상호 작용을 하지 않는 클래스를 굳이 빌드할 이유가 거의 없기 때문이다.

어떤 한 가지 클래스는 그 밖에 클래스에 서비스한다.

한편으로 서비스를 받는 클래스가 그 밖의 클래스에 서비스하기도 하고 클래스끼리 서비스하기도 한다.

5.5. 재사용을 고려한 설계

객체는 다른 시스템에서 재사용할 수 있으며, 재사용을 염두에 두고 코드를 작성해야 한다.

클래스를 다양한 시스템에서 사용할 수 있게 하려면 재사용할 일을 염두에 두고 클래스를 설계해야 한다.

설계 과정에서 생각을 많이 해야 하는 이유이다.

그렇다고 모든 시나리오를 예측히는 것은 불가능하다.

5.6. 확장성을 고려한 설계

클래스에 새로운 기능을 추가하는 일은 기존 클래스를 확장하고 몇 가지 새로운 메서드를 추가하고 다른 클래스의 행위를 수정하는 일처럼 간단할 수 있다.

모든 것을 다시 작성할 필요는 없다. 이때 상속이 필요하다.

Person클래스를 작성하는 일이 이제 막 끝난 경우라면 Employee클래스나 Customer클래스를 작성할 수 있다는 사실을 고려해야 한다.

이런 경우에 Person클래스를 확장 가능(extensible) 클래스라고 부른다.

이런 확장 클래스의 성격으로 작업할 때 필요하지 않은 기능까지 상속되는 경우가 종종 발생하는데 이 부분이 앞에서 논의한 추상화 지침, 수준을 다루는 것이다.

Person클래스에는 개인과 관련된 데이터와 행위만 포함되어야 한다.

그 다음 다른 클래스가 이를 서브클래스로 삼아서 적절한 데이터와 행위를 상속할 수 있다.

SOLID의 원칙 중 한가지는 클래스는 확장할 수 있도록 개방되어야 하지만 수정하는 일에 대해서는 폐쇄되어야 한다는 원칙이 있다.

인터페이스를 사용해 클래스에 코딩을 하면 실시간으로 테스트하고 배포한 코드를 건드리지 않고 데코레이터와 같은 온갖 종류의 패턴을 사용해 필요한 것을 확장할 수 있다.

정적메서드를 사용하게 되면 클래스끼리의 강한 결합이 발생하게 된다.

정적 메서드는 추상화할 수 없다.

정적 메서드는 흉내낼 수 없다.

정적 인터페이스를 제공할 수 없다.

유일하게 사용 가능할 때는 Helper클래스를 만들 때이다.

5.6.1 알아보기 쉽게 이름을 짓기

이름만 보고도 객체가 무엇을 표현하고 있는지를 알 수 있어야 한다.

이러한 명명 규칙은 종종 여러 조직의 코딩 표준에 의해 결정된다.

팀 by 팀

5.6.2. 이식하기 어려운 코드를 추상화하기

이식하기 어려운 코드를 사용해야 하는 시스템을 설계하는 경우에 이런 코드를 클래스에서 뽑아 내어 추상화해야 한다.

추상화한다는 것은 이식하기 어려운 코드를 자체 클래스나 최소한 자체 메서드로 분리한다는 뜻이다.

5.6.3. 객체 복사 및 객체 비교 방식을 제공

객체가 복사 및 비교될 수 있는 가능성이 있기 때문에(뒷장에서 다룸) 그러한 방법을 설계해야 한다.

5.6.4. 범위를 가능한 한 작게 유지

범위를 최대한 작게 유지하면 추상화와 구현부 은닉이 자연스럽게 이뤄진다.

가능한 한 많은 속성과 행위를 지역화를 하는 것이다.

이런 식으로, 클래스를 유지하고 테스트하고 확장하는 편이 훨씬 쉽다.

인터페이스를 사용하면 이런 면이 더 강화된다.

범위 및 전역 데이터

전역 변수의 범위를 최소화하는 게 좋은 프로그래밍이며, 이는 객체지향에만 국한되지 않는다.
실제로 객체지향 개발 시에는 전역 데이터가 없다.

5.7. 유지보수를 고려한 설계

유용하고 간결한 클래스가 되게 설계하면 유지보수성이 크게 좋아진다.

클래스를 설계할 때 확장성을 염두에 두고 설계하듯이 향후의 유지보수도 염두에 두고 설계해야 한다.

클래스를 설계할 때에는 코드를 관리하기 쉬운 여러 조각으로 작성해 둔 다음에 합성할 수 있게 설계하는 게 바람직하다.

여러 조각으로 나뉜 코드를 유지보수하기가 큰 코드를 유지보수하기보다 쉬운 편이다.

유지보수성을 높이는 가장 좋은 방법 중의 하나는 서로 의존하는 코드를 줄이는 것이다.

커플링, 거버넌스를 줄이는 것이 핵심

처음부터 클래스를 올바르게 설계했다면, 시스템을 변경할 때는 객체의 구현부만 변경해야 한다.

어떤 식으로든 공개 인터페이스를 변경하지 말아야 한다.

공개 인터페이스를 변경하는 순간 모든 시스템에 파급 효과가 발생한다.

처음부터 연결정도만 설계하고 구현부의 로직을 붙이거나 수정하는 방식을 생각해보게 되는데 과연 그정도 예상능력이 가능할까??

5.7.1. 개발 과정 반복

대부분의 설계 및 프로그래밍의 함수와 마찬가지로 반복적인 과정을 겪는게 바람직하다.

모든 코드를 한 번에 작성하지 말아야 한다.

코드 크기를 작게 해서 작성한 다음에 각 단계별로 코드를 빌드하고 테스트하자.

테스트 계획을 잘 짜 놓으면 인터페이스가 충분하지 않은 영역을 빨리 찾아낼 수 있다.

5.7.2. 인터페이스 테스트

인터페이스를 최소한으로 구현한 것을 종종 스텁(stubs)이라고 한다.

스텁을 잘 사용하면 실제 코드를 작성하지 않고도 인터페이스를 테스트할 수 있다.

5.8. 객체 지속성 사용

객체의 지속성은 많은 객체지향 시스템에서 해결해야 하는 또 다른 문제이다.

지속성(persistence)이란 객체의 상태를 유지한다는 개념이다.

프로그램을 실행할 때 어떤 방식으로든 객체를 저장하지 않으면 객체가 죽어 버려서 다시는 복구되지 않는다.

느낀점

이번 챕터에서 되게 테스트 코드, 오류처리, 클래스 설계에 대한 생각에 대한 생각이 많아지는 것 같다.

6. 객체를 사용해 설계하기

우리는 제품을 구매하면 그 제품이 정상적으로 작동할 것으로 기대한다.

하지만 모든 제품이 그런 것은 많은 제품이 생산될 때 대부분의 시간과 노력이 설계 단계가 아닌 공학단계에 쓰인다는 점이다.

앞 장에선 우수한 클래스를 설계하는 것을 알아봤다면 이번 장은 우수한 시스템을 설계하는 방법을 알아본다.

시스템은 서로 상호 작용하는 클래스들을 가지고 정의할 수 있다.

6.1. 설계 지침

진정한 설계 방법론은 한 가지뿐이라는 말은 오해다.

절대적으로 사실이 아니며 설계를 하기 위한 방법 중에 특별히 옳은 방법이나 그른 방법은 없다.

오늘날에도 많은 설계 방법론들이 있으며, 그에 따른 지지자들이 존재한다.

그러나 가장 중요한 문제는 사용할 설계 방식이 아니라 메서드 사용법을 어떻게 설계했느냐다.

이런 주제는 단순한 설계 과정을 넘어 전체 소프트웨어 개발 과정을 포괄하도록 확장될 수 있다.

일부 조직은 표준 소프트웨어 개발과정을 따르지 않거나 준수하지 않는다.

좋은 설계를 만드는 데 있어 가장 중요한 요소는 자신과 조직이 편안하게 느끼고 이를 고수하며 계속 개선하는 과정을 찾는 것이다.

아무도 따르지 않는 설계 과정을 구현하는 것은 의미가 없다.

일반적으로 견고한 객체지향 설계 과정에는 다음 단계가 포함된다.

  1. 적절한 분석 수행
  2. 시스템 설명 작업명세서 개발
  3. 이 작업명세서로부터 요구사항 수집
  4. 사용자 인터페이스용 프로토타입 개발
  5. 클래스 식별
  6. 각 클래스의 역할을 결정
  7. 다양한 클래스가 서로 상호 작용하는 방식을 결정
  8. 만들고자 하는 시스템을 설명하는 고급 모델을 구성

객체지향 개발의 경우에 고급 시스템 모델이 특히 중요하다.

시스템 모델이나 객체 모델은 클래스 다이어그램들과 클래스의 상호 작용들로 이뤄진다.

이 때 표기법으로 UML이 많이 사용되는데 시각적으로 나타내기 유용하다.

표준으로 자리잡지 못해서 지금은 많이 사용하지 않는 것 같다..

지속적인 설계 과정

최선의 의도와 계획에도 가장 조그만 설계를 하는 경우가 아니라면 설계 과정은 지속적인 반복 과정이다.

제품을 테스트한 후일지라도 설계를 변경해야 할 일이 생긴다.

제품을 변경하거나 기능을 추가하는 일을 확실히 마치는 시점에 도달하는 일은 프로젝트 관리자의 역할이다.

필자는 이렇게 해서 나온 제품이나 설계를 1판이라고 부른다.

사용해 볼 만한 설계 방법론은 다양한다.

폭포수 모형이라고 부르는 초기 방법론은 다양한 단계 사이에 엄격한 경계선을 긋는다.

이 경우엔 설계 단계는 구현 단계 이전에 완료되어야 하며, 구현 단계는 테스트 단계 이전에 완료되어야 한다.

사실 이런 방법론은 현실적이지 않다.

현재는 프토로 타이핑, 익스트림 프로그래밍, 애자일, 스크럼 등과 같은 다른 설계 모델에서 진정한 반복과정을 촉진하는 데 힘쓰게 한다.

이 모델에서는 설계 단계를 완료하기도 전에 개념을 증명할 목적으로 일부를 구현해 보게 한다.

따라서 요즘에는 잘 사용하지 않는 폭포수 모델이지만 목표는 이해가 된다.

코딩을 시작하기전 완벽하고 철저하게 설계하는 것이 바람직하다고 알고 있다.

설계를 마친 뒤 제품의 출시 국면으로 직행하지 않고 설계 국면을 다시 반복하기로 결정할 수 있다.

이 처럼 설계 과정은 반복적이며, 반복 자체를 피할 수 없다..

그러나 이 반복을 최소한으로 유지해야 한다.

즉, 반복은 불가피하지만 최소한의 설계, 넓은 설계는 필요하다는 것

폭포수 모델의 요구사항을 조기에 식별하고 설계 변경을 최소로 유지하려고 하는 이유를 요약하면 다음과 같다.

  • 설계 단계에서 요구사항이나 설계를 변경하는 비용이 구현 단계나 배포 단계에서 그러는 경우보다 상대적으로 적다.
  • 구현 단계에서는 설계 변경 비용이 상당히 높다.
  • 배포 단계 이후에 설계를 변경하려고 할 때 드는 비용은 첫 번째 항목과 비교할 때 천문학적이다.

마찬가지로, 건축 설계까 완료되기 전에 꿈 같은 집을 짓기 시작하고 싶지 않을 것이다.

실제로 테스트를 통해 버그가 단 하나도 없는 상태를 추구한다고 보면, 소프트웨어를 철저히 테스트하는 일이 불가능할 수 있다.

그러나 이론상으로는 이것이 항상 목표다.

우리는 항상 가능한 많은 버그를 제거하려고 노력해야 한다.

교량과 소프트웨어를 직접 비교할 수는 없다.

그러나 소프트웨어는 교량 건설과 같은 더 난해한공학 분야와 동일한 수준의 공학적 우수성을 위해 노력해야 한다.

품질이 열악한 소프트웨어는 치명적일 수 있다.

안전 기기에 삽입된 소프트웨어가 그 예이다.

6.1.1. 적절한 분석 수행

설계를 구축하고 소프트웨어 제품을 생산하는 데에는 다양한 변수가 관련되어 있다.

사용자는 모든 단계에서 개발자와 협력한다.

분석 단계에서 사용자와 개발자는 작업 설명, 프로젝트 요구사항 및 실제 프로젝트 수행 여부를 결정하기 위해 적절한 연구 및 분석을 수행해야 한다.

마지막 요점은 조금 놀라운 것처럼 보일 수 있지만 중요하다.

분석 단계 동안이라도 정당한 사유가 있다면 프로젝트를 중단해야 한다.

사람들은 너무나 자주 프로젝트에 애착을 보인다든가, 사내 권력 관계 때문에 어절 수 없이 그때까지 해 오던 방식 그대로 진행하는 경우가 많다.

지금 말하고 있는 이론 소프트웨어의 원칙은 객체지향에만 국한되지 않고 일반적으로 소프트웨어 개발에 적용된다.

6.1.2. 작업명세서 작성

작업명세서는 시스템을 설명하는 문서이다.

요구사항을 결정하는 것이 분석 단계의 궁극적인 목표이지만, 이 시점에서 요구사항은 아직 최종 형식이 아니다.

시스템의 모양과 느낌을 명료하게 알 수 있어야 한다.

6.1.3. 요구사항 수집

소요제기서는 시스템이 어떤 일을 했으면 좋겠다고 사용자가 생각하는 것을 설명하는 문서이다.

소요제기서의 세부 사항 수준까지 고도의 기술을 발휘할 필요는 없지만, 요구사항은 최종 제품에 대한 사용자 요구의 진정한 본질을 나타내기에 충분할 만큼 구체적이어야 한다.

6.1.4. 시스템 프로토타입 제작

사용자와 개발자가 시스템을 쉽게 이해할 수 있게 하는 가장 좋은 방법 중 하나는 프로토 타입을 만드는 것이다.

무엇이든지 프로토타입으로 만들어 볼 수 있다.

그러나 대부분의 사람은 프로토타입을 시뮬레이션된 사용자 인터페이스로 여긴다.

실제 화면과 화면 흐름을 만들면 사람들이 작업할 내용과 시스템의 느낌에 대해 쉽게 알 수 있다.

어쨌든 프로토타입에는 최종 시스템의 모든 기능이 포함되어 있지는 않다.

6.1.5. 클래스 식별

요구사항이 문서화되어 소요제기가 작성되면 클래스를 식별하기 위한 과정에 착수할 수 있다.

소요제기서에 기록된 요구사항을 바탕으로 클래스를 식별하는 방법 중에 모든 명사를 강조 표시하는 것이 편리하다.

이러한 명사는 사람, 장소 및 사물과 같은 객체를 나태내는 경향이 있기 때문이다.

처음부터 모든 클래스를 다 찾아내려고 애쓰지 않도록 한다.

결국 클래스를 추가하거나 제거하면서 설계 전반의 다양한 클래스를 변경해야 하기 때문이다.

앞서 말한 설계 과정은 반복과정임을 잊지 말자.

6.1.6. 클래스 역할 결정

식별한 클래스의 역학은 직접 결정해야 한다.

여기에는 클래스가 저장해야 하는 데이터와 클래스가 수행해야 하는 연산들이 포함된다.

Employee객체는 급여를 계산하고 해당 계정으로 돈을 이체하는 일을 담당한다. 다양한 급여 요울이나 다양한 은행 계좌 번호를 저장할 수 있다.

6.1.7. 클래스 간 협력 방식 결정

대부분의 클래스는 분리되어 존재하지 않는다.

클래스는 특정한 책임을 담당해야 하지만, 원하는 것을 얻기 위해 그 밖의 클래스와 상호 작용을 해야 하는 경우가 많다.

그렇기 때문에 클래스 간에 메세지가 오가게 된다.

어떤 클래스에 그 밖의 클래스가 지닌 정보가 필요하거나 특정 클래스가 그 밖에 클래스에 어떤 일을 시키려고 한다면, 해당 클래스가 정보를 제공하거나 일을 대신해 줄 클래스로 메세지를 보내면 된다.

6.1.8. 시스템 설명 클래스 모델 작성

모든 클래스가 결정되고 클래스의 역할과 협동 방식을 나열한 후에는, 전체 시스템을 나타내는 클래스 모델을 합성해 볼 수 있다.

이러한 클래스 모델은 다양한 클래스가 시스템 내에서 상호 작용하는 방식을 보여준다.

책에선 UML을 사용

6.1.9. 사용자 인터페이스 프로토타입을 코드로 작성

설계 과정에서 사용자 인터페이스의 프로토타입을 만들어야 한다.

이 프로토타입은 설계 과정의 반복을 탐색하는 데 도움이 되는 유용한 정보를 제공한다.

시스템 사용자에게 사용자 인터페이스는 시스템

6.2. 객체 래퍼

필자가 여러번 강조하는 내용으로 객체지향 프로그래밍이 구조적 프로그래밍과 별개의 패터다임이라는 오해를 불식시키는 것이였고, 필자가 그런 오해를 좋아하지 않는다는 점을 나타내는 것이었다.

구조적이지 않게 프로그램을 작성할 수 있는 방법은 없다.

따라서 객체지향 프로그래밍 언어을 사용하고 건전한 객체지향 설계 기술을 사용하는 프로그램을 작성할 때 구조적 프로그래밍 기술도 함께 사용한다.

이 문제를 해결할 방법은 없다.

좀 더 예를 들어 설명한다면 속성과 메서드가 포함된 새 객체를 만들면 해당 메서드에 구조적 코드가 포함된다.

사실, 이러한 메서드에는 대부분 구조적 코드가 포함된다고 말할 수도 있다.

이 방법은 이전 장에서 살펴본 컨테이너라는 개념과 잘 맞는다.

실제로 메서드 수준에서 코딩하는 시점에 도달하면 코볼, C 등과 같은 구조적 언어로 프로그래밍하던 시절 이후로도 코딩 사고 과정은 크게 달라지지 않았다.

좀 더 넓게 본다면 내 생각은 언어와 이런 패러다임은 모두 나의 생각을 표현하기 위한 도구에 불과하다.

6.2.1. 구조적 코드

프로그래밍 로직의 기본 상황을 논할 수 있지만, 필자가 강조했듯이, 기본 객체지향 구조는 캡슐화, 상속, 다형성 및 합성으로 이뤄진다.

구조적 프로그래밍을 다루는 다른 책에선 컨스트럭트(구성소), 순서(sequence), 조건(conditions) 및 반복(iterations)이라는 4가지 기본 구조를 설명한다.

순서 부분은 주어진 것으로 상단에서 시작해 하단으로 진행하는 것이 논리적이기 때문이다.

구조적 프로그래밍의 핵심은 조건과 반복에 있다.

6.2.2. 구조적 코드 둘러싸기

속성을 정의하는 일도 코딩으로 간주되지만, 객체의 행위들은 메서드로 정의한다.

그리고 대부분의 코드 로직이 이러한 메서드에 들어 있게 된다.

이 부분은 당연하게 해오던 코딩 방식이지만 필자는 구조적과 객체지향적으로 바라본다.

class SomeMath{
  public int add(int a, int b)
  {
    return a + b;
  }
}

add라는 메서드가 있는데 여기서 사용되는 구조적 코드는 a + b는 add라는 메서드안에 둘러싸인다.

간단한 예이지만 구조적 코드를 둘러싸기만 하면 된다.

따라서 사용자가 이 방법을 사용하려고 할 때 필요한 것은 다음에 보이는 메서드의 시그니처다.

public class TestMath{
  public static void main(String[] args){
    int x = 0;

    SomeMath math = new SomeMath();
    x = math.add(4, 5);
    System.out.println(x);
  }
}

정말 기본적인 형태의 래퍼이다.

6.2.3. 이식하기 어려운 코드를 둘러싸기

객체 래퍼의 또 다른 용도는 이식하기 어려운 코드(네이티브 코드)를 감추기 위함이다.

개념은 본질적으로 동일하다.

그러나 이런 경우의 요점은 오직 하나의 플랫폼에서만 실행할 수 있는 코드를 가져다 사용하는 프로그래머들에게 간단한 인터페이스를 제공하는 메서드로 캡슐화하는 것이다.

ex) 윈도우 플랫폼의 경고음 코드를 메서드로 캡슐화화여 제공

6.2.4. 기존 클래스를 둘러싸기

기존의 구조적 코드나 이식 불능 코드를 새로운 클래스로 둘러싸야 할 필요는 있지만, 기존 클래스를 둘러싸야 할 필요성은 그리 명백하지 않을 수 있다.

그러나 기존 클래스에 대한 래퍼를 작성해야 하는 이유도 많다.

소프트웨어 개발자는 종종 다른 사람이 작성한 코드를 사용한다.

공급업체에서 구매했거나 조직 내에서 내부적으로 작성한 코드는 변경하기 힘든 경우가 많다.

이런 경우 래퍼를 사용하면 기존 클래스를 변경하지 않고도 기존 클래스의 기능을 확장할 수 있다.

소프트웨어 개발 시에 래퍼를 사용하는 일은 개발자의 관점뿐만 아니라 공급업체의 관점에서도 상당히 광범위하게 행해진다.

래퍼는 소프트웨어 시스템을 개발할 때 중요한 도구이다.

6.3. 결론

다시한번 강조하지만 객체지향 코드와 구조적 코드는 서로 배타적인 게 아니다.

실제로 우리는 구조적 코드를 사용하지 않으면 객체를 만들 수 없다.

따라서 우리가 객체지향 시스템을 구축하는 과정이라면 이미 설계 시에 구조적 기술을 사용하는 셈이나 마찬가지다.

객체 래퍼는 전통적인 코드와 객체지향적인 코드에서 이식 불능 코드에 이르기까지 다양한 기술을 캡슐화하는 데 사용된다.

객체 래퍼의 주요 목적은 코드를 사용하는 프로그래머에게 일관된 인터페이스를 제공하는 것이다.

느낀점

이 책은 확실히 객체지향만을 다루지 않는다.

엔지니어로서 소프트웨어를 접하는 마음가짐이나 생각해야하는 사고방식은 물론 객체지향에 대한 오해를 바로잡고자 한다.

래퍼에 대한 생각이 그냥 단순하게 감싸는 용도로 생각했는데 인터페이스 단위나 함수처럼 낮은 레벨의 래퍼까지 생각못해봤다.

당연하게 하고 있는 행동들에 대한 이론을 이제 배운 기분..

7. 상속과 합성에 익숙해지기

상속과 합성은 객체지향 시스템설계에서 중요한 역할을 한다.

실제로도 어렵고 흥미로운 많은 부분 (논란도 많은)이 상속과 합성 사이에서 결정된다.

객체지향 설계 방식이 발전함에 따라 일부 개발자는 상속을 아예 배제하기도 한다.

행위를 직접 상속하기보다 인터페이스를 상속하는 편이 더 일반적이다.

데이터/모델을 주로 상속받고, 행위는 주로 구현하는 경향이 있다.

그럼에도 상속과 합성은 여전히 재사용 메커니즘이다.

상속이라는 이름에서 알 수 있듯이 실제 부모/자식 관계가 있는 다른 클래스의 특성과 행위를 상속한다.

합성은 객체들을 사용해 또 다른 객체를 작성하는 일까지 포함하는 개념이다.

7.1. 객체 재사용

상속과 합성이 존재하는 주된 이유는 객체를 재사용하기 위해서이다.

상속과 합성을 통해 기존 클래스를 활용해 새로운 클래스를 만들 수 있는데 이는 사실 미리 만들어 둔 클래스를 재사용하기 위한 유일한 방법이다.

  • 상속: is-a의 관계로 개는 포유동물이다.
  • 합성: has-a의 관계로 자동차에는 엔진이 있다.

합성은 다른 클래스를 사용해 더 복잡한 클래스를, 즉 일종의 어셈블리를 구축하는 작업이 포함된다.

이 경우 자식, 부모 관계가 없으며 기본적으로 복합 객체를 구성하는 방법이다.

처음 객체지향이 등장했을 때 상속이 가장 객체지향의 큰 장점으로 여겨졌다.

상속이 재사용의 본질이고, 상속은 재사용의 궁극적인 표현이였다.

하지만 시간이 지남에 따라 합성을 통해 설계해야 한다고 주장하는 사람들이 많아졌다.

그렇다고 상속을 배제하라는 것은 아니다.

상속은 효과적인 기술이며, 객체지향 설계에서 중요한 역할을 한다.

레거시 코드 유지보수나 개발자 툴킷에서 상속을 사용하는 것은 매우 효과적이다.

상속이 종종 오용되거나 남용되는 이유는 상속이 설계 전략으로 사용되는 근본적인 결함 때문이 아니고 상속이 무엇인지 제대로 이해하지 못했기 때문이다.

결론은 둘 다 잘 사용할 줄 알아야 한다는 점이고 객체지향 시스템 구축에 강력한 기술이라는 것이다.

따라서 우리는 두 가지 장단점을 모두 이해하고 적절한 맥락에서 이들을 사용하기 위해 시간을 들여야 한다.

7.2. 상속

상속을 사용하여 부모의 기능이 보장된다고 해서 테스트 코드를 작성하지 않아도 되는 것은 아니다.

가장 많이 등장하는 문제점인 펭귄의 예로 설명한다.

행위를 지역적으로 오버라이딩할 수 있지만, 펭귄은 여전히 fly라는 메서드를 가지고 있을 것이다.

이런 의미없는 메서드를 사용하는 것은 SOLID원칙 중 LSP를 위반하는 것이다.

따라서 날지 않는 새와 같이 처음 계층 구조에서 한번 더 상속 구조로 내려가는 게 바람직할 수 있다.

하지만 실제로는 위와 같은 과정은 매우 복잡해진다..

7.2.1. 일반화와 특수화

generalization-specialization

Dog라는 단일 클래스에서 다양한 개 품종을 고려하여 구현된다면 이는 일반화-특수화라고 불린다.

상속을 이용할 때 고려해야할 중요 사항이다.

이는 상속 트리를 따라 내려갈수록 사물들이 더 특수화된다는 생각이다.

트리의 맨 위가 가장 일반적인 개념이고, 아래로 내려갈수록 특수화된다.

상속이라는 개념은 상속이란 공통적인 성질을 배제해 나가면서 일반화에서 특수화로 나아가는 일이다.

7.2.2. 설계 결정

이론적으로 보면 가능한 한 많은 공통성을 고려하는 편이 바람직하다.

그러나 모든 설계 문제와 마찬가지로 때로는 이게 지나치게 좋아서 오히려 부담이 될 수도 있다.

공통성을 최대한 고려하면 현실 세계에 최대한 가깝게 표현할 수 있지만, 모델에 대해서는 근접하게 표현하지 못할 수도 있다.

컴퓨터가 잘하지 못하는 것
분명히 컴퓨터 모델이 현실 세계의 상황을 근사할 수 는 있다.
컴퓨터는 숫자 처리에 능숙하지만, 더 추상적인 작업에는 적합하지 않다.

현실은 불확실한 경우가 더 많음

더 큰 시스템에선 가능한 한 사물들을 단순하게 유지하는 것이 좋다.

덜 복잡하게 설계할지, 아니면 더 많은 기능을 갖추도록 설계할지는 일종의 균형잡기와 같다.

시스템이 너무 복잡해서 그 무게만으로 붕괴될 정도의 복잡성을 추가하지 않으면서 유연한 시스템을 구축하는 게 일차적인 목표이다.

설계 시에는 항상 상충 관계에 부딪히게 되는데 미래를 너무 염두하여 설계하거나 바로바로 현재에서 설계하는 둥의 균형잡기가 매우 어렵다..

7.2.3. 합성

객체가 그 밖의 객체들을 포함할 수 있다는 생각은 자연스럽다.

컴퓨터의 그래픽카드, 하드디스크 등등의 구성 또는 합성은 쉽게 이해가 가능하다.

이는 해당 객체들이 각각 독립적으로 동작한다고 이해하기 때문이다.

특정 객체가 다른 객체로 합성되고 해당 객체가 객체 필드로 포함될 때마다 새 객체를 복합체나 응집체또는 컴포지션인 객체라고 한다.

7.2.4. UML로 합성 표현하기

UML로 표현하는 방법을 알려준다.

표준이 아니고 최근에는 잘 사용하지 않기 때문에 따로 정리하지 않는다.

7.3. 캡슐화가 객체지향의 기본이 되는 이유

캡슐화는 객체지향의 기본 개념이다.

인터페이스/구현부라고 하는 패러다임을 다룰 때면 우리는 사실 캡슐화에 대해서 이야기 하는 것이다.

기본적인 질문은 클래스에서 무엇을 노출해야 하고 무엇을 노출해서는 안 되는가에 관한 것이다.

이 캡슐화는 데이터 및 행위와 동등한 관계에 놓여 있다.

어떤 클래스의 설계에 관한 결정을 논의할 때에는 데이터와 행위를 잘 작성된 클래스로 캡슐화하는 일에 초점을 맞춰야 한다.

캡슐화는 객체지향의 기본 요소이므로 객체지향 설계의 기본 규칙 중 하나인 것이다.

상속은 또한 세 가지 주요 객체지향 개념 중 하나로 간주된다.

그러나 어떤 면에서 보면 실제로는 상속이 캡슐화를 방해한다.

어떻게 이럴 수 있을까?

7.3.1. 상속이 캡슐화를 약화시키는 방법

캡슐화는 공개 인터페이스 부분과 비공개 임플리멘테이션 부분으로 구분해서 따로따로 모아 두는 과정이다.

본질적으로 클래스는 자신이 아닌 그 밖의 클래스가 알 필요가 없는 모든 것을 숨긴다.

사례중 한 가지는 상속으로 인해 그 밖의 클래스에 대해서는 강력하게 캡슐화되는 꼴이 되지만, 정작 슈퍼클래스와 서브클래스 사이의 캡슐화는 약해지는 경우를 말한다.

문제는 슈퍼클래스에서 구현부를 상속한 후에 해당 구현부를 변경해 버리면 이러한 슈퍼클래스 내의 변경 내용이 클래스 위계구조를 통해 파급된다는 점이다.

이렇게 줄줄이 전파되는 효과는 잠재적으로 모든 서브클래스에 영향을 미친다.

이러한 파급효과는 예상치 못한 문제를 일으킬 수 있다.

즉, 파급 클래스에 변경된 내용이 투명하게 드러나지 않기 때문에 문제가 발생할 수 있다.

7.3.2. 다형성의 자세한 예

많은 사람들이 다형성(Polymorphism)을 객체지향 설계의 초석이라고 생각한다.

완전히 독립적인 객체를 만들 목적으로 클래스를 설계하는 것이 객체지향의 핵심이다.

잘 설계된 시스템에서 객체는 그것에 관한 모든 중요한 질문에 답할 수 있어야 한다.

일반적으로 객체는 스스로 어떤 역할을 해야한다.

이 독립성은 코드 재사용의 기본 메커니즘 중 하나이다.

다형성은 많은 모양을 의미하고 메세지 객체로 전송될 때 객체는 해당 메세지에 응답하도록 정의된 메서드를 가져야 한다.

상속 위계구조에서 모든 자식 클래스는 해당 슈퍼클래스에서 인터페이스를 상속한다.

그러나 각 자식 클래스는 별도의 엔터티이므로 동일한 메세지의 대한 별도의 응답이 필요할 수 있다.

7.3.3. 객체 책임성

추상클래스는 인스턴스화할 수 없지만, 파생클래스는 인스턴스화할 수 있다.

따라서 각 추상 클래스의 메서드를 각 파생클래스의 성격에 맞게 구현해야 한다.

그렇기에 다형성이 필요하고 전제는 다양한 객체에 메세지를 보낼 수 있다.

7.4. 결론

존경받는 객체지향 설계자들은 될 수 있으면 기본적으로 합성을 사용하면서 필요할 때만 상속을 사용해야 한다고 했다.

그러나 합성을 반드시 상속보다 많이 사용해야 하는 건 아니다.

뭐가 맞다 틀리다는 없으며, 상황에 맞춰 적절하게 섞어 쓰는 것이 올바르다.

느낀점

상속과 합성에 관한 내용으로 좀 더 깊게 생각하게 되는 챕터였다.

일반적으로 생각하는 상속에 문제점에 대해서도 조심스러운 입장과 왜 그런 문제점이 생기는 지 예제로 설명되어 있어서 도움이 되기도 했고 무작정 합성을 주로 사용하려고 하는데 그것도 문제가 될 수 있다는 것을 알게 되었다.

8. 프레임워크 및 재사용: 인터페이스와 추상 클래스를 사용해 설계하기

인터페이스와 프로토콜 및 추상클래스라는 개념들을 확장한다.

인터페이스와 프로토콜 및 추상 클래스는 코드 재사용을 위한 강력한 메커니즘으로 계약이라고 하는 개념의 기초를 제공한다.

8.1. 코드: 재사용할 것인가, 사용하지 않을 것인가?

코드를 처음 만들기 시작한 시점부터 지금까지 모든 프로그래머들은 재사용을 위해 코드를 작성하고 있다.

객체지향도 같은 맥락으로 코드를 재사용한다.(객체라는 개념으로)

하지만 앞서 말했듯이 객체지향 패러다임을 따르는 것만이 재사용 가능한 코드를 개발하는 유일한 방법이 아니다.

객체지향이라는 몇 가지 유용한 메커지즘은 있다.

프레임워크를 작성하는 것..!

8.2. 프레임워크란?

코드 재사용이라는 개념과 손을 잡을 수 있는 개념으로 표준화라는 개념이 있는데, 이 개념을 플러그 앤 플레이라고도 부른다.

프레임워크(framework)라는 개념은 이러한 플러그 앤 플레이 및 재사용 원칙을 중심으로 한다.

일반적으로 사용하는 오피스군의 애플리케이션또한 같은 디자인을 재사용하거나 같은 기능을 통일시켜둔다.

문서 처리 프레임워크에는 일반적으로 문서 작성, 문서 저장, 텍스트 자르기, 텍스트 복사, 텍스트 붙여넣기, 문서 검색 등의 작업이 포함된다.

이 프레임워크를 사용하려면 개발자가 애플리케이션을 만들 때 사전에 저장된 인터페이스를 사용해야 한다.

이처럼 미리 지정된 인터페이스는 표준 프레임워크를 준수하는데, 이는 두 가지 명백한 장점이 있다.

먼저, 이미 말한 것과 같이 모양과 느낌이 일관되므로 최종 사용자는 프레임워크를 배울 필요가 없다.

둘째, 개발자는 미리 작성하고 테스트까지 한 코드를 활용할 수 있다.(테스트문제는 매우 큰 이점이다.)

새로운 열기 대화 상자가 이미 존재하고 철저하게 테스트된 경우에 굳이 새 열기 대화 상자를 만드는 코드를 작성할 필요가 있을까?

8.3. 계약이란?

계약을 개발자가 API사양을 준수해야 하는 메커니즘으로 생각할 수 있다.

종종 API를 프레임워크라고도 부른다.

계약이란, 두 명 이상의 개인 또는 당사자 간의 약속, 특히 법적 구속력이 있는 약속을 말한다.

계약을 사용할 때면 개발자는 프레임워크에 정의된 규칙을 준수해야 한다.

여기에는 메서드 이름, 매개변수의 수 등과 같은 문제가 포함된다.

개발자가 계약을 위반할 수 있기 때문에 이행 강제가 필수적이다.

이행을 강제하지 않으면 불량 개발자는 프레임워크에서 제공하는 사양을 사용하지 않고 바퀴를 다시 발명하는 일에 비유해 볼 수 있는 일, 즉 스스로 코드를 처음부터 작성해버릴 수 있다..

계약을 구현하는 방법은 추상 클래스와 인터페이스를 사용하는 것이다.

8.3.1. 추상 클래스

추상 클래스는 구현부가 없는 메서드가 한 개 이상 들어 있는 클래스다.

Shape라는 추상클래스가 있다고 가정한다면 이 클래스는 인스턴스화할 수 없으므로 추상적인 것이다.

실생활에서는 도형을 그려달라고 요청할 때 먼저 물어볼 것인 “어떤 종류의 도형인가요?”라는 개념도 추상적인 것이다.

이것을 계약에 어떻게 적용할까?

도형을 그리는 애플리케이션을 만든다고 했을 때 해당 애플리케이션의 목표는 현재 설계에 표현된 모든 종류의 도형과 나중에 추가될 수 있는 도형을 그리는 것이다.

먼저 모든 도형이 동일한 구문으로 자신을 스스로 그릴 수 있게하려고 한다.

draw()라는 메서드가 모든 도형에 포함된다면 draw() 메서드를 호출하여 도형을 그릴 수 있다.

다음으로 모든 클래스는 각자 행위에 대해서 책임을 져야 한다는 점을 기억하자.

따라서 어떤 클래스가 draw()라고 불리는 메서드를 제공해야 한다고 하면 해당 클래스는 자체적으로 코드를 구현해야 한다.

따라서 추상 클래스인 Shape가 존재한다면 Circle는 Shape의 확장(즉, 상속)이고 Circle은 draw() 메서드를 구현해야 한다.

이는 계약이라고 볼 수 있다.

Circle이 draw()를 구현하지 않는다면 컴파일되지 않는다. 따라서 Circle은 Shape와 계약을 지키지 못하는 셈이 된다.

Circle이 실제로 draw() 메서드를 구현하지 않으면 Circle 자체도 추상체인 것으로 간주한다.

따라서 또 다른 서브 클래스가 상속을 받아 draw() 메서드를 구현해야 한다.

8.3.2. 인터페이스

인터페이스를 정의하기 전에, C++에는 인터페이스라고 하는 컨스트럭트(구성소)가 없다는 점을 알고 간다.

인터페이스라는 용어

소프트웨어 용어에서 많이 사용되는 인터페이스라는 용어는 혼동될 수 있다.

1. 그래픽 사용자 인터페이스는 GUI라고 한다.
2. 클래스와 관련해서 인터페이스라는 말은 기본적으로 메서드의 시그니처를 가리킨다.
3. Object-C 및 스위프트에서는 코드를 물리적으로 분리된 모듈로 나누는데, 이 모듈들을 각기 인터페이스와 구현부라고 부른다.
4. 인터페이스와 프로토콜은 기본적으로 부모 클래스와 자식 클래스 간의 계약이다.

인터페이스와 동일한 기능을 추상 클래스로 제공할 수 있다면, 왜 굳이 자바와 닷넷에서는 인터페이스라고 부르는 컨스트럭트를 제공하려 하는가?

Object-C와 스위프트에서는 왜 굳이 프로토콜을 제공하는가?

우선 C++은 다중 상속을 지원하지만 자바와 닷넷은 지원하지 않는다.

자바와 닷넷 Object-C 및 스위프트의 클래스들은 하나의 부모 클래스에서만 상속할 수 있지만, 많은 인터페이스를 구현할 수 있다.

따로는 상속을 구현 상속이라고 하며, 인터페이스는 정의 상속이라고 부른다.

8.3.3. 종합

추상 클래스와 인터페이스가 모두 추상 메서드를 제공하는 경우에 두 클래스의 실질적인 차이점은 무엇일까?

추상 클래스는 추상 메서드와 구성 메서드를 모두 포함할 수 있지만, 인터페이스는 추상 메서드만 포함할 수 있다.

나중에 더 많은 포유류(mammal)를 추가할 목적으로 개를 대표하는 클래스를 설계한다고 가정한다면 논리적으로 mammal 클래스는 추상 클래스가 되어야 한다.

public abstract class Mammal {
    public void generateHeat() {System.out.println("Generating heat");}
    public abstract void makeNoise();
}

이 클래스는 추상 메서드인 makeNoise()와 구성 메서드인 generateHeat()를 모두 포함한다.

모든 포유류는 공통적으로 온기를 내뿜기 때문에 generateHeat() 메서드는 구상적이어야 한다.

그러나 포유류는 다양한 소리를 낼 수 있으므로 makeNoise() 메서드는 추상적이어야 한다.

좀 더 합성, 상속, 인터페이스관계를 알아본다.

public class Head{
    String size;

    public String getSize() {
        return size;
    }

    public void setSize(String size) {
        this.size = size;
    }
}
public interface Nameable {
    public String getName();
    public void setName(String name);
}
public class Dog extends Mammal implements Nameable{
    private String name;
    private Head head;

    public void makeNoise() {
        System.out.println("Bark");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • Dog은 Mammal의 일종이므로 (Dog is-a Mammal)이 관계는 상속이다.
  • Dog은 Nameable을 구현하므로 (Dog implements Nameable)이 관계는 인터페이스이다.
  • Dog은 Head을 가지고 있으므로 (Dog has-a Head)이 관계는 합성이다.

여기서 Nameable관계도 인터페이스이긴 하지만 여전히 상속되지 않는가?

여기서 인터페이스가 왜 특별한지를 알아야 한다.

이 특별한 차이점을 이해해야 객체지향 설계를 이해할 수 있다.

상속은 is-a 관계가 있을 때 사용하지만, 인터페이스는 그다지 엄격하지 않은 관계에도 사용할 수 있다.

  • 개는 포유동물의 일종이다. (is-a)
  • 파충류는 포유류의 일종이 아니다. (is not a)

따라서 파충류 클래스는 Mammal 클래스에서 상속할 수 없다.

그러나 인터페이스는 다양한 클래스를 초월한다.

  • 개에 이름을 지어줄 수 있다. (nameable)
  • 도마뱀에 이름을 지어줄 수 있다. (nameable)

이렇게 전혀 관련 없는 클래스끼리도 인터페이스를 사용하여 관계를 맺을 수 있다.

이것이 추상 클래스와 인터페이스의 근본적인 차이점이다.

추상 클래스는 구현체의 일종인 셈이다. 실제로 Mammal은 generateHeat()라는 구상 메서드를 제공했다.

우리는 어떤 포유류가 있을 지 알지 못하지만, 모든 포유류가 온기를 낸다는 점을 알고 있다.

그러나 인터페이스는 행위만 모델링한다.

인터페이스는 어떠한 구현부도 제공하지 않으며, 행위만 제공한다.

인터페이스로는 연결 관계가 없을 수도 있는 클래스들 간에도 동일한 행위를 지정할 수 있다.

이름을 지어 주는 행위를 개에게 적용할 수 있을 뿐만 아니라 자동차와 행성 등에도 적용할 수 있다.

8.3.4. 컴파일러를 사용해 입증해 보기

인터페이스가 진정한 is-a 관계임을 증명하거나 반증할 수 있을까?

자바나 C#의 경우는 컴파일러가 이 점을 알려준다.

Dog D = new Dog();
Head H = D;

이 코드는 오류가 발생한다.

Test.java:6: incompatible types
Head H = D;

확실히 Dog가 Head인 것은 아니다.

우리는 이것을 알고 있을 뿐만 아니라 컴파일러도 이것을 알고 있다.

Dog D = new Dog();
Mammal M = D;

이것은 진정한 상속 관계이며, Dog은 Mammal이기 때문에 컴파일러가 이를 허용한다.

Dog D = new Dog();
Nameable N = D;

이 코드또한 잘 작동한다.

따라서 우리는 Dog를 이름을 지닌 존재라고 말해도 된다.

이것은 상속 관계와 인터페이스 관계라는 게 is-a관계를 만들어 낸다는, 간단하지만 효과적인 증거다.

인터페이스 관계가 적절하게 사용된다면 ‘~과 같은 행위’라는 식으로 해석된다.

물론 ‘is-a’인 데이터 인터페이스들이 있을 수 있지만, 더 자주 전자 형식을 사용하게 될 것이다.

8.3.5. 계약하기

구현부가 없는 메서드를 추상 클래스나 인터페이스 안에 두는 식을 간단하게 계약을 정의할 수 있다.

계약을 지킬 서브클래스로 설계할 때 부모 클래스나 인터페이스가 구현하지 않은 메서드에 대한 구현부를 서브 클래스가 제공해야만 한다.

앞에서 언듭했듯이 계약의 장점 중 하나는 코딩 규칙을 표준화하는 것이다.

코딩 표준을 사용하지 않을 때 발생하는 일을 예로 들어보자.

public class Planet {
    String planetName;

    public void getPlandetName() {
        return planetName;
    }
}
public class Car {
    String carName;

    public void getCarName() {
        return carName;
    }
}
public class Dog {
    String dogName;

    public void getDogName() {
        return dogName;
    }
}

이러한 클래스를 사용하는 사람이 누구든지 각 인스턴스의 이름을 검색하는 방법을 파악하려면 관련 문서를 보아야 한다는 문제점이 있다.

이래서 Nameable 인터페이스가 필요하다.

이름을 사용하는 모든 유형의 클래스와 계약하자는 말이다.

이렇게 하면 여러 클래스를 사용하는 사용자가 한 클래스를 사용하다가 다른 클래스를 사용해야 할 때 객체의 이름을 지정하기 위해 현재 구문을 파악하지 않아도 된다.

public interface Nameable {
    public void getName();
}

public class Planet implements Nameable {
    String planetName;

    public void getName() {
        return planetName;
    }
}

public class Car implements Nameable {
    String carName;

    public void getName() {
        return carName;
    }
}

public class Dog implements Nameable {
    String dogName;

    public void getName() {
        return dogName;
    }
}

여기서 드는 생각은 계약이라는 측면에서 인터페이스는 너무 좋지만, 이것을 이행하지 않는 프로그래머가 있다면 어떻게 될까?

사실 결론은 표준 계약을 위반하는 것은 막을 수 없다는 것이다.

그러나 어떤 경우에는 계약을 어기는 일 때문에 심각한 어려움을 겪게 된다.

팀, 프로젝트, 회사내에서 판단하는 일이기에

8.3.6. 시스템 접속점

기본적으로 계약은 코드가 내부로 들어가기 위한 접속점이다.

시스템중에 추상화할 곳이 있다면 어디에서나 계약을 사용할 수 있다.

추상화할 곳을 특정 클래스의 객체들에 연결하는 대신에, 여러분은 계약을 구현하는 객체라면 무엇이든지 연결할 수 있다.

어디에 계약이 유용할지 알고 있어야 하는 건 맞지만, 계약을 남용할 가능성도 있다.

9. 객체 구축과 객체지향 설계

전 장에선 상속에 대한 깊이 있는 주제를 다루었다면 이번 장에선 합성에 대한 개별 객체간의 상호작용을 알아본다.

9.1. 합성 관계

앞 장들에서 설명했듯이 합성은 전체 중의 한 부분을 나타낸다는 것을 설명했다.

상속은 is-a관계이지만, 합성은 has-a관계이다.

자동차에 핸들이 포함된다.

합성을 사용하는 이유는 더 단순한 부품으로 묶어서 시스템으 구축할 수 있기 때문이다.

연구에 따르면 기억력 좋은 사람도 단기적으로 한 번에 일곱 개의 일시적인 내용만을 기억할 수 있다.
따라서 우리는 개념을 추상적으로 다루기를 좋아한다.
즉. 운전대와 네 개의 바퀴, 엔진등을 가진 대형 장치보다 자동차라고 말하는 것과 같다.

합성은 또한 부품을 바꿀 때에도 도움이 된다.

모든 운전대가 똑같다면 특정 자동차에 어떤 운전대가 설치되어 있는지는 중요하지 않다.

소프트웨어 개발에서 교체 가능한 부품이 있다는 건 곧 재사용을 의미한다.

9.2. 단계적으로 구축하기

합성을 사용할 때의 또 다른 주요 이점은 시스템과 자식 시스템을 독립적으로 구축할 수 있으며, 더 중요하게 독립적으로 테스트하고 유지보수를 할 수 있다는 것이다.

사실 소프트웨어 시스템이 상당히 복잡하다는 데는 의문의 여지가 없다.

양질의 소프트웨어를 구축하려면 우선 적용 규칙을 따라야 한다.

가능한 한 간단하게 유지한다는 규칙이다.

큰 소프트웨어 시스템이 제대로 작동하게 하고 유지보수를 쉽게 할 수 있으려면 더 작고 관리하기 쉬운 부분으로 나누어야 한다.

이렇게 하면 어떤 장점이 있는지 알아보자.

  • “복잡하지만 안정된 시스템은 보통 위계구조라는 형태를 취하는데, 각 시스템은 더 단순한 하부 시스템을 사용해 구축되며, 각 하부 시스템은 여전히 그보다 더 단순한 하부 시스템으로 구축된다.”
  • “안정적이면서도 복잡한 시스템은 거의 다 분해해 볼 수 있다.”
  • “복잡하지만 안정된 시스템은 거의 항상 서로 다른 조합으로 배열된 몇 가지 종류의 자식시스템으로만 합성된다.”
  • “작동 중인 안정 시스템은 거의 항상 작동해 왔던 단순한 시스템에서 발전했다.”

만약 합성의 개념을 사용하지 않는다면 자바 및 닷넷 프레임워크는 굉장히 불리해진다.

객체가 동적으로 적재되기 때문에 설계를 분리해야만 한다.

만약 애플리케이션으 배포한 후에 클래스 파일 중 하나를 다시 작성해야 한다면 해당 특정 클래스만 수정할 수 있지만 단일 파일이라면 애플리케이션 자체를 재배포해야 한다.

컴포넌트 패턴..!

주요 이점 중 하나는 조직 내의 다른 개발자 또는 타사 공급업체가 작성한 컴포넌트를 사용할 수 있다는 것이다.

9.3. 합성 유형

일반적으로 결합(association)과 응집(aggregation)이라는 두 가지 유형의 합성 방식이 있다.

두 방식은 모두 객체 간의 협업 관계를 나타낸다.

합성의 주요 장점 중 결합은 자동차와 핸들의 관계를 나타낸다.

합성은 결합의 한 가지 형태일까?
합성은 객체지향 기술의 또 다른 영역 영역으로 닭이나 달걀이냐 대한 문제가 먼저 발생한다.

모든 형태의 합성에는 has-a 관계가 포함된다.

그러나 전체 부분을 시각화하는 방법에 따라 결합과 응집 간에 미묘한 차이가 있다.

응집에서는 일반적으로 전체만 표시하고 결합에서는 일반적으로 전체를 이루는 부분을 표시한다.

9.3.1. 응집

아마도 가장 직관적인 형태는 응집일 것이다.

응집은 복합 객체가 그 밖의 객체들로 구성되어 있음을 의미한다.

TV를 생각할 때 우리가 보기에 TV는 단일 장치로 되어있다.

대체로 사람들은 TV에 마이크로칩이나 스크린이나 튜너 등이 포함되어 있다는 사실까지 생각하지 않는다.

물론 전원버튼이나 화면 표시장치등은 볼 수 있다.

단순 차를 구매하는 것과 같다.

9.3.2. 결합

응집은 일반적으로 전체만 보는 관계를 나타내는 반면, 결합은 전체와 부분을 모두 나타낸다.

전통적인 데스크탑 컴퓨터 시스템을 본다면 컴퓨터 시스템은 전체를 나타낸다.

모니터, 키보드, 마우스 및 기본 상자는 컴포넌트에 해당한다.

각 컴포넌트가 별도의 객체이지만, 이것들이 모두 모여 컴퓨터 시스템 전체를 나타낸다.

컴퓨터 본체는 마우스, 키보드 및 모니터가 일부 작업을 대신하게 된다.

결합은 서비스들이 별개로 이뤄지고 있음을 나타낸다.

결합 대 응집
응집체는 다른 객체들을 가지고 합성할 복합 객체다.
결합체는 한 객체가 그 밖의 객체로부터 서비스를 받기를 원할 때 사용된다.

9.3.3. 결합체와 응집체를 사용하기

예제를 봐도 사실 무엇이 결합체이고 무엇이 응집체인지 명확하게 구분짓기 힘들다.

흥미로운 설계 결정 중 많은 부분이 결합체를 사용할지 아니면 응집체를 사용할지를 결정하는 일과 관련되어 있다고 할 수 있다.

예를 들어, 결합체를 설명하는 데스크탑 컴퓨터는 일부 응집이라는 개념이 들어 있다.

컴퓨터 본체, 마우스, 키보드 등과 상호작용은 결합으로 나타내지만, 컴퓨터 본체 자체는 응집을 나타낸다.

따라서 정답은 없다.

9.4. 의존체 회피하기

합성을 사용할 때 객체를 서로 의존하지 않게 하는 편이 바람직하다.

서로 의존하는 객체를 만드는 한 가지 방법은 도메인들을 섞는 것이다.

세상에서 가장 좋은 점은 특성 상황을 제외하고 한 도메인의 객체를 다른 도메인의 객체와 섞어서는 안된다는 점이다.

스테래오 예제를 들어서 설명한다면 모든 컴포넌트를 별도의 도메인에 유지함으로써 음향기기를 더 쉽게 유지할 수 있다.

예를 들어, CD 컴포넌트가 고장 나면 CD 플레이어를 보내 개별적으로 수리할 수 있다.

CD플레이어와 MP3플레이어의 도메인은 서로 다르다.

이 경우 CD 플레이어와 MP3 플레이어를 구매하는 식으로 유연하게 대처할 수 있다.

때로는 도메인을 섞으면 편리할 때도 있다.

몇 년 동안 사용해 온 좋은 구식 TV/VCR 콤비 시스템이 있다.

동일한 모듈에 둘 다 있는 것이 편리하다.

하지만 TV가 고장나면 둘다 사용할 수 없게 된다.

이처럼 편리성을 따를 것인지 아니면 안정성을 따를 것인지와 같은, 특정 상황에서 더 중요한 사항을 결정해야 한다.

정답은 없다. 트레이드 오프만 있을 뿐

인터페이스들이 이 문제를 해결하는 데 쓰이며, 의존체들을 관리하는 게 인터페이스가 주로 하는 일이다.

인터페이스가 공유 라이브러리에 정의되어 있고 구현부가 더 구상적인 클래스에 정의되어 있으면 행위 계약을 사용해 도메인을 섞어 볼 여지가 생긴다.

9.5. 카디널리티

카디널리티는 결합에 참여하는 객체 수와 참여가 선택적인지 아니면 필수적인지 여부를 설명한다고 했다.

카디널리티를 결정하기 위한 질문은 다음과 같다.

  • 어떤 객체가 그 밖의 객체들과 협업하는가?
  • 각 협업에 몇 개의 객체가 참여하는가?
  • 협업은 선택 사항인가, 아니면 필수인가?

만약 Person에서 상속받고 다음 예에 나오는 클래스들과 관계가 있는 Employee 클래스를 만든다.

  • Division
  • JobDescription
  • Spouse
  • Child

이 클래스들이 무엇을 하는가? 이 클래스들을 선택해서 쓸 수 있는가? Employee는 몇 개나 필요한가?

  • Division
    • 이 객체에는 직원이 근무하는 부서와 관련된 정보가 포함된다.
    • 각 직원은 부서를 위해 일해야 하므로 관계는 필수다.
    • 직원은 한 개 부서에서만 근무한다.
  • JobDescription
    • 이 객체에는 직무에 대한 설명이 포함되어 있으며, 급여 등급 및 급여 범위와 같은 정보가 포함되어 있다.
    • 각 직원은 직무 기술서를 가지고 있어야 하므로 관계는 필수다.
    • 직원은 회사에서 재직하는 동안에 다양한 일을 맡을 수 있다. 따라서 한 직원에게 많은 직무 기술서가 있을 수 있다.
  • Spouse
    • 이 간단한 예에서 spouse 클래스에는 기념일만 포함된다.
    • 직원은 결혼했거나 결혼하지 않았을 수 있다. 따라서 배우자는 선택사항이다.
    • 직원은 배우자를 한 명만 가질 수 있다.
  • Child
    • 이 간단한 예제에서 Child 클래스에는 FavoriteToy 문자열만 포함된다.
    • 직원에게는 자녀가 있거나 없을 수 있다.
    • 직원에게는 자녀가 없거나 무한한 자녀가 있을 수 있다.
선택적/결합 카디널리티 필수 여부
Employee/Division 1 필수
Employee/JobDescription 1..n 필수
Employee/Spouse 0..1 선택
Employee/Child 0..n 선택

9.5.1. 선택적 결합체

결합체들을 다룰 때 가장 중요한 문제 중의 하나는 애플리케이션이 선택적 결합체를 확인하도록 설계되었는지 확인하는 것이다.

즉, 코드는 결합체가 null인지를 확인해야 한다.

public String getSpouseName() {
    return Sqousel //매우 위험
}

결론은 null을 확인하고 이를 유효한 조건으로 처리해야 한다는 것이다.

9.6. 결론

이번 장에선 합성에 대한 좀 더 깊은 부분을 알아보고 두 가지 주요 형식인 응집과 결합에 대해서도 살펴봤다.

상속이라는 관계로는 새 객체가 기존 객체의 새로운 종류임을 나타내는 반면, 합성이라는 관계로는 객체 간의 상호작용(협업)을 나타낸다.

10. 디자인 패턴

업무용 소프트웨어 시스템을 작성하려면 개발자는 사업방식을 완전히 이해해야 한다.

결과적으로 개발자는 종종 회사의 업무 과정에 대해 가장 친밀한 지식을 갖게 된다.

우리가 포유류 클래스를 만들 때, 모든 포유류가 특정 행위와 특성을 공유하기 때문에 이 포유류 클래스를 사용하면 개나 고양이 등의 클래스들을 수없이 만들 수 있다.

이렇게 하다 보면 개, 고양이, 다람쥐 및 기타 포유류를 연구할 때 효과적인데, 이는 패턴을 발견할 수 있기 때문이다.

이런 패턴을 기반으로 특정 동물을 살펴보고 그게 포유류인지 아니면 행위와 특성의 패턴이 포유류와 다른 파충류인지 판단할 수 있다.

역사적으로 우리는 이러한 패턴 사용해왔다.

이러한 패턴은 소프트웨어 분야의 중요한 부분으로 소프트웨어 재사용형태와 밀접한 관련이 있다.(디자인 패턴)

패턴은 재사용 가능한 소프트웨어 개발 개념에 아주 적합하다.

객체지향 개발은 모두 재사용에 관한 것이므로 패턴과 객체지향 개발은 함께 진보한다.

디자인 패턴의 기본 개념은 모범 사례의 원칙과 관련이 있다.

모범 사례에 따르면 우수하고 효율적인 솔루션이 만들어질 때, 이러한 솔루션은 다른 사람들이 실패로부터 배웠던 방식과 이전의 성공에 따른 이익을 얻을 수 있었던 방식을 문서화한다는 것을 의미한다.

10.1. 디자인 패턴이 필요한 이유

디자인 패턴의 개념이 반드시 재사용 가능한 소프트웨어의 필요성에서 시작된 것은 아니다.

디자인 패턴에 대한 중요한 업적은 건물과 도시 건설에 관련되어 있다.

패턴은 우리 환경에서 반복적으로 발생하는 문제들을 기술하며, 그 문제에 대한 핵심 해법도 기술하는데, 이런 방식 덕분에 우리는 그러한 해법을 백만 번 거듭해서 사용할 수 있으며, 그럼에도 같은 방식을 두 번 다시 따르지 않아도 된다..

10.1.1. 패턴의 네 가지 요소

GoF는 패턴을 다음 네 가지 필수 요소로 설명한다.

  • 패턴 이름
    • 패턴 이름은 디자인 문제, 솔루션 및 결과를 한두 단어로 설명하는 데 사용할 수 있는 조종간 같은 것
    • 패턴에 이름을 붙여두면 어휘가 즉시 늘어나며 너 높은 수준으로 설계를 추상화할 수 있다.
    • 패턴에 대한 어휘를 갖추면 동료와 소통할 수 있고 문서로 교신할 수 있으며 자신과도 이야기 할 수 있다.
    • 이해하기 쉬워지면 절충산 설계를 다른 사람들에게 쉽게 전달할 수 있다.
  • 문제
    • 문제는 패턴을 적용할 시점을 설명한다.
    • 이것으로 문제 자체와 내용을 설명할 수 있다.
    • 알고리즘을 객체로 표현하는 방법과 같은 특정 설계 문제들을 설명할 수 있다.
    • 융통성이 없는 설계의 증상인 클래스 구조나 객체 구조를 설명할 수 있다.
  • 해법
    • 해법은 설계, 관계, 책임 및 협업을 이루는 요소를 설명한다.
    • 패턴은 여러 상황에 적용할 수 있는 템플릿과 같기 때문에, 해법은 구체적인 특정 설계나 구현을 설명하지 않는다.
    • 그 대신 패턴으로는 설계 문제를 추상적으로 설명하며, 요소를 일반적으로 배치해 설계 문제를 어떻게 해결하는지 보여준다.
  • 귀결
    • 귀결이란 패턴을 적용한 결과와 절충점을 말한다.
    • 귀결이 종종 무시되기도 하지만, 우리가 설게 결정 내용을 설명할 때는 귀결이 설계 대안을 평가하고 적용 패턴의 비용과 이점을 이해하는데 중요하다.
    • 소프트웨어 귀결은 시공간상의 절충과 관련이 있다.
    • 귀결로 언어 문제 및 구현 문제도 해결할 수 있다.

10.2. 스몰토크의 모델/뷰/컨트롤러

MVC는 종종 디자인 패턴의 기원을 설명하는 데 사용된다.

모델/뷰/컨트롤러(MVC)라는 패러다임은 스몰토크에서 사용자 인터페이스를 생성하는데 사용했다.

스몰토크
스몰토크는 그 당시 가장 인기있는 객체지향 언어였다..

Design Patterns에서는 다음과 같은 방식으로 MVC 컴포넌트들을 정의한다.

모델(model)은 애플리케이션 객체이고 뷰(view)는 화면 표현이며, 컨트롤러(controller)는 사용자 인터페이스가 사용자 입력에 반응하는 방식을 정의한다.

이전 패러다임에서는 모델, 뷰, 컨트롤러가 단일 엔터티 안에 모두 모여 있는 문제가 있었다.

예를 들어, 단일 객체에 이 세 가지 컴포넌트가 모두 들어 있는 식이다.

MVC패러다임에서는 이 세 가지 컴포넌트에 개별적이며 구별되는 인터페이스들이 있다.

따라서 우리는 어떤 애플리케이션의 사용자 인터페이스를 변경하려고 한다면 뷰만 변경하면 된다.

이는 객체지향 개발의인터페이스 대 구현부와 관련이 있다.

가능한 인터페이스와 구현부를 분리하려고 한다.

또한 인터페이스들끼리도 서로 최대한 분리하려고 한다.

MVC는 아주 일반적이면서도 기본적인 프로그래밍 문제와 관련된 특정 컴포넌트 간의 인터페이스를 명시적으로 정의한다.

MVC 개념을 따르고 사용자 인터페이스, 비즈니스 로직 및 데이터를 분리하면서 시스템이 훨씬 유연하고 강력해진다.

예를 들어, 사용자 인터페이스가 클라이언트 시스템에 있고, 비즈니스 로직이 애플리케이션 서버에 있고, 데이터가 데이터 서버에 있다고 가정한다면 개발 도중에 비즈니스 로직에 영향 없이 GUI의 형태를 변경할 수 있다.

MVC의 단점
MVC는 훌룡한 디자인이지만, 선행 디자인에 많은 주의를 기울여야 한다는 점에서 복잡할 수 있다.
이는 일반적인 객체지향 디자인의 문제이다.
좋은 디자인과 성가신 디자인 사이에는 미세한 경계선이 있다.
그렇다면 과연 완전한 디자인과 관련해 시스템이 얼마나 복잡해야 하는가?
정닶은 없고, 항상 트레이드 오프를 생각하자.

10.3. 디자인 패턴의 종류

Design Patterns에는 세 가지 범주로 분류된 총 23개의 패턴이 있다.

  • 생성 패턴(creational patterns)
    • 객체를 직접 인스턴스화하지 않은 채로 객체를 만든다.
    • 이를 통해 특정 사례에 대해 어떤 객체를 생성해야 할지 결정할 때 프로그램의 유연성이 향상된다.
  • 구조 패턴(structural patterns)
    • 복잡한 사용자 인터페이스나 계정 데이터와 같은 객체 그룹을 더 큰 구조로 합성할 수 있다.
  • 행위 패턴(behavioral patterns)
    • 시스템의 객체 간 통신을 정의하고 복잡한 프로그램에서 흐름을 제어하는 방법을 정의한다.

10.3.1. 생성 패턴

생성 패턴은 다음 범주별로 나눠볼 수 있다.

  • 추상 팩토리(Abstract Factory)
  • 빌더(Builder)
  • 팩토리 메서드(Factory Method)
  • 프로토타입(Prototype)
  • 싱글톤(Singleton)

이 책에선 각 패턴을 자세하게 다루지 않고 디자인 패턴이 뭔지 설명하는 내용이 주를 이룬다.

생각

확실히 멘토님께서 디자인 패턴먼저 공부하지 말라고 하신 이유를 다시한번 느낀다.  
게임 프로그래밍에 적합한 디자인 패턴도 물론 있지만 해당 디자인 패턴을 공부한다고 억지로 적용하면 오히려 독이다.

현재 구조에서 필요한 범주 생성인지, 행위인지, 구조인지 필요성을 느낄 때 해당 범주내에서 적합한 디자인 패턴을 공부하고 적용하는 것이 더 효울적이고 기억에 남는다는 것.

공부의 순서는 없지만 적합한 방법은 있는 것 같다.
팩토리 메서드 디자인 패턴

객체 생성, 즉 인스턴스화는 객체지향 프로그래밍에서 가장 기본적인 개념 중에 하나일 수 있다.

해당 객체가 존재하지 않으면 객체를 사용할 수 없다는 것은 말할 필요도 없다.

코드를 작성할 때 객체를 인스턴스화하는 가장 확실한 방법은 new키워드를 사용하는 것이다.

abstract class Shape{

}

class Circle extends Shape{

}

Circle circle = new Circle();

이 코드는 문제없이 동작하지만, 코드에서 Circle이나 해당 문제의 다른 도형들을 인스턴스화 해야 할 곳이 많을 것이다.

대부분의 경우에 Shape를 만들 때마다 처리해야 하는 특정 객체 생성 매개변수가 있을 것이다.

결과적으로 객체 생성 방식을 변경할 때마다 Shape 객체가 인스턴스화되는 모든 위치에서 코드를 변경해야 한다.

한 곳을 변경하면 잠재적으로 다른 많은 곳에서 코드를 변경해야 하므로 코드가 밀접하게 묶이게 된다..

이 접근법의 또 다른 문제점은 클래스를 사용해 프로그래머에게 객체 작성 로직을 노출시킨다는 것이다.

이러한 상황을 해결하기 위해 팩토리 메서드 패턴을 구현할 수 있다.

팩토리 메서드 패턴 정리 글

팩토리 메서드는 모든 인스턴스화를 캡슐화해 구현 전반에 걸쳐 균일해야 한다.

팰토리를 사용해 인스턴스화하면, 팩토리는 적절하게 인스턴스화 한다.

팩토리 메서드 패턴

팩토리 메서드 패턴의 근본적인 목적은 정확한 클래스를 지정하지 않고도 객체를 생성하는 일과 사실상 인터페이스를 사용해 새로운 객체 유형을 생성하는 일을 담당하는 것이다.

어떤 면에서는 팩토리를 래퍼라고 생각할 수 있다.(그렇게 생각했었다..)

객체를 인스턴스화하는 데 중요한 로직이 있을 수 있으며, 우리는 프로그래머(사용자)가 이 로직에 관심을 갖지 않았으면 한다면..

즉, 값을 검색하는 로직이 일부 로직 내부에 있을 때 접근자 메서드의 개념과 거의 같다.(게터 세터)

필요한 특정 클래스를 미리 알 수 없을 때 팩토리 메서드를 사용하면 된다.

즉, 이번 예제의 모든 클래스는 Shape의 서브클래스여야 한다.

사실 팩토리 메서드는 필요한걸 정확하게 모를 때 사용된다.

나중에 클래스의 일부를 추가할 수 있게 되며 필요한 게 무엇인지를 알고 있다면 생성자나 세터 메서드를 통해 인스턴스를 주입할 수 있다.

기본적으로 이게 다형성에 대한 정의이다.

위 링크에도 피자로 같은 예제가 있지만 책의 예제로 한번 더 복습한다.

enum형태로 도형의 종류를 정의한다.

enum ShapeType{
    CIRCLE,
    RECTANGLE,
    TRIANGLE
}

생성자와 generate()라고 부르는 추상 메서드만으로 Shape 클래스를 추상적인 것으로 정의할 수 있다.

abstract class Shape{
  private ShapeType shapeType = null;

  public Shape(ShapeType shapeType){
    this.shapeType = shapeType;
  }

  public abstract void generate();
}

Circle, Rectangle, Triangle 클래스는 Shape 클래스를 상속받아 구현한다.

class Circle extends Shape{
  public Circle(){
    super(ShapeType.CIRCLE);
    generate();
  }

  @Override
  public void generate(){
    System.out.println("Circle");
  }
}

class Rectangle extends Shape{
  public Rectangle(){
    super(ShapeType.RECTANGLE);
    generate();
  }

  @Override
  public void generate(){
    System.out.println("Rectangle");
  }
}

class Triangle extends Shape{
  public Triangle(){
    super(ShapeType.TRIANGLE);
    generate();
  }

  @Override
  public void generate(){
    System.out.println("Triangle");
  }
}

이름에서 알 수 있듯이 ShapeFactory 클래스는 실제적인 팩토리다.

generate()메서드에 초점이 맞춰져 있다.

팩토리는 많은 장점을 제공하지만, generate()메서드는 실제로 Shape를 인스턴스화하는 애플리케이션 내의 유일한 위치이다.

class ShapeFactory{
  public static Shape generateShape(ShapeType shapeType){
    Shape shape = null;

    switch(shapeType){
      case CIRCLE:
        shape = new Circle();
        break;
      case RECTANGLE:
        shape = new Rectangle();
        break;
      case TRIANGLE:
        shape = new Triangle();
        break;
      default:
        // 예외
        break;
    }

    return shape;
  }
}
생각

모든 디자인 패턴은 핵심적인 부분은 비슷하지만 각각의 패턴도 구현하는 언어, 스타일, 컨벤션의 차이가 다 다르다.

자바라서 그런지 모르겠지만 default보다 차라리 예외를 아래서 잡고 각각 리턴을 하거나, Shape 자체에 static메서드로 두거나, 인터페이스 DI로 좀 더 유연성을 강화할 수도 있다.

개인의 구현 차이와 해당 프로젝트의 규모, 팀 컨벤션을 고려해서 작성하는 것이 바람직한 것 같다.

이러한 개별 객체를 인스턴스화하는 전통적인 접근 방식은 프로그래머가 다음과 같이 new 키워드를 사용해 객체를 직접 인스턴스화 하는 것이다.

public class TestFactoryPattern{
  public static void main(String[] args){
    Shape circle = new Circle();
    Shape rectangle = new Rectangle();
    Shape triangle = new Triangle();
  }
}

그러나 팩토리 메서드 패턴을 사용하면 다음과 같이 객체를 인스턴스화 할 수 있다.

public class TestFactoryPattern{
  public static void main(String[] args){
    Shape circle = ShapeFactory.generateShape(ShapeType.CIRCLE);
    Shape rectangle = ShapeFactory.generateShape(ShapeType.RECTANGLE);
    Shape triangle = ShapeFactory.generateShape(ShapeType.TRIANGLE);
  }
}

더 나아가서 C#의 리플렉션을 사용하면 팩토리 메서드 패턴을 사용하지 않고도 객체를 인스턴스화 할 수 있다.

public class TestFactoryPattern{
  public static void main(String[] args){
    Shape circle = (Shape)Activator.CreateInstance(typeof(Circle));
    Shape rectangle = (Shape)Activator.CreateInstance(typeof(Rectangle));
    Shape triangle = (Shape)Activator.CreateInstance(typeof(Triangle));
  }
}

하지만 같은 추상화 레벨이나 생성 로직의 통일성 개인성을 보장하지 못하기 때문에 접근이나 가독성이 부족할 수 있는 것 같다..

실제로는 데이터 베이스상의 Unit을 읽어와서 객체를 동적으로 생성하여 관리하는 것 같다.

10.3.2.구조패턴

구조 패턴(structural pattern)은 객체 그룹에서 더 큰 구조를 만드는 데 사용된다.

다음 일곱 가지 디자인 패턴이 구조 패턴의 범주에 속한다.

  • 어댑터(Adapter, 즉 적응자)
  • 브리지(Bridge, 즉 가교)
  • 컴포지션(Composition, 즉 ‘합성체’)
  • 데코레이터(Decorator, 즉 ‘장식자’)
  • 파사드(Facade)
  • 플라이웨이트(Flyweight)
  • 프록시(Proxy)
어댑터 디자인 패턴

어댑터 패턴은 이미 존재하는 클래스에 대해 다른 인터페이스를 작성하는 방법이다.

어댑터 패턴은 기본적으로 클래스 래퍼를 제공한다.

다시 말해, 기존 클래스의 기능을 새로우면서도 이상적으로 볼 때 더 나은 인터페이스로 통합하는(둘러싸는) 새로운 클래스를 만든다.

래퍼의 간단한 예는 자바 클래스인 Integer다.

Integer 클래스는 그 안에 단일 Integer값을 둘러싼다.

객체지향 시스템에서는 모든 것이 객체이기 때문에, 기본적인 int, float등과 같은 기본 데이터 형식은 객체가 아니다.

이러한 기본 데이터 형식들을 바탕으로 함수를 실행해야 할 때 이런 기본 데이터 형식들을 객체로 취급해야 한다.

따라서 래퍼 객체를 작성하고 그 안에 기본 데이터 형식들을 두고 둘러싼다.

다음과 같은 기본 데이터 형식을 사용할 수 있다.

int myInt = 10;

그리고 이 기본 데이터 형식을 Integer로 둘러쌀 수 있다.

Integer myInteger = new Integer(myInt);

이제 형식 변환을 수행할 수 있으며, 해당 자료를 문자열로 취급할 수 있다.

String myString = myInteger.toString();

이 래퍼를 사용하면 원래부터 있던 정수를 객체로 취급해 객체의 모든 장점을 제공할 수 있다.

이것이 어댑터 패턴의 기본 아이디어이다.

어댑터의 말 그대로 한국의 콘센와 일본의 콘센트를 연결하는 어댑터의 역할을 한다.

사용하는 패키지나 라이브러리의 호환성이나 중간 역할을 위해 래퍼로 감싸는 것이다.

10.3.3.행위패턴

행위 패턴(behavioral pattern)은 다음과 같은 범주로 구성된다.

  • 책임 연쇄(Chain of responsibility)
  • 커맨드(Command, 즉 ‘명령’) 패턴
  • 인터프리터(Interpreter, 즉 ‘해설자’) 패턴
  • 이터레이터(Iterator, 즉 ‘반복자’) 패턴
  • 미디에이터(Mediator, 즉 ‘중재자’) 패턴
  • 메멘토(Memento, 즉 ‘기념비’) 패턴
  • 옵저버(Observer, 즉 ‘관찰자’) 패턴
  • 스테이트(State, 즉 ‘상태’) 패턴
  • 스트레이지(Strategy, 즉 ‘전략’) 패턴
  • 탬플릿 메서드(Template method) 패턴
  • 비지터(Visitor, 즉 ‘방문자’) 패턴
이터레이터 디자인 패턴

이터레이터(iterator)는 벡터와 같은 컬렉션을 순회하기 위한 표준 메커니즘을 제공한다.

컬렉션의 각 항목 한 번에 하나씩 접근할 수 있도록 기능을 제공해야 한다.

이터레이터 패턴은 정보 은닉을 제공해 컬렉션의 내부 구조를 안전하게 유지한다.

이터레이터 패턴은 또한 서로 간섭하지 않고 둘 이상의 이터레이터가 작성될 수 있도록 규정한다.

C#IEnumerable 인터페이스를 사용해 이터레이터 패턴을 구현한다.

public interface IEnumerable{
  IEnumerator GetEnumerator();
}

IEnumerator 인터페이스는 컬렉션의 각 항목을 순회하는 데 사용된다.

public interface IEnumerator{
  bool MoveNext();
  object Current{get;}
  void Reset();
}

자바는 다음과 같다.

package Iterator;

import java.util.*

public class Iterator{
  public static void main(String[] args){
    ArrayList<String> list = new ArrayList<String>();

    list.add("one");
    list.add("two");
    list.add("three");
    list.add("four");

    iterate(list);
  }

  public static void iterate(ArrayList<String> list){
    foreach(String s : list){
      System.out.println(s);
    }
  }
}

10.3.4. 안티패턴

디자인 패턴은 긍정적인 방식으로 발전하지만, 안티패턴(antipatterns)은 끔찍한 경험들을 모아 놓은 것으로 생각할 수 있다.

안티패턴이란 용어는 특정 유형의 문제를 사전에 해결하기 위해 디자인 패턴이 생성된다는 사실에서 비롯된다.

반면에, 안티패턴은 문제에 대한 반응이고 나쁜 경험으로부터 얻어진다.

요컨대, 디자인 패턴이 견고한 설계 실습을 기반으로 한 반면에, 안티패턴은 피해야할 관행으로 생각할 수 있다.

많은 사람들이 안티패턴이 디자인 패턴보다 더 유용하다고 생각한다.

안티패턴은 이미 발생한 문제를 해결하도록 설계되었기 때문이다..

안티패턴은 기존 설계를 수정하고 실행 가능한 솔루션을 찾을 때까지 해당 설계를 지속적으로 리팩토링한다.

  • 안티패턴의 좋은 예 몇가지
    • 싱글톤(Singleton)
    • 서비스 로케이터(Service Locator)
    • 매직 스트링/넘버(Magic String/Number)
    • 인터페이스 부풀리기(Interface Bloat)
    • 예외에 기반한 코딩(Exception Driven Coding)
    • 오류 숨기기/오류 삼키기(Hiding Swallowing Errors)

10.4. 결론

패턴은 일상 생활의 일부이며, 이는 객체지향 설계에 대해 생각해야 하는 방식이다.

정보 기술과 관련된 많은 것들과 마찬가지로 해법의 근원은 실제 상황에서 발각된다.

11. 의존적이거나 경직된 클래스가 되지 않게 하기

앞 1장에서 다룬 것과 같이 객체지향 프로그래밍의 핵심은 캡슐화, 상속, 다형성, 합성이다.

인터페이스(접속)도 추가하는 게 좋겠지만 특정 유형의 상속이라고 본다.

개발자들 사이에서 상속과 합성은 항상 뜨거운 감자이다.

요즘은 상속보단 합성에 좀 더 초점을 두고 상속 자체를 단일 계층 수준이나 2레벨까지만 제한하여 최소하는 경향이 있다.

상속을 사용하는 방법에 초점을 둔 이유는 묶임(coupling) 문제와 관련이 있다.

상속 사용에 대한 논쟁은 거의 재사용성과 확장성 및 다형성에 관해서이지만, 사실상 상속은 클래스들 간에 의존체(dependencies)가 있게 하는 꼴이므로 문제를 일으킬수 있다.

상속과 합성에 익숙해지기에서는 상속이 실제로 캡슐화를 약화시키는 방법에 대해 논의했는데, 이는 기본 개념이기 때문에 직관적이지 않은 것처럼 보인다.

주의
7장에서도 언급된 내용이지만 상속을 피하라는 말이 아니다.
여기서 논의하는 내용은 실제로 의존체들과 묶임성이 강한 클래스들을 피하는 일에 관한 것이다.
상속 사용 시기는 이 논의에서 중요한 부분이다.

그렇다면 상속이 아니라면 무엇을 사용해야 할까?

간단히 말하자면 합성이 답이다.

다른 책에선 구성으로 표현하기도 한다.

필자는 전반적으로 클래스를 재사용하는 방법에 대해서 상속과 합성이라는 두 가지 방법만 있다고 주장한다.

상속 방식으로는 부모 클래스에서 자식을 생성할 수 있고, 합성 방식으로는 클래스 내에 그 밖의 클래스들을 넣을 수 있다.

그렇다면 상속을 피해야 한다면 상속을 배우는 대 시간을 소비할 필요가 있을까?

간단히 답하면 많은 코드에서 상속이 사용된다.

대부분 개발자가 곧 이해하겠지만 유지보수 시점에 이르러서야 대부분의 코드에 당면하게 된다.

따라서 상속을 사용해 작성된 코드를 수정하고 개선하고 유지보수하는 방법을 이해해야 한다.

우리가 새로운 코드를 작성할 때 상속을 사용해야 할 수도 있다.

요약하자면, 프로그래머는 가능한 모든 객체지향 기반 기술과 더불어 개발자가 활용해 볼 만한 모든 도구는 배 두는게 좋다는 말이다..

또한, 이 말은 프로그래머가 자신이 활용해 오던 다양한 객체지향 기술이나 객체지향 도구에만 집착하지 말고 새로운 기술과 도구를 익히는 일도 생각해 두어야 한다는 말이기도 하다.

여기서 필자가 가치 판단을 하고 있지 않다는 점을 이해해야 한다.

즉, 상속이 문제가 되므로 피해야 한다고 주장하는 게 아니다.

상속이 어떻게 사용되는지를 완전히 이해하고 대안적인 설계 방식을 신중하게 연구한 후에 비로소 스스로 결정하는 중요함을 강조하고 있다.

생각

책으로만 상속이나 설계에 대해 완벽하게 이해하기는 불가능하다.

내 생각엔 그렇다.. 실제로 샌드박스로 만들어 보거나 프로젝트로 경험하며 감각을 키워야 하는 부분이 존재한다.

이는 시간과 노력이 생각보다 많이 들어간다..

따라서 이번 장의 예제는 클래스를 설계하는 최적의 방법을 반드시 설명하려는 의도로 보인 게 아니고 오히려 상속과 합성 사이의 결정과 관련된 문제에 대해 생각하게 하기 위한 훈련용 연습 문제인 것이다.

모든 기술을 발전시키면서 좋은점은 지키면서도 그다지 좋지 않으면 개선하는 게 필요하다.

또한, 합성에는 고유한 묶임 문제가 있다.

7장에서 설명한 결합과 응집 즉, 컴포지션에 대한 문제가 있다.

응집체는 다른 객체에 포함된 객체이며
결합체는 매개변수 목록을 통해 다른 객체로 전달되는 객체이다.
응집체는 객체에 포함되어 있기 때문에 서로 밀접하게 연결되어 있으므로 피해야 한다.

따라서 상속으로 인해 클래스끼리 서로 강하게 묶인다는 평판이 있었지만, 합성으로도 강하게 묶이는 클래스를 만들 수 있다.

응집체들을 사용해 스테레오를 만드는 일이란 단일 컴포넌트 안에 모든 컴포넌트가 포함된 제품인 CD 카세트를 만드는 일에 비유할 수 있다.

다양한 상황에서 CD 카세트는 무척 편리하다.

집어 들고 쉽게 움직일 수 있으며, 특별한 조립이 필요하지 않다.

그러나 이런 식으로 설계하면 많은 문제가 생길 수 있다.

MP3 플레이어가 방가지만 전체 장치를 수리점에 맡겨야 한다.

더 나쁜 것은 전기 문제와 같이 전체 붐 박스를 사용할 수 없게 만드는 많은 문제가 발생할 수 있다.

결합체들을 사용해 스테레오를 만들면 응집체에서 발생하는 많은 문제점을 피할 수 있다.

컴포넌트 음향기기를 연결선으로 연결해 둔 한 뭉치의 결합체들이라고 생각해 보자.

이 설계에는 스피커, CD 플레이어, 턴테이블 및 카세트 플레이어와 같은 다른 여러 객체에 연결된 중심 객체가 있으며, 이것을 리시버라고 부른다.

사실 이 리시버는 제작업체가 어느 곳이든지 별 상관없는 해법이라고 생각할 수 있는데, 상품 진열대에서 이 컴포넌트를 쉽게 구할 수 있기 때문이다.

이 상황에서 CD 플레이어가 고장 나면 CD플레이어만 분리하여 고치거나 작동하는 새 CD 플레이어로 교체할 수 있다.


9장에서 말했듯이 강하게 묶이는 클래스는 일반적으로 눈살을 찌푸리게 하는 게 맞기는 해도 때로는 강하게 묶이게 하는 설계로 인한 위험을 감수해야 하는 경우도 있다.
CD플레이어가 그 예이며 강하게 묶이게 하는 설계 방식으로 만든 것임에도 때로는 선호되는 선택지이다.

11.1. 합성 대 상속, 그리고 의존성 주입

먼저 예제의 상속 모델을 상속이 아니라 합성으로 다시 설계하는 것에 초점을 맞출 것이다.

두 번째로 최적의 솔루션은 아니지만 응집을 사용함에도 합성으로 다시 설계하는 방법을 보여준다.

세 번째는 응집체를 피하고 그 대신에 결합체를 사용해 설계하는 방법을 보여준다.

11.1.1. 상속

class Mammal{
  public void eat() { System.out.println("I am Eating"); }
}

class Bat extends Mammal{
  public void fly() { System.out.println("I am Flying"); }
}

class Dog extends Mammal{
  public void bark() { System.out.println("I am Barking"); }
}

public class TestMammal{
  public static void main(String[] args){

    Dog fido = new Dog();
    fido.eat();
    fido.bark();

    Bat batty = new Bat();
    batty.eat();
    batty.fly();
  }
}

이 설계에서 Mammal은 모든 포유류가 무언가를 먹어야만 한다고 가정하므로 eat()라는 단일행위를 갖게 된다.

그러나 우리가 Mammal의 서브클래스인 Bat와 Dog를 추가하게 되면, 즉시 상속 문제가 발생한다.

개는 짖을 수 있지만 모든 포유류가 짖는 것은 아니다. 또한 박쥐는 날 수 있지만 모든 포유류가 날 수 있는 것은 아니다.

문제는 이러한 메서드들이 어디에 속해야 하는가다.

이전의 펭귄 예제에서와 같이 조류라고 해서 다 날 수 있는 게 아니기 때문에 상속 위계구조에서 메서드를 배치할 위치를 경정하는 것은 까다로울 수 있다.

Mammal 클래스를 FlyingMammals와 walkingMammals로 분리하는 방식은 모든 게 드러나지 않은 빙산 중에 한 부분만 드러낼 뿐이므로 아주 우아한 해결책은 아니다.

진짜 빙산의 일각 그림 너무 공감..

어떤 포유류는 헤엄칠 수 있고, 어떤 포유류는 알을 넣기도 한다.

더욱이, 개별 포유동물 종이 가지고 있는 수많은 다른 행위가 있을 수 있으며, 이러한 모든 행위에 대해 별도의 클래스를 만드는 일은 비현실적일 수 있다.

이러한 설계는 is-a관계로 접근하는 대신에 has-a 관계를 사용해 설계를 탐색해야 한다.

11.1.2. 합성

이 전략에서는 클래스 자체를 행위를 포함하지 않고 각 행위에 대해 개별 클래스를 만든다.

따라서 상속 위계구조에 행위를 배치하는 대신에 각 행위에 대한 클래스를 만들고 나서는 필요한 행위만 포함함(응집)으로써 개별 포유류를 만들 수 있다.

주의
응집이라는 용어는 앞 단락에서 사용되었다는 점에 유의하자
이번 예제는 상속 대신에 합성을 사용하는 방법이며, 여전히 중요한 묶음 개념이 남아있다.
따라서 인터페이스를 사용해 다음 예제로 나아가는 중간 교육 단계로 생각하자.

class Mammal{
  public void eat() { System.out.println("I am Eating"); }
}

class Walkable{
  public void walk() { System.out.println("I am Walking"); }  
}

class Flyable{
  public void fly() { System.out.println("I am Flying"); }
}

class Dog{
  Mammal dog = new Mammal();
  Walkable walkable = new Walkable();
}

class Bat{
  Mammal bat = new Mammal();
  Flyable flyable = new Flyable();
}

public class TestMammal{
  public static void main(String[] args){

    Dog fido = new Dog();
    fido.dog.eat();
    fido.walkable.walk();

    Bat batty = new Bat();
    batty.bat.eat();
    batty.flyable.fly();
  }
}

참고
이번 예제는 상속 대신에 합성을 사용하는 방법을 설명하는 것이다.
설계에 상속을 전혀 사용할 수 없다는 의미가 아니다.
설계 단계에서 모든 포유류가 포식 행위를 한다고 했다면 앞처럼 eat() 메서드를 Mammal 클래스에 추가하는 것이 더 나은 설계일 수 있다.
항상 그렇듯이, 이게 설계 결정이다.

아마도 이 논의의 핵심은 앞서 다루었던 개념, 상속이 오히려 캡슐화를 깬다는 점일 것이다.

Mammal 클래스를 변경하게 되면 모든 Mammal 서브클래스를 다시 컴파일해야 하기 때문에 이런 측면을 쉽게 이해할 수 있다.

이는 이와 같은 클래스들이 서로 밀접하게 결합되어 있으며, 클래스를 가능한 한 많이 분리한다는 목표에 반한다는 뜻이다.

합성의 예제에서 Whale 클래스를 추가한다고 해도 이전에 작성된 클래스 중 어느것도 다시 작성할 필요는 없다.

만약 Swimmable이라는 클래스와 Whale이라는 클래스를 추가한다면 Swimmable 클래스를 Dolphin 클래스에 재사용할 수 있다.

class Swimmable{
  public void swim() { System.out.println("I am Swimming"); }
}

class Whale{
  Mammal whale = new Mammal();
  Swimmable swimmable = new Swimmable();
}

기존 애플리케이션은 이전에 존재했던 클래스를 변경하지 않은 채로 이 기능을 추가할 수 있다.

필자의 경험 법칙 중 한가지는 다형성이 꼭 필요한 상황에서만 상속을 사용하라는 것이다.

Shape에서 상속된 Circles와 Rectangle은 상속을 합법적으로 사용하는 것일 수 있다.

반면에, 걷기나 날기와 같은 행위는 상속에 대한 좋은 후보가 아닐 수 있다.

구현 상속/정의 상속 구분 그리고, 상속은 주로 데이터/모델링 상속, 행위는 주로 구현

합성을 사용해 이 솔루션을 구현했지만 설계에는 심각한 결함이 있다.

new 키워드를 사용하는 것이 명백하기 때문에 객체가 강하게 묶여 있다.

클래스를 분리하는 연습을 완료하기 위해 의존성 주입이라는 개념을 소개한다.

의존성 주입 정리글

간단하게 정리하여 객체를 생성하는 대신에 매개변수 목록을 통해 다른 객체 내부로 외부 객체를 주입한다는 뜻이다.

11.1.3. 의존성 주입

이전 단원에 나온 예제에서는 합성을 통해 Dog에게 Wakeable 행위를 제공했다.

Dog클래스는 문자 그대로 다음 코드 조각과 같이 Dog 클래스 자체 내에 새로운 Walkable 객체를 만들었다.

class Dog{
  Walkable walkable = new Walkable();
}

이것은 실제로 동작하지만, 클래스들은 여전히 서로 강하게 결합되어 있다.

이전 예제에서 클래스를 완전히 분리하기 위해 앞에서 언급한 의존성 주입 개념을 구현해 본다.

의존성 주입과 제어 역전의 원칙(IoC)은 종종 함께 다뤄진다.

제어 역전에 대한 한 가지 정의는 다른 사람이 의존체를 인스턴스화해 전달하게 하는 것이다.

이것을 바로 이번 예제에서 구현할 것이다.

모든 포유류가 날고 걷고 헤엄치는 것은 아니기 때문에 분리과정을 위해 포유류의 행위를 나타내는 인터페이스를 만든다.

interface IWalkable{
  public void walk();
}

이 인터페이스의 유일한 메서드는 walk()이며, 구현부를 제공하기 위해 구상 클래스에 남겨진다.

class Dog extends Mammal implements IWalkable{
  IWalkable walker;

  public void setWalker(IWalkable walker){
    this.walker = walker;
  }

  public void walk(){
    walker.walk();
  }
}

Dog 클래스는 Mammal 클래스를 확장하고 IWalkable 인터페이스를 구현한다.

또한 Dog 클래스는 의존성을 주입하는 메커니즘을 제공하는 참조 및 세터를 제공한다.

IWalkable walker;

public void setWalker(IWalkable walker){
  this.walker = walker;
}

간단하게 말해서 이것이 의존성 주입이다.

Walkable 행위는 새 키워드를 사용해 Dog클래스 내에 작성되지 않는다.

매개변수 목록을 통해 Dog클래스에 주입된다.

class Mammal{
  public void eat() { System.out.println("I am eating"); }
}

interface IWalkable{
  public void walk();
}

class Dog extends Mammal implements IWalkable{
  IWalkable walker;

  public void setWalker(IWalkable walker){
    this.walker = walker;
  }

  public void walk(){
    walker.walk();
  }

  public class TestMammal{
    public static void main(String[] args){
      Walkable walker - new Walkable();
      Dog fido = new Dog();
      fido.setWalker(walker);
      fido.eat();
      fido.walk();
    }
  }
}

이 예제에서는 세터를 사용해 의존성을 주입하지만 이 방법이 유일하지는 않다.

생성자를 통해서 주입하는 방법

11.2. 결론

의존성 주입은 구현한 클래스의 구성을 클래스 의존체들의 구성으로부터 분리한다.

이는 매번 자신만의 것을 직접 제작하는 대신에 상품 진열대에서 무언가를 구입해오는 일과 같다.

이게 상속과 합성에 대한 토론의 핵심이다.

단순히 토론이라는 점에 유의해야 한다.(정답x)

스스로 상속과 합성에 대한 문제를 생각하고 설계에 반영해야 한다.

12. 객체지향 설계의 SOLID 원칙

객체지향 프로그래밍과 관련해 많은 개발자가 가장 흔히 하는 말 중에 하나는 ‘실제 세계를 모델링할 수 있는 게 객체지향의 주요 장점이다.’라는 말이다.

고전적인 객체지향 개념에서는 많이 사용되는 말이지만, 로버트 마틴씨의 말에 따르면 객체지향이 우리가 생각하는 방식과 밀접하다고 말하는 것은 그저 마케팅에 불과하다고 한다.

그 대신에, 그는 객체지향이란 핵심 의존성들을 역전시킴으로써 경직된 코드나 취약한 코드 및 재사용이 불가능 코드가 되지 않게 하는 식으로 의존체들을 관리하는 일이라고 말한다.

고전적인 객체지향 프로그래밍 과정에서는 코드를 종종 실제 상황에 맞게 모델링하는 경우가 많다.

예를 들어, 개가 포유류의 일종이라면(is-a)이 관계를 상속으로 나타내는게 명백한 선택지이기는 하다.

엄격한 has-a 및 is-a 리트머스 테스트라는 게 수년간 객체지향적 마음가짐의 한 부분을 차지했다.

그러나 이 책 전체에서 보여주듯 상속 관계를 강요하려고 하면 설계 문제가 생길 수 있다.

짖지 않는 개와 짖는 개, 날지 못하는 새와 날 수 있는 새를 상속 설계를 잘 선택해서 하는 것만으로 서로 구분되게 할 수 있다고 생각하는가?

has-a와 is-a를 엄밀하게 구분해 결정하는 데 집중하는 것만이 최선의 접근 방식은 아닐 것이다.

우리는 클래스를 분리하는 데 더 집중해야 한다.

밥 삼촌(로버트 마틴)은 재사용할 수 없는 코드를 설명하기 위해 다음과 같은 세 가지 용어를 정의한다.

  • 경직성(rigitity): 프로그램의 한 부분을 변경하면 다른 부분까지 변경해야 하는 경우
  • 취약성(fragility): 관련이 없는 곳에서 오류가 발생하는 경우
  • 부동성(immobility): 코드를 원래 맥락에서 벗어나 재사용할 수 없는 경우

이러한 문제들을 해결하고 목표를 달성하기 위해 SOLID가 도입되었다.

로버트 마틴이 ‘소프트웨어 설계를 더 이해하기 쉽고 더 유연하며 유지보수 가능하게 만들기‘위해 도입한 다섯 가지 설계 원칙을 SOLID라고 한다.

밥 아저씨의 말에 따르면 모든 객체지향 설계에 적용되지만, SOLID원칙은 애자일 개발이나 적응형(adaptive) 소프트웨어 개발과 같은 방법론의 핵심 철학을 형성할 수 있다.

  • SRP: 단일 책임 원칙(Single Responsibility Principle)
  • OCP: 개방-폐쇄 원칙(Open-Closed Principle)
  • LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
  • ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
  • DIP: 의존성 역전 원칙(Dependency Inversion Principle)

이 원칙들을 다뤄보면서 수십년 동안 존재해 온 고전적인 객체지향 원칙과 관련지어 본다.

12.1 객체지향 설계의 SOLID 원칙

12.1.1 단일 책임 원칙(SRP)

단일 책임 원칙(Single Responsibility Principle)에 따르면 클래스를 변경한 이유가 단일해야 한다.

프로그램의 각 클래스와 모듈은 단일 작업에 중점을 두어야 한다.

따라서 같은 클래스 안에 다른 이유 때문에 변경될 메서드를 넣지 않도록 한다.

클래스를 설명하는 글에 ‘그리고’라는 단어가 포함되면 SRP가 깨질 수 있다.

다시 말해서, 모든 모듈이나 클래스는 소프트웨어가 제공하는 기능의 단일 부분에 대해서만 책임을 져야 한다.

그 책임을 완전히 캡슐화해야 한다.

이 추상 클래스를 상속 받은 클래스는 반드시 calcArea() 메서드를 구현해야 한다.

abstract class Shape{
    protected String name;
    protected double area;
    public abstract double calcArea();
}

추상 클래스를 상속받은 Circle 클래스는 자체적으로 calcArea() 메서드를 구현한다.

class Circle extends Shape{
    private double radius;
    public Circle(double radius){
        this.radius = radius;
    }
    public double calcArea(){
        area = Math.PI * radius * radius;
        return area;
    }
}

주의
단일 책임 원칙에 초점을 맞출 뿐만 아니라 가능한 한 간단한 예제가 되게 하려고 Circle 클래스를 만들었다.

CalculaterAreas라는 클래스는 Shape 배열에 포함된 다른 도형의 면적을 합산한다.

class CalculaterAreas{
    Shape[] shapes;
    double sumTotal = 0;

    public CalculaterAreas(Shape[] shapes){
        this.shapes = shapes;
    }

    public double sumAreas(){
        sumTotal = 0;
        for (Shape shape : shapes){
            sumTotal += shape.calcArea();
        }
        return sumTotal;
    }

    public void output(){
        System.out.println("Sum of the areas of provided shapes: " + sumTotal);
    }
}

CalculaterAreas 클래스는 출력을 처리하므로 문제가 있다.

면적 계산 행위와 출력 행위가 같은 클래스에 포함되어 있다.

public class TestShape{
    public static void main(String[] args){
        Circle circle = new Circle(1);

        Shape[] shapeArray = new Shape[1];
        shapeArray[0] = circle;

        CalculaterAreas calculator = new CalculaterAreas(shapeArray);

        calculator.sumAreas();
        calculator.output();
    }
}

이 처럼 테스트 애플리케이션이 준비되면 단일 책임 원칙 문제에 중점을 둘 수 있다.

다시 말하지만 이 클래스는 출력 행위와 합산 행위가 포함되어 있다.

여기서 근본적인 문제는 다음과 같다.

메서드의 기능을 변경하려면 면적을 합산하는 메서드를 변경할지 여부에 관계없이 CalculatorAreas 클래스를 변경해야 한다.

예를 들어, 어떤 시점에서 간단한 텍스트가 아닌 HTML로 출력하고 싶다면 어떻게 해야 할까?

지금은 책임이 결합되어 있기 때문에 영역을 합한 코드를 다시 컴파일하고 재배치해야 한다.

단일 책임 원칙에 따르면, 한 메서드를 변경해도 다른 메서드에 영향을 미치지 않게 하여 불필요한 컴파일이 없도록 하는 것이 목표다.

‘한 클래스에는 변화해야 할 이유가 한 가지, 아니 단 한 가지여야 한다. 즉, 변화해야 할 책임이 단일해야 한다.’

이를 해결하기 위해, 두 개의 메서드를 서로 분리된 클래스에 넣을 수 있는데, 그중에 하나는 원래 콘솔 출력용이고 다른 하나는 HTML 출력용이다.

class CalculaterAreas{
    Shape[] shapes;
    double sumTotal = 0;

    public CalculaterAreas(Shape[] shapes){
        this.shapes = shapes;
    }

    public double sumAreas(){
        sumTotal = 0;
        for (Shape shape : shapes){
            sumTotal += shape.calcArea();
        }
        return sumTotal;
    }
}

class OutputAreas{
    double areas = 0;

    public OutputAreas(double areas){
        this.areas = areas;
    }

    public void console(){
        System.out.println("Sum of the areas of provided shapes: " + areas);
    }

    public void html(){
        System.out.println("<HTML>");
        System.out.println("Sum of the areas of provided shapes: " + areas );
        System.out.println("</HTML>");
    }
}

이제 테스트 애플리케이션을 다시 작성해보자.

public class TestShape{
    public static void main(String[] args){
        Circle circle = new Circle(1);

        Shape[] shapeArray = new Shape[1];
        shapeArray[0] = circle;

        CalculaterAreas calculator = new CalculaterAreas(shapeArray);
        OutputAreas output = new OutputAreas(calculator.sumAreas());
        
        output.console();
        output.html();
    }
}

여기서 요점은 요구사항에 따라 다양한 대상으로 출력을 보낼 수 있다는 것이다.

그 밖에도 Json이 필요하다면 CalculatorAreas 클래스를 변경하지 않고 OutputAreas 클래스에 Json 출력 메서드를 추가할 수 있다.

결과적으로 CalculatorAreas클래스를 독립적으로 재배포하면서도 그 밖의 클래스에 전혀 영향을 미치지 않는다.

생각

개인적인 생각으로 OutputAreas 클래스는 한 가지 행위만 하고 있긴 하지만 좀 더 나아가서 콘솔 출력과 HTML 출력을 각각 클래스로 분리하는 것이 더 좋을 것 같다.

인터페이스로 묶어서 해당 인터페이스를 상속받아 출력 기능을 각각 구현하는 것이 좀 더 바람직..?

클래스를 쪼갤 수 있으면 쪼개서 작게 만들어야 하기도 하고 DI를 사용해 사용자에게 new가 아닌 주입을 받아 사용하는 것이 좋다고 생각한다.

12.1.2. 개방/폐쇄 원칙(OCP)

개방/폐쇄 원칙(Open/Close Principle)에 따르면 클래스를 수정하지 않고 클래스의 행위를 확장할 수 있어야 한다.

class Rectangle{
    public double length;
    public double width;

    public Rectangle(double length, double width){
        this.length = length;
        this.width = width;
    }
}

class CalculateAreas{
    private double area;

    public double calculateRectangleArea(Rectangle rectangle){
        area = rectangle.length * rectangle.width;
        return area;
    }
}

public class OpenColsed{
    public static void main(String[] args){

        Rectangle rectangle = new Rectangle(1, 2);
        CalculateAreas calculator = new CalculateAreas();

        System.out.println("Area of rectangle: " + calculator.calculateRectangleArea(rectangle));
    }
}

이 애플리케이션이 사각형에 한해서 작동한다는 사실은 개방/폐쇄 원칙을 설명하는 제약 조건을 제공한다.

CalculateAreas 클래스에 Circle을 추가하려면 모듈 자체를 변경해야 한다.

분명히 이것은 개방/폐쇄 원칙과 상충되며, 모듈을 변경하기 위해 모듈을 변경할 필요가 없다는 것을 명시하고 있다.

Shape라는 추상 클래스를 만들어 상속받게 하여 getArea()의 구현을 강제할 수 있는데, 이때 Shape클래스 자체를 변경할 필요 없이 원하는 만큼의 다양한 클래스를 추가할 수 있다.

이제 Shape클래스가 폐쇄되어 있다고 말할 수 있다.

abstract class Shape{
    public abstract double getArea();
}

class Rectangle extends Shape{
    public double length;
    public double width;

    public Rectangle(double length, double width){
        this.length = length;
        this.width = width;
    }

    @Override
    public double getArea(){
        return length * width;
    }
}

class Circle extends Shape{
    public double radius;

    public Circle(double radius){
        this.radius = radius;
    }

    @Override
    public double getArea(){
        return Math.PI * radius * radius;
    }
}

class CalculateAreas{
    private double area;

    public double calculateArea(Shape shape){
        area = shape.getArea();
        return area;
    }
}

public class OpenColsed{
    public static void main(String[] args){

        Shape rectangle = new Rectangle(1, 2);
        Shape circle = new Circle(1);

        CalculateAreas calculator = new CalculateAreas();

        System.out.println("Area of rectangle: " + calculator.calculateArea(rectangle));
        System.out.println("Area of circle: " + calculator.calculateArea(circle));
    }
}

이런 식으로 구현하면 새 Shape를 추가할 때 CalculateAreas 클래스를 변경할 필요가 없다.

이는 레거시 코드에 대한 걱정없이 코드를 확장할 수 있음을 의미한다.

핵심은 개방/폐쇄 원리는 자식 클래스를 통해 코드를 확장해야 하며, 원래 클래스는 변경할 필요가 없다는 것이다.

그러나 확장(Extension)이라는 단어는 SOLID와 관련된 여러 토론에서 문제가 된다.

우리가 상속보다 합성을 선호한다면 이것이 개방/폐쇄 원칙에 어떤 영향을 미칠까?

SOLID 원칙 중 하나를 따르는 경우에, 코드가 또 다른 SOLID 원칙 중 하나를 따라야 할 수 있다.

예를 들어, 개방/폐쇄 원칙을 따르도록 코드를 설계했는데 이 코드가 단일 책임 원칙을 준수할 수도 있다는 말이다..!

생각

단일 책임 원칙에서는 Shape를 가지고 다형성을 구현, 여기서도 Shape를 가지고 다형성을 구현했다.

다른 점은 CalculateAreas 클래스의 행위에 대한 인터페이스 설계인 것 같다.

해당 인터페이스를 열어 두어 다형성, 캡슐화를 위해 클래스를 분리하고 이를 보여주는 예제가 아마 나올 듯 하다.

12.1.3. 리스코프 대체 원칙(LSP)

리스코프 대체(치환) 원칙(Liskov Substitution Principle)에 따르면 부모 클래스의 인스턴스를 해당 자식 클래스 중 하나의 인스턴스로 교체할 수 있게 설계해야 한다.

부모 클래스가 무언가를 할 수 있다면 자식 클래스도 그것을 할 수 있어야 한다.

합리적으로 보일 수 있지만.. 리스코프 대체 원칙을 위반하는 사례를 먼저 본다.

abstract class Shape{
    protected double area;

    public abstract double getArea();
}

class Rectangle extends Shape{
    private double length;
    private double width;
    
    public Rectangle(double length, double width){
        this.length = length;
        this.width = width;
    }

    @Override
    public double getArea(){
        area = length * width;
        return area;
    }
}

class Square extends Rectangle{
    public Square(double side){
        super(side, side);
    }
}

public class LiskovSubstitution{
    public static void main(String[] args){
        
        Rectangle rectangle = new Rectangle(1, 2);

        System.out.println("Area of rectangle: " + rectangle.getArea());

        Square square = new Square(2);

        System.out.println("Area of square: " + square.getArea());
    }
}

문제없는 코드이다.

직사각형은 도형의 일종으므로(is-a) 모든 것이 좋아보인다.

정사각형또한 직사각형의 일종으로(is-a) 아직까지는 문제가 없는 것 같다.

만약 이런 관계가 아니라면 어떻게 해야 할까?

이제는 다소 철학적인 이야기를 해야한다.

정사각형이 실제로는 직사각형의 일종인가?

많은 사람들이 그렇게 말하지만 정사각형은 특수한 유형의 도형일 수 있지만 직사각형과는 속성이 다르다.

직사각형은 사격형이기도 하지만 평행사변형이기도 하다.(대각에 놓인 변끼리 일치하는 경우)

한편, 정사각형은 마름모이지만, 직사각형은 그렇지 않다.

따라서 직사각형과 정사각형 간에는 약간 다른 점이 있다.

실제로는 객체지향 설계 시에 기하학이 문제 되지는 않는다.

우리가 직사각형과 정사각형을 어떻게 만드느냐가 문제다.

public Rectangle(double l, double w){
    length = l;
    width = w;
}

생성자에는 분명히 두 개의 매개변수가 필요하다.

그러나 부모 클래스인 Rectangle은 Square생성자가 두 개이기를 기대하지만, 한 개만 필요하다.

실제로 면적을 계산하는 기능은 두 클래스에서 미묘하게 다르다.

사실 Square는 동일한 매개변수를 두 번 전달하여 Rectangle을 속인다.

이것은 받아들일 수 있는 해결책처럼 보일지 모르지만,실제로는 코드를 혼란스럽게 할 수 있고 의도하지 않은 유지보수 문제를 초래할 수 있다.

이것은 일관성이 없는 설계 결정이며, 아마도 의심스러운 설계 결정일 것이다.

생성자가 다른 생성자를 호출하는 것을 보게 된다면 설계를 일시 중지하고 다시 생각해보는 게 좋다.

적절하지 않은 자식 클래스가 아니여서 그럴 수 있기 때문

이런 딜레마를 해결하기 위해선 정사각형과 직사각형을 다음과 같이 설계해야 한다.

abstract class Shape{
    protected double area;

    public abstract double getArea();
}

class Rectangle extends Shape{
    private double length;
    private double width;
    
    public Rectangle(double length, double width){
        this.length = length;
        this.width = width;
    }

    @Override
    public double getArea(){
        area = length * width;
        return area;
    }
}

class Square extends Shape{
    private double side;

    public Square(double side){
        this.side = side;
    }

    @Override
    public double getArea(){
        area = side * side;
        return area;
    }
}

public class LiskovSubstitution{
    public static void main(String[] args){
        
        Rectangle rectangle = new Rectangle(1, 2);

        System.out.println("Area of rectangle: " + rectangle.getArea());

        Square square = new Square(2);

        System.out.println("Area of square: " + square.getArea());
    }
}

12.1.4. 인터페이스 분리 원칙(ISP)

인터페이스 분리 원칙(Interface Segregation Principle)에 따르면 몇 개의 큰 인터페이스가 있는 편보다는 작은 인터페이스가 많은 편이 바람직하다.

클래스도 마찬가지

이번 예제는 Mammal, eat() 및 makeNoise()에 대한 여러 행위를 포함하는 단일 인터페이스를 작성한다.

interface Mammal{
    public void eat();
    public void makeNoise();
}

class Dog implements Mammal{
    @Override
    public void eat(){
        System.out.println("Dog eats");
    }

    @Override
    public void makeNoise(){
        System.out.println("Dog barks");
    }
}

public class myClass{
    public static void main(String[] args){
        Dog dog = new Dog();
        dog.eat();
        dog.makeNoise();
    }
}

이것은 잘 작동하지만, Mammal 인터페이스는 Dog 클래스에 대해 너무 많은 것을 요구한다.

Mammal에 대한 단일 인터페이스를 만드는 대신에 모든 행위에 대해 별도의 인터페이스를 만들 수 있다.

interface Eater{
    public void eat();
}

interface NoiseMaker{
    public void makeNoise();
}

class Dog implements Eater, NoiseMaker{
    @Override
    public void eat(){
        System.out.println("Dog eats");
    }

    @Override
    public void makeNoise(){
        System.out.println("Dog barks");
    }
}

public class myClass{
    public static void main(String[] args){
        Dog dog = new Dog();
        dog.eat();
        dog.makeNoise();
    }
}

실제로 우리는 Mammal 클래스에서 행위를 분리한다.

따라서 상속을 통해 단일 Mammal 엔터티를 만드는 대신에 이전 장에서 취한 전략과 비슷한 합성 기반 설계로 이동한다.

즉, 이 원칙을 사용하면 자연스럽게 합성 기반으로 설계가 이동된다.

이 접근법을 사용하면 단일 Mammal 클래스에 포함된 행위를 강요하지 않고 합성으로 Mammal들을 만들 수 있다.

예를 들어 누군가가 먹지 않고 대신에 피부를 통해 영양분을 흡수하는 Mammal을 발견했다고 가정해보자.

eat()이라는 행위가 포함된 단일 Mammal 클래스에서 상속받으면 새 포유류에는 이 행위가 필요하지 않다.

그러나 모든 행위를 별도의 단일 인터페이스로 분리하면 각 포유동물을 정확하게 제시하는 방식으로 구축할 수 있다.

12.1.5. 의존성 역전 원칙(DIP)

의존성 역전 원칙(Dependency Inversion Principle)은 코드가 추상화에 의존해야 한다고 명시하고 있다.

종종 의존성 역전과 의존성 주입이라는 용어가 서로 교환해서 쓸 수 있는 말처럼 들리겠지만, 이 원칙을 논의할 때 이해해야 할 몇 가지 핵심 용어는 다음과 같다.

  • 의존성 역전: 의존체들을 역전시키는 원칙
  • 의존성 주입: 의존체들을 역전시키는 행위
  • 생성자 주입: 생성자를 통해서 의존성을 주입
  • 파라미터 주입: 메서드의 파라미터를 통해서 의존성을 주입

의존성 역전의 목표는 구상적인 것에 결합하기보다는 추상적인 것에 결합하는 것이다.

어떤 시점에서는 분명히 구상적인 것을 만들어야 하지만, 우리는 main() 메서드에서와 같이 가능한 한 사슬을 멀리 뻗어 구상 객체를 만들려고 노력한다.

생각

가능한 한 사슬을 멀리 뻗어 구상 객체를 만들려고 한다..

우리는 최대한 추상적으로 다뤄야 코드의 유연성이 높아진다는 것을 알지만, 그 시점이 어디쯤인지 몰라서 망설이는 경우도 있을 것 이라고 생각한다.

멀리 뻗어 구상 객체를 만들려고 한다는 말 자체가 앞 장을 읽고 경험을 해보니 이해가 되는 말이라 되게 와닿는다.

의존성 역전 원칙의 목표 중 하나는 컴파일타임이 아니라 런타임에 객체를 선택하는 것이다.

우리는 이전 클래스를 다시 컴파일하지 않고도 새 클래스를 작성할 수도 있다.(새 클래스를 작성해 주입)

1단계: 초기 예제

Mammal클래스의 예제를 다시 살펴본다.

abstract class Mammal{
    public abstract String makeNoise();
}

Cat과 같은 자식 클래스는 상속을 사용해 포유류의 행위인 makeNoise()를 활용한다.

class Cat extends Mammal{
    @Override
    public String makeNoise(){
        return "Meow";
    }
}

class Dog extends Mammal{
    @Override
    public String makeNoise(){
        return "Bark";
    }
}
public class TestMammal{
    public static void main(String[] args){
        Mammal cat = new Cat();
        Mammal dog = new Dog();

        System.out.println(cat.makeNoise());
        System.out.println(dog.makeNoise());
    }
}
2단계: 행위를 분리해 내기

앞의 코드에는 잠재적으로 심각한 결함이 있다.

코드는 포유류와 행위를 연결한다.

포유류의 행위를 포유동물 자체로부터 분리하면 상당한 이점을 얻을 수 있다.

이렇게 하기 위해 우리는 포유류뿐만 아니라 포유류가 아닌 것들도 모두 사용할 수 있는 MakingNoise라는 클래스를 만든다.

abstract class MakingNoise{
    public abstract String makeNoise();
}

class CatNoise extends MakingNoise{
    @Override
    public String makeNoise(){
        return "Meow";
    }
}

Cat 클래스와 분리된 MakingNoise 행위를 사용하면 다음 코드 조각과 같이 Cat 클래스 자체의 하드코딩된 행위 대신에 CatNoise 클래스를 사용할 수 있다.

abstract class Mammal{
    public abstract String makeNoise();
}

class Cat extends Mammal{
    private MakingNoise noise = new CatNoise();

    @Override
    public String makeNoise(){
        return noise.makeNoise();
    }
}

두 번째 단계까지 적용된 전체 애플리케이션이다.

public class TestMammal{
    public static void main(String[] args){
        Mammal cat = new Cat();
        Mammal dog = new Dog();

        System.out.println(cat.makeNoise());
        System.out.println(dog.makeNoise());
    }
} 

문제는 명확하다.

주요 부분은 분리했지만, Cat은 여전히 Cat울음 소리내기 행위를 인스턴스화 하기 때문에 우리는 의존성 역전이라는 목표에 도달하지 못했다는 것이다.

private CatNoise noise = new CatNoise();

Cat은 저수준 모듈인 CatNoise에 결합된다.

다시 말해서 Cat은 CatNoise와 연결되서는 안 되며, 울음 생성을 위한 추상화에 연결되어야 한다.

실제로 Cat 클래스는 울음 생성 동작을 인스턴스화하지 말고 대신 주입을 통해 받아야 한다.

이는 처음 설명한 구상적인 부분이 아닌 런타임에 추상적으로 처리 가능하기 때문에 Cat이 메인이 아니다.

3단계: 의존성 주입

이 마지막 단계에서 우리는 설계의 상속 측면을 완전히 버리고 합성을 통한 의존성 주입을 활용하는 방법을 조사한다.

상속보다는 합성이라는 개념이 탄력을 받는 주요 이유 중 하나는 상속 위계구조가 필요하지 않다는 점이다.

초기 구현엔 Cat과 Dog가 기본적으로 정확히 같은 코드를 포함하고 있다.

서로 다른 울음소리만 돌려줄 뿐이다.

결과적으로는 상당한 중복이 발생한다.

따라서 포유동물이 많으면 울음소리를 유발하는 코드가 많을 것이다.

아마도 더 나은 설계는 포유류가 울음소리를 내도록 코드를 취하는 것이다.

한 단계 나아가서 특정 포유류를 버리고 다음과 같이 간단히 Mammal클래스를 사용하는 것이다.

class Mammal{
    MakingNoise noise;

    public Mammal(MakingNoise noise){
        this.noise = noise;
    }

    public String makeNoise(){
        return noise.makeNoise();
    }
}

이제는 Cat 울음 소리 생성 행위를 인스턴스화하고 이를 Animal 클래스에 제공하여 Cat처럼 행위를 하는 포유류를 만들 수 있다.

실제로, 전통적인 클래스 구축 기술을 사용하는 대신에 행위를 주입하여 언제든 Cat을 조립할 수 있다.

Mammal cat = new Mammal(new CatNoise());

이제는 의존성 주입을 논의할 때 객체를 실제로 인스턴스화하는 시점이 중요한 고려 사항이다.

목표는 주입을 통해 객체를 작성하는 것이지만, 어느 시점에서는 객체를 인스턴스화해야 한다.

결과적으로 설계 결정은 이 인스턴스화를 수행할 시기를 중심으로 이루어진다.

의존성 역전의 목표는 특정 시점에서 구체적으로 무언가를 만들어야 하지만, 무언가 구상적인 게 아닌 추상적인 것에 결합하는 것이다.

따라서 간단한 목표 중 하나는(new 키워드를 사용함으로써) 메서드에서 그러는 것처럼 최대한 멀리까지 이어지게 구상 객체를 만드는 것이다.

new 키워드를 볼 때 마다 언제나 그 대상의 값을 평가하자.

12.2. 결론

SOLID원칙은 오는날 사용되는 가장 영향력 있는 객체지향 지침 중 하나이다.

필자는 SOLID의 가장 흥미로운 점을 상투적인 부분이 없다는 것을 강조하는데 각각의 특성이 캡슐화, 상속, 다형성 그리고 합성까지 연결되는 부분에 집중한다.

특히, 상속 대 합성..

개인적인 생각 + 많은 말들이 그러하듯 정말 좋은 원칙이고 따라야 하는 것은 맞지만 신봉하지는 말자 주의이다.

팀 협업에 있어 다른 인원이 이해하기 쉽고 유지보수가 쉬운 코드를 작성하는 것이 더 중요하다고 생각된다.

물론 이런 원칙에 맞춰 짜여진 코드가 읽기 쉽겠지만 다른 사람의 코드가 그러지 못하다 해서 비난하는게 맞을까?

코드리뷰를 통해 교정은 해야하겠지만 모른다고 무시하는 것은 아닌 것 같다.

최근 친구와 대화에서 코틀린 관련 public에 대한 이야기를 나눴는데 친구는 객체지향 빠돌이에 대한 부정적인 인식이 강했다.

개인적으로 객체지향을 공부하는 입장에선 쉽게 이해가지 않았지만 친구의 자바 롬복 라이브러리, 코틀린의 성격에 대한 이야기를 들어보니 그럴 수 있겠다는 생각이 들었다..

너무 개인적인 이야기까지 책 리뷰에 적은 것 같지만 이번 책은 여기서 마무리.

댓글남기기