Notice
Recent Posts
Recent Comments
Link
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

코딩 이래요래

SOLID 원칙 본문

JAVA

SOLID 원칙

강범호 2025. 4. 7. 09:32

객체지향 설계의 SOLID 원칙

객체지향 설계에서 유지보수성과 확장성을 높이기 위해 지켜야 하는 5가지 원칙


🔸 1. SRP (단일 책임의 원칙, Single Responsibility Principle)

  • 한 클래스는 오직 하나의 책임만 가져야 한다.
  • 클래스가 하나 이상의 책임을 가지면 변경에 취약해지고 유지보수가 어렵다.

❌ 잘못된 예시 (책임 혼합):

class Product {
    String name;
    int price;

    void payment(); // 결제 기능
    void printReceipt(); // 영수증 출력 기능
}

 

 SRP를 적용한 예시 (책임 분리):

class Product {
    String name;
    int price;
}

class PaymentService {
    void payment(Product product); // 결제 책임만 수행
}

class ReceiptPrinter {
    void printReceipt(Product product); // 영수증 출력 책임만 수행
}
  • 클래스별 하나의 책임만 존재하므로 변경사항이 있을 때 유연한 대처가 가능하다.
  • 유지보수 및 확장성이 높아진다.

* SRP를 적용하면 유지보수  확장성을 높이고, 기본적인 객체지향(OOP) 설계 원칙 중 하나임

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

  • 소프트웨어 요소는 확장에는 열려 있고(Open), 수정에는 닫혀(Close) 있어야 한다.
  • 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있도록 설계한다.
  • 핵심 메커니즘은 추상화와 다형성을 사용하는 것이다.

❌ 잘못된 예시 (확장 시 기존 코드 변경):

class Payment {
    void pay(String type) {
        if (type.equals("cash")) {
            // 현금 결제
        } else if (type.equals("card")) {
            // 카드 결제
        }
    }
}

 

 OCP를 적용한 예시 (확장 가능 구조):

interface PaymentService {
    void pay();
}

class CashPayment implements PaymentService {
    @Override
    public void pay() {
        // 현금 결제
    }
}

class CardPayment implements PaymentService {
    @Override
    public void pay() {
        // 카드 결제
    }
}

class PaymentProcessor {
    private PaymentService paymentService;

    public PaymentProcessor(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void process() {
        paymentService.pay(); // 구현체에 따라 동작 다름
    }
}
  • 새로운 결제 방법 추가 시 PaymentService 인터페이스 구현 클래스만 추가하면됨
  • 기존 코드는 변경하지 않아도됨
  • 기존 PaymentsService는 수정하지 않고(close), 새로운 클래스를 만들어 확장(open)

* OCP를 적용하면 새로운 요구사항에도 기존 클래스를 매번 수정하는 위험과 유지보수 비용을 낮출 수 있음

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

  • 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야함
    • 상위 클래스 위치에 하위 클래스로 바꿔도 역할을 하는데 문제가 없어야 한다는 의미
  • 하위 클래스는 상위 클래스의 행동을 변경하거나 제한해선 안 됨
    • 하위 클래스가 상위 클래스의 메소드를 오버라이딩 하더라도 기존의 상위 클래스의 메소드의 정의를 위반해서는 안됨

⚠️ 주의할 두 가지 조건:

  • 사전조건 강화 금지: 자식 클래스에서 부모의 조건보다 더 엄격한 조건을 추가하면 안 됨
// 잘못된 예시
class TopClass {
    void up(int count) {
        // 처리
    }
}

class BottomClass extends TopClass {
    @Override
    void up(int count) {
        if (count > 50) return; // 조건 강화(잘못된 사례)
    }
}
  • 사후조건 약화 금지: 자식 클래스에서 부모보다 약한 결과값(예: null)을 반환하면 안 됨
// 잘못된 예시
class TopClass {
    String print() {
        return "10";
    }
}

class BottomClass extends TopClass {
    @Override
    String print() {
        return null; // 조건 약화(잘못된 사례)
    }
}

 

 올바른 상속 관계의 예시 (LSP 준수):

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

class Cat implements Animal {
    public void makeSound() {
        System.out.println("야옹!");
    }
}

// 모든 하위 클래스는 Animal 인터페이스의 규약을 지킨다.

 

💡 핵심:

  • 상속은 반드시 is-a 관계일 때만 사용하자
  • 그 외의 경우에는 앞에서 학습한 컴포지션(Compositon)을 이용해 기존의 메소드는 그대로 이용하면서 결합도를 낮추자

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

  • 하나의 큰 인터페이스보다는, 목적과 역할에 따라 인터페이스를 잘게 나누는 원칙
    • 범용적인 인터페이스를 구현하고 설계하면 이 인터페이스를 구현하는 클래스는 사용하지 않는 기능들에 의존하게 되어 변경이 어려워 짐
  • 클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않아야함

❌ 잘못된 예시 (너무 많은 기능):

interface Device {
    void print();
    void scan();
    void fax();
}

 

 ISP를 적용한 예시 (역할별 인터페이스 분리):

interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

interface Fax {
    void fax();
}

class MultiFunctionPrinter implements Printer, Scanner, Fax {
    public void print() { /* 출력 구현 */ }
    public void scan() { /* 스캔 구현 */ }
    public void fax() { /* 팩스 구현 */ }
}

class SimplePrinter implements Printer {
    public void print() { /* 출력만 구현 */ }
}
  • 필요한 기능만 선택적으로 구현 가능하여 불필요한 의존성 제거
  • SRP와 연결되어 유지보수성도 향상됨

🔸 5. DIP (의존관계 역전 원칙, Dependency Inversion Principle)

  • 고수준 모듈은 저수준 모듈에 직접 의존하지 않고, 추상화(인터페이스 또는 추상 클래스)에 의존해야 함
  • 구현 클래스가 아닌 인터페이스나 추상 클래스에 의존하여 결합도를 낮춘다

❌ DIP 위반 예시 (강한 결합):

class EmailService {
    public void sendEmail(String message) {
        System.out.println("Email sent: " + message);
    }
}

class Notification {
    private EmailService emailService;

    public Notification() {
        emailService = new EmailService(); // 직접 의존
    }

    public void alert(String message) {
        emailService.sendEmail(message);
    }
}

 

 DIP를 적용한 예시 (인터페이스에 의존):

interface MessageService {
    void sendMessage(String message);
}

class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("Email sent: " + message);
    }
}

class SMSService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("SMS sent: " + message);
    }
}

class Notification {
    private MessageService messageService;

    public Notification(MessageService messageService) {
        this.messageService = messageService; // 인터페이스에만 의존
    }

    public void alert(String message) {
        messageService.sendMessage(message); // 구현체와는 결합하지 않음
    }
}
  • MessageService를 interface class로 변경
  • Notification 클래스는 MessageService 인터페이스에 의존하므로 DIP 원칙 준수
  • EmailService, SMSService 등 구현체를 쉽게 교체할 수 있음
  • 확장성과 재사용성이 증가하고, 코드 수정 없이 새로운 구현체 추가 가능

💡 핵심:

  • 구현체 변경 시 코드 수정 없이 자유롭게 추가 / 확장이 가능
  • DIP는 결합도를 낮춰 유연한 설계를 가능하게 함

해당 게시글은 학습한 내용을 정리하고 복습하며 정리한 글이므로 틀린 부분이 있을 수 있음.

'JAVA' 카테고리의 다른 글

자료구조 - List  (0) 2025.04.13
자료구조  (0) 2025.04.13
객체지향 프로그래밍(OOP)의 4가지  (0) 2025.04.01
JVM의 메모리 구조  (0) 2025.03.31
Java의 클래스와 객체  (0) 2025.03.31