컴퓨터 프로그래밍에서 SOLID란?
- 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것
- SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침
// 코드 냄새(code smell): 코드에서 심한 문제를 일으킬 가능성이 있는 프로그램 소스 코드의 특징 - 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 원칙들을 적용
- 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전박적 전략의 일부
SOLID 원칙
두문자 | 약어 | 개념 |
S | SRP | 단일 책임 원칙 |
O | OCP | 개방-폐쇄 원칙 |
L | LSP | 리스코프 치환 원칙 |
I | ISP | 인터페이스 분리 원칙 |
D | DIP | 의존관계 역전 원칙 |
SRP: 단일 책임 원칙(Single Responsibility Principle)
- 한 클래스는 하나의 책임만 가져야 한다
- 하나의 클래스가 하나 이상의 책임을 가지면 하나의 책임 변경이 다른 책임의 수정을 발생시킨다(coupling)
- SRP가 적용되면
- 응집도가 높아지고, 결합도가 낮아진다.(high cohesion, low coupling)
- 관심사의 분리(SoC) -> 클래스를 더욱 튼튼하게 만든다.
- 높은 자유도, 코드의 단순화, 유지보수 용이
- 예시) 비즈니스 로직과 데이터베이스 처리 로직을 분리하기: DAO
class Person {
String lastName, firstName;
//데이터베이스 처리 로직
String insert() {...}
//비즈니스 로직
Exemption getExemption() {...}
}
한 클래스에 데이터베이스 처리 로직과 비즈니스 로직이 같이 있으면 커플링이 생기게 된다.
이렇게 되면 데이터베이스 접근 루틴 변경 시 다른 비즈니스 로직에 영향이 갈 수 있다.
class Person {
String lastName, firstName;
//비즈니스 로직
Exemption getExemption() {...}
}
class PersonDAO {
//데이터베이스 처리 로직
String insert() {...}
}
데이터베이스 접근 관련 로직을 DAO클래스에 넣어서 커플링을 줄일 수 있다.
이렇게 코드를 짜면 개발 시 비즈니스 로직에만 집중할 수 있다는 장점이 있다.
// 데이터 맵퍼 패턴
OCP: 개방-폐쇄 원칙(Open-Closed Principle)
- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 예시) if else 문 대신 상속 사용하기
class Animal {
String name;
...
animalSound() {
if (name == 'lion') {
return 'roar';
} else if (name == 'mouse') {
return 'squeak';
}
}
}
이런 식의 로직은 새로운 동물이 추가될 때마다 로직이 변경되어야한다.
class Animal {
String name;
...
animalSound() {}
}
class Lion extends Animal {
animalSound() {
return 'roar';
}
}
class Mouse extends Animal {
animalSound() {
return 'squeak';
}
}
상속을 사용하면 확장도 쉽고 Animal 로직을 수정해야할 일도 없어진다.
LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 하위 클래스는 상위 클래스를 대체할 수 있어야 한다.
// 쉽게 생각하면 그냥 상속의 다형성을 사용하는 거다. circle이 shape에 들어갈 수 있는 거 - 코드가 클래스 유형을 확인하는 경우 이 원칙을 위반한 것
- LSP원칙을 따르기 위해 LSP 요구사항을 따라야함.
- 상위 클래스에 상위 클래스 매개변수를 허용하는 메서드가 있는 경우. 하위 클래스는 상위 클래스 or 하위 클래스 유형을 인수로 받아들여야 함
- 상위 클래스가 상위 클래스 유형을 반환하는 경우. 하위 클래스는 상위 클래스 or 하위 클래스 유형을 반환해야 함
- 예시) 상위클래스의 메소드를 부르면 하위 클래스의 메소드가 호출되는 예시(다형성)
function AnimalLegCount(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
}
}
AnimalLegCount(animals);
LSP 원칙을 위배한 예시이다. 이 메소드는 모든 동물 유형을 알고 관련 legcount함수를 호출해야 한다.
class Animal {
//...
LegCount();
}
class Lion extends Animal{
//...
LegCount() {
//...
}
}
function AnimalLegCount(a: Array<Animal>) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);
이렇게 구성하면 다리 수를 반환하기 위해 Animal 유형을 알 필요가 없음.
알고있는 건 매개변수가 Animal 클래스 또는 하위 클래스인 Animal 유형이어야 한다는 것 뿐!!
ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 클라이언트별로 세분화된(fine grained) 인터페이스 만들기
- 클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요해서는 안 됨.
- +) 인터페이스가 하나의 작업만 수행해야 한다
- 이 원칙은 크기가 큰 인터페이스 구현의 단점을 다룬다
예시: 사용하지 않는 인터페이스를 필수적으로 구현해야하는 사례
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
}
class Circle implements IShape {
drawCircle(){ //... }
drawSquare(){ //... }
drawRectangle(){ //... }
}
class Square implements IShape {
drawCircle(){ //... }
drawSquare(){ //... }
drawRectangle(){ //... }
}
딱 봐도 중복코드가 많고 지저분하다. Square는 drawCircle을 사용하지 않는데도 구현을 해야한다.
클라이언트(여기서 Rectangle, Circle 및 Square)는 필요하지 않거나 사용하지 않는 메서드에 강제로 의존해서는 안 된다.
인터페이스는 하나의 작업만 수행해야 하고, 다른 작업은 다른 인터페이스로 추상화되어야 한다.
interface IShape {
draw();
}
class Circle implements IShape {
draw(){
//...
}
}
class Triangle implements IShape {
draw(){
//...
}
}
편안..
DIP: 의존관계 역전 원칙(Dependency Inversion Principle)
- 종속성은 구체화가 아닌 추상화에 있어야 한다
- 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
- 의존성 주입(DI)은 이 원칙을 따르는 방법 중 하나다.
- DIP A. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 함
- DIP B. 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항은 추상화에 따라 달라짐
소프트웨어 개발에서 앱이 주로 모듈로 구성되는 시점이 옴. 이런 일이 발생하면 종속성 주입을 사용하여 문제를 해결해야 한다. 하위 수준 구성 요소에 따라 작동하는 상위 수준 구성 요소
class XMLHttpService extends XMLHttpRequestService {}
class Http {
constructor(private xmlhttpService: XMLHttpService) { }
get(url: string , options: any) {
this.xmlhttpService.request(url,'GET');
}
post() {
this.xmlhttpService.request(url,'POST');
}
//...
}
상위 수준 구성 요소: Http
하위 수준 구성 요소: HttpService
=> 이 설계는 OCP(DIP A)를 위반함.
현재 Http 클래스는 XMLHttpService 클래스에 의존함.
HTTP 연결 서비스를 변경하기 위해 변경해야 하는 경우 Nodejs를 통해 인터넷에 연결하거나 http 서비스를 Mock하고 싶은 경우 -> 코드를 편집하기 위해 Http의 모든 인스턴스를 힘들게 이동해야 함.
고수준 모듈은 저수준 모듈에 의존해서는 안 됨. 추상화에 의존해야 한다
Http 클래스는 사용 중인 Http 서비스 유형에 덜 신경을 써야 한다. 이를 위해 연결 인터페이스를 만들어야함.
interface Connection {
request(url: string, opts:any);
}
class Http {
constructor(private httpConnection: Connection) { }
get(url: string , options: any) {
this.httpConnection.request(url,'GET');
}
post() {
this.httpConnection.request(url,'POST');
}
//...
}
class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts:any) {
xhr.open();
xhr.send();
}
}
1. XMLHttpService 클래스는 Connection 인터페이스 구현하여 만듦.
2. Connection 인터페이스에 request 메서드를 통해서 연결 유형의 인수를 Http 클래스에 전달
=> Http로 전달되는 Http 연결 서비스의 유형에 관계없이/ 네트워크 연결 유형을 알 필요 없이 쉽게 네트워크에 연결할 수 있다
이제 상위 수준 모듈과 하위 수준 모듈 모두 추상화에 의존함
- Http 클래스(고수준 모듈)는 Connection 인터페이스(추상화)에 의존
- Http 서비스 유형(저수준 모듈)은 차례로 Connection 인터페이스(추상화)에 의존
또한 이 DIP는 Liskov 대체 원칙을 위반하지 않도록 강제한다. 연결 유형 Node-XML-MockHttpService는 상위 유형 연결을 대체할 수 있음.
참고:
https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688
모든 개발자가 알아야만 하는 SOLID 원칙 - 1편 | Doublem.org
[객체지향 SW 설계의 원칙] ② 사례연구, 단일 책임 원칙 - ZDNet korea
'CS' 카테고리의 다른 글
API vs Library vs Framework (0) | 2023.11.27 |
---|---|
토큰 이코노미 시대? NFT가 뭘까? (0) | 2023.09.09 |
빅데이터란? (1) | 2023.09.07 |
보일러플레이트 코드란?(Boilerplate code) (0) | 2023.06.21 |
REST API(+ springboot 예제) (0) | 2023.01.23 |