객체지향 프로그래밍
- 오늘은 오류 없이 작동 & 내일은 쉽게 변경가능한 코드를 만들자
객체지향 패러디임; 협력, 책임, 역할
- 객체도 사회적 동물
- 객체는 독립적인 존재 X
- 객체지향 세계에서는 공동체를 만들어 서로 상호작용하며 자신의 존재 여부를 증명(=객체 간의 상호작용을 통해서 Application의 필요한 작은 단위의 기능을 제공)
- 객체지향 세계에서 객체는 자아를 가지는 로봇이 됨
- 객체지향 패러다임책상, 침대 등등..... -> 이것들을 다 능독적인 존재가 되는, 이불이 사람을 덮는다 가 가능해지는 느낌
- 객체지향 세계에서는 독점은 없다
- Application간 목표를 달성하기 위한 기능을 쪼개서 적절한 객체에 분배하고 객체들 간의 적절한 협력을 만들어 주는 것이 목표
- 3번이 가장 중요
- 객체지향 스럽게 어플리케이션을 만드는 것은 작은 작은 기능, 책임을 가지고 있는 object들을 여러개 만들어서 다양하게 조합하여 다양한 기능을 만드는 것, 그렇게 설계 해 나가야됨
협력
- Application의 주요 기능을 구현하기 위해서 작은 단위로 쪼개진 기능을 설명하는 한 줄 요약본
- 협력은 객체간의 상호 작용을 통해서만 이루어짐
- 객체 간의 상호 작용은 메세지를 통해서만 가능
책임
- 협력에 참여하기 위해서 객체가 책임지고 있는 전체 기능 정의서
역할
- 동일한 책임을 수행하는 객체의 집합체
- 동일한 책임을 수행하는 객체가 여러 개 있을 경우, 협력을 개별적으로 만들지 않음
의존성
의존성(Dependency)란?
- A객체가 B객체에게 "give me the money"라는 문맥을 가지는 메시지를 보냈을 때 B객체가 A객체가 보낸 메시지를 처리할 수 있다라는 사실을 알고 있는 지식(정보)를 의존성이라고 말한다.
- 알고 있는 지식의 수준에 따라서 높은 의존성(강한 결합도), 낮은 의존성(느슨한 결합도)라고 표현하기도 한다.
- B객체의 변경이 A객체에게 얼마만큼의 영향을 주는가
Coupling; 강한 결합도, 낮은 결합도
- 의존성 관계가 높을 수록 강한 결합도라고 이야기하고 낮을 수록 느슨한 결합도라고 표현한다.
- 결합도(Coupling)란, B객체에 변경이 발생하는 경우, B객체를 의존하고 있는 다른 객체들에게 전이 되는 정도의 수준을 표현한 것이라고 보면 된다.
- 느슨한 결합도를 유지하기 위해서 아래와 같은 목표를 가져야 한다.
- 올바른 의존성 주입
- 변경을 방해하는 의존성을 제거
올바른 의존관계 형성
Constructor 함수를 통한 의존 관계 형성
- 의존하는 객체의 전체적인 행동에 대해서 지속적으로 의존 객체에게 메시지를 보내야 하는 경우에 사용하는 것이 좋음
- 왜 의존성을 Constructor함수를 통해 하는가?
- 어느 객체에 의존할지 런타임에 결정되기 때문
- 생성 후, 의존성에 대한 불변성 보장 및 코드 일관성 부분에서 장점이 있음
Parameter of Method를 통한 의존 관계 형성
- 생성자 방식과 반대로, 메서드가 실행되는 동안에만 일시적으로 의존 관계를 맺어도 무방할 때 사용하는 것이 좋다.
- 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에도 유용하다.
- Setter 방식과의 차이점은 특정 함수(Setter) 호출해서 의존 관계를 맺는 것이 아니라, 단순히 인자로 전달한다는 점이 다르다
Setter 함수를 통한 의존 관계 형성
- 의존하고 있는 대상을 변경할 수 있는 가능성을 열어두고 싶을 때 사용하면 좋음
- 의존 관계를 맺지 않은 상태에서 의존 대상에게 메시지를 요청하게 되면 NPE(Null Point Exception)이 발생하여 Application 에 치명적인 오류를 일으켜 정상 동작을 못하게 할 수도 있기 때문에 설계 시, 이 부분을 주의 해야함
변경을 방해하는 의존성 관계
- 변경에 따른 영향이 전파되는 범위가 너무 커서 변경을 할 수 없는 상황을 이야기한다. -> 레거시
변경을 방해하는 의존성 주입 방식
- Constructor 함수에서 new 키워드를 사용하여 구현체를 직접 할당하는 방식은 올바르지 않다. (파라미터로 받는게 아닌)
- 구현부에 숨겨저 있어 독자 입장에서 찾기 힘들 수도 있고, 구현체에 직접적으로 의존하는 것은 병경을 방해하는 시작점 이기 때문
- 무조건 나쁜것? -> 100% 나쁜것은 없지만 표준 클래스 또는 Default로 할당이 필요한 경우는 생성자 체이닝을 이용해서 직접 할당하는 방식을 가끔 채택하는 것 또한 나쁘지 않다
응집도와 결합도
응집도란?
- 클래스에 포함된 내부 요소(상태와 행동)가 하나의 책임(목적)과 연관된 정도와 책임을 수행하기 위해서 필요한 기능들이 하나의 연관 클래스에 잘 정의가 되어 있는지를 알려주는 척도이다.
응집도와 결합도는 비례 or 반비례 관계?
- 응집도가 낮아지거나 높아진다고 하더라도 결합도에는 영향을 줄 수 없는 구조
- 하나의 책임이 여러 개의 클래스들로 분산 되어 있고 분산된 클래스를 의존하는 객체들이 분산된 클래스의 내부 구현까지 알고 있다면 낮은 응집도와 높은 결합도를 가지는 구조가 될 것이다.
- 하지만, 캡슐화를 통한 내부 구현을 숨기는 형태를 가진다면 아무리 많은 클래스가 해당 클래스를 의존한다고 하더라도 높은 응집도와 낮은 결합도를 가지고 있는 클래스라고 이야기할 수 있지 않을까?
상속
상속이란?
- 부모 클래스(Super or 기반 클래스)가 가지고 있는 상태와 행동을 자식 클래스(Sub or 기반 클래스)가 물려 받아서 코드를 재사용할 수 있게 해주는 기법을 말한다.
상속을 사용하는 이유
- 변경에서 가장 큰 악 중 하나인 "코드의 중복"을 제거하기 위함
상속; 재사용의 함정
- 캡슐화 규칙 위반
- 상속은 단순히 부모 클래스의 행독만 상속 받는 것이 아니라, 상태까지 상속 받기 때문에 상속을 제대로 활용하기 위해서는 내부 구현을 알아야함
- Public Interface에 의존하는 것이 아니라 코드에 의존하는 설계 구조 탄생
- 부모 클래스와 자식 클래스 간 강한 결합이 생김
- 자식 클래스에서 호출하는 부모 클래스의 함수가 수정되거나 부모 클래스의 내부 구현이 변경되면 자식 클래스 또한 변경의 영향을 받아 자식클래스 또한 변경을 적용해야 하는 문제가 있다.
- 자식 클래스가 메서드 오버라이딩 시, 부모 클래스의 인터페이스를 사용할 때 의도하지 않는 동작을 수반하여 예상치 못한 오류를 만날수도 있다.
(ex. HashSet을 상속한 CustomeHashSet)
- 객체지향 이론과 SOLID 규칙을 지키면서 코딩을 하는 이유는 "변경이 유연한 설계"를 하기 위함인데 상속은 이것을 무력하게 만드는 기법이다
- 변겨의 유연함과 규칙이 깨진다
- 상속 관계는 컴파일 타임에 결정된다, 그런데 객체지향이 추구하는 방향성은 작은 기능들을 조합해 유연하게 문맥을 완성 시키는 것인데, 상속 관계의 경우는 컴파일 시점에 결정되기 때문에 런타임 시점에 문맥의 변경이 어렵다(OCP 위반)
- 의도 하지 않은 기능을 부모 클래스에게 상속 받아 자식 클래스의 기능 설계(or 규칙)와 다르게 동작할 가능성이 생긴다(불필요한 기능 상속)
상속을 사용 해도 될때
- 개념적으로 명확한 is -a 관계에 있는 경우, 상위 클래스가 확장할 목적으로 설계되었고 상속에 대한 설계도가 문서화 및 최신화 되어 있다면 괜찮음
합성
상속과 다른 점은?
상속
- 기반이 되는 클래스(Car)를 중심으로 변경이 필요한 부분들만 코드를 재사용(오버라이딩)하여 파생 클래스(Diesel Car, Gasoline Car)를 만들어 내는 구조
- 상속 관계는 컴파일 시점에 결정된다.
합성
- 기반 클래스에 필요한 기능을 제공하는 외부 객체를 런타임 시점에 적절하게 선택하여 주입하는 구조를 가진다.
- 합성 관계는 런타임 시점에 결정된다. 그래서 합성은 작은 기능들을 조합해서 다양한 기능을 만들어낼 수 있다.(OCP, DIP)
참고
https://i-am-your-father.notion.site/java-OOP-477254766b5440059732a48d0fba2780