약어 | 개념 | ||
S | SRP | The Single Responsibility Principle 단일 책임 원칙 |
A class should have one, and only one, reason to change. 한 클래스는 하나의 책임만을 가져야 한다. |
O | OCP | The Open Closed Principle 개방-폐쇄 원칙 |
You should be able to extend a classes behavior, without modifying it. 소프트웨어 객체(클래스, 모듈, 함수 등)는 확장에 열려있어야 하고, 수정에 대해서는 닫혀 있어야 한다. |
L | LSP | The Liskov Substitution Principle 리스코프 치환 원칙 |
Derived classes must be substitutable for their base classes. subType은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다. |
I | ISP | The Interface Segregation Principle 인터페이스 분리 원칙 |
Make fine grained interfaces that are client specific. 클라이언트는 자신이 사용하지 않을 메소드를 구현하도록 강요받지 말아야 한다. |
D | DIP | The Dependency Inversion Principle 의존성 역전 원칙 |
Depend on abstractions, not on concretions. 고수준 모듈이 저수준 모듈에 의존해서는 안된다. |
단일 책임 원칙(The Singel Responsibility Principle)
A class should have one, and only one, reason to change.
클래스의 수정 원인은 단 하나여야 한다.
단일 책임 원칙에 따라 하나의 클래스는 자신의 책임에 따라
응집도가 높고 결합도가 낮은 방식으로 설계가 되어야 한다.
만약 해당 원칙이 지켜지지 않았을 때 일어날 문제를 단순화하여 간접적으로 체험해 보면 다음과 같다.
사용자가 로그인하고 메인으로 이동하고 팝업 이벤트를 띄워 줘야지~
라고 생각하며 개발자 A 씨가 만든 클래스라고 할 때,
이 프로젝트는 점차 커지고 복잡한 요구사항이 늘어났다.
"(어떤 특정 이벤트) 후에 메인으로 이동되게 해 주세요"
"(어떤 특정 이벤트) 때에 팝업 이벤트를 보여주게 해 주면 좋을 것 같아요"
"(본인인증 이라던지) 후에는 로그인시켜 주면 좋을 것 같은데"
이때 개발자 A 씨는 아차 싶은 마음이 들 것이다.
하지만 회사는 개발자 A 씨의 당혹감 따위는 기다려 주지 않을 것이다.
"아, 로그인에 간편 로그인 방식을 추가해야 할 것 같아, 가능하지?"
자고로 망하지 않는 회사란 사용자에게 계속적인 서비스를 제공하고 끊임없는 콘텐츠를...
"팝업 이벤트를..."
"이동 이벤트에..."
"아! 로그인할 때.."
A class should have one, and only one, reason to change.
클래스의 수정 원인은 단 하나여야 한다.
그렇기 때문에 하나의 클래스에는 하나의 책임을 가져야 하며,
그 책임이라는 것은 어떠한 변경 사항에 1대 1로 대응할 수 있는 클래스의 역할을 뜻 한다.
SRP가 지켜지지 않았을 경우 해당 클래스의 직관성이 떨어지고 테스트도 용이하지 않다.
(하나의 클래스에 여러 기능이 결합되어 있기 때문에 정확히 하나의 기능을 테스트하기 어렵다)
확장성 또한 떨어지며 예상치 못하게(본인이 생각한 것보다 더욱)
결합도를 높이는 결과를 가져올 것이다.
개방-폐쇄 원칙(The Open Closed Principle)
You should be able to extend a classes behavior, without modifying it
클래스 동작을 수정 없이 확장할 수 있어야 한다.
소프트웨어 객체(클래스, 모듈, 함수 등)는 확장에 열려있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
OCP 원칙이 지켜지지 않았을 때
해당 예시는 얄팍한 코딩사전, SOLID 원칙에서 참고하였습니다.
class 파일 {
fun 파일 만들기(type: 파일종류): 파일 {
return if (type == PDF) {
// TODO pdf 파일로 출력하는 코드
} else if (type == EXCEL) {
// TODO excel 파일로 출력하는 코드
}
...
}
}
해당 코드는 OCP 원칙이 지켜지지 않은 코드라고 볼 수 있는데,
만들어야 할 파일의 종류가 늘어날수록 파일의 파일 만들기 함수를 계속 수정해야 한다.
새로 만들 HWP 파일을 만들려고 기존에 잘 만들어져 있는 PDF, EXCEL 등등 코드들이 보호되지 않는 것이다.
개발자들은 "휴먼"이기 때문에 이러한 사소한 이유로 실수가 발생할 수 있으며,
앗차! 하는 순간에 고작 HWP 지원해 주기 위해 한 작업이 모든 파일 만들기 기능을 마비시킬 수도 있는 거다.
(최악의 상황이지만..)
OCP 원칙을 지켰을 때
class 파일 {
fun 파일 만들기()
}
class PDF: 파일 {
fun 파일 만들기()
}
class EXCEL: 파일 {
fun 파일 만들기()
}
...
이 경우 HWP 파일을 지원해야 해서 만들 때
새로운 HWP라는 class를 만들어서 작업하게 된다.
기존에 PDF와 EXCEL 작업을 한 개발자가 아니더라도 쉽게 확장이 가능하다.
(기존 기능의 히스토리를 예민하게 팔로업 하지 않고도 쉽게 쉽게 문제없이 추가적인 기능 제공이 가능)
심지어 어떤 문제가 일어나도 기존의 기능들이 마비되거나 그런 문제가 발생하지 않고
오직 새로 추가한 HWP class에 대한 문제만 발생한다.
리소코프 치환 원칙(Liskov Substitution Principle)
Derived classes must be substitutable for their base classes.
Sub Type은 언제나 자신의 기반 타입(Base Type)으로 교체할 수 있어야 한다.
자식 클래스는 항상 자신의 부모 클래스의 역할을 수행할 수 있어야 한다.
LSP 원칙이 지켜지지 않은 코드
해당 예시는 얄팍한 코딩사전, SOLID 원칙에서 참고하였습니다.
class 새 {
fun 먹다()
fun 날다()
}
class 참새: 새 {
fun 먹다()
fun 날다()
}
class 타조: 새 {
fun 먹다()
fun 날다() {
// 타조는 날지 못함!!
throw 에러()
}
}
"타조"의 객체를 "새" 타입으로 받았을 경우
새.날다()를 실행할 경우 예상치 못한 동작을 실행할 것이다.
val 앞집새: 새 = ???
앞집새.날다() // 만약 앞집새가 타조라면?
LSP 원칙을 지킨 코드
interface 날 수 있음 {
fun 날다()
}
class 새 {
fun 먹다()
}
class 참새: 새, 날 수 있음 {
fun 먹다()
fun 날다()
}
class 타조: 새 {
fun 먹다()
}
주의할 점은 LSP 원칙은 문제는 단순히 잉여 오버라이딩 문제는 아니다.
여기서 핵심은 자식 객체는 부모 객체의 역할을 수행한다라는 부분에 더 집중해야 한다.
해당 예시에 대해서 얄팍한 코딩사전님이 직사각형을 상속받는 정사각형 클래스로 정말 잘 설명해 주셨다.
이 부분이 살짝 이해가 안 된다면 한번 듣는 것을 강력 추천.
Kotlin 컬렉션에서 LSP 위반이라고 볼 수 있는 예시
Vector는 데이터를 어떤 위치에서든 입출력할 수 있는 기능을 기대하지만,
Stack은 데이터를 후입선출이라는 제한적인 입출력이 가능한 기능이다.
LSP에 대한 원칙이 지켜지지 않았을 경우에 대한 문제를 명확하게 와닿지 않는다면,
실무에서 자주 마주칠 수 있는 골치 거리 예시가 있다!
안드로이드에서 TextView를 상속받는 EditText를 상속받는데,
(많은 View들이 다음과 같은 상속 관계로 이루어져 있다.)
많은 개발자들이 stackoverflow에서 EditText에서 이 속성을 적용했는데 why not working!! 하는 것을 볼 수 있다.
며칠 전에도 찾아본 ellipsize 속성 이슈...
기본적으로 클래스는 캡슐화되어 있고 개발자는 해당 클래스의 인터페이스가 모두 정상적으로 동작하는 것을 기대하기 때문에 일어나는 일이다.
그렇기 때문에 LSP가 지켜지지 않고 개발하는 개발자한테는 딱히 해당 문제에서 크게 문제점을 느끼지 못하지만
해당 구현체를 알지 못하는 개발자가 그 클래스를 사용하려고 할 때, 해당 문제는 그 개발자의 뒤통수를 강하게 치게 될 것이다.
(화가 난 개발자가 나의 뒤통수를 치러 올 수도 있다)
딱히, 안드로이드 View들이 잘못 만들어졌다는 의미는 아니다..
그냥 개발 중 이런 혼란이 자주 발생하는 것을 보았기 때문에 LSP가 지켜지지 않았을 때 일어날 문제들의 예시를 든 것이다..
이런 부분에서, 어떤 설계가 좋은 설계일까?라는 것은 다음과 같은 상황에서도
쾌적하게 프로젝트가 운영될 수 있다면 아주 이상적인 설계가 아닐까 생각한다.
해당 프로젝트를 고이 쌓서 몇 년 뒤에
제3의 개발자가 먼지를 털고 프로젝트를 열었을 때도
해당 개발자가 문제없이 잘 운영할 수 있을까?
인터페이스 분리 원칙(Interface Segregation Principle)
Make fine grained interfaces that are client specific.
client에 알맞은 일반적인 인터페이스를 만들어야 한다.
클라이언트는 자신이 사용하지 않을 메서드를 구현하도록 강요받지 말아야 한다.
인터페이스도 책임에 따라 분리되어야 한다는 원칙인데,
예를 들어, 만약 해당 인터페이스가 있을 경우
해당 예시는 얄팍한 코딩사전, SOLID 원칙에서 참고하였습니다.
interface 운전면허증 {
fun 승용차 운전 방법()
fun 버스 운전 방법()
fun 오토바이 운전 방법()
fun 경운기 운전 방법()
}
평생 경운기를 운전할 필요가 없는 2종보통 면허자가
경운기 운전 방법을 알아야 하는 골칫거리가 발생할 것이다.
class 2종보통운전면허증: 운전면허증 {
override fun 승용차 운전 방법() {
// 안전밸트 메고, 시동 걸고, 사이드 브레이크...
}
override fun 버스 운전 방법() { } // 나 왜 2종 땀?
override fun 오토바이 운전 방법() { } // 나 왜 2종 땀?
override fun 경운기 운전 방법() { } // 나 왜 2종 땀?
}
class 사람 {
var 면허증: 운전면허증
fun 운전면허증따기(면허: 운전면허증) { }
...
}
val 김철수: 사람 = 사람()
김철수.운전면허증따기(2종보통운전면허증())
그렇기 때문에 이럴 경우 인터페이스를 분리하여 구현하는 것이
인터페이스 분리 원칙을 따르는 것이다.
interface 소형운전면허증 { }
interface 2종운전면허증 { }
interface 대형운전면허증 { }
...
만약 운전 마스터 클래스일 경우 다음과 같을 것이다.
class 운전 달인: 소형운전면허증, 2종보통운전면허증, 대형운전면허증 ... { }
이렇게 인터페이스를 분리하여 인터페이스 분리 원칙을 따라 구현하면
필요한 인터페이스만 상속받아 구현할 수 있다.
의존선 역전 원칙(Dependency Inversion Principle)
Depend on abstractions, not on concretions.
구체적인 것이 아닌, 추상적인 것에 의존해라
고수준(추상) 모듈이 저수준(구체) 모듈에 의존해서는 안 된다.
추상적인 것은 구체적인 것에 의존하면 안 된다.
DIP를 적용하지 않을 경우
"문"은 구체적인 사물임으로 저수준 모듈이고
"밀기/당기기"는 일반적인 행위를 제어하는 추상회된 고수준 모듈입니다.
해당 모습을 (의사) 코드로 보면 다음과 같습니다.
class 문 {
fun 열기()
fun 닫기()
}
class 여닫이 {
val door: 문 = 문()
fun 밀기 {
door.열기()
}
fun 닫기 {
door.닫기()
}
}
고수준의 구체적인 클래스의 "여닫이"클래스가 "문"이라는 구체적인 저수준 모듈을 가지고 있습니다
"여닫이" 클래스가 "문"에 의존되어 있습니다.
고수준 모듈이 저수준 모듈에 의존되어 있네요
(추상적인 것인 구체적인 것에 의존)
이렇게 구현할 때 다음과 같은 상황에서 곤란해질 수 있습니다.
정직하게 "미닫이" 클래스를 작성해 줍니다.
class 문 {
fun 열기()
fun 닫기()
}
class 여닫이 {
val obj: 문 = 문()
fun 밀기 {
obj.열기()
}
fun 닫기 {
obj.닫기()
}
}
class 미닫이 {
val obj: 문 = 문()
fun 밀기 {
obj.열기()
}
fun 닫기 {
obj.닫기()
}
}
혹시 문제점이 보이나요?
아직 문제점이 명확이 떠오르지 않는다면 한번 더 가정을 해봅시다!
만약 이 상황에서 열고 닫을 수 있는 다른 물체가 생길 경우 어떤 식으로 코드를 수정해야 할까요?
"문"이라는 클래스의 열기(), 닫기()에 열쇠라는 매개변수가 필요하게 수정된다면 수정범위가 어떻게 될까요..!
분명...! "문" 클래스와 "여닫이", "미닫이" 클래스를 따로 구현했는데..!
조금 아찔해져서 저는 DIP를 적용하기로 합니다.
DIP를 적용시켰을 때
미닫이와 여닫이의 열기/닫기 행위를 인터페이스 만들어 의존성을 역전시켰습니다.
해당 모습을 (의사) 코드로 보면 다음과 같습니다.
interface 열기/닫기할 수 있는 물체 {
fun 열기()
fun 닫기()
}
class 문: 열기/닫기할 수 있는 물체 {
overrid fun 열기()
overrid fun 닫기()
}
class 여닫이 {
val obj: 열기/닫기할 수 있는 물체 = ???
fun 밀기 {
obj.열기()
}
fun 닫기 {
obj.닫기()
}
}
class 미닫이 {
val obj: 열기/닫기할 수 있는 물체 = ???
fun 밀기 {
obj.열기()
}
fun 닫기 {
obj.닫기()
}
}
이렇게 코드를 수정한 다음,
아까 말했던 아찔한 상황을 다시 떠올려 보도록 합시다.
해당 경우에서도 뚜껑이 있는 상자라는 클래스를 구현만 하면 끝난다
다른 코드에는 수정 사항이 전혀 생기지 않는 것이다!
class 뚜껑 있는 상자: 열기/닫기할 수 있는 물체 {
overrid fun 열기()
overrid fun 닫기()
}
다음은 아까 말했던 만약 "문"을 열고 닫는데 열쇠가 필요한 경우이다.
이 경우도 "문"에 대한 수정사항만 발생하고 다른 클래스에는 아무런 변경사항이 존재하지 않는다! (와!)
해당 부분은 [문] -> [열쇠가 필요한 문]으로 상속을 통해 기존 코드의 변경사항 없이도 확장이 가능하다.
와! 그러면 기존 코드의 어떤 부분도 수정하지 않고 기능을 확장할 수 있다. 와!
class 문: 열기/닫기할 수 있는 물체 {
var 문열쇠: 열쇠? = null
fun 열쇠 넣기(k: 열쇠) {
문열쇠 = k
}
overrid fun 열기() {
if (문열쇠 != null) {
// 문 열기
} elses {
// 열쇠가 없을 경우 동작
}
}
overrid fun 닫기() {
if (문열쇠 != null) {
// 문 닫기
} elses {
// 열쇠가 없을 경우 동작
}
}
}
참고자료: http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
ArticleS.UncleBob.PrinciplesOfOod
The Principles of OOD What is object oriented design? What is it all about? What are it's benefits? What are it's costs? It may seem silly to ask these questions in a day and age when virtually every software developer is using an object oriented language
butunclebob.com
얄팍한 코딩사전, SOLID 원칙 - 객체지향 디자인 패턴의 기본기(YouTube)
'Android > 학습' 카테고리의 다른 글
[Android] ViewModel Instance 생성 with ViewModelProvider (3) | 2024.11.28 |
---|---|
[Android] Jetpack ViewModel (1) | 2024.11.14 |
[Android] Bundle이란? (feat. Activity 간 데이터 전달에 Intent를 사용하는 이유) (0) | 2024.10.01 |
[CS] 객체지향 프로그래밍(OOP: Object-Oriented Programming)과 절차지향 프로그래밍(Procedural Programming) (1) | 2024.09.04 |
[Android] Android 권장 앱 아키텍처를 통해 보는 ViewModel과 Repository (0) | 2022.09.18 |