본 문서에서는 SOLID 원칙을 학습합니다. 각 원칙에 대한 이해를加深하고자 Java 코드 예제를 제공합니다.

SOLID 원칙은 객체 지향 프로그래밍에서 사용되는 다섯 가지 디자인 원칙의 집합입니다. 이 원칙을 준수하면 견고한 소프트웨어를 개발할 수 있으며, 코드를 더 효율적이고, 가독성이 있고, 유지보수하기 쉬워지게 만들어줍니다.

SOLID는 다음을 의미하는 약어입니다:

  • 단일 책임 원칙
  • 개방/封闭 원칙
  • 리스코프 대체 원칙
  • 인터페이스 분리 원칙
  • 의존성 역전 원칙

단일 책임 원칙

단일 책임 원칙은 모든 클래스가 단일한, 명확한 책임을 가져야하며, 변경되는 이유가 단 하나여야 한다고 합니다.

public class Employee{
  public String getDesignation(int employeeID){ // }
  public void updateSalary(int employeeID){ // }
  public void sendMail(){ // }
}

위 예제에서 Employee 클래스는 getDesignationupdateSalary와 같은 몇몇 직원 클래스 전용 동작을 가지고 있습니다.

또한 sendMail라는 다른 메서드도 있고, 이는 Employee 클래스의 책임을 벗어납니다.

이러한 동작은 이 클래스에 고유하게 없으며, 존재하면 단일 책임 원칙을 위반합니다. 이를 극복하려면 sendMail 메서드를 별도의 클래스로 옮기면 됩니다.

다음은 그 방법입니다:

public class Employee{
  public String getDesignation(int employeeID){ // }
  public void updateSalary(int employeeID){ // }
}

public class NotificationService {
    public void sendMail() { // }
}

개방/關閉 원칙

개방/關閉 원칙에 따르면, 구성요소는 확장을 위해 개방되어야 하지만, 수정을 위해는關閉되어야 합니다. 이 원칙을 이해하기 위해, 도형의 면적을 계산하는 클래스의 예를 들어보겠습니다.

public class AreaCalculator(){
  public double area(Shape shape){
    double areaOfShape;
    if(shape instanceof Square){
        // 정사각형의 면적 계산
    } else if(shape instanceof Circle){
        // 원의 면적 계산
    }
    return areaOfShape;
  }

위 예제의 문제는, 미래에 새로운 Shape 유형의 인스턴스가 면적을 계산해야 하는 경우, 추가적인 조건 else-if 블록을 추가하여 위 클래스를 수정해야 합니다. Shape 유형의 새 객체마다 이렇게 할 수 밖에 없습니다.

이를 극복하기 위해, 인터페이스를 생성하고 각 Shape이 이 인터페이스를 구현하도록 할 수 있습니다. 그런 다음, 각 클래스는 자신의 면적을 계산하는 방법을 제공할 수 있습니다. 이렇게 되면 미래에 프로그램을 쉽게 확장할 수 있게 됩니다.

interface IAreaCalculator(){
  double area();
}

class Square implements IAreaCalculator{
  @Override
  public double area(){
    System.out.println("Calculating area for Square");
    return 0.0;
   }
}

class Circle implements IAreaCalculator{
  @Override
  public double area(){
    System.out.println("Calculating area for Circle");
    return 0.0;
   }
}

리스코프 대체 원칙

리스코프 대체 원칙은 superclass 객체를 subclass 객체로 대체할 수 있어야 하며, 프로그램의 올바른 작동을影响力하지 않는다고 합니다.

abstract class Bird{
   abstract void fly();
}

class Eagle extends Bird {
   @Override
   public void fly() { // 일부 구현 }
}

class Ostrich extends Bird {
   @Override
   public void fly() { // 더미 구현 }
}

상기 예제에서 Eagle 클래스와 Ostrich 클래스는 모두 Bird 클래스를 확장하고 fly() 메서드를 오버라이드합니다. 그러나 Ostrich 클래스는 날 수 없으므로 더미 구현을 제공하도록 강제되어 있으며, Bird 클래스 객체를 그것으로 교체하면 동일하게 동작하지 않습니다.

이는 Liskov 치환 원칙을 위반합니다. 이를 해결하려면 날 수 있는 새들을 위한 별도의 클래스를 생성하고 Eagle 클래스가 그것을 확장하게 하고, 다른 새들은 fly 동작을 포함하지 않는 다른 클래스를 확장하도록 합니다.

abstract class FlyingBird{
   abstract void fly();
}

abstract class NonFlyingBird{
   abstract void doSomething();
}

class Eagle extends FlyingBird {
   @Override
   public void fly() { // 일부 구현 }
}

class Ostrich extends NonFlyingBird {
   @Override
   public void doSomething() { // 일부 구현 }
}

인터페이스 분리 원칙

인터페이스 분리 원칙에 따르면, 클라이언트가 필요하지 않은 동작을 강제시키는 작고 초점이 명확한 인터페이스를 구축해야 합니다.

간단한 예제는 도형의 면적과 부피를 모두 계산하는 인터페이스를 가지는 것입니다.

interface IShapeAreaCalculator(){
  double calculateArea();
  double calculateVolume();
}

class Square implements IShapeAreaCalculator{
  double calculateArea(){ // 면적 계산 }
  double calculateVolume(){ // 더미 구현 }
}

이 문제는 Square 도형이 이를 구현하면 calculateVolume() 메서드를 필요 없이 구현하도록 강제되어 있습니다.

다른 한편으로, 큐브는 둘 다 구현할 수 있습니다. 이 문제를 극복하기 위해 인터페이스를 분리하고 두 개의 별도의 인터페이스를 가진다: 넓이를 계산하는 것과 부피를 계산하는 것입니다. 이렇게 하면 개별적인 도형이 어떻게 구현할지 결정할 수 있습니다.

interface IAreaCalculator {
    double calculateArea();
}

interface IVolumeCalculator {
    double calculateVolume();
}

class Square implements IAreaCalculator {
    @Override
    public double calculateArea() { // 넓이를 계산합니다 }
}

class Cube implements IAreaCalculator, IVolumeCalculator {
    @Override
    public double calculateArea() { // 넓이를 계산합니다 }

    @Override
    public double calculateVolume() {// 부피를 계산합니다 }
}

의존성 반전 원리

의존성 반전 원리에 따르면, 고급 모듈은 저급 모듈에 의존해서는 안 됩니다. 다시 말해, 추상화를 따라가고 느슨한 결합을 보장해야 합니다.

public interface Notification {
    void notify();
}

public class EmailNotification implements Notification {
    public void notify() {
        System.out.println("Sending notification via email");
    }
}

public class Employee {
    private EmailNotification emailNotification; 
    public Employee(EmailNotification emailNotification) {
        this.emailNotification = emailNotification;
    }
    public void notifyUser() {
        emailNotification.notify();
    }
}

주어진 예제에서, Employee 클래스는 직접 EmailNotification 클래스에 의존합니다. 이는 저급 모듈입니다. 이렇게 하면 의존성 반전 원리를 위반합니다.

public interface Notification{
  public void notify();
}

public class Employee{
  private Notification notification;
  public Employee(Notification notification){
      this.notification = notification;
  }
  public void notifyUser(){
    notification.notify();
  }
 }

 public class EmailNotification implements Notification{
    public void notify(){
        // 이메일을 통한 알림 구현 
    }
 }

 public static void main(String [] args){
    Notification notification = new EmailNotification();
    Employee employee = new Employee(notification);
    employee.notifyUser();
 }

위의 예제에서는 느슨한 결합을 보장했습니다. Employee 클래스는 구체적인 구현에 의존하지 않고, 추상화(알림 인터페이스)에만 의존합니다.

알림 모드를 변경해야 한다면, 새로운 구현을 만들고 Employee에 전달할 수 있습니다.

결론

결론으로, 이 문서에서 SOLID 원리의 본질을 간단한 예제를 통해 다루었습니다.

이러한 원리는 고체적인 확장성과 재사용성이 있는 응용 프로그램을 개발하는 데 기반을 제공합니다.

Let’s connect on LinkedIn → 저희는 LinkedIn에서 연결해 주세요.