SOLID 원칙에 대한 확실한 설명

발행: (2025년 12월 31일 오후 09:03 GMT+9)
12 min read
원문: Dev.to

I’m happy to translate the text for you, but I don’t see the content you’d like translated—only the source line. Could you please provide the full text you’d like me to translate into Korean? Once I have it, I’ll keep the source link at the top unchanged and translate the rest while preserving all formatting.

소개

소프트웨어가 **“알고리즘이 잘못되었다”**는 이유로 실패하는 경우는 드뭅니다.
대부분은 서서히 퇴화합니다: 새로운 기능을 추가하기 어려워지고, 버그가 다시 나타나며, 아주 작은 변경조차 위험하게 느껴집니다.

SOLID 원칙은 코드를 더 오래 이해하기 쉽고 적응 가능하게 유지할 수 있는 다섯 가지 실용적인 방법을 제공합니다.

SOLID세 가지 물질 상태 중 하나가 아닙니다.
이는 유지보수가 매우 쉬운 코드를 작성하도록 돕는 다섯 가지 설계 원칙의 약어입니다.

원칙은 다음과 같습니다:

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
  2. 개방‑폐쇄 원칙 (Open‑Closed Principle, OCP)
  3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
  4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  5. 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

이 원칙들을 함께 적용하면 느슨한 결합, 명확한 책임, 그리고 안정적인 추상화를 촉진하여 장기간에 걸쳐 살아남을 수 있는 코드를 만들 수 있습니다.

1. 단일 책임 원칙 (SRP)

클래스는 변경 이유가 하나만 있어야 합니다.

나쁜 예

class UserService {
    public void validate(User user) { /* … */ }
    public void saveToDatabase(User user) { /* … */ }
    public void sendEmail(User user) { /* … */ }
    public void log(User user) { /* … */ }
}

문제점

  • 네 가지 변경 이유 (validate, saveToDatabase, sendEmail, log).
  • 재사용이 어려움 – 호출자는 검증만 필요할 때 이메일 전송이나 DB 쓰기를 원하지 않음.
  • 테스트가 악몽 – 로거, 이메일 클라이언트, DB 클라이언트를 모두 모킹해야 함.

더 나은 설계

class UserValidator {
    public void validate(User user) { /* … */ }
}

class UserRepository {
    public void save(User user) { /* … */ }
}

class EmailNotifier {
    public void send(User user) { /* … */ }
}

class Logger {
    public void log(User user) { /* … */ }
}

각 클래스는 이제 변경 이유가 하나이며 독립적으로 사용할 수 있습니다 (예: 검증만 수행하고 이메일은 보내지 않음).

좌우명: Ek class – ek kaam (한 클래스 – 한 작업).

2. Open‑Closed Principle (OCP)

소프트웨어 엔티티는 확장에 열려 있어야 하지만 수정에는 닫혀 있어야 합니다.

Violating OCP

class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.width * r.height;
        } else if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        }
        // Adding a Square would require modifying this method → OCP violation
    }
}

OCP‑compliant design

abstract class Shape {
    abstract double area();
}

class Rectangle extends Shape {
    double width, height;
    @Override double area() { return width * height; }
}

class Circle extends Shape {
    double radius;
    @Override double area() { return Math.PI * radius * radius; }
}

// New shapes (e.g., Square) just extend Shape – no change to AreaCalculator
class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.area();
    }
}

이제 Square(또는 새로운 도형)를 추가해도 AreaCalculator수정할 필요가 없습니다.

Mantra: Extend karo, edit nahi (확장하라, 수정하지 마라).

3. 리스코프 치환 원칙 (LSP)

상위 클래스의 객체는 하위 클래스의 객체로 프로그램의 바람직한 속성을 변경하지 않고 교체할 수 있어야 합니다.

문제 예시

interface Bird {
    void fly();
    void walk();
}

class Sparrow implements Bird {
    public void fly() { /* flying logic */ }
    public void walk() { /* walking logic */ }
}

class Ostrich implements Bird {
    public void fly() { throw new UnsupportedOperationException(); }
    public void walk() { /* walking logic */ }
}

Ostrichfly() 계약을 위반합니다 – 어떤 Bird든 날 수 있다고 기대하는 코드는 실패하게 됩니다.

더 나은 추상화

interface Walkable {
    void walk();
}

interface Flyable {
    void fly();
}

abstract class Bird implements Walkable { /* common walk logic */ }

class Sparrow extends Bird implements Flyable {
    public void fly() { /* flying logic */ }
}

class Ostrich extends Bird {
    // No fly() method – respects LSP
}

이제 makeWalk(Bird b)SparrowOstrich 모두에서 예상치 못한 동작 없이 정상적으로 동작합니다.

Memory aid (Hindi): Agar bachche ka behavior parents ke jaise hai, to wo party mein ja sakta hai (하위 클래스가 상위 클래스와 같은 행동을 보이면, 서로 교체해서 사용할 수 있다).

4. 인터페이스 분리 원칙 (ISP)

클라이언트는 사용하지 않는 인터페이스에 의존하도록 강요받아서는 안 된다.

ISP 위반

interface IWorker {
    void work();
    void eat();   // 로봇은 먹지 않는다!
}

Robot 클래스는 의미 없는 eat() 메서드를 구현해야 하므로, 더미 코드와 강한 결합도가 생깁니다.

ISP 준수 설계

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() { /* … */ }
    public void eat()  { /* … */ }
}

class Robot implements Workable {
    public void work() { /* … */ }
}

이제 각 클래스는 실제로 필요한 인터페이스만 구현합니다.

좌우명: 분할 정복 – 큰 인터페이스를 집중된 작은 인터페이스로 나눕니다.

5. 의존성 역전 원칙 (DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 두 모듈 모두 추상화에 의존해야 합니다.

결합도가 높은 예시

class MasalaTeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class TeaShop {
    private MasalaTeaMaker maker = new MasalaTeaMaker(); // 하드코딩된 의존성!

    public String orderTea(String type) {
        if ("masala".equals(type)) {
            return maker.makeTea();
        }
        return "Only Masala available"; // 경직됨!
    }
}

TeaShopMasalaTeaMaker에 직접 묶여 있습니다. 새로운 차 종류를 추가하려면 TeaShop을 수정해야 합니다.

DIP‑준수 설계

interface TeaMaker {
    String makeTea();
}

class MasalaTeaMaker implements TeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class GreenTeaMaker implements TeaMaker {
    public String makeTea() { return "Green Tea ready!"; }
}

class TeaShop {
    private final Map<String, TeaMaker> makers = new HashMap<>();

    public TeaShop(Map<String, TeaMaker> makers) {
        this.makers.putAll(makers);
    }

    public String orderTea(String type) {
        TeaMaker maker = makers.get(type);
        return (maker != null) ? maker.makeTea() : "Tea not available";
    }
}
  • TeaShop추상화TeaMaker에 의존합니다.
  • 새로운 차 메이커를 추가하더라도 TeaShop을 수정할 필요가 없으며, 맵에 등록하기만 하면 됩니다.

요약

원칙핵심 아이디어전형적인 만트라
SRP클래스당 하나의 변경 이유한 클래스 – 한 일
OCP기존 코드를 수정하지 않고 행동을 확장확장하라, 수정은 하지 마라
LSP하위 타입은 기본 타입을 대체할 수 있어야 함자식의 행동은 부모와 같아야 함
ISP비대한 인터페이스를 클라이언트‑특화 인터페이스로 분리분할 정복
DIP구체가 아닌 추상에 의존고수준 ↔ 저수준을 인터페이스로 연결

이 다섯 가지 원칙을 적용하면 느슨하게 결합되고, 높은 응집력을 가지며, 쉽게 유지보수 가능한 소프트웨어를 구축할 수 있어 시간이 지나도 우아하게 진화할 수 있습니다.

Source:

추상화를 활용한 TeaShop 예제 업데이트

아래는 원본 스니펫을 정리한 버전입니다.
코드는 이제 의존성 역전 원칙(SOLID의 “D”)을 따르며, 구체 클래스 대신 추상화(TeaMaker)에 의존합니다.

1️⃣ 도메인 모델 (Java)

// 1. Abstraction -------------------------------------------------
public interface TeaMaker {
    String makeTea();
}

// 2. Concrete implementations --------------------------------------
public class MasalaTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Masala Tea ready!";
    }
}

public class GingerTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Ginger Tea ready!";
    }
}

// 3. High‑level module (depends on the abstraction) -------------
public class TeaShop {

    // Constructor injection (setter injection is also possible)
    private final TeaMaker teaMaker;

    public TeaShop(TeaMaker teaMaker) {
        this.teaMaker = teaMaker;
    }

    public String orderTea(String type) {
        // The shop no longer knows about concrete makers.
        // It simply asks the injected TeaMaker to prepare the drink.
        return teaMaker.makeTea();
    }
}

2️⃣ 작동 방식

구성 요소역할의존성
TeaMaker모든 차 제조 클래스가 따라야 할 계약을 정의합니다.
MasalaTeaMaker / GingerTeaMakerTeaMaker 계약을 구현하는 구체 클래스입니다.TeaMaker 구현
TeaShop차를 주문하는 고수준 클래스입니다.오직 TeaMaker(추상화)만 의존

TeaShop은 이제 MasalaTeaMakerGingerTeaMaker를 직접 생성하거나 알지 못합니다. 대신 실행 시점에 특정 TeaMaker가 주입됩니다.

3️⃣ 의존성 주입 옵션

주입 방식예시
생성자 주입 (위 예시)new TeaShop(new MasalaTeaMaker())
세터 주입java\npublic void setTeaMaker(TeaMaker teaMaker) { this.teaMaker = teaMaker; }\n
필드 주입 (프레임워크 전용)@Inject private TeaMaker teaMaker;

프로젝트 아키텍처에 가장 적합한 방식을 선택하세요.

4️⃣ 빠른 기억법

“추상화에 의존하고, 클래스에 의존하지 마라.”

인터페이스에 의존하고 구체 클래스에 의존하지 않는다는 의미입니다. 이 원칙을 기억하면 SOLID의 의존성 역전 원칙을 잘 지킬 수 있습니다.

5️⃣ 마무리

위와 같이 리팩터링하면 SOLID 원칙, 특히 D(Dependency Inversion)를 만족하게 됩니다.
궁금한 점이 있으면 아래에 댓글을 남겨 주세요.

읽어 주셔서 감사합니다 – 앞으로도 기대해 주세요!

Back to Blog

관련 글

더 보기 »