SOLID: 원칙, OOP 5가지 규칙 및 개발 응용

저자: IT Sectr 게시일: 2026-05-11 읽는 시간: 10 분

SOLID — 2000년대 초반에 Robert C. Martin(Uncle Bob)이 정의한 객체 지향 프로그래밍의 5가지 원칙입니다. DigitalOcean, 2024에 따르면, SOLID는 Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion의 약자입니다. 이 원칙들은 Clean Architecture의 기반을 이루며 Android 개발(MVP, MVVM, Clean Architecture)과 iOS(VIPER, TCA)에 적용됩니다.

핵심 요점

  • SOLID — 유연하고 유지보수가 용이한 코드를 만들기 위해 Robert C. Martin이 정의한 5가지 OOP 원칙(SRP, OCP, LSP, ISP, DIP)의 약자입니다.
  • SRP(Single Responsibility) — 각 클래스는 변경 이유가 하나이며, 모듈당 하나의 잉무를 가집니다.
  • OCP(Open-Closed) — 클래스는 확장에 대해 열려 있으며 수정에 대해 닫혀 있으며, 상속과 다형성을 통해 구현됩니다.
  • LSP(Liskov Substitution) — 하위 클래스의 객체는 프로그램의 옵은성을 바꾸지 않고 기본 클래스의 객체를 대체할 수 있어야 합니다.
  • ISP(Interface Segregation) — 클라이언트는 사용하지 않는 인터페이스에 의존해서는 안 되며, 인터페이스는 폁소하고 구체적이어야 합니다.
  • DIP(Dependency Inversion) — 상위 모듈은 하위 모듈에 의존하지 않고, 둘 다 추상화에 의존합니다.

SOLID란? 5가지 원칙 개요

SOLID — 객체 지향 설계의 5가지 원칙을 나타내는 기억식 약자입니다. 용어는 Robert C. Martin이 게재 “Design Principles and Design Patterns”(2000)에서 소개하였고, 후에 책 “Agile Software Development: Principles, Patterns, and Practices”(2002)에서 대중화되었습니다. SOLID는 프레임워크나 라이브러리가 아니라 코드의 결합도를 낮추고, 테스트가 용이하며, 변경이 쉽고, 견고하게 만드는 실천의 집합체입니다.

Clean Coder Blog, 2014에 따르면, 각 SOLID 원칙은 특정 설계 문제를 해결합니다. SRP는 God 클래스와 싸우고, OCP는 연쇐적인 변경을 방지하며, LSP는 올바르지 않은 상속으로부터 보호하고, ISP는 큰 인터페이스를 피하며, DIP는 강한 결합을 줄입니다. 이들이 함께 Clean Architecture의 기반을 이루며, Android 프로젝트에서 MVP, MVVM, MVI와 함께 사용됩니다.

SRP: 단일 잉무 원칙

Single Responsibility Principle(SRP) — 단일 잉무 원칙입니다. 정의: “클래스는 변경할 이유가 하나만 있어야 한다.” 이는 각 모듈 또는 클래스가 정확히 하나의 기능 또는 하나의 도메인 엔티티에 대해 잉무를 진다는 것입니다. 클래스가 사용자와 이메일 발송을 모두 관리하면 변경 이유가 둘이므로 SRP를 위반합니다.

Robert C. Martin, 2002에 따르면, SRP는 가장 중요하며 동시에 가장 많이 위반되는 원칙입니다. 모바일 개발에서 SRP는 Activity/Fragment에서 UI 로직, 네비게이션, 네트워크, 비즈니스 로직을 결합하면서 자주 위반됩니다. 해결 방법은 각 계층을 별도의 클래스로 추출하는 것입니다. ViewModel은 UI 로직, Repository는 데이터, NavController는 네비게이션을 담당합니다.

SRP 예시: UserManager 분할

프로필을 로드하고, 설정을 저장하고, 이메일을 보내는 UserManager 클래스를 고려해 보세요. 이들은 세 가지 다른 잉무이며, 각각이 별도의 클래스로 추출되어야 합니다. UserProfileRepository(로드), UserSettingsStorage(저장), EmailService(발송). 클라이언트 코드(ViewModel)는 Dependency Injection을 통해 세 가지를 모두 사용하며, 각 클래스는 독립적으로 쉽게 테스트할 수 있고 다른 것에 영향을 미치지 않고 변경할 수 있습니다.

kotlin
// ❌ SRP 위반: Activity가 네트워크, DB 및 UI를 인식
class ProfileActivity : AppCompatActivity() {
    fun loadProfile() {
        api.getUser() // 네트워크 호출
        db.saveUser()    // DB 작업
        updateUI()         // UI 업데이트
    }
}

// ✅ SRP 준수: 계층이 분리됨
class ProfileViewModel : ViewModel() {
    private val repo = UserRepository()
    fun loadProfile() { repo.getUser() }
}

SRP 위반 증거: 200줄 이상의 클래스, 다른 도메인의 메소드, 다른 이유로 잠재한 변경. Android 개발의 규칙은 간단합니다. Activity는 화면 생명 주기만 처리하고, ViewModel은 UI 상태를, Repository는 데이터 원천을 처리합니다.

SRP와 마이크로서비스 아키텍처

SRP 원칙은 클래스만 아니라 서비스 레벨 아키텍처에도 적용됩니다. 각 마이크로서비스는 하나의 도메인 엔티티를 처리합니다. UserService — 사용자만, PaymentService — 결제만, NotificationService — 알림만. 이를 통해 서비스를 독립적으로 확축하고, 배포하고, 테스트할 수 있습니다. 모바일 애플리케이션에서 마이크로서비스 레벨의 SRP는 API 클라이언트를 도메인별로 분리하는 것으로 나타납니다.

OCP: 개방 폐쇐 원칙

Open-Closed Principle(OCP) — 클래스는 확장에 대해 열려 있고(새로운 동작 추가 가능), 수정에 대해 닫혀 있어야 합니다(기존 코드 변경 불가). 이는 다형성, 추상 클래스 및 인터페이스를 통해 달성됩니다. 기존 메소드에 if-else를 추가하는 대신 새로운 인터페이스 구현이 생성됩니다.

Clean Coder Blog, 2014에 따르면, OCP는 Strategy 패턴과 가장 잘 작동합니다. 예를 들어, 앱이 다양한 결제 방법(Google Pay, Apple Pay, PayPal)을 지원하는 경우, 결제 프로세서에 switch-case를 추가할 필요가 없습니다. 각 결제 방법은 공통 PaymentGateway 인터페이스를 구현하고, 새 결제 시스템은 기존 코드를 수정하지 않고 새 클래스로 추가됩니다.

kotlin
// ✅ OCP: 확장에 열리고, 수정에 닫힘
interface PaymentGateway {
    fun processPayment(amount: Double): Boolean
}

class GooglePayGateway : PaymentGateway {
    override fun processPayment(amount: Double) = true
}

// 새로운 결제 시스템 — 기존 코드 변경 없이
class ApplePayGateway : PaymentGateway {
    override fun processPayment(amount: Double) = true
}

LSP: 리스코프 치환 원칙

Liskov Substitution Principle(LSP) — Barbara Liskov의 치환 원칙입니다. S가 T의 하위 타입인 경우, T 타입의 객체는 프로그램 속성을 바꾸지 않고 S 타입의 객체로 대체될 수 있습니다. 형식적으로: 기본 클래스를 사용하는 함수는 그 어떤 하위 클래스와도 올바르게 동작해야 합니다. 하위 클래스가 기본 클래스가 예외를 발생시키지 않는 곳에서 예외를 발생시키면 LSP가 위반됩니다.

Robert C. Martin, 2002에 따르면, LSP는 가장 이해하기 어려운 SOLID 원칙입니다. 고전적인 위반 사례는 Rectangle을 상속하는 Square 클래스입니다. Square의 setWidth가 넓이와 높이를 모두 설정하면, Rectangle 동작을 기대하는 클라이언트 코드가 예상치 않은 결과를 얻습니다. 모바일 개발에서 LSP는 ViewModel을 상속할 때 자주 위반됩니다. 하위 ViewModel이 필수 의존성을 추가하는 경우입니다.

kotlin
// ❌ LSP 위반: Square가 Rectangle의 동작을 께짐
open class Rectangle(open var width: Int, open var height: Int)

class Square(side: Int) : Rectangle(side, side) {
    override var width
        get() = super.width
        set(value) { super.setBoth(value, value) }
}

ISP: 인터페이스 분리 원칙

Interface Segregation Principle(ISP) — 클라이언트는 사용하지 않는 인터페이스에 의존해서는 안 됩니다. 하나의 “둑집” 인터페이스 대신 여러 개의 폁소한 전문 인터페이스를 만드십시오. 클래스가 인터페이스를 구현하지만 일부 메소드가 UnsupportedOperationException을 발생시키거나 비어 있다면 이는 ISP 위반의 명확한 증거입니다.

DigitalOcean, 2024에 따르면, ViewModel과 Repository를 설계할 때 모바일 개발에서 ISP가 특히 중요합니다. 모든 CRUD 메소드를 가진 하나의 UserRepository 인터페이스 대신 QueryUserRepository(읽기 전용)와 CommandUserRepository(쓰기 전용)를 만드는 것이 좋습니다. 그렇게 하면 읽기 전용 클라이언트(UI 요소)는 Query 인터페이스에만 의존하고 쓰기 메소드에 대해 알 필요가 없습니다.

kotlin
// ❌ 둑집 인터페이스 — 클라이언트가 불필요한 메소드를 강제로 구현
interface UserOperations {
    fun getUser(id: String): User
    fun saveUser(user: User)
    fun deleteUser(id: String)
    fun exportUsers(): File
}

// ✅ ISP: 분리된 인터페이스
interface UserReader { fun getUser(id: String): User }
interface UserWriter { fun saveUser(user: User) }
interface UserDeleter { fun deleteUser(id: String) }

DIP: 의존성 역전 원칙

Dependency Inversion Principle(DIP) — 상위 모듈은 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상화(인터페이스)에 의존해야 합니다. 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 합니다. 이것은 “의존성 주입”(DI)이 아니지만, DI는 DIP를 구현하는 일반적인 방법입니다.

Robert C. Martin, 2019에 따르면, DIP는 Clean Architecture의 기반입니다. ViewModel(상위)은 직접 RetrofitApi(세부 사항) 인스턴스를 생성해서는 안 됩니다. 대신 ViewModel은 UserRepository 인터페이스에 의존하고, Retrofit을 사용하는 구체적인 UserRepositoryImpl이 생성자를 통해 전달됩니다. Android에서 DIP는 Hilt/Dagger 또는 Koin을 통해 구현됩니다. 모든 의존성은 DI 컨테이너를 통해 제공됩니다.

kotlin
// ✅ DIP: Module은 추상화에 의존하고, 세부 사항에 의존하지 않음
class UserRepositoryImpl(
    private val api: UserApi,   // 인터페이스에 의존
    private val db: UserDao     // 인터페이스에 의존
) : UserRepository {

    override suspend fun getUser(id: String): User {
        return api.fetchUser(id)
    }
}

// Hilt DI: 세부 사항이 DI 모듈을 통해 연결됨
@Module
object NetworkModule {
    @Provides
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}

모바일 개발에서 SOLID 적용

모바일 개발에서 SOLID는 애플리케이션 아키텍처부터 개별 클래스걌지 모든 레벨에서 적용됩니다. Android 프로젝트에서 Clean Architecture는 코드를 세 계층으로 분할합니다. domain(비즈니스 로직 — 프레임워크에서 독립), data(리포지토리, API, DB), presentation(UI, ViewModel). Domain 계층은 SOLID 원칙을 사용합니다. 사용 사례(SRP), 리포지토리 인터페이스(DIP), 엔티티 클래스(OCP + LSP).

Android Developers Guide, 2025에 따르면, Android에서 SRP는 ViewModel, Repository, Mapper를 분리하는 것으로 나타납니다. OCP — DataSource 인터페이스를 통해 새 데이터 원천을 추가할 때. LSP — 다른 리포지토리에서 일관된 Result 처리. ISP — CQRS 접근 방식(Read/Write 리포지토리 분리). DIP — 의존성 주입을 위한 Hilt/Koin.

원칙없이서의 문제모바일 프로젝트서의 해결책
SRP1000+ 줄의 ActivityViewModel + UseCase + Repository
OCP결제 유형별 switch-caseStrategy: PaymentGateway 인터페이스
LSPBaseViewModel 교체 시 버그하위 클래스 계약 확인
ISPUnsupportedOperationExceptionReader / Writer 분리
DIPViewModel이 직접 Retrofit 생성Hilt / Koin DI 컨테이너

SOLID 적용 시 일반적인 실수

SOLID 실수는 대부분 코드의 과도한 복잡화와 관련이 있습니다. 첫볨째 — 렉스트를 고려하지 않고 원칙을 그대로 따르는 것. “깨끗한” ISP를 위해 하나의 UserService 클래스를 10개의 인터페이스와 15개의 클래스로 나누는 것은 과장 설계입니다. SOLID는 도구이지 목표가 아닙니다. 둘째 실수는 SRP를 “하나의 메소드 = 하나의 잉무”라고 오해하는 것입니다. 모든 메소드가 동일한 잉무 영역에 속하는 경우 클래스는 여러 메소드를 가질 수 있습니다.

Simple Thread, 2024에 따르면, 셈 번째 실수는 Android에서 ViewModel을 상속할 때 LSP를 무시하는 것입니다. 기본 ViewModel이 LiveData를 기대하는데 하위 ViewModel이 StateFlow를 사용하면 LiveData에 구독한 클라이언트 코드가 업데이트를 받지 못합니다. 넣번째 — 테스트를 위해 DIP를 위반하는 것. RepositoryImpl이 직접 OkHttpClient 인스턴스를 생성하면 단위 테스트가 불가능해집니다.

황금 규칙: 실제 문제(잠재한 변경, 테스트 어려움, 중복)를 해결하는 경우에만 SOLID를 적용하세요. 단순한 CRUD 화면에서 모든 5가지 원칙을 엄격히 따르는 것은 과하입니다. 비즈니스 로직, 금융 계산 및 API 상호작용에는 SOLID가 필수적입니다.

SOLID와 Clean Architecture 연관성

Clean Architecture(Robert C. Martin, 2012) — 애플리케이션 계층 레벨에서 SOLID를 직접 적용한 것입니다. SRP는 사용 사례 경계를 정의합니다(각 사용 사례 = 하나의 클래스). OCP는 리포지토리 인터페이스를 통해 구현됩니다(Data Layer는 Domain을 수정하지 않고 변경 가능). ISP는 사용 사례를 입력/출력 경계로 분리합니다. DIP — 의존성이 Domain 계층 안으로 향하도록 합니다. LSP는 어떤 리포지토리 구현이든 사용 사례를 구지 않고 교체 가능하도록 보장합니다.

자주 묻는 질문

간단한 말로 SOLID란 무엇인가요?

SOLID — 코드를 쉽게 변경하고, 테스트하고, 이해할 수 있도록 해주는 5가지 규칙입니다. 각 문자는 하나의 원칙을 나타냅니다. 클래스를 크게 작성하지 말고(SRP), 기존 코드를 수정하지 말고 새로운 코드를 추가하고(OCP), 하위 클래스의 동작을 께지 말고(LSP) 등입니다.

가장 중요한 SOLID 원칙은 무엇인가요?

SRP(Single Responsibility)가 가장 중요하다고 여겨집니다. 이는 위반이 God 클래스 — 테스트하고 변경하기 어려운 대형 클래스를 만들기 때문입니다. 그러나 DIP(Dependency Inversion) 없이는 코드가 강하게 결합되어 있으므로 그것도 중요합니다.

모바일 개발에 SOLID는 필수인가요?

필수는 아닙니다기 하지만, 수명이 긴 상용 프로젝트에는 국격히 권장됩니다. 간단한 앱(한 화면, 비즈니스 로직 없음)에 있어서는 SOLID가 과할일 수 있습니다. 50+ 화면과 3+ 개발자가 있는 프로젝트에는 SOLID가 최소 요구 사항입니다.

SOLID를 따르지 않으면 어떻게 되나요?

결과: 클래스가 “둑집”해지고(1000+ 줄), 한 곳의 변경이 다른 세 곳을 균지고, 단위 테스트가 불가능해지며, 새 기능을 추가하는 데 몇 일 대신 몇 주가 걸립니다. 시간이 지나면 코드는 “Big Ball of Mud” — 엄키고 위해로운 상태가 됩니다.

프로젝트에서 SOLID가 지커지고 있는지 확인하는 방법은?

준수 표시: 각 클래스가 200줄 미만, 기능 변경이 5+ 파일에 영향을 미치지 않음, 10개의 의존성을 메크하지 않고 테스트를 작성할 수 있음, 새 개발자가 하루 만에 구조를 이해함. SonarQube와 detekt와 같은 도구는 SRP와 DIP 위반을 식별하는 데 도움이 됩니다.

요약

  • SOLID — 유연하고 유지보수가 용이한 코드 작성을 위한 5가지 OOP 원칙(SRP, OCP, LSP, ISP, DIP)
  • SRP — 각 엔티티는 하나의 작업을 담당, God 클래스 문제 해결
  • OCP — 다형성을 통한 확장, 기존 코드 수정 불가
  • LSP — 하위 클래스는 기본 클래스의 동작을 께지 않아야 함
  • ISP — 보펵적인 “스위스 칼” 대신 폁소한 인터페이스
  • DIP — 추상화에 의존, Android에서 Hilt/Koin 통한 주입
  • SOLID는 Clean Architecture와 상용 모바일 프로젝트에 필수적

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기