java 예제로 보는 SOLID 원칙

행행·2023년 9월 26일
0
  • 다음은 CHAT-GPT에서 발췌한 자료입니다.

SOLID는 객체 지향 프로그래밍에서 다섯 가지 중요한 설계 원칙을 나타내는 약어입니다. 이러한 원칙은 소프트웨어를 더 견고하고 유지보수 가능하며 확장 가능하도록 만드는 데 도움을 줍니다. SOLID 원칙은 다음과 같습니다:

  1. 단일 책임 원칙 (Single Responsibility Principle - SRP): 클래스는 하나의 책임만 가져야 하며, 클래스가 변경되어야 하는 이유는 오직 하나여야 합니다. 이것은 클래스가 변경되어야 하는 이유를 최소화하여 클래스를 유지보수하기 쉽게 만듭니다.

  2. 개방-폐쇄 원칙 (Open-Closed Principle - OCP): 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 합니다. 이것은 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 확장할 수 있게 해줍니다.

  3. 리스코프 치환 원칙 (Liskov Substitution Principle - LSP): 하위 클래스는 상위 클래스를 대체할 수 있어야 합니다. 이것은 상속 관계에서 서브클래스가 슈퍼클래스를 대체하면서 프로그램이 정상적으로 동작해야 함을 의미합니다.

  4. 인터페이스 분리 원칙 (Interface Segregation Principle - ISP): 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 합니다. 이것은 인터페이스를 작고 구체적으로 만들어서 클라이언트가 필요한 메서드만 구현하도록 하는 원칙입니다.

  5. 의존성 역전 원칙 (Dependency Inversion Principle - DIP): 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 합니다. 이것은 추상화를 통해 시스템의 유연성을 높이고 결합도를 낮춥니다.

1. SRP 예제코드

  • Single Responsibility Principle (SRP)는 클래스는 하나의 책임만 가져야 하며, 클래스가 변경되어야 하는 이유는 오직 하나여야 한다는 원칙입니다.
// SRP 위반 예제
class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public void calculateSalary() {
        // 급여 계산 로직
    }

    public void generateReport() {
        // 보고서 생성 로직
    }
}

위의 코드에서 Employee 클래스는 두 가지 다른 책임을 가지고 있습니다. 하나는 급여 계산(calculateSalary)이고 다른 하나는 보고서 생성(generateReport)입니다. 이는 SRP를 위반하는 예제입니다.

SRP를 준수하는 코드로 수정하면 다음과 같습니다.

// SRP 준수 예제
class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public void calculateSalary() {
        // 급여 계산 로직
    }
}

class ReportGenerator {
    public void generateReport(Employee employee) {
        // 보고서 생성 로직
    }
}

여기서 Employee 클래스는 급여 계산과 관련된 책임만을 가지며, 보고서 생성은 별도의 ReportGenerator 클래스로 분리되었습니다. 이렇게 하면 각 클래스는 하나의 책임만 갖게 되어 SRP를 준수하게 됩니다.

2. OCP 예제코드

  • 개방-폐쇄 원칙 (Open-Closed Principle, OCP)은 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 폐쇄되어야 한다는 원칙입니다. 이것은 새로운 기능을 추가하거나 확장할 때 기존 코드를 수정하지 않아야 한다는 의미입니다.
// 도형을 그리는 인터페이스
interface Shape {
    void draw();
}

// 원 클래스
class Circle implements Shape {
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// 사각형 클래스
class Rectangle implements Shape {
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

// 그림 그리는 클래스
class Drawing {
    public void drawShape(Shape shape) {
        shape.draw();
    }
}
  • 위의 코드에서 Shape 인터페이스를 구현한 클래스들(Circle과 Rectangle)은 확장에 열려 있습니다. 이것은 새로운 도형 클래스를 추가할 때 기존 코드를 수정하지 않고도 가능합니다. Drawing 클래스는 Shape 인터페이스를 사용하여 도형을 그리는데, 이것은 기존 코드를 수정하지 않고 새로운 도형을 추가할 수 있음을 보여줍니다.

  • 예를 들어, 삼각형 클래스를 추가하려면 다음과 같이 할 수 있습니다.

// 삼각형 클래스
class Triangle implements Shape {
    public void draw() {
        System.out.println("삼각형을 그립니다.");
    }
}

이렇게 하면 Triangle 클래스를 추가하더라도 Drawing 클래스나 기존 도형 클래스를 수정할 필요가 없으므로 OCP를 준수하고 있습니다.

3. LSP 예제코드

  • 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)은 서브타입(subtype)은 언제나 그것의 슈퍼타입(super type)으로 대체할 수 있어야 한다는 원칙입니다. 즉, 어떤 클래스의 인스턴스가 있을 때 그 클래스의 하위 클래스의 인스턴스로 대체해도 프로그램은 정상적으로 동작해야 합니다.
class Bird {
    public void fly() {
        System.out.println("날아갈 수 있습니다.");
    }
}

class Sparrow extends Bird {
    // Sparrow는 Bird를 확장하면서 추가적인 행위를 정의하지 않음
}

class Ostrich extends Bird {
    public void fly() {
        // 타조는 날지 못하므로 오버라이딩하여 구현을 변경
        System.out.println("날지 못합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird sparrow = new Sparrow();
        Bird ostrich = new Ostrich();

        sparrow.fly(); // "날아갈 수 있습니다." 출력
        ostrich.fly(); // "날지 못합니다." 출력
    }
}
  • 위의 코드에서 Bird 클래스는 fly 메서드를 가지고 있으며, Sparrow 클래스는 이 메서드를 오버라이딩하지 않고 상속합니다. 반면에 Ostrich 클래스는 fly 메서드를 오버라이딩하여 새로운 구현을 제공합니다. 그러나 Sparrow와 Ostrich 모두 Bird 타입의 객체로 대체할 수 있으며, 프로그램은 정상적으로 작동합니다. 이것이 Liskov 치환 원칙을 따르는 예제입니다.

LSP를 준수하면 프로그램의 유연성이 향상되며, 다형성을 활용하여 코드를 더 쉽게 확장하고 유지보수할 수 있게 됩니다.

4. ISP 예제코드

  • 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다. 이것은 한 인터페이스가 여러 개의 작은 인터페이스로 분리되어야 함을 의미합니다.
// ISP 위반 예제
interface Worker {
    void work();
    void eat();
}

class Human implements Worker {
    public void work() {
        // 일하는 로직
    }
    
    public void eat() {
        // 식사하는 로직
    }
}

class Robot implements Worker {
    public void work() {
        // 일하는 로직
    }
    
    public void eat() {
        // 로봇은 먹지 않는데도 먹는 메서드를 구현해야 함
    }
}
  • 위의 코드에서 Worker 인터페이스는 두 가지 메서드인 work와 eat을 가지고 있습니다. 그런데 Robot 클래스는 eat 메서드를 구현해야 하는데, 로봇은 먹지 않으므로 이는 ISP를 위반하는 예제입니다.

  • ISP를 준수하는 코드로 수정하면 다음과 같습니다:

// ISP 준수 예제
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() {
        // 일하는 로직
    }
    
    public void eat() {
        // 식사하는 로직
    }
}

class Robot implements Workable {
    public void work() {
        // 일하는 로직
    }
}
  • 위의 코드에서 Worker 인터페이스를 Workable과 Eatable로 분리했습니다. 이렇게 하면 클라이언트는 자신이 필요로 하는 메서드만을 구현하면 되므로 ISP를 준수합니다. Robot 클래스는 Workable 인터페이스만 구현하면 되므로 eat 메서드를 구현할 필요가 없습니다.

5. DIP 예제코드

  • 의존성 역전 원칙 (Dependency Inversion Principle, DIP)은 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙입니다. 이는 소스 코드의 의존성이 추상화에 의해 정의되어야 하며, 구체적인 구현에 의존해서는 안 된다는 것을 의미합니다.

아래의 Java 코드 예제는 DIP를 준수하는 예제를 보여줍니다. 이 예제에서는 LightBulb 클래스가 Switch 인터페이스에 의존하고 있습니다.

// 저수준 모듈
class LightBulb {
    public void turnOn() {
        System.out.println("전구가 켜집니다.");
    }

    public void turnOff() {
        System.out.println("전구가 꺼집니다.");
    }
}

// 고수준 모듈
interface Switch {
    void operate();
}

class RemoteControl implements Switch {
    private LightBulb bulb;

    public RemoteControl(LightBulb bulb) {
        this.bulb = bulb;
    }

    public void operate() {
        bulb.turnOn();
    }
}
  • 위의 코드에서 RemoteControl 클래스가 LightBulb 클래스에 직접 의존하고 있으므로 DIP를 위반하는 예제입니다. 이를 DIP 원칙을 준수하는 방식으로 수정하겠습니다.
// 저수준 모듈
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    public void turnOn() {
        System.out.println("전구가 켜집니다.");
    }

    public void turnOff() {
        System.out.println("전구가 꺼집니다.");
    }
}

// 고수준 모듈
interface Switch {
    void operate();
}

class RemoteControl implements Switch {
    private Switchable device;

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

    public void operate() {
        device.turnOn();
    }
}
  • 이제 RemoteControl 클래스는 Switchable 인터페이스에 의존하고 있으며, LightBulb 클래스가 이 인터페이스를 구현하므로 DIP를 준수하고 있습니다. 이렇게 하면 고수준 모듈(RemoteControl)이 저수준 모듈(LightBulb)에 직접 의존하지 않고 추상화(Switchable)에 의존하게 되어, 시스템이 더 유연하고 확장 가능해집니다.
profile
성장하려고 분투 중인 개발자

0개의 댓글