본문 바로가기
스터디/오브젝트

오브젝트 02 - 객체지향 프로그래밍

by 디토20 2024. 2. 6.
반응형

 

 

오브젝트 02 - 객체지향 프로그래밍

 

 

2.1 영화 예매 시스템

- 영화, 상영, 할인 정책, 할인 조건이 있는 영화 예매 시스템이 존재한다.

 

 

2.2 객체지향 프로그래밍을 향해

2.2.1 협력, 객체, 클래스

  진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다. 이를 위해서는 아래의 두 가지에 집중해야 한다.

  1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
    • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
    • 따라서 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
    • 객체를 중심에 두는 접근 방법은 설계를 순하고 깔끔하게 만든다.
  2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
    • 객체를 협력하는 공동체의 일원으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다.
    • 객체 지향적으로 생각하고 싶다면 객체를 고립된 존재로 바라보지 말고 협력에 참여하는 협력자로 바라봐야 한다.
    • 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으분류하고 타입을 기반으로 클래스를 구현하라.

 

2.2.2 도메인을 따르는 프로그램 구조

  • 소프트웨어는 사용자가 원하는 어떤 문제를 해결하기 위해 만들어진다.
  • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
영화 예매 도메인을 구성하는 타입들의 구조
  • 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사해야한다.
  • 클래스 사이의 관계도 최대한 도메인 개념 관계와 유사하게 만들어 쉽게 이해할수 있어야 한다.

 

2.2.3 클래스 구현하기

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 boolean isSequence(int sequence) {
    	return this.sequence == sequence;
    }
    
    public Money getMovieFee() {
    	return movie.getFee();
    }
}

 

  • 인스턴스 변수의 가시성은 private이고 메서드의 가시성은 public라는 점을 주목하자.
  • 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다.
  • 경계의 명확성이 객체의 자율성을 보장하고 프로그래머에게 구현의 자유를 제공한다.

 

자율적인 객체
  • 객체는 상태행동을 함께 가지는 복합적인 존재다.
  • 객체는 스스로 판단하고 행동하는 자율적인 존재다.
  • 객체는 데이터와 기능을 객체 내부로 함께 묶어 캡슐화를 한다.
  • 접근 수정자를 통해 외부에서의 접근을 통제할 수 있는 접근 제어 메커니즘도 제공해 객체를 자율적인 존재로 만든다.
  • 퍼블릭 인터페이스 : 외부에서 접근 가능한 부분
  • 구현 : 내부에서만 접근 가능한 부분

 

프로그래머의 자유
  • 구현 은닉으로 인해 코드를 사용하는 개발자는 내부 구현은 무시한 채 인터페이스만 알고 있어도 기능을 사용할 수 있다.
  • 구현 은닉으로 인해 코드를 작성하는 개발자는 내부 구현을 마음대로 변경할 수 있다.

 

 

2.3 할인 요금 구하기

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.runni ngTime = runningTime;
        this.fee = fee;
        this .discountPolicy = discountPolicy;
    }
    
    public Money getFee() {
    	return fee;
    }
    
    public Money calculateMovieFee(Screening screening) {
    	return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

  • 이 메서드에는 어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않는다.
  • 단지 discountPolicy에게 메시지를 전송할 뿐이다.

 

2.3.1 할인 정책과 할인 조건

  • 할인 정책은 금액 할인과 비율할인 두가지로, 대부분의 코드가 유사하다.
  • 중복 코드를 제거하기 위해 공통코드를 부모 클래스에 보관 후, 상속받게 한다.
public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();
    
    public DiscountPolicy(DiscountCondition ... conditions) {
    	this.conditions = Arrays.aslist(conditions);
    }
    
    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);
}

 

  • DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만 실제로 요금을 계산 하는 부분은 추상 메서드인 getDiscountAmount 메서드에게 위임한다.
  • 실제로는 DiscountPolicy를 상속 받은 자식 클래스에서 오버라이딩한 메서드가 실행될 것이다.
  • 이처럼 부모 클래스에 기본적인 알고리 즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.

 

 

2.4 상속과 다형성

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

DiscountPolicy 상속 계층 클래스다이어그램
  • 어떤 클래스가 다른 클래스에 접근할 수 있거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스는 의존성이 있다고 말한다.
  • Movie는 구현 클래스가 아닌 추상 클래스에 의존하고 있다.
  • 코드의 의존성과 실행 시점의 의존성은 서로 다를 수 있다.
    • 즉, 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있음
    • 장점
      • 유연하며 쉽게 재사용 할 수 있음
      • 확장 가능
      • 디버깅이 쉬움
    • 단점
      • 코드를 이해하기 어려워짐
      • 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 함
      • 디버깅 하기 어려워짐

 

2.4.2 차이에 의한 프로그래밍

상속
  • 클래스의 코드를 수정하지 않고 재사용해서 새로운 클래스를 만들 수 있게 한다.
  • 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
  • 클래스 사이의 관계를 설정하는 것만으로 기존 클래스의 모든 속성과 행동을 새 클래스에 물려줄 수 있다.
  • 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.

 

2.4.3 상속과 인터페이스

  • 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.
  • 상속을 통해 자식 클래스는 자신의 인터페이스에 부모의 인터페이스를 포함하게 된다.
  • 결과적으로 자식 클래스는 부모가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수있다.
public class Movie {
    public Money calculateMovieFee(Screening screening) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

  •  Movie 객체는 자신과 협력하는 객체가 중요한것이 아니라 calculateDiscountAmount 메시지를 수신하는 것이 중요
  • Movie는 협력 객체가 calculateDiscountAmount 라는 메세지를 이해할 수 있다면 그게 어떤 클래스인지는 관심이 없음
  • 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고 부른다.
자식 클래스가 부모 클래스의 다입으로 변환되는 업캐스팅

 

 

 

2.4.4 다형성

  • 다형성이란 동일한 메시지를 수신했을  때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
    • Movie는 calculateDiscountAmount 라는 동일한 메세지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스에 달려있다.
  • 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
  • 다형성은 객체지향 프로그램의 컴파일 시간과 실행 시간의 의존성이 다를 수있다는 사실을 기반으로 한다.
  • 메시지와 메서드를 실행 시점에 바인딩 한다 -  지연 바인딩 또는. 동적 바인딩

 

2.4.5 인터페이스와 다형성

  • 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때가 있다.
  • 자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.
  • 추상클래스를 이용한 할인 정책과는 다르게 할인 조건은 구현을 공유할 필요가 없기때문에 인터페이스를 이용해 상속한다.

 

 

 

 

2.5 추상화와 유연성

2.5.1 추상화의 힘

추상화는 좀 더 일반적인 개념들을 표현한다.
  • 위의 그림은 자식 클래스를 생략한 코드 구조를 그림으로 표현한 것이다.
  • 이 그림은 추상화의 두 가지 장점을 보여준다.
    • 첫째, 요구사항의 정책을 높은 수준에서 서술 할 수 있다.
      • 영화의 예매 요금은 '금액 할인 정책'과 '두개의 순서 조건, 한개의 기간 조건'을 이용해 계산 할 수 있다 -> 영화예매 요금은 최대 하나의 할인정책과 다수의 할인 조건으로 계산 할 수 있다
      • 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
    • 둘째, 설계가 좀 더유연해진다.
      • 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.

 

2.5.2 유연한 설계

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

 

  • 위 방식의 문제점은 할인이 없을 경우를 예외 케이스로 취급하기 때문에 지금까지의 일관된 협력 방식이 무너진다.
  • 기존 경우에는 할인 금액의 계산을 DiscountPolicy에서 했지만, 이 경우에는 Movie에서 하게 되어 책임의 주체가 달라진다.
  • 따라서 일관성을 지키기 위해 NoneDiscountPolicy 클래스를 추가하자
public class NoneDiscountPolicy extends DiscountPolicy {
	@Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;		
    }
}

 

  • 중요한 것은 기존의 코드는 수정하지 않고 새로운 클래스(NoneDiscountPolicy)를 추가해 애플리케이션을 확장했다는 것이다.
  • 추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다.

 

 

 

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

  • NoneDiscountPolicy 정책은 getDiscountAmount 메서드를 사용하지 않아, 위처럼 설계를 변경 할 수도 있다.
  • 이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋으나, 현실적으로는 NoneDiscountPolicy 하나만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있다.
  • 구현과 관련된 모든 것들은 트레이드오프의 대상이 될 수 있다.

 

 

2.5.4 코드 재사용

  • 코드를 재사용하는 방법들 중에는 상속과, 합성이 있다.
  • 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법이다.
  • Movie가 DiscountPolicy의 코드를 재사용하는 방법이 바로 합성이다.
  • 왜 많은 사람들이 상속 대신 합성을 선호할까?

 

 

2.5.6 상속

  • 상속은 객체지향에서 코드를 재사용하기 위해 자주 사용된다.
  • 하지만 두가지 큰 문제점이 있다.
1. 상속은 캡슐화를 위반한다.
  • 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
  • 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
  • 캡슐화 약화는 클래스의 강결합을 만들어 부모 클래스가 변경되면 자식 클래스도 변경될 확률이 높아진다.
  • 결과적으로 코드 변경이 어려워진다.

 

2. 설계를 유연하지 못하게한다.
  • 상속은 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다.
  • 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.

 

2.5.5 합성

  • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
  • Movie는 DiscountPolicy의 인터페이스를 통해 약결합 된다.
  • Movie는 DiscountPolicy가 외부에 calcuateDiscountAmount 메서드를 제공한다는 사실만 알고, 내부 구현은 전혀 모른다.
  • 합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.

 

1. 효과적인 캡슐화
  • 인터페이스에 정의된 메시지를 통해서만 재사용이 가능해 구현을 효과적으로 캡슐화 할 수 있다.

 

2. 쉬운 인스턴스 교체

 

public class Movie {
	private DiscountPolicy discountPolicy;
    
    public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy= discountPolicy;
    }
}


Movie avatar = new Movie("아바타", Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(Money.wons(800), ... ));
    
avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1, ... ));

 

  • 간단하게 실행 시점에 할인 정책을 변경할 수 있다.

 

 

 

 

 

 

 

 

728x90
반응형

댓글