카테고리 없음

[유레카 / 백엔드] TIL - 7 (SOLID)

coding-quokka101 2025. 10. 22. 11:00

 

“좋은 객체지향 설계의 핵심은 SOLID에 있다.”

 

프로그래밍을 처음 배울 때는 “코드를 돌아가게 하는 것”이 목표였다. 하지만 개발 경험이 쌓이고 협업 프로젝트를 하다 보면,
유지보수하기 좋은 코드”의 중요성을 절실히 느끼게 된다. 그 기준점이 바로 SOLID 원칙이다.
SOLID는 단순히 이론적인 개념이 아니라, “확장 가능한 코드”를 만들기 위한 객체지향 설계의 핵심 철학이다.

그래서 오늘은 SOLID의 다섯 가지 원칙을 ✅ 개념적 배경 → ✅ 코드 예제 → ✅ 리팩토링 실전 적용 순으로 정리해본다.

SOLID 원칙이란? 실전 예제로 쉽게 이해하는 객체지향 설계 5대 원칙


🧠 SOLID란?

SOLID는 객체지향 설계에서 유연성(Flexibility), 확장성(Extensibility), **유지보수성(Maintainability)**을 높이기 위한 5가지 원칙의 집합이다.

약어 원칙명 핵심 목표

S Single Responsibility Principle 단일 책임 원칙
O Open/Closed Principle 개방-폐쇄 원칙
L Liskov Substitution Principle 리스코프 치환 원칙
I Interface Segregation Principle 인터페이스 분리 원칙
D Dependency Inversion Principle 의존 역전 원칙

🔹 1. SRP (Single Responsibility Principle — 단일 책임 원칙)

“하나의 클래스는 오직 하나의 이유로만 변경되어야 한다.”

이 원칙은 **응집도(cohesion)**를 높이고 **결합도(coupling)**를 낮추는 데 목적이 있다.
즉, 클래스가 여러 역할을 동시에 하면 그만큼 수정 시 리스크가 커진다.

🧩 개념적 이해

  • 클래스가 2개 이상의 책임을 가지면,
    하나의 변경이 다른 기능에 의도치 않은 부작용을 일으킬 수 있다.
  • 유지보수 시에도 “이 부분을 바꾸면 어디까지 영향이 갈까?”라는 걱정이 생긴다.
  • 따라서 각 클래스는 하나의 역할만 수행해야 하며,
    각 책임은 별도의 객체로 분리되어야 한다.

❌ 위반 예시

class UserService {
    public void register(String user) {
        saveToDatabase(user);
        sendWelcomeEmail(user);
    }
    private void saveToDatabase(String user) { ... }
    private void sendWelcomeEmail(String user) { ... }
}

이 클래스는 데이터 저장과 이메일 발송이라는 두 가지 책임을 동시에 가진다.

✅ 개선된 설계

class UserRepository {
    void save(String user) { ... }
}

class EmailService {
    void sendWelcomeEmail(String user) { ... }
}

class UserService {
    private final UserRepository repo;
    private final EmailService mailer;
    void register(String user) {
        repo.save(user);
        mailer.sendWelcomeEmail(user);
    }
}

→ 이제 UserService는 “회원 가입 관리”라는 단일 책임만 가진다.

SRP 적용 전 후 클래스 다이어그램

 


🔹 2. OCP (Open/Closed Principle — 개방-폐쇄 원칙)

“소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.”

🧩 개념적 이해

  • 기능을 확장할 때는 기존 코드를 건드리지 않아야 한다.
  • 이는 인터페이스 기반 설계를 통해 가능하다.
  • 새 기능을 추가할 때, 기존 클래스의 내부 로직을 수정하지 않고
    **다형성(polymorphism)**으로 대체하는 구조를 만들면 된다.

✅ 예시

interface Notifier {
    void send(String message);
}

class EmailNotifier implements Notifier {
    public void send(String message) { System.out.println("Email: " + message); }
}

class SlackNotifier implements Notifier {
    public void send(String message) { System.out.println("Slack: " + message); }
}

class AlertService {
    private final Notifier notifier;
    AlertService(Notifier notifier) { this.notifier = notifier; }
    void alert(String msg) { notifier.send(msg); }
}

→ 새로운 알림 방식(SMSNotifier 등)을 추가해도 AlertService 수정이 필요 없다.

OCP 구조의 다형성 흐름도

 


🔹 3. LSP (Liskov Substitution Principle — 리스코프 치환 원칙)

“자식 클래스는 언제나 부모 클래스로 대체될 수 있어야 한다.”

🧩 개념적 이해

  • LSP는 상속의 올바른 사용을 다룬다.
  • 자식 클래스가 부모 클래스의 규약을 위반하면,
    상속은 오히려 버그의 원인이 된다.
  • 즉, **‘IS-A’ 관계가 성립하는가?**를 항상 고민해야 한다.

❌ 위반 예시

class Bird {
    void fly() { System.out.println("날 수 있음"); }
}

class Penguin extends Bird {
    @Override
    void fly() { throw new UnsupportedOperationException("펭귄은 못 날아요"); }
}

→ Penguin은 Bird처럼 행동하지 않으므로 LSP 위반.

✅ 개선된 설계

interface Bird { }
interface FlyableBird extends Bird { void fly(); }

class Sparrow implements FlyableBird { public void fly() { System.out.println("참새 날다"); } }
class Penguin implements Bird { }

→ “모든 새가 날 수 있다”는 가정 자체를 분리하여 설계.

LSP 위반 vs 개선 구조 다이어그램


🔹 4. ISP (Interface Segregation Principle — 인터페이스 분리 원칙)

“클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.”

🧩 개념적 이해

  • 하나의 인터페이스가 너무 많은 기능을 담으면,
    특정 구현체는 불필요한 의존성을 갖게 된다.
  • 이로 인해 “빈 메서드” 구현이나 “불필요한 의존 주입”이 발생한다.
  • 따라서 작고 명확한 인터페이스 여러 개로 나누는 것이 바람직하다.

✅ 예시

interface Workable { void work(); }
interface Eatable { void eat(); }

class Human implements Workable, Eatable { ... }
class Robot implements Workable { ... }

→ 각 객체가 자신에게 필요한 인터페이스만 구현.

하나의 큰 인터페이스


🔹 5. DIP (Dependency Inversion Principle — 의존 역전 원칙)

“고수준 모듈은 저수준 모듈에 의존해서는 안 되며,
둘 다 추상화에 의존해야 한다.”

🧩 개념적 이해

  • DIP는 **의존성 주입(Dependency Injection)**과 밀접한 관계가 있다.
  • 직접 객체를 생성하는 대신,
    추상 인터페이스를 통해 외부로부터 주입받는 방식으로
    코드의 결합도를 낮추는 것이 핵심이다.

✅ 예시

interface Switchable { void turnOn(); }

class Lamp implements Switchable {
    public void turnOn() { System.out.println("불이 켜졌습니다."); }
}

class Button {
    private Switchable device;
    public Button(Switchable device) { this.device = device; }
    void press() { device.turnOn(); }
}

→ Button은 이제 Lamp가 아닌 Switchable이라는 추상화에 의존한다.

DIP 의존 방향 다이어그램


🧱 SOLID 위반 사례 리팩토링 일기


🧩 1. 문제 상황

class OrderManager {
    public void createOrder(String product) {
        saveToDB(product);
        sendNotification(product);
    }
    private void saveToDB(String product) { ... }
    private void sendNotification(String product) { ... }
}

→ SRP 위반, OCP 위반, DIP 위반까지 종합세트였다.

한 클래스가 모든 역할


🔧 2. 리팩토링 후 — SRP, OCP, DIP 적용

class OrderService {
    private final Database db;
    private final Notifier notifier;

    public OrderService(Database db, Notifier notifier) {
        this.db = db;
        this.notifier = notifier;
    }

    public void createOrder(String product) {
        db.save(product);
        notifier.send(product);
    }
}

interface Database { void save(String product); }
interface Notifier { void send(String product); }

class MongoDatabase implements Database {
    public void save(String product) { System.out.println("MongoDB 저장 완료"); }
}

class EmailNotifier implements Notifier {
    public void send(String product) { System.out.println("이메일 발송 완료"); }
}

→ 역할이 명확히 분리되고, 의존성도 추상화되었다.

→ 이제 OrderService는 데이터 저장 방식이나 알림 방식에 전혀 의존하지 않는다.
→ 변경은 닫혀 있고, 확장은 열려 있는 구조로 진화했다.

SOLID 적용 전후 Before & After UML 다이어그램




🧬 3. LSP와 ISP 적용

LSP 위반을 피하기 위해, 상속 관계보다는 행동을 기준으로 인터페이스 분리했다.

ISP 원칙을 반영해 각 기능을 인터페이스로 나누고, 필요한 기능만 구현하도록 변경했다.

LSP + ISP 구조 개선 비교 다이어그램


 4. 리팩토링 후 느낀 점

“SOLID는 복잡한 걸 더 복잡하게 만드는 게 아니라, 복잡함을 질서 있게 정리하는 원칙이다.”

  • SRP로 클래스 간 의존이 명확해졌다.
  • OCP와 DIP로 새 기능을 추가해도 수정이 거의 없다.
  • LSP, ISP로 상속 구조가 더 유연해졌다.

💬  회고

✔ 오늘의 성취:

한 클래스에 모든 걸 몰아넣던 구조를 객체지향적으로 재설계했다.
유지보수가 쉬운 코드의 가치를 실감했다.

✔ 개선할 점:

다음엔 Spring의 DI 컨테이너를 이용해 SOLID 원칙을 프레임워크 차원에서 실천해볼 예정이다.

✔ 나만의 팁:

리팩토링 전후 구조를 UML로 시각화하면,
원칙 적용의 효과를 직관적으로 파악할 수 있다.


✨ 마무리하며

 

처음엔 SOLID가 어렵게 느껴졌지만, 실제 리팩토링에 적용하니 그 힘을 체감했다. "유지보수성”은 결국 설계의 품질에서 나온다는 걸 깨달았다.