오브젝트 : 코드로 이해하는 객체지향 설계 (조영호 저) 를 읽고 정리합니다.
객체지향 프로그래밍
객체지향의 핵심은 애플리케이션 기능 구현을 위해, 객체들이 협력하며 상호작용하는 것!
- 프로그래밍 관점에 치우쳐서 객체지향을 바라보지말자.
- 프로그래밍 관점에서 생각해보면, 우리는 기능을 만들기 위해 클래스를 짜고, 클래스 안에 메소드와 변수들을 채워넣어야한다.
- 하지만? 우리는 클래스들이 서로 상호작용한다고 생각하는게 아니라, 객체들이 서로 상호작용한다고 생각해야한다.
🤷 그럼 뭘 생각하며 객체지향을 구현해요?
-
어떤 객체가 필요한가?
: 기능 구현을 위해 어떤 클래스가 필요한지를 고민하는게 아니라, 어떤 객체가 필요한지 고민해야 한다.
-
객체는 기능을 구현하기 위한 협력체의 일원!
: 기능 구현을 위해 다른 객체에게 도움을 주거나 의존하고 있는지 생각해야 한다.
🤷♀️ 예?
무슨 소린지 모르겠다면, 도메인을 활용해 이해해보자.
-
도메인 (domain) : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
- 우리가 구현하려는 기능 = 문제를 해결하려는 수단 = 도메인
- Ex. 문제 : 영화를 보고 싶은데, 보려면 티켓이 필요해!
- Ex. 수단(도메인) : 티켓 예매
-
요구사항부터 구현까지 전부 객체의 관점으로 바라보자
- 도메인을 구성하는 개념들을 나열하고, 그 개념들을 객체와 클래스로 재구성하자.
- Ex. 티켓 예매 도메인을 구현하려면 어떤 상태와 행동이 필요할까?
: 영화, 예매, 상영, 영화별 적용되는 할인 정책, 할인 정책1, 정책 2...
- Ex. 이 상태와 행동들 중 유사한 친구들을 묶어서 하나의 개념으로 만들자.
: 할인정책1과 할인정책2는 >할인 정책< 이라는 점에서 공통점을 가지니까, 하나로 묶자.
- Ex. 이렇게 만든 개념을 클래스로 구현하자.
: 할인정책은 할인정책1이라는 객체와 할인정책2라는 객체를 가질 수 있는 클래스로 정리된다.
-
즉, 클래스의 구조가 도메인의 구조와 유사할 때 프로그램의 구조를 이해하고 예상하기 쉽다.
오케. 그럼 클래스는 어떻게 구현해야 하나요?
좋은 코드는? 변경하기 쉬운 코드. 변경하기 쉬운 코드는? 이해하기 쉬운 코드!
🧚 우리는 객체가 자율적인 존재라고 약속했어요!
- 객체가 자율적이고, 외부에 덜 의존적이게 되려면 외부 간섭을 최소화해야 한다.
- 나는 떡볶이 먹고 싶을 때 떡볶이 먹을거고, 만두 먹고 싶으면 만두 먹을건데 이상한 애가 찾아와서 아냐 넌 만두를 먹으려면 단무지가 필요한 애야~~ 하면 성질나니까..!
- 이를 위해 객체는 상태와 행동을 객체 내부로 묶고 (= 캡슐화), 외부에서의 접근은 허가된 친구만 가능하도록 제어한다 (= 접근 제어).
- 인터페이스와 구현의 분리 원칙
- 퍼블릭 인터페이스 (public interface) : 외부에서 접근 가능
- 구현 (implementation) : 외부에서 접근 불가능, 내부에서만 접근 가능
🤨 객체 접근 제어가 자율성에 어떻게 영향을 미치는지 잘 모르겠어요.
- 중요한건 변경을 관리하는 것!
- 프로그래머를 두 종류로 구분해보자.
- 클래스 작성자 : 새로운 데이터 타입을 프로그램에 추가한다
- 클라이언트 프로그래머 : 클래스 작성자가 만든 데이터 타입을 사용한다
- 클래스 작성자 A씨는 내가 신나게 짠 코드가 클라이언트 B씨에게 어떤 영향을 줄 지 걱정하고, 클라이언트 B씨는 빠르게 기능을 만들고 싶은데 A씨가 코드를 갑자기 바꿀까봐 걱정한다.
- 하지만 A씨의 코드 핵심은 안에 숨겨두고, 이 코드에 접근하는 간접적인 방법만 B씨에게 알려준다면?
- A씨는 내부 로직만 변경하면 되고, B씨는 내부 로직에 무관하게 접근 통로만 신경쓰면 된다!
- 우린 이걸 구현 은닉 (implementation hiding) 이라고 부르기로 했어요
- 정리 : 객체로의 접근을 제어해 사용자 영향 고려 없이 내부 구현을 변경 가능하게 만들 수 있다.
😯 근데.. 객체는 협력해야 한다고 하지 않으셨나요?
B씨가 A씨의 코드에 접근하는 간접적인 방식 = 객체 상호 작용 = 메시지를 주고 받는 행위
근데 우리 아까 비슷한 친구들을 묶어놓지 않았나요?
메시지를 받은 객체가 어떤 친구로 응답해야 하는지 어떻게 알 수 있을까?
🐳 상속은 몬가요..?
- 기존 클래스를 기반으로 새로운 클래스를 만드는 행위
- 서로 공통점이 있지만, 차이점도 있는 클래스를 쉽게 추가할 수 있다. (= 차이에 의한 프로그래밍)
- 그렇다고 목적이 코드 재사용은 아니에요~
- 부모의 인터페이스를 자식이 물려받을 수 있게 만드는게 중요!
- 즉 구현이 아닌 인터페이스 상속이 객체지향적으로 가치있는 상속
- 외부 객체가 봤을 때, 자식과 부모를 동일한 타입이라고 생각하게 만드는게 주 목적이다.
- 템플릿 메서드 (Template Method) 패턴
- 부모는 기본적인 알고리즘 흐름을 구현하고, 중간에 필요한 처리는 자식에게 위임하는 패턴
- 코드 (클래스) 의존성과 실행 시점 (객체) 의존성이 다를 때 확장 가능하다.
- 객체는 내가 어떤 클래스의 인스턴스와 협력하는지가 아니라, 내가 협력하는 친구가 내 메시지를 수신할 수 있는지 아닌지가 중요하다.
- 따라서 객체가 소통할 수 있는 창구를 부모에게 터두고 처리 자체는 담당 자식에게 맡기자.
- 상속을 통해 자식에게 부모의 인터페이스가 존재하기에 자식은 부모를 대신(업캐스팅)할 수 있다.
- 단, 이 경우 기능은 유연해져도 코드 가독성은 떨어질 수 있다 ^.ㅠ
🐳 다형성은요?
- 동일한 메시지를 전송했을 때, 메시지 수신 객체 클래스에 따라 실행되는 메서드가 달라지는 것
- 이를 위해 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
- Ex. 부모가 받을 수 있는 동일한 메시지를 전송했을 때, 각기 다른 자식이 메시지를 처리한 경우
- 메시지에 응답할 (= 실행될) 메서드를 컴파일 시점이 아닌 실행 시점에 정한다.
- 메시지와 메서드를 실행 시점에 바인딩한다 = 지연 바인딩 = 동적 바인딩
- 자식 클래스들이 순수하게 인터페이스만 공유하는 경우는?
- 마찬가지로 동일한 인터페이스를 공유하면서, 부모를 대신해 사용될 수 있기에 업캐스팅이 적용되며 다형적!
즉 상속과 다형성에는 추상화가 깔려있다.
추상화
최대한 인터페이스에 초점을 맞추고, 자식 클래스에게 결정권을 일임하는 것
📌 추상화를 통해 요구사항 정책을 높은 수준에서 서술할 수 있다.
세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다는 의미
- 자식 하나하나를 언급하는게 아니라, 인터페이스 수준에서 설명할 수 있으니까!
- Ex. 할인 정책 1, 할인 정책 2로 구분하지 않고 전체 할인 정책으로 구분
- 추상화를 통해 애플리케이션의 기본 협력 흐름을 기술할 수 있다 (= 상위 정책 기술)
- 디자인 패턴, 프레임워크 등 재사용 가능한 설계들은 모두 위와 같은 메커니즘을 활용한다.
📌 추상화를 통해 유연하게 설계할 수 있다.
책임의 위치를 정하기 위해 조건문을 사용하기보다는, 예외 케이스를 최소화하고 일관성을 유지해라.
- 설계는 포괄적으로 하고, 세부 사항은 자식이 처리하게 결정 위임
- 기존 구조를 수정하지 않으면서 새로운 기능을 쉽게 추가 & 확장할 수 있다.
- 컨텍스트 독립성 (context independency)
- 설계가 구체적인 상황에 결합되지 않아, 어떤 클래스와도 협력이 가능하다.
📌 트레이드 오프
- 구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다.
- Ex. 이상적으로는 인터페이스를 활용해 책임을 명확하게 하는게 좋지만, 또 현실적으로 생각했을 때 아주 작고 단순한 기능 하나만을 위해 인터페이스를 추가하는건 과할 수 있다.
- 따라서 모든 코드를 합당한 이유를 기반으로 설계하고 작성해야 한다.
코드 재사용
코드를 재사용할 땐 상속 말고 합성을 쓰기로 약속해요~
- 합성 : 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법
🔨 상속을 이용한 코드 재사용의 문제점
클래스를 통한 강결합
- 캡슐화 위반
- 부모 클래스의 구현이 자식 클래스에게 노출됨
- 즉, 자식이 부모에게 강하게 결합되고, 부모의 변경에 따라야 할 가능성이 생겨 코드 변경하기 어려워진다
- 설계를 유연하지 않게 만듬
- 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다
- 따라서 실행 시점에 객체 종류를 변경할 수 없다
🔨 합성을 이용한 코드 재사용의 장점
인터페이스를 통한 약결합
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용할 수 있다
- 메서드를 외부에 제공한다는 것만 알고, 내부 구현에 대해서는 전혀 모르는 상태!
- 즉, 상속과 달리 인터페이스를 통해 약하게 결합된다.
- 따라서 구현을 효과적으로 캡슐화할 수 있고, 변경시 의존하는 인터페이스만 교체하면 되어 변경이 쉽다.
⚒ 그렇다고 상속을 쓰지 말라는건 아니고..!
- 코드를 재사용할 때는 : 합성
- 다형성을 위해 인터페이스를 재사용할 때는 : 상속과 합성을 함께 조합하기
객체지향의 핵심은 객체들의 협력과 상호작용이 적절하게 이루어져야 한다는 것!
- 적절한 협력을 식별하고, 적절한 객체에게 협력에 필요한 역할을 할당하자
- 프로그래밍 관점에 치우치는게 아니라, 객체를 지향하는 관점에서 설계해야한다.