대가는 결과를 만든다

SOLID 원칙에 대한 정리 본문

카테고리 없음

SOLID 원칙에 대한 정리

yunzema 2024. 6. 25. 10:45
반응형

SOLID원칙이란 객체 지향 설계 프로그래밍의 유지보수성을 높이고, 유연하고, 확장이 쉽게하기 위해 마틴 아저씨가 만든 원칙이다.

패턴보다 작지만 표준화 작업, 아키텍처 설계에 이르기까지 다양하게 적용될 수 있는 원칙이다.

 

1. SRP(Single Responsibility Principle) 단일 책임의 원칙

2. OCP(Open Close Principle) 개방 폐쇄의 원칙

3. LCP(The Liskov Substitution Principle) 리스코브 치환의 원칙

4. ISP(Interface Segregation Principle) 인터페이스 분리의 원칙

5. DIP(Dependency Inversion Principle) 의존성역전의 원칙

 

1. SRP - 단일책임의 원칙

1-1. 정의

- 작성된 클래스는 하나의 기능만 가짐. 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야함.

- 한 책임의 변경으로 인한 연쇄작용을 방지하기 위함.

- 책임을 분배함으로 코드 가독성 향상과, 유지보수 용이하게 하기 위함.

- 책임이란 단어를 상기하며, 복잡 다양한 실무 속에서 꾸준한 연습이 필요함.

1-2.  적용

- 여러 원인에 의한 변경이 발생 시 혼재된 책임을 각각 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 함.

- 산발적으로 분포된 책임(Shotgun surgery)들을 한 곳에 모으면서 설계를 깨끗하게 한다 -> 응집성을 높인다.

- 응집력이 있다면 병합을 하고, 결합력이 있다면 분리해야 한다.

 

2.  OCP - 개방폐쇄의 원칙

1-1. 정의

- 소프트웨어 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 열려있고, 변경에 닫혀 있어야 한다.

- 요구/추가사항의 발생으로 인해 기존 구성요소는 수정이 일어나지 않고, 쉽게 확장가능해야 한다.

1-2.  적용

- 변경(확장)될 것과 변하지 않을 것을 엄격히 구분

- 이 두 모듈이 만나는 지점에 인터페이스 정의

- 구현에 의존하기보다 정의한 인터페이스에 의존하도록 한다.

- 확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 복잡도가 올라갈 수 있다.

- 인터페이스는 가능하면 변경되지 않아야 한다. 따라서 인터페이스를 정의할 때 추상화 레벨을 잘 정해야 한다.

 

3.  LSP - 리스코브 치환의 원칙

1-1. 정의

- 서브 타입은 언제나 기반이 되는 타입으로 교체할 수 있어야 한다.

- 기반 타입이 약속한 규약을 지켜야 한다. (확장에 대한 인터페이스를 준수해야 한다.)

- LSP는 OCP를 구성하는 구조가 된다.

1-2.  적용

- 두 개체가 똑같은 일을 한다면, 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다. 공통된 연산이 없다면 완전 별개의 클래스를 생성한다.

- 똑같은 연산을 제공하지만, 약간씩 다르다면 공통의 인터페이스를 만들고 상속하여 둘이 이를 구현한다.

- 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 상속을 한다.

- 예) Collection 프레임워크 설계 - LinkedList와 HashSet은 모두 Collection 인터페이스를 상속하고 있으므로 modify 함수는 정상동작하며, modify 변화에는 닫혀 있으면서, Collection의 변경과 확장에는 열려 있는 구조 OCP가 된다.

LinkedList list = new LinkedList();
modify(list);

void modify(LinkedList list) {
	list.add(...);
    doSomething(list);
}

====================================

Collection collection = new HashSet();
modify(collection);

void modify(Collection collection) {
	collection.add(...);
    doSomething(collection);
}

 

1-3.  적용 이슈

- 상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 IS-A 관계(사과(자식)는 과일(부모)이다, 과일은 사과가 아니다.)가 있을 경우로만 제한되어야 한다.

- LSP를 지키기 어렵다면 상속대신 합성(composition)을 사용

- 상속 구조가 필요하다면 Extract Subclass, Push Down Field, Push Down Method등의 리펙토링 기법을 이용하여 LSP 준수하는 계층 구조를 구성

- 서브 클래스의 제약사항이 기반 클래스의 제약사항보다 느슨하거나 같아야 한다. (서브클래스의 강 조건으로 인해 기반클래스에서 실행되던 것이 안될 수도 있기 때문)

 

4.  ISP - 인터페이스 분리의 원칙

1-1. 정의

- 하나의 일반적인 인터페이스보다는 여러개의 구체적인 인터페이스가 낫다.

- 인터페이스의 단일책임을 강조하며, 변화로 인해 여러 책임을 가지는 경우(여러 인터페이스/클래스로 인해 부분집합만 이용하는 경우) 인터페이스를 분리 한다.(클래스의 단일책임을 강조하는 SRP와 같은 맥락)

1-2.  적용

- 클래스 상속을 이용하여 인터페이스를 분리

- 위임(Delegation)을 통해 객체 인터페이스로 분리.

- 위임이란 HAS-A 관계로 위임관계에 있는 인스턴스를 가지고 있는 상태이다. 다른 클래스의 기능을 사용해야 하지만 그 기능을 변경하고 싶지 않고, 특정 일의 책임을 다른 클래스에 맡기는 패턴이다.

 

//상속 구조
interface Worker {
	fun work()
    fun takeVacation()
}

class Worker_1 : Worker {
	override fun work() = println("First Worker")
    override fun takeVacation() = println("First Worker Take Vacation")
}

class Worker_2 : Worker {
	override fun work() = println("Second Worker")
    override fun takeVacation() = println("Second Worker Take Vacation")
}

open class Worker_1 : Worker {
	override fun work() = println("First Worker")
    override fun takeVacation() = println("First Worker Take Vacation")
}

//Manager 정의
class Manager : Worker_1()

fun main() {
	val manager = Manager()
    manager.work()  // First Worker
    //Manager 클래스가 Worekr_1 클래스에 종속되어 버리고 오로지 Worker_1 만을 위한 Manager가 되어버린다.
}

=========================================================================
//위임 구조
class Manager(val worker: Worker) {
	fun work() = worker.work()
    fun takeVacation = worker.work()  // Manager가 쉬어도 Worker는 일해야한다
}

fun main() {
	val manager = Manager(Worker_1())
    manager.work()  // First Worker
}
//Manager가 Worker_1 클래스에 강하게 묶이지 않아 언제든지 새로운 Worker에 대한 인터페이스를 구현이 가능
//Manager의 인스턴스는 Worker 인터페이스를 구현하는 클래스의 인스턴스에게 위임할 수 있다

참고: https://dahoonkk.tistory.com/entry/Kotlin-%EC%9C%84%EC%9E%84-%ED%8C%A8%ED%84%B4Delegation-Pattern%EA%B3%BC-%EC%9C%84%EC%9E%84Delegation-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%83%81%EC%86%8DInheritance

 

1-3.  적용 이슈

- 기 구현된 클라이언트에 변경을 주지 말아야 한다.

- 두개 이상의 인터페이스가 공유하는 부분의 재사용을 극대화 한다.

- 서로 다른 성격의 인터페이스를 명백히 분리한다.

 

 

5. DIP - 의존성역전의 원칙

1-1.  정의

- 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊어, 관계를 최대한 느슨하게 만드는 원칙

- 키워드

  - DI(Dependency Injection) - 의존성 주입. 구성요소간 의존 관계가 외부를 통해 정의되게하는 패턴. 생성자 주입/Setter 주입/인터페이스 주입

  - DIP(Dependency Injection Principle) - 상위 모듈은 하위 모듈에 의존해서는 안되며, 상위모듈과 하위 모듈 모두 추상화에 의존해야 한다. 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

  - IOC(Inversion Of Control) - 외부 라이브러리의 코드가 프로그래머가 작성한 코드를 호출한다.

  - Hook - 선택적으로 오버라이드 할 수 있도록 만들어둔 메서드를 통해 의존성 주입

 

1-2.  적용

- Layering : 정의되어 있는 레이어 간의 의존이 바로 연결이 되는 것이 아닌, 둘 사이에 존재하는 추상 레벨을 통해 의존해야 함. 이를 통해 상위 모듈은 하위레벨의 모듈로의 의존성에서 벗어나 그자체로 재사용/확장성을 보장받을 수 있음.

 

Comments