Taehyeon-Kim / OOP-IN-SWIFT

Object Oriented Programming
Apache License 2.0
6 stars 0 forks source link

[taekki] 객체지향의 사실과 오해 3 · 4장 #5

Open Taehyeon-Kim opened 2 years ago

Taehyeon-Kim commented 2 years ago
Taehyeon-Kim commented 2 years ago
loc sentence
75p 해리 벡이 창조한 지하철 노선도의 핵심은 지도가 당연히 가져야 한다고 생각되는 '정확성'을 버리고 그 '목적'에 집중한 결과다. (중략) "이 지도는 상식에 근거한 것입니다. 지하철을 갈아탈 때 지형 때문에 골치 아플 필요가 있을까요? 지형은 중요한 것이 아닙니다. 중요한 것은 연결, 즉 열차를 갈아타는 것입니다.
76p 해리 백의 지하철 노선도를 통해 알 수 있는 것처럼 훌륭한 추상화는 목적에 부합하는 것이어야 한다.
77p 추상화의 첫 번째 차원은 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 단순하게 만드는 것이다. 두 번째 차원은 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거함으로써 단순하게 만드는 것이다.
83p 공통점을 기반으로 객체들을 묶기 위한 그릇을 개념이라고 한다.
87p 분류는 객체지향의 가장 중요한 개념 중 하나다. 어떤 객체를 어떤 개념으로 분류할지가 객체지향의 품질을 결정한다. 객체를 적절한 개념에 따라 분류하지 못한 애플리케이션은 유지보수가 어렵고 변화에 쉽게 대처하지 못한다.
90p 컴퓨터 안에 살아가는 데이터를 목적에 따라 분류하기 시작하면서 프로그래밍 언어 안에는 서서히 타입 시스템이 자라나기 시작했다. 타입 시스템의 목적은 메모리 안의 모든 데이터가 비트열로 보임으로써 야기되는 혼란을 방지하는 것이다.
92p 그렇다면 객체는 데이터인가? 그렇지 않다. 다시 한번 강조하지만 객체에서 중요한 것은 객체의 행동이다.
95p 객체를 결정하는 것은 행동이다. 데이터는 단지 행동을 따를 뿐이다. 이것이 객체를 객체답게 만드는 가장 핵심적인 원칙이다.
98p 객체지향에서 일반화/특수화 관계를 결정하는 것은 객체의 상태를 표현하는 데이터가 아니라 행동이라는 것이다.
101p 왜 타입을 사용해야 하는가? 객체지향은 객체를 지향하는 것이므로 객체만 다루면 되지 않는가? 타입을 사용하는 이유는 인간의 인지 능력으로는 시간에 따라 동적으로 변하는 객체의 복잡성을 극복하기가 너무 어렵기 때문이다.
102p 그림 3.12
103p 타입은 추상화다. 타입을 이용하면 객체의 동적인 특성을 추상화할 수 있다. 결국 타입은 시간에 따른 객체의 상태 변경이라는 복잡성을 단순화할 수 있는 효과적인 방법인 것이다.
105p 지금은 객체를 분류하는 기준은 타입이며, 타입을 나누는 기준은 객체가 수행하는 행동이라는 사실만을 기억하기 바란다. 객체를 분류하기 위해 타입을 결정한 후 프로그래밍 언어를 이용해 타입을 구현할 수 있는 한 가지 방법이 클래스라는 사실을 아는 것만으로도 충분하다.
loc sentence
109p 개별적인 객체의 행동이나 상태가 아니라 객체들 간의 협력에 집중하라.
110p 결과적으로 협력은 다수의 요청과 응답으로 구성되며 전체적으로 협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성된다.
115p 책임을 어떻게 구현할 것인가 하는 문제는 객체와 책임이 제자리를 잡은 후에 고려해도 늦지 않다.
115p 객체의 책임은 '객체가 무엇을 알고 있는가'와 '무엇을 할 수 있는가'로 구성된다.
117p 책임은 객체지향 설계의 품질을 결정하는 가장 중요한 요소다. (중략) 객체의 책임을 이야기할 때는 일반적으로 외부에서 접근 가능한 공용 서비스의 관점에서 이야기한다. 즉, 책임은 객체의 외부에 제공해 줄 수 있는 정보(아는 것의 측면)와 외부에 제공해 줄 수 있는 서비스(하는 것의 측면)의 목록이다. 따라서 책임은 객체의 공용 인터페이스(public interface)를 구성한다. 공용 인터페이스의 개념은 뒤에서 다룰 객체지향의 중요한 원리 중 하나인 캡슐화로 이어진다.
119p 객체지향 설계는 협력에 참여하기 위해 어떤 객체가 어떤 책임을 수행해야 하고 어떤 객체로부터 메시지를 수신할 것인지를 결정하는 것부터 시작된다. 어떤 클래스가 필요하고 어떤 메서드를 포함해야 하는지를 결정하는 것은 책임과 메시지에 대한 대략적인 윤곽을 잡은 후에 시작해도 늦지 않다.
119p 역할이 재사용 가능하고 유연한 객체지향 설계를 낳는 매우 중요한 구성요소이기 때문이다.
125p 그림 4.7
126p 1~2문단, 역할의 개념과 그 중요성에 대해 설명하고 있음.
127p 객체는 역할이 암시하는 책임보다 더 많은 책임을 가질 수 있다.
128p 객체가 상태의 일부로 데이터를 포함하는 것은 사실이지만 데이터는 단지 객체가 행위를 수행하는 데 필요한 재료일 뿐이다. 객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서다.
129p 그것은 바로 객체를 섬으로 바라보던 잘못된 눈길을 거두고 올바른 곳을 바라보는 것이다. - 그 동안 하나의 뷰를 만들고, 하나의 기능을 만드는데에만 집중하던 자신을 반성하게 되는 문장이다.
129p 협력이라는 문맥에서 객체가 수행하게 될 적절한 책임, 즉 행동을 결정한 후에 그 행동을 수행하는 데 필요한 데이터를 고민해야 한다. 그리고 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 어느 정도 결정된 후에 클래스의 구현 방법을 결정해야 한다.
137p 테스트-주도 개발은 객체지향에 대한 깊이 있는 지식을 요구한다. 테스트를 작성하기 위해 객체의 메서드를 호출하고 반환값을 검증하는 것은 순간적으로 객체가 수행해야 하는 책임에 관해 생각한 것이다. 테스트에 필요한 간접 입력 값을 제공하기 위해 스텁(stub)을 추가하거나 간접 출력 값을 검증하기 위해 목 객체(mock object)를 사용하는 것은 객체와 협력해야 하는 협력자에 관해 고민한 결과를 코드로 표현한 것이다.
Taehyeon-Kim commented 2 years ago

추상화, Zedd0202

타입을 정의한다는 것은 개념화의 다른 말이고 또한 추상화이다. 이를 구현하는 수단 중 하나가 클래스(Class)이고, Swift에서는 Class, Struct, Protocol, Enum이 될 수 있을 것 같다. 예를 들어 개발자를 하나의 타입으로 묶어보자. 아래와 같은 느낌이지 않을까 싶다.

현실 세계에는 iOS, Android, Java, 어셈블리 개발자 등 여러 분야의 개발자가 있을텐데 세부사항은 모두 덜어내고 개발을 한다는 특성만 뽑아서 일반화, 추상화를 해볼 수 있다. (어떤 개발을 하는지는 중요하지 않는다. 개발의 역할을 할 수 있다는 것이 중요하다.)

class Developer {}
struct Developer {}
protocol Developer {}
Taehyeon-Kim commented 2 years ago

Dev: 객체지향, 어떻게 설계해야 할까, Heechan

글이 굉장히 이해하기 쉽게 잘 쓰여져 있다. 몇 번 다시 읽는 것을 추천한다.

현재는 글의 일부를 그대로 발췌한 상태입니다. 이후에 나의 용어로 정리하기

클래스를 만드는데 몰두해 클래스를 무턱대고 만들어두고, 이를 중심으로 만드는 것은 확장하기 어려운 구조를 만들어낼 가능성이 높다. 잘못하면 God Object 같이 비대한 크기, 무거운 책임을 가진 객체를 만들어 내 한번 수정하면 cost가 상당해지는 문제가 발생할 수도 있다.

깔끔한 협력과 메시지를 전달하기 위해서 추상화를 활용할 수 있다.

...

추상화에 대해 보다보면, 외부와 협력에 필요한 요소만을 잘 뽑아내 제공하면 객체 간의 협력 관계를 더 깔끔하게 만들 수 있지 않을까 하는 생각이 든다. 위 사진에서 본 것 같은 개발자라는 명찰, 타입 종류만 보여주고 여기엔 이런 기능이 있다는 것을 알 수 있게 한 후, 세부적인 구현은 클래스 내에서 하는거다.

...

이는 인터페이스라는 개념으로 이어진다.

...

그런 것처럼 우리는 객체의 기능도 인터페이스로 제공할 수 있다. 객체가 다른 객체와 상호작용할 수 있는 방법은 메세지 뿐으로 해야 하기 때문에, 객체의 인터페이스에는 어떤 메세지를 수신할 수 있는지, 그 목록을 담고 있어야 한다.

protocol Developable {
  func develop() -> Bool
}

class iOSDeveloper: Developable {
  func develop() -> Bool {
    /* ...develop something... */
    if result != nil {
      return true
    }
    return false
  }
}

class webDeveloper: Developable {
  func develop() -> Bool {
    /* ...develop something... */
    if result != nil {
      return true
    }
    return false
  }
}
let person1 = iOSDeveloper()
let person2 = webDeveloper()

func process(developer: Developable) -> Bool {
  /* ... */
  return developer.develop()
}

process(developer: person1)
process(developer: person2)

여기서 (객체는 아니지만) 함수 process 는 받아오는 인자가 Developable을 채택한 객체임을 알고 있고, 그렇기에 대상이 iOS 개발자인지, Web 개발자인지 신경 쓸 필요없이 developer.develop() 메세지를 사용할 수 있다.

이렇게 예시를 통해 인터페이스가 어떻게 객체 간의 협력을 더 좋게 만드는지 확인할 수 있었다.

...

그래서 객체가 스스로의 책임을 스스로가 다 하며, 외부와 내부 구현을 완전히 분리하는 것이 상당히 중요한 과제다. 이런 과정에서 대두되는 특징이 캡슐화다.

class iOSDeveloper: Developable {
  private func 코드짜기() {
    /* ... */
  }

  private func 테스트하기() {
    /* ... */
  }

  private func 빌드하기() {
    /* ... */
  }

  func develop() -> Bool {
    코드짜기()
    테스트하기()
    코드짜기()
    테스트하기()
    빌드하기()
    /* ... */
  }
}

이런 식으로 만들어진다고 생각하자. iOS 개발자는 원하는 개발을 해내기 위해, 객체 내에서 자율적으로 메서드들을 선택한다. 코드 짜고, 테스트 해보고, 코드를 다시 짜고, 테스트 후 빌드까지 한다.

이런 과정과 내부 메서드들은 외부에서 알 수가 없다. 외부에서는 Developable이라는 인터페이스를 통해 공개된 기능에 대해서만 알 수 있다.

만약 내부에 숨겨진 메서드들이 변경되어도 상관없다. 결국 다른 객체에서 원하는 develop 의 결과값만 이상하게 나오지 않도록 객체 자신 내에서 코드를 수정해주면 된다. 다른 객체의 어디가 영향 받는지, 그에 맞춰 어떻게 바꿔야 할지 골머리 앓을 이유가 없어진다.

...

이처럼 인터페이스 기능을 이용해 외부에서 내부 구현을 알 수 없도록 캡슐화까지 진행할 수 있으며, 이런 특징들이 모두 메세지를 통한 객체 사이의 협력 관계를 이상적으로 만든다.

Taehyeon-Kim commented 2 years ago

나중에 읽으려고 찾은 링크