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

오브젝트: 코드로 이해하는 객체지향 설계 12장을 읽으며

lgvv 2024. 11. 19. 02:07

오브젝트: 코드로 이해하는 객체지향 설계 12장을 읽으며

 

느낀점

self, super 등 다형성에 점점 더 지식이 늘어가는 것 같다

 

가끔 수동 배포 할 때 ad hoc이란 용어가 있었는데, 임시라는 의미였다니!

 

다형성

코드 재사용을 목적으로 상속을 사용하면 변경하기 어렵고 유연하지 못한 설계에 이를 확률이 높아짐

  • 상속의 목적은 코드 재사용이 아님
  • 상속은 타입 계층을 구조화하기 위해 사용해야 함.
  • 클라이언트 관점에서 인스턴스들을 동일하게 행동하는 그룹으로 묶기 위해서라면 사용하지 말아야 함.
  • 상속의 일차적인 목적은 코드 재사용이 아닌 서브 타입의 구현이라는 사실을 이해할 것

객체지향에서 다형성은 유니버셜 다형성 임시(ad Hoc) 다형성으로 분류할 수 있음

  • 유니버셜 다형성은 매개변수(Parametric) 다형성 포함(Inclusion) 다형성으로 구분할 수 있음
  • 임시 다형성은 오버로딩(overloading) 다형성 강제(Coercion) 다형성으로 구분할 수 있음.
  • 일반적으로 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우를 가리켜 오버로딩 다형성이라고 부름
// ✅ 오버로딩 다형성
class Money { 
   func plus(money: Int) { /*...*/ }
   func plus(money: Double) { /*...*/ }
   func plus(money: Float) { /*...*/ }
}

// ✅ 강제 다형성
let a = 1 + 2 // 결과는 3
let b = String(1) + "2" // 결과는 "12", 이 경우 명시적 타입 변환을 통해 문자열 연결

// ✅ 매개변수 다형성 (제네릭과 연관)
class Money { 
   func plus<T>(money: T) { /*...*/ }
}

// ✅ 포함 다형성 (수신하는 객체에 따라 달라짐)
protocol DiscountPolicy {}
class AmountDiscountPolicy: DiscountPolicy {}
class PercentageDiscountPolicy: DiscountPolicy {}

class Money { 
    private var policy: DiscountPolicy
}

 

상속의 양면성

객체지향 패러다임의 근간을 이루는 아이디어는 행동 데이터를 하나의 실행 단위 안으로 통합하는 것

  • 상속의 메커니즘을 이해하는 것에는 몇가지 개념이 필요
    • 업 캐스팅
    • 동적 메서드 탐색
    • 동적 바인딩
    • self 참조
    • super 참조
  • 부모의 메서드를 자식의 새로운 구현으로 대체하는 것을 메서드 오버라이딩이라고 함
  • 메서드와 이름은 동일하지만 시그니처는 다른 메서드의 자식 클래스에 추가하는 것을 메서드 오버로딩이라고 함
// ✅ 오버라이드, UIViewContoller의 ViewDidLoad와 같음.
class Parent {
    func foo() {
    	print("parent-foo")
    }
}

class Child: Parent {
    override func foo() {
    	print("child-override-foo")
    }
}

// ✅ 오버로딩, 타입만 다름
class Calculator {
    func add(a: Int, b: Int) -> Int {
        return a + b
    }

    func add(a: Double, b: Double) -> Double {
        return a + b
    }
}

let calculator = Calculator()
print(calculator.add(a: 3, b: 5))       // 출력: 8
print(calculator.add(a: 3.5, b: 5.2))   // 출력: 8.7




동적 메서드 탐색과 다형성

객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정하며, 상속 계층의 역발향으로 이뤄지고 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸.

  • self와 this에 대한 간략한 설명
    • 정적 타입 언어에 속하는 C++, JAVA, C#에서는 self참조를 this라고 부름. 동적 타입 언어에 속하는 스몰토크, 루비 언어에서는 self참조를 나타내는 키워드로 self를 사용.
    • 파이썬에서는 self 참조의 이름을 임의로 정할 수 있지만 대부분의 개발자들은 전통을 존중해서 self라는 이름을 사용
  • 자신이 처리할 수 없는 메시지를 받은 경우에는 상속 계층에 따라 부모에게 자동적으로 메시지를 위임
  • 메시지를 수신했을 때 실제로 어떤 메서드를 실행할지를 결정하는 것은  컴파일 시점이 아닌 실행 시점에 이루어 지며, 메서드를 탐색하기 위해 동적인 문맥 사용

 

자동적인 메시지 위임

동적 메서드 탐색 입장에서 상속 계층은 메시지를 수신한 객체가 자신이 이해할 수 없는 메시지를 부모 클래스에게 전달하기 위한 물리적인 경로를 정의하는 것으로 볼 수 있음

  • 일부 언어들은 상속이 아닌 다른 방법을 이용해 메시지를 자동으로 위임할 수 있는 메커니즘을 제공
  • 루비의 모듈(module), 스칼라의 트레이트(trait), 스위프트의 프로토콜(protocol)과 확장(extension)이 있음
  • 상속 계층에 독립적으로 메시지를 위임할 수 있는 장치


정적 타입 언어와 이해할 수 없는 메시지

정적 타입 언어에서는 코드를 컴파일 할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단

  • 따라서 상속 계층 전체를 탐색한 후에도 메시지를 처리할 수 있는 메서드를 발결하지 못했다면 컴파일 에러를 발생


동적 타입 언어와 이해할 수 없는 메시지

동적 타입 언어에서는 부모 클래스의 방향으로 메서드를 탐색

  • 차이점이라면 동적 타입 언어에는 컴파일 단계가 존재하지 ㅇ낳기 때문에 실제로 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없음
  • 이해할 수 없는 메시지를 처리할 수 없는 동적 타입 언어는 순수한 관점에서 객체지향 패러다임을 구현한다고 볼 수 있음
  • 동적 타입 언어는 인터페이스와 메서드가 정의된 구현을 분리할 수 있음
  • 동적 타입 언어의 이러한 동적인 특성과 유연성은 코드를 이해하고 수정하기 어렵게 만들뿐만 아니라 디버깅 과정을 복잡하게 만들기도 함.

이해할 수 없는 메시지와 도메인-특화 언어(Domain-Specific Language, DSL)

  • 동적 타입 언어의 특징은 메타 프로그래밍 영역에서 진가를 발휘
  • 동적 타입 언어의 이러한 특징으로 인해 동적 언어 정적 타입 언어 보다 더 쉽고 강력한 도메인 특화 언어를 개발할 수 있는 것으로 간주
  • 마틴 파울러는 동적 타입 언어의 이용해 도메인-특화 언어를 개발하는 방식을 동적 리셉션(dynamic reception)이라고 부름

 

self 대 super

self 참조의 가장 큰 특징은 동적이라는 점. self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥 시점을 실행 시점에 결정

  • self의 이런 특성과 대비해서 언급할 만한 가치가 있는 것이 바로 super 참조(super reference)
  • super 참조의 문법
    • 대부분의 객체지향 언어는 부모 클래스에서부터 메서드 탐색이 시작하게 하는 super 참조를 위한 의사변수를 제공
    • 부모 클래스의 인스턴스에게 메시지를 전송하는 것처럼 보이기 때문에 super 전송이라고 부름
    • super 참조는 부모 클래스의 코드에 접근할 수 있게 함으로써 중복 코드 제거 가능

 

상속 대 위임

자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 표현

  • 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 표현
  • 다른 객체에서 전달해서 처리를 요청하는 것을 위임(delegation)