[오브젝트 스터디] Chapter 02 객체지향 프로그래밍

2024. 12. 15. 20:45·Java

안녕하세요. 시험 기간이 끝나서 다시 오브젝트를 공부해보겠습니다. 이번에는 오브젝트 책의 챕터2 객체지향 프로그래밍에 대해서 알아봅시다. 자바의 기본적인 개념에 대해서는 알고 있다는 전제하에 시작하겠습니다.  글을 길게 써 이해력을 높일 수 있겠지만 그 만큼 읽는데 시간이 많이 들어 안좋습니다. 이 글 또한 적절한 트레이드오프의 산물이겠습니다.

 

이번 장에서는 영화 예매 시스템을 통해 객체지향 프로그래밍에 대해 배워보자.

 

먼저 우리는 기본적으로 영화를 예매한다고 한다. 그런데? 실제로는 영화를 예매하는 것이 아니다. 우리는 그 시간대에 상영하는 영화를 예매하는 것이다. 이러한 용어 구분의 필요성이 있다. 

 

여기에 할인 정책과 할인 조건을 추가한다. 할인 정책과 할인 조건은 함께 적용 받을 수 있다.

 

협력, 객체, 클래스

우리는 자바로 프로그램을 작성할 때 먼저 무슨 클래스를 만들지 고민을 한다. 그러나 진정한 객체지향 프로그래밍은 클래스를 생각하는 것이 아니고 객체에 초점을 맞춰 고민해야 하는데

 

이렇게 고민해보자.

1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하기.

2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 보기.

 

도메인의 구조를 따르는 프로그램 구조

도메인은 사용자가 프로그램을 사용하는 분야를 말하는데 우리가 영화 예매 프로그램을 만들려면 영화 예매라는 도메인 분야를 제대로 설계를 할 수 있다. 이게 무슨 말이냐면 영화 예매에 대한 기본적인 지식을 알아야한다는 것이다. 우리가 영화 예매 프로그램을 만들려면

영화가 상영되고 이러한 상영된 영화를 예매하고 각종 할인 정책을 받아 돈을 지불받아 예약을 완료시킨다는 이런 기본적인 도메인 지식이 깔려있어야 한다는 것이다. 그리고 그 도메인 지식에 따라 클래스명을 짓는다. 상영이라는 말이 있는데 대충 지어버리면 안된다. 이렇게 만들어야 프로그램의 구조를 이해하고 예상하기 쉽게 만들 수 있다.

클래스 구현하기

그럼 이제 구현을 해보자.

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDate whenScreened;

    public Screening(Movie movie, int sequence, LocalDate whenScreened) {
        this.movie = movie;
        this.sequence = sequence;
        this.whenScreened = whenScreened;
    }

    public LocalDate getStartTime() {
        return whenScreened;
    }

    public boolean isSequence(int sequence) {
        return this.sequence == sequence;
    }

    public Money getMovieFee() {
        return movie.getFee();
    }

 

기본적인 Screening 클래스를 만들었다.

 

여기서 눈 여겨 봐야할 점은 접근 제어자 인데 접근 제어자를 통해 클래스의 내부와 외부를 구분할 수 있다.

이렇게 하면 뭐가 좋을까? 객체에게 자율성을 부여할 수 있다. 이전에 객체에게 자율성을 부여하는 것을 꽤 중요하게 여겼었다.

더 중요한 이유로는 프로그래머에게 구현의 자유를 제공하기 때문이다.

 

객체는 상태(state)와 행동(behavior)의 복합체이다. 그리고 스스로 판단하고 행동하는 자율적인 존재라는 것이다.

자율적인 존재로 만들기 위해선 외부의 간섭을 최소화해야한다. 다른 객체가 간섭하고 그러면 전혀 자율성을 침해받을 것이다.

 

그리고 접근제어자를 통해 클래스 작성자와 클라이언트 프로그래머 둘로 나눌 수 있다. 메서드를 개발한 사람과 사용하는 사람이 같을 수도 있겠지만 그렇지 않을 수 있다. 정보은닉을 통해 클래스 작성자는  외부에 미치는 영향을 걱정하지않고 마음대로 변경할 수 있고 클라이언트 프로그래머는 내부의 구현을 모르더라도 인터페이스만 알고 있으면 클래스를 사용 할 수 있기 때문에 모든 것을 알지 않아도 괜찮다.

그렇기 때문에 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야한다.

 

협력하는 객체들의 공동체

이전 글에서는 금액을 구현하기위해 Long 타입을 사용했었는데 Money라는 객체 를 만들어주면 의미를 좀 더 전달 해줄 수 있다.

객체가 단 하나의 인스턴스 변수만 포함하더라도 개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높일 수 있다.

협력에 관한 짧은 이야기

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.

 

예를들어 Screening이 Movie의 calculateMovieFee에 요청하면 Movie는 적절히 처리한 후 응답을 넘긴다. 그러니까 적절히 계산을 해서 요청했던 Screening에게 값을 넘겨 준다는 것이다.

 

할인 요금 계산을 위한 협력 시작하기

자 이제 할인 요금 계산을 위해 기본적인 Movie 클래스를 작성해보자.

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }
    
    public Money getFee() {
        return fee;
    }
    
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }

 

이 메서드에는 이상한 점이 있다. 어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않는다.

단지 discountPolicy에 메세지를 전송할 뿐 이다.

 

할인 정책과 할인조건

 

할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분된다. 

대부분의 코드가 유사하고 할인 요금을 계산하는 방식만 다르다. 그러면 어떻게 해야하겠는가?

상속을 통해 중복되는 코드를 제거해주고 오버라이딩을 통해 계산 방식만 바꿔주면 되겠다.

 

=> DiscountPolicy는 단순히 인터페이스 역할만 해주면 된다.

 

이렇게 바꾸면 Movie클래스 어디에서도 할인 정책을 찾아볼순 없지만 실행 시에 할인정책이 선택되어 할인을 적용 받는다.

 

컴파일 시간 의존성과 실행 시간 의존성

어떻게 이러한 일이 가능한 것인가? 상속으로 인해 가능하다.

 

어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.

 

코드 상으로는 Movie클래스가 AmountDiscountPolicy나 PercentDiscountPolicy 클래스와 의존하는 부분이 없지만 실행 시점에는 의존하게 된다.

 

그러나 이렇게 컴파일 시간 의존성과 실행 시간 의존성이 다를 경우에 코드를 이해하기 어려워진다. 코드를 이해 하기 위해 객체를 생성하고 연결하는 코드 부분을 찾아야하기 때문이다. 하지만 더 유연하고 확장이 가능해진다. 이런 의존성의 양면성이 설계가 트레이드 오프의 산물이라는 것을 잘 보여준다.

 

차이에 의한 프로그래밍

상속은 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속을 사용해 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가 할 수 있다.

이런 것을 차이에 의한 프로그래밍이라고 부른다.

 

상속과 인터페이스

일반적으로 상속의 목적은 메서드나 인스턴스 변수를 재사용하기 위한 것이라고 생각하지만 그렇지 않다.

상속은 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받기 때문에 가치가 있다. 무슨말이냐면

 

public Money calculateMovieFee(Screening screening) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}

 

Movie 클래스의 calculateMovieFee를 보자 단순히 discountPolicy에 메세지를 전송한다. Movie 입장에서 자신과 협력하는 객체가 어떤 클래스의 인스턴스인지 중요 한것이 아니고 calculateDiscountAmount 메세지를 수신할 수 있다는 사실 만이 중요하다.

이러한 것이 왜가능하냐면 업캐스팅을 통해 AmountDiscountPolicy와 PercentDiscountPolicy가 사용될 수 있기 때문이다.

 

 

다형성

메세지와 메서드는 다른 개념이다. Movie DiscountPolicy에 calculateDiscountAmount 메세지를 전송하는것이고 이것을 받고 실행되는 메서드는 달라진다. 실행할 때 Movie가 AmountDiscountPolicy를 받았다면 오버라이딩된 메서드가 실행 될 것이고 PercentDiscountPolicy를 받았을 경우도 같다.

 

Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메세지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는데 이를 다형성이라고 부른다.

 

메세지를 실행시점에 바인딩하면 지연바인딩, 동적바인딩이라 부르고 컴파일 시점에 바인딩한다면 초기 바인딩, 정적바인딩이라고 부른다.

 

상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있겠지만 이것이 다형성을 구현할 수 있는 유일한 방법은 아니다.

 

추상화의 힘

할인 정책은 구체적인 금액 할인 정책과 비율 할인 정책을 포괄 하는 추상적인 개념이다.

이처럼 할인 정책이라는 인터페이스를 만들어 추상화를 이용하면 설계가 좀 더 유연해 질 수 있다.

 

세부적인 내용을 무시한 채 쉽고 간단하게 표현할 수 있다는 장점이 있다. 

 

유연한 설계

이번에는 할인 정책이 적용되지 않았을때에 대해 살펴보자.

그냥 간단하게 할인 요금을 계산할 필요없이 영화에 설정된 기본금액을 그대로 사용하면 되겠다. 

 

public Money calculateMovieFee(Screening screening) {
    if(discountPolicy == null) {
        return feel;
    }
    
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}

 

근데 이 방식에는 문제점이 있다. 할인 정책이 없는 경우를 예외 케이스로 취급했기 때문에 일관된 방식이 무너진다는 단점이 있다.

 

그렇다면 DiscountPolicy를 상속받아 새로운 NoneDiscountPolicy를 만들면 어떨까?

 

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    proteted Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

 

기존에 있는 클래스는 수정하지 않고 확장할 수 있게 되었다.

이러한 것은 DiscountPolicy가 여러가지를 포괄 할 수 있도록 추상적이기 때문에 가능한 일이다.

따라서 추상화는 유연한 설계를 하는것을 도와준다.

 

그러나 한가지 문제점이 더있다. 부모 클래스인 NoneDiscountPolicy에서 오버라이딩 된 calculateDiscountAmount() 메서드를 호출할 때 원래 calculateDiscountAmount() 메서드에서 getDiscountAmount() 메서드를 호출하는데 호출하지 않고 때문이다. getDiscountAmount()는 어떤 값을 리턴하더라도 상관이 없는 문제가 생긴다.  이 문제를 해결하기 위해 (계속..)

추상클래스와 인터페이스의 트레이드 오프

DiscountPolicy를 인터페이스를 만들고

public interface DiscountPolicy {
    Money calculateDiscountAmount(Screening screening);
}

 

원래의 DiscountPolicy를 DefaultDiscountPolicy로 변경하고 인터페이스를 구현하도록 수정한다.

public abstract class DefaultDiscountPolicy implements DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DefaultDiscountPolicy(DiscountCondition... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    @Override
    public Money calculateDiscountAmount(Screening screening) {
        for(DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening Screening);
}

 

그리고 이제 NoneDiscountPolicy가 DiscountPolicy 인터페이스를 구현하도록 변경하면 개념적인 혼란과 결합을 제거할 수 있다.

 

 

인터페이스를 사용하도록 변경한 설계가 과연 더 좋을까?

 

인터페이스를 추가하는 것이 과할 수 도있고 변경전의 NoneDiscountPolicy클래스 역시 할인금액이 0원이라는 것을 효과적으로 전달한다. 모든것은 트레이드 오프의 산물이고 그러한 산물이 합당한 이유를 가져야한다는 것을 저자는 말하고 있다. 항상 이러한 것을 하는 합당한 이유가 뭘까 생각하는 습관을 들여보는 것이 좋겠다. 여기서 나는 변경 전의 클래스가 나은 것 같다는 생각이 든다. 일단은 코드 작성 의도가 제대로 전달이 되니까.

 

코드 재사용

상속을 사용해서 코드 재사용을 하기 위해 사용되는 방법인데 상속보다는 합성을 선호한다.

 

합성

https://inpa.tistory.com/entry/OOP-💠-객체-지향의-상속-문제점과-합성Composition-이해하기 

합성이 뭔지 합성을 사용해야하는 이유를 제대로 알려주셔서 갖고와 봤다. 

 

하지만 상속을 절대로 사용하지 말라는 것은 아니고 써야할때는 써야한다는 것을 저자는 말해준다.

'Java' 카테고리의 다른 글

[오브젝트 스터디] Chapter 05 책임 할당하기  (3) 2025.01.07
[오브젝트 스터디] Chapter 03 역할, 책임, 협력  (1) 2024.12.27
[오브젝트 스터디] Chapter 01 객체, 설계  (3) 2024.11.24
인프런 김영한 실전 자바 기본 편 후기  (4) 2024.10.11
'Java' 카테고리의 다른 글
  • [오브젝트 스터디] Chapter 05 책임 할당하기
  • [오브젝트 스터디] Chapter 03 역할, 책임, 협력
  • [오브젝트 스터디] Chapter 01 객체, 설계
  • 인프런 김영한 실전 자바 기본 편 후기
PENGU
PENGU
  • PENGU
    펭구 랩
    PENGU
  • 전체
    오늘
    어제
    • 분류 전체보기 (31)
      • Computer Science (6)
        • OS (0)
        • Network (0)
        • Algorithm (6)
      • 코테대비 (7)
      • Java (5)
      • Python (9)
        • 파이썬 문법 (8)
      • Project (1)
      • 이야기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • Computer Science
    • Operation System
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    조영호 오브젝트
    점프 투 파이썬
    백준 replace
    자바 재귀식
    피보차니수
    오브젝트 챕터2
    자바 피보나치의수
    오브젝트 자바
    파이썬 자료형
    swea1206
    책임 ㅜㅈㅇ심 설계
    파이썬 기초
    싸피 대비
    오브젝트 스터디
    백준 2460
    데이터 중심 설계
    점프투파이썬
    백준 코테
    조영호 자바
    백준 대비
    swea 자바
    파이썬
    백준 자바
    백준 코테 대비
    싸피 코테 대비
    오브젝트
    오브젝트 리뷰
    코테 대비
    swea view
    책임 중심 설계
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
PENGU
[오브젝트 스터디] Chapter 02 객체지향 프로그래밍
상단으로

티스토리툴바