이 영역을 누르면 첫 페이지로 이동
AlwaysLikeNewbie 블로그의 첫 페이지로 이동

AlwaysLikeNewbie

페이지 맨 위로 올라가기

AlwaysLikeNewbie

always-like-newbie 님의 블로그 입니다.

객체지향 디자인 패턴의 첫 걸음, SOLID 원칙!

  • 2024.10.25 19:43
  • Computer Science/객체지향 디자인 패턴 (OODP)

 

0. 들어가며


오랜 시간동안 수많은 개발자들은 효율적이고, 견고하며, 유지보수가 용이한 프로그램을 만들기 위해 고민했습니다. 그 결과, 객체지향 디자인 패턴의 SOLID 원칙이 탄생했습니다.
지금부터, SOLID 원칙을 학습하며 객체지향 디자인 패턴의 첫 걸음을 떼어 봅시다!

 

이 글은 제가 애청하는 얄코님의 'SOLID 원칙 - 객체지향 디자인 패턴의 기본기' 의 내용을 사전 동의 하에 정리한 글입니다. 어려운 내용을 정말 쉽게 가르쳐 주시기 때문에, OODP에 관심 많으신 분들께 강력하게 추천드립니다.

 

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


단일 책임 원칙은 말 그대로, 한 클래스는 하나의 책임만 가지고 있어야 한다는 의미입니다.
만약 한 클래스가 여러 메서드를 가지고 있다면, 이들을 통해 수행하는 대표적인 업무가 하나이어야 합니다.

 

지금부터, 예제를 살펴보겠습니다.

public class UserService {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }

    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }

    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

 

단일 책임 원칙 AS-IS 예제의 Class diagram

 

UserService 클래스는 User를 DB에 저장하고, User에게 웰컴 메일을 보내고, User의 활동을 기록하는 역할을 맡고 있습니다.
하나의 클래스가 여러 역할을 맡고 있어 기능이 늘어날수록 유지보수가 어려워지는 단점이 있습니다.

 

이를 해결하기 위해 다음과 같이 코드를 개선할 수 있습니다.

public class UserRepository {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }
}

public class UserActivityLogger {
    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

public class UserService {
    private UserRepository userRepository = new UserRepository();
    private EmailService emailService = new EmailService();
    private UserActivityLogger userActivityLogger = new UserActivityLogger();

    public void registerUser(User user) {
        userRepository.saveUser(user);
        emailService.sendWelcomeEmail(user);
        userActivityLogger.logUserActivity(user);
    }
}

 

단일 책임 원칙 TO-BE 예제의 Class diagram

 

UserService 클래스는 User를 등록하는 역할을 맡고, 세부 작업(User 저장, 웰컴 메일 보내기, User 활동 기록)은 해당 역할을 수행하는 다른 클래스에게 맡기는 것을 볼 수 있습니다.
이렇게 하면 요구사항 변경이 생겨도 해당 클래스만 변경하면 되기 때문에 유지보수에 유리합니다.

 

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


개방-폐쇄 원칙은 각 클래스가 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙입니다.
쉽게 말해, 클래스를 수정하지 말고 확장해서 사용하라는 의미입니다.

 

예제 코드를 살펴봅시다.

public class ReportGenerator {
    public void generateReport(String type) {
        if (type.equals("PDF")) {
            System.out.println("Generating PDF report...");
        } else if (type.equals("HTML")) {
            System.out.println("Generating HTML report...");
        }
        // If we need to add another format, we have to modify this method.
    }
}

 

ReportGenerator 클래스는 타입이 PDF인지 혹은 HTML인지에 따라 해당하는 레포트를 생성합니다.
만약 이후에 XML 등 다른 타입의 문서를 생성해야 한다면 어떻게 될까요?
아래 if문을 계속 늘려가야 할 것입니다. 그럴수록 코드는 점점 가독성이 낮아지고 유지보수하기 어려워질 것입니다.

또한, 개발자가 실수로 기존 코드를 수정하여 버그가 발생할 위험도 있습니다.

 

이를 해결하기 위해 다음과 같이 코드를 개선할 수 있습니다.

public interface Report {
    void generate();
}

public class PDFReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating PDF report...");
    }
}

public class HTMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating HTML report...");
    }
}

public class XMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating XML report...");
    }
}

public class Main {
    public static void main(String[] args) {
        Report pdfReport = new PDFReport();
        pdfReport.generate();  // Generating PDF report...

        Report htmlReport = new HTMLReport();
        htmlReport.generate();  // Generating HTML report...

        Report xmlReport = new XMLReport();
        xmlReport.generate();  // Generating XML report...
    }
}

 

개방 폐쇄 원칙 TO-BE 예제의 Class diagram

 

Report 인터페이스를 정의하고, 각 타입마다 이를 구현합니다.
이제, 새로운 타입이 추가되면 Report 인터페이스를 구현한 새로운 클래스만 추가하면 됩니다.
이전보다 훨씬 유지보수하기 쉬운 코드로 개선되었네요.

 

3. 리스코프 치환 원칙 (Liskov substitution Principle)


리스코프 치환 원칙은 자식 클래스는 언제나 부모 클래스를 치환할 수 있어야 한다는 의미입니다.
쉽게 말해, 자식은 최소한 부모가 하는 일은 다 해야 한다는 의미입니다.

 

예제 코드를 살펴봅시다.

// Parent class Bird
public class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

// Child class Penguin that violates LSP
public class Penguin extends Bird {
    @Override
    public void fly() {
        // Penguins cannot fly
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly(); // Bird is flying

        Bird penguin = new Penguin();
        penguin.fly(); // Throws UnsupportedOperationException
    }
}

 

리스코프 치환 원칙 AS-IS 예제의 Class diagram

 

펭귄은 새의 한 종류입니다. 따라서, 위와 같은 상속 관계는 맞는 것처럼 보입니다.
하지만 Penguin 클래스는 부모인 Bird 클래스의 fly 메서드를 수행할 수 없습니다. 따라서, 이는 틀린 설계입니다.

 

이는 Flyable이라는 독립적인 인터페이스를 분리하고, Bird의 자식 클래스 중 날 수 있는 클래스에만 적용하여 해결할 수 있습니다.
이제, Penguin 클래스를 사용하는 클라이언트 코드 입장에서는 실수로 Exception을 발생시킬 위험을 없앨 수 있습니다.

// Interface for birds that can fly
public interface Flyable {
    void fly();
}

// Base class Bird
public class Bird {
    public void eat() {
        System.out.println("Bird is eating");
    }
}

// Class for a bird that can fly
public class Sparrow extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

// Class for a bird that cannot fly
public class Penguin extends Bird {
    // Penguins do not implement Flyable
}

public class Main {
    public static void main(String[] args) {
        Bird sparrow = new Sparrow();
        sparrow.eat(); // Bird is eating
        ((Flyable) sparrow).fly(); // Sparrow is flying

        Bird penguin = new Penguin();
        penguin.eat(); // Bird is eating
        // ((Flyable) penguin).fly(); // Compilation error, Penguin is not Flyable
    }
}

 

리스코프 치환 원칙 TO-BE 예제의 Class diagram

 

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


인터페이스 분리 원칙은 클래스는 자신이 사용하지 않을 메서드를 구현하도록 강요받지 말아야 한다는 의미입니다.
한 마디로, 인터페이스도 책임에 따라 분리되어야 한다는 뜻입니다.

 

예제를 살펴봅시다.

// Interface for workers
public interface Worker {
    void work();
    void eat();
}

// Class representing a regular worker
public class Employee implements Worker {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

// Class representing a robot
public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }

    @Override
    public void eat() {
        // Robots do not eat
        throw new UnsupportedOperationException("Robots do not eat");
    }
}

public class Main {
    public static void main(String[] args) {
        Worker employee = new Employee();
        employee.work(); // Employee is working
        employee.eat(); // Employee is eating

        Worker robot = new Robot();
        robot.work(); // Robot is working
        robot.eat(); // Throws UnsupportedOperationException
    }
}

 

인터페이스 분리 원칙 AS-IS 예제의 Class diagram

 

Robot 클래스는 eat 메서드를 수행하지 못 합니다. 하지만 Worker 인터페이스를 구현해야 하니, 어쩔 수 없이 eat 메서드를 구현합니다.
만약 클라이언트 코드에서 이 내용을 모르고 Robot 객체에서 eat 메서드를 호출하면 Exception이 발생합니다.
따라서, 이는 틀린 설계입니다.

 

이는 다음과 같이 해결할 수 있습니다.

// Interface for work-related actions
public interface Workable {
    void work();
}

// Interface for eating-related actions
public interface Eatable {
    void eat();
}

// Class representing a regular worker
public class Employee implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

// Class representing a robot
public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }
    // Robot does not implement Eatable interface
}

public class Main {
    public static void main(String[] args) {
        Workable employee = new Employee();
        employee.work(); // Employee is working
        ((Eatable) employee).eat(); // Employee is eating

        Workable robot = new Robot();
        robot.work(); // Robot is working
        // ((Eatable) robot).eat(); // Compilation error, Robot does not implement Eatable
    }
}

 

인터페이스 분리 원칙 TO-BE 예제의 Class diagram

 

기존 Worker 인터페이스에서 구현해야 했던 eat 메서드와 work 메서드를 각각 Eatable, Workable 인터페이스로 분리합니다.
그리고 work만 수행할 수 있는 Robot 클래스는 Workable만 구현합니다.
이를 통해 클라이언트 코드에서 실수로 Exception을 발생시킬 위험을 줄일 수 있습니다.

 

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


의존성 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 된다라는 의미입니다.

지금부터 예제를 통해 설명드리겠습니다.
아래 예제를 보면 Switch 클래스는 Fan 클래스의 인스턴스를 속성으로 포함하고 있고, turnOn/turnOff 메서드 실행 시 Fan 클래스의 spin/stop 메서드를 실행합니다.
따라서, Fan 클래스는 구체적인 동작을 직접 구현하므로 저수준 모듈, Switch 클래스는 이를 제어하는 추상화된 로직을 제공하기 때문에 고수준 모듈이라고 부를 수 있습니다.
이 코드에서는 turnOn/turnOff 메서드 호출 시 spin/stop 메서드가 그대로 호출되기 때문에 고수준 모듈인 Switch 클래스는 저수준 모듈인 Fan 클래스에 의존적입니다.


모든 문제는 여기서 비롯됩니다.

첫 째로, spin/stop 메서드의 이름이나 매개변수가 변경된다면 turnOn/turnOff 메서드도 이에 따라 수정되어야 합니다.
둘 째로, Switch는 Fan 외에 다른 클래스를 다룰 수 없습니다. 한 마디로, 확장성이 없습니다.

// Low-level class
public class Fan {
    public void spin() {
        System.out.println("Fan is spinning");
    }

    public void stop() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Fan fan;

    public Switch(Fan fan) {
        this.fan = fan;
    }

    public void turnOn() {
        fan.spin();
    }

    public void turnOff() {
        fan.stop();
    }
}

 

의존성 역전 원칙 AS-IS 예제의 Class diagram

 

이러한 문제는 다음과 같이 해결할 수 있습니다.

// Interface for switchable devices
public interface Switchable {
    void turnOn();
    void turnOff();
}

// Low-level class implementing the interface
public class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan is spinning");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }

    public void turnOff() {
        device.turnOff();
    }
}

 

의존성 역전 원칙 TO-BE 예제의 Class diagram

 

먼저, turnOn/turnOff 메서드를 구현하는 Switchable 인터페이스를 정의합니다.
저수준 모듈인 Fan 클래스는 이를 구현하고, 고수준 모듈인 Switch 클래스는 Switchable 인터페이스를 속성으로 포함합니다.
이제, Switch와 Fan은 Switchable 인터페이스를 통해 소통하기 때문에 위에서 언급한 수정 문제와 확장성 문제 모두 해결할 수 있습니다.

 

6. 결론 (+ 나의 생각)


저는 5가지 원칙 AS-IS, TO-BE 예제 클래스 다이어그램을 보면서 '인터페이스나 추상 클래스를 매개하여 구상 클래스들끼리 소통하도록 설계해야 하는구나'라는 깨달음을 얻었습니다.

여러분은 어떤 생각을 하셨는지 궁금하네요. 댓글로 남겨주시면 감사하겠습니다!

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

다른 글 더 둘러보기

정보

AlwaysLikeNewbie 블로그의 첫 페이지로 이동

AlwaysLikeNewbie

  • AlwaysLikeNewbie의 첫 페이지로 이동

검색

메뉴

  • Github
  • 홈

카테고리

  • 분류 전체보기 (5)
    • Computer Science (2)
      • 객체지향 디자인 패턴 (OODP) (1)
      • Java (1)
    • 우아한테크코스 (2)
      • 프리코스 (2)
    • ETC (1)

방문자

  • 전체 방문자
  • 오늘
  • 어제

댓글

블로그 구독하기

  • 구독하기
  • RSS 피드

정보

AlwaysLikeNewbie의 AlwaysLikeNewbie

AlwaysLikeNewbie

AlwaysLikeNewbie

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. © AlwaysLikeNewbie. Designed by Fraccino.

티스토리툴바