Yejun Cheon
Yejun Cheon
log
study
read
coffee
Sign In
배운 것을 정리합니다.

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

예
예준천
카테고리
  1. OOP
  2. Java

1장 객체 설계

5장 책임할당하기

5.3 구현을 통한 검증.

코드로 적어보니 다음과 같은 개선 사항이 존재한다. DiscountCondition은 3가지 이유로 변경될수 있다.
1.
새로운 할인조건 추가로 인한 isSatisfiedBy 메소드의 조건 추가
2.
isSatisfiedBySequence의 로직 변경
3.
isSatistfiedByPeriod의 로직 변경
하나의 클래스가 여러가지 변경이유를 가진다는 것은 단일 책임의 원칙(SRP)의 위배되며 높은응집도를 달성하지 못했다고 볼수 있다. 그러므로 변경의 이유에 따라 클래스를 분리하자.
변경의 이유를 쉽게 파악하기 꿀팁!
•
인스턴스 변수의 초기화 시점이 다른 부분을 분리의 후보로 잡아보자.
•
인스턴스 변수 사용의 방식을 살펴보자. 모든 속성을 다 사용하면 응집도가 높은거다!
이렇게 나눈다면 PeriodCondition과 SequenceCondition두개의 클래스가 나뉘지만, Movie는 두가지 클래스와 협력해야한다. 이건 전체적인 결합도도 높아지고(Movie가 Condition 1개에서 각각의 2개로 결합이 늘어남),변경이 더 어려워져서(새로운 할인조건 추가시 List 인스턴스 추가해야하고 판별 메서드도 추가해야함) 설계 품질이 나빠진거다.
Movie 입장에서는 두 클래스 모두 같은 책임을 수행한다. 구현방식의 차이일 뿐이므로 ‘역할’이 등장한다.
자바에서는 이를 구현하기위해 추상클래스(구현을 공유)와 인터페이스(책임만 정의)를 사용한다.
💡
POLYMORPHISM 패턴
객체에 타입에 따라 변하는 로직이 있다면 조건문보다는 다형성을 이용하자.
💡
PROTECTED VARIATIONS 패턴
변경될 가능성이 높다면 캡슐화해서 예측가능한 안정적인 인터페이스 뒤로 숨기자.
도메인의 구조가 코드의 구조를 이끈다. 그러므로 구현을 가이드할 수 있는 도메인 모델을 선택하자.

5.4장 책임주도설계의 대안. 절차지향 후 리팩터링

몬스터메서드
•
엄청 많은 작업을 수행함.
•
재사용불가능. 재사용 원할시 복붙이므로 중복을 초래.
•
변경 필요시 수정 할 부분 찾기 어려움.
→ 응집도가 낮고, 이해 어렵고, 재사용 어렵고, 변경도 어려움
STEP1 : 응집도 높게 메서드 분해
STEP2 : 자신이 소유하는 데이터를 스스로 처리할 수 있도록 메서드 이동
STEP3: POLYMORPHISM,PROTECTED VARIATIONS 패턴들을 적용

9장 유연한 설계

9.1 OCP

확장에 열려있다 : 요구사항의 변경에 동작을 추가하여 기능을 확장할 수 있다.
수정에 닫혀있다 : 기존의 코드의 수정 없이 위가 가능하다.
이걸 가능하게 하는건, 컴파일타임 의존성을 고정 & 런타임 의존성 변경이다.
위를 가능하게 하는건 결국 추상화에 의존하게 하는 것이다. (추상화 된 부분은 수정에 닫혀있어야 함)

9.2 생성사용분리

객체가 생성되는 곳과, 사용되는 곳을 분리시켜라. 생성은 결국 컴파일타임 의존성을 하위 타입으로 고정시키는 단계이다. 그러니까 이게 객체가 사용되는 곳에 위치한다면, 추상화되어 수정에 닫혀있어야 할 부분이 오염되는 꼴이다.
만약, 도메인 개념과 무관하게 생성-사용 시점을 분리하게 된다면 Factory라고 명명하자. (도메인 개념으로 해결할 수 없는 객체분해와 책임할당은 순수한 인공 객체를 만들어 해결하자는 Pure Fabrication 패턴에 따른다.)

9.3 의존성 주입

생성이 분리된 곳에는 오로지 사용의 책임만이 남는다. 누가 인스턴스를 넣어줄까? 의존성주입은 외부의 독립적인 객체가 인스턴스를 생성후 이를 전달하는 방법이다. 의존성 주입에는 constructor injection, setter injection, method injection 3가지방법이 있다.
다른 방식으로는 SERVICE LOCATOR 패턴이 있는데, 이건 (깊은 호출 계층에 drilling 하며 전달, 로직과 무관한 시스템 객체)와 같은 특수상황을 제외하면 사용하지 않는게 좋다. 숨겨진 의존성이기 때문이다. 캡슐화를 위반하고, 런타임 에러를 발생시켜 디버깅을 어렵게 만든다.

9.4 의존성 역전원칙

상위수준의 모듈은 하위수준의 모듈에 의존해서는 안된다.
의존성의 방향
구체(하위수준 모듈) → 추상화
상위수준 모듈 → 추상화

9.5 케바케

유연한설계 ↔ 복잡성 은 트레이드오프이다.

10장 상속과 코드재사용

10.1 중복코드

DRY Principle : Don’t Repeat Yourself
중복코드는 항상 함께 수정되어야하기에 위험하다.
Type 코드를 활용해서 로직을 분기시켜 중복코드를 합칠 수도 있지만, 이건 낮은 응집도와 높은 결합도 문제를 야기한다.
그렇다면 원클래스를 상속받아서 오버라이딩하고, 중복되는 부분을 super.x()로 합칠 수도 있다. 이건 너무 강한 결합을 만들어 수정이 어려워진다.

10.2장 취약한 기반 클래스 문제 Fragile Base Class Problem

문제점 : 결합도가 너무 높아진다. 부모클래스의 수정이 자식클래스의 수정으로 이어진다.
예시 1. 일반 Phone 을 상속받은 NightlyDiscountPhone
예시 2. Vector를 상속받은 Stack. add메소드와 같은 불필요한 퍼블릭 인터페이스를 상속받는다.
문제점 : 메소드 오버라이딩은 부작용을 가진다.
self-use가 가능한 메서드는 파급효과를 분명히 하기 위해, 무엇을 이 아니라 어떻게에 대한 내부 구현을 문서화 해야한다.
문제점 : 동시수정해야함.
부모클래스에 변경사항 발생시 자식클래스도 변경해야함.

10.3장 구현이 아닌 추상화에 의존하게 바꾸자.

이를 위한 tip 원칙
1.
두 메서드가 유사해보인다면, 차이점을 추출해라. 이러면 동일한형태의 메서드 두개로 만들 수 있음.
2.
자식 코드를 상위로 올려라.
method 1 : a b c d → a x() c d + x() = b
method 2 : a e c d →a x() c d + x() = e
동일한 a x() c d 를 상위 추상화로 묶어 올린다. 이 추상화 인터페이스에서는 x()는 구현하지 않고 자식클래스에 구현을 강제한다. 각 클래스는 자신에 맞는 x()를 구현한다.

11장 합성과 유연한 설계

11.1장 상속을 합성으로 변경하기

1.
불필요한 인터페이스 상속 문제
has a 관계로 바뀌게 된다면, 내가 직접 메서드로 공개하지 않는다면 합성관계 클래스의 인터페이스가 외부로 노출되지 않는다.
2.
메소드 오버라이딩의 오작용 문제
super 호출시 부모클래스가 자신의 클래스를 사용하는 방법에 대해 알아야하는 문제가 있었다. 합성을 사용한다면, super가 아닌 명시적인 클래스의 메서드를 호출하여 사이드 이펙트를 줄일 수 있다.
3.
부모클래스 - 자식클래스 동시수정 문제
이건 합성으로도 고치지 못한다. 그래도 파급효과를 합성 주체 클래스 내부로 캡슐화하여 퍼블릭인터페이스가 오염되는 경우는 없어 몽키패치가 가능하게 한다.

11.2장 상속으로 인한 조합 폭발 (Class Explosion)

하나의 기능을 추가하기 위해, 수많은 클래스를 추가해야하는 상황.
자바에서는 단일상속만을 지원하기 때문에, 상의 3벌(trait1) 하의 3벌(trait2)이라면, 총 9개의 착장(구현클래스)가 생기게 된다. 9개에는 중복코드도 엄청 많고, 새로운 특성의 추가도 쉽지 않다.

11.3장 합성관계로 변경

Phone은 추상화된 가격 정책 인터페이스 RatePolicy를 필드로 가진다(has a). RatePolicy는 기본정책(BasicRatePolicy)와 부가정책(AdditionalRatePolicy) 추상클래스로 나뉜다. 부가정책은 기본정책에 추가적으로 가격 수정이 일어나는 구조이기에, 이전 정책을 필드로 가지고(has a) 이 정책의 계산 결과에 추가적인 가격 결정 로직을 afterCalculated()에 정의한다.
아무리 한 가격 정책에 포함된 부가정책이 많아지더라도, nextPolicy(이것도 추상화된 RatePolicy 인터페이스)에 이전에 계산될 정책들을 소유한 구조는 문제없이 유지된다. 이를 바탕으로 Linear한 가격 계산로직이 만들어지게 된다.
내가 이 예제를 보며 배워야할 테크닉은 다음과 같았다.
1.
Phone과 가격정책을 분리한다.
2.
순서가 상관있는 정책시퀀스를 linked List 느낌으로 구현.
3.
부가 정책 다음 정책이 부가정책일 수도 있고, 일반정책이 올수도 있게 하는 RatePolicy로 추상화되어있다.

12장 다형성

12.1 다형성의 종류

Coercion 강제 : 언어에 의한 자동 타입변환
Overloading : 동일 클래스 내에 동일한 이름, 서로 다른 타입의 파라미터
Parametric 매개변수 : Generic을 활용한 다형성
Inclusion 포함 : 메시지가 동일하더라도 수신한 객체 타입에 따라 행동이 달라짐. (SubType Polymorphism)
상속의 진정한 목적은 코드 재사용이 아니라 Inclusion Polymorphism을 구현하기 위해서임.

12.2 상속의 양면성

데이터 관점의 상속 : 자식클래스의 인스턴스 안에 부모클래스의 인스턴스를 포함한다.
행동관점의 상속 : 부모클래스의 일부 메서드를 자식클래스로 포함시킴.
객체는 데이터는 인스턴스별로 독립적인 메모리를 할당받지만, 메서드는 한번만 로드해두고 해당 메모리 포인터를 가진다. 상속을 하게 된다면, 상위 객체 메서드 메모리에 대한 포인터도 가지게 된다.

12.3 업캐스팅과 동적 바인딩

업캐스팅 : 부모 클래스 타입으로 선언된 변수에 자식클래스 인스턴스 할당 가능
dynamic Binding : 선언된 변수 타입이 아닌 메시지 수신 객체 타입에 따라 실행되는 메서드가 결정됨. 컴파일 타임이 아닌 런타임에 동적으로 바인딩.
이 두가지는 OCP를 가능케함.

12.4 동적 메소드 탐색과 다형성

상속계층을 따라 이동하는 메서드 탐색 체인. 자동적인 메시지 위임이 이루어짐.
객체지향의 실행 메서드 선택 기준
1.
메시지 수신 객체 내부
2.
부모클래스, 없다면 그 부모클래스
3.
최상위에도 없다면 예외 발생 & 탐색종료

self참조 vs super 참조

메서드는 실행될때 실행되는 문맥(self)이 소속 클래스로 고정되어있지 않고 동적으로 바뀐다.
메시지를 수신한 인스턴스는 GradeLecture이기 때문에 stats()가 Lecture에 정의되어 있더라도 stats 메소드가 실행되는 시점에서 문맥 self는 GradeLecture이다.
반면 super 참조는 소속클래스의 항상 부모클래스를 가리키게 된다. super를 활용해 부모클래스에 메시지를 전달 한다는 건 메서드 실행보다 더 넓은 의미인 메서드 탐색을 시작한다는 뜻을 가진다.

12.5 상속 vs 위임

상속 : 자식클래스에서 부모클래스로 self 참조를 전달하는 매커니즘.
class Lecture
	def initialize(name, scores)
		@name=name
		@scores - scores
	end
	
	def stats(this)
		this.getEvaluationMethod()
	end
	
	def getEvaluationMethod()
		"PF"
	end
end




class GradeLecture
	def initialize(name, canceld, scores)
		@parent = Lecture.new(name, scores)
		@canceld= canceld
	end
	
	def stats(this)
		@parent.stats(this)
	end
	
	def getEvaluationMethod()
		"Grade"
	end
end
→ 1. 자식클래스에서 명시적인 부모 클래스 링크 저장. (동적 메서드 탐색 메커니즘 흉내)
→ 2. 자식클래스의 stats는 아무런 작업 없이 단순히 부모 클래스로 요청을 전달(위임)
→ 3. getEvaluationMethod는 부모 메서드 오버라이딩함. 외부에서는 이게 먼저보임.
→ 4. stats를 위임할 때 문맥(this)도 함께 보내줌. 그래서 GradeLecture의 getEvaluationMethod가 실행됨.
위임 : 자신이 수신한 메시지를 다른 객체에 동일하게 전달해 처리를 요청하는 것. 이때 문맥을 같이 전달함.
포워딩 : 단순 메서드 호출.(코드 재사용)

프로토타입기반의 객체지향언어

몰겄음

13장 서브클래싱과 서븥타이핑

13.1 타입

Symbol : name of type
Intension : meaning
Extension : 객체의 집합
타입의 목적.
1.
수행 가능한 유효 오퍼레이션의 집합을 정의 → 객체가 수신가능한 메시지의 집합 (퍼블릭 인터페이스)
2.
오퍼레이션에 미리 약속된 문맥을 제공함. →

13.3 서브클래싱

언제 상속을 사용해야하는가
1.
상속관계가 is-a 관계를 모델링하는가([자식]은 [부모]이다.)
2.
클라이언트 입장에서 부모클래스의 타입으로 자식클래스를 사용해도 무방한가(행동호환성)
Ex) 펭귄은 새이다. 새는 날수 있다. → 의미적인 Is-a 관계의 문제점을 보여줌.행동적 관점에서 가 중요함.
클라이언트는 fly가 동작할 것이라 기대하고 메시지를 전송할것.
1.
빈 오버라이딩 메서드 구현 → 아무 일x
2.
예외 던지기 → 이럴 줄 몰랐을 것.
3.
클라이언트에서 펭귄이 아닐 때만 fly 던지기. → OCP 위반
1.
계층분리를 통한 기대 분리. Bird를 상속받는 FlyingBird에만 fly 퍼블릭 인터페이스를 생성. → Good
2.
클라이언트에 따른 인터페이스 분리.
이때 만약 Bird의 코드 재사용을 원한다면 상속보다는(Flyer 인터페이스도 넘어와서 오염시킴) 합성을 활용하자.
인터페이스 분리 원칙 (ISP)
💡
비대한 클래스는 클라이언트 사이에 이상하고 해로운 결합이 생기게 만든다. 그러므로 클라이언트는 자신이 실제로 호출하는 메서드에만 의존해야한다. 이건 비대한 클래스의 인터페이스를 여러개의 클라이언트에 특화된 인터페이스들로 분리함으로써 성취될 수 있다. 이렇게하면 호출하지 않는 메서드에 대한 클라이언트의 의존성을 끊고 서로에 대해 독립적이 되게 만들 수 있다.
단 자연어에 현혹되어 불필요한 복잡도를 높이지는 말자. 내가 본뜨려고 하는 세계가 나는 새와 날지 않는 새를 구별해야하는 세계인지 다시 생각해보자.
서브 클래싱 : 클래스 상속 / 구현 상속 → 코드 재사용이 목적
클래스의 내부 구현자체를 상속받는것에 초점을 맞춤
서브 타이핑 : 인터페이스 상속 → 타입계층을 구성.
Behavioral substitution이 만족시킴.

13.4 리스코프 치환원칙

💡
S형의 각 객체 o1에 대해T 형의 객체 o2가 하나 있고,
모든 T가 S로 치환될때, 동작이 변하지 않는다면, S는 T의 서브타입이다.
⇒ 서브타입은 그것의 기반타입에 대해 대체 가능해야한다.
Yejun Cheon
Subscribe to 'Yejun Cheon'
Subscribe to my site to be the first to receive notifications and emails about the latest updates, including new posts.
Join Slashpage and subscribe to 'Yejun Cheon'!
Subscribe
👍