티스토리 뷰
728x90
반응형
영화 예매 시스템
- 이번에 다룰 예제는 영화 예매 시스템입니다. 영화의 상영시간을 예매하는 예매자는 1개의 할인 정책과 N개의 할인 조건에 만족한다면 요금을 할인 받을 수 있습니다.
- 할인 정책은 금액 할인 정책과 비율 할인 정책으로 구체화할 수 있습니다.
- 할인 조건은 순번 조건, 기간 조건 등으로 구체화할 수 있습니다.
- 앞서 구체화 한다는 말은 나눌 수 있다는 의미를 가집니다.
객체지향 프로그래밍을 향해
💡협력, 객체, 클래스
- 앞서 나온 예제를 중심으로 영화 예매 시스템을 프로그래밍할 때 가장 처음 할 일은 각 역할에 맞는 클래스를 선언하고 역할에 맞게
속성을 세팅하는 것입니다. 하지만 이는 객체지향의 본질과는 거리가 멀다고 합니다.(그럼 어떻게 구성해,,,?) - 진정한 객체지향 패러다임으로서의 전환은 클래스가 아닌 객체에 초점을 맞출 때만 얻을 수 있다고 합니다.
객체지향 프로그래밍을 위한 두가지
- 첫째, 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지를 고민해야 합니다.
- 둘째, 객체를 독립적인 존재가 아닌 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야합니다. 객체는 홀로 존재하는 것이 아닌 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재입니다. 객체를 협력하는 공동체의 구성원으로 바라보면 유연하고 확장 가능한 설계를 가질 수 있습니다.
- 이렇게 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현해야 합니다.
💡클래스 구현하기
상영 도메인 클래스
- 아래 상영 도메인 클래스를 보면 인스턴스 변수의 접근 제한자는 private이고 메서드의 접근 제한자는 public인 것을 볼 수 있습니다.
- 접근 제한자를 통해 경계를 명확히 구분함으로써 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 정할 수 있습니다.
- 이렇게 경계를 명확히 나누는 이유는 객체의 자율성을 보장하기 위함입니다.
// 상영 도메인
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public LocalDateTime getStartTime() {
return whenScreened;
}
public Movie getMovieFee() {
return movie.getFee();
}
public boolean isSequence(int sequence) {
return this.sequence == sequence;
}
}
자율적인 객체
- 우리는 먼저 두 가지 중요한 사실을 알아야 합니다.
- 첫째, 객체는 상태와 행동을 복합적으로 가지는 존재입니다.
- 둘째, 객체는 스스로 판단하고 행동하는 자율적인 존재입니다.
- 많은 사람들이 객체를 상태와 행동을 함께 포함하는 식별 가능한 단위로 정의합니다. 객체지향 이전에는 데이터와 기능을 독립적인 존재로 생각하고 서로 엮어 프로그램을 구성했다면 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 프로그램을 구성합니다. 이렇게 묶는 것을 캡슐화라고 합니다.
- 접근 제한자를 통해 객체 내부에 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위함입니다. 객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 만드는 것입니다.
🤔 왜 접근 제한자를 통해 접근을 막아야 할까요?
- 접근 제한자를 사용함으로써 외부에서는 객체가 현재 어떤 상태인지, 어떤 생각을 가지고 있는지 알아서는 안되며, 결정에 직접적으로 개입하려고 해서도 안됩니다. 객체에게 원하는 것을 요청하고 객체는 스스로 결과를 만들어 반환 수 있음을 믿고 기다려야 합니다.
프로그래머의 자유
- 프로그래머의 역할은 클래스의 작성자와 클라이언트 프로그래머로 구분할 수 있습니다. 쉽게 표현하면 클래스의 작성자는 자신의
도메인 영역에서 데이터와 기능을 복합적으로 사용해 어떠한 요청에 대해 응답을 주는 사람이고, 클라이언트 프로그래머는 클래스 작성자에게 데이터를 요구하는 사람입니다. - 이렇게 구분을 짓는 이유는 서로의 서로의 목적이 다르기 때문입니다. 클라이언트 프로그래머의 목적은 필요한 클래스를 엮어 서비스를 빠르고 안정적으로 구축하는 것이고, 클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 은닉하는 것입니다.
- 이렇게 나눔으로써 클라이언트 프로그래머는 내부의 구현은 신경안쓰고 인터페이스만 알아도 클래스를 사용할 수 있으며 기억해야 하는 양을 줄일 수 있습니다. 또한 클래스 작성자는 인터페이스를 바꾸지 않아도 외부에 미치는 영향을 걱정하지 않고 내부 구현을 마음대로 변경할 수 있습니다.
할인 요금 구하기
💡할인 정책과 할인 조건
- 추상 클래스 및 인터페이스를 사용하여 다형성을 제공할 수 있습니다. 다형성을 사용함으로써 컴파일 시점이 아닌 런타임 시점에 유동적으로 정책이나 조건을 바꿔가며 사용할 수 있습니다.
상속과 다형성
💡컴파일 시간 의존성과 실행 시간 의존성
- 아래 예제는 Money 클래스에서 할인 정책에 의존하고 있다는 가정입니다.
- discountPolicy는 추상 클래스이므로 해당 클래스를 상속 받고 있는 서브 클래스에 의해 주입 받을 수 있습니다.
반면 percentDiscountPolicy는 추상 클래스를 구현한 클래스이므로 본인만 주입받을 수 있습니다. - 여기서 말하고자 하는것은 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다는 것입니다. 다시 말해 Money 클래스의 코드 의존성을 보면 DiscountPolicy를 의존하고 있지만 실행 시점에는 해당 클래스를 구현한 클래스를 의존할 수 있습니다.
public class Money {
// 실행 시점 의존성
private DiscountPolicy discountPolicy;
// 컴파일 시점 의존성
private PercentDiscountPolicy percentDiscountPolicy;
public Money(DiscountPolicy discountPolicy, PercentDiscountPolicy percentDiscountPolicy) {
this.discountPolicy = discountPolicy;
this.percentDiscountPolicy = percentDiscountPolicy;
}
}
🤔 그렇다면 실행 시점 의존성은 상황에 유연하게 대처할 수 있고, 확장 가능할텐데 항상 좋을까요?
- 이 또한 그렇지 않습니다. 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워질 수 있습니다. 이는 의존성의 양면성으므로 설계시 적절한 트레이드 오프가 필요한것 같습니다.
💡상속과 인터페이스
- 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려 받을 수 있기 때문입니다. 이는 상속을 바라보는 일반적인 인식과는 거리가 있는데, 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라 생각하기 때문입니다.
- 아래 예제는 Movie 클래스의 인스턴스 변수 중 DiscountPolicy가 런타임 시점에 PercentDiscountPolicy, AmountDiscountPolicy 중 어떤 것을 주입 받느냐에 따라 할인정책을 유동적으로 적용할 수 있습니다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.getDiscountAmount(screening));
}
}
다형성
- 다형성은 객체지향 프로그래밍의 컴파일 시점 의존성과 실행 시점 의존성이 다를 수 있다는 사실에 기반을 두고 있습니다.
- 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미합니다. 따라서 다형적인 협력에 동참하는 객체들은 모두 같은 메시지를 이해할 수 있어야 합니다. 다시말해 구현체는 공통된 상위 타입을 정의하고 있어야 합니다.
- 다형성은 실행 시점에 의존성을 주입하므로 지연 바인딩 또는 동적 바인딩이라 부릅니다. 반대로 컴파일 시점 의존성은 초기 바인딩, 정적 바인딩이라 부릅니다.
구현 상속과 인터페이스 상속
- 상속을 구현 상속과 인터페이스 상속으로 구분할 수 있습니다.
- 구현 상속은 서브클래싱이라 부르고 인터페이스 상속은 서브타이핑이라 부릅니다.
- 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라 부르고, 다형적인 협력을 위해 부모 클래스와 자식 클래스간 인터페이스를 공유할 수 있도록 상속을 이용하는 것을 인터페이스 상속이라 부릅니다.
- 상속은 구현 상속이 아닌 인터페이스 상속을 위해 사용해야 하며, 코드 재사용을 위한 상속은 변경에 취약한 코드를 낳게 되므로 자제해야 합니다.
추상화와 유연성
💡추상화의 힘
- 추상화를 사용하면 요구사항의 정책을 높은 수준에서 서술할 수 있습니다.
- 추상화를 사용하면 설계가 조금 더 유연해질 수 있습니다.
요구사항의 정책을 높은 수준에서 서술
- 영화 예매시 금액 할인 정책이나 비율 할인 정책을 사용한다는 사실이 중요할 때도 있겠지만 어떤 때는 할인 정책이 존재한다고 말하는 것만으로 충분할 경우가 있습니다. 이렇게 추상화를 이용하면 필요에 따라 표현의 수준을 조정할 수 있습니다.
유연한 설계
- 할인 정책 중 아무런 할인 정책이 적용되지 않을 수도 있습니다. 아래는 영화의 예매 비용을 계산할 때 정책이 없다면 영화의 기본 요금을 반환하고 있습니다. 이렇게 된다면 할인 정책이 없는 경우를 특별한 케이스로 취급하고 있기 때문에 지금까지의 일관성이 무너지게 됩니다.
- 기존에는 할인 금액을 계산하는 책임이 DiscountPolicy를 구현한 클래스에게 있었지만 할인 정책이 없는 경우는 Movie 클래스가 책임을 지게 됩니다. 따라서 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 좋지 않은 선택입니다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
return fee;
} else {
return fee.minus(discountPolicy.getDiscountAmount(screening));
}
}
}
해결방법은?
- 할인 정책을 적용하지 않는 클래스를 만들고 영화의 기본 요금을 반환할 수 있도록 합니다.
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
💡코드 재사용
- 상속은 코드를 재사용하기 위해 널리 사용되는 방법입니다. 그러나 널리 사용된다고 해서 가장 좋은 방법은 아닙니다.
- 상속보다는 합성이 더 좋은 방법이 될 수 있습니다. 합성이란 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법입니다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy; // 합성
}
💡상속 Vs 합성
상속
- 상속은 캡슐화를 위반합니다. 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약해집니다. 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높입니다.
결과적으로 상속을 과하게 사용하면 코드의 변경을 어렵게 만듭니다. - 상속은 유연한 설계를 방해합니다. 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정합니다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능합니다.
합성
- 앞서 Movie 클래스는 DiscountPolicy를 합성을 통해 사용하고 있습니다.
- 합성을 사용하게 되면 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있습니다. 또한 의존하는 인스턴스를 교체하는 것도 쉽기 때문에 유현한 설계를 가질 수 있습니다.
이와 관련해서 이펙티브 자바 아이템 18 - 상속보다는 컴포지션을 사용하라. 도 참고하면 좋을거 같습니다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 - 6장 메시지와 인터페이스 (0) | 2022.10.09 |
---|---|
오브젝트 - 5장 책임 할당하기 (0) | 2022.10.07 |
오브젝트 - 4장 설계 품질과 트레이드오프 (0) | 2022.10.02 |
오브젝트 - 3장 역할, 책임, 협력 (1) | 2022.09.30 |
오브젝트 - 1장 객체, 설계 (2) | 2022.09.23 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- spring boot excel download paging
- spring boot redis 대기열 구현
- java userThread와 DaemonThread
- service based architecture
- redis sorted set으로 대기열 구현
- polling publisher spring boot
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- 레이어드 아키텍처란
- spring boot 엑셀 다운로드
- 트랜잭셔널 아웃박스 패턴 스프링부트
- JDK Dynamic Proxy와 CGLIB의 차이
- spring boot redisson sorted set
- redis 대기열 구현
- space based architecture
- redis sorted set
- 서비스 기반 아키텍처
- transactional outbox pattern
- java ThreadLocal
- pipe and filter architecture
- transactional outbox pattern spring boot
- spring boot poi excel download
- 자바 백엔드 개발자 추천 도서
- 공간 기반 아키텍처
- spring boot redisson destributed lock
- microkernel architecture
- pipeline architecture
- @ControllerAdvice
- spring boot excel download oom
- spring boot redisson 분산락 구현
- 람다 표현식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함