SOLID

반응형
반응형

https://proandroiddev.com/compile-less-with-solid-bd0633c1f84

SOLID는 객체지향 원칙으로
SRP, OCP, LSP, ISP, DIP의 앞자리를 하나씩 부르는것을 말한다.
생각보다 원칙을 지키면서 코드를 작성하는 것은 쉽지 않고,
설령 원칙을 지키면서 코드를 작성할 수 는 있지만,
원칙을 지키기다 보면 인터페이스나 클래스를 추가하게되 오히려 위 원칙을 지키지 않는 것이 훨씬 더
좋은 상황이 발생할 수 도 있다. (사람바이사람, 상황바이상황)

따라서 상황에 맞춰서 SOLID원칙을 지키면서 할지 아니면 그냥 구현으로 할지 결정을 해야한다.

그러면 이제 SOLID를 하나씩 살펴보면서
어떻게 사용될지 생각해보자.

SRP : 단일 책임 원칙
이 원칙은 하나의 클래스에는 단 하나의 행동만 가져야 된다는 원칙이다.
오해하면 안되는게 절대로 하나의 기능만 있어야 된다는 뜻은 절대 아니다.
간단히 Employee 클래스를 만들었다.

public class Employee {
  private int pay;
  private int hour;
}

Employee는 직원이라는 뜻을 가졌고
시간에 할당에서 pay를 받는다.

그래서 시간을 기록하는 메서드와 
pay를 주는 메서드를 만들었다.

  public int sendPay(int pay) {
    return pay;
  }
  
  public int recordHour(int hour) {
    return hour;
  }

그리고 또, 임금을 받으면 각 직원이 얼마씩 받는지에 알아야 한다.
그냥 귀찮으니 set,get을 이용하자. 

  public void setPay(int pay) {
    this.pay = pay;
  }

  public int getPay() {
    return pay;
  }

  public void setHour(int hour) {
    this.hour = hour;
  }

  public int getHour() {
    return hour;
  }

이 코드에는 총 2가지 문제점을 가지고 있다.
1. 임금을 받는 거와 임금을 주는 것을 같은 클래스에서 작동하고 있다.(시간도 마찬가지)
2. 임금을 주는 직원과 시간을 기록하는? 직원은 서로 다르다.

물론 2번 같은 경우는 같은 곳에서 이루어질 수 도 있다.
그렇기 때문에 그렇구나라고 넘어갈 수도 있겠지만,
첫 번째같은 경우는 임금을 주는것과 받는것이 같은 클래스이기 때문에 문제가 심각하다.
코드상으로 따지면 스스로 돈을 받는 구조가 되기 때문이다.

    public static void main(String[] args) {
        Employee employee1 = new Employee();
        employee1.setPay(employee1.sendPay(10));
    }

이런 이상한 코드가 만들어진다.
아무리 돈을 주는 사람도 직원이라고는 하지만, 스스로 돈을 받는 시스템은 이상하다.
그렇다고 자영업도 아닌데 어디서 돈을 받는건지 신기하다는 생각뿐이다.

그래서 코드를 이렇게 수정했다.

public class Account {
  public int sendPay(int pay) {
    return pay;
  }
}

sendPay를 Account라는 클래스로 만들어서 돈을 전달하게 했다.
물론, 실제로는 상위 직원?에게 승인을 받아야 되지만 그거는 없다고 생각하고 설명할려구 한다.

코드는 이렇게 수정되었다.

    public static void main(String[] args) {
        Account account = new Account();
        Employee employee1 = new Employee();
        employee1.setPay(account.sendPay(10));
    }

이제 돈을 주는것은 Account라는 직원이 대신해주는것으로 수정되었다.
이런것이 SRP가 아닐까 생각이든다.
hour도 이와 비슷하게 하면 되지 않을까 생각이 든다.
현재 account는 돈을 받는 수단이 전혀 없다.
돈을 주는 직원도 직원인데 돈을 못 받는것 아쉽지만? 귀찮으니 넘어가자.
완벽한 코드를 작성하는게 목적이 아니라 SRP를 이해하는것이 목표니까 말이다.

이 원칙을 잘지킨 패턴은 퍼사드 패턴이라고 한다.
퍼사드 패턴은 다음처럼 작성되어진다고 한다.

public class EmployeeFacade {
  public int sendPay(int pay) {
    return pay;
  }
  public int recordHour(int hour) {
    return hour;
  }
}
public class Account {
  private final EmployeeFacade employeeFacade;

  public Account() {
    this.employeeFacade = new EmployeeFacade();
  }

  public int sendPay(int pay) {
    return employeeFacade.sendPay(pay);
  }
}

어떻게 보면 쓸모없는 코드 같아 보이지만, 이것을 하는 목적은 관심사 분리에 있다.
그러니까 sendPay라는 기능을 Account뿐만아니라 다른곳에서도 사용할 수 있게 해주는게 아닐까 생각이 든다.
패턴이 틀렸다고 해도 할말 없다. 왜냐하면 머릿속에서 끄집어서 겨우 작성한것이기 때문이다.
그러니 퍼사드 패턴을 사용하면 SRP를 지킬 수 있다. 정도라만 이해하면 될것 같다.
물론 이 패턴을 사용한다고 SRP를 100%보장하는것은 아니지만, 어느정도 방지할 수 있다 정도만 이해하면 될 거라 생각한다.

OCP : 
OPEN CLOSE 정책인데
이말은 확장에는 열려있고 변경에는 닫혀 있어야 된다는 뜻이다.
어찌되었든 확장을 하든 변경을 하든 코드의 변경이 일어난다는 건데
아무리 생각해도 코드 단위로는 이것의 핵심을 파악하기란 쉽지 않다.

이 원칙들은 코드를 어떻게 작성해야 되는지 말하는 원칙이 아니라 객체 지향에 대한 원칙이다.
따라서 객체 지향 관점으로 생각해야 된다.
즉, 객체의 변경은 거의 일어나지 않아야 하며, 확장에는 열려있어야 된다.

객체의 변경이 일어나지 않으면서 확장을 시키는 방법은 딱 한가지가 존재한다.
그것은 바로 상속을 이용하는 방법이다.

그림에서 보는것 처럼 기존의 코드를 만든다.
그리고 그것을 상속하는 복사본들을 만든다.

처음에는 복사본1만 존재하였다. 하지만 기능이 추가되어 복사본2도 만들게 되었다.
OCP를 적용하지 않는 상태에서 복사본2의 기능을 추가하려면 
복사본1의 기능을 수정해야 된다. 
이것이 바로 OCP에 위배되는 행동이다 왜냐하면 복사본1을 변경하였기 때문이다.

OCP를 지키려면 복사본1은 건드리면 안된다.
새로이 복사본2를 만들어서 새로운 코드를 작성하는 것이 더 좋다.
물론, 복사본1의 기능을 수정해서 복사본2의 기능을 추가하는게 더 나을 수도 있다.
위에서 말했듯이 이것은 어디까지나 선택이며, 굳이 OCP를 지킬 필요는 없다 생각이 든다.

그러면 언제 OCP를 사용하는게 좋을까?
고민을 해봤다.
내생각에 기능을 사용할때 가장 유용할것 같다.
모델에 이것을 사용하는 건 아닌것 같구
서비스쪽 코드를 작성할때 유용할거라 생각이 든다.
원래 코드를 작성하지 않으려 했지만, 한번 작성해봤다.
OCP를 적용하지 않는 버전과 적용한 버전을 이렇게 두개를 보여줄 예정이다.

OCP를 적용하지 않는 버전

  public void computerFixed() {
    System.out.println("컴퓨터를 수리합니다.");
  }
  
  public void radioFixed() {
    System.out.println("라디오를 수리합니다.");
  }


  이렇게 되면 새롭게 수리해야될 제품이 생긴다면? 메소드가 하나씩 추가될 것이다.
하지만 OCP가 적용된건 어떨 까?

OCP를 적용한 버전

public class RepairService {

  public void fixed() {
    System.out.println("수리되었습니다.");
  }

}

public class ComputerRepairService extends RepairService{


  @Override
  public void fixed() {
    System.out.println("컴퓨터를 수리하였습니다.");
  }
}

public class RadioRepairService extends RepairService{

  @Override
  public void fixed() {
    System.out.println("라디오를 수리합니다.");
  }
}

이런식으로 기존 코드를 건드리지 않고 상속을 이용해서 기능을 추가하였다.
이렇게 되면 상속만 하면 되기 때문에 기존 코드를 건드리지 않아도 된다.
코드의 양으로 비교해보면 (클래스 부분 제외) 2 줄 vs 6줄이나 차이 난다.
이래서 어쩌면 OCP를 안 지키는 것이 좋을 수도 말한것이다.

여기에서는 상속을 이용했지만 인터페이스를 사용해서 구현하는 방법도 OCP를 지키는 방법중 하나라 생각한다.

LSP : 리스코프 치환 원칙이라고 불리며
하위 객체를 사용해도 아무런 문제없이 동작을 해야 된다는 뜻이된다.

이거는 바로 코딩을 해서 보는것이 더 좋을거라 생각이 든다.

public class Programmer {

}

여기 개발자가 존재한다. 이 개발자의 특징은 모든 프로그래밍 언어를 다룰 수 있다는 것이 특징이다.
즉, 코딩 능력이 출중하다는 뜻이된다.

  public void coding() {
    System.out.printf("%s 코딩을 합니다.","언어");
  }

 처음 이 개발자가 배운 언어는 C언어라고 한다. 그래서
C언어도 만들었다.

public class Clang {

  @Override
  public String toString() {
    return "c언어";
  }
}

이제 이 개발자가 C언어를 코딩할 수 있게 도와주자.

public class Programmer {

  public void coding(Clang clang) {
    System.out.printf("%s 코딩을 합니다.",clang.toString());
  }
}

만약, 다음 언어로 자바를 학습하게 된다면,
C언어자리에 자바가 들어가게 되어야 된다.
근데 이미 Clang이라는 것이 자리잡고 있기 때문에 함부로 Java로 변경하는것은 쉽지 않아보인다.
그래서 이 둘의 공통점을 고민해본결과 프로그래밍 언어라는 공통 점을 발견했다.
다음 처럼 수정했다.

public class Language {


  @Override
  public String toString() {
    return "프로그래밍 언어";
  }
}

public class Programmer {

  public void coding(Language language) {
    System.out.printf("%s 코딩을 합니다.",language.toString());
  }
}

 이렇게 하면 C언어랑 자바를 두개다 코딩이 가능해진다.
물론 아직 상속을 사용하지 않아서 정상적으로 나오지는 않는다.

public class Java extends Language{

  @Override
  public String toString() {
    return "자바";
  }
}

이제 C언어와 자바를 사용할 수 있게 되었다 
다시 프로그래머로 돌아가자.

public class Programmer {

  public void coding(Language language) {
    System.out.printf("%s 코딩을 합니다.",language.toString());
  }
}

여러 방법이 존재하겠지만, 제일 간단한 방법으로는 If문을 활용하는 방법이다.
(이 방법은 DIP를 어겼다는 점에서 참고만 해주시죠)
이런식으로 고치면 된다.

public class Programmer {

  public void coding(Language language) {
    
    if(language instanceof Java java) {
      System.out.printf("%s 코딩을 합니다.",java.toString());
    }
    
    else if(language instanceof Clang clang) {
      System.out.printf("%s 코딩을 합니다.",clang.toString());
    }
  }
}

이렇게 코딩을 하게 되면 어떤 언어를 사용하던지간에 아무런 문제없이 동작할 수 있게 된다.
즉, 프로그래머는 Language를 Java와 C언어 둘중 아무거나 해도 코드는 돌아간다는 뜻이 된다.
이것이 LSP다.
물론 이 코드에는 문제점이 존재한다.
 - if문으로 나눌 필요가 없다는 점
 - 비슷한 코드를 작성해서 리펙토링이 필요한점 
등등 여러가지 문제가 존재하지만 이게 LSP다.
만약에 여기에 English가 추가되면 어떻게 될까?
애초에 영어같은 경우는 프로그래밍 언어에 맞지 않기 때문에 추가된다면 LSP를 지킨다고 할 수 없다.
왜냐하면 영어는 프로그래밍 언어가 아니기 때문이다.
코드상에서도 자유자재로 바꿀수 있어야 되지만 이것이 납득이 되게 만드는것이 중요하다고 생각이 든다.

ISP : 인터페이스 분리의 원칙이라고 불린다.
책을 읽었는데 생각많큼 이해가 되지 안하서 객체의 관점에서 생각해보려 한다.

가장 먼저 인터페이스 분리를 해야 되는 이유에 대해 생각해 보자.
예를들어

public interface People {
  void read();
  void jump();
  void walk();
  void write();
}

이런 인터페이스가 존재한다고 한다.
얼핏 보기에는 사람이 하는일들이라 이렇게 지정해도 아무런 문제가 되지 않을 것 같지만
이는 ISP를 지키지 않았다.
왜냐하면 read와 write는 글과 관련된 활동이고
jump와 walk같은 경우는 운동과 관련된 활동이기 때문이다.
물론 두개다 사람이 저 행위들을 한다는 점에서 틀린 말은 아니지만 이 두개를 서로 다른 인터페이스로 나누는 것이 더 좋다고 느껴진다.
이렇게 나누면 어떨까?

public interface WorkoutActivity {
  void walk();
  void jump();
}

public interface BookActivity {
  void write();
  void read();
}

이렇게 나누니 조금더 어떤 활동을 할지 조금더 와닿는다는 느낌이 든다.
이것에 대해 이게 ISP다라고 말하기는 어렵지만 적어도 내가 생각하기에 이것이 ISP라 생각이 든다.

DIP : 드디어 마지막!!
의존성 제어의 원칙 또는 의존 관계 제어의 원칙이라고 부른다.

이것은 어떠한 객체가 다른 객체가 무슨일을 하는지 몰라야 한다는 뜻이다.
이것도 코드로 보여주는 것이 편할것 같다.

public class Teacher {
  
}

public class Student {

}

여기에는 선생과 학생이 등장한다.
선생은 가르치고 학생은 학습한다.

public class Teacher {

  public String teach() {
    System.out.println("수학을 가르칩니다.");
    return "수학";
  }
}

선생은 수학을 가르친다고 합시다.

public class Student {
  
  public void learn() {
    System.out.println("무언가를 학습합니다.");
  }
}

현재 학생은 선생이 무엇을 가르치는지 모르는 상황입니다.
그래서 이런식으로 수정했습니다.

public class Student {

  public void learn() {
    Teacher teacher = new Teacher();
    System.out.println(teacher.teach() + "를 학습합니다.");
  }
}

이 코드는 DIP를 지키지 않았습니다. 무엇이 잘못되었을까요?
learn이라는 메서드를 확인해보면 learn메소드를 사용하면 선생이 나옵니다.
만약, 이게 현실이라면 사교육 시장은 이걸로 종결일것입니다.
이거는 말도 안됩니다. 매직
위 사안은 2가지 방법으로 해석이 가능합니다.(더 있을 수 있겠지만 저는 2가지로 생각했습니다.)
1. learn메서드를 사용하면 teacher이 등장한다.
2. student는 무언가를 학습할때마다 teacher이 필요하다.

그러면 코드를 이렇게 수정하면 무엇이 변경될까요?

public class Student {
  private Teacher teacher;

  public Student(Teacher teacher) {
    this.teacher = teacher;
  }

  public void learn() {
    System.out.println(teacher.teach() + "를 학습합니다.");
  }
}

이렇게 되면 학생은 외부에서 고용된 선생이 가르친 내용을 학습하게 됩니다.
물론, 학생이 독학 할 수 도 있겠지만, 이거는 어디까지나 예제니까요.
근데 이렇게 생각할 수 있습니다. 
학생은 이미 선생을 알고 있는거 아닌가요?
Student코드를 보면 알겠지만 어디에도 new Teacher()은 보이지 않습니다.
즉, Teacher객체는 Student 객체에서 발생이 되지 않는 점은 분명합니다.
이것이 DIP입니다. 더 나아가면 Teacher객체를 인터페이스로 만들어서 수학선생님, 영어선생님, 등등을 추가할 수 있습니다.

지금까지 SOILD에 대해 공부해봤는데
어떤 원칙은 코드상에서 바로 알아볼 수 있는 원칙이 있는 반면
어떤 어떤 원칙들은 코드상에서는 확인하기 어려운 원칙들이 존재하는 것 같습니다.
솔직히 코드상에서 알아보라면 다 알아볼 수 있을 것 같긴한데
굳이 나누자면
코드상에서 나타나기 어려운 : SRP, LSP, ISP 
코드상에서 확인 하기 쉬운 : OCP,  DIP
이건 어디까지나 제 생각입니다. 저 같은 경우 이렇다는 거지 이것이 절대적인 것인것은 아닙니다.
근데 OCP는 위쪽이 맞는것 같기도 하고 애매하군요. 

반응형

'프로그래밍 언어 > 자바' 카테고리의 다른 글

인터페이스  (0) 2022.03.21
SOLID  (0) 2021.09.10
LocalDateTime (미 정재 .ver)  (0) 2021.09.04
람다  (0) 2021.03.01
자바9 모듈  (0) 2021.02.22
제네릭  (0) 2021.02.21

댓글(0)

Designed by JB FACTORY