SRP - 단일 책임 원칙
⭐️ 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다
즉 하나의 클래스는 하나의 역할만 해야한다는 의미이다.
하나의 클래스가 너무 많은 역할을 맡고 있을 때 객체지향에서는 '나쁜 냄새가 나는 코드'라고 부른다.
SRP가 잘 지켜지지 않은 설계의 예시를 보자.
✏️ EX 1)
사람 클래스가 있고, 이 사람 클래스에 의존하는 여러가지 클래스들(애인, 부모님, 회사 등)이 존재한다고 하자.
이 사람 클래스 안에 여러 가지 역할에 해당하는 메소드를 모두 넣는 경우가 SRP가 잘 지켜지지 않은 설계이다. 하나의 클래스가 여러 역할을 하고 있기 때문이다.
아래와 같이 역할을 나누어 클래스를 여러 개로 분리한다면 SRP를 잘 지킨 더 좋은 설계가 된다.
✏️ EX 2)
더 예시를 들어보자. 만약 "음식"이라는 상위 클래스가 있고, 이를 확장하는 하위 클래스로 "마라탕"과 "아이스크림"이 있다고 해보자.
이 때, 마라탕의 맵기를 의미하는 속성(필드)를 "int 맵기;"로 음식 클래스 안에 선언해놓았다고 하자. 하지만 아이스크림에는 맵기라는 변수가 필요가 없다.
📌 따라서 음식 클래스 안에는 아이스크림과 마라탕에 모두 필요한 속성값만 선언하고(예를들면 가격, 칼로리 등등), 겹치지 않는 필드들은 역할을 분리하여 각각의 하위 클래스 안에만 선언에 놓는 것이 SRP를 잘 지킨 설계이다.
✏️ EX 3)
마지막으로 예시를 하나 더 들어보자.
class 반려동물 {
final static Boolean 고양이 = true;
final static Boolean 강아지 = false;
Boolean 종류;
void 기분좋을때행동() {
if (this.종류 == 고양이) {
// 꼬리를 일자로 세운다.
} else {
// 꼬리를 좌우로 흔든다.
}
}
}
반려동물을 나타내는 클래스를 선언하고, 내부에 고양이인지 강아지인지를 구분할 수 있는 Boolean 변수를 선언했다.
그리고 기분좋을때행동() 메소드 안에서 분기문으로 그 값을 확인하여 고양이일 때 행동와 강아지일 때 행동이 달라지도록 구현했다.
📌 *메소드가 단일 책임 원칙을 지키지 않을 경우 나타나는 대표적인 냄새가 이런식으로 분기 처리를 하는 if문이다.
*
SRP를 지키는 설계로 리팩토링 해주면 분기문을 쓰지 않아도 된다. 즉, 반려동물이라는 상위클래스를 선언하고 이를 확장한 고양이와 강아지 클래스를 각각 선언하여 기분좋을때() 행동 메소드를 오버라이딩 해주면 된다.
abstract class 반려동물 {
abstract void 기분좋을때행동();
}
class 고양이 extends 반려동물 {
@Override
public void 기분좋을때행동() {
// 꼬리를 일자로 세운다.
}
}
class 강아지 extends 반려동물 {
@Override
public void 기분좋을때행동() {
// 꼬리를 좌우로 흔든다.
}
}
3장에서는 자바가 지원하는 객체지향 4대 특성 캡슐화, 상속, 추상화, 다형성에 대해서 설명하고 있다.
이 4대 특성 중 이 SRP와 가장 연관이 큰 것은 모델링 과정을 담당하는 추상화이다.
OCP - 개방 폐쇄 원칙
⭐️ 클래스, 모듈, 함수 등은 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.
위 문장을 더 의역해보면 아래와 같은 문장을 이끌어 낼 수 있다.
자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
개방 폐쇄 원칙을 이해하기 위해 아래 예시를 살펴보자.
✏️ EX 1)
만약 위와 같이 마티즈 클래스에 의존하고 있는 운전자 클래스가 있었다고 하자. 근데 소나타라는 클래스가 추가되었고, 운전자가 소나타를 삼으로써 마티즈가 아닌 쏘나타 클래스에 의존하게 되었다.
기존 마티즈는 기어 수동 조작이었지만, 소나타는 기어 자동 조작이다.
기어 조작 방식이 바뀌었다고 해서, 운전자의 운전 스타일까지 변해야할까? 그건 아닐 것이다.
그래서 객체지향에서는 이 경우 OCP를 지켜 더 나은 방법으로 디자인을 할 수 있다. 즉 자신의 확장(소나타라는 새로운 클래스에서 메소드의 기능이 변경됨)은 쉽게 할 수 있지만, 한 클래스(소나타)가 바뀐다고 해서 그와 의존하고 있는 클래스들(운전자 클래스)은 변하지 않아도 되도록 설계할 수 있는 것이다.
자동차라는 상위 클래스 또는 인터페이스를 중간에 둠으로써 다양한 종류의 자동차 클래스가 생겨도 운전자는 자동차에만 의존하고 있기 때문에 운전 스타일에 영향을 받지 않게 되었다.
다양한 자동차가 생긴다는 것은 자동차 입장에서는 자신의 확장에 개방돼 있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄돼 있는 것이다.
✏️ EX 2)
자바 프로그래머는 자신이 작성한 코드가 리눅스에서 실행될지, 윈도우즈에서 실행될지 걱정하지 않는다.
운영체제마다 그에 맞는 JVM이 있고, 그 여러 JVM에 대해서 상관없이 실행될 수 있는 자바 바이트 코드(.class 파일)가 있기 때문이다.
이 .class 파일이 자바 코드와 여러 운영체제 간의 완충제(인터페이스 같은 역할)을 해주고 있다.
여러 운영체제에 대해서는 열려 있지만, 자바 코드에 수정에 대해서는 닫혀있는 것이다. 이것도 일종의 OCP적인 설계라고 볼 수 있을 것 같다.
📌 OCP를 지키지 않으면 객체 지향의 장점인 유연성, 재사용성, 유지 보수성을 얻을 수가 없다. 따라서 OCP는 객체 지향 프로그래밍에서 반드시 지켜야하는 원칙이다.
LSP - 리스코프 치환 원칙
⭐️ 서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.
위의 말을 더 쉽게 말하면
** 하위 클래스의 인스턴스는 상위 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.**
책 <스프링 입문을 위한 자바 객체 지향의 원리와 이해>에서는 상위 클래스 - 하위 클래스, 인터페이스 - 구현 클래스는 다음의 관계를 지켜야한다고 설명한다.
하위 클래스 is a kind of 상위클래스 : 하위 클래스는 상위 클래스 종류 중 하나임
구현 클래스 is able to 인터페이스: 구현 클래스는 인터페이스를 할 수 있음
근데 이 관계가 깨지도록 상속(확장)을 하는게 바로, 조직적/계층적 구조로 확장을 하는 것이다.
이 말은 그림으로 보면 더 이해가 쉬울 것이다.
만약 이런 식으로 상속이 이뤄졌다고 해보자 그러면
아버지 영희 = new 딸();
위에서 LSP는 하위 클래스의 인스턴스는 상위 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다
고 했다.
아버지의 하위 클래스인 딸이 상위 클래스인 아버지에 대입이 돼서 아버지의 역할을 할 수 있는가? 많이 이상하다.
이렇게 계층도로 된 상속 구조는 LSP가 지켜지지 않은 설계이다.
올바른 설계를 보자.
조류 짹짹이 = new 참새();
이는 LSP에 위배되는 것이 없다.
LSP를 지켜서 하위 클래스가 상위 클래스의 한 종류가 되도록 상속 구조를 정하는 것이 좋은 설계이다.
ISP - 인터페이스 분리 원칙
⭐️ ** 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다. **
ISP는 앞에서 SRP에서 들었던 예시를 가지고 다시 설명할 수 있다.
SRP를 설명하며 하나의 사람 클래스를 직원, 남자친구, 자식 클래스로 분할하여 하나의 클래스가 하나의 책임만 가지도록 분할했었다.
ISP를 통해서도 하나의 클래스가 너무 많은 책임을 지고 있는 문제를 해결하도록 구현할 수 있다. 바로 아래처럼 사람 클래스가 여러 인터페이스를 구현하도록 하고, 다른 클래스들이 자신과 관련된 인터페이스에만 의존하도록 설계하는 방법이다.
여기서 중요한 것은 다른 클래스가 자신이 필요한 메소드를 가지는 인터페이스에만 의존해야한다는 것이다. (인터페이스 최소주의: 인터페이스를 통해 메소드를 외부에 제공할 때에는 최소한의 메소드만 제공해야함)
예를 들면, 부모님 클래스에게 출근하기() 메소드를 제공할 필요가 없다.
일반적으로 상위 클래스는 풍부할 수록, 인터페이스는 내부에 내용이 적을 수록 좋은 설계이다.
DIP - 의존 역전 원칙
⭐️ ** 고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야한다. **
이 말은 ** 자신보다 변하기 쉬운 것에 의존하지 말라 **는 의미이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기 때문에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 의존 역전 원칙이다.
Reference
책 <스프링 입문을 위한 자바 객체 지향의 원리와 이해>
'개념 공부 > 기타' 카테고리의 다른 글
[Java / Web] 서블릿과 서블릿 컨테이너, 톰캣의 동작 (0) | 2024.05.16 |
---|---|
깃 명령어 (0) | 2020.08.18 |
[Git] 현재 레포를 강제로 리셋하고, 다시 푸시하기 (0) | 2020.08.04 |
[Git] 리포지토리(레포지토리) 관련 명령어 (0) | 2020.07.29 |
[CSS3] 구조적 가상 클래스 선택자 (0) | 2020.06.28 |