SOLID, 객체 지향 설계의 5가지 원칙

최진민·2021년 10월 10일
2

디자인 패턴

목록 보기
1/10
post-thumbnail

🍟SOLID?

  • 좋은 설계 : 프로그램에 새로운 비즈니스 요구사항이 추가될 때, 영향을 받는 범위가 적은 설계
  • SOLID : 객체 지향 설계 시, 지켜야할 5가지 원칙
    • SRP (Single Responsibility Principle) : 단일 책임 원칙
    • OCP (Open-Closed Principle) : 개방 폐쇄 원칙
    • LSP (Liskov Substitution Principle) : 리즈코프 치환 원칙
    • ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
    • DIP (Dependency Inversion Principle) : 의존 역전 원칙


🍙SRP

  • SRP (Single Resposibility Principle) : 단일 책임 원칙
  • 객체는 단 하나의 책임을 가져야 한다.
  • 응집도를 높이고 결합도를 낮춘다.
    • 응집도 : 한 프로그램의 요소가 얼마나 뭉쳐있는지
    • 결합도 : 프로그램 구성 요소 사이가 얼마나 의존적인지
  • 예제) 19살 이상인 사용자의 이름만 변경하도록 설계할 때
    • Before

      class User {
          private String name;
          private int age;
      
          public User(String name, int age) {
              this.name = name;
              this.age = age;
          }
          
          /**
          getter, setter
          **/
      }
      
      class UserSetting {
          private User user;
      
          public UserSetting(User user) {
              this.user = user;
          }
      
          public void changeUserName(String name){
              if (upToNineteen()) {
                  user.setName(name);
              }
          }
      
          public boolean upToNineteen() {
              return this.user.getAge() >= 19;
          }
      }
      • UserSetting은 사용자의 이름을 변경하는 changeUserName()과 사용자의 나이가 19세 이상인지 확인하는 upToNineteen()의 역할(책임)이 두 가지가 있다. 한 클래스가 두 개의 책임을 갖는 것은 SRP에 위배되는 행위다.
    • After

      class User {
          private String name;
          private int age;
      
          public User(String name, int age) {
              this.name = name;
              this.age = age;
          }
          
          /**
          getter, setter
          **/
      }
      
      class UserSetting {
          private final User user;
      
          public UserSetting(User user) {
              this.user = user;
          }
      
          public void changeUserName(String name) {
              if (UserVerify.upToNineteen(user)) {
                  this.user.setName(name);
              }
          }
      
      }
      
      class UserVerify {
          public static boolean upToNineteen(User user) {
              return user.getAge() >= 19;
          }
      }
      • UserSetting은 이름을 변경하는 역할만을 갖고, 새로 만든 클래스 UserVerify는 나이가 19세 이상인지 판별하는 역할만을 가진다. 두 가지로 나누었기 때문에 SRP를 위반하지 않는다.


🍖OCP

  • OCP (Open-Closed Principle) : 개방 폐쇄 원칙
  • 기존의 코드를 변경하지 않고 (closed) 기능을 추가할 수 있도록 (open) 설계되어야 한다.
  • 캡슐화 : 여러 객체에서 사용하는 기능을 interface or abstract class로 구현
  • 주로 캐스팅if-else 구문에서 위배되는 경우 발생
  • 예제 1) 동물 울음 소리
    • Before

      class Animal {
          String name;
      
          public Animal(String name) {
              this.name = name;
          }
          
          public getName(){
              return this.name;
          }
      }
      
      class MyAnimal {
          List<Animal> animals = new ArrayList<>();
      
          public void animalSound(){
              for (Animal animal : animals) {
                  if (animal.getName().equals("lion")) {
                      System.out.println("roar");
                  } else if (animal.getName().equals("mouse")) {
                      System.out.println("zikzik");
                  } else if (animal.getName().equals("snake")) {
                      System.out.println("hiss");
                  } else {
                      System.out.println("etc.");
                  }
              }
          }
      }
      • 만약, "dog"를 추가하면 MyAnimal.animalSound()를 아래와 같이 dog에 대한 내용을 추가해야 한다.
        public void animalSound(){
            for (Animal animal : animals) {
                if (animal.name.equals("lion")) {
                    System.out.println("roar");
                } else if (animal.name.equals("mouse")) {
                    System.out.println("zikzik");
                } else if (animal.name.equals("snake")) {
                    System.out.println("hiss");
                } else if (animal.name.equals("dog")) {
                    System.out.println("bowwow");
                } else {
                    System.out.println("etc.");
                }
            }
        }
    • After

      import java.util.ArrayList;
      import java.util.List;
      
      interface Animal {
          void makeSound();
      }
      
      class Lion implements Animal {
          @Override
          void makeSound() {
              System.out.println("roar");
          }
      }
      
      class Mouse implements Animal {
          @Override
          void makeSound() {
              System.out.println("zikzik");
          }
      }
      
      class Snake implements Animal {
          @Override
          void makeSound() {
              System.out.println("hiss");
          }
      }
      
      class Dog implements Animal {
          @Override
          void makeSound() {
              System.out.println("bowwow");
          }
      }
      
      class MyAnimal {
          List<Animal> animals = new ArrayList<>();
      
          public void animalSound(){
              for (Animal animal : animals) {
                  animal.makeSound();
              }
          }
      }
      • 특정 동물을 추가하더라도 MyAnimal에 대한 코드 수정은 없고 (closed), 새로운 동물 추가에 대한 확장은 열려있다. (open)


🥠LSP

  • LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
  • 자식 클래스는 언제나 부모 클래스 타입으로 교체 가능해야 한다.
  • = 자식 클래스가 부모 클래스의 역할을 충실히 해내며, 확장해 나가야 한다.
  • = 오버라이딩 하지 말자.
  • 상속의 룰 + OCP를 위반하지 않도록 하는 원칙
  • 예제) 할인된 가방 가격
    • Before

      class Bag {
          private int price;
      
          public void setPrice(int price){
              this.price = price;
          }
      }
      
      class DiscountedBag extends Bag {
          private double discountedRate = 0.0;
      
          public void setDiscounted(double dc) {
              discountedRate = dc;
          }
      
          @Override
          public void setPrice(int price) {
              super.setPrice(price - (int) (discountedRate * price));
          }
      }
      • Bag에 존재하는 메서드 setPrice()를 자식 클래스인 DiscountedBag에서도오버라이딩해서 사용했다. 이는 LSP를 위반하는 행위다.
    • After

      class DiscountedBag extends Bag {
          private double discountedRate = 0.0;
      
          public void setDiscounted(double dc) {
              discountedRate = dc;
          }
      
          public void setDiscountedPrice(int price) {
              super.setPrice(price - (int) (discountedRate * price));
          }
      }
      • setDiscountedPrice(...)과 같이 새로운 메서드를 형성하여 오버라이딩을 피했기 때문에 LSP를 지킬 수 있었다.


🍻ISP

  • ISP (Interface Substitution Principle) : 인터페이스 분리 원칙
  • 자신이 사용하지 않는 인터페이스는 구현하지 말자.
  • 예제) 곱하기 기능이 추가되는 요새 계산기
    • Before

      interface Calculator {
          void add();
          void subtract();
          void multiply();
      }
      
      class CalculatorRecentVer implements Calculator {
          @Override
          public void add() {
              System.out.println("+");
          }
      
          @Override
          public void subtract() {
              System.out.println("-");
          }
      
          @Override
          public void multiply() {
              System.out.println("*");
          }
      }
      
      class CalculatorPastVer implements Calculator {
          @Override
          public void add() {
              System.out.println("+");
          }
      
          @Override
          public void subtract() {
              System.out.println("-");
          }
      
          //필요없는 메서드
          @Override
          public void multiply() {
              System.out.println(".........");
          }
      }
      • Calculator에 현재 3개의 연산이 담겨있지만 과거 계산기에는 multiply()가 없었다. 필요치 않은 기능을 구현한 Interface이기 때문에 ISP를 위반했다.
    • After

      interface Calculator {
          void add();
          void subtract();
      }
      
      interface CalculatorRecent {
          void multiply();
      }
      
      class CalculatorPastVer implements Calculator {
          
          @Override
          public void add() {
              System.out.println("+");
          }
      
          @Override
          public void subtract() {
              System.out.println("-");
          }
      }
      
      class CalculatorRecentVer implements Calculator, CalculatorRecent {
      
          @Override
          public void add() {
              System.out.println("+");
          }
      
          @Override
          public void subtract() {
              System.out.println("-");
          }
      
          @Override
          public void multiply() {
              System.out.println("*");
          }
      }
      • 최근 계산기인 CalculatorResent 인터페이스를 추가하고 최신 계산기에만 해당 인터페이스를 상속받아 *에 대한 연산을 가능하도록 구현했다. 이는, ISP를 지키는 행위다.


🍧DIP

  • DIP (Dependency Inversion Principle) : 의존 역전 원칙
  • 추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다.
  • 고수준 모듈은 저수준 모듈에 의존하면 안된다. 저수준 모듈이 고수준 모듈에 의존해야 한다.
  • 예제) 매일 다르게 마시고 싶은 사람
    • Before

      class Americano {
          void drink() {
              System.out.println("americano");
          }
      }
      
      class Latte {
          void drink() {
              System.out.println("latte");
          }
      }
      
      class Water {
          void drink() {
              System.out.println("water");
          }
      }
      
      class Person extends Americano {
          //
      }
      
      public class DrinkSome {
          public static void main(String[] args) {
              Person p = new Person();
              p.drink(); //아메리카노만 마실 수 있는 사람...
          }
      }
      • 추상화 되어야 할 Person이 구체적인 Americano만 의존한다. 이는, DIP에 위배되는 행위다.
    • After

      interface SomeDrink {
          void drink();
      }
      
      class Americano implements SomeDrink {
          @Override
          public void drink() {
              System.out.println("americano");
          }
      }
      
      class Latte implements SomeDrink {
          @Override
          public void drink() {
              System.out.println("latte");
          }
      }
      
      class Water implements SomeDrink {
          @Override
          public void drink() {
              System.out.println("water");
          }
      }
      
      class Person {
          SomeDrink ame = new Americano();
      
          SomeDrink latte = new Latte();
      
          SomeDrink water = new Water();
      
          public void drinkAmericano(){
              ame.drink();
          }
          
          public void drinkLatte(){
              latte.drink();
          }
      }
      
      public class DrinkSome {
          public static void main(String[] args) {
              Person p = new Person();
              p.drinkAmericano();
          }
      }
      • PersonSomeDrink의 인스턴스화를 통해 여러 객체를 생성 및 초기화할 수 있고 main에서는 필요한 메서드만을 사용할 수 있도록 했다.
profile
열심히 해보자9999

0개의 댓글