객체, 설계
객체지향 프로그래밍
역할, 책임, 협력
설계 품질과 트레이드 오프
책임 할당하기
메시지와 인터페이스
객체 분해
의존성 관리하기
유연한 설계
상속과 코드 재사용
합성과 유연한 설계
다형성
서브클래싱과 서브타이핑
일관성 있는 협력
디자인 패턴과 프레임워크
https://github.com/ghkdgus29/object-code/tree/main/chapter1/bad
나쁜 설계 구현 (
bad 패키지
)
bad 패키지
는 빨간색 부분을 만족시키지 못한다.
이해가 어렵다.
동작이 우리의 예상을 크게 벗어난다.
여러 세부적인 내용을 기억해야 한다.
get
매서드를 체이닝하기 위해서 각 객체들이 어떤 내부필드를 갖는 지 알아야 한다.Ticket ticket = ticketSeller.getTicketOffice().getTicket();
Theater
가 너무 많은 세부정보를 알고 있다.Theater
의 코드도 변경해야 한다.bad 패키지
클래스 다이어그램A 객체
가 B 객체
의 세부정보를 알고있을 때,
B 객체
세부정보 변경에 의해 A 객체
코드도 변경해야 한다면,
A 객체
는 B 객체
에 의존성이 있다고 표현
bad 패키지
의 Theater
클래스는 Audience
, Bag
, TicketOffice
, Ticket
에 의존하고 있다.
Theater
의 너무 많은 의존관계들을 끊어낸다.
각 객체들이 스스로 문제를 해결할 수 있는 자율적인 존재로 만든다.
https://github.com/ghkdgus29/object-code/tree/main/chapter1/good
좋은 설계 구현 (
good 패키지
)
public class Theater {
private TicketSeller ticketSeller;
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
Theater
는TicketSeller
의 자세한 구현은 알지 못한다.TicketSeller
는enter(Audience audience)
라는 인터페이스만을Theater
에 제공한다.TicketSeller
의 내부는 캡슐화되었다.
밀접하게 연관된 작업만을 수행하고
연관성 없는 작업은 다른 객체에게 위임하는 객체를
응집도가 높다고 말한다.
자신의 데이터를 스스로 처리하는 자율적인 객체 ➔ 응집도 ⬆ , 결합도 ⬇
절차적 프로그래밍 (Procedural Programming)
프로세스와 데이터를 별도의 모듈에 위치시키는 방식
bad 패키지
에서
Theater
는 작업을 처리하는 프로세스그 외 클래스 들
작업에 필요한 데이터프로세스가 모든 데이터에 의존한다.
Theater
에 집중객체지향 프로그래밍 (Object-Oriented Programming)
프로세스와 데이터를 동일한 모듈에 위치시키는 방식
good 패키지
에서
의존성이 적절히 관리된다.
Theater
➔ TicketSeller
TicketSeller
➔ Audience
객체에 적절한 책임을 할당해야 한다.
쉽게 변경할 수 있는 설계가 중요하다.
https://github.com/ghkdgus29/object-code/tree/main/chapter2/good
- Chapter 2 예제 구현
어떤 클래스가 필요한 지 고민하기 전에 어떤 객체들이 필요한지 고민한다.
객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다.
객체는 상태와 행동을 가지는 복합적인 존재
객체는 스스로 판단하고 행동하는 자율적인 존재
인터페이스와 구현의 분리
퍼블릭 인터페이스
public
)구현
private
, protected
)객체의 상태는 숨기고 행동만 외부에 공개해야 한다.
시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지 전송
Movie
가 추상 클래스인 DiscountPolicy
에 의존컴파일 시간 의존성
Movie
인스턴스를 생성할 때 넣어준 구체적인 클래스(인자) 에 의존 Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800), // DiscountPolicy 가 아닌 AmountDiscountPolicy 에 의존
new SequenceCondition(1),
new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59))));
- 고정할인 적용
Movie titanic = new Movie("타이타닉",
Duration.ofMinutes(180),
Money.wons(11000),
new PercentDiscountPolicy(0.1, // DiscountPolicy 가 아닌 PercentDiscountPolicy 에 의존
new PeriodCondition(DayOfWeek.TUESDAY, LocalTime.of(14, 0), LocalTime.of(16, 59)),
new SequenceCondition(2)));
- 비율할인 적용
DiscountPolicy
를 상속한다면,Movie
의 코드 수정은 발생하지 않는다.인터페이스 : 객체가 이해할 수 있는 메시지 목록
부모 클래스의 모든 인터페이스를 자식 클래스가 물려 받는다.
자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지 수신 가능
메시지 전송 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. (업캐스팅)
즉, 메시지 전송 객체는 메시지 수신 객체가 어떤 인스턴스인지 관심이 없다.
내가 보내는 메시지를 이해할 수 있는지만 관심이 있다.
메시지 전송 객체는 동일한 메시지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지 수신 객체의 클래스가 무엇이냐에 따라 달라지는 것
컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있기 때문에 발생
다형적인 협력에 참여하는 객체들은 인터페이스가 동일해야 한다.
객체지향은 메시지와 메서드를 실행시점에 바인딩(Dynamic binding) 하기 때문에 다형성을 구현할 수 있다.
구현 상속 (subclassing)
인터페이스 상속 (subtyping)
복잡한 자료, 모듈로 부터 핵심적인 개념, 기능을 간추려 내는 것
상위 클래스는 하위 클래스보다 추상적
장점
합성 : 다른 객체 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용
코드 재사용을 위한 상속의 단점
캡슐화 위반
자식 클래스는, 부모 클래스의 내부 구조를 잘 알아야 한다.
부모 클래스의 구현이 자식 클래스에게 노출된다.
부모 클래스의 변경은 자식 클래스의 변경을 낳는다.
설계의 유연성 ⬇
상속은 컴파일 시점에 부모 클래스와 자식 클래스가 강하게 결합한다.
따라서 실행 시점에는 객체 종류를 변경할 수 없다.
코드 재사용을 위한 합성
인터페이스를 통해 약하게 결합한다.
캡슐화
설계의 유연성 ⬆
객체들이 애플리케이션 기능을 구현하기 위해 수행하는 상호작용
오로지 메시지 전송만을 사용해 객체간 협력할 수 있다.
협력이 객체의 행동을 결정한다.
협력에 참여하기 위해 객체가 수행하는 행동
객체의 책임은 2가지로 나뉜다.
책임을 수행하는 데 필요한 정보를 가장 잘 알고있는 정보전문가에게 책임을 할당해야 한다.
시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
시스템 책임을 더 작은 책임으로 분할
분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당
객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우
메시지를 먼저 정하고, 처리할 객체를 나중에 선택한다.
행동이 상태를 결정한다.
특정한 협력안에서의 책임의 집합
연극으로 치면 로미오는 역할이고, 로미오를 연기하는 배우들은 구체적인 객체와 같다.
즉, 역할은 구체적인 객체들을 포괄하는 추상화
https://github.com/ghkdgus29/object-code/tree/main/chapter4/bad
데이터 중심 설계
- 데이터 중심의 설계는 변경에 취약
- 너무 이른 시기에 데이터에 관해 결정한다.
- 협력이라는 문맥을 고려하지 않고, 객체를 고립시킨 채 오퍼레이션을 결정한다.
데이터 (상태) 중심 객체 분할
객체는 자신의 데이터를 조작하기 위한 오퍼레이션을 정의
객체의 상태에 집중
퍼블릭 인터페이스에 데이터와 관련된 세부사항이 노출되기 쉽다.
getter
, setter
책임 중심 객체 분할
다른 객체가 요청하는 오퍼레이션을 처리하기 위한 데이터 보관
객체의 행동에 집중
책임을 드러내는 안정적인 퍼블릭 인터페이스
외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화 (추상화)
불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 숨기는 것
변경의 영향을 통제한다.
응집도
모듈 내의 요소들이 연관되어 있는 정도
모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 응집도 ⬆
모듈 내의 요소들이 서로 다른 목적을 추구한다면 응집도 ⬇
결합도
어떤 모듈이 다른 모듈에 대해 얼마나 의존하는지에 대한 척도
어떤 모듈이 다른 모듈에 대해 너무 자세히 알고 있다면 두 모듈의 결합도 ⬆
어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면, 두 모듈은 결합도 ⬇
어떤 모듈의 내부구현 변경이 다른 모듈에 영향을 미치는 경우에도 결합도 ⬆
높은 응집도와 낮은 결합도
캡슐화를 지키면 모듈의 높은 응집도와 낮은 결합도는 따라온다.
따라서 캡슐화에 집중하라.
Single Responsibility Principle
클래스는 단 한가지의 변경 이유만을 가져야 한다.
서로 다른 이유로 변경되는 코드가 하나의 클래스안에 공존해선 안된다. (낮은 응집도)
단순히 객체 내부의 데이터를 감추는 것은 데이터 캡슐화
로 캡슐화의 한 종류이다.
진정한 캡슐화란 변할 수 있는 건 어떤 것이라도 감추는 것을 의미한다.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() {
return type;
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
// ...
}
public boolean isDiscountable(int sequence) {
// ...
}
}
getType()
➔DiscountConditionType
을 내부에 포함하고 있음을 노출isDiscountable()
➔DayOfWeek
,LocalTime
,sequence 정보
를 내부에 포함하고 있음을 노출
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public MovieType getMovieType() {
return movieType;
}
public Money calculateAmountDiscountedFee() {
// ...
}
public Money calculatePercentDiscountedFee() {
// ...
}
public Money calculateNoneDiscountedFee() {
// ...
}
}
- 할인 정책의 종류를 인터페이스를 통해 외부에 노출시키고 있다.
- 만약 할인정책이 수정된다면, 해당 메서드에 의존하는 모든 객체들은 영향을 받는다.
https://github.com/ghkdgus29/object-code/tree/main/chapter5
책임 주도 설계
앞서 살펴본 데이터 중심 설계는 협력을 고려하지 않고, 고립된 객체 상태에 초점을 맞추기에 다양한 문제점이 발생한다.
책임 중심 설계를 통해 이러한 문제점들을 해결할 수 있다.
데이터보다 행동을 먼저 결정하라
협력이라는 문맥안에서 책임을 결정하라
객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다.
클라이언트가 보낼 메시지를 결정한 후, 메시지를 수신할 객체를 선택한다.
객체가 메시지를 선택 X
메시지가 객체를 선택 O
메시지가 존재하기 때문에 메시지를 처리할 객체가 필요한 것이다.
클라이언트는 메시지 수신자에 대한 어떠한 가정도 할 수 없기 때문에 수신자의 내부구현이 깔끔하게 캡슐화된다.
애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 간주
이 책임을 처리할 메시지 결정
메시지를 전송할 객체는 무엇을 원하는지 고려
메시지를 처리할 적합한 객체를 선택
메시지를 수신할 적합한 객체는 누구인지 고려
책임을 수행하는데 필요한 정보를 알고 있을만한 정보전문가에게 메시지를 처리할 책임을 할당한다.
이와 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성된다.
응집도가 낮다는 것은 변경의 이유가 하나 이상이라는 걸 말한다.
인스턴스 변수가 초기화되는 시점이 다르다.
모든 메서드가 객체의 모든 속성을 사용하지 않는다.
객체의 타입에 따라 변하는 행동이 있다면
타입을 명시적인 클래스로 분리하고
변화하는 행동을 각 타입의 책임으로 할당하는 패턴
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
타입을 명시적인 클래스로 분리
public class PeriodCondition implements DiscountCondition{ ... }
public class SequenceCondition implements DiscountCondition{ ... }
변화하는 행동을 각 타입의 책임으로 할당
이 경우 새로운 DiscountCondition
타입을 추가하더라도, Movie
가 영향을 받지 않는다.
https://github.com/ghkdgus29/object-code/tree/main/chapter6
명령-쿼리를 분리하지 않아 버그가 발생하는 코드
객체지향 애플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지다.
객체가 수신하는 메시지가 객체의 퍼블릭 인터페이스가 된다.
메시지를 수신했을 때, 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇이냐에 달려있다.
전통적인 메서드 호출과 메시지 전송은 다르다.
메시지 전송은 송수신자가 느슨하게 결합된다.
전송자는 자신이 전송할 메시지만 알면된다.
수신자는 누가 전송했는지 알 필요없이 메시지를 받았음만 알면 된다.
송수신자 간의 결합도를 낮춘다.
객체가 의사소통을 위해 외부에 공개하는 메시지 집합 = 퍼블릭 인터페이스
퍼블릭 인터페이스에 포함된 메시지 = 오퍼레이션
메시지를 수신했을 때 실제 실행되는 코드 = 메서드
DiscountConditon
의 isSatisfiedBy(screening)
= 오퍼레이션SequenceCondition
, PeriodCondition
의 isSatisfiedBy(screening)
= 실제 구현을 포함하는 메서드
- 인터페이스의 각 요소는 오퍼레이션
- 오퍼레이션은 추상화
- 메서드는 오퍼레이션의 구현
메서드의 이름과 파라미터 목록을 합쳐 시그니처라고 부른다.
오퍼레이션은 실행코드 없이 시그니처만을 정의한 것
디미터 법칙
묻지 말고 시켜라
의도를 드러내는 인터페이스
명령 - 쿼리 분리
객체의 내부구조에 강하게 결합되지 않도록 협력 경로를 제한하라
"오직 인접한 이웃하고만 말하라."
"오직 하나의 도트만 사용하라."
클래스 내부의 메서드는 아래 해당하는 인스턴스에만 메시지를 전송해야 한다.
클래스의 속성
메서드의 매개변수
메서드 내에서 생성된 지역 객체
메시지 수신자의 내부구조가 전송자에게 노출 X
훌륭한 메시지는 객체의 내부구조를 묻는 메시지가 아니라, 원하는걸 시키는 메시지이다.
상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체하라.
이를 통해 정보전문가에게 책임을 할당하게 된다.
작업을 어떻게 수행하는지 드러내는 메서드는 좋지 않다.
협력하는 객체의 종류를 노출한다.
메서드 명으로 내부 구현을 설명하기 때문에 캡슐화를 위반
public class PeriodCondition {
public boolean isSatisfiedByPeriod(Screening screening) { ... }
}
public class SequenceCondition {
public boolean isSatisfiedBySequence(Screening screening) { ... }
}
- 할인 여부 조건이 변경된다면, 클라이언트가 호출하는 메서드를 변경해야 한다.
- 변경에 취약하다.
작업이 무엇을 수행하는지 드러내는 메서드가 좋다.
이해하기 쉽다.
설계의 유연성을 향상시킨다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class PeriodCondition implements DiscountCondition{
@Override
public boolean isSatisfiedBy(Screening screening) { ... }
}
public class SequenceCondition implements DiscountCondition{
@Override
public boolean isSatisfiedBy(Screening screening) { ... }
}
- 두 메서드는 동일한 메시지를 서로 다른 방법으로 처리한다.
- 두 객체를 동일한 타입 계층으로 묶으면, 두 메서드는 서로 대체 가능하다.
루틴 : 기능 모듈, 아래의 개념들을 포함한다.
프로시저 : 내부 상태 변경 O (부수 효과 O), 반환값 X
함수 : 내부 상태 변경 X (부수 효과 X), 반환값 O
명령
객체의 상태를 수정하는 오퍼레이션
프로시저와 유사
반환값 X
부수 효과에 주의해야 한다.
쿼리
객체와 관련된 정보를 반환하는 오퍼레이션
함수와 유사
반환값 O
부수 효과가 없으므로 다른 부분에 영향을 주지 않는다.
참조 투명성을 만족한다.
오퍼레이션은 명령인 동시에 쿼리여서는 안된다.
명령과 쿼리가 뒤섞이면 실행 결과를 예측하기 어렵다.
https://github.com/ghkdgus29/object-code/tree/main/chapter7
procedure
: 프로시저 추상화module
: 모듈 추상화abstractdata
: 추상 데이터 타입clasz
: 클래스 , 객체지향
불필요한 정보를 제거하고 현재 문제 해결에 필요한 핵심만 남기는 작업
일반적인 추상화 방법은 한 번에 다뤄야하는 문제의 크기를 줄이는 것
큰 문제를 해결가능한 작은 문제로 나누는 것 = 분해 (decomposition)
프로시저 추상화
데이터 추상화
(객체지향)
즉 객체지향이란 프로시저 추상화와 데이터 추상화를 통합한 객체들을 만들어 시스템을 분해하는 것
시스템을 프로시저 단위로 분해
시스템은 하나의 커다란 메인 함수
현대적인 시스템은 동등한 수준의 다양한 기능으로 구성된다.
비즈니스 로직과 사용자 인터페이스가 설계 초기부터 밀접하게 결합된다.
성급하게 실행 순서를 결정한다.
하위 함수는 상위 함수가 강요하는 문맥에 종속적이다.
어떤 데이터를 어떤 함수가 사용하고 있는지 알기 어렵다.
변경에 취약한 설계
변경되는 부분은 은닉시키고 안정적인 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것
복잡한 부분은 내부로 숨기고 외부엔 모듈을 추상화하는 간단한 인터페이스만 제공
기능이 아니라 변경의 정도에 따라 시스템을 분해
장점
모듈 내부 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
비즈니스 로직과 사용자 인터페이스를 분리한다.
네임스페이스 오염을 방지한다.
단점
타입 : 변수에 저장할 수 있는 내용물의 종류
언어가 기본적으로 제공하는 내장 타입으로는 프로그램을 표현하는데 한계가 있다.
데이터를 상태와 행위를 가지는 독립적인 객체로 표현
특징
타입을 정의할 수 있다.
타입 인스턴스의 오퍼레이션을 정의할 수 있다.
타입이 같은 여러개의 인스턴스들을 생성할 수 있다.
한계점
데이터만 독립적인 객체로 분리했을 뿐, 핵심 로직은 추상 데이터 타입 외부에 존재
타입 내에서 객체 분류 기준 필드가 필요하다.
추상 데이터 타입과 클래스는 다르다.
추상 데이터 타입은 상속과 다형성을 지원하지 않는다.
객체기반 프로그래밍이라고도 부른다.
상속과 다형성을 지원한다.
클래스는 절차를 추상화한다.
추상 데이터 타입은 오퍼레이션을 기준으로 실제 타입을 분류한다.
객체지향은 타입을 기준으로 오퍼레이션을 묶는다.
- 추상 데이터 타입 ➔ 클래스
Employee
를 상속하면, 얼마든지 새로운 직원 유형을 구현할 수 있다.Client
는Employee
에만 의존한다.
- 구체적인
SalariedEmployee
나HourlyEmployee
에 의존 XClient
의 코드 변경 X
이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가하는 특성을 개방 - 폐쇄 원칙이라 한다. (Open - Closed Principle)
https://github.com/ghkdgus29/object-code/tree/main/chapter8/good
컨텍스트 확장
NoneDiscountPolicy
,OverlappedDiscountPolicy
협력을 위해 의존성은 필수적이다.
그러나 과도한 의존성은 설계의 유연성을 떨어뜨린다.
따라서 의존성을 적절하게 관리해야 한다.
어떤 객체가 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 한다면
의존성은 항상 단방향이다.
의존하는 요소
➔의존되는 요소
PeriodCondition
은 잠재적으로Movie
에 의존
의존이 연쇄적으로 전파될 수 있다.
중간의 객체가 내부구현을 효과적으로 캡슐화했다면 변경은 전파되지 않는다.
클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다.
구체적인 클래스를 안다면 특정한 문맥에 강하게 결합된다.
구체적으로 알게되면 다른 문맥에서의 재사용이 어려워진다.
클래스가 자신과 협력할 객체의 추상적인 인터페이스에 대해서만 안다면
다른 문맥에서의 재사용이 수월
이를 컨텍스트 독립성이라 함
실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.
컴파일 타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다.
의존성 해결 방법 3가지
생성자
setter
메서드 인자
바람직한 의존성 == 느슨한 결합도
구체 클래스 ➔ 추상 클래스 ➔ 인터페이스 에 의존할수록 결합도는 낮아진다.
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
...
this.discountPolicy = new AmountDiscountPolicy(...);
}
}
Movie
는DiscountPolicy
,AmountDiscountPolicy
에 의존한다.- 메서드 내부에서 인스턴스를 직접 생성하여, 의존성이 인터페이스에 표현되지 않는다.
- 이를 숨겨진 의존성이라 한다.
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
}
- 생성자를 사용해 모든 의존성을 해결
- 메서드 내부에서 인스턴스를 직접 생성하지 않아, 의존성이 인터페이스에 모두 표현된다.
- 이를 명시적인 의존성이라 한다.
사용과 생성의 책임을 분리한다.
의존성을 생성자에 명시적으로 드러낸다.
구체 클래스가 아닌 추상 클래스에 의존하게 한다.
이를 통해 설계를 유연하게 만들 수 있다.
new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10),
new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(12, 0)),
new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(21, 0))));
클라이언트가 생성의 책임을 가진다.
훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.
Open-Closed Principle , OCP
소프트웨어 개체는 확장에 대해 열려있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
컴파일타임 의존성을 고정시키고 런타임 의존성을 확장하고 수정할 수 있는 구조는 OCP를 만족한다.
Movie
와DiscountPolicy
의 컴파일타임 의존성은 고정, 런타임 의존성은 확장
추상화에 의존하는 것이 핵심
추상화 부분은 다양한 상황에서의 공통점이므로 수정에 닫혀있다.
추상화를 통해 생략된 부분은 확장의 여지를 남긴다.
객체의 생성과 사용이 공존하면 OCP를 위반하게 된다.
객체를 생성하는 책임은 클라이언트로 옮겨 생성과 사용을 분리할 수 있다.
생성에만 특화된 FACTORY
를 사용할 수도 있다.
public class Factory {
public Movie createAvatarMovie() {
return new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
}
}
객체 생성만을 전담하는
Factory
public class Client {
private Facotry factory;
public Client(Factory factory) {
this.factory = factory;
}
public Money getAvatarFee() {
Movie avatar = factory.createAvatarMovie();
return avatar.getFee();
}
}
Client
가 필요로 하는 객체 생성을Factory
에 위임할 수 있다.
이 경우
Client
는 사용의 책임만 가진다.
사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 전달되어 의존성을 해결하는 것
의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내 필요한 의존성을 전달받을 수 있다.
생성자 주입
setter 주입
메서드 주입
명시적이기 때문에 의존성을 파악하기 위해 코드 내부를 읽을 필요 없다.
SERVICE LOCATOR
패턴은 의존성을 감추는 패턴이다.
의존성을 구현 내부로 감추면 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 발견된다.
코드의 내부 구현을 이해할 것을 강요한다.
객체 간 협력할 때 협력의 본질은 상위 수준의 클래스가 갖고 있다.
상위 수준 클래스는 하위 수준 클래스에 의존해선 안됨
상위 수준 클래스
Movie
가 하위 수준 클래스AmountDiscountPolicy
에 의존
상위 수준 클래스, 하위 수준 클래스 모두 추상클래스인
DiscountPolicy
에 의존
이를 의존성 역전 원칙이라 한다.
Dependency Inversion Principle, DIP
전통적인 소프트웨어 개발에선 상위 수준 모듈이 하위 수준 모듈에 의존한다.
이러한 전통적인 의존성 구조를 뒤집기 때문에 DIP 라 부른다.
유연한 설계는 변경하기 쉽고 확장하기 쉽다.
단순하고 명확한 설계는 이해하기 쉽다.
따라서 미리 걱정하지 말고 유연성이 필요할 때 유연하게 설계하자
https://github.com/ghkdgus29/object-code/tree/main/chapter10/inheritance
코드 재사용을 위해 상속을 사용
중복 코드는 변경을 방해한다.
중복 코드 결정 기준은 모양이 아니다.
중복 여부를 판단하는 기준은 변경이다.
클래스를 재사용하기 위해 새로운 클래스를 추가하는 기법
상속 계층의 계단을 내려갈 때마다 코드를 이해하기 어려워진다.
상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 구현방법을 정확하게 알아야 한다.
부모 클래스와 자식 클래스간의 결합도를 높인다.
부모 클래스의 수정은 수많은 자식 클래스의 수정을 동반한다.
부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상
상속은 자식 클래스를 점진적으로 추가해 기능을 확장하는데는 용이하지만
객체지향의 기반은 캡슐화를 통한 변경의 통제
부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깰 수 있다.
문제를 해결하기 위해서는 자식 클래스들이 추상화에 의존해야 한다.
차이점 (세부 구현) 은 자식 클래스로,
공통적인 부분 (퍼블릭 인터페이스) 은 부모 클래스로 이동하라
그러나 이렇게 하더라도 부모 클래스의 인스턴스 변수의 변경은 자식 클래스들의 생성자에 영향을 미친다.
상속으로 인한 클래스사이의 결합은 피할 수 없다.
https://github.com/ghkdgus29/object-code/tree/main/chapter11
inheritance
패키지 : 상속을 사용해 코드 재사용synthesis
패키지 : 합성을 사용해 코드 재사용
상속은 부모 클래스 - 자식 클래스를 연결해 코드를 재사용
합성은 재사용 코드를 가진 객체를 내부 필드로 포함하여 코드를 재사용
합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용한다.
상속을 사용할 때 : 클래스 사이의 높은 결합도
합성을 사용할 때 : 객체 사이의 낮은 결합도
모든 가능한 조합별로 자식 클래스를 만들어야 한다.
새로운 정책 추가 시 많은 수의 클래스를 상속계층안에 추가해야 한다.
클래스 폭발
- 조합별로 자식 클래스를 만들어야 한다.
- 복잡한 이름, 많은 수의 자식 클래스
- 새로운 정책 추가 시, 또다시 많은 수의 자식 클래스 생성 필요
코드 재사용을 위한 상속은 지양하라
컴파일 타임 관계를 런타임 관계로 변경
모든 가능한 조합별로 클래스를 만들지 않아도 된다.
합성을 사용한 구조
public class Client {
public static void main(String[] args) {
// 일반 요금제 -> 세금 정책
Phone phone1 = new Phone(
new TaxablePolicy(
new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
0.05));
// 일반 요금제 -> 기본 요금 할인 정책 -> 세금 정책
Phone phone2 = new Phone(
new TaxablePolicy(
new RateDiscountablePolicy(
new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
Money.wons(500)),
0.05));
// 일반 요금제 -> 세금 정책 -> 기본 요금 할인 정책
Phone phone3 = new Phone(
new RateDiscountablePolicy(
new TaxablePolicy(
new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
0.05),
Money.wons(1000)));
// 심야할인 요금제 -> 세금 정책 -> 기본 요금 할인 정책
Phone phone4 = new Phone(
new RateDiscountablePolicy(
new TaxablePolicy(
new NightlyDiscountPolicy(Money.wons(700), Money.wons(1000), Duration.ofSeconds(10)),
0.05),
Money.wons(1000)
)
);
}
}
인스턴스 생성 시점에 다양한 조합 가능
새로운 기본 정책 추가
- 상속을 코드 재사용에 사용한 경우, 4 + 1개의 클래스가 추가되었다.
- 반면, 합성을 코드 재사용에 사용한 경우, 1개의 클래스만 추가하면 된다.
정책 변경 시 오직 하나의 클래스만 수정하면 된다.
https://github.com/ghkdgus29/object-code/tree/main/chapter12
많은 형태를 가질 수 있는 능력, Polymorphism
하나의 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력
오버로딩 다형성
메서드명은 같지만 메서드 인자가 다르면 다른 메서드로 취급한다.
덕분에 유사한 작업을 수행하는 메서드명을 통일할 수 있다.
강제 다형성
매개변수 다형성
변수를 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정
제네릭 프로그래밍과 관련이 있다.
포함 다형성
메시지가 동일하더라도 수신한 객체의 실제 타입에 따라 실제 수행되는 행동이 달라지는 능력
서브타입 다형성이라고도 함
객체지향 프로그래밍에서 가장 대표적인 형태의 다형성
상속의 목적은 코드 재사용 X
다형성을 가능하게 하는 타입 계층을 구축하는 것이 진짜 목적
부모 클래스와 자식 클래스에 동일한 시그니처를 가진 메서드가 존재할 경우
자식 클래스의 메서드 우선순위가 더 높다.
메시지를 수신했을 때 부모 클래스의 메서드가 아닌 자식 클래스의 메서드가 실행
이처럼 부모 클래스의 구현을 새로운 구현으로 대체하는 것을 메서드 오버라이딩 이라 한다.
자식 클래스의 메서드가 부모 클래스의 메서드를 감춘다.
인스턴스 개수와 상관없이 클래스는 하나만 메모리에 로드된다.
각 인스턴스는 자신의 클래스를 가리키는 포인터(class
)를 가진다.
클래스는 자신의 부모 클래스를 가리키는 포인터(parent
)를 가진다.
만약 현재 인스턴스가 가리키는 클래스에 원하는 메서드가 존재하지 않으면
부모 클래스를 차례로 거슬러 올라가며 원하는 메서드를 찾는다.
GradeLecture
인스턴스의 메모리 구조
부모 클래스 타입으로 선언된 변수에 자식 클래스를 할당하는 것
부모는 마음이 넓어 자식을 담을 수 있다.
명시적인 타입 변환 필요 X
선언된 변수의 타입이 아니라 메시지를 수신하는 실제 객체의 타입에 따라 실행되는 메서드가 결정되는 것
객체지향 시스템이 메시지를 처리할 메서드를 컴파일 시점이 아닌 런타임 시점에 결정하기 때문에 가능
컴파일 타임에 호출할 함수를 결정하는 정적 바인딩에 대응되는 개념
메시지 전송 코드만으로는 어떤 클래스의 어떤 메서드가 실행될 지 알 수 없다.
따라서 실제 객체의 클래스의 메서드를 호출한다는 표현은 옳지 않다.
실제 객체에게 메시지를 전송한다는 표현이 맞다.
super 참조
역시 마찬가지로 부모 클래스에게 메시지를 전송한다.
self 전송
은 타입 상관없이 실제 들어온 현재 객체의 클래스부터 메서드를 탐색한다.
super 전송
은 클래스에 코드로서 존재한다.
객체가 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해 처리를 요청하는 것
위임은 현재 실행 객체를 가리키는 self 참조
를 인자로 함께 전달
포워딩은 self 참조
전달 X
이를 통해 두 객체간의 실행 문맥을 공유할 수 있다.
상속은 이러한 메시지 위임을 자동화해준다.
https://github.com/ghkdgus29/object-code/tree/main/chapter13
- LSP를 위반하는 직사각형 - 정사각형 예제
상속은 부모와 자식을 묶는 타입 계층을 구현하기 위해 사용되어야 한다.
부모 클래스는 자식 클래스의 일반화
자식 클래스는 부모 클래스의 특수화
이를 통해 다형적으로 동작하는 객체들의 관계를 얻을 수 있다.
사물을 분류하기 위한 틀
공통의 특징을 공유하는 대상들의 분류
타입에 속하는 개별 대상을 타입의 인스턴스 (객체) 라 부른다.
타입의 목적
타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의
타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공
프로그래밍 언어의 관점에서 타입은 호출가능한 오퍼레이션의 집합을 정의한다.
객체지향 프로그래밍에서의 오퍼레이션 == 객체가 수신할 수 있는 메시지
따라서 객체의 타입은 객체가 수신할 수 있는 메시지의 종류 (퍼블릭 인터페이스) 를 정의한다.
동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.
객체에게 중요한 것은 행동
객체들이 동일한 상태를 갖더라도 퍼블릭 인터페이스가 다르면 서로 다른 타입이다.
객체들이 다른 상태를 갖더라도 퍼블릭 인터페이스가 같다면 서로 같은 타입이다.
- 자바, C++, 루비 ➔ 인스턴스
- 클래스기반, 객체지향 언어, 프로그래밍 언어 ➔ 타입
타입들을 일반화와 특수화 관계를 가진 계층으로 표현하는 것
일반적인 상위타입을 슈퍼타입
구체적인 하위타입을 서브타입이라 한다.
슈퍼타입과 서브타입간의 관계를 형성하는 기준은 퍼블릭 인터페이스다.
일반적인 퍼블릭 인터페이스를 가지는 객체 ➔ 슈퍼타입
구체적인 퍼블릭 인터페이스를 가지는 객체 ➔ 서브타입
서브타입 인스턴스 집합은 슈퍼타입의 인스턴스 집합에 포함된다.
둘을 나누는 기준은 상속을 사용하는 목적
서브클래싱 : 코드 재사용을 목적으로 상속
서브타이핑 : 부모 클래스 인스턴스 대신 자식 클래스 인스턴스를 사용할 목적으로 상속 (인터페이스 상속)
자식 클래스는 모두 서브 클래스지만, 모든 서브 클래스가 서브 타입을 만족하진 않는다.
코드상으로는 슈퍼타입으로 정의되나, 런타임에 서브타입 객체로 대체할 수 있다.
타입을 구현하는 일반적인 방법 ➔ 클래스
타입계층을 구현하는 일반적인 방법 ➔ 상속 + α
타입계층을 구현할 때 지켜야 하는 제약사항
클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
클라이언트는 부모 클래스와 자식 클래스의 차이점을 몰라야 한다.
두 타입사이에 행동이 호환될 경우에만 타입계층으로 묶어야 한다.
행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.
클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.
Client1
의 요구사항 변경에 의한Flyer
인터페이스의 변경은
Bird
에만 영향을 준다.Client2
,Walker
,Penguin
에는 영향을 주지 않는다.
클라이언트의 요구에 맞춰 인터페이스를 분리하면 변경에 대한 영향을 제어할 수 있다.
클라이언트의 요구가 바뀌더라도 영향의 파급효과를 효과적으로 제어할 수 있다.
비대한 인터페이스는 응집도 있는 특화된 인터페이스로 분리해야 한다.
클라이언트는 자신이 실제로 호출하는 메서드에만 의존해야 한다.
Liskov Substitution Principle, LSP
올바른 상속 관계 (타입 계층) 를 구축하기 위한 지침
부모 클래스를 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것임을 보장해야 한다.
유연한 설계의 기반
사전조건 : 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
사후조건 : 서버가 클라이언트에게 보장해야 하는 조건
클래스 불변식 : 메서드 실행 전후로 인스턴스가 만족시켜야 하는 조건
서브타입이 LSP를 만족시키기 위해서는 클라이언트-슈퍼타입간에 체결된 계약을 준수해야 한다.
서브타입에 더 강력한 사전조건을 정의할 수 없다.
서브타입에 더 약한 사후조건을 정의할 수 없다.
https://github.com/ghkdgus29/object-code/tree/main/chapter14
inconsistent
패키지 : 일관적이지 않은 설계consistent
패키지 : 일관적인 설계
일관성은 설계에 드는 비용을 감소시킨다.
코드의 이해가 쉬워진다.
변하는 개념을 변하지 않는 개념으로부터 분리한다.
변하는 개념을 캡슐화하라.
일관성은 요구사항이 추가될수록 깨질 수 있다.
현재의 설계에 맹목적으로 일관성을 맞추지 말고, 변경에 맞춰 지속적인 코드 개선이 필요하다.
디자인 패턴
소프트웨어 설계에서 자주 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결방법
여러 용도로 사용할 수 있는 설계 아이디어
프레임워크
반복적으로 발생하는 문제와 해법의 쌍
패턴의 이름을 통해 다른 사람과의 의사소통을 돕는다.
실무 컨텍스트에서 유용하게 사용해왔고 다른 실무 컨텍스트에서도 유용할 것이라 예상되는 아이디어
특정한 상황에 적용할 수 있는 설계를 쉽고 빠르게 떠올릴 수 있다.
디자인 패턴은 설계의 방향을 제시하는 출발점
설계의 목표가 돼서는 안된다.
패턴은 맹목적으로 사용해선 안된다.
애플리케이션 개발자가 요구사항에 맞게 커스터마이징 할 수 있는 애플리케이션 골격
부분적으로 구현된 추상 클래스, 인터페이스 집합, 다양한 컴포넌트들을 제공
제어 흐름에 대해 미리 정의
제어 역전 원리
도메인 핵심 개념들 사이의 협력 관계를 재사용
프레임워크가 개발자가 구현한 서브클래스를 사용
- 상위 패키지 : 프레임워크
- 하위 패키지 : 개발자가 구현해야 하는 세부사항
출처 : 조영호님의 오브젝트
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=266736479
와 벌써 2회독이신가요