앞의 3개 장을 통해 객체 지향의 개념과 4대 특성 학습
이제 객체 지향 프로그램을 작성할 수 있는 도구 획득! 용도에 맞게 사용하자
좋은 도구가 있어도 올바르게 사용하지 않으면 요리를 만드는 작업은 고됨
즉, 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이나 원칙을 배워보자!
객체 지향 언어의 시초: Simula67이 1960년 발표되고 50년 이상 세월이 흐름
수많은 시행 착오와 best practice 속에서 객체 지향 설계(OOD; Object Oriented Design)의 정수라고 할 수 있는 5원칙 집대성
SOLID
- SRP(Single Responsiblity Principle): 단일 책임 원칙
- OCP(Open Closed Principle): 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 역전 원칙
응집도는 높이고(High Conhesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립
[결합도와 응집도]
좋은 소프트웨어 설계를 위해서는 결합도는 낮추고, 응집도는 높이는 것이 바람직
결합도: 모듈(클래스) 간의 상호 의존 정도
결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이
결합도 수준) 데이터 결합도, 스탬프 결합도, 컨트롤 결합도, 외부 결합도, 공유 결합도, 내용 결합도
응집도: 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성
응집도가 높은 모듈은 하나의 책임에 집중하고, 독립성이 높아져 재사용이나 기능의 수정, 유지 보수가 용이
응집도 수준) 기능 응집도, 순차 응집도, 통신 응집도, 절차 응집도, 시간 응집도, 논리 응집도, 우연 응집도
SOLID는 객체 지향 프로그램을 구성하는
속성, 메서드, 클래스, 객체 패키지, 모듈, 라이브러리, 프레임워크, 아키텍처 등 다양한 곳에 다양하게 적용되는 것
SOLID가 적용됐는지 아닌지 애매모호 or 보는 사람의 관점에 따라 다르게 해석될 수 있는 소지가 있음
-> SOLID는 제품이 아니라 개념이라서
우리가 만드는 제품, 즉 소프트웨어에 녹여 내야 하는 개념!
잘 녹여내면 그렇지 않은 소프트웨어에 비해, 상대적으로 이해하기 쉽고, 리팩토링과 유지보수가 수월, 논리적으로 정연!
SOLID는 객체 지향 4대 특성을 발판으로 하고 있음
6장에서 소개할 디자인 패턴의 뼈대이며, 스프링 프레임워크의 근간
각 예제가 SOLID 원칙의 전부가 아닌 한 사례일 뿐임을 명심
SRP - 단일 책임 원칙
Single Reponsibility Principle
"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다"
남자라고 하는 클래스와 남자 클래스에 의존하는 다양한 클래스
남자는 피곤함. 역할과 책임이 너무 많음!!!!!!!!!!!!! -> stinky smell~
ex) 여자친구랑 헤어지면, 기념일 챙기기, 키스하기 할 필요 있음? 그리고 여자친구랑 헤어진 스트레스를 온 관계에 풀 것임(영향)
SRP = 역할(책임)을 분리하자!
남자라는 하나의 클래스가 역할과 책임에 따라 네 개의 클래스로 쪼개짐
+ 역할과 클래스명이 딱 떨어지니 이해하기 좋음
-> 이제 여자친구랑 이별하더라도 남자친구만 상처 받으면 됨, 다른 관계에는 어떠한 영향도 끼치지 않음!ㅋㅋㅋㅋㅋ
단일 책임 원칙은 잘된 경우보다 잘못된 경우를 살펴보는게 이해하는데 좋다!
1. SRP 위반 클래스
남자는 반드시 군대를 가고, 여자는 절대로 군대를 가지 않는다.
그런데 사람 클래스에 군번 속성이 있다면?
사람형 참조 변수 줄리엣이 가진 군번 속성에 값을 할당하거나 읽어 오는 코드를 제제할 방법이 없음!
=>
남자 클래스와 여자 클래스에 공통점이 없다면, 사람 클래스는 제거! / 공통점이 많다면 사람 클래스를 상위 클래스로 해서 공통점만 사람 클래스에 두고 남,여 클래스는 사람 클래스를 상속하고 차이점만 각자 구현!
즉, 남자 클래스, 여자 클래스로 분할하고 남자 클래스에만 군번 속성을 갖게 하자!
2. SRP 위반 테이블
하나의 속성이 여러 의미를 갖는 경우도 SRP을 지키지 못하는 경우
데이터베이스 테이블에 존재하는 하나의 필드가 토지인 경우 면적을, 건물인 경우 층수를 나타내는 경우
-> 자바 코드에서는 if문을 여기저기 사용해야만 함
테이블을 설계할 때도 단일 책임 원칙을 고려하자!! => 정규화 과정을 조금 더 확장해서 생각해보면 테이블과 필드에 대한 단일 책임 원칙의 적용!
3. SRP 위반 메서드
강아지 클래스 만들고 소변보다() 메서드 구현
package srp.bad;
public class 강아지 {
final static Boolean 숫컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
void 소변보다() {
if (this.성별 == 숫컷) {
// 한쪽 다리를 들고 소변을 본다.
} else {
// 뒤다리 두 개로 앉은 자세로 소변을 본다.
}
}
}
강아지 클래스의 소변보다() 메서드가 수컷 강아지의 행위와 암컷 강아지의 행위를 모두 구현하려고 하기에 SRP 원칙 위배
SRP를 지키지 않을 경우 나타나는 대표적인 냄새: 분기처리를 위한 if문
리팩토링하면 아래와 같다
package srp.good;
public abstract class 강아지 {
abstract void 소변보다();
}
public class 숫컷강아지 extends 강아지 {
void 소변보다() {
// 한쪽 다리를 들고 소변을 본다.
}
}
public class 암컷강아지 extends 강아지 {
void 소변보다() {
// 뒤다리 두 개로 앉은 자세로 소변을 본다.
}
}
SRP를 만족하지 못한 대표적인 현실 사례: 슈퍼맨, 만능 엔터테이너, 팔망미인, 멀티 플레이어
SRP와 가장 연관이 깊은 객체 지향 4대 원칙: 모델링 과정을 담당하는 추상화
애플리케이션의 경계를 정하고, 추상화를 통해 클래스를 선별하고 속성과 메서드를 설계할 때 반드시 SRP를 고려하는 습관들 들이자!!
OCP - 개방 폐쇄 원칙
Open Closed Principle
"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다."
= "자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다."
창문/기어가 수동이던 마티즈 -> 창문/기어가 자동인 소나타
차종을 바꾸니 운전자의 행동에도 변화가 온다?? -> XXX
상위 클래스 or 인터페이스를 두자!
ex) 다양한 자동차가 생긴다
자동차 입장: 자신의 확장에는 개방
운전자 입장: 주변의 변화에는 폐쇄
ex) JDBC도 데이터베이스 프로그래밍에서 개방-폐쇄 원칙의 좋은 예시!
JDBC 사용 클라이언트 : 데이터베이스 변경에 개방(Connection 설정하는 부분 말고는 따로 수정 필요 X)
자바 어플리케이션: 데이터 베이스라는 주변의 변화에 닫혀 있음
데이터베이스: 자신의 확장에는 열려 있음
자바 어플리케이션 ---- (JDBC 인터페이스: 완충 장치) --- DB들
ex) 자바 언어
자바 개발자: 운영체제의 확장에 개방(다양한 구동 환경에 걱정하지 않고 본인이 작업하고 있는 개발 PC에 설치된 JVM에서 구동되는 코드만 작성)
개발자가 작성한 소스코드 : 운영체제의 변화에 닫혀 있음
각 운영체제별 JVM: 확장에 열려 있는 구조
개발자의 소스 코드 --- (목적 파일: 완충장치) --- 운영체제별 JVM
ex) 편의점
손님: 편의점 직원이 바뀌어도 물건 구매에 영향 받지 않음
직원: 교대라는 확장 행위에 열려 있음 (+구매 담당자의 행위를 추가하거나, 보안 담당자의 행위를 추가하는 확장에 대해 열려 있음)
OCP 원칙을 무시하고 프로그램 작성시,
OOP의 가장 큰 장점인 유연성, 재사용성, 유지 보수성 등을 얻을 수 없다.
반드시 지켜져야할 원칙!!
스프링 프레임워크 - OCP의 김연아! OCP를 교과서적으로 활용하고 있음.
LSP - 리스코프 치환 원칙
Liskov Substitution Principle
"서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다."
= "하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다."
객체 지향에서의 상속: 조직도나 계층도가 아닌 분류도!!
상속은 아래 두가지 조건 만족해야함
- 하위 클래스 is kind of 상위 클래스 : 하위 분류는 상위 분류의 한 종류
- 구현 클래스 is able to 인터페이스 : 구현 분류는 인터페이스할 수 있어야 함
위 두 문장대로 구현된 프로그램: LSP 잘 키지고 있음
아니라면, 상속이 조직도나 계층도 형태로 구축된 경우
아버지 춘향이 = new 딸()
아버지를 상위 클래스(기반 타입)로 하는, 딸 이라는 하위 클래스(서브 타입)
상위 클래스 객체 참조 변수에는 하위 클래스의 인스턴스 할당할 수 있는데 --> 이상함!
딸이 아빠의 역할을 하고 있다?! 춘향이는 아버지형 객체 참조 변수이기에 아버지 객체가 가진 행위(메서드)를 할 수 있어야함
LSP 법칙 위배!
동물 뽀로로 = new 펭귄()
분류도 형태! LSP 법칙 만족!
분류도는 하위의 존재하는 것들이 상위에 있는 것들의 역할을 하는데 전혀 문제가 없다.
LSP 법칙 -> 객체 지향의 '상속' 특성을 올바르게 활용하면 자연스럽게 얻게 됨
- 하위형에서 선행 조건은 강화될 수 없다.
- 하위형에서 후행 조건은 약화될 수 없다.
- 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.
ISP - 인터페이스 분리 원칙
Interface Segregation Principle
"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다"
SRP에서 제시한 해결책: 남자 클래스를 토막내서 하나의 역할(책임)만 하는 다수의 클래스로 분할
남자를 토막내는 것이 너무 잔인하다면? ISP, 인터페이스 분리 원칙을 쓰자!
남자 클래스를 토막내는 것이 X
자아 붕괴(?) 또는 다중 인격화(?)
ex)
여자친구를 만날 때는 남자친구 역할만 할 수 있도록 인터페이스로 제한
어머니와 있을 때는 아들 인터페이스로 제한 ...
결론적으로,
SRP와 ISP는 같은 문제에 대한 두 가지 다른 해결책!
특별한 경우가 아니라면 SRP가 더 좋은 해결책
+) 인터페이스 최소주의 원칙
"인터페이스를 통해 메서드를 외부에 제공하라 때는 최소한의 메서드만 제공하라"
ex) 남자친구 인터페이스에 사격하기() 메서드를 제공할 필요도 없고, 제공해서도 안된다
상위 클래스는 풍성할 수록 좋고, 인터페이스는 작을 수록 좋다!
LSP에 따라 하위 객체는 상위 객체인 척 할 수 있음
package poorSuperClass;
import java.util.Date;
public class Driver {
public static void main(String[] args) {
사람 김학생 = new 학생("김학생", new Date(2000, 01, 01), "20000101-1234567",
"20190001");
사람 이군인 = new 군인("이군인", new Date(1998, 12, 31), "19981231-1234567",
"19-12345678");
System.out.println(김학생.이름);
System.out.println(이군인.이름);
// System.out.println(김학생.생일); // 사용불가
// System.out.println(이군인.생일); // 사용불가
System.out.println(((학생) 김학생).생일); // 캐스팅 필요
System.out.println(((군인) 이군인).생일); // 캐스팅 필요
// System.out.println(김학생.주민등록번호); // 사용불가
// System.out.println(이군인.주민등록번호); // 사용불가
System.out.println(((학생) 김학생).주민등록번호);
// 캐스팅 필요
System.out.println(((군인) 이군인).주민등록번호);
// 캐스팅 필요
김학생.먹다();
이군인.먹다();
// 김학생.자다(); // 사용불가
// 이군인.자다(); // 사용불가
((학생) 김학생).자다(); // 캐스팅 필요
((군인) 이군인).자다(); // 캐스팅 필요
// 김학생.소개하다(); // 사용불가
// 이군인.소개하다(); // 사용불가
((학생) 김학생).소개하다(); // 캐스팅 필요
((군인) 이군인).소개하다(); // 캐스팅 필요
((학생) 김학생).공부하다(); // 캐스팅 필요
((군인) 이군인).훈련하다(); // 캐스팅 필요
}
}
빈약한 상위 클래스를 이용한 경우 여기저기 형변환이 발생 -> 상속의 혜택을 제대로 누리지 못함
객체 참조 변수를 사람형이 아닌 학생형/군인형으로 선언하고 사용하면 되겠지만 그럼 굳이 상속 구조 만들 필요 없음!!
상위 클래스형의 참조 변수를 이용해야 상속의 가장 큰 혜택을 볼 수 있음!
package richSuperClass;
import java.util.Date;
public class Driver {
public static void main(String[] args) {
사람 김학생 = new 학생("김학생", new Date(2000, 01, 01), "20000101-1234567",
"20190001");
사람 이군인 = new 군인("이군인", new Date(1998, 12, 31), "19981231-1234567",
"19-12345678");
System.out.println(김학생.이름);
System.out.println(이군인.이름);
System.out.println(김학생.생일);
System.out.println(이군인.생일);
System.out.println(김학생.주민등록번호);
System.out.println(이군인.주민등록번호);
// System.out.println(김학생.학번); // 사용불가
// System.out.println(이군인.군번); // 사용불가
System.out.println(((학생) 김학생).학번);
// 캐스팅 필요
System.out.println(((군인) 이군인).군번);
// 캐스팅 필요
김학생.먹다();
이군인.먹다();
김학생.자다();
이군인.자다();
김학생.소개하다();
이군인.소개하다();
// 김학생.공부하다(); // 사용불가
// 이군인.훈련하다(); // 사용불가
((학생) 김학생).공부하다(); // 캐스팅 필요
((군인) 이군인).훈련하다(); // 캐스팅 필요
}
}
사용 불가능한 경우나, 불필요한 형 변환이 없음!
다만, 소개하다() 메서드의 경우, 학생/군인 클래스가 다른 기능을 수행해야함! 둘다 필요하긴 하다!
-> 추상 메서드 사용 (추상 메서드가 있으면 그 클래스도 추상 클래스가 된다)
package richSuperClass;
import java.util.Date;
public abstract class 사람 {
String 이름;
Date 생일;
String 주민등록번호;
void 먹다() {
System.out.println(이름 + " 식사 중");
}
void 자다() {
System.out.println(이름 + " 취침 중");
}
abstract void 소개하다();
}
package richSuperClass;
import java.util.Date;
public class 학생 extends 사람 {
String 학번;
public 학생(String 이름, Date 생일, String 주민등록번호, String 학번) {
this.이름 = 이름;
this.생일 = 생일;
this.주민등록번호 = 주민등록번호;
this.학번 = 학번;
}
void 공부하다() {
System.out.println(이름 + " 공부 중");
}
@Override
void 소개하다() {
String msg = "";
msg += "이름: " + 이름;
msg += "\n생일: " + 생일;
msg += "\n주민등록번호: " + 주민등록번호;
msg += "\n학번: " + 학번;
System.out.println(msg);
}
}
인터페이스는 그 역할에 충실한 최소한의 기능만 공개!!
인터페이스는 "~할 수 있는(is able to)" 이라는 기준으로 만드는 것이 정석
+)
DIP - 의존 역전 원칙
Dependency Inversion Principle
"고차원 모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라"
"자신보다 변하기 쉬운 것에 의존하지 마라"
자동차 자신보다 더 자주 변하는 스노우 타이어에 의존하기에 부서지기 쉬움!!!!
자동차가 구체적인 타이어들이 아닌 추상화된 타이어 인터페이스에만 의존하게 함
-> 타이어가 변경돼도 자동차는 그 영향을 받지 않는 형태
OCP와 데자뷰!
하나의 해결책에 여러 설계 원칙이 녹아 있는 경우가 많음.
기존에는 스노우 타이어가 그 무엇에도 의존하지 않는 클래스
-> 추상적인 타이어 인터페이스에 의존하게 됐다(의존의 방향 역전)
+ 자동차는 자신보다 변하기 쉬운 스노우 타이어에 의존하던 관계를 중간에 추상화된 타이어 인터페이스를 추가해 두고 의존 관계 역전시키고 있다.
즉,
자신보다 변하기 쉬운 것에 의존하던 것을, 추상화된 인터페이스나 상위 클래스를 두어, 변하기 쉬운 것의 변화에 영향받지 않게 하는 것
= 의존 역전 원칙
인간보다는 신을, 아이보다는 어른을 부하 직원보다는 상사에게 의지하는 이유 => 변할 가능성이 적기 때문...(와...통찰이다)
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에,
하위 클래스나, 구체 클래스가 아닌 위의 것에 의존하라!!
JBDC에서도 확인 가능
정리 - 객체 지향 세계와 SOLID
SOLID는 객체 지향을 올바르게 프로그램에 녹여내기 위한 원칙
객체 지향 4대 특성을 제대로 활용한 결과
SoC(Separation Of Concerns): 관심사의 분리
관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고,
관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리
하나의 속성, 하나의 메서드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어있어야 한다! = SoC
관심사가 다르고 변화의 시기가 다르면 분리해야함
SoC를 적용하면 자연스럽게 SRP, ISP, OCP에 도달하게 됨!
스프링 또한 SoC를 통해 SOLID를 극한까지 적용하고 있음
- SRP(단일 책임 원칙): 어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다.
- OCP(개방 폐쇄 원칙): 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
- LSP(리스코프 치환 원칙): 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
- ISP(인터페이스 분리 원칙): 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.
- DIP(의존 역전 원칙): 자신보다 변하기 쉬운 것에 의존하지 마라
SOLID원칙 적용하면 소스 파일의 개수는 더 많아지는 경향
-> 많아진 파일이 논리를 더욱 잘 분할, 잘 표현하기에 이해하기 쉽고, 개발하기 쉬우며, 유지와 관리, 보수하기 쉬운 소스가 됨
얻는 혜택 >>>> 늘어나는 파일 개수에 대한 부담
객체 지향 세계 = 현실 세계 같아야 한다 + 모델링을 통해 추상화됨(초점)
추상화된 객체 지향 세계에는 그에 맞는 법도가 있음
'SW Books > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
[자바 객체지향 원리] 6장 - 스프링이 사랑한 디자인 패턴 (1) | 2023.08.31 |
---|---|
[자바 객체지향 원리] 4장 - 자바가 확장한 객체 지향 (0) | 2023.08.28 |
[자바 객체지향 원리] 3장 - 자바와 객체 지향 (0) | 2023.08.10 |
[자바 객체지향 원리] 2장 - 자바와 절차적/구조적 프로그래밍 (1) | 2023.08.07 |
[자바 객체지향 원리] 1장 - 사람을 사랑한 기술 (1) | 2023.08.07 |