조영호님의 우아한세미나

2021-05-09

설계란 무엇인가?

코드를 어떻게 배치할까 의사결정 이다. 어떤 클래스에 어떤 코드를 넣을 것인지에 관련한

그럼 어떻게 코드를 넣어야 할까?

  • 변경의 초점에 맞춰서
  • 의존성에 따라서

의존성이란 무엇인가?

의존성 사진

A가 B를 의존한다고 할 때, 다음과 같이 그린다. 여기서, B가 변경될 때, A도 영향을 받는다. 그러므로 의존성은 변경과 관련된 일이다.

그러나 항상 B가 변경된다고 A가 변경되는 걸까? 만약 설계를 잘했으면, B의 내부구현이 바뀐다고 A가 변경이 일어나지 않을 수 있다.

의존성 == 변경에 의해서 영향을 받을 수 있는 가능성

  • 클래스 사이 의존성
  • 패키지 사이 의존성

클래스 의존성의 종류는 4가지가 있다.

의존성 종류

  • 연관관계

    class A {
      private B b;
    }
    
  • 의존관계

    class A {
      public B method(B b) {
        return new B();
      }
    }
    
  • 상속관계

    class A extends B {
    }
    
  • 실체화관계

    class A implements B {
    }
    

패키지 의존성

의존성 종류

설계

도메인 컨셉

이 개념들이 런타임 시 다음과 같은 모양으로 바뀐다.

도메인 오브젝트

주문을 하게 되면 다음의 컨셉을 갖는다.

도메인 주문 컨셉

주문도 런타임 시 이런 모양으로 바뀌게 된다.

도메인 주문 오브젝트

메뉴와 주문을 붙이면 다음의 모습이 된다.

도메인 오브젝트 매뉴와 주문

가게 라는 객체를 중심으로해서 메뉴와 주문이 런타임에 객체들의 관계가 엮인다.

설계 문제점

메뉴선택 문제점

사장님이 메뉴를 1인세트를 등록했다고 가정하자.

그럼 왼쪽의 앱 화면처럼 1인세트가 노출이 된다.

그런데 유저가 1인세트를 선택하고, 장바구니에 담았다. (장바구니정보가 서버에 있지않고, 로컬에 있다고 가정)

그런데, 그 후에 사장님이 1인 세트의 메뉴의 이름을 0.5인세트로 변경후, 가격도 반값으로 변경을 했다.

이렇게 되면 사용자가 담았던 장바구니의 메뉴와, 서버에 있는 메뉴와 불일치가 일어난다.

주문 불일치

주문 검증

주문 검증

  • 사장님이 등록한 메뉴의 이름과 주문으로 들어온 메뉴의 이름이 같은지 검증
  • 옵션그룹의 이름과 주문옵션 그룹의 이름이 같은지 검증을 해야한다.
    • 짜장면 8,000원등록했는데, 짬뽕 8,000원으로 변경이 일어날 경우를 위해서 이름까지 검증해야한다.
  • 옵션의 이름과 주문옵션의 이름이 같은지 검증
  • 옵션의 가격과 주문옵션의 가격이 같은지 검증
  • 장바구니에 담아놓고 한참 뒤에 주문버튼을 눌렀을 경우를 위해 가게가 영업중인지 확인
  • 최소주문금액에 만족하는지 검증

주문 검증 로직 협력 설계

주문 검증 로직 협력 설계

처음 시작은 주문하기() 메세지가 주문 객체로 전송이 된다.

그 다음 validation 로직을 모두 태워야 한다.

클래스 다이어그램

클래스 다이어그램은 이렇게 나온다.

관계를 설정할 때는 방향성이 필요하다

의존성의 관점에서 이 설계를 살펴보자.

개발이 어려운 이유는 위에 그림은 메모리상에서 돌아가는 동적인 구조이다. 객체들이 메세지를 주고받고, 객체가 생성되고, 소멸된다는 것은 시스템상에서 실제로 실행되는 구조이다. 시간이라는게 들어감. 굉장히 변화무쌍하다.

실제로 개발자는 이변화무쌍한 가능성들을 다 정적인 코드로 담아야 한다. 되게 많이 변경이 될 수 있는 모든 가능성들을 관계, 메소드 , 로직 등 정적으로 만들어져야 한다.

정적인 구조를 만들었지만, 코드를 짤때, 정적인 무언가를 찾아야 한다. 그것이 의존성이다.

어떤 객체가 어떤 식으로 의존을 할거야 라는 걸을 실제 코드 상의 정적으로 그려져야 하고, 그걸 관계라고 한다.

런타임에 클래스 인스턴스가 다른 클래스 인스턴스로 어떤식으로 협력이 이루어지는걸 암시하는 것이다.

객체에서는 관계가 필요하다. 의존성이란 것은 어떤애가 어떤애한테 의존해 라는 것이다.

Source로 부터 Target이 필요하다. 어떤방향으로 흐르는지, DB랑은 다르다 fk가 있으면 방향성이 없음, 아무데나 갈수 있음.

방향성을 결정하는것이 굉장히 중요하다. 관계를 결정하는건, 런타임시 협력이 어떤 방향으로 흐르는지로 결정한다.

어떤 객체가 어떤 객체에게 메세지를 보내는지로도 결정한다.

  • 데이터베이스 경우 fk를 잡아만 두면 양방향으로 흐를 수있다.

관계 방향성

방향성을 결정하는 것이 굉장히 중요하다. 관계를 결정하는 것은 막 하는 것이 아니라, 객체가 어떻게 협력하는지, 런타임 시 객체들이 어떤 방향으로 협력하는지 라는 걸 바탕으로 해서 관계를 잡아줘야 한다.

객체와 객체사이의 어떤식으로 협력하는지, 어떤 메세지를 보내는지를 바탕으로 관계의 방향을 잡아줘야한다. 그것이 바로 의존성의 방향이 된다.

관계의 방향 = 협력의 방향 = 의존성의 방향

의존성 방향

상속관계와 실체화 관계는 매우 분명하다, 왜냐하면, 상속관계에 경우, extends가 보이면 상속관계이고, implements가 보이면 실체화 관계이기 때문이다.

연관관계

연관관계가가 있는데, 내가 협력을 하려고하는데, 어떤 객체 사이의 영구적으로 탐색구조를 잡아줘야할 때, 대부분 데이터 구조를 따라 가지만, 원한다면 끊을 수는 있다.

어떤 객체에서 어떤 객체로 빈번하게 가야하고, 그 경로를 영구적으로 박아놓는게 좋다고 생각이 들면 연관관계로 설정하면 된다.

연관관계 설정

의존 관계

의존 관계는 일시적인것, 그 객체가 그객체로 가는 경로가 필요 없다. 여기서 OptionGroupSpecification에서 OrderOptionGroup을 일시적 파라미터로만 받고 끝나버린다.

의존관계 설정

여기서 중요한거는 연관관계냐 의존관계냐 설정이 아니라, 방향성이 중요하다.

내가 참조를 할때는 항상 이유가 있어야 한다. 내가 연관관계를 넣는이유, 내가 의존관계를 넣는 이유는 런타임 객체들이 어떻게 협력하느냐에 따라 달라진다.

연관관계를 더 알아보자

연관관계 = 탐색가능성

연관관계는 탐색가능성을 말한다.

Order에서 OrderLineItem으로 연관관계를 넣었다는 것은 Order라는 객체를 통해서 내가 원하는 OrderLineItem을 어떤 방식으로 찾아갈 수 있다.

연관관계와 협력

두 객체간의 통로가 굉장히 영구적으로 유지가 되어야 해 라는 판단 근거가 있어야 연관관계를 사용한다.

연관관계 구현법

일반적으로 연관관계를 구현하는 방법은 위와 같이 객체 참조를 사용하는 것이다.

일반적이라고 얘기하는건, 다른방법으로도 구현이 가능하다는 말이다.

연관관계라는 개념이 있꼬, 객체참조라는 구현이 있다. 개념이 무조건 1가지의 구현만 있는게 아니다.

구현시작

연관관계 구현법

주문하기의 협력은 Order라는 객체의 주문하기()라는 메세지가 도착했을 때 시작된다.

어떤 객체가 어떤 메세지를 받는 다는 것은 그 객체의 public 메서드로 구현이 된다는 것이다.

메서드가 필요한 이유는 메세지를 받기 위해

위에는 2가지 로직이 있음

  • 주문이 올바른지?
  • 주문의 상태를 바꾸는..

연관관계 구현법

그러고나서, Order라는 객체가 Shop과 OrderLineItem쪽으로 이동할 수 있어야 한다.

(주문이, 가게와 주문항목 쪽으로 메세지를 보낼수 있어야 한다.)

저자는, 이 이동이 영구적인 관계라고 판단했다.

연관관계 구현법

두번 째는 validate 메서드를 구현한 것이다.

가게 validation

객체 협력

위와 같은 그림이 완성 됐다.

구현 2

그 다음 각각의 주문항목들이 옳바른지, OrderLineItem에게 validate 메세지를 보내고, OrderLineItem이 menu에게 메세지를 보낸다.

객체 협력2

  • Order에서 OrderLineItem으로 validate() 메세지를 보내고
  • OrderLineItem에서 Menu로 validateOrder() 메세지를 보낸다.

객체 협력3

위에 그림은 Menu 클래스에서 validateOrder()를 구현한 코드이다.

메뉴의 이름과 주문항목의 이름이 같은지 비교를 한다.

이 플로우가 아래의 그림처럼 된다.

객체 협력4

그리고 나서 OptionGroupSpecification에서 옵션그룹의 이름과 주문옵션그룹의 이름이 같은지를 확인한다.

객체 협력5

객체 협력6

그리고 난 후 Option쪽으로 메세지를 보내서 OptionGroupSpecification과 비교를 한다.

객체 협력7

최종 모습

객체 협력8

이 객체간의 관계는 레이어 아키텍처상에 도미엔 레이어에 속한다고 보면 된다.

코드를 구현하려고 하면, request을 받고, DB에도 저장해야하는데, 이렇게 되면 도메인 영역에 벗어난 객체도 구현을 해줘야 한다.

여기서는 서비스와 인프라 관련 객체만 구현을 했다.

레이어 아키텍처 -1

레이어 아키텍처 -2

요청에 OrderService에 Cart라는 객체로 왔다고 가정해보자.

받은 Cart 객체를 OrderMapper.mapFrom() 을 이용해서 Cart -> Order로 형변환을 한다.

Order로 형변환하면 최종모습의 Flow가 나온다. Order.place()


양방향 의존성을 피하라

의존성 종류

A가 바뀌면 B도 바뀌고 B가 바뀌면 A도 바뀐다. 이건, 한 클래스에 있어야 할 코드들을 억지로 찢어논것이다.

다중성이 적은 방향을 선택하라

의존성 종류

의존성이 필요 없다면 제거하라

의존성 제거

패키지 사이의 의존성 사이클을 제거하라

의존성 종류

A pacakge -> B pacakge , B pacakge -> C pacakge, C pacakge -> A pacakge 가 되면 사이클이 생기는데, 이러면, 이 세개의 pacakge들은 한 pacakge 있어야 된다. 같이 바뀌기 때문.


설계 개선

Q. 설계를 어떻게 개선을 해야하는지 많이 물어보시는데, 클래스 하나만 보여주신다.

거기서 정말 해드릴 수 있는게 적다. 이름이나, 메소드의 사이즈나, 이런것들에 관한것만 말하다 끝나는데, 객체들이 어떻게 협력이 되고, 그 메소드가 그 클래스에 있는게 맞아? 이런것들을 해드려야하는데, 많이 못해준다.

그래서 해드릴수 있는 말은,

  • 의존성이 어떻게 되어있는지 보세요.

항상 코드를 짜면 종이에다가 의존성을 그려본다. 의존성을 그려봤을 때, 이상한게 있으면, 그 코드는 이상한 부분이 많은 것임.

최종 코드 결과물은 굉장히 이쁘게 보이지만, 그 과정을 겪는 모습은 굉장히 더럽다.

저자도, 처음엔 도저히 어떻게 나눌지 생각이 안나서 먼저 절차적으로 짤 때가 많다고한다. 다 짠 다음, 객체들의 의존성을 다이어그램으로 (손) 으로 그려보면 이상한 의존성들이 보인다고 한다.

설계를 개선할 때는 무조건 의존성을 그려보세요.

  • 내 코드에 의존성이 어떤 모양을 갖고 있지?
  • 패키지 끼리 사이클은 도나?
  • 패키지를 잘못 나눴나?

패키지라는 말이 기술적으로 들리지만, 기술적인것이 아니다, 도메인에 개념에 맞게 패키지 이름을 부여하고, 어떤 클래스를 넣어야할지 결정해야 한다.

구현의 문제점

문제점 -1

의존성 다이어그램

이 의존성 다이어그램의 문제점은 사이클이 돈다는 것이다.

의존성 사이클 문제

domain 레이어 안에, Shop과 Order사이의 사이클이 돈다.

패키지 의존성 문제

Shop과 Order사이의 양방향 관계가 성립이 된다.

내가 order 패키지에 있는 OrderOptionGroup이나, OrderOption을 고칠 때, OptionGroupSpecification이나, OptionSpecification도 같이 고쳐야 하고,

그 반대도 마찬가지다.

패키지 의존성 문제

  • Order 패키지는 Shop패키지에 있는 Shop 클래스에 의존성이 걸려있음

  • Shop 패키지는, Order패키지에 있는 OrderOptionGroup과 OrderOption 클래스에 파라미터로 의존성이 걸려있음.

해결법 - 1

해결법1

  • OptionGroupSpecification은 OptionGroup을 파라미터로 받는다.
  • OrderOptionGroup은 OptionGroup으로 형변환을 한다.

해결법2

이렇게 작성하면 코드가 shop 패키지 한쪽으로 흐르게 된다.

해결법3

이상해 보일 수 있는데, 추상화를 시킨 부분이라고 한다.

추상화

사람들은 추상화란 개념이 인터페이스나, 추상클래스만 있다고 오해하고있음.

추상화란 무엇이냐? 바로 잘 변하지 않는 것

어떤거에 비해 어떤 거는 잘 변하지 않는다면 추상적인 것!

OptionGroup과 Option은 클래스지만,

OptionGroupSpecification이랑 OrderOptionGroup 보다는 OptionGroup이 더 추상적.

객체 참조의 문제점

2problem-1

객체참조를 연관관계를 구현하면 다양한 이슈들이 발생한다.

2problem-1

객체들이 다 연결되어 있어서, 객체를 어디든지 탐색이 가능하다.

메모리상에 있을 땐 이슈가 되지 않지만, ORM일 때 문제가 생긴다.

OSIV도 연관관계의 대한 문제

  • 객체참조를 어디까지 해야해요?
  • Lazy로딩 이슈를 어떻게 해결해요?

근본적인 문제는 객체가 다 연결되있음

2problem-1

연관된 객체를 수정해야 한다면,

  • Order 객체를 가져온 후
  • 전부 다 찾는다음
  • Set을 해준다

하지만 트랜잭션이 길어진다.

2problem-1

  • 트랜잭션의 경계도 모호해짐

예제

지금까지 살펴본 케이스는 주문완료 된 상태를 본 것

  • 주문이 완료되려면 결제를 해야함
  • 결제를 하고나면 배달완료

이 플로우를 살펴보자.

결제1

결제를 하는 서비스로직이다.

  • Order의 상태를 변경하고
  • Delivery의 상태를 바꾼다.

결제3

배달이 완료 됐다고 가정하자.

배달1

  • Order의 상태를 바꾸고
  • 가게쪽에서 수수료를 부가한다.
  • Delivery 상태를 바꾼다.

이 로직의 메소드를 보면 @Transactional 어노테이션이 있기 때문에, 하나의 트랜잭션 안에서 이뤄진다.

트랜잭션

업데이트가 하나의 트랜잭션 안에 이루어진다.

트랜잭션

하지만 이 3개의 객체가 변경되는 주기가 다르다.

  • 사장님은 내 가게를 영업중으로 바꾸는 주기
  • 주문은 사용자가 주문취소하는 등 주기가 있고,
  • 배달도 상태가 바뀌는 주기가 있음

그래서 롱트랜잭션으로 묶으면 성능저하가 있음.

객체 참조가 꼭 필요할까?

객체 참조는 결합도가 가장 높은 의존성이다.

그래서 반드시 깨야하는 의존성이기도 하다.

강의 내용이 많아서 여기까지만 정리합니다.

강의

https://www.slideshare.net/baejjae93/ss-151545329?from_action=save

https://www.youtube.com/watch?v=dJ5C4qRqAgA&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9CTech