OO, OOP, SOILD원칙과 디자인 패턴(작성 중)

햄스터아저씨·2021년 9월 1일
0

일단 원칙의 위계먼저 알아보자.
OO Basic > OOP 원칙 > SDILD 원칙 > Design Pattern

OOP 4대 혹은 5대 기본원칙

OOP: Object-Oriented Programming 혹은 Object-Oriented Principle

추캡상다 + 결

  1. 추상화(Abstraction): 필요한 특징만 추려서 모으고 이름을 붙여라.
  2. 캡슐화(Encapsulation): 세부 내용을 몰라도 설계할 수 있게 만들어라, 몰라도 쓸 수 있게 하면 의존성이 줄어든다.
  3. 상속(Inheritance): 중복된 것은 상속을 써서 재사용해라. UML에서 상속은 일반화와 같다.
  4. 다형성(Polymorphism): 한 객체는 여러 모습(class)로 표현될 수 있고 반대로 한 표현으로 여러 객체를 사용할 수 있다.
    (상속, 인터페이스, 추상클래스로 구현이 가능하다)

+. 객체 결합(Composition): 한 객체가 다른 객체를 가지는 것. (코드 재사용에 있어, 상속과는 다른 방식)

왜 이런 원칙이 생겼을까?

철수가 태초에 클래스 하나짜리 프로그램을 만들었다.
이 한 클래스는 너무 커서 불편했다. 그래서 철수는 생각했다.
"쓰지 않는 정보가 너무 많네? 서로 다 섞이는군... 필요한 정보끼리 모아서 쓰면 좋지 않을까?"
그래서 "추상화"를 해서, 필요없는 정보를 제거하고, 하나였던 클래스를 몇개로 나누었다.
각 클래스는 각자에게 필요한 정보만 남으니, 생각하기가 너무 편했다.

클래스를 나눠 쓰는것이 좋다는걸 깨달은 철수는 여러 클래스를 만들게 되었다.
그런데 클래스가 서로를 사용하고, 내부값을 변경하다보니 의존성이 강해지고, 상태가 꼬이는 문제가 생겼다.
철수는 다시 생각했다. "지금 만드는 클래스가 다른 클래스의 내부를 몰라도, 쓸 수 있으면 좋겠는데?"
그래서 정보를 모으고, 숨기는 "캡슐화"를 하니, 객체 내부에 대한 고려를 할 필요 없이 쓰기만 하면 되니까 편했고,
클래스 간의 의존성도 서로 수십개씩 연관되어 있던걸 대부분 떨어뜨려서, 한 두개로 줄게 하니, 생각하기가 참 편했다.

그런데 어느 날 중복된 코드를 가진 클래스들이 생겼다.
캡슐화 때문에 클래스 내의 정보를 알 수 없기 때문에 복사하고 붙여넣은 것이다. 철수는 다시 생각했다.
"중복된 애들을 모아놓으니까 똑같은데? 코드를 한번만 쓰면 좋겠는걸?"
그래서 중복을 제거하기 위해 상속이란걸 생각했다.
그래서 중복된 코드를 "일반화" 해 필요한걸 하나로 모으고, 중복된 클래스들이 그것을 상속하게 하니 보기에 참 좋았다.
이래서 "세분화" 같은 원칙은 없는 것이다.

근데 또 문제가 생겼다.
일반화 시켜서 상속은 했는데, 막상 쓰려니 다시 구분해야 하는게 귀찮았던 거다. 철수는 다시 생각했다.
"그냥 일반화 된 걸로도 쓸 수 있으면 좋을 것 같은데?"
그래서 객체에게 본체여도 일반화 된 모습으로도 쓸 수 있게하는 "다형성"을 갖게하니 각기 다른걸 따로 생각하지 않아도 되니 더욱 편해졌다.

한참이 지난 뒤, 철수는 자신의 코드를 까먹고, 기존의 클래스와 비슷한 클래스를 몇개 더 만들게 되었다.
결국 코드 중복이 발생했는데, 제거하려고 보니 일반화할 필요가 없던 것이다.
"이건 다 같은 일이니, 한 클래스가 다 일을 처리할꺼면 그냥 그 클래스에게 일을 위임시키면 되겠네?"
그래서 객체를 포함시켜 사용해오던 것을 "객체 결합" 이라고 부르며 적극적으로 써보니 참 편했다.

원칙은 결국 "보기 좋게" 해서 "생각하기 편하게" 하려는 것이다.
그러면 더 적은 시간에 더 많은 일을 해낼 수 있으니까.

추상화는 하나의 클래스를 작게만들고, 서로 연관된 정보는 모아 보기좋게 한다. (어떻게 보면, 인간이 하는 클러스터링이다)
캡슐화는 클래스간 의존성을 줄여 생각하기 수월하게 한다. 100개의 고민을 2개로 줄이는 거다.
상속은 비슷한 것끼리 비슷한 속성을 가지는 걸 일반화시켜 모으로, 그 속성을 가지게 한다.
다형성은 상속으로 일반화 시킨것을 세부화시킬 필요 없이 그대로 써도 되도록 하여 편하게 한다.
객체 결합은 이미 있는 클래스를 다른 클래스에 포함시켜, 코드를 재활용 하는 것이다.

UML 화살표

원칙을 사용하는 것은 결국 "쉬운 설계"를 위함이므로, 설계 때 사용할 기호를 알아야 한다.

  • 각각은 UML 표시로는 구분이 가능하나, 언어 내에서는 잘 구분되지 않는다.

Class간의 관계강도를 설명하자면, 약한 것부터 강해지는 순서대로 설명하겠다.

  1. 의존(Dependency): A --> B (가장 약한 결합 )
    A가 B를 참조(references)한다
public class A { 
    void Baz(Bar bar) {
    } 
};
  1. 직접연관(Directed Association): A ㅡ> B
    A가 B를 사용하는 경우.
    서로 연관은 있지만, 없어도 되는 관계
    [사람 ㅡ> 자동차] 있을 수 도 있고, 없을 수 도 있고.
public class Order {
    private Customer customer;
}
  1. 연관(Association): A ㅡ B
    직접연관이 서로 되어있는 경우이다. <->를 ㅡ 로 표시한다.
    A는 B를, B는 A를 서로 사용한다
    상호 직접연관 관계로, 멤버변수로 가진 것.
    그래서 직접연관보다 그냥 연관이 더 강하다고 볼 수도 있겠다.
public class Computer {
    private Moniter moniter;
}
public class Moniter {
    private Computer computer;
}
  1. 집합, 집합연관(Aggregation): ◇-
    A ◇- B 일 때, A는 B를 가져야만한다. 화살표 머리촉이 생략된 모습으로, 네모 부분이 꼬리다. (◇->)
    외부에서 생성된 B를 빌려쓴다.(has-a + whole-part) 그래서 속이 비어있다.
    A가 B를 만들진 않으며, A가 사라진다고, B가 사라지지도 않는다.
    또 B는 다른 곳에서 사용할 수 있다.
    [자동차 ◇- 바퀴] 자동차가 없어도 바퀴는 존재할 수 있다.
    [은행 ◇- 고객] 은행이 없어져도 고객(정보)는 사라지지 않는다. (뭐?😲)
    JAVA에서 보자면, B는 A외 다른 곳에서도 사용될 수 있다.
    이 때 아마 B는 인터페이스일 것이다.
    [바퀴 ◁ㅡ 여름타이어, 겨울타이어, 레이싱타이어 등]
public class Foo { 
    private Bar bar; 
    Foo(Bar bar) { 
       this.bar = bar;
    }
}

//OR

public class PlayList {
    private List<Song> songs;
}
  1. 합성, 복합연관(Composition): ◆-
    A ◆- B 일 때, A가 B를 직접 new하여 소유(own)한다. 그래서 속이 꽉 차있다.
    A가 전체(손바닥) B가 부분(손가락) 개념으로, A가 사라지면 B도 사라진다.
    ◆가 머리가 아니라 꼬리다. ◆- == ◆->
    [손 ◆- 손가락] 손이 없으면 손가락도 없다, 왼손을 펴보자. 기호랑 똑같이 생겼을 것이다.
    [은행 ◆- 고객] 은행이 없어질 때, 고객(정보)도 같이 사라진다. 😀
    JAVA에서 보자면, A만 B를 사용한다. A가 사라지면 B는 가비지콜렉팅 당할 것.
public class Foo {
    private Bar bar = new Bar(); 
}

//OR

public class Apartment{
    private Room bedroom;
    public Apartment() {
       bedroom = new Room();
    }
}
  1. 실체화(Realization, Implementation): --▷
    A --▷ B 일 때, A가 B를 구현한 구현체이다.

  2. 일반화, 상속(Generalization): ㅡ▷ (가장 강한 결합)
    A ㅡ> B 일 때, A는 B를 상속받는다.

화살표 모양만 관계강도 순서대로 두면 아래와 같다.

  1. -->
  2. ㅡ>, ㅡ
  3. ◇-, ◇->
  4. ◆-, ◆->
  5. --▷
  6. ㅡ▷

연관(Association), 집합(Aggregation), 합성(Composition)은 어떻게 구분되는가?

JAVA 코드로는 연관과 집합이 구분되지 않지만, 합성은 구분된다.(합성은 new, delete시 같이 생성, 삭제된다)
개념적으로는 아래와 같은 포함관계로도 볼 수 있다.

참고
What is the difference between association, aggregation and composition?

디자인 패턴

SOILD 원칙이란?

2000년대 초반 로버트 마틴이 책으로 써낸 것을 요약한 것.

  • S: SRP: Single Responsibility Principle: 단일 책임 원칙
    한 클래스는 하나의 책임만 다해라. 책임 갯수는 곧 클래스 갯수가 된다.
    책임이 너무 크다면 나눈다.
    생각해보자. 뛰어난 리더가 나타나면 모든 사람이 그에게 의존한다. 책임을 안나누면 리더는 과로로 쓰러질 것이다.

    • 언제써요?
      • 클래스를 만들 때마다.
    • 어떻게 써요?
      • 추상화와 캡슐화의 기준을 책임으로 나눈다.
        책임의 크기는 어느정도로 하면 좋을까?
        • 적당한 정보만 갖도록, 적당한 의존성만 갖도록, 최종적으로 적당한 클래스의 갯수가 나오도록 나눠야 한다.
          개발자가 한눈에 이해할 수 있다면 충분히 작은 크기일 것이다.
          책임 1개에, 의존성 1개?
      • 책임을 직접 지지 말고 해당 책임을 가진 객체를 활용하거나, 만들어서 포함해 사용한다.
    • 연관된 OOP는 뭔가요?
      • 추상화 -> 하나의 책임을 지는데 필요한 정보는 어떤 것들이 있는가? 너무 많거나 적진 않은가?
      • 캡슐화 -> 하나의 책임을 지는데 이정도면 충분히 숨겼는가? 의존성이 적게 생길 것 같은가?
      • 다형성 -> 한 Type은 하나의 책임만 가진다.
      • 객체 결합 -> 다른 책임을 진 클래스를 사용한다.
    • 어떤 클래스는 큰 책임을 져야 할 것 같은데요?
      • 그러면 각 책임을 가진 클래스들을 포함하여 문제를 해결한다.
        책임은 커도 코드는 간단한 클래스를 만들자.
    • 안지키면 어떻게 되는가?
      너무 많은 클래스가 큰 책임을 가진 클래스에 의존하게 된다
      = A를 건드렸는데 B가 터진다 = 이게 왜 터지지? = 끝도없는 디버깅 = 끝도 없는 수정 = 과로
    • 단점이 있나요?
      • 책임을 너무 많이 나누면 클래스가 너무 많아질 수 있다.
      • 너무 작게 나누면 성능이 나빠질 수 있다.
  • O: OCP: Open/Closed Principle: 개방-폐쇄 원칙
    클래스는 확장에는 열려있고, 변경에는 닫혀야 한다.

    • 언제 써요?
      • 수정이 너무 빈번할 때
      • 수정사항이 수정이 아닌 어떤 Type을 새로 만드는 방식으로 해결 가능해 보일 때
    • 어떻게 써요?
      • 상속, 인터페이스, 추상클래스 등 다형성을 쓴다. 이미 만든 클래스는 변경이 필요없도록 한다.
      • 새로운 type을 늘리는 방식으로 확장가능하게 해라.
    • 연관된 OOP는 뭔가요?
      • 다형성 -> 다형성을 갖게해라.
      • 상속: LSP 원칙을 지킨다면 쓸 수 있을지도...
    • 안지키면 어떻게 되는가?
      • 뭐 하나 추가되면 반드시 수정해야 한다 = 끝도없는 수정이 발생한다.
    • 단점이 뭔가요?
      • 인터페이스 쓰는 비용이 크다.
  • L: LSP: Liskov Substitution Principle: 리스코프 치환원칙
    상속은 아무때나 쓰는 것이 아니다.

    • 그럼 상속은 언제 써요?
      • B 대신 A를 써도 말이 된다면, 상속이 가능한 관계이다.
        부모와 자녀를 치환해봤을 때도 성립하면 상속이 맞다. == 일반화로서의 상속이다.
        자녀가 부모에게 돈을 줘도 상속세를 내야 하듯이?
    • 일반화는 아닌데, 상속을 해야만 할 것 같은 관계라면요?
      • 포함관계를 사용하는 걸 검토해보자. 답이 있을 것이다.
    • 연관된 OOP는 뭔가요?
      • 상속 -> 함부로 쓰지 마라
    • 안지키면 어떻게 되는가?
      • 일반화가 아니니 이도저도 아닌 상속관계가 성립된다. -> 상속을 푸는 것 외에 해법이 없다. -> 가다보면 OCP도 깨지게 된다.
        가능한 빨리 리팩토링을 해야한다.
      • 실제로 Java 의 Date 라이브러리가 이 문제를 가지고 있다.
        날짜와 시간이 상속관계... 날짜는 시간이 없는데요? -> 그럼 터트리자 -> Exception -> 모든 JAVA 개발자들의 고통
    • 단점이 있나요?
      • LSP를 만족한다고 해도 상속보다 포함관계를 쓰는 편이 낫다.
      • 일반화가 가능하다면 인터페이스를 쓸 수도 있다. 인터페이스 쓰는게 아마 백번 나을꺼다.
  • I: ISP: Interface Segregation Principle: 인터페이스 분리원칙
    하나의 큰 인터페이스를 쓰지말고, 나눠라. 불필요한 메서드를 구현하지 말라.

    • 뭐가 좋아져요?
      • 구현해야 하는 불필요한 인터페이스 함수가 없어진다.
      • 약간 핀트가 어긋난 것 같은 의존관계가 딱 맞게 된다.
    • 언제 써요?
      • A -> B 의존 관계인데, A가 B에서 의존하지 않는 함수가 있을 때.
      • 불필요하게 구현해야 하는 인터페이스 함수가 있을 때.
      • 인터페이스를 만들고 관리하는 비용 대비 효과가 있을때
    • 어떻게 써요?
      • 의존하는 함수만 모은 인터페이스를 만든다.
      • SRP를 해서 책임을 한번 더 나누자.
    • 연관된 OOP는 뭔가요?
      • 추상화를 더 작게 고려한다.
      • 캡슐화를 더 작게 고려한다.
      • 다형성을 가져라.
    • 클래스만 많아지고, 이득이 없는 것 같을 때는요?
      • 불필요하게 구현해야 하는 함수가 없다면 나눌 필요가 없다.
    • 안지키면 어떻게 되나요?
      • 만들지 않아도 되는 함수를 반드시 구현해야 하게 된다.
      • 의존하지 않는 함수를 호출하게 되면 터진다.
    • 단점은 뭐예요?
      • 인터페이스를 만드는 비용이 비싸다.
  • D: DIP: Dependency Inversion Principle: 의존관계 역전원칙
    • 언제써요?
      • A -> B -> C 같이 A가 C까지 깊게 의존하는 엉망인 관계라면... 의존관계를 역전시켜라
      • 모듈, 라이브러리를 만들 때
    • 어떻게 해요?
      • 인터페이스나 추상클래스를 써서 ◁- 관계를 추가해라.
        (B를 인터페이스로 만들고, 그 구현체인 B1을 만들어, B1이 B인터페이스에 의존하게.)
      • A -> B' ◁- B1 -> C' ◁- C1
      • 라이브러리 사용은 인터페이스로만 하도록 권장한다.
    • 뭐가 좋아지는 거예요?
      • A가 C를 직접 호출하지 않으므로, C가 변경되어도 A를 변경하지 않아도 된다.
      • 변경에 따른 수정을 덜하게 된다.
      • 캡슐화가 강해진다.
    • 연관된 OOP는 뭔가요?
      • 다형성 -> 다형성을 가져서, 캡슐화를 좋게해라.
      • 캡슐화
    • 잘보면, 의존관계를 정말로 역전시킨게 아니다. 역방향인 인터페이스 의존관계를 추가캡슐화를 보강한 것
      • 어찌보면 그냥 더 복잡해졌을 뿐이다...
        하위 클래스인 B, C가 캡슐화가 덜 되었던 것을, 다형성을 가진 인터페이스로 강화시켜서, 해결했다고 볼 수 있다.
        이경우 B, C를 인터페이스로 만들면 A는 C가 바뀌어도 영향을 받지 않는다.
    • 안지키면 어떻게 되나요?
      • 저수준의 변경이 main 코드의 에러를 발생시킨다.
      • 유틸리티를 바꿨을 뿐인데 서비스가 죽는다
    • 단점은 뭔가요?
      • 인터페이스를 만드는 비용이 비싸다.

JAVA사용 측면에서 요약하자면

  • SRP: 항상 책임을 나눠라.
  • LSP: 상속은 가능하면 쓰지말라.
  • LSP, OCP, ISP, DIP: 인터페이스를 쓰는 비용보다 리팩토링 비용이 쓸만하면 써라.

사실 SOILD 원칙을 정말 잘 고수하면 "좋은 설계"가 나오게 된다.
디자인 패턴은 오히려 "보지못한 설계, 구성방법, 구조"를 이름 붙여서 부른다는 것이 더 맞는 표현이다.

대표적인 디자인 패턴모음 2개

  1. GRASP 패턴
    패턴이라기보단 원칙이다.
    책임 할당에 대한 9개의 원칙
  2. GoF 패턴
    23개의 패턴에 이름을 붙였다.
    다만 오래된 패턴이라, 원래 모습에서 변형된 패턴도 존재한다
    (* 예를들면 옵저버 패턴은 라이브러리 자체가 Deprecated 되었고, 지금은 구독-발행 형태의 패턴으로 변형되었다. 개념은 비슷하다.)

GRASP

  1. Information expert
  2. Creator
  3. Controller
  4. Indirection
  5. Low coupling
  6. High cohesion
  7. Polymorphism
  8. Protected variations (OCP)
  9. Pure fabrication
profile
서버도 하고 웹도 하고 시스템이나 인프라나 네트워크나 그냥 다 함.

0개의 댓글