4 분 소요

이번 포스트는 소프트웨어 Design에 있어 5가지 주요 원칙인 SOLID Principles에 대해 다룬다.

SOLID Design Principles

Hierarchy of Pattern Knowledge

단계 개념
OO Basics 추상화, 캡슐화, 다형성, 상속
OO Principles 변화 캡슐화, 합성 우선, 인터페이스 기반 설계
Design Pattern 전략 패턴 (Strategy Pattern)
Design Smells 나쁜 설계 징후들

Design Smells

  • 나쁜 설계의 징후들
  • 유지보수를 어렵게 하고, 변화에 취약하게 한다.

  • 종류들
이름 증상
Rigidity (경직성) 수정 시 연쇄적 변경 발생
Fragility (취약성) 수정이 다른 부위에서 의도치 않은 오류 발생
Immobility (부동성) 재사용이 어려움 (결합도가 높음)
Viscosity (점착성) 정석적인 코드 작성보다 “빠른 해킹”이 더 쉬움
Needless Complexity (불필요한 복잡성) 현재 필요하지 않은 복잡한 구조 존재
Needless Repetition (불필요한 반복) copy-paste 된 반복적 코드
Opacity (불투명성) 코드의 목적/의도가 이해하기 어려움
  • Design Smells잘못 관리된 의존성 (Dependency) 에서 주로 발생한다.
  • SOLID 원칙은 이러한 스멜을 줄이고 유지보수성이 높은 코드를 만들기 위한 가이드이다.

Dependency Management

  • 의존성 관리 실패 -> 스파게티 코드를 유발한다.
  • 어떻게 의존성을 구성해야할까? 기준이 무엇일까?
    • SOLID Principles

Object-Oriented Design Principles

대표 설계 원칙들

  1. SOLID 원칙 : 핵심원칙
  2. GRASP Pattern : 책임 할당 중심 설계 원칙
  3. OO 원칙 : 변화 캡슐화 / 합성 우선 / 인터페이스 기반 설계

SOLID Principles (5대 원칙)

  • 의존성 관리에 대한 원칙적 접근을 제공한다.
  • 복잡한 디자인 패턴의 기반이 되는 튼튼한 설계 원칙이다.
  • 유연성, 견고성, 재사용성이 높은 코드를 작성이 가능하다.
원칙 설명
SRP 클래스는 하나의 책임만 가져야 함
OCP 확장에는 열려 있고, 수정에는 닫혀 있어야 함
LSP 서브타입은 부모 타입을 대체할 수 있어야 함
ISP 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스 분리
DIP 고수준 모듈이 저수준 모듈에 의존하면 안 됨 → 둘 다 추상화에 의존해야 함

1. SRP

SRP = Single Responsibility Principle

  • “클래스는 하나의, 오직 하나의 변경 이유만을 가져야 한다.”

  • 책임이 많을수록 변경 가능성이 높아진다.
    • 이는 곧 버그 발생 가능성을 높인다.
  • 하나의 책임만을 가지게 될 때 생기는 장점
    • 변경 범위가 최소화된다.
    • 변경 시 다른 기능에 미치는 영향이 최소화된다.
    • 응집도 (Cohesion)가 높은 클래스가 되어 유지보수성이 높아진다.

Responsibility란

  • 클래스의 책임 / 계약 / 의무
  • 이 클래스가 왜 바뀌어야 하는지에 대한 변경 이유이다.

SRP 위반 사례 1) Student 클래스

  • Student 클래스에 정렬 방법을 구현
class Student implements Comparable {
    int compareTo(Object o) { ... }
}
  • 문제점
  1. Student는 비즈니스 Entity인데 정렬 방법까지 책임을 가진다.
    • 이는 SRP를 위반한다.
  2. 정렬 기준을 변경할 때 Student 클래스 및 모든 관련 클라이언트 재컴파일이 필요하다.
  • 해결방법

정렬 책임을 분리한다.

-> 별도의 Comparator를 구현하면 된다. (이는 정렬 기준이 변경해도 Student 클래스에 영향을 주지 않는다.)

SRP 위반 사례 2) Rectangle 클래스

  • Rectangle 클래스가 두 개의 서로 다른 책임을 가진다.
  1. Computational Geometry Application : area() 메서드 사용
  2. Graphical Application (GA) : draw( ) 메서드를 사용한다.

문제점

  • CGA, GA가 둘 다 Rectangle에 의존하기에 한 쪽 변경 시 다른 쪽도 재 컴파일이 필요하다.
  • 서로 관련 없는 두 책임이 한 클래스에 묶여 있다.

개선 방법

  • Rectangle 클래스는 primitive attributes만 유지한다.
  • GeometricRectangle, GraphicRectangle으로 분리 → 서로 독립적 변경 가능.
    • 각 책임이 독립적으로 변경 가능하여 유지보수성이 높아진다.

Responsibility 식별이 어려운 경우는 어떻게 할 것인가?

“확장을 하다보니 늦게 깨달을 수도 있다.”

  • 단일 인터페이스에 두 책임을 혼재 -> SRP 위반
  • 애플리케이션 변화 방향에 따라서 책임을 구분하면 된다.
  • 불필요한 복잡성 (Needless Complexity)은 피해야 한다.
    • 증상이 없으면 성급하게 SRP를 적용할 필요는 없다.

2. OCP

OCP = Open-Closed Principle

  • “소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.”
  • 기존 코드를 수정하지 않고 새로운 기능을 추가가 가능해야 한다.
  • 즉, 변경을 최소화하고, 확장 가능성을 확보해야 한다.

Open for Extension : 모듈의 기능을 확장할 수 있다.

Closed for Modification : 확장을 위한 변경이 기존 코드에 큰 영향을 주지는 않아야 한다.

Violation indicator : 변경 시에 rigidity (경직성) 발생 위험이 있다.

위반 징후

  • 한 가지 변경이 여러 모듈에 연쇄적 변경을 유발하게 된다면
    • 이는 Design Smell에 속한다.

Abstraction

  • 확장은 가능하고, 기존 코드에 수정하지 않기 위한 핵심원리이다.

  • 추상화 (Abstraction)을 사용하면 OCP 를 달성 할 수 있다.

구체 클래스가 아닌 인터페이스에 의존

  • 어떤 변화에 대해서 OCP를 적용할지 선택적으로 판단해야 한다.
  • 모든 경우에 과도한 abstraction 도입은 지양해야 한다.
    • 비용 고려도 필요하다.

3. LSP

LSP = Liskov Substitution Principle

  • “Subtype은 언제나 Base type으로 교체가 가능해야 한다.”

  • 부모 타입으로 정의된 변수에 자식 타입 객체를 할당해도 프로그램은 정상적으로 동작해야 한다.
  • IS-A 관계가 의미상으로도 성립해야 한다.

Subtyping vs Implementation Inheritance

구분 설명
Subtyping IS-A 관계 성립 → LSP 만족
Implementation Inheritance 코드 재사용 목적 → IS-A 관계 성립 여부는 보장 안 됨

대부분의 OOP 언어 (Java, C++, C#)은 extends가 둘 다 구현 해준다.

LSP 위반 사례

  1. Queue extends List

Queue는 FIFO 구조로 작동한다.

따라서 List의 임의 접근 연산과 의미적으로 다르다. => LSP 위반

  1. Java.sql.Time extends java.util.Date

Time은 Date로 교체가 불가능하다.

  • 만약 f(PType x) 함수가 CType이 들어오면 오작동한다. => LSP 위반
  • 개발자가 if (x instanceof CType) 같은 코드를 추가하면 => OSP 위반도 발생할 수 있다.

4. DIP

DIP = Dependency Inversion Principle

  • “고수준 모듈(high-level module)은 저수준 모듈(low-level module)에 의하면 안된다.”
  • 둘 다 abstraction에 의존해야 한다.

Why inversion?

= 기존 structured design은 상위 계층 -> 하위 계층으로 의존성이 존재한다.

  • 의존성 관리에 용이하다.
  • 고수준 정책 (Policy)의 재사용이 용이하다.
  • 변경에 대한 유연성이 증가한다.

5. ISP

ISP = Interface Segregation Principle

  • “클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안된다”
  • 많은 시스템에서 Fat Interface가 만들어진다.
  • 서로 다른 목적의 client 들이 동일 Interface를 공유하게 되면
    • interface가 변경할 때 불필요한 영향을 받는다.
    • client가 원하지 않는 불필요한 메서드까지 구현을 알아야한다.
      • 이는 설계 응집도를 낮춘다.

해결 방법

  • Interface를 Client 관점에서 잘게 나누어 설계한다.
  • Client는 딱 필요한 기능만 제공받는 Interface에만 의존하게 된다.