Challenge, and Growth ! Introduce
                                                                                                             

Welcome to my blog 🙌🏻

Total

Today

Yesterday













Knowledge/디자인 패턴

SOLID란? - 객체지향 개발의 원칙

뽀시라운 2024. 4. 8. 23:19
반응형
SMALL

[목차여기]

 

 

 

UML이란? - UML을 사용하는 이유

[목차여기] UML(Unified Modeling Language) UML은 통합 모델링 언어이다. '통합', '모델링', '언어' 이렇게 단어를 하나씩 떼어 보면 그 정의를 짐작할 수 있다. 소프트웨어 개념을 다이어그램으로 그리기

proysm.tistory.com

 

앞서 UML을 이용하는 이유와, 클래스 다이어그램을 그리는 방법을 알아봤다. 이제 다이어그램을 그리거나 개발을 할 때 지켜야 할 설계원칙에 대해서 정리할 것이다. '설계'라는 것은 결국 실제 코드를 작성하기 위함이므로 이해하기 쉽게 단순하고 간결해야 할 필요가 있다. 그래서 객체지향 개발 원칙인 SOLID가 탄생한 것이다.

 

 

SOLID 란?

SOLID에 대해 본격적으로 알아보기 전에 '나쁜 설계'가 무엇인지 알아야 한다. 개발자의 입장에서 생각하면 쉽다. 만약, 한 부분을 수정했을 때 다른 부분들의 수정이 반드시 필요하고 그 수정으로 인하여 또 다른 수정이 필요하다면? 줄줄이 소시지처럼 연쇄반응이 발생한다. 또, 시스템에서 한 부분을 수정했을 때 다른 부분이 작동을 멈춘다던가, 필요 없는 코드가 반복된다던가, '변경-컴파일-테스트' 시간이 매우 긴 이 모든 상황이 나쁜 설계를 가리킨다. 많이 들어봤을 '스파게티 코드'도 나쁜 설계(잘못된 의존 관계) 때문에 발생한 것이다.

 

* 여기서 표현한 '잘못된 의존 관계'는 나쁜 설계의 주원인이라고 할 수 있다. 객체지향을 공부한 사람이라면 의존 관계가 왜 중요한지 알 것이다.

 

그래서 나쁜 설계를 범하지 않기 위해 SOLID를 잘 지켜서 설계해야 한다. SOLID는 5가지 원칙의 앞글자를 땄다. 나도 처음에는 5가지 원리의 단어가 헷갈렸었는데 공부하면서 자연스럽게 기억하게 된 것 같다. 처음에는 어색하겠지만 다른 사람들과 소통할 때 "LSP가 지켜지지 않은 설계 같아요!"라고 말하는 것이 "이 B클래스는 A클래스를 상속받았지만 B가 A를 대체할 수 없는 것 같아서 나쁜 설계 같아요~"라고 말하는 것보다 훨씬 나으니,, 기억하자 :) 물론 좋은 설계를 위한 것이 첫 번째 이유다.

원칙 설명
SRP Single Responsibility Principle (단일 책임의 원리)
OCP Open Close Principle (개방 폐쇄의 원리)
LSP Liskov Substitution Principle (리스코프 교체의 원리)
ISP Interface Segregation Principle (인터페이스 격리 원리)
DIP Dependecy Inversion Principle (의존관계 역전의 원리)

 

 

SRP (Single Responsibility Principle, 단일 책임의 원리)

하나의 클래스는 하나의 책임만 가지도록

 

하나의 클래스에 너무 많은 데이터들과 함수, 기능, 책임들이 포함되어 있다면 그 클래스를 변경해야 하는 이유가 많아진다.

 

그래서 어떤 클래스를 변경해야 하는 이유가 오직 하나이도록 한다.

public class Member {
    public void writeToDisk();
    public void readFromDisk();
    public int calculateEntryFee();
    public void displayOnMemberReport();
    public void displayOnEntryFee();
}

 

ClubMember 클래스는 아주 많은 정보를 담고 있다.

 

"멤버를 디스크에 저장하거나 읽어오는 방법,  참가 횟수 계산, 참가비 계산, 다양한 보고서 형식 출력"

 

만약 회원비를 계산하는 방법을 '횟수제'에서 '기간제'로 바꾼다면 ClubMember 클래스를 변경해야 한다. 그리고 Disk에 저장하는 방식이 바뀌어도 ClumMember 클래스를 변경해야 한다. 그래서 이 설계는 결합도가 높다. 따라서 각 개념을 다른 클래스로 분리하여 한 개념이 변경될 때 그 개념이 담긴 클래스만 수정할 수 있도록 하는 것이 좋다.

 

클래스 다이어그램을 그려보면 다음과 같다. 

[그림1] 좌: SRP를 지키지 못함, 우: SRP를 지켰음

 

왼쪽은 SRP를 지키지 못한 다이어그램이고, 우측은 지킨 다이어그램이다. 여러 가지 책임이 가득했던 Member 클래스에서 책임이 분산되어 SRP가 지켜진 것을 볼 수 있다.

 

 

OCP (Open-Close Principle, 개방 폐쇄의 원리)

확장에 대해서는 개방되고, 변경에 대해서는 폐쇄되어야 한다.

 

변경되는 부분(구현)을 한 곳에 모아두고 변경되지 않는 부분(추상)을 한 곳에 모아두면 된다.

 

그러면 변경은 계속 한 곳에서만 일어나며 확장은 쉬워진다. 이 원리는 퍼싸드 패턴브리지 패턴에 잘 적용되어 있다.

 

[그림2] OCP 예시 - 퍼싸드

 

DataBase에 모든 API가 모여있다고 생각한다면, Member와 Schedule은 자신들과 관련된 API만 골라 쓰고 싶을 것이다. MemberDB와 ScheduleDB가 그 역할을 하는 것이다. 이 그림이 퍼싸드 패턴을 설명하고 있다. Member가 직접 쿼리(API)를 날리지 않도록 DataBase에 접근하는 꼭 필요한 함수를 모아놓은 인터페이스나 추상클래스를 퍼싸드라고 한다.

 

[그림3] OCP 예시 - 브리지

 

이 그림은 브리지 패턴을 설명하고 있다. MemberDataBaseImplementation(줄여서 Impl)은 DataBase에서 Member와 관련된 API를 골라서 구현하고 있다. 그리고 MemberDB를 상속받아서 UnitTestDataBase는 데이터베이스를 단위테스트 하고, Impl은 메서드를 구현하고 있다. Member는 자신이 MemberDB를 이용하여 메소드를 사용만 하면 된다. 이로써 추상과 구현 부분을 구분 지었다. 추상(변하지 않는 부분)은 MemberDB이고, 구현(변하는 부분)은 UnitTestDataBase와 Impl이다. 

 

 

LSP (Liskov Substitution Principle, 리스코프 교체의 원리)

서브타입은 항상 베이스타입으로 교체될 수 있어야 한다.

 

서브타입이 항상 베이스타입으로 교체된다는 것은 슈퍼클래스가 서브클래스에 대해 알 필요 없이 사용하게 해야 된다는 것이다.

 

아래는 잘못된 예시다.

[그림4] 잘못된 예시

 

calcPay()는 급여를 계산하는 메서드이다. SalariedEmployee와 HourlyEmployee는 Employee 클래스를 상속받아서 calcPay()를 사용할 것이다. 그리고 서브클래스를 베이스클래스로 교체해도 아무 문제없다. 그런데 VolunteerEmployee 클래스를 추가하려고 하는데 자원봉사자는 급여를 받지 않는다. 그래서 Employee의 calcPay()가 필요하지 않다. 이 경우에는 서브클래스가 베이스클래스로 교체될 수 없으므로 LSP를 만족하지 않는다. 그래서 잘못된 예시인 것이다.

 

자원봉사자는 직원이 아니기 때문에 애초에 Employee 클래스에서 파생해서는 안 됐다.

 

 

ISP (Interface Segregation Principle, 인터페이스 격리의 원리)

클라이언트는 자신이 사용하지도 않는 메서드에 의존하면 안 된다.

 

앞서 "자신들과 관련된 API만 골라 쓰고 싶을 것이다."라는 문장을 사용하였는데, 자신이 사용하지도 않는 메서드가 담긴 클래스를 상속받는 것은 불필요하다.

 

[그림5] 인터페이스 격리가 이루어지지 않은 그림

 

왼쪽의 EnrollmentReportGenerator 클래스는 getName()과 getDate()만 필요하고 오른쪽의 AccountsReceivable 클래스는 prepareInvoice()와 postPayment()만 필요하지만 현재 4개의 메서드가 StudentEnrollment에 함께 존재한다. 그래서 불필요한 메서드에 의존관계가 존재한다. postPayment() 메서드에 새 인자를 추가한다면 StudentEnrollment의 선언을 바꾸는 것이고 결국 EnrollmentReportGenerator 클래스를 다시 컴파일하고 배포해야 할 수도 있다. 

 

[그림6] 인터페이스 격리가 이루어진 그림

 

그래서 각 의도에 맞게 인터페이스를 분리시킨다. 클라이언트는 자신이 사용하고자 하는 메서드만 있는 인터페이스를 제공받게 되었다.

 

 

DIP (Dependency Inversion Principle, 의존 관계 역전의 원리)

추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 된다.
고차원 모델은 저 차원 모델에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.

 

위의 두 가지 의미는 결국 자주 변경되는 Concrete 클래스에 의존하지 말라는 것이다. 자신이 상속받은 클래스가 자꾸 변경되면 자신도 영향을 받기 때문에 더 끈끈해지고 나쁜 설계가 되는 것이다.

 

 

회고

SOLID를 공부하며 나의 개발 과정을 떠올리게 되었다. 나도 모르게 원리를 적용하여 설계를 한 경우도 있었고, 아주 잘못하게 설계한 경우도 있었다. 구현과 설계를 나눈 점은 잘한 것 같지만 클라이언트가 필요로 하는 것을 인터페이스로 정의하는 과정에서 불필요한 메서드도 속해있었던 것 같다. UML을 알기 전과 후의 개발 스탠스가 매우 달라질 것이라고 생각한다. 이제 이 UML을 가지고 디자인 패턴을 공부하려고 한다.

반응형
LIST
loading