한번 읽으면 두번 깨닫는 객체지향 프로그래밍 내용 정리
추천사
현재의 소프트웨어 개발 및 운영 환경은 MSA, 클라우드가 대세. 마이크로 서비스는 비즈니스 기능 단위의 서비스를 작게 쪼개 API 단위로 개발하여, 소프트웨어 구조에서 개발과 유지보스를 더욱 쉽게 만들어주는 구조. 마이크로 서비스의 원천은 결국 객체지향의 객체이다. 따라서 객체지향을 이해하고 객체지향적으로 프로그래밍하는 것은 여전히 클라우드 시대에서도 제일 중요한 부분이다. - 강상진 -
최근 IT 개발과 서비스 환경이 클라우드로 이동하면서 가장 화자되는 용어는 MSA, CI/CI(Continuous Integration/Continuous Deployment) 이다. 이 둘의 철학과 장점에 많은 사람들이 동의하지만 실제 구현을 위해서 많은 어려움을 겪는다. MSA에 관련된 책들을보면, 많은 내용이 객체지향과 닮아있을 뿐 아니라 그 뿌리가 같은 바탕에서 시작되었다고 느낄 것이다. 오래된 개념 같지만 다시 재조명이 필요한 객체지향이라는 개념을 쉽게 접근할 수 있도록 해준다. - 장현태 -
Chapter 1. 발상의 전환
[객체지향의 정의와 목표의 전환]
1. 대부분 개발자가 기능 중심 개발에 매달리는 이유 - 열악한 개발환경과 촉박한 납기. 본래부터 최저가의 프로젝트로 촉박한 일정이 고객의 요구사항 변경으로 더욱 촉박해진다. 고객은 프로젝트 구성원보다도 훨씬 더 기능중심적으로 생각한다.(고객이기 때문에 매우 당연함) 이 사태가 당장 눈앞에 해치워야 할 요구사항의 기능을 개발하는데 급급해진다.
2. 기능 중심 개발 - 코드 품질 저하. 당장 기능구현은 당연히 최우선되어야 한다. 하지만 기능 구현에만 집착하면 더 중요한 다른일 (유지보수 용이, 확장성 등)들은 신경 쓸 여력이 없어진다. 개발자는 의도하지 않아도 기능 중심 개발로 내몰린다. 이는 코드의 중복, 코드 속성의 과도한 노출, 코드 메소드의 과도한 노출, 배치의 일관성 상실, 가독성 저하, 의존성 증가, 사이드이펙트 증가, 재사용 불가, 코드수정 및 추가 디버그의 어려움, 유지보수와 고도화 프로젝트에 악영향이라는 부작용을 낳는다. -> 기능 중심 개발은 비록 작동하더라도 악취가 풍긴다. (code smell) 나중에 개선하고 싶어도 스파게티 코드로 고치기 어렵고 유지보수가 힘들며 추가 프로젝트도 힘들다.
3. 유연한 소프트웨어는 코드 품질 향상 - 고객이 원하는 좋은 소프트웨어 = 고객이 원하는 기능 구현 + 유연한 구조
[유연한 소프트웨어가 되었을 때의 특징]
. 코드이 중복이 거의 없다 : 하나의 클래스는 하나의 일만 한다는 원칙을 잘 지키면, 코드 중복이 거의 없다.
. 코드 속성과 메소드의 캡슐화가 잘 되어 있다 : 캡슐화 원칙을 잘 지키면, 보호해야할 속성, 감춰야 할 메소드가 잘 정리되어있어 코드의 의존성이 낮아지고, 가독성이 좋아진다.
. 코드 배치의 일관성이 잘 지켜졌다. : 메소드는 자신의 이름에 해당하는 로직만 담기고, 변수 배치의 일관성이 있으며 다른 클래스에 무질서하게 의존하지 않는다.
. 코드 가독성이 좋다. : 유연한 소프트웨어는 클래스의 속성과 메소드가 필요한 분량만 가진다. 코드의 길이가 너무 짧지도 너무 길지도 않다.
. 코드 의존성이 낮다. : 각 클래스, 메소드, 속성의 역할이 명확하기 때문에 의존을 연결할 때 어떻게 해야 효과적인지 눈에 잘 들어오고, 필요한 코드에만 의존한다.
. 사이드 이펙트 감소 : 하나의 코드 수정이 다른 코드로 영향을 끼치는 상황이 줄어들어 사이드 이펙트가 감소한다. 그래서 코드를 수정할 때 전보다 안심하고, 수정 후 버그 테스트가 수월하다.
. 코드 재사용이 쉽다. : 코드 의존성이 낮아지고 가독성이 좋아지면 코드를 모듈화하기 좋아진다. 재사용이 좋아지면 개발 생산성도 대폭 향상된다.
. 코드 수정,추가,디버그가 쉽다. : 실무에서 디버그는 개발만큼 중요하다. 디버깅은 결국 코드를 수정하는 작업이다. 코드 품질이 좋아지면 수정 작업도 편해진다.
. 유지보수 와 고도화 프로젝트하기 좋다. : 유연한 소프트웨어는 고객의 수정,확장 요구사항을 더 빠르게 구현할 수 있기 때문에 고객은 더 만족할 수 밖에 없다. 개발자는 생산성이 높아져 업무하기가 수월해진다.
4. 관계의 의존성은 낮게, 기능의 집중도는 높게 (= 높은 응집도와 낮은 결합도)
객체지향에서는 기능보다 관계가 더 중요하다. 소프트웨어의 모듈은 하나의 일만 잘 수행해야 하고, 하나의 모듈이 다른 모듈에 의존하는 경우를 최대한 줄요야 한다.
5. 객체지향의 정의와 목표
"객체지향은 낮은 관계의 의존성과 높은 기능의 집중도를 지향하여, 소프트웨어의 유연함을 극대화하는 개발 기법이다."
객체지향의 정의 = 객체지향의 목표 = 객체지향적인 개발
= 낮은 관계의 의존성과 높은 기능의 집중도 = 유연한 소프트웨어 = 좋은 코드 품질
[고정관념의 전환]
1. 객체지향의 구현 대상은 현실 세계? >> 객체지향은 현실 세계를 구현하기 보다는 새로운 세계를 창조한다.
2. 객체지향은 현실 세계와 동일하다? >> 객체지향 소프트웨어의 객체 = 현실 세계의 사물 특성 + 주도성
3. 객체 하나하나를 잘 만들어야 한다? >> TDD 테스트 주도 개발. 로직 구현보다는 객체가 외부에 노출하는 인터페이스를 잘 설계하는 것. 객체지향적으로 리팩토링 하면서 객체 내부와 객체 간의 관계를 깔끔하게 정리하는 것.
"점(객체 하나하나)보다 선(객체 간의 관계)로 접근한다."
4. 기술을 완벽하게 적용해야 한다? >> 전문가 조언은 얻을 것만 얻고 버릴 것은 버리자. 예를 들어 "GoF의 디자인 패턴" 책에는 23개의 디자인 패턴이 있다. 현장에서 많이 쓰고 객체지향의 원리를 잘 알 수 있는 패턴만 골라 익혀 빠르게 현장에서 쓰고 이 후 하나씩 패턴을 익힌다. UML을 지나치게 표준에 맞춰 그릴 필요 없다. 의사소통이라는 더 큰 목표에 맞춰 편하게 그리자. 현장에서 바쁜 일정과 여러 개발자와 협업하다 보면 꼼꼼하게 설계하지 못할 때도 있다. 규칙을 지향은 하되 완벽하게 지키지 않아도 큰 해는 없다. 보통은 납기준수가 가장 우선적인 목표기 때문이다.
5. 정리
"객체 지향의 구현 대상은 보통 새롭게 창조된 세계이다"
"객체지향은 현실 세계보다 훨씬 주도적이다"
"객체 하나하나를 잘 만드는 것보다 객체 간의 협력에 집중한다."
"기술을 완벽하게 적용할 필요까지는 없다. 우선 할 수 있는 부분부터 먼저 적용한다."
Chapter 2. 객체지향을 돕는 도구
[객체지향 생각의 도구(관찰, 추상화, 패턴인식)]
1. 프로그래밍 언어의 사상을 배우고 따른다는 것 - 언어의 문법을 배우고 따른다. 언어의 사상을 배우고 따른다.
2. 절차지향 언어의 사상과 단점
객체지향 이전의 언어는 주로 절차지향 언어였다. 절차지향 언어는 처리-판단-반복의 기초 문법, 함수 등의 명령어를 활용하여 위에서 아래로(절차적으로) 수행하는 언어이다. 또한 함수를 이용하여 로직의 모듈화를 할 수 있다. '어떤 기능'을 함수로 모듈화 하고 이를 재사용하는 것이다. 그러나 절차지향의 단점은 소프트웨어가 커지면 전역 변수를 여러 함수에서 같이 조작할 수 있다. 함수의 역할은 객체지향의 클래스보다 역할이 한정될 수밖에 없다. 함수는 if문 for문 처럼 명령어의 한 요소처럼 쓰인다. 소프트웨어가 커지고 복잡해질수록, 효과적인 모듈화, 재사용성에 한계가 있다.
3. 객체지향 언어의 사상과 장점
절차지향 언어의 단점을 개선하고 더 나은 장점을 추구하고자 객체지향이 탄생했다. 객체지향은 프로그램을 처리-판단-반복-함수호출의 명령어들의 연결이라고 보는 과거 시각에서 벗어나 여러개의 독립된 단위, 객체들의 모임으로 접근했다.
"객체지향은 사람이 사물(물체)에 대해 인식하는 사고를 거의 그대로 활용하여 프로그래밍할 수 있다 .객체지향은 인간이 이해하기 좋은 언어이다."
절차지향 언어는 데이터를 대부분 공동으로 관리하여 여러 함수가 데이터를 함께 쓴다. 때문에 데이터를 여러 함수가 같이 의존하게 된다. 이렇게 의존성이 높으면 A함수가 데이터 조작으로 데이터를 함께 공유해서 쓰고 있는 B함수의 작동이 잘못될 수 있는 치명적인 버그가 생길 수 있다.
객체지향은 '데이터 관리 주체'가 '데이터를 소유한 객체'로만 명확하게 지정되도록 유도한다. 그래서 객체 간에 데이터를 의존하는 경우가 낮아진다.
객체지향의 장점 -> 사람이 사물을 바라보는 익숙한 관점을 프로그래밍으로 구현 -> 객체지향에서의 사물은 객체 -> 객체는 속성과 기능 2가지가 있다 -> 속성과 기능을 소유한 객체 스스로 책임지게 유도함 -> 관계의 의존성을 낮추고, 기능의 집중도는 올리는 출발점.
4. 관찰 >> 추상화 >> 패턴인식
관찰단계에서 의미있는 데이터만 발췌하고, 중복된 데이터는 하나로 합칠 수도 있다. 추상화를 통해 반복적으로 동리하게 인식할 수 있는 패턴들이 있다. 이는 객체간의 관계와 관련되거나 반복되는 로직일 수도 있다. 나쁜 패턴은 버리고, 좋은 패턴을 적절하게 적용할 줄 알아야 한다. 객체 간의 관계를 잘 조직화 하도록 도와주는 규칙과 노하우가 있다. "디자인 패턴"과 "리팩토링"이라는 기법이다.
5. 관찰 : 사물이나 현상을 자세히 살펴보는 것
관찰로 사물과 현상 관련 데이터를 인식한다. 이때 점보다 선으로 접근한다. 이 데이터는 사물과 현상의 본질을 꿰뚫는 단서를 제공해야 한다. 이 사물의 관찰과정에서 드러나는 데이터들은, 사물의 중요한 특징을 누락하지 않아야 한다. 이 특징들이 지나치게 많아 과잉정보가 되어, 사물의 판단과 해석에 지장을 주어서는 안 된다.
6. 추상화 : 우리가 인식한 많은 데이터 중 의미 있는 데이터만 가져오고 나머지는 버리고, 더 큰 단위의 의미 있는 정보로 만들 수 있다'
점의 추상화 = 많은 데이터 중에 의미 있는 데이터만 가져오고 나머지는 버린다.
선의 추상화 = 의미 있는 데이터를 모아 더 큰 단위의 의미 있는 정보로 만든다.
7. 패턴인식 : 관계의 의존성은 낮추고, 기능의 집중도를 높이는 방법은 개발 경험을 통해 지속적으로 개선된다.
디자인 패턴은 점(코드,함수,로직)의 재활용이 아니라 선(관계)의 재활용이다.
[객체지향 표현의 도구(UML)]
UML(Unified Modeling Language)는 객체지향 모델링을 시각적으로 나타내는 도구이다.
UML 종류
- 유스 케이스 다이어그램 : 요구분석단계에서 업무의 전체 시스템 구성을 한눈에 파악할 때 도움.
- 클래스 다이어그램 : 클래스의 정보와 클래스 간의 관계를 한눈에 파악하는데 도움.
- 시퀀스 다이어그램 : 객체와 객체 사이의 메세지 흐름을 표현하여 한눈에 파악하는데 도움.
- 상태차트 다이어그램 : 객체 내부의 행동의 흐름을 그려 이벤트별 상태 변화를 한눈에 파악하는데 도움.
- 액티비티 다이어그램 : 순서도와 비슷하여 객체들의 활동순서를 파악하는데 도움을 줘 작업의 흐름을 한눈에 파악하는데 도움.
- 컴포넌트 다이어그램 : 레고조립 설명서처럼 완성된 모듈끼리 서로 어떻게 연결되는지 컴포넌트 간의 관계를 한눈에 파악하는데 도움.
- 배포 다이어그램 : 배치 어플리케이션과 서버가 어떻게 배치가 되었는지 보여줘서 배치 상태를 한눈에 파악하도록 도움.
Chapter 3. 객체지향의 넓이
[객체지향의 기본 요소 5가지]
1. 객체(Object)
객체는 사람이 인식할 수 있는 최소한의 의미를 가진 사물이다. + 객체는 세상에서 유일한 실체가 있는 사물이다. = 객체는 사람이 이식할 수 있는 최소한의 의미를 갖고, 유일하고, 실체로 존재하는 사물이다.
2. 클래스(Class)
분류와 범주의 차이에서, 분류는 종류별로 구분하여 체계적이고 계층적으로 정리한 Tree 와 같고, 범주는 상세 목록에 해당하는 Node명이라 할 수 있다. 분류와 범주를 통해 우리는 세상의 사물과 생명의 종류를 추상화(단순화)시켜 바라볼 수 있다.
분류와 범주 ≒ 클래스 = 추상적 ; 객체화 = 객체 = 구체적
클래스는 코드로 만든 설계도일 뿐, 소프트웨어 세상으로 나온 구체적인 실체는 아니다. 클래스를 소프트웨어로 탄생시키는 작업을 객체지향 언어에서 클래스를 기반으로 객체를 생성한다고 표현한다.
3. 속성(Attribute)
속성은 객체가 가지고 있는 고유 값이고 변수 혹은 상수라고도 한다. 뿐만 아니라 객체 자체를 변수로 받을 수 있다. 좋은 설계를 위해 어떤 속성을 정의해야 할까? 우리는 객체에 해당하는 속성을 완벽하게 찾는 것을 목표하기 보다, 속성의 캡슐화만 주의하면 된다. 속성은 ' 객체가 주어진 역할을 최대한 잘 수행하기 위해서, 어떤 기능이 필요할까?' 하는 관점으로 접근해야 한다.
. 속성은 객체가 가진 고유한 값이다.
. 객체의 속성을 추출할 때는 현실 세계에 존재하는 대상을 객체로 만들거나,
. 현실세계에는 없는 새로운 객체를 만들어야 하는 두 가지 상황이 있다.
. 완벽한 속성을 추출하기 위해 애쓸 필요는 없다.
. 속성을 현실 속 사물과 완벽하게 동일하게 구현할 필요도 없다.
. 객체의 기능을 가장 잘 발휘할 수 있는 속성을 찾아야 한다.
. 객체가 다른 객체와 잘 협력할 수 있도록 돕는 속성을 찾아야 한다.
. 이것은 점(객체 하나나하나)보다 선(객체 간의 관계)으로 접근하면 지킬 수 있다.
4. 메소드(method)
객체지향에서는 하나의 객체를 크게 속성과 메소드로 나눈다. 메소드를 절차지향의 함수처럼 로직관점이 아닌 객체 설계 관점으로 시야를 넓게 가진다. 즉, 객체가 제 역할을 수행할 때 필요한 기능은 무엇일까 생각하는 관점에서 메소드를 작성한다. 캡슐화를 지키고 속성을 다른 객체가 접근하지 못하게 private으로 선언하고, 메소드로만 접근하도록 구성한다.
. 메소드는 절차지향의 함수처럼 활용할 수 있다.
. 메소드는 객체가 자신 또는 다른 객체에게 제공하는 '기능'이다.
. 메소드는 객체가 외부에 제공할 기능(행동)을 적절한 문장으로 정의하고, 그 기능을 코드로 구현한 결과물이다.
. 메소드는 객체의 속성을 변경하는 유일한 통로가 되어야 한다.
. 메소드는 다른 객체와 상호작용하는 통로이다.
5. 생성자(Constructor)
생성자는 객체가 처음 생성될 때 호출되는 메소드이다. 생성자의 인자값을 다르게 주어서 여러 생성자를 호출하는 오버로딩이 많이 사용된다. 생성자가 필요없다면 생성자를 구현하지 않아도 된다.
[객체지향의 근본 조건 7가지]
객체지향 보물지도 구성
객체(클래스) - 클래스는 코드로 만든, 태어날 객체의 설계도이며, 객체는 클래스라는 틀을 통해 실체로 태어난 존재이다.
클래스 그룹 - 부모 클래스와 자식클래스 구성원들의 그룹이다. 최상위에 부모 클래스 또는 인터페이스가 있고 상속받은 자식 클래스들이 존재한다.
부모클래스(인터페이스) - 클래스 그룹에서 유일하게 존재하는 최상위 클래스
자식클래스(상속 클래스) - 부모 클래스로부터 속성과 기능을 상속받은 클래스
실행클래스 - 객체를 어떻게 생성하고 실행할 것인지 코딩하는 클래스. 자동차의 운전석과 같음. main() 메소드가 있는곳
클라이언트 클래스(컨텍스트 객체) - 어느 클래스 그룹의 기능을 사용(의존)하는 클래스
소프트웨어(프로그램) - 소프트웨어와 프로그램에 큰 의미 차이를 두지 않고 객체, 클래스처럼 편하게 호칭함.
1. 상속(세로) inheritance
기존의 분류 계층에서 새로운 하위 계층이 생성될 경우, 이 하위 계층의 특징은 기존 상위 계층의 특징을 기본적으로 소유하면서, 다시 자신만의 특징을 추가할 수 있다. 다만 접근지정자에 따라 상속범위를 제한 할 수 있다.
ex) java 접근지정자
public > protected > default > private 순으로 상속 범위 정해짐.
public : 패키지, 상속 유무에 관계 없이 모든 곳에서 접근 가능
protected : 상속받은 모든 곳에서 접근 가능
default : 같은 패키지에서만 접근 가능
private : 자기 자신만 접근 가능
상속의 효과
코드관점 - 코드의 재사용 가능, 코드의 수정(확장)의 편리함.
개발 편의성 관점 - 인간에게 익숙한 분류와 범주의 개념 적용.
낮은 관계의 의존성과 높은 기능의 집중도를 위한 관점 - 오버라이딩 가능, 폴리모피즘 가능
2. 오버로딩(Overloading)
메소드 이름은 같지만, 메소드 인자값을 다르게 해서 같은 메소드 이름을 가져도 별개의 메소드로 보고 접근할 수 있게 하는 개념
3. 오버라이딩(Overriding)
상속을 받은 부모 클래스에서 정의한 메소드를, 자식 클래스의 특징에 맞게 로직을 덮어쓰기 해서 재구현하는 개념
4. 폴리모피즘(Polymorphism)
같은 그룹에 속하는 클래스들의 동일한 메소드를 호출 할 때 자식 클래스들이 저마다 다른 로직을 수행하고 리턴하는 것.
폴리모피즘으로 구성할 때의 장점
1)관계의 의존성이 낮아진다.
2)클라이언트 클래스의 코드 변경 없이, 기능의 수정과 확장이 무제한으로 가능하다.
폴리모피즘을 위한 원칙 3가지
1) 기능을 제공할 비슷한 종류의 클래스들을 모아 클래스 그룹으로 조직화한다.
2) 기능을 제공할 클래스 그룹의 인터페이스를 통일한다.
3) 클라이언트 클래스에서는 쓰고 싶은 클래스 그룹의 부모 클래스만 의존하고 자식 클래스를 인자로 넘겨받아 저장한다.
5. 캡슐화(Encapsulation)
속성을 숨기고 보호하는 캡슐화도 있지만 변하는 기능을 분리하여 어떤 틀 안에 숨기고 보호할 수 있는 캡슐화 개념도 있다.
속성은 private으로 선언한다. getter 메소드를 만들어 getter 메소드로만 속성을 가져올 수 있게 한다. setter 메소드로 데이터를 제한적으로 조작하게 허용하거나, 아예 setter메소드를 제거하여 외부로부터 속성값의 조작을 금지한다.
비슷한 기능을 하나의 객체에 모두 구현하려고 하는 경우, 하나의 클래스에 로직이 섞여서 객체가 지저분해진다. 그래서 유지보수나 기능확장이 어렵다. 그러나 분리가 가능한 기능을 별도의 클래스 그룹으로 캡슐화하면, 하나의 클래스에 필요한 최소한의 기능만 깔끔하게 구현되고, 모듈화한 다른 기능은 우리가 원하는 기능만 선택하여 사용하게 할 수 있다.
6. 인터페이스(Interface)
인터페이스는 어떤 대상의 의미를 구현할 때 지켜야 할 규칙과 표준을 의미한다. 추상적인 개념의 의미를 완벽하게 구현하기 위해서는 제시하는 인터페이스를 지켜야 한다는 뜻이다. java 를 기준으로 클래스에 메소드 정의만 있꼬 속성이나 메소드 내부로직은 모두 제거되어 있는 것이 인터페이스이다. 인터페이스는 단독으로 어떤일도 할 수 없다. 어떤 클래스가 인터페이스를 상속받고, 구현해야 의미가 있다. 만약 인터페이스를 상속받았는데, 인터페이스에 명시된 메소드를 모두 구현하지 않으면 컴파일러에서 에러를 발생한다.
인터페이스의 효과
. 다중 상속 효과가 가능하다.
. 인터페이스와 로직이 명확하게 분리된다.
. 인터페이스와 로직이 명확히 분리되어서 부모 클래스 코드가 깔끔해진다.
. 인터페이스와 로직이 명확하게 분리되어 보다 중요한 메소드 명세를 더 강조할 수 있다.
. 인터페이스와 로직이 명확히 분리되어 외부에 노출할 필요 없는 로직을 캡슐화한다.
. 개발자 시야가 넓어져 나무보다 숲을, 로직보다 인터페이스 관점으로 볼 수 있다.
한 개의 파일에(하나의 클래스에) 모두 개발하는 것
. 코드가 지저분해진다.
. 원하는 기능을 제공하는 객체/메소드/속성을 찾기 힘들다.
. 객체의 의존 일관성이 없어지고 복잡해진다.
. 객체지향의 장점, 디자인패턴을 적용하기가 어려워진다.
. 기능을 변경하거나 확장할 때 기존 코드를 직접 고쳐야 할 가능성이 높아진다.
. 사이드 이펙트가 높아진다.
7. 위임(가로)Delegation
상속의 문제 : 자식 클래스 중 부모 클래스의 기능이 없어야 하는 경우가 발생할 경우, 그리고 그 경우가 수십 수백종류가 늘어날 경우. 즉, 기능 명세(인터페이스)는 정해진 것이 아니고 종종 변경될 일이 생길 것이라고 하는 경우 수많은 상속관계 클래스를 찾아 코드를 수정해야 한다. 상속은 상속관계를 끊지 않는 한 상속받는 것으로 고정되어 변경이 불가능하다. 이럴 경우 상속대신 위임을 한다. 변경되는 부분을 별도 클래스그룹으로 분리한 뒤 해당 인터페이스에만 의존한다. 그리고 이 클래스그룹의 객체를 속성으로 가진다.
상속을 지나치게 사용하면 오히려 프로그램에 'code smell' 이 나는 경우가 생긴다. 이때는 위임 구조로 리팩토링하여 객체지향에서 본래 의도하는 유연성, 확장성, 유지보수 편리성을 다시 회복할 수 있다.
"세로(상속)보다 가로(위임)로 접근하라"
[객체지향 구현 원리 5가지 - SOLID]
1. SRP(Single Responsibility Principle)
클래스는 분업화가 철저한 공장의 노동자와 같다. 공장이 잘 운영되려면 노동자는 오직 하나의 일만 해야 한다. 분업화된 공장은 노동자가 자기 일을 소홀이 하고 다른 일을 하는 즉시 전체 생산 시스템에 문제가 생긴다. 클래스는 오직 하나의 일만 수행해야 객체지향 공장의 생산성이 높아진다. SRP 단일 책임의 원리라고 한다. 객체가 제공하는 모든 기능은 단 하나의 책임을 수행하는 데 집중되어 있어야 한다. 각 클래스의 수정 이유가 오직 하나라면, 설계자는 단일 책임의 원리를 정확히 구현한 것이다. 이 명제를 확인하기 위한 CRC카드라는 방법이 있다.
______ 클래스에 대한 SRP 분석
_____ 가(이) 자신을 _____한다.
_____ 가(이) 자신을 _____한다.
_____ 가(이) 자신을 _____한다.
제목에 내가 만들고 싶은 클래스 이름을 적고, 그 아래 내가 계획한 메소드를 적어본다. 그 다음에 클래스가 SRP 규칙을 지키고 있는지 확인해본다.
SRP 규칙을 지킨다는 것은 객체지향 개발에 있어 '깨진 창문 효과'를 예방하는 것과 같다.
2. OCP(Open Closed Principle)
요구사항은 기존에 개발된 기능을 수정하는 건이 있다. 또는 새로운 기능을 확장하는 요구사항이 있다. 이럴 경우 개발자의 가장 직관적인 수정 방법은 기존 클래스 안 메소드의 내부 로직을 변경하는 것이다. 그러나 이것은 '사이드 이펙트'가 발생하기 쉽다. 예를 들어 A 클래스 그룹의 메소드가 예고도 없이 수정되었다면, 이 클래스 그룹을 사용하는 B클래스의 오작동이 발생할 수 있다. 현장에서 곤혹스러운 일이, 기존에 작동중인 코드를 수정하면 사이드 이펙트가 걱정되어, 기존 코드의 정상 작동유무, 사이드 이펙트 여부를 번거롭게 테스트해야 하는 상황이 발생한다.
그래서 우리는 기존 클래스를 수정하지 않는 것이 좋다. 대신 새로운 클래스나 기능을 만들어 확장해야 한다.
클래스는 기능확장에 대해서는 열려있지만, 코드 수정에 대해서는 닫혀있어야 한다.
3. LSP(Liskov Subsitution Principle)
자식 클래스는 부모 클래스가 사용되는 곳(이 클래스 그룹을 사용하는 다른 클래스)에 대체될 수 있어야 한다. 상속과 폴리모피즘을 확실하게 적용하고 싶을 때 곱씹어야 할 중요한 원칙이다. 만약 자식클래스가 1,2,3 3개가 존재하는데 자식클래스3에만 메소드가 추가되었다고 생각해보자. 클라이언트 클래스는 자식클래스3의 인터페이스를 알 수 없다. 즉 자식클래스3은 부모 클래스가 사용되는 곳에 대체될 수 없으므로 LSP를 위반한 것이다. 그렇다고 클라이언트 클래스가 자식 클래스3을 직접 의존하는 것은 객체지향의 근본원칙(LSP 위반보다 더 심각한)도 위반하는 것이다. 이럴 경우 위임으로 문제를 해결한다. (상속,단순위임,위임)
상속, 단순위임, 위임은 어떨때 사용하는가?
단순 위임 : 은 클라이언트 클래스가 다른 클래스의 기능을 사용하지만, 사용하는 기능을 변경할 필요가 없을 때 사용한다. 단순 위임은 단순 위임을 해주고있는(기능을 제공해 주는) 클래스 기능이 변하지 않을 때 사용한다.
위임 : 만약 단순 위임을 해주고 있는 클래스의 기능을 다른 기능으로 교체할 수도 있는 요구사항이 있을 때는 자식 클래스를 만들어 교체할 수 있는 '위임'을 쓴다.
상속 : 한편으로, 자식 타입들은 부모 타입들이 사용되는 곳에 100% 대체할 수 있다는 LSP 규칙을 지킬 수 있다면 '상속'을 사용해도 좋다.
4. ISP(Interface Segregation Principle)
클래스는 자신이 사용하지 않는 메소드에 의존하면 안된다.
ISP 법칙은 SRP법칙과 비슷하다. SRP는 클래스 관점에서 클래스가 하나의 일만 해야 한다고 가이드라인을 제시한다. ISP는 인터페이스 관점에서 '클래스는 자신이 사용하지 않는 메소드에 의존하면 안된다'라는 인터페이스 사용 가이드 라인을 제시한다.
5. DRY(Don't Repeat Yourself)/DIP(Dependency Inverion Principle)
DRY
중복되는 코드는 하나로 통합한다. 객체지향뿐만 아니라 모든 개발 방법론에서 권장하는 규칙이다. 객체지향에 맞게 풀어 쓰면 '공통부분을 추출하여 추상화하고 한곳에 두어 중복코드를 피하라' 이다. 이것이 DRY라는 코드 반복 금지의 원리이다. 중복된 코드가 존재하면,
1) 이 코드를 사용하는 클라이언트 클래스들은 어느 코드를 사용해야 할지 혼란스럽다.
2) 만약 개발자가 이 중복된 코드를 동시에 관리하지 못하고 하나의 코드만 관리한다면, 다른 중복된 코드는 추가 요구사항을 반영하지 못하고 이전 코드로 남게 되는 경우가 발생한다.
3) 그렇다면 두 코드를 사용하는 클라이언트 클래스 중 일부는 잘못된 코드를 사용하기 때문에, 조만간 버그가 발생한다.
DIP
Dependency Inversion Principle 직역하면 의존관계 역전 원칙이다. 구체적인 클래스 대신 추상적인 클래스에 의존하라는 뜻이다. 폴리모피즘의 원칙, 클라이언트 클래스에서는 부모 클래스만 의존하고 자식 클래스를 인자로 넘겨받아 저장한다와 동일한 원리이다.
Chapter 4. 디자인패턴의 깊이
[화려한 동작을 많이 아는 것보다 중요한 동작 몇 가지를 깊게 익힌다]
디자인 패턴은 Gang of Four 라고 불리는 객체지향의 고수들이 창시했다. '객체와 객체간의 관계에서 반복적으로 발견되는 좋은 원칙을 끌어내 패턴으로 만들어낸 것' 이다. 디자인 패턴 카탈로그에는 23개 패턴이 존재한다. 그런데 주변을 보면 디자인패턴 중에 실제로 직접 써보거나 간접적으로 써본 경우는 한 자릿수를 벗어나기 힘들다. 그래서 자주 쓰는 디자인패턴을 골라 깊이를 파자고 말한다.
"디자인패턴은 점(코드,함수,로직)의 재활용이 아니라 선(관계)의 재활용이다."
[Strategy pattern]
디자인패턴의 기본기 : Strategy pattern (보통 처음으로 배우는 디자인패턴)
클라이언트 클래스(컨택스트 객체) 가 클래스 그룹의 자식클래스에 의존하지 않는 설계. Strategy Pattern 은 위임을 활용. 위임은 메소드의 실제 구현을 다른 클래스에 위임하는 것. 클라이언트 클래스는 오직 strategy 클래스만 의존하고 strategy 클래스를 상속받는 자식클래스를 자유롭게 변경,확장,수정할 수 있다. 이때 클라이언트 클래스의 코드수정은 없다.
적용예시 ) 파서(client) -> 기본 자료 구조(parent class) - XML파서(child class), JSON파서(child2), TEXT 파서(child3)
[State pattern]
바뀌는 상태를 캡슐화 하는것. 상태 변경 관련 로직을 유연하게 개발하는 설계. strategy pattern 과 클래스 구성도가 거의 같다. 하지만 strategy pattern 은 교체 가능한 알고리즘을 캡슐화 하고 클라이언트가 사용하고 싶은 알고리즘을 취사선택해 인자로 넘긴다. 반면 state pattern 은 대상 객체의 상태를 변경할 때 쓰인다. 주로 수많은 if문을 집어넣을 상태 처리 로직 대신에 사용하는 패턴이다. 보통 클라이언트 클래스의 메소드 명세를 똑같이 구현하는 state 클래스 그룹을 만들어 통째로 위임한다. 마치 해당 클래스가 통째로 state 클래스로 바뀌는 효과가 일어난다.
적용예시 ) 게임에서 캐릭터들의 상태에 따른 행동변화, TCP등의 네트워크 통신 상태 변경 등
[Template method pattern]
제어 흐름을 '일관성 있게 통제' 하는 설계. 프레임워크에 전반적으로 사용된다. 거의 바뀌지 않는 것을 바뀌는 것으로부터 분리해서 보호한다. 보호해야 할 것은 '제어 흐름'이다. Template method 패턴은 프레임워크에서 개발자가 정형화된/일관된 코딩을 하도록 유도해야 할 경우 많이 쓰인다. 이 패턴은 '표준 메뉴얼'처럼 프레임워크 설계자(개발 책임자/아키텍트)가 원하는 방식으로 개발을 유도한다. 여러 개발자들과 협업할 때, 일관성 있는 개발을 끌어내기 좋은 패턴이다.
[Decorator pattern]
데코레이터 클래스 그룹이 부모클래스를 상속받아 부모클래스의 인터페이스와 동일하다. 부모객체를 전역변수로 가지고 있어서 데코레이터에 데코레이터를 추가하면 추가한 데코레이터의 기능이 같이 실행된다. 관련 데코레이터를 무제한 추가할 수 있다. 데코레이터 생성자에 부모클래스를 인자로 받아 저장한다. 인자로 받은 객체의 기능을 호출하면서 추가기능을 덧붙인다.
데코레이터 역할을 하는 클래스는 모두 같은 클래스 그룹에 속한다. 그래서 어떠한 객체라도 심지어 데코레이터들끼리도 인자로 받아들일 수 있고 재귀적으로 실행할 수 있다. 이때 쓰인원칙이 OCP이다
Chapter 5. 한 점 보기
[객체지향의 한 점]
Gang of Four 와 같은 객체지향 고수가 있고 다른 고수들도 있다. 객체지향은 주어진 문제를 좋은 알고리즘으로 해결하는 능력에서 더 나아가 낮은 관계의 의존성과 높은 기능의 집중도를 가진 유연한 소프트웨어를 만드는 능력이다. 즉 기능보다 관계 중심 접근법이다.
기능은 변한다. 그러나 객체간의 관계는 거의 변하지 않는다.
변하는 부분을 변하지 않는 부분으로부터 분리한다.
[가족문파, Objective-C와 스프링 프레임워크]
아이폰 앱 개발은 objective-c가 기반이다. 최근 등장한 swift는 객체지향과 함수형 언어를 같이 쓴다. objective-c에서 일관되게 잘 쓰는 패턴이 있다. Delegator패턴이다. 이 패턴이 중요한 사상이 되어 아이폰개발에 적극적으로 사용된다. 템플릿 메소드 패턴과 유사하다. 템플릿메소드는 상속으로 접근했고 objective-c delegator 패턴은 위임으로 접근했다.
[스프링 프레임워크]
java기반 프로젝트에사 스프링 프레임워크를 쓰지 않는 곳은 드물다. 바탕에는 IoC기법이 있다. 이 기법은 Strategy 패턴을 어떻게 구성할까 깊게 고민하지 않고, 단지 XML등의 설정으로 편하게 구현할 수 있다. 객체지향 고급기법을 알고 싶으면 하기 키워드로 찾는다.
책임주도 설계, 도메인 모델, 디자인 패턴(패턴을 다 다룬 책), 리팩토링, 테스트 주도 개발(TDD), 스프링 프레임워크
[전통의 문파,RDB]
관계형 데이터베이스 모델링/튜닝은 잘 설계하고 SQL을 효율적으로 튜낭하는 방법론 혹은 기법이다. 관계형 모델보다 객체지향이 나중에 등장했다.
데이터를 다루는 것은 RDB가, 소프트웨어의 움직임을 다루는 것은 객체지향과 함수형 프로그래밍이 양축으로 되어 명성을 유지할 것이다.
관찰, 추상화, 패턴인식 사고도 RDB할 때 도움이 된다.
[신생문파, 함수형 언어]
객체지향은 움직이는 부분을 캡슐화하여 코드 이해를 돕는다. 함수형 언어는 움직이는 부분을 최소화하여 코드 이해를 돕는다.
함수형언어는 사이드 이펙트를 최대한 멀리한다. 그 방법은 순수함수를 만드는 것이다. 순수함수란 동일한 인자가 들어오면 항상 동일한 값을 리턴하는 함수이다. 함수형 언어가 객체지향을 대체할까? 대부분 공존할 것이라 한다. 지금까지 흐름으로 JAVA,Swift,Kotlin 도 양쪽개념이 공존한다.
객체지향은 움직이는 부분을 캡슐화하여 코드 이해를 돕는다.
함수형 언어는 움직이는 부분을 최소화하여 코드 이해를 돕는다.
[독후한줄평]
실무에서 치열하게 살아옴에도 역량강화를 끊임없이 실천하고 바쁜와중에 무술도 연마하시면서 이해가 쉽도록 무공으로 빗대어 예를 들어주신 저자 김동헌님께 감사드린다. 저자가 다른 책을 낸다면 무조건 사 볼 의향이 있다. 여타 많은 지식서적처럼 거드름 피우지 않고 자신의 시행착오를 솔직히 밝히며 개발철학을 토해내듯 서술한 책을 만나는 일은 인생에서 자주있는 기회가 아니다. 이 책의 유일한 단점은 촌스러운 표지다.