SOLID 원칙

김운채·2023년 5월 19일
0

Java

목록 보기
4/11

흔히 객체지향 프로그래밍을 얘기할때 꼭 빼먹지 않는 단어가 있다.
바로 SOLID 원칙.

solid랑 객체지향이라니 이 무슨 재밋는 모순이냐고 🤷‍♀️

암튼 📌 SOLID 원칙이란, 객체지향 프로그래밍의 5가지 설계 원칙을 말한다.

밑에 이놈들의 앞글자를 따서 만들어졌다.

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Priciple): 개방 폐쇄 원칙
  • LSP(Listov Substitution Priciple): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존 역전 원칙

SOLID 원칙을 얘기하는 이유는, 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있기 때문이다.

Why SOLID?
✔ 변경에 유연한 아키텍트
✔ 이해하기 쉬운 아키텍트
✔ 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트 기반의 아키텍트

설계시에 강제되는 것은 아니다.
그러니 아.. 이것은 SOLID원칙에 위배되잖아!!!

하고 엎을 필요는 없다.

프로젝트에 적용할 원칙의 수는 코드의 구성에 따라 다르다고 보면 된다. 각 원칙은 특정 문제를 해결하기 위한 지침일 뿐이며, 만일 코드에 해당 문제가 없으면 원칙을 적용할 이유가 없다.

하지만 디자인 패턴(Design Pattern)들이 SOLID 설계 원칙에 입각해서 만들어진 것이기 때문에, 표준화 작업에서부터 아키텍처 설계에 이르기까지 다양하게 적용되는 되는 SOLID 원칙에 대해서 자세하게 알아보자. 💁‍♀️

객체지향 설계과정

일단 객체지향프로그래밍을 하기 위해 설계는 어떤 순서로 하는지 알아보자.

  1. 요구사항 (제공해야 할 기능) 을 찾고 세분화 한다. 그리고 그 기능을 알맞은 객체로 할당한다.
  2. 기능을 구현하는 데에 필요한 데이터를 객체에 추가한다.
  3. 해당 데이터를 이용하는 기능을 구현한다. (기능은 최대한 캡슐화)
  4. 객체 간에 어떻게 메소드 호출을 주고받을 지 결정한다.

이 과정에서의 SOLID 원칙은 다음과 같다.

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

한 클래스는 하나의 책임만 가져야 한다.

여기서의 '책임' 이라는 의미는 하나의 '기능 담당' 으로 보면 된다.
즉, 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되도록 클래스를 따로따로 여러개 설계하라는 원칙이다.

만일 하나의 클래스에 기능(책임)이 여러개가 있다면 기능 변경(수정) 이 일어났을때 수정해야할 코드가 많아진다.

예를 들어 A를 고쳤더니 B를 수정해야하고 또 C를 수정해야하고, C를 수정했더니 다시 A로 돌아가서 수정해야 하는...

SRP 원칙을 적용해서 책임을 잘 쪼개서 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용에서 벗어날 수 있다.

수정을 덜하는 것 = 프로그램의 유지보수성 높이기
👉 최종적으로 SRP 원칙의 목적은 프로그램의 유지보수 성을 높이기 위한 설계 기법이다.

하지만 실무적으로 비즈니스로직이라는 것은 범위가 넓고 다양하고, 복잡하기 때문에 이 원칙을 이용해서 설계하기가 쉽지가 않다. 도메인에 대한 이해가 부족하면 자칫 원칙에서 멀어질 수 있다.

SRP 원칙 적용 주의점

  1. 클래스명은 책임의 소재를 알수있게 작명
    클래스가 하나의 책임을 가지고 있다는 것을 나타내기 위해, 클래스명을 어떠한 기능을 담당하는지 알수 있게 작명하는 것이 좋다.

  2. 책임을 분리할때 항상 결합도, 응집도 따지기
    응집도란 한 프로그램 요소가 얼마나 뭉쳐있는가를 나타내는 척도이고,
    결합도는 프로그램 구성 요소들 사이가 얼마나 의존적인지를 나타내는 척도이다.

좋은 프로그램이란 응집도를 높게, 결합도는 낮게 설계하는 것을 말한다.

따라서 여러가지 책임으로 나눌때는 각 책임간의 결합도를 최소로 하도록 코드를 구성해야 한다.

하지만 너무 많은 책임 분할로 인해, 책임이 여러군데로 파편화 되어있는 경우에는 산탄총 수술로 다시 응집력을 높여주는 작업이 추가로 필요하다.
그니까 적당히~ 하라는 것. 사실 젤 어려운것임

산탄총 수술 :
Move Field와 Move Method를 통해 책임을 기존의 어떤 클래스로 모으거나, 그럴만한 클래스가 없다면 새로운 클래스를 만들어 해결한다. 즉 산발적으로 여러 곳에 분포된 책임들을 한 곳에 모으면서 설계를 깨끗하게 정리하여 응집도를 높인다.


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

클래스는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

OCP 원칙은 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성해야 하는 설계 기법이다.

확장에 열려있다 - 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있음

변경에 닫혀있다 - 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한함.

그냥 추상화 쓰세요! 이다.
유지보수성이 좋고 재사용 가능한 코드를 만드는 기반으로써 객체 지향의 장점을 극대화하는 원리 인것이다.

OCP 원리 적용방법

  1. 먼저 변경(확장)될 것변하지 않을 것을 엄격히 구분한다.
  2. 이 두 모듈이 만나는 지점에 추상화(추상클래스 or 인터페이스)를 정의한다.
  3. 구현체에 의존하기보다 정의한 추상화에 의존하도록 코드를 작성 한다.

예시

OCP 원칙의 가장 잘 따르는 예시가 바로 자바의 데이터베이스 인터페이스인 JDBC이다.

만일 자바 애플리케이션에서 사용하고 있는 데이터베이스를 MySQL에서 Oracle로 바꾸고 싶다면, 복잡한 하드 코딩 없이 그냥 connection 객체 부분만 교체해주면 된다.

즉, 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀(closed) 되어 있는 것이다. 반대로 데이터베이스를 손쉽게 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 말이 된다.

OCP 원칙 적용 주의점

추상화(추상 클래스 or 인터페이스)를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
OCP 원칙의 위반은 LSP 와 ISP위반으로 이어지게 된다.


3. LSP (Liskov Substitution Principle) 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

LSP는 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다는 원칙 이다.

즉, LSP는 다형성의 특징을 이용하기 위해, 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미하는 것이다.

예시

이러한 LSP 원칙을 잘 적용한 예제가 자바의 컬렉션 프레임워크(Collection Framework) 이다.

만일 변수에 LinkedList 자료형을 담아 사용하다, 중간에 전혀 다른 HashSet 자료형으로 바꿔도 add() 메서드 동작을 보장받기 위해서는 Collection 이라는 인터페이스 타입으로 변수를 선언하여 할당하면 된다.

왜냐하면 인터페이스 Collection의 추상 메서드를 각기 하위 자료형 클래스에서 implements하여 인터페이스 구현 규약을 잘 지키도록 미리 잘 설계되어 있기 때문이다.

void myData() {
	// Collection 인터페이스 타입으로 변수 선언
    Collection data = new LinkedList();
    data = new HashSet(); // 중간에 전혀 다른 자료형 클래스를 할당해도 호환됨
    
    modify(data); // 메소드 실행
}

void modify(Collection data){
    list.add(1); // 인터페이스 구현 구조가 잘 잡혀있기 때문에 add 메소드 동작이 각기 자료형에 맞게 보장됨
    // ...
}

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

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다

ISP 원칙은 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙이다.

SRP 원칙클래스의 단일 책임을 강조한다면, ISP인터페이스의 단일 책임을 강조하는 것으로 보면 된다.
즉, SRP 원칙의 목표는 클래스 분리를 통하여 이루어진다면, ISP 원칙은 인터페이스 분리를 통해 설계하는 원칙이다.

ISP 원칙은 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표이다.

ISP 원칙 적용 주의점

1. SRP 와 ISP 원칙 사이의 관계

SRP가 클래스의 단일 책임 원칙이라면, ISP는 인터페이스의 단일 책임 원칙이라고 했다.

즉, 인터페이스에 기능에 대한 책임에 맞게 추상 메소드를 구성하면 된다는 말이다.
하지만 책임을 준수하더라도 실무에서는 ISP가 만족되지 않을 수 있는 케이스가 존재한다.

예를들어 위와 같이 게시판 인터페이스엔 글쓰기, 읽기, 삭제 추상 메서드가 정의되어 있다. 이들은 모두 게시판에 필요한 기능들이며 게시판만을 이용하는 단일 책임에 위배되지 않는다.

하지만 이를 구현하는 일반 사용자 입장에선 게시글 강제 삭제 기능은 사용할 수 없기 때문에 결국 ISP 위반으로 이어진다.

따라서 책임을 잘 구성해 놓은 것 같지만 실제 적용되는 객체에겐 부합되지 않을 수 있기 때문에 책임을 더 분리해야 한다.

2. 인터페이스 분리는 한번만

본래 인터페이스라는 건 한번 구성하였으면 왠만해선 변하면 안되는 정책같은 개념이다.

이미 구현되어 있는 프로젝트에 또 인터페이스들을 분리한다면, 이미 해당 인터페이스를 구현하고 있는 온갖 클래스들과 이를 사용하고 있는 클라이언트(사용자)에서 문제가 일어날 수 있기 때문이다.

따라서, 한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스들을 분리하지말라는 것이다.


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

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

DIP 원칙은 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙이다.

구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 것이다.
= 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는, 변화하기 어려운 것 거의 변화가 없는 것에 의존하라는 것

변하기 어려운 것
➡ 정책, 전략과 같은 큰 흐름, 개념 같은 추상적인 것

변하기 쉬운 것
➡ 구체적인 방식, 사물 등과 같은 것

다시 말하면 클라이언트(사용자)가 상속 관계로 이루어진 모듈을 가져다 사용할때, 하위 모듈을 직접 인스턴스를 가져다 쓰지 말라는 뜻이다. 하위 모듈의 구체적인 내용에 클라이언트가 의존하게 되어 하위 모듈에 변화가 있을 때마다 클라이언트나 상위 모듈의 코드를 자주 수정해야 되기 때문이다.

상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에, 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이다.

=> 한마디로 상위의 인터페이스 타입의 객체로 통신하라는 원칙 => 캡슐화





참고자료 :
https://old-developer.tistory.com/96
inpa.tistory.com/entry/OOP-💠-객체-지향-설계의-5가지-원칙-SOLID
velog.io/@y_dragonrise/소프트웨어-설계-의존-역전-원칙DIP-Dependency-Inversion-Principle
https://catsbi.oopy.io/ded9db4f-6ed6-48c7-9d46-8a0d88b1b6d0

0개의 댓글