[Spring] SOLID 원칙

수빈·2024년 11월 23일

Spring

목록 보기
5/5

오늘은 자바를 사용하는 개발자가 알아야 하는 객체 지향 설계의 5가지 원칙인 SOLID 원칙에 대해서 포스팅 해보려고 한다.

SOLID 원칙이란 ?

SOLID 원칙은 로버트 C.마틴이 제시한 객체지향 설계의 원칙이다. SOLID는 다섯가지 원칙의 앞글자를 따서 만들어졌으먀, 다음과 같은 의미를 가진다.

  • S: 단일 책임 원칙 (Single Responsibility Principle, SRP)
  • O: 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
  • L: 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
  • I: 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  • D: 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

이제 각각의 원칙에 대해서 알아보도록 하겠다 !



단일 책임 원칙 (SRP)

"하나의 클래스는 하나의 책임만 가져야 한다"

한 클래스는 단일 기능 또는 하나의 목적만 수행해야 한다. 만약 여러가지 일을 처리하려고 하면, 코드가 복잡해지고 수정하기가 어려워지게 된다.

예시: 나쁜 예

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

    public void sendEmail() {
        // 이메일 전송 로직
    }
}

위의 코드에서 ReportManager라는 클래스는 보고서를 생성하는 것과 이메일을 전송하는 두 가지 일을 처리하게 된다. 이는 클래스의 책임이 분산된 상태이다.


수정 : 좋은 예시

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

public class EmailSender {
    public void sendEmail() {
        // 이메일 전송 로직
    }
}

이렇게 역할별로 분리를 하게 되면 각각 클래스는 하나의 책임만을 가지므로 유지보수가 쉬워진다.



개방-폐쇄 원칙 (OCP)

"확장에는 열려 있고, 수정에는 닫혀 있어야 한다."

코드를 수정하지 않고도 기능을 확장할 수 있도록 설계해야 한다. 즉, 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있어야 한다.

예시 : 나쁜 예시

public class PaymentProcessor {
    public void processPayment(String type) {
        if (type.equals("creditCard")) {
            System.out.println("Processing credit card payment");
        } else if (type.equals("paypal")) {
            System.out.println("Processing PayPal payment");
        }
    }
}

위의 코드에서 creditCard, paypal 이 아닌 새로운 결제 방식을 추가하려면 기존 코드를 수정해야 한다. 이를 아래와 같이 인터페이스 형태로 개선을 하게 되면 아래와 같은 코드로 작성할 수 있다.

public interface Payment {
    void processPayment();
}

public class CreditCardPayment implements Payment {
    public void processPayment() {
        System.out.println("Processing credit card payment");
    }
}

public class PayPalPayment implements Payment {
    public void processPayment() {
        System.out.println("Processing PayPal payment");
    }
}

public class PaymentProcessor {
    public void processPayment(Payment payment) {
        payment.processPayment();
    }
}

새로운 결제방식이 추가된다면 Payment 인터페이스를 구현하기만 하면 되므로 기존 코드를 수정할 필요가 없게 된다.



리스코프 치환 원칙 (LSP)

"하위 클래스는 반드시 상위 클래스의 행동을 대체할 수 있어야 한다."

하위 클래스가 상위 클래스의 역할을 완전히 대신할 수 있어야 하며, 프로그램의 동작은 변하지 않아야 한다.

예시 : 나쁜 예시

public class Rectangle {
    protected int width, height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = this.height = height;
    }
}

SquareRectangle을 상속했지만, Rectangle의 동작을 완전히 대체할 수 없다.
예를 들어, SquaresetWidth(5)를 호출한 후 setHeight(10)을 호출하면 정사각형의 특성이 깨집니다.


개선 : 좋은 예 ```java public interface Shape { int getArea(); }

public class Rectangle implements Shape {
private int width, height;

public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
}

public int getArea() {
    return width * height;
}

}

public class Square implements Shape {
private int side;

public Square(int side) {
    this.side = side;
}

public int getArea() {
    return side * side;
}

}

이제 `Rectangle`과 `Square`는 독립적인 클래스이며, 서로의 동작을 침해하지 않는다.

<br><br>

## 인터페이스 분리 원칙(ISP)
**"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."**

큰 인터페이스를 여러 개의 작은 인터페이스로 나누어, 사용하지 않는 기능을 구현하지 않아도 되게 만든다. 

예시 : 나쁜 예시
```java
public interface Worker {
    void work();
    void eat();
}

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

    @Override
    public void eat() {
        throw new UnsupportedOperationException("Robot doesn't eat");
    }
}

Roboteat 메서드를 사용할 필요가 없지만, 인터페이스를 구현하느라 어쩔 수 없이 작성을 하게 됩니다.


개선

public interface Worker {
    void work();
}

public interface Eater {
    void eat();
}

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

public class Human implements Worker, Eater {
    @Override
    public void work() {
        System.out.println("Human is working");
    }

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

이렇게 작성하면 각각의 클래스는 필요한 인터페이스만 구현하면 되므로 불필요한 메서드 작성이 줄어들게 된다.



의존성 역전 원칙 (DIP)

"고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다."

구체적인 구현이 아닌, 추상화(인터페이스나 추상 클래스)에 의존하도록 설계해야 한다.

예시 : 나쁜 예시

public class Keyboard {
}

public class Computer {
    private Keyboard keyboard;

    public Computer() {
        this.keyboard = new Keyboard();
    }
}

ComputerKeyboard라는 구체적인 구현에 의존하므로, 다른 입력 장치(ex. 마우스 등등,,)를 추가하기 어렵다.


개선

public interface InputDevice {
}

public class Keyboard implements InputDevice {
}

public class Computer {
    private InputDevice inputDevice;

    public Computer(InputDevice inputDevice) {
        this.inputDevice = inputDevice;
    }
}

이제 InputDevice 인터페이스를 통해 입력 장치를 추상화 하였으므로, 키보드 외에도 다른 입력 장치를 쉽게 추가할 수 있다.



SOLID 원칙의 중요성

  • 유지보수성 : 코드를 수정할 때 다른 부분에 영향을 미치지 않음
  • 확장성 : 새로운 요구사항을 추가할 때 기존 코드를 수정하지 않음
  • 재사용성 : 코드가 모듈화되어 재사용이 용이함





새로운 프로젝트의 설계단계에 이 SOLID 원칙을 적용한다면 깔끔한 코드를 작성할 수 있을 것 같다.

profile
Development History

0개의 댓글