본문 바로가기
TIL

2024.05.13 57일차 WEB 객체지향 SOLID

by Song.dev 2024. 5. 13.

 

객체지향 설계 원칙 SOLID

  1. SRP (Single Responsibility Principle): 단일 책임 원칙
  2. OCP (Open-Closed Principle): 개방-폐쇄 원칙
  3. LSP (Liskov Substitution Principle): 리스코프 치환 원칙
  4. ISP (Interface Segregation Principle): 인터페이스 분리 원칙
  5. DIP (Dependency Inversion Principle): 의존 역전 원칙

 

  • 제어의 역전(IoC) : 객체 생성의 제어권을 외부로 넘긴다.
  • 의존성 주입(DI) : 외부에서 생성된 객체를 주입받는 개념 (결합도 ↓)

출처 위키백과_의존성주입

 


SRP : 단일 책임 원칙

  • 하나의 클래스는 하나의 책임만 가져야 한다는 원칙
  • 클래스가 변경되어야 하는 이유는 오직 하나여야 한다는 것을 의미
  • 클래스를 작게, 더 작은 단위로 나누어 관리하면 코드를 더 읽기 쉽고 유지보수하기 쉽게 만들어줌
더보기

SRP 예시

사용자 정보를 나타내는 클래스 User는 생성자, 유효성 검사, 데이터 저장 등 여러 기능을 가지고 있음

 

단일 책임 원칙을 지키도록 개선해보면

User클래스 : 사용자 정보만 관리

유효성 검사 : UserValidator

데이터 저장 : UserDAO 

 

클래스를 더 작은 단위로 분리하여 단일 책임을 부여하면

코드의 유지보수성이 증가하고, 다른 기능을 추가하거나 변경해야 할 때 영향을 최소화

public class User {
    private String username;
    private String password;
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    public boolean isValid() {
        // 유효성 검사
        return true;
    }
    
    public void save() {
        // 데이터 저장
    }
}

-------------------
public class User {
    private String username;
    private String password;
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
   
}

public class UserValidator {
    public boolean isValid(User user) {
        // 유효성 검사
        return true;
    }
}

public class UserDAO {
    public void save(User user) {
        // 데이터 저장
    }
}

 

 

OCP : 개방-폐쇄 원칙

  • 확장에는 열려 있고 변경에는 닫혀 있어야 한다는 원칙
  • 시스템을 변경하기 위해서 기존 코드를 수정하지 않고 새로운 코드를 추가할 수 있도록 설계
더보기

OCP 예시

새로운 도형이 추가되더라도 AreaCalculator를 수정할 필요 없음

public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private double width;
    private double height;

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

    public double calculateArea() {
        return width * height;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.calculateArea();
        }
        return totalArea;
    }
}

 

LSP : 리스코프 치환 원칙

  • Liskov Substitution Principle (LSP)는 다형성을 지원하는 객체지향의 중요한 원칙
  • 자식 클래스가 부모 클래스의 인스턴스 대신 사용될 때 언제나 정상적으로 작동해야 한다는 것을 의미
더보기

LSP 위반 예시

 

Square 클래스는 Rectangle 클래스와 다른 동작을 수행하기 때문에 LSP를 위반

즉, Rectangle 클래스의 인스턴스가 예상대로 작동하지 않고 100을 출력

 

Square 클래스에서 setWidth()와 setHeight() 메소드가 각각 동일한 값을 설정하지 않도록 수정해야함

 

public class Rectangle {
    protected int width;
    protected int height;

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

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

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

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

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

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

public class LspExample {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(5);
        rectangle.setHeight(10);
        System.out.println(rectangle.calculateArea()); // 50

        Square square = new Square();
        square.setWidth(5);
        square.setHeight(10);
        System.out.println(square.calculateArea()); // 100
    }
}

 

 

ISP : 인터페이스 분리 원칙

  • ISP(Interface Segregation Principle)
  • 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리하는 것을 의미
더보기

ISP 예시

 - Rectangle 클래스는 부피 계산이 필요없으므로 Area 인터페이스만 implements

 - Cube 클래스는 부피 계산도 필요하므로 Area, Volume 인터페이스를 implements

public interface Area {
    double calculateArea();
}

public interface Volume {
    double calculateVolume();
}

public class Rectangle implements Area {
    private double width;
    private double height;

    public double calculateArea() {
        return width * height;
    }
}

public class Cube implements Area, Volume {
    private double width;
    private double height;
    private double depth;

    public double calculateArea() {
        return 2 * (width * height + width * depth + height * depth);
    }

    public double calculateVolume() {
        return width * height * depth;
    }
}

 

DIP : 의존성 역전의 원칙

  • DIP(Dependency Inversion Principle)
  • 상위 객체는 하위 객체의 구체성에 의존해서는 안되며 추상화에 의존해야 한다는 원칙
  • DIP의 두 가지 규칙
  1. 고차원 모듈은 저차원 모듈에 의존해서는 안된다.
  2. 추상화는 세부사항에 의존해서는 안된다. 세부사항은 추상화에 의존해야 한다.
더보기

DIP 예시

 - Switch 클래스는 Light 인터페이스를 통해 RedLight 클래스와 의존 관계를 맺고 있음

 - Switch 클래스는 RedLight 클래스 대신에 Light 인터페이스를 사용

 - 만약 RedLight 클래스에 변경이 생긴다 해도 Switch 클래스는 영향을 받지 않음

public interface Light {
    void turnOn();
}

public class RedLight implements Light {
    @Override
    public void turnOn() {
        System.out.println("Red Light turned on");
    }
}

public class Switch {
    private Light light;
    
    public Switch(Light light) {
        this.light= light;
    }
    
    public void flip() {
        if (light!= null) {
            light.turnOn();
        }
    }
}

 

SOLID 예시

코드 개선 1 → 2 → 3 → 4

1, 2번 : 구체적인 객체 인스턴스에 의존 → DIP 위반

3번 : 추상화된 인터페이스에 의존 → DIP 해결, 그러나 여전히 의존객체를 바꾸려면 코드를 직접 변경

4번 : 객체 생성의 제어권을 이 클래스에서 다른 클래스로 이전

         호텔 객체 생성시 반드시 의존객체를 전달하도록 강요  → OCP 해결

 

 - 각 클래스의 객체 생성은 외부(HotelManager) 클래스로 분리

 - Manager에서 적절한 객체를 리턴하여 의존성 주입

 - Restaurant, Chef가 교체되더라도 Hotel클래스를 수정할 필요가 없어짐 = OCP 원칙 만족

 

이런 Manager 클래스 역할을 스프링이 담당하게 됨

 

public class Hotel {

    // 레스토랑
//    public WesternRestaurant restaurant = new WesternRestaurant(); -- 1
//    public AsianRestaurant restaurant = new AsianRestaurant();     -- 2
        
    public Restaurant restaurant = new AsianRestaurant();            -- 3
    public Restaurant restaurant;                                    -- 4
    

    // 헤드쉐프
//    public JannChef headChef = new JannChef();                     -- 1
//    public KimuraChef headChef = new KimuraChef();                 -- 2
    
    public Chef headChef = new KimuraChef();                         -- 3
    public Chef headChef;                                            -- 4


    // 호텔을 소개하는 기능
    public void inform() {
        System.out.printf("우리 호텔의 레스토랑은 %s입니다. " +
                        "그리고 헤드쉐프는 %s입니다.\n"
                , restaurant.getClass().getSimpleName()
                , headChef.getClass().getSimpleName());

        restaurant.order();
    }
}