좋은 소프트웨어는 Clean-Code 로 부터 시작한다.
좋은 재료를 사용하지 않으면 건물의 아키텍쳐가 좋고 나쁨에 의미가 없다.
반대로 좋은 재료를 사용하더라도 아키텍쳐가 엉망이라면 나쁜 품질의 소프트웨어가 될 수 있다.
그래서 프로그래밍 세상에는 좋은 아키텍쳐를 정의하는 원칙이 필요한데 그것이 SOLID 원칙이다.
SOLID 원칙의 목적
SOLID 원칙은 아래와 같은 목적을 지니고 있다.
- 변경에 유연하다.
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
SOLID 종류
SRP: 단일 책임 원칙(Single Responsibility Principle)
소프트웨어 시스템이 가질 수 있는 최적의 구조는 시스템을 만드는 조직의 사회적 구조에 커다란 영향을 받는다.
따라서 각 소프트웨어 모듈의 변경은 이유가 단 하나여야만 한다.
나는 이 책을 읽기 전까지 한가지 책임을 하는 클래스나 컴포넌트를 작성해야 한다. 라고 이해했는데
사실 모듈의 변경의 이유가 단 하나라고 하는 것이 더 맞는 설명이라는 생각이 들게 되었다.
예를 들면 아래와 같은 상황 때문이다.
만약 아래와 같은 클래스 도식이 있다고 해보자.
첫번째로 calculatePay 는 회계팀에서 기능을 정의하고 있다고 가정해보자.
두번째로 reportHours 는 인사팀에서 기능을 정의하고 있다고 해보자.
세번째로 save 는 DBA가 기능을 정의하고 있다고 해보자.
어쩌면 Employee 는 직원이 가지고 있는 책임의 일을 한다고 생각할지도 모른다.
하지만 위에서 말한 시스템을 만드는 조직의 사회적 구조에 커다란 영향을 생각해보자.
아래와 같은 상황이 나온다면 Employee 가 과연 변경의 이유가 한가지일지 생각해보아야 한다.
calculatePay 와 ReportHours 는 내부메소드(private) regularHours 를 공유하고 있다고 해보자.
그런데 여기서 회계팀에서 갑자기 업무 시간을 계산하는 방식을 수정하게 되었다고 해보자.
개발자가 개발하는 도중에 인사팀에서 사용하는 메소드에 사이드 이펙트를 전파하고 있다는 사실을 알 수도 모를 수도 있게 된다.
만약 모른다면 인사팀의 reportHours 로 계산한 보고서는 엉터리 보고서가 되고 말것이다.
만약에 알게 된다면 reportHours 를 위해 코드를 추가하거나 변경해야 하는 일이 발생할 것이다.
위와 같은 현상이 왜 일어난다고 생각하는가?
바로 시스템을 만드는 두개의 조직. 즉, 두개의 액터가 같은 메소드에 의존하고 있었기 때문이다.
즉 모듈의 변경 이유를 하나로 만들어야만 한다. 그것이 SRP 원칙을 준수하는 길이다.
OCP: 개방-폐쇄 원칙(Open-Close Principle)
개방-폐쇄 원칙이란 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 함을 의미한다.
이 원칙은 상당히 중요하다고 생각하는데, 결국 우리가 어떠한 정책을 수정하거나 추가할때 소프트웨어를 엄청나게 수정해야 한다면, 정책을 추가하기도 어려워 질 것이고, 수정하기도 어려워지는 문제에 부딪힐 것이다.
즉 소프트웨어 아키텍쳐의 성공을 위해서는 개인적인 생각에는 가장 중요한 원칙이 아닐까 싶다.
예를 들어 원래 보고 있던 보고서를 갑자기 웹 페이지가 아닌 프린터로 보여달라고 한다면 얼마나 많은 코드가 변경될까?
이상적인 변경치는 0 이다.
이를 위해서는 아래와 같은 최소 아래와 같은 설계가 이루어져 있어야 할 것이다.
위와 같은 설계처럼 되어 있다면 데이터를 출력하는 계층(Representation Layer) 을 인터페이스로 지정해 두었기에 의존성 역전 원칙 통해 이를 쉽게 제어할 수 있을 것이다. 예를 들면 내가 프린터 출력을 쓰고 싶으면, 프린터 디펜던시를 주입해주면 되고, 웹으로 출력을 하고 싶다면 웹 디펜던시를 주입하면 될 것이다.
LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
리스코프 치환 원칙은 상위타입의 객체에 대응되는 하위타입의 객체로 치환하더라도 상위 타입에 의존해 있는 기존 동작 방식이 바뀌지 않는 것을 의미한다.
자바에서 아주 가장 쉽게 예를 들면 아래와 같은 예시를 들 수 있을 것이다.
예를 들어 내가 코드를 구현하는데 List 형태의 자료구조를 이용해야 한다고 생각해보자.
public Temp {
private List<Integer> lists;
public Temp(List<Integer> lists) {
this.lists = lists;
}
}
위에서 말했듯 상위형태(List) 에 의존하는 Temp 는 ArrayList 를 쓰던, LinkedList 를 쓰던 정상적으로 동작할 것이다.
ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
인터페이스 분리 원칙은 음 간단하게는 의존 관계가 뚜렷하지 않는 인터페이스들은 서로 분리되어야 한다?
말로 설명하기 어려우니 아래 그림을 한번 보자.
위와 같은 상황에서 User1 은 op1 만, User2 는 op2 만, User3 는 op3 에 의존하고 있다고 해보자.
이경우에 User1 은 op1 말고 op2, op3 는 의존하고 있지 않음에도 OPS 를 의존하고 있다.
즉 이런경우 필요없이 의존하고 있는 것이므로 세분화가 필요하다.
불필요한 의존성으로 인해 재배포 및 재컴파일을 해야하는 상황이 생길 수 있다.
DIP: 의존성 역전 원칙 (Dependency Inversion Principle)
의존성 역전 원칙은 위에서 설명했듯 구체적인 구현체를 직접 선언하는 구조가 대부분의 형태였는데, 이것이 아닌 외부에 의해 의존성이 주입될 경우 기존처럼 의존성이 외부에 의해 결정될 수 있는 의존성 역전 현상이 발생하게 된다.
그래서 의존성 역전 원칙에서 가장 중요하게 말하는 유연한 소프트웨어는 아래와 같다.
소스코드의 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템
DIP 가 전달하려는 내용은 아래로 요약해 볼 수 있다.
변동성이 큰 구체 클래스를 참조하지 말라! 대신 추상 인터페이스를 참조해라!
- 위와 같이 말하는 이유는 우리의 구현체는 언제나 계속해서 바뀌지만, 인터페이스는 구현체에 비해 잘 바뀌지 않는다. 즉 소프트웨어는 잘 바뀌지 않는 것에 의존해야 한다. 그래야 최소한의 변경이 일어난다.
변동성이 큰 구체 클래스로부터 파생하지 말라.
- 이 규칙은 위의 규칙과 같은 이치이지만, 우리가 구현체에서부터 파생되기 보다는 추상체에서부터 파생되라는 뜻이다. 즉 비슷한 기능이라면 구현체에서 파생되는 것 보다는 인터페이스로 정의하는 것이 좋을 수 있다는 뜻이다.
구체 함수를 오버라이드 하지 말아라.
- 대부분 구체 함수는 소스코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 의존성을 제거할 수 없게 된다.
오늘은 매우 간단하게 SOLID 원칙을 정리해 보았다.
대부분의 내용은 클린 아키텍쳐 라는 책을 공부하며 정리한 글이니, 궁금하다면 그 책을 읽어보길 바란다.
'Architecture' 카테고리의 다른 글
Web 계층에 Port 를 통해 더 자유로운 코드 짜기 (0) | 2022.01.20 |
---|---|
클린 아키텍쳐와 Domain Layer (2) | 2021.12.08 |
협력과 메세지 (0) | 2021.12.02 |
응집도와 결합도 (1) | 2021.11.28 |
컴포넌트 (0) | 2021.11.17 |