객체지향 설계 원칙 - SOLID

2026. 3. 8. 21:30·기술 학습

개요

객체지향의 핵심은 구현과 역할을 분리하는 것이다.

객체에게 역할을 부여하고, 그 역할에 따라 책임을 할당하며, 객체 간 협력을 통해 시스템이 동작한다.

 

그렇다면 객체지향에서 좋은 설계와 아키텍처란 무엇일까?

바로 SOLID 원칙이다.

SOLID는 좋은 설계를 얻기 위해 지켜야 할 규범이며, 이 원칙들은 소프트웨어의 유지보수성과 확장성을 높이기 위한 것이다.

유지보수성

무엇이 유지보수성을 높이는 것일까? 가독성?

가독성은 사람마다 다르며, 좋지 못한 코드가 오히려 가독성이 더 좋을 수도 있다. 가독성은 유지보수성의 명확한 기준이 되지 못한다.

 

유지보수성을 높이는 명확한 기준은 다음 세 가지이다.

  1. 영향 범위 — 코드 변경으로 인한 영향 범위가 어떻게 되는가?
  2. 의존성 — 소프트웨어의 의존성 관리가 잘 이뤄지고 있는가?
  3. 확장성 — 쉽게 확장 가능한가?

SOLID 설계 원칙을 따르면 변경에 의한 영향 범위를 축소할 수 있고, 의존성을 제대로 관리할 수 있고, 쉽게 확장할 수 있다.


S - SRP (Single Responsibility Principle) 단일 책임 원칙

원칙의 목적

변경으로 인한 영향 범위를 최소화하자는 것이다.

그래서 클래스를 변경해야 할 이유는 단 한 가지여야 한다.

책임이란 무엇인가?

책임의 범위는 어디까지일까?

예를 들어 Developer 클래스에 프론트엔드 개발, 백엔드 개발, 프론트 배포, 백엔드 배포가 모두 들어있다면 SRP 위반일까?

  • FE, BE로 나눠야 하나?
  • FE, BE, SRE로 나눠야 하나?
  • 외부(비개발자)가 보기엔 하나의 Developer가 아닌가?

책임은 문맥을 포함한다. 따라서 바라보는 이의 입장이나 상황마다 다르게 해석될 수 있다.

액터(Actor) 기준

이를 위한 기준이 있다.

  • 하나의 모듈은 하나의, 단 하나의 액터에 대해서만 책임져야 한다.

액터란 메시지를 전달하는 주체이다. 액터가 클래스(객체)에 메시지를 보내고, 클래스는 그에 응답한다.

  • 액터가 한 명인가? → SRP 원칙을 지킴
  • 액터가 여럿인가? → SRP 원칙을 위배함

따라서 객체지향에서 논하는 객체가 갖는 「역할, 책임, 협력」에서 책임은 곧 액터에 대한 책임이고 협력이다.

이제 클래스를 변경할 이유는 단 하나로, 액터의 요구사항이 변경되었을 때이다.

SRP의 목표

  • 클래스가 변경됐을 때 영향받는 액터는 단 하나여야 한다.
  • 클래스를 변경할 이유는 유일한 액터의 요구사항이 변경될 때 뿐이어야 한다.

O - OCP (Open-Closed Principle) 개방 폐쇄 원칙

원칙의 목적

기존 코드를 수정하지 않으면서도 확장이 가능한 시스템을 만드는 것이다.

코드를 확장하고자 할 때 취할 수 있는 최고의 전략은, 기존 코드를 아예 건드리지 않는 것이다.

이를 위해 역할과 구현을 분리하고, 구현이 아닌 역할에 의존해야 한다.

 

AS-IS

[Order] → [Food]
   ↘
    [Brand Product]

Order 내부
- if 등 조건절...

Order가 Food라는 구현체에 의존하고 있는 상태에서 Brand Product가 추가되면, Order는 Brand Product에도 의존하게 된다.
Order 내부에서 타입에 따른 if 조건절 분기가 필요해지며, 새로운 상품 유형이 추가될 때마다 기존 코드를 수정해야 한다.

즉, 수정에 열려있는 확장이 되는것이다.

 

TO-BE

[Order] → [Calculable]  ← 역할 (인터페이스)
             ↑    ↑
         [Food]  [Brand Product]  ← 각각의 구현체

Order는 Calculable이라는 역할(인터페이스)에 의존한다.

Food와 Brand Product는 각각 Calculable을 구현하는 구현체이다.

새로운 상품 유형을 추가할 때 기존 코드를 수정할 필요 없이 새로운 구현체만 추가하면 된다.

따라서, 수정에는 닫힌채 확장할 수 있다.

OCP의 목표

  • 확장하기 쉬우면서도 변경으로 인한 영향 범위를 최소화하는 것이다.

L - LSP (Liskov Substitution Principle) 리스코프 치환 원칙

원칙의 목적

  • 기본 클래스(상위)의 파생 계약을 파생 클래스(하위)가 제대로 치환할 수 있어야 한다.

위반 사례

interface Member {
    double getDiscountRate(); // 정상적인 할인율 값 반환
}

class BlacklistedMember implements Member {
    @Override
    public double getDiscountRate() {
        throw new RuntimeException("블랙리스트 회원은 할인 불가");
    }
}

Q. 다형성을 이용하는 곳에서 Member 인터페이스 하위 객체를 반복문으로 순회하며 getDiscountRate()를 호출하면 어떤 문제가 발생할까?

A. 상속받은 하위 객체가 상위 객체를 대신하여 역할을 수행할 수 없다. 상속받은 하위 객체를 신뢰할 수 없게 되며, 하위 객체의 변경이 외부로 전파되게 된다. 왜나면 결국 호출하는 쪽에서 아래와 같은 instanceof 체크가 필요해지기 때문이다..

if (member instanceof BlacklistedMember) {
    // 예외 처리...
}

 

즉, LSP는 안전한 다형성 사용과 OCP를 지키기 위한 필수 전제 조건이다.

올바른 설계

애초에 Member와 BlacklistedMember 둘 다가 getDiscountRate()를 구현해야 하는 것이 잘못된 설계다.
ISP 원칙에 맞게 분리해야 한다.

// 회원의 공통 속성
interface Member {
    // 아이디, 패스워드, 가입일 등 ...
}

// 할인 적용 역할
interface Benefitable {
    double getDiscountRate();
}

// 일반 회원 - 할인 가능
class RegularMember implements Member, Benefitable {
    @Override
    public double getDiscountRate() { return 0.1; }
}

// 블랙리스트 회원 - 할인 불가
class BlacklistedMember implements Member {
    // getDiscountRate()를 구현할 필요가 없다
}

Meber와 Benefitable로 역할을 분리함으로써, 할인이 가능한 회원만 Benefitable을 구현하고 블랙리스트 회원은 Member만 구현하면 된다.


I - ISP (Interface Segregation Principle) 인터페이스 분리 원칙

원칙의 목적

어떤 클래스가 자신에게 필요하지 않은 인터페이스의 메소드를 구현하거나 의존하지 않음으로써, 인터페이스의 크기를 작게 유지하고 클래스가 필요한 기능에만 집중하는 것이다.

통합된 인터페이스는 구현체에 불필요한 구현을 강요할 수 있다.

범용성을 갖춘 하나의 인터페이스보단 다수의 특화된 인터페이스를 만드는 게 낫다.

역할을 다 쪼개면 응집도가 낮아지는 것 아닌가?

단순히 "유사한 코드라서 한 곳에 모아두겠다"는 것은 가장 낮은 응집도이다.

인터페이스 분리 원칙에서 말하는 인터페이스는 곧 역할이다. 따라서 역할과 책임을 분리하고, 역할을 세세하게 나누라는 것이다.

이를 통해 기능적 응집도를 높이는 것이다.

기능적 응집도란?

모듈 내 컴포넌트들이 같은 기능을 수행하도록 설계된 경우를 말한다.

  • 모듈이 [어떤 목적]을 갖고 있고, 컴포넌트들은 [그 목적]을 달성하기 위해 협력하며, 오직 [그 목적]에 관련된 작업만 수행하는 것이다.
  • 목적 예시: 주문 처리, 주문 취소, 주문 예약 등 — 모두 하나의 목적을 위해 존재하는 컴포넌트가 된다.

D - DIP (Dependency Inversion Principle) 의존 역전 원칙

원칙의 목적

"구체화가 아닌 추상화에 의존해야 한다."

  1. 상위 모듈은 하위 모듈에 의존해서는 안 된다. 상위, 하위 모듈 둘 다 추상화에 의존해야 한다.
  2. 추상화는 세부사항에 의존해서는 안 된다. 세부사항이 추상화에 의존해야 한다.

쉽게 표현하면 다음과 같다.

  • 고수준 모듈은 추상화에 의존해야 한다.
  • 고수준 모듈이 저수준 모듈에 의존해서는 안 된다.
  • 저수준 모듈은 추상화를 구현해야 한다.

여기서 말하는 의존이란?

의존은 다른 객체나 함수를 사용하는 상태이다. 즉, 사용만 해도 의존하는 것이다.

중요한 것은 "어떻게 하면 의존성을 낮출 수 있는가"이다.

Q. 사용하면 의존인데, 강약의 차이가 있을 수 있을까?

A. 있다. 강한 의존과 약한 의존이 있다.

의존은 다시 말해 결합(Coupling)이고, 결합에는 강도가 있으며, 결합에는 다양한 방법이 존재한다.

의존성을 약화시키는 기법 — 의존성 주입(DI)

의존성 주입은 필요한 의존성을 외부에서 넣어주는 것이다. 여기엔 아래 방법들이 있다.

  • 생성자 주입 — 생성자 파라미터로 의존성을 전달
  • 수정자 주입 — setter 메소드로 의존성을 전달
  • 필드 주입 — Spring의 @Autowired로 의존성을 전달

상세한 구현 객체(new 사용)에 의존하는 것을 지양하고, 구현 객체가 인스턴스화되는 시점을 최대한 뒤로 미루는 것이 핵심이다.

참고로 Spring 프레임워크가 바로 이를 해주는 도구다.
개발자가 직접 객체를 생성하지 않고, 프레임워크가 애플리케이션 실행 시점에 의존 관계를 해석하여 주입한다.

의존성 "역전"

의존성의 정의는 했다. 그렇다면 역전은 무엇일까?

 

AS-IS

[Restaurant] ——의존——→ [HamburgerChef]
 - 상위 모듈               - 구현체
 - 고수준 모듈              - 하위 모듈
                         - 저수준 모듈
  • 레스토랑이 햄버거 셰프에 직접 의존한다.

TO-BE

[Restaurant] ——의존——→ [Chef]  ← 추상화 (역할)
 - 상위 모듈               ↑
 - 고수준 모듈        [HamburgerChef]
                       - 세부사항
                       - 저수준 모듈
                       - 하위 모듈
  • 레스토랑은 셰프라는 역할에 의존하며, 햄버거 셰프도 셰프라는 역할에 의존한다.

위에서 무엇이 역전되었는가?

HamburgerChef(햄버거 요리사)가 의존을 당하고 있었는데, 의존을 하도록 변경되었다. 의존 화살표가 들어오고있었는데 나가도록 역전된 것이다.

의존성 역전의 효과

의존성 역전을 적용함으로써 추상화에 의존하는 형태로 바뀐다. 즉, 의존성 역전 원칙은 곧, "세부사항에 의존하지 않고 정책에 의존하도록 코드를 작성하라"이다.

경계의 형성

의존성 역전을 적용하면 경계가 만들어진다. Chef 인터페이스를 기준으로 경계가 형성되는 것이다.

┌─────────────────────────┐
│  레스토랑 모듈 (상위 모듈)    │
│  [식당] → [Chef]         │
└─────────────────────────┘
              ↑
┌─────────────────────────┐
│  햄버거 모듈 (하위 모듈)     │
│      [햄버거 요리사]       │
└─────────────────────────┘
  • 이 경계를 기준으로 모듈을 나눌 수 있고, 모듈 간 상하 관계를 파악할 수 있다.

Chef가 하위 모듈 경계로 들어가면 안 되는 이유

만약 Chef 인터페이스가 햄버거 모듈(하위 모듈) 안에 있다면, 상위 모듈인 레스토랑이 하위 모듈의 Chef에 의존하게 된다.

이때 햄버거 모듈의 Chef를 한식 요리 모듈의 Chef로 교체해야 한다면, 상위 모듈이 하위 모듈의 변경에 영향을 받게 된다.

따라서 추상화(인터페이스)는 반드시 상위 모듈 경계 안에 위치해야 한다.


정리

원칙 핵심 목표
SRP 단일 액터, 단일 책임 변경의 영향 범위 최소화
OCP 역할과 구현 분리 수정 없는 확장
LSP 하위 타입 치환 보장 안전한 다형성 사용 및 OCP 원칙을 지킴
ISP 인터페이스 세분화 기능적 응집도 향상
DIP 추상화에 의존 의존성 역전으로 경계 형성

 

보다시피 SOLID 원칙들은 서로 독립적인 것이 아니라, 유기적으로 연결되어 있다.

 

"SRP로 책임을 분리하고, OCP로 확장에 열린 구조를 만들며, LSP로 다형성의 안전성을 보장하고, ISP로 역할을 세분화하며, DIP로 의존성을 역전시켜 모듈 간 경계를 형성한다."

 

결국 이 모든 원칙이 지향하는 것은 하나다. 유지보수성과 확장성이 높은 소프트웨어를 만드는 것.

반응형
저작자표시 비영리 변경금지 (새창열림)

'기술 학습' 카테고리의 다른 글

MSA에서 CORS 문제를 해결하는 4가지 전략  (0) 2026.03.27
TCP/IP 체크섬(Checksum) 내부 동작 원리  (0) 2026.03.23
Java 기반 동기/비동기, 블로킹/논블로킹 정리  (0) 2026.03.23
ANSI Isolation Level vs MySQL Isolation Level: 같은 이름, 다른 보장  (0) 2026.03.15
MSA에서 ACID의 의미  (0) 2026.03.10
'기술 학습' 카테고리의 다른 글
  • TCP/IP 체크섬(Checksum) 내부 동작 원리
  • Java 기반 동기/비동기, 블로킹/논블로킹 정리
  • ANSI Isolation Level vs MySQL Isolation Level: 같은 이름, 다른 보장
  • MSA에서 ACID의 의미
구름뭉치
구름뭉치
구름의 개발일기장
    반응형
  • 구름뭉치
    구름 개발일기장
    구름뭉치
  • 전체
    오늘
    어제
    • ALL (290)
      • 프로젝트 (23)
        • 토스페이먼츠 PG 연동 시리즈 (12)
        • JWT 방식 인증&인가 시리즈 (6)
        • 스우미 웹 애플리케이션 프로젝트 (1)
        • 스프링부트 기본 보일러 플레이트 구축 시리즈 (2)
        • 마이크로서비스 아키텍쳐 시리즈 (1)
      • 스프링 (43)
        • 스프링부트 API 설계 정리 (8)
        • 스프링부트 RestAPI 프로젝트 (18)
        • 스프링부트 WebSocket 적용기 (3)
        • 스프링 JPA 정리 시리즈 (5)
        • 스프링 MVC (5)
        • 스프링 배치 (2)
        • 토비의 스프링 정리 (2)
      • 기술 학습 (6)
        • 아파치 카프카 (9)
        • 클린 코드 (4)
        • 디자인 패턴의 아름다움 (2)
        • 모던 자바 인 액션 (7)
        • JVM 스레드 딥다이브 (7)
      • Web (25)
        • 정리글 (20)
        • GraphQL 정리글 (2)
        • Jenkins 정리글 (3)
      • 취업 (6)
      • CS (77)
        • 네트워크 전공 수업 정리 (11)
        • OSI 7계층 정리 (12)
        • 운영체제 정리 (19)
        • 데이터베이스 정리 (5)
        • MySql 정리 (17)
        • GoF의 Design Pattern 정리 (12)
      • 알고리즘 (70)
        • 백준 (56)
        • 프로그래머스 (12)
        • 알고리즘 정리본 (1)
      • 기초 지식 정리 (2)
      • 일상 (8)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    키보드 손목 받침대
    류블라냐
    마우스 패드
    마우스
    부다페스트
    레이저
    동유럽
    크로아티아
    mx master s3 for mac
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
구름뭉치
객체지향 설계 원칙 - SOLID
상단으로

티스토리툴바