본문 바로가기
푸닥거리

OOP(Object-Oriented Programming) 의 SOLID 원칙

by ┌(  ̄∇ ̄)┘™ 2022. 7. 10.
728x90

SOLID는 유지보수하기 좋은 객체지향(OOP) 소프트웨어를 만들기 위한 5가지 설계 원칙입니다. 각 글자가 하나의 원칙을 뜻하며, 함께 지키면 확장하기 쉽고, 바꾸기 안전하며, 테스트하기 좋은 코드가 됩니다. C# 예제와 함께 하나씩 정리합니다.

약자 원칙 한 줄 요약
S 단일 책임 (SRP) 클래스는 하나의 책임만
O 개방-폐쇄 (OCP) 확장엔 열고, 수정엔 닫는다
L 리스코프 치환 (LSP) 자식은 부모를 대체할 수 있어야
I 인터페이스 분리 (ISP) 안 쓰는 메서드를 강요하지 말라
D 의존 역전 (DIP) 구체가 아니라 추상에 의존하라

S — 단일 책임 원칙 (Single Responsibility)

클래스는 하나의 책임(역할)만 가져야 합니다. 하나의 클래스가 여러 일을 하면 코드가 길고 복잡해지며, 수정할 때 서로 얽혀 위험해집니다. 아래 Customer는 DB 작업과 에러 로깅을 함께 하고 있어 SRP를 위반합니다 — 로깅은 별도 클래스로 분리해야 합니다.

class Customer
{
    public void Add()
    {
        try { /* DB code */ }
        catch (Exception ex)
        {
            // ⚠️ 로깅 책임이 섞여 있음 (SRP 위반)
            System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
        }
    }
}

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

클래스는 확장에는 열려 있고 수정에는 닫혀 있어야 합니다. 새 기능을 추가할 때 기존 코드를 고치는 대신 상속·구현으로 확장합니다. 등급별 할인은 Customer를 수정하지 않고 자식 클래스로 확장합니다.

class Customer
{
    public virtual double GetDiscount(double totalSales) => totalSales;
}
class SilverCustomer : Customer
{
    public override double GetDiscount(double totalSales)
        => base.GetDiscount(totalSales) - 50;
}
class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
        => base.GetDiscount(totalSales) - 100;
}

L — 리스코프 치환 원칙 (Liskov Substitution)

자식 클래스는 부모 클래스를 언제든 대체할 수 있어야 합니다. 부모 타입을 기대하는 자리에 자식을 넣어도 프로그램이 깨지지 않아야 하죠. 인터페이스로 계약을 명확히 하면 이 원칙을 지키기 쉬워집니다.

interface IDiscount { double GetDiscount(double totalSales); }
interface IDatabase { void Add(); }

class Enquiry : IDiscount
{
    public double GetDiscount(double totalSales) => totalSales - 5;
}
class Customer : IDiscount, IDatabase
{
    public void Add() { /* DB code */ }
    public double GetDiscount(double totalSales) => totalSales;
}

I — 인터페이스 분리 원칙 (Interface Segregation)

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 큰 인터페이스 하나에 여러 기능을 몰아넣으면, 그중 일부만 필요한 구현체가 쓸모없는 메서드까지 구현해야 합니다. 인터페이스를 �&zw잘게 나눠 필요한한 것만 구현하게 합니다.

interface IDatabase { void Add(); }        // 기존 클라이언트용
interface IDatabaseV1 : IDatabase { void Read(); } // 신규 클라이언트용

class CustomerWithRead : IDatabaseV1
{
    public void Add()  { /* ... */ }
    public void Read() { /* 읽기 로직 */ }
}

// 기존 클라이언트는 영향 없음
IDatabase i = new Customer();  i.Add();
// 신규 클라이언트만 Read 사용
IDatabaseV1 iv1 = new CustomerWithRead();  iv1.Read();

D — 의존 역전 원칙 (Dependency Inversion)

고수준 모듈이 저수준 모듈에 직접 의존하면 안 되고, 둘 다 추상화에 의존해야 합니다. 즉 "구체 클래스"가 아니라 "인터페이스"에 의존하라는 것이죠. Customer가 특정 로거(파일/이벤트뷰어/이메일)에 직접 묶이지 않고 ILogger 추상에만 의존하면, 로깅 방식을 생성자 주입으로 갈아끼울 수 있습니다.

interface ILogger { void Handle(string error); }

class FileLogger : ILogger
{
    public void Handle(string e) => System.IO.File.WriteAllText(@"c:\Error.txt", e);
}
class EmailLogger : ILogger
{
    public void Handle(string e) { /* 이메일로 전송 */ }
}

class Customer : IDiscount, IDatabase
{
    private readonly ILogger _logger;
    public Customer(ILogger logger) { _logger = logger; } // 의존성 주입
}

// 로깅 방식을 외부에서 결정
IDatabase i = new Customer(new EmailLogger());

이 DIP가 바로 스프링 등에서 말하는 의존성 주입(DI)의 이론적 뿌리입니다. S에서 분리했던 로깅 책임이 D에서 추상화로 주입되며 깔끔하게 마무리되는 것을 볼 수 있습니다.

정리

SOLID의 목표는 하나 — 변경이 생겼을 때 고쳐야 할 코드의 범위를 최소화하는 것이다.

다섯 원칙은 서로 맞물려 있습니다. SRP로 책임을 나누고(S), 확장 가능하게 열고(O), 안전하게 치환하며(L), 인터페이스를 잘게 쪼개고(I), 추상에 의존(D)하면 — 요구사항이 바뀌어도 시스템 전체를 흔들지 않고 국소적으로 대응할 수 있습니다. 이 원칙들은 디자인 패턴과 함께 볼 때 더 선명해집니다.

 

간단한 C# 예제를 사용한 SOLID 아키텍처 원칙 - VisualGuide.org

Contents 내용물소개솔리드 란 무엇입니까?이해 S– SRP(단일책임원칙)이해 O – 개방형 폐쇄 원칙이해 L– LSP(리스코프 치환 원리)이해 I – ISP(인터페이스 분리 원칙)이해 D– 의존성 반전 원리SOLID

visualguide.org

728x90

댓글