본문 바로가기
-- 오늘 있었던 개발 일기

solid

by code study 2025. 12. 7.

SOLID 원칙이란?

객체지향 프로그래밍에서 유지보수가 쉽고 확장 가능한 코드를 작성하기 위한 5가지 설계 원칙입니다.

SOLID는 각 원칙의 앞글자를 딴 약자

  • SRP: Single Responsibility Principle (단일 책임 원칙)
  • OCP: Open-Closed Principle (개방-폐쇄 원칙)
  • LSP: Liskov Substitution Principle (리스코프 치환 원칙)
  • ISP: Interface Segregation Principle (인터페이스 분리 원칙)
  • DIP: Dependency Inversion Principle (의존관계 역전 원칙)

1️⃣ SRP - 단일 책임 원칙

"한 클래스는 하나의 책임만 가져야 한다"

❌ 잘못된 예시

// 회원 관리, 이메일 발송, DB 저장을 모두 담당 (책임이 너무 많음!)
public class User {
    private String name;
    private String email;

    public void saveToDatabase() {
        // DB 저장 코드
    }

    public void sendEmail() {
        // 이메일 발송 코드
    }

    public void generateReport() {
        // 리포트 생성 코드
    }
}

✅ 올바른 예시

// 각 클래스가 하나의 책임만 담당
public class User {
    private String name;
    private String email;
    // 회원 정보만 관리
}

public class UserRepository {
    public void save(User user) {
        // DB 저장만 담당
    }
}

public class EmailService {
    public void sendEmail(User user) {
        // 이메일 발송만 담당
    }
}

왜 중요한가?

  • 코드 수정 시 영향 범위가 줄어듦
  • 테스트하기 쉬워짐
  • 재사용성이 높아짐

2️⃣ OCP - 개방-폐쇄 원칙

"확장에는 열려있고, 수정에는 닫혀있어야 한다"

새로운 기능 추가 시 기존 코드를 수정하지 않고 확장할 수 있어야 합니다.

❌ 잘못된 예시

public class PaymentProcessor {
    public void processPayment(String type) {
        if (type.equals("CARD")) {
            // 카드 결제 처리
        } else if (type.equals("CASH")) {
            // 현금 결제 처리
        }
        // 새로운 결제 수단 추가할 때마다 이 코드를 수정해야 함!
    }
}

✅ 올바른 예시

// 인터페이스 정의
public interface Payment {
    void process();
}

// 각 결제 수단을 별도 클래스로 구현
public class CardPayment implements Payment {
    public void process() {
        // 카드 결제 처리
    }
}

public class CashPayment implements Payment {
    public void process() {
        // 현금 결제 처리
    }
}

// 새로운 결제 수단 추가 시 기존 코드 수정 없이 확장만 하면 됨
public class KakaoPayment implements Payment {
    public void process() {
        // 카카오페이 처리
    }
}

왜 중요한가?

  • 기존 코드를 건드리지 않아 버그 발생 위험이 줄어듦
  • 새로운 기능 추가가 쉬워짐

3️⃣ LSP - 리스코프 치환 원칙

"자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 한다"

❌ 잘못된 예시

public class Bird {
    public void fly() {
        System.out.println("날아갑니다");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
        // 부모 클래스의 fly()를 제대로 대체하지 못함
    }
}

✅ 올바른 예시

public abstract class Bird {
    public abstract void move();
}

public class Eagle extends Bird {
    @Override
    public void move() {
        System.out.println("날아갑니다");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        System.out.println("뒤뚱뒤뚱 걸어갑니다");
    }
}

왜 중요한가?

  • 다형성을 올바르게 사용할 수 있음
  • 예상치 못한 동작을 방지

4️⃣ ISP - 인터페이스 분리 원칙

"클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다"

❌ 잘못된 예시

// 너무 많은 기능을 하나의 인터페이스에 담음
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// 로봇은 eat()과 sleep()이 필요 없는데 구현해야 함
public class Robot implements Worker {
    public void work() { /* 작업 */ }
    public void eat() { /* 불필요 */ }
    public void sleep() { /* 불필요 */ }
}

✅ 올바른 예시

// 인터페이스를 역할별로 분리
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

// 필요한 인터페이스만 구현
public class Human implements Workable, Eatable, Sleepable {
    public void work() { /* 작업 */ }
    public void eat() { /* 식사 */ }
    public void sleep() { /* 수면 */ }
}

public class Robot implements Workable {
    public void work() { /* 작업 */ }
    // eat(), sleep() 구현 불필요!
}

왜 중요한가?

  • 불필요한 의존성 제거
  • 코드가 더 명확해짐

5️⃣ DIP - 의존관계 역전 원칙

"구체적인 것이 아닌 추상적인 것에 의존해야 한다"

❌ 잘못된 예시

// 구체적인 클래스에 직접 의존
public class UserService {
    private MySQLDatabase database = new MySQLDatabase();

    public void saveUser(User user) {
        database.save(user);
        // MySQL에서 Oracle로 바꾸려면 코드 수정 필요!
    }
}

✅ 올바른 예시

// 인터페이스(추상)에 의존
public interface Database {
    void save(User user);
}

public class MySQLDatabase implements Database {
    public void save(User user) { /* MySQL 저장 */ }
}

public class OracleDatabase implements Database {
    public void save(User user) { /* Oracle 저장 */ }
}

public class UserService {
    private Database database; // 인터페이스에 의존

    public UserService(Database database) {
        this.database = database; // 외부에서 주입받음
    }

    public void saveUser(User user) {
        database.save(user);
        // DB 변경 시 UserService 코드 수정 불필요!
    }
}

왜 중요한가?

  • 결합도가 낮아져 유연한 구조가 됨
  • 테스트가 쉬워짐 (Mock 객체 사용 가능)
  • 스프링부트에 의존성 주입이 바로 DIP 원칙을 구현한 것

실전 예제 - 도서 관리 시스템

// SRP: 각 클래스가 하나의 책임만
public class Book {
    private String title;
    private String author;
}

// OCP: 새로운 할인 정책 추가 시 확장만 하면 됨
public interface DiscountPolicy {
    int getDiscountAmount(int price);
}

public class StudentDiscount implements DiscountPolicy {
    public int getDiscountAmount(int price) {
        return price * 10 / 100; // 10% 할인
    }
}

// ISP: 필요한 기능만 인터페이스로 분리
public interface Borrowable {
    void borrow();
    void returnBook();
}

public interface Reservable {
    void reserve();
}

// DIP: 구체적인 클래스가 아닌 인터페이스에 의존
public class LibraryService {
    private BookRepository repository; // 인터페이스에 의존

    public LibraryService(BookRepository repository) {
        this.repository = repository;
    }
}

SOLID 원칙을 지키면 얻는 이점

이점설명
🔧 유지보수 용이 코드 수정이 쉽고 안전해짐
🚀 확장성 새로운 기능 추가가 쉬워짐
🧪 테스트 용이 각 부분을 독립적으로 테스트 가능
🔄 재사용성 코드를 다른 곳에서도 활용 가능
👥 협업 효율 코드 이해가 쉬워져 팀 작업이 수월함

핵심 정리

꼭 기억해야 할 SOLID

  1. SRP → 한 클래스는 한 가지 일만
  2. OCP → 수정은 닫고, 확장은 열어두기
  3. LSP → 자식은 부모를 완벽히 대체 가능해야
  4. ISP → 인터페이스는 작고 명확하게
  5. DIP → 구체적인 것 말고 추상적인 것에 의존

 

내가 생각해본 적용 과정

코드 작성 시 스스로 질문하기
    ↓
이 클래스의 책임이 명확한가? → SRP
새 기능 추가 시 기존 코드 수정이 필요한가? → OCP
상속 관계가 올바른가? → LSP
인터페이스가 너무 크지 않은가? → ISP
구체 클래스에 직접 의존하고 있지 않은가? → DIP

 

찾아본 다른 원칙들

1. DRY(Don't Repeat Yourself) - 중복 코드를 만들지 말자,

2. KISS(Keep It Simple, Stupid) - 단순하게 만들자,

3. YAGNI(You Aren't Gonna Need It) - 필요 없는 기능은 만들지 말자