Java/스프링 입문을 위한 자바 객체 지향의 원리와 이해
Ch 05. 객체 지향 설계 5원칙 SOLID
계란💕
2023. 12. 1. 17:43
객체 지향 설계의 원칙, SOLID 에 대해 각 예시와 함께 살펴보려고 한다.
평소에 자주 사용하는 Java, JDBC 도 SOLID와 관련되어 있다.
SRP (Single Responsibility Principle, 단일 책임원칙)
- 단일책임원칙이란? 하나의 클래스는 하나의 역할만 수행해야한다는 의미.
- "어떤 클래스를 변경해야하는 이유는 오직 하나뿐이어야한다."
Ex 1) SRP를 만족하는 설계
- "남자" 클래스는 다음 네 가지 역할을 하고 다음 8개의 메서드로 구성된다.
- 남자친구 역할 - 기념일 챙기기(), 데이트하기()
- 아들 역할 - 효도하기(), 안마하기
- 사원 역할 - 출근하기(), 아부하기()"
- 소대원 역할 - 사격하기(), 구보하기()
- 이를 SRP에 맞게 바꾸려면?
- 남자클래스 안에서의 역할과 책임이 많다.
- 따라서, 역할과 책임에 맞게 남자 클래스를 => [ 남자친구, 아들, 사원, 소대원] 4개의 클래스로 분리하는 것이다.
Ex 2) 속성이 SRP를 만족하지 않는 경우
<hide/>
class 사람{
String 군번;
}
사람 로미오 = new 사람();
사람 줄리엣 = new 사람();
줄리엣.군번 = "232434"; // (*)
- 줄리엣의 군번에 값을 넣고 있다.
- 이런 잘못된 부분을 제한할 수 없다.
- 따라서, 남자, 여자 클래스를 분리하고 남자 클래스에만 군번 속성을 만들면 된다.
- 이렇게 하나의 속성이 여러 의미를 갖는 것도 SRP를 지키지 못하는 것이다.
Ex 3) 메서드가 SRP를 지키지 않는 이유
<hide/>
class 강아지{
final static Boolean 수컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
}
void 소변보다(){
if(this.성별 = 수컷){
// 한쪽다리 들고 소변 본다.
}else{
// 뒷다리두개를 굽혀 앉은 자세로 본다.
}
}
- 메서드 안에서 성별에 따라서 분기 처리되는 것을 알 수 있다.
- 그런데 여기에서 메서드가 암, 수 행위를 모두 구현하려하기 때문에 SRP를 위반하는 것이다.
- 따라서 아래와 같이 추상클래스를 적용하는 게 좋다.
<hide/>
abstract class 강아지{
abstract void 소변보다();
}
class 수컷 extends 강아지{
void 소변보다(){
// 한쪽다리 들고 소변 본다.
}
}
class 암컷 extends 강아지{
void 소변보다(){
// 뒷다리두개를 굽혀 앉은 자세로 본다.
}
}
OCP(Open Closed Principle, 개방 폐쇄 원칙)
- 소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에 열려있고 변경에 대해서는 닫혀있어야한다.
- 자신의 대화에는 열려있고 주변의 대화에는 닫혀있어야한다.
Ex 1) OCP를 지키지 못한 예시
- 어떤 운전자가 마티즈를 몬다고 하자.
- 운전자 → 마티즈클래스 (창문 수동조작, 기어 수동 조작)
- 그런데 만약, 차종이 바뀌어서 소나타를 몬다면?
- 운전자 → 소나타클래스 (창문 자동조작, 기어 동 조작)
- 이 경우에 OCP를 적용해서 리팩토링할 수 있다.
- 운전자 → 자동차 ← ( 마티즈클래스 소나타클래스 )
- 이렇게 바꾸면 자동차 인터페이스를 만들어서 마티즈, 소나타 클래스가 자동차 인터페이스를 구현하도록 하면 된다.
- 다양한 자동차가 생긴다는 것은
- 자동차 입장에서 확장에 열려있고
- 운전자 입장에서는 변화에 폐쇄되어있는 것이다.
Ex 2) OCP를 지키는 좋은 예시 - JDBC
Java 애플리케이션 ↕ JDBC 인터페이스 ↕ JDBC 드라이버(오라클) JDBC 드라이버(MySQL) JDBC 드라이버(MS-SQL) ↕ ↕ ↕ 오라클DB MySQL DB MS-SQL DB |
- JDBC를 사용하는 클라이언트는 DB의 종류가 바뀌더라도 Connection을 설정하는 부분 외에는 코드를 수정할 게 없다.
- DB 종류가 바뀌더라도 Java 애플리케이션에 영향이 없도록 JDBC는 완충 장치 역할을 하는 것이다.
- Java 애플리케이션은 DB 라는 주변의 환경에 닫혀 있다. DB 교체는 DB가 자신의 확장에 열려있는 것을 의미한다.
Ex 3) OCP를 지키는 좋은 예시 - Java
- Java 코드를 작성할 때, 코드를 실행할 환경의 OS에 따라서 다르게 작성할 필요가 없다.
- 어떻게 가능할까?
- 각 (OS별) JVM 과 목적파일이라는 이 있기 때문에 개발자는 본인이 작업 중인 개발 PC에서 돌아가고 있는 JVM에서 구동되는 소스코드 만 작성하면 된다.
- 즉, 소스코드 는 OS의 변화에 닫혀있고 (OS별) JVM 사이에는 목적파일 이라는 완충 장치가 있다.
Liskov Substitution Principle (LSP , 리스코프 치환법칙)
- 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해서 상위클래스의 인스턴스 역할하는데 문제가 없어야한다.
- 서브 타입은 언제든 자신의 기반 타입으로 교체할 수 있어야한다.
- 객체 지향의 상속은 다음 조건을 만족해야한다.
- 하위 클래스 is kind of 상위 클래스
- 구현 클래스 is able to 인터페이스
- 계층도(ex) 가족 관계), 조직도가 아닌 분류도로 나타낼 수 있어야한다.
Ex) 리스코프 치환법칙의 잘못된 예시
- 아버지 - 상위클래스, 딸 - 하위 클래스
- 위와 같은 상황은 앞에서 살펴본 객체 지향의 상속 조건을 만족하지 않는다.
'딸 is kind of 아버지'
ISP(Interface Seperation Principle, 인터페이스 분리 원칙)
- 클라이언트는 자신이 사용하는 메서드에 의존 관계를 맺으면 안된다는 의미.
- 앞서 살펴본 SRP(단일 책임 원칙)의 대체로 사용할 수 있는 방법이다.
- 같은 문제에 대한 두 가지 해결책이라고 볼 수 있다.
- 특별한 경우가 아니면 SRP를 사용하는 게 좋다.
- 남자친구 클래스 => [남자친구, 아들, 사원, 소대원] 각 4개의 인터페이스로 나누어서 각 인터페이스를 구현하는 것이다.
- 아까 예제에 4개의 인터페이스가 중간에 하나씩 추가된 형태라고 볼 수 있다.
cf) 인터페이스 최소 주의 원칙
- 인터페이스를 통해 메서드를 외부에 제공하는 경우, 최소한의 메서드만 제공해야한다.
- ex) 앞에서 살펴본 예시에 대해서 남자친구 클래스에서는 사격하기()를 제공할 수 없다는 의미.
상위클래스가 풍성해야하는 이유와 관련해서 다음 예제를 살펴보자.
Ex 1) 빈약한 상위클래스
- 사람
<hide/>
public abstract class 사람 {
public String name;
abstract void 먹다();
public 사람(String name) {
this.name = name;
}
}
- 학생
<hide/>
public class 학생 extends 사람{
String birthday;
String identityNumber;
String studentNumber;
void 자다(){
}
void 소개하다(){
}
void 공부하다(){
}
@Override
void 먹다() {
}
public 학생(String name, String birthday, String identityNumber, String studentNumber) {
super(name);
this.birthday = birthday;
this.identityNumber = identityNumber;
this.studentNumber = studentNumber;
}
}
- 군인
<hide/>
public class 군인 extends 사람{
String birthday;
String identityNumber;
String armyNumber;
void 자다(){
}
void 소개하다(){
}
void 훈련하다(){
}
@Override
void 먹다() {
}
public 군인(String name, String birthday, String identityNumber, String armyNumber) {
super(name);
this.name = name;
this.birthday = birthday;
this.identityNumber = identityNumber;
this.armyNumber = armyNumber;
}
}
- driver
<hide/>
사람 김학생 = new 학생("김학생", "940907", "940907-2333123", "20130646");
사람 이군인 = new 군인("김학생", "981231", "981231-2333123", "20230909");
System.out.println(김학생.name);
System.out.println(이군인.name);
// System.out.println(김학생.birthday);
// System.out.println(이군인.birthday); // 군인으로 형변환하지 않으면 컴파일 에러난다.
System.out.println(((학생) 김학생).birthday);
System.out.println(((군인) 이군인).birthday);
// System.out.println(김학생.identityNumber);
// System.out.println(이군인.identityNumber); // 군인으로 형변환하지 않으면 컴파일 에러난다.
System.out.println(((학생) 김학생).identityNumber);
System.out.println(((군인) 이군인).identityNumber);
김학생.먹다();
이군인.먹다();
// 김학생.자다();
// 이군인.자다();
((학생) 김학생).자다();
((군인) 이군인).자다();
// 김학생.소개하다();
// 이군인.소개하다();
((학생) 김학생).소개하다();
((군인) 이군인).소개하다();
((학생) 김학생).공부하다();
((군인) 이군인).훈련하다();
Note)
- 위에서 생성한 인스턴스들은 상위 클래스(사람)의 속성이나 메서드 (이름, 먹다())에만 직접 접근 가능하다.
- (군인, 학생) 같은 하위 클래스에서 정의한 속성이나 메서드()에 접근하기 위해서는 명시적 형변환을 반드시 거쳐야한다.
- 결과적으로 보면 여기 저기서 형변환이 발생한다.
- 애초에 인스턴스 '김학생'을 학생 타입으로 선언하고 '이군인'을 군인 타입으로 선언할 수도 있으나 그러면 상속 구조를 만들 필요가 없다.
- 학생, 군인에 공통적인 속성, 메서드가 불필요하게 있어서 이를 묶을 필요가 있다.
Ex 2) 풍성한 상위클래스
- 사람
<hide/>
public abstract class 사람 {
String name;
String birthday;
String identityNumber;
abstract void 먹다();
abstract void 자다();
abstract void 소개하다();
public 사람(String name, String birthday, String identityNumber) {
this.name = name;
this.birthday = birthday;
this.identityNumber = identityNumber;
}
}
- (학생 군인 클래스 생략)
- driver
<hide/>
사람 김학생 = new 학생("김학생", "940907", "940907-2333123", "20130646");
사람 이군인 = new 군인("김학생", "981231", "981231-2333123", "20230909");
System.out.println(김학생.name);
System.out.println(이군인.name);
System.out.println(김학생.birthday);
System.out.println(이군인.birthday);
System.out.println(김학생.identityNumber);
System.out.println(이군인.identityNumber);
김학생.먹다();
이군인.먹다();
김학생.자다();
이군인.자다();
김학생.소개하다();
이군인.소개하다();
// 김학생.공부하다(); 컴파일 에러
// 이군인.훈련하다(); 컴파일 에러
((학생) 김학생).공부하다();
((군인) 이군인).훈련하다();
Note)
- 불필요한 형변환이 발생하지 않는다.
- 하위클래스에서 정의한 메서드에 접근하기 위해서 명시적 형변환이 필요한 것은 1번과 동일하다.
DIP(Dependency In Principle, 의존 관계 역전 원칙)
- 추상화된 것은 구체화에 의존하면 안 된다는 의미.
Ex) 의존 관계 역전 원칙
- 자동차 → 스노우타이어: 자동차가 스노우타이어에 의존하는 경우를 떠올리자.
- 자동차는 한번 사면 몇 년 타야한는데 스노우 타이어는 계절마다 일반 타이어로 바꿔야한다.
- 다시 말해, 자동차는 자주 변하지 않고 스노우타이어는 자주 변한다.
- 그러면 자동차는 자신보다 변하기 쉬운 스노우타이어에 의존하므로 자신이 부서지기 쉽다는 걸 알고있다.
- 위 관계를 개선하려면 아래와 같이 바꿔야한다.
- 자동차 → (interface) 타이어 ← implement —(스노우타이어, 일반타이어, 광폭타이어)
- 이렇게 바꾸면 자동차가 구체적인 것(스노우타이어, 일반타이어, 광폭타이어)에 의존하지 않고 추상적인 타이어 interface에 의존하도록 함으로써 타이어 종류가 바뀌어도 자동차는 아무 영향을 받지 않도록 만든다.
- 결과적으로 의존의 방향이 역전됐다.
관심사의 분리 - SOC(Seperation of Concerns)
- 관심사가 같은 것끼리 하나의 객체 안으로 또는 친한 객체로 모으고 관심이 없는 것끼리는 가능한 떨어뜨려서 서로 분리하는 것을 말한다.
- 하나의 속성 또는 메서드 또는 클래스 또는 모듈 또는 패키지 안에는 하나의 관심사만 들어가야한다.
- SOC를 적용하면 자연스럽게 SRP, ISP, OCP에 도달하게 된다.
Note
- con
- con
찾아보기
- con
- con
출처 - 「 스프링 입문을 위한 자바 객체 지향의 원리와 이해 - 김종민 」