공부/소프트웨어공학

[소프트웨어공학] SOLID 원칙에 대하여

돌멩이수프 2024. 5. 13. 23:30
728x90

 

SOLID 원칙이란 로버트 마틴이 말한 다섯 가지 설계 원칙이다. 

지난 글에 이어서 썼던 cohesion(응집도), coupling(결합도)도 기본적인 설계원칙이다.

보통 설계원칙을 말하라고 하면 cohesion, coupling을 말하는 경우일 수도, SOLID를 말하는 경우일 수도 있다.

cohesion과 coupling을 바탕으로 SOLID가 탄생했다는 것과 SOLID는 5가지라는 것을 알아두자. 

 

 

 

1️⃣ SRP(Single Responsibility Priciple) : 단일 책임 원칙

Cohesion과 관련되어있다. Cohesion이 높을 수록 좋은 시스템이라 할 수 있다. 기존 Cohesion에서는 하나의 모듈이 하나의 기능을 할 때 functional cohesion이라 하였다. 그 범위를 모듈에서 클래스로 넓힌 것이다.

 

한 클래스는 하나의 책임만 가지도록 설계하고

클래스를 변경해야 하는 이유가 하나여야 한다.

한 클래스는 한 actor만 책임져야 한다.

 

예를 들어, 장학금을 관리하는 시스템이 있을 때 장학금 관리자와 장학금 수여자는 같은 책임을 갖는 시스템의 같은 기능을 사용하게 된다. ➡️ 이런 상황은 SRP를 만족한다고 할 수 없다. 한 클래스에 두 actor가 들어오기 때문이다.

위 상황에서 SRP가 만족되려면 학점 계산()이라는 클래스와 장학금 계산() 이라는 클래스를 나누어 사용해야 한다.

 

📛 메소드가 하나라고 SRP를 만족하는 건 아니다. 📛

 

 

2️⃣ OCP(Open Closed Principle) : 개방 폐쇄 원칙

기존의 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있어야 한다는 설계 원칙이다.

 

OCP를 만족하지 않는 클래스를 만족하도록 하려면,

1. 클래스에서 자주 변하는 것을 식별하고

2. 변하는 것을 클래스로 분리한다.

3. 자주 변하는 것을 포용하는 개념을 추상 클래스나 인터페이스로 추상화한다.

4. 2번에서 만든 클래스를 3번에서 만든 추상화의 자식 클래스로 모델링한다.

 

쉽게 예를 들어 사람은 고양이, 개, 앵무새 등의 동물을 사랑한다는 것을 표현하는 시스템이 있다고 하자.

사람이 사랑하는 동물은 바뀌지만 사람이 동물을 사랑한다는 사실은 변하지 않는다. 이때 사람이 동물을 사랑한다 ➡️ 를 interface(또는 abstract)로 만들고 / 개 extends 동물 또는 고양이 extends 동물 등등처럼 interface를 확장하여 클래스를 완성한다.

✅ 새로운 동물(예를 들어 코끼리)이 추가될 때 코끼리 extends 동물 처럼 간단하게 새로운 내용을 추가할 수 있다.

이때 잘 변화하는 자식 클래스는 private로 둔다. 자주 변하는 요소가 외부에서 영향 받지 않게 하기 위해서다.

정보 은닉의 개념을 담고 있다.

 

📛 클래스의 한 관점에서 OCP를 만족한다고 해서 모든 관점에서 OCP가 만족되는 것은 아니다. 📛

 

 

3️⃣ LSP(Liskov Substitution Principle) : 리스코프의 대입 원칙

일반화 관계를 적절하게 사용했는지 점검하는 원칙이다. 슈퍼 클래스-서브 클래스는 행위적 일관성이 있어야 한다.

슈퍼 클래스 대신 서브 클래스로 대체해도 프로그램 의미가 변하지 않아야 한다.

직사각형 너비를 구하는 클래스를 상속 받은 정사각형 너비를 구하는 클래스가 있을 때, 정사각형 클래스를 사용하여 직사각형 너비를 구해도 정상적인 결과를 얻을 수 있어야 한다.

 

 

4️⃣ ISP(Interface Segregation Principle) : 인터페이스 분리 원칙

인터페이스를 클라이언트에 특화되도록 분리해야 한다. Small Interface 

클라이언트의 관점에서 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다.

 

 

5️⃣ DIP(Dependency Inversion Principle) : 의존성 역전 원칙

상위 모듈은 하위 모듈에 의존해서는 안된다. 두 모듈 모두 다른 추상화된 모듈에 의존해야 한다. 추상화된 것은 구체적인 것에 의존해서는 안되며 구체적인 것이 추상화된 것에 의존해야 한다.

public class Plus {
    public void sum(ConsoleInput in, ConsoleOutput out) {
        List<Integer> input = in.getInputs();
        int r = input.stream().mapToInt(i -> i).sum();
        out.print(input, r);
    }
}

 

추상적인 상위 모듈인 Plus가 구체적인 하위 모듈 ConsoleInput과 ConsoleOutput에 의존하는 잘못된 상황을 보여주는 클래스와 클래스 다이어그램이다. 기존 코드 수정 없이 새로운 기능을 추가할 수도 없는 OCP 위반 상황이기도 하다.

OCP와 마찬가지로 잘 변하는 클래스를 low level, 잘 변하지 않는 클래스를 high level로 두어야 한다. 기능을 포함하는 interface를 만들고 자세한 기능은 extends하여 설계한다.

 

 

예를 들어 위 클래스 다이어그램처럼 설계하는 것이 옳다. 이때 D1은 Plus처럼 핵심 로직이고 IFoo1과 IFoo2는 핵심로직에 도움을 주는 low level module이다.

관련있는 것을 아예 패키지로 묶는 것이 best다.

728x90