Post

02 SOLID

02 SOLID

1. SOLID 원칙

1. SOLID 원칙

  • 유지보수가 쉽고, 확장 가능하며, 오류가 적은 소프트웨어를 만들기 위한 설계 지침
  • SOLID 원칙은 ‘좋은 객체지향 설계의 뼈대’이며, 소프트웨어 품질과 생산성을 높이기 위한 기본 철칙

2. SOLID의 시작

배경 요소내용
복잡한 대규모 소프트웨어 개발유지보수와 확장성의 어려움 증가
객체지향 설계의 표준 부재다양한 설계 방식으로 인한 혼란과 비효율성
변화에 유연한 설계 요구변경이 잦은 현실적 요구사항 대응 필요
경험 기반 연구 및 실용화Robert C. Martin이 여러 설계 규칙을 체계적으로 정리함

3. SOLID 원칙의 종류

원칙해결하려는 문제공학적 설명 (CS 관점)
S - 단일 책임 원칙 (SRP)하나의 클래스가 여러 이유로 바뀌는 것 → 변경에 취약책임 분리 → 응집도↑, 변경 영향도↓
O - 개방/폐쇄 원칙 (OCP)기존 코드를 직접 수정해야만 기능 추가 가능기존 코드는 닫고, 확장만 열어둬서 안정성 확보
L - 리스코프 치환 원칙 (LSP)하위 클래스가 부모 역할을 제대로 못함 → 버그다형성이 깨짐 → 계약 기반 설계(contract-based design)
I - 인터페이스 분리 원칙 (ISP)인터페이스가 지나치게 비대함 → 불필요한 구현 강제역할 최소화된 인터페이스로 분리
D - 의존 역전 원칙 (DIP)하위 모듈에 상위 모듈이 직접 의존 → 결합도 ↑추상화 계층에 의존 → DI 컨테이너와 잘 결합됨

2. 역할과 책임

1. 역할(Role)

  • 객체가 전체 시스템 안에서 “어떤 일을 맡고 있는가”.
  • 다른 객체들과의 관계 속에서 맡은 위치.
  • 역할은 객체가 무엇을 할 수 있는지를 정의하는 계약(Interface)이나 사용 패턴과 연결됨.
  • 역할 중심으로 설계하면 객체 간 협력과 재사용이 쉬워짐.

2. 계약

  • 객체가 특정 역할(Role)을 수행할 때, 다른 객체와 어떤 방식으로 상호작용할지에 대한 약속
  • “이 메서드는 어떤 입력을 받으면 어떤 행동을 해야 하며, 어떤 결과를 반환해야 한다.”
  • 인터페이스에서 메소드를 정의한 것과 같음

3. 책임(Responsibility)

  • 객체(또는 클래스)가 “무엇을 해야 하는가”.
  • “어떤 기능을 담당하고 어떤 의무를 가지고 있는가”를 의미.
  • 클래스 하나가 너무 많은 일을 하지 않도록 역할을 작게 나누는 것.

4. 역할(Role)과 책임(Responsibility) 예시

1
2
3
4
// 메일을 보내는 역할 < 시스템상 포지션 >
public interface Notifier { 
    void send(String message); // send() 메서드를 제공해야 한다(계약)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 각기 구현체는 책임을 짐. < 해당 포지션이 수행하는 실질 업무 > 
public class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        // 책임: 이메일 전송
        System.out.println("Send email: " + message);
    }
}

public class SMSNotifier implements Notifier {
    @Override
    public void send(String message) {
        // 책임: 문자 전송
        System.out.println("Send SMS: " + message);
    }
}

3. SOLID 원칙 종류

1. SRP ( 단일 책임 원칙 )

1. 기능

  • 상황 : 리포트 데이터를 관리하는 기능
  • 개선전 : 리포트의 특정 기능을 조정하면 전체 영향을 줄 수 있음.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    public class Report {
      private String content;
    
      public Report(String content) { this.content = content; }
    
      public String getContent() { return content; }
    
      public void printReport() { System.out.println(content);  // 출력 책임 }
    
      public void saveToFile(String filename) { // 파일 저장 책임 }
    
      public void sendEmail(String address) { // 이메일 전송 책임 }
    }
    
  • 개선 후
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    // 보고서 데이터만 책임
    public class Report {
      private String content;
      public Report(String content) { this.content = content; }
      public String getContent() { return content; }
    }
    
    // 출력 담당
    public class ReportPrinter {
      public void print(Report report) {
          System.out.println(report.getContent());
      }
    }
    
    // 저장 담당
    public class ReportSaver {
      public void saveToFile(Report report, String filename) {
          // 파일로 저장하는 코드
      }
    }
    

    2. check 🔍

  • 하나의 클래스가 데이터 처리, 출력, 저장, 로깅 등 서로 다른 기능을 동시에 담당하고 있다면, 한 기능이 바뀌면 다른 기능도 영향을 받을 수가 있어서 조정이 어려움.
  • 따라서 각 클래스마다 단일 기능으로 만들어, 해당 역할의 책임이 있는 클래스만 건드릴 수 있도록 하는게 유리함.

2. OCP (개방-폐쇄 원칙)

1. 기능

  • 상황 : 알림 기능에 이메일, 문자뿐 아니라 푸시 알림도 추가 필요.
  • 개선전 : 새로운 알림 수단 추가 때마다 if-else문을 수정해야 함
    1
    2
    3
    4
    5
    6
    7
    8
    
     class Notification {
       void notify(String type, String msg) {
         if (type.equals("email")) { // 이메일 발송
         } else if (type.equals("sms")) { // 문자 발송
         }
       }
      }
    
    
  • 개선후
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      interface Notifier { void send(String msg); }
    
      class EmailNotifier implements Notifier {
         public void send(String msg) { /* 이메일 발송 */ }
      }
    
      class SMSNotifier implements Notifier {
         public void send(String msg) { /* 문자 발송 */ }
      }
    
      class PushNotifier implements Notifier {
         public void send(String msg) { /* 푸시 알림 발송 */ }
      }
    
      class Notification {
          private Notifier notifier;
          Notification(Notifier notifier) { this.notifier = notifier; }
          void notify(String msg) { notifier.send(msg); }
       }
    

2. check 🔍

  • 기능이 추가 또는 변경 될 경우에, 다른 클래스에 영향을 줄이기 위함.
  • 추상화를 통해서 공통적인 역할을 정하고, 새로운 기능들을 상속이나 구현을 통해서 내부 구현만 확장하여 사용할 수 있도록 함.
  • 기능이 추가되거나 변경될 경우 기존 클래스의 수정을 최소화하고, 다른 클래스나 모듈에 영향을 주지 않도록 설계하는 원칙

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

1. 기능

  • 상황 : 조류(Bird) 클래스를 상속받아 타조(Ostrich) 클래스를 만들었는데, 날지 못하는 타조 때문에 문제가 발생
  • 개선전 : 부모 클래스가 제공하는 행위를 자식이 수행하지 못함
    1
    2
    3
    4
    5
    6
    7
    8
    
    class Bird {
       void fly() { System.out.println("날아간다"); }
    }
    
    class Ostrich extends Bird {
       void fly() { throw new UnsupportedOperationException("타조는 못 날아"); }
    }
    
    
  • 개선후
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface Bird {
      void move(); // 날다 -> 이동 으로 변경함(더 추상적인 부분으로)
    }
    
    class FlyingBird implements Bird {
      public void move() { System.out.println("날아서 이동"); //나는 새는 날아서 }
    }
    
    class Ostrich implements Bird {
      public void move() { System.out.println("걸어서 이동"); //걷는 새는 걸어서 }
    }
    

    2. check 🔍

  • 객체가 부모 클래스를 상속받았을때, 부모의 역할을 온전히 대체하지 못한다면, 신뢰할 수 없는 구조가됨. 그러면 OOP가 가진 유지보수성과 확장성의 장점이 무너지기 때문에, 자식 클래스가 부모 클래스를 대체해도 프로그램의 의미와 기능이 유지되어야함.

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

1. 기능

  • 상황 : 복합기 장치(MultiFunctionDevice)를 구현하는데, 단순 프린터(SimplePrinter)는 스캔과 팩스 기능이 없어서 문제
  • 개선전 : 필요 없는 메서드까지 강제 구현해야함.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface MultiFunctionDevice {
      void print();
      void scan();
      void fax();
    }
    
    class SimplePrinter implements MultiFunctionDevice {
      public void print() { /* 프린터 기능 */ }
      public void scan() { throw new UnsupportedOperationException(); }
      public void fax() { throw new UnsupportedOperationException(); }
    }
    
  • 개선후 : 필요한 기능만 구현
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    interface Printer { 
      void print(); // 기능 분리
    }  
    
    interface Scanner {
      void scan(); // 기능 분리
    }
    
    interface Fax {
      void fax(); // 기능 분리
    }
    
    class SimplePrinter implements Printer { 
      public void print() { /* 프린터 기능 */ }
    }
    

    2. check 🔍

  • 비대하고 다기능적인 인터페이스는 자식 클래스가 구현했을때 불필요한 기능의 구현을 요구하게 될 수 있고, 인터페이스의 역할이 늘어날 경우 기존에 구현했던 클래스도 사용하지 않아도 추가 구현해야하는 상황이 생김.
  • 따라서 많은 역할을 주어진 인터페이스 구조보다는 역할을 나누어서 단계를 나누는 방법등을 활용하여 필요한 것만 구현할 수 있도록 함.

5. DIP (의존 역전 원칙)

1. 기능

  • 상황 : UserService가 직접 MySQLDatabase 클래스를 생성해 DB에 저장하는 구조
  • 개선전 : 구체 클래스에 직접 의존해 유연성 떨어짐.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    class MySQLDatabase {
         void save(String data) { /* 저장 */ }
     }
    
     class UserService {
        private MySQLDatabase db = new MySQLDatabase();
        void saveUser(String user) {
             db.save(user);
         }
      }
    
  • 개선후 : 인터페이스에 의존해 유연성 상승.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
     interface Database {
        void save(String data);
     }
    
     class MySQLDatabase implements Database {
         public void save(String data) { /* 저장 구현 */ }
     }
    
     class UserService {
         private Database db;
    
         UserService(Database db) {
             this.db = db;
     }
    
        void saveUser(String user) {
             db.save(user);
        }
    }
    

    2. check 🔍

  • 클래스가 직접 다른 클래스의 구체적인 구현을 생성하거나 사용하면, 그 구현한 클래스가 수정될 경우 함께 변경해야 하는 상황에 놓이게됨.
  • 따라서 인터페이스, 추상클래스등에 의존하여 역할에 따른 개발을 하여, 결합도를 낮추는 방향으로 조정해야함.

5. SOLID 🔍

  • 솔리드원칙은
    • SRP는 클래스를 작게 만들고
    • OCP는 확장을 가능하게 하며
    • LSP는 상속의 안정성을 보장하고
    • ISP는 인터페이스도 SRP처럼 분리하며,
    • DIP는 이 모든 구조를 느슨하게 연결함.
  • SOLID 원칙은 변화에 유연하고, 유지보수가 쉬운 소프트웨어 설계 구조를 만들기 위해서 하는 원칙으로 지속적인 변화의 대응을 중심에 두고, 객체지향의 유연성과 재사용성을 끌어올려서 유지보수와 확장에 능동적으로 대처하기 위한 방법
  • SOLID는 소프트웨어의 지속 가능한 성장과 변화 대응을 위한 구조를 설계하는 방법
This post is licensed under CC BY 4.0 by the author.