티스토리 뷰

IT/SOLID

💎SOLID 원칙 완전 정복 - 객체지향 설계의 뼈대를 만드는 5가지 철칙

Stv 2025. 5. 7. 21:22

목차


    SOLID 원칙은 유지보수성, 확장성, 유연성, 테스트 용이성을 갖춘 객체지향 설계를 위한 핵심 5대 원칙이다.
    이 원칙을 제대로 이해하고 실무에 적용하면, 요구사항 변경에도 끄떡없는 탄탄한 시스템을 만들 수 있다.

     

    1. SRP - 단일 책임 원칙 (Signle Responsibility Principle)

     

    📌 정의

    "하나의 클래스는 하나의 책임만 가져야 한다."
    정확히 말하면 클래스는 오직 하나의 변경 이유만 가져야 한다.

     

    🎯 왜 중요한가?

    • 여러 책임을 가진 클래스는 변경 범위가 커짐
    • 하나의 책임이 바뀔 때 전혀 관련 없는 기능까지 깨질 위험있음
    • 테스트가 어려워지고 응집도가 낮아짐

    ❌ 위반 예시

    public class ReportManager {
        void generateReport() { ... }
        void saveToDB() { ... }
        void sendEmail() { ... }
    }

     

    ➡️ 생성/저장/전송이라는 3가지 책임을 동시에 가짐

     

    ✅ SRP 적용 구조

    class ReportGenerator { ... }
    class ReportSaver { ... }
    class ReportSender { ... }

     

    🔎 실무 포인트

    • "그리고(and)"가 클래스/메서드 이름에 들어가면 SRP 의심
    • Controller가 로직 + 응답 포맷 + 데이터 호출까지 한다면 분리 고려

    2. OCP - 개방/폐쇄 원칙 (Open/Close Principle)

     

    📌 정의

    "소프트웨어 구성 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다."

     

    🎯 왜 중요한가?

    • 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 함
    • OCP 위반 -> 수정 시 Side Effect -> 테스트 대혼란

    ❌ 위반 예시

    if (type == "A") { ... }  
    else if (type == "B") { ... }  
    else if (type == "C") { ... }

     

    ➡️ 새로운 타입이 생길 때마다 조건문 추가 -> 기존 코드 건드려야 함

     

    ✅ OCP 적용 구조 (전략 패턴 활용)

    interface DiscountPolicy { ... }
    
    class VipDiscountPolicy implements DiscountPolicy { ... }
    class GoldDiscountPolicy implements DiscountPolicy { ... }
    
    class OrderService {
        private final DiscountPolicy policy;
        public OrderService(DiscountPolicy policy) { this.policy = policy; }
    }

     

    🔎 실무 포인트

    • if/else가 반복된다면 -> 다형성으로 분기 처리
    • Spring에서는 인터페이스 + @Autowired 조합으로 OCP 실현 가능

    3. LSP - 리스코프 치환 원칙 (Liskov Subsititution Principle)

     

    📌정의 

    "자식 클래스는 부모 클래스를 대체해도 제대로 동작해야 한다."

     

    🎯왜 중요한가?

    • 다형성을 유지하려면 하위 타입이 상위 타입의 동작 계약을 깨면 안 됨
    • 자식 클래스가 부모 클래스의 기능을 무력화하거나, 의도를 왜곡하면 설계 전체가 망가짐

    ❌ 위반 예시

    class Rectangle {
        void setWidth(int w) { ... }
        void setHeight(int h) { ... }
    }
    
    class Square extends Rectangle {
        void setWidth(int w) { setHeight(w); }
        void setHeight(int h) { setWidth(h); }
    }

     

    ➡️ Square는 Rectangle을 대체할 수 없는 잘못된 is-a 관계 -> LSP 위반

     

    ✅ LSP 적용 구조

    • 공통 인터페이이스로 분리하고, 동작 방식이 다른 객체는 아예 분리
    interface Shape { int area(); }
    
    class Rectangle implements Shape { ... }
    class Square implements Shape { ... }

     

    🔎 실무 포인트

    • 자식 클래스가 부모 클래스 메서드를 무력화하거나 예외를 던지면 LSP 의심
    • 테스트 코드에서 다형성이 안 된다면 LSP 위반 가능성이 높음

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

     

    📌정의

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

     

    🎯왜 중요한가?

    • Fat Interface는 모든 구현체가 필요 없는 메서드까지 구현하게 만듦
    • 불필요한 의존성과 변경 전파를 유발
    • 모듈화/테스트성/유지보수성 모두 하락

    ❌위반 예시

    interface Machine {
        void print();
        void scan();
        void fax();
    }
    
    class Printer implements Machine {
        public void print() { ... }
        public void scan() { throw new UnsupportedOperationException(); }
        public void fax() { throw new UnsupportedOperationException(); }
    }

     

    ✅ ISP 적용 구조

    interface Printer { void print(); }
    interface Scanner { void scan(); }
    interface Fax { void fax(); }
    
    class MultiFunctionPrinter implements Printer, Scanner, Fax { ... }
    class SimplePrinter implements Printer { ... }

     

    🔎 실무 포인트

    • 서비스 인터페이스가 너무 많다면 분리 대상
    • 클라이언트(사용자)의 관점에서 "정말 필요한 기능"만 노출해야 함

    5. DIP - 의존성 역전 원칙 (Dependency Inversion Principle)

     

    📌정의

    "상위 모듈은 하위 모듈에 의존하면 안 된다. 모두가 추상화에 의존해야 한다."

     

    🎯왜 중요한가?

    • 고수준 비즈니스 로직이 저수준 구현체에 의존하면 유연하지 못함
    • 구현이 바뀔 때마다 비즈니스 로직이 수정됨 -> 유지보수 어려움

    ❌위반 예시

    public class OrderService {
        private final FileLogger logger = new FileLogger(); // 직접 의존
    }

     

    ✅ DIP 적용 구조

    interface Logger { void write(String msg); }
    
    class FileLogger implements Logger { ... }
    
    class OrderService {
        private final Logger logger;
        public OrderService(Logger logger) { this.logger = logger; }
    }

     

    ➡️ OrderService는 구현이 아닌 Logger라는 추상화에만 의존

     

    🔎 실무 포인트

    • DIP를 위해 꼭 필요한 것 -> 인터페이스 기반 설계 + 의존성 주입(DI)
    • Spring은 기본적으로 DIP가 실현된 구조 (Bean 주입, 인터페이스 기반 프로그래밍)

    🔁 SOLID 전체 요약 도표

    원칙  목적 지키면 안 지키면
    SRP 단일 책임 클래스 하나 = 하나의 역할 변경 꼬이고, 테스트 어려움
    OCP 확장에는 열림, 변경에는 닫힘 기존 코드 변경 없이 저장 if/else 지옥, 사이드 이펙트
    LSP 부모 -> 자식 치환 가능 다형성 유지, 유연한 설계 예상치 못한 동작, 다형성 파괴
    ISP 인터페이스 분리 필요한 기능만 의존 Fat Interface로 유지보수 어려움
    DIP 추상화에 의존 유연하고 테스트 쉬움 구현체에 강결합, 확장 불가

     

    🛠️ 실무 적용 전략

    • 인터페이스 기반으로 설계하고 구현은 DI로 주입
    • 서비스 클래스는 단일 책임을 가지도록 설계
    • 전략 패턴 등으로 조건 분기 대신 다형성 활용
    • 테스트 가능한 구조 = 좋은 설계의 핵심
    • SOLID는 따로따로가 아닌, 서로 연결된 원칙들임

    📚 마무리 : SOLID는 "읽는 것"이 아니라 "디자인에 녹여야" 하는 것

    • SRP -> 변경 이유를 나누자
    • OCP -> 코드는 그대로, 기능만 추가하자
    • LSP -> 진짜 is-a 관계인지 확인하자
    • ISP -> 작은 인터페이스가 더 유연하다
    • DIP -> 구현이 아니라 역할에 집중하자

    🔜 다음 포스팅부터는 Spring에 대해서 깊게 들어가보고자 한다. 먼저 IoC, DI, AOP에 대해서 깊게 들어가보려고 한다. 또한 SOLID와도 연관지어 내용을 다뤄보도록 하겠다.