본문 바로가기
Study/오브젝트

[오브젝트] 2장. 객체지향 프로그래밍

by Nahwasa 2022. 11. 23.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.


    CHAPTER 02. 객체지향 프로그래밍

    01 영화 예매 시스템

    - 코드 (github)


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

    - 협력, 객체, 클래스

    • 대부분의 사람들은 클래스(class)를 경험한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
    • 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞춰야 얻을 수 있다.
    • 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다. 협력에 참여하는 협력자로써의 객체가 어떤 상태와 행동을 가지는지 결정한 후, 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현해야 한다.

     

    - 도메인

    • 도메인(domain)은 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야이다.
      • 예를들어 영화 예매 시스템의 목적은 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는 것
      • ☆개인적으로 생소한 용어였다. 내 경우엔 "소프트웨어로 해결하고자 하는 문제 영역" 혹은 요구사항 보다는 좀 더 큰 범위이지만, "요구사항" 정도로 바꿔서 읽으니 잘 읽히긴 했다.
    • 객체지향 패러다임은 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있게 해준다.
    • 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있다.
    • 따라서 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
    • 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.

     

    - 클래스의 내부와 외부를 구분

    • 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다.
    • 외부에서는 객체의 속성에 직접 접근할 수 없도록 막고, 적절한 public 메서드를 통해서만 내부 상태를 변경할 수 있게 해야 한다.
    • 경계의 명확성이 객체의 자율성을 보장
    • 프로그래머에게 구현의 자유를 제공

     

    - 자율적인 객체

    • 객체는 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재
    • 객체는 스스로 판단하고 행동하는 자율적인 존재
    • 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현
    • 캡슐화 : 데이터와 기능을 객체 내부로 함께 묶는 것
    • 접근 제어 : 대부분의 객체지향 프로그래밍 언어들은 캡슐화에서 한걸음 더 나아가 접근 제어(access control) 매커니즘도 보통 함께 제공됨
      • 접근 수정자(access modifier) : public, protected, private 등을 사용
    • 다른 객체에게 원하는 것을 요청하고는 객체가 스스로 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.
    • 캡슐화와 접근 제어를 통해 객체를 외부에서 접근 가능한 부분(public interface)와 오직 내부에서만 접근 가능한 부분(implementation)으로 나눌 수 있다.
      • public interface(퍼블릭 인터페이스) : public으로 지정된 메서드만 포함
      • implementation(구현) : private 메서드, protected 메서드, 속성
      • ☆자바의 경우 public은 전부 접근 가능, private는 자기 자신과 동일한 클래스에서만 접근 가능(동일 클래스라면 다른 객체(인스턴스)도 접근 가능함), default(접근 수정자를 안쓴거)는 동일 패키지에서 접근 가능, protected는 동일패키지 및 다른 패키지에 있더라도 자식(하위) 클래스면 접근 가능.

     

    - 구현 은닉

    • 클래스 작성자(class creator) : 새로운 데이터 타입을 프로그램에 추가
      • ☆ 라이브러리를 제공하는 쪽이라고 보면 됨. API를 통한 서비스라면 API를 만들어 제공하는 쪽에 해당
      • 클라이언트 작성자에게 필요한 부분만 공개해야 함. 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써, 클라이언트 작성자에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있음. -> 즉, public 영역을 변경하지 않는다면 코드를 자유롭게 수정할 수 있음.
    • 클라이언트 작성자(client programmer) : 클래스 작성자가 추가한 데이터 타입을 사용
      • ☆ 라이브러리를 사용하는쪽. API를 통한 서비스라면 제공된 API를 호출하는 쪽에 해당
      • 클래스 작성자가 숨겨둔 부분은 접근할 수 없으므로 내부의 구현은 무시한 채 인터페이스만 알고 있으면 되므로 담아둬야 하는 지식의 양을 줄일 수 있음

     

    - 협력하는 객체들의 공동체

    • ☆ 47page의 코드 관련해서, 1. isLessThan, isGreaterThanOrEqual 보다 Comparable을 implements해서 compareTo를 두는게 더 낫지 않았을까? -> 좀 더 명확한 메소드명으로 표기할 수 있으니 isLessThan, isGreaterThanOrEqual도 좋은 것 같다. 2. 오브젝트 책 깃헙에 가보면 equals랑 hashcode 함수도 있는데, '@Override' 표기가 없다. -> 안써도 동작은 하지만, 명시적으로 써주는게 더 좋을 것 같다.
    • 협력(collaboration) : 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용
    • 객체지향 프로그램을 작성할 때는 먼저 협력의 관점에서 어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다.
    • ☆ 인터페이스 = 메시지 = 요청(request)
    • ☆ 구현 = 메서드 = 응답(response)
    • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐이다. -> 메시지를 수신한 객체는 스스로 결정에 따라 자율적으로 메시지를 처리할 방법(메서드)를 결정한다.

    03 할인 요금 구하기

    - Movie 클래스 코드 어디에도 할인 정책을 판단하는 코드는 존재하지 않는다. 단지 discountPolicy에게 메시지를 전송할 뿐이다.

     

    - 할인 정책인 금액 할인 정책과 비율 할인 정책은 대부분의 코드가 유사하고 할인 요금을 계산하는 방식만 조금 다르다.

    • 중복 코드를 제거하기 위해 공통 코드를 부모 클래스에 두고, 금액 할인 정책과 비율 할인 정책 클래스가 상속받게 한다.
    • 이 때 프로그램 내에서 부모 클래스의 인스턴스를 생성할 필요가 없으므로 추상 클래스(abstract class)를 사용했다.
    • 이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLACTE METHOD 이라고 부른다.

     

    - ☆ 56page 2번째 코드 관련 : 빌더로 구현하는게 더 깔끔하지 않을까? -> 전부 필수값이니 빌더로 구현하는건 오류를 발생시킬 여지가 있어 별로일 것 같고, 빌더에서 필수값을 체크하게 변경한다면 책에 나온 예시에서는 사용할 의미가 없을 것 같다.


    04 상속과 다형성

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

    • 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다. (그림 2.7에서 코드상으로는 Movie는 DiscountPolicy에만 의존)

    • 하지만 Movie 인스턴스는 실행 시에 추상 클래스인 DiscountPolicy가 아니라 AmountDiscountPolicy 혹은 PercentDiscountPolicy에 의존해야 한다.
    • ☆ 설계시 의존성은 컴파일 시간 의존성과 실행 시간 의존성 중 어떤걸 고려해야 할까? -> 컴파일 시간 의존성을 고려하면 될 것 같다.
    • 코드의 의존성(컴파일 시간 의존성)과 실행 시점의 의존성은 서로 다를 수 있다.
    • 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다.
    • 다만, 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기는 어려워진다.
    • 트레이드오프 : 설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다.

     

    - 상속과 인터페이스

    • 일반적으로 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각한다.
    • 하지만 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
      • 상속을 통해 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있다.
    • 업캐스팅 : 자식 클래스가 부모 클래스를 대신하는 것
    • 구현 상속 : 서브클래싱(subclassing), 코드를 재사용하기 위한 목적으로 상속을 사용하는 것. 지양하는 것이 좋다.
    • 인터페이스 상속 : 서브타이핑(subtyping), 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것. 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.

     

    - 코드 재사용 (70 page 내용임) 

    • 위에서 일반적으로 대부분의 사람들은 상속의 목적이 재사용이라고 얘기했다.
    • 일반적으로 코드 재사용을 위해서는 상속보다는 합성(composition)을 선호하는 것이 더 좋은 방법이다.
      • 합성 : 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법
    • 상속의 단점 1 - 캡슐화를 위반 : 상속을 이용하기 위해서는 부모 클래스의 내부구조를 잘 알고 있어야 한다.
      • 캡슐화의 약화 -> 자식과 부모 클래스가 강하게 결합 -> 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률이 높음 -> 상속을 과도하게 사용할 시 코드를 변경하기도 어려워짐.
    • 상속의 단점 2 - 설계가 유연하지 않다. : 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시 결정하므로 실행 시점에 객체의 종류를 변경할 수 없다.
    • 코드 재사용을 위해 합성을 사용할 경우의 장점
      • 인터페이스를 통해 합성으로 이용하려는 객체와 약하게 결합됨 -> 즉, 인터페이스에 정의된 메시지를 통해서만 코드를 재사용할 수 있다. -> 구현을 효과적으로 캡슐화할 수 있고, 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
    • 상속을 절대 사용하지 않아야 한다는 것은 아니다. 코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만, 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.

     

    - 다형성

    • 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 개체의 클래스가 무엇이냐에 따라 달라진다(즉, 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다.).
    • 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
    • 따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. (인터페이스가 동일해야 한다)
    • 상속은 인터페이스를 통일하기 위해 사용한 구현 방법 중 하나이다.
      • 상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있으므로 대부분의 사람들은 다형성을 얘기할 때 상속을 함께 언급하지만, 상속이 다형성을 구현할 수 있는 유일한 방법은 아니다.
    • 다형성을 구현하는 방법은 매우 다양하지만, 메시지에 응답하기 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
      • 즉, 메시지와 메서드를 실행 시점에 바인딩한다. -> 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)
    • ☆ 다형성은
      • 컴파일 시간 의존성과 실행 시간 의존성이 달라야 함
      • 외부 인터페이스가 존재해야만 함
      • 실행 시간 의존성이 바뀔 때 컴파일 시간 의존성과 동일한 외부 인터페이스를 지원해야 함

     

    - ☆ 상속을 통한 다형성

    • ☆ 외부 인터페이스가 없는 상태에서의 상속은 다형성이 아니다.
      • This principle can also be applied to object-oriented programming and languages like the Java language. Subclasses of a class can define their own unique behaviors and yet share some of the same functionality of the parent class. (oracle java tutorials - Polymorphism)
      • e.g. @MappedSuperclass 를 통해 공통 매핑 정보를 상속받아 사용하는 경우 -> 혹시 공통 함수를 생성하더라도 실행 시점에서 업클래스할 일이 없으므로 다형성일 수 없음.
    • 상속은 다형성을 구현하는 하나의 방법 중 하나이다.
    • ☆ 실행 시간 의존성이 바뀔 때 컴파일 시간 의존성과 동일한 외부 인터페이스를 지원해야 함. 다만 추가로 더 넣는건 괜찮음(부모가 3개라고 자식도 상속받은 3개 외에 추가로 만들지 않을 필요는 없음)
    • ☆ 결론적으로 상속을 통한 다형성 구현은
      • 부모클래스를 상속(extends) 또는 구현(implements)을 한다. (이 때 인터페이스가 없다면 다형성과 관련 없음.)
      • 함수를 재정의한다. (안하면 그냥 구현 상속(subclassing))
      • 그리고 실행 시점에 메서드가 결정된다.

    05 추상화와 유연성

    - 추상화의 힘

    • 추상화 : 구현의 일부(추상 클래스인 경우) 또는 전체(자바 인터페이스 경우)를 자식 클래스가 결정할 수 있도록 결정권을 위임한다.
    • 장점1 : 추상화의 계층만 따로 떼어 놓고 보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
      • 세부사항(금액 할인 정책과 비율 할인 정책을 사용한다)에 억눌리지 않고 상위 개념(할인 정책이 존재한다)만으로 도메인의 중요한 개념을 설명할수 있게 한다.
      • 재사용 가능한 설계의 기본을 이루는 디자인 패턴(design pattern)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용한다.
    • 장점2 : 설계가 좀 더 유연해진다.
      • 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.

     

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

    • 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다.
    public class Movie {
        public Money calculateMovieFee(Screening screening) {
        	if (discountPolicy == null) {
            	return fee;
            }
            return fee.minus(discountPolicy.calculateDiscountAmount(screening));
        }
    }
    • 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택해야 한다.

     

    • 방법1 : 기존 클래스 구조 그대로 NoneDiscountPolicy 클래스 추가
      • 문제 : Money.ZERO로 의도를 전달한 건 좋았으나, 애초에 부모 클래스인 DiscountPolicy에서 할인 조건이 없을 경우에는 getDiscountAmount()를 호출하지 않으므로 사실 어떤 값을 넣더라도 상관이 없다. -> 따라서 부모인 DiscountPolicy 레벨에서 해결을 해줘보자(->방법2).
    public class NoneDiscountPolicy extends DiscountPolicy {
        @Override
        protected Money getDiscountAmount(Screening screening) {
            return Money.ZERO;
        }
    }

     

    • 방법2 : 방법1에서 문제가 됬던걸 해결하기 위해 DiscountPolicy를 인터페이스로 바꾸고, NoneDiscountPolicy가 getDiscountAmount()가 아닌 calculateDiscountAmount()를 오버라이딩 하도록 변경.
      • 이상적으로는 방법2가 더 좋지만, 현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있을 것이다. 어쨌든 방법1의 NoneDiscountPolicy 클래스 역시 할인 금액이 0원이라는 사실을 효과적으로 전달했었다. -> 구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다. "고민하고 트레이드오프하라."
    public class NoneDiscountPolicy implements DiscountPolicy {
        @Override
        public Money calculateDiscountAmount(Screening screening) {
            return Money.ZERO;
        }
    }

     

    댓글