📜 제목으로 보기

  • 참고 유튜브 : https://www.youtube.com/watch?v=navJTjZlUGk
  • 정리본: https://github.com/LenKIM/object-book
  • 코드: https://github.com/eternity-oop/object
  • 책(목차) : https://wikibook.co.kr/object/

  • 참고 유튜브 : https://www.youtube.com/watch?v=navJTjZlUGk
  • 정리본: https://github.com/LenKIM/object-book
  • 코드: https://github.com/eternity-oop/object
  • 책(목차) : https://wikibook.co.kr/object/

  • 책 12장은 range bind 개념으로 여기선 건너띈다.
  • 13장 contract와 관련된 개념을 다룬다.

계약에 의한 설계

  • 계약은 짠 코드가 장난감인지 vs 제품인지 판가름하게 된다.
    • 계약하지 않는 코드는, 돌아가게 스냅샷만 짜서, 장난감 코드가 대부분이다.
  • 단위테스트를 안걸어도 안정적인 코드를 짜려면??
    • 계약에 의한 프로그래밍으로 경우의 수가 없어야한다.
    • 경우의 수가 있는 코드들은, 그만큼만 커버해서, 다른 경우의수에 불안정하다

계약의 조건 4가지

image-20220712111915576

  • 객체지향의 세계에서는 메세지를 통해 계약한다.
    • 메세지를 보내는 sender
    • 메세지를 받는 receiver사이에 더 상세한 내용이 계약으로 있다.
      • 범위, 시간, 등 다 계약으로 정해놔야한다.
  1. 전달받은 메세지의 규격 = precondition을 지켜줘야한다.
    • sender가 규격을 지켜서 보내진 않는다.
    • precondition검증은 receiver가 한다.
    • 우리는 receiver가 메세지를 전달받아 규격(precondition)을 검증하는 것을 기본정책으로 하고 있다. 안맞으면 안받는다.
  2. 전달할 메세지의 규격(postcondition)
    • receiver가 받아 검증했지만, 자기가 보낼 것도 receiver 안에서 검증해야한다.
    • receiver는 내 결과물을 상대방에 전달하기 전에 검증해야한다.
  3. 객체 자신의 규격(class invariant)
    • 객체는 원래 상태를 가지는데, 메세지를 받았을 때, 특정 필드값을 만족할 때만 제대로 메세지가 처리된다.
    • 메세지를 받기 이전에, 불변성invariant 검증이 되어있어야한다.
      • 내 상태조건이 변하지 않도록 확정되어서 메세지 처리가 가능한지 검증
  4. 위임된 책임의 context
    • 내가 하는 짓이 이 context가 맞는지
  • 4가지 조건이 다 만족되어야 계약이 가능하다.

01 불변성(invariant) 검증

image-20220712112918519

  • 메세지를 주고 받기 전에, (setter등으로 상태가 변화한다면 thr로) 객체 자신은 불변성 검증을 통해 불변성을 확보해야 메세지 처리가 가능하다
  • 객체 초기상태, 객체 메세지 수신 전 상태가 확립되어야한다.
    • 메세지와 무관한 객체의 상태필드값이다.
      • 메세지는 인자값(pre) or 리턴값(post) or 지역변수들(post)로 이루어진다.
    • 필드값들을 제대로 갖고 있는지 상태 점검 해야한다
    • 초기화 할당해서 확보 or DI로 위임해서 꽂아서 확보하던지 해야한다.
      • 객체 초기화는 프로그래머 역량에 달려있어서 믿을 수 없다. solution, computer에게 DI로 맡기자

Plan

  • 기존

    image-20220712113109006

    • 하나의 요금제를 감당하고 있는 Plan은 set으로 calls통화기록를 받아서 가지고 있다.
    • 이 calls를, 전략주입 구상클래스에게 넘겨줘서 계산책임을 위임하고, 결과를 받는다.
    • private필드, final을 준수하고 있으니 확장가능(상속을 해줘도) 안전하다
문제점과 해결(객체 필드 상태null -> setter호출 이전에 빈값으로 초기화)
  1. 주입받을 calculator필드는 초기값이 null이다. image-20220712113445989

    1. setter를 호출하지 않았따면, calculateFee호출시 NP에러가 날 것이다.
    2. 그러나 대외적인 인터페이스(class signature)를 보면, thr가 안달려있어서, 예외가 발생할 것이라 예상도 할 수 없다.(보고서 정상작동 하지 않을 것이라는 생각을 할 수도 없다)
    3. Calculator 내부를 보면, image-20220712114046938
      1. 부분적인 계산체인을 위한 calcs라는 전략객체 컬렉션 필드를 소유하고 있고
      2. 위임받은 책임인 calcCallFee를 보면, calls는 한꺼번에 받고, 초기값을 받아서, 누적해서 결과물을 낸다.
      3. 즉, 자신이 계산하는 것이 아니라, 자신 소유 전략객체들을 계산합으로 결과물을 내어준다.
  2. calculator는 생성자에 받아야하는 조건이 없으므로 null초기화로 인해 없는 상태를 방지하기 위해서 객체를 미리 넣어주는 방법으로 안정성을 확보할 수 있다.

    image-20220712114414774

    • 또한, calculateFee()의 calls 역시 미리 초기화된 상태라, 에러날 일이 없다
  3. 필드에 대해 유효한 초기화 방법은 필드 초기화 or 생성자로 할당하여 NULL대신 NULL을 대신할 수 있는 empty객체를 할당해주는 것이다.

02 사전조건(precondition) 검증 - 인자값 검증(전처리)

image-20220712114708054

  • 사전조건은 일반적으로 validation이라 부르며, 다른 언어들에서는 required라고도 부른다.
    • 넘어가기전에 위에서 검증하고 가
    • white list 이론: 38선을 경계로 위에서 방역/검증하고, 밑에서는 valdiation로직 없이 깨끗한 white list영역으로 취급한다.
      • 모든 인자들이나 검증되지 않는 값들을 위에서 검증하여 white list를 만든 뒤, 로직을 전개한다.
      • if로 return으로 짤라버리는 쉴드패턴
  • 메세지로 받는 값을 스스로 검증한다.
  • 내가 직접검증안하고, 형으로도 검증할 수 있다.
    • VO Money가 대표적인 예. Money클래스를 브릿지로 써서, integer를 중간에 형을 더 만들어서 안정성/불변성을 확보하는 형태
    • 검증때문에 한번더 감싸는 것

plan

image-20220712154225367

문제점과 해결(인자null or 인자의 상태null -> if로무시With불린flag토하기with기존값사용가능확인 or if thrWith메세지로통보)
  1. java는 모든 객체형에 대해 null이 가능하므로 NULL인자로 호출가능한 메서드들의 인자로 넘어온 객체메세지를 null검사안하는 것이 맘에 안든다.

    1. 제임스고슬링이 java에서 null을 만든 것을 실수라 한 것처럼, null은 모든 메서드의 시그니쳐를 무력화시킨다.
      • null이 값이 없다는 것이 문제가 아니라, null이 만능키라서 문제
  2. 첫번째 해결책: 언어나 컴파일러의 기능을 이용하여 인자에 @NonNull같은 어노테이션을 쓰자

    image-20220712154716736

    • 컴파일시, 컴파일러가 모든 코드를 조사해서 null이 생기면 컴파일에러로 거른다.
      • 컴파일 때가 아닌, 런타임상에서 null이 아닌 실제 인스턴스가 들어갈 수 있으며, 컴파일러는 런타임시점을 모르므로 제한이 있다.
      • 다른 필드나 지역변수들은 null이 허용되기 때문에, @NonNull인자외에 다른변수들이 runtime시 null을 만들어서 인자에 null이 들어갈 수 있다.
    • typescript, swift, kotlin 등과 같이 애초에 null이 올수 없는 변수를 쓰는 경우에는 해결이 되는 방법이다.
  3. 두번째 해결책: null이면 if로 무시 with 외부에서 알수 있는 boolean flag + 무시하고 기존값 써도되는지 확인 image-20220712155307224

    1. null이면 if로 무시하고 해당로직은 슬그머니 넘어가되 최소한 boolean값으로 결과를 응답해준다. 우리가 많이 하는 선택지
    2. 나빠보이지만, 자바컬렉션들은 add나 put시 if로 슬그머니 null을 넘어가며, 넘어간 여부를 boolean으로 대답을 해준다.
      1. 그렇다면, void가 아니라 최소한 boolean형으로 return하도록 수정해야만한다. 외부에서 알수 있는 flag를 세워줘야한다.
      2. 또한, if로 무시할 수 있는 상태 == 실패시 기본값(add시 빈컬렉션) 이미 존재하며 그대로 사용해도 되는 상태인지 확인도 해야한다.
        • 실패해도 기본값이 이미 초기화되어있으며, 그 것을 사용해도 되는지
        • 예를 들어, setCalculator에서 if로 무시해서, 기본calc를 사용하게 되는데, 요금이 250만원 넘게 나와버리면, 기본calc는 사용하면 안되는 상황이다. 이럴 경우, if로 무시하고 넘어가면 안된다.
      3. 근데, boolean으로 응답해줬다면, 밖에서 기존값 쓰면 안되어서 따로 처리하게 될 것이다.
  4. 세번째해결책: 예외로 처리 image-20220712160521184

    1. if로무시하며 boolean flag하는 것보다 더 강력하게, null이면 예외메세지로 통보하고 있다.
      • 참고로 if로무시하며 불린flag를 바깥에 토하는 것if thr로 멈추는 것둘다 runtime에서 조치할 수 있는 2가지 모든 것이다.
    2. 한번 null에 대한 정책을 thr로 태웠으면, if무시with불린flag토하기는 더이상하지말고, 다른 로직도 모두 thr를 태우도록 통일해야한다.
      • 어떤 것은 불린flag토하고, 어떤 것은 thr토하면 괴롭다
      • 정책을 세우면, 통일해서 일관성을 만들어야한다.
사전조건 검증(null인자)에 대한 정책 3+1가지 정리
  1. 언어를 바꾸거나 애노테이션을 쓴다.

  2. if를 통해 불린flag를 리턴하거나

  3. 명시적인 thr로 멈춘다.

    • 우리는 3번을 중심으로 한다.

    • 모든 인자를 가진 함수에 다 써야한다. 그래서 인자가 많은 함수가 귀찮다.

  4. 객체 메세지라면, 객체의 사용될 상태 검사(null/empty)

    image-20220712161651441

    • Calculator는 전략객체들을 컬렉션필드에 담고 있으므로, 컬렉션 필드가 비었으면, null이나 마찬가지인 놈으로서 해당필드 empty검사를 해줘야한다.
    • 객체는 자기자신에 대한 validation의 책임을 외부에 제공해야하므로 객체가 메세지로 사용된다면, 책임수행 중에 자기자신의 상태에 대한 validation책임도 늘어난다
      • validation때문에 main/client에서 보이지 않는 메서드들이 늘어나는 경우로서, 메세지로 들어가는 타class의 메서드 내부의 인자 사전조건 검사에 쓰이는 자기 상태검증용 메소드들이 늘어난다.

03 사후조건(postcondition) 검증 - (받아온) 결과값 검증(후처리)

image-20220712162559912

  • 일반적으로 결과값 검증이라고 한다.
    • 재고처리 시스템 등은 사후조건 검증을 빡빡하게 찬다.
  • 보내줄 값이 올바름을 검증한다.
  • return하는 형에 대해, 그 형을 만드냐 못만드냐로 검증을 미룰 수도 있음.

plan

문제점과 해결(결과값 검증 -> 도메인의 영역이다.)
  • 사후검증 대상은 calculateFee()이다.

    image-20220712162849249

    • 일반적으로 객체는 자기의 일을 직접수행하지 않고, 메세지를 통해 책임수행이 적합한 객체에게 위임해서 결과값을 받아온다. 받아온 결과값은 내가 한 것이 아니므로 반드시 검증한 뒤 반환한다
    • 계산책임을 위임받은 calculator가 결과를 반환해주는데, 받아서 다른 데 반환하기 전에 다시, 내가 검증을 해야하는데 사후조건 검증은 로직이 아니라 도메인의 영역이다.
      • Money를 반환할 예정인데, 0원이나 음수를 보내줘도 되는지 확인 -> call이 있다면 0원이하일 수 없다.
    • 위임해서 받아온 값은 반환하기 전에 내가 검증한다.

      • 계산재료인 call이 없었으면 모르겠는데, call이 있는데도 불구하고 && 돈이 0원이나 음수가 되면 thr

      • 도메인지식을 if에 올려 죽여버린다

      image-20220712164140977

계약별 책임할당

  • Plan이 현재 모든 계약조건을 다 검증하고 있다.
  • 하지만 Plan은 Calculator에게 계산책임은 위임한 상태이므로 같이 일하고 있는데, 모든 계약조건을 Plan이 다 검증해야할까?

사전계약조건 책임할당

  • 인자로 넘어왔을 때, 인자가 null인지 체크하는데, 인자의 상태체크는 물어보지말고 스스로 죽도록 위임할 수 있음.

precondition1) 자신의 상태null, 인자null체크는 상대방에게 위임할 수 없다.

  • 참고로, 인텔리제이는 precondition으로 인자 null체크를 넣어둔 메서드에 대해, null을 넣으면, 코드힌트에서 알려준다.

    image-20220712164920750

precondition2) 인자의 상태null/emtpy체크는? 해당 객체인자가 자신상태validation메소드를 제공해주는데?

image-20220712165114221

image-20220712165124254

isEmpty()는 간접적으로 내장을 까는 메소드. <물어보고 행동하면, 변화(시)의 여파가 찾아온다. 검증도 스스로 죽어라고 시킨다(위임)>
  • 간접적으로 내 안의 컬렉션필드의 size를 알려주는 코드라서 getter나 마찬가지다.

    • 메소드가 캡슐화하는데 실패한 코드다

      image-20220712171349693

      • if로 물어보고 어떤 행동을 했기 때문이다.
      • 물어보고 check를 시켜서 스스로 thr로 죽여야한다.
  • my) 인자로 넘어온 특정 객체에 대한 상태검사내가 하지말고 시켜서 스스로 죽게 만들자

    • 자신에 대한 검증메소드가 추가될 때, 스스로 죽도록 하는 메소드로 만들 것

    image-20220712171739114 image-20220712171746455

    • 각 객체의 상태검증은 본인이 하도록 위임해서 스스로 죽게하고 나는 호출만 한다.
      • 객체도 구조가 바뀌면 다 바뀔텐데, 니가 알아서 검증하고 스스로 죽고 오세요.
if로무시with불린Flag보다 thr로 멈추게하면 좋은 점
  • 만약 if로무시했다면 불린flag를 받아서 내가 thr를 처리해줘얗나다.
  • thr로 처리했다면, 스스로 죽게 검증을 완벽하게 위임할 수 있다.

사후계약조건 책임할당

postcondition1) this가 필요없는 결과값 검증(this조차 받아가는 객체)은, 결과계산 위임객체에게 계산후 검증도 위임한다.

image-20220712172242654

  • 객체는 적합한 객체에게 책임을 위임하고 결과값을 받아오며, 그 결과값을 반환하기 전에 내가 직접 검증해야한다고 했다.

  • 하지만, 메소드 내부에서 결과값(result) 검증 로직을 보면 Plan 자신의 의존성(this)이 존재하질 않아, plan이 검사할 이유가 1개도 없다

    • this가 존재하는지 유무는 보라색 변수를 확인하면 되는데, 사용되는 보라색 변수는 calls하나가 있긴 하지만, 이미 계산메서드의 인자로 던져준 상태이므로, 계산로직 위임 객체calc가 이미 가진 의존성이다.
  • 빨간 박스안에는 this가 나올 요소가 하나도 없는 상태이다.

    • this가 안나오면 나의 메서드가 아니다라는 증거이다.
  • 즉, 쉽게말하면 result는 내가 가진 정보(this)로 와서 검증받을 대상이 아니라, result를 반환해주는 책임객체에서 검증을 끝내고 와야하는 대상이다.

    image-20220712173126719

    • 위임한 객체 calc에서 알아서 결과값을 검증하고, thr로 스스로 죽어라고 검증책임을 위임한다.

    image-20220712173223267

  • 계약조건의 준수는, 한쪽에 모는 것이 아니라 섬세하게 누구에게 책임이 있는지 판단해서, 그쪽에 위임해야한다.

사전/사후계약조건 검증 중에 내 context(this, 보라색변수)를 통한 검증이 아니면, 검증도 물어보지말고 스스로 죽도록 시킨다.(위임)
  1. 인자 calc의 상태검증을, isEmpty()로 물어보고 검증한다면? calc가 변할 때, 변화의 여파가 여기까지 온다.

    • 내 정보this가 필요한 것도 아닌데, 물어보고 행동하면, 변화의 여파가 온다.

    image-20220712174637225

  2. 결과값 result의 검증을, 0이냐/음수냐를 물어보고 행동한다면, 결과값계산로직이 바뀐다면 변화시 여파가 온다.

    • 내 정보(this)를 이미 결과계산로직에서 받아갔다면, 결과값 검증은 물어보지말고 결과값 계산하는 객체가 스스로 죽도록 시킨다.

    image-20220712172242654

pre/postcondition 검증책임은 대부분 위임받은 객체들이 가져가야하며, 인자/결과값로직의 검증 코드가 안보인다면, 이미 계약관계라고 판단한다.

image-20220712175645352

  • 상태값이나 결과값에 대한 검증을 물어보지말고, 위임된 객체에게 시킨다.
    • 인자 객체의 검증코드없이 나는 검증된 값임을 메소드로 증명하고있다면 이미 계약관계라고 판단한다
    • 결과값에 대한 검증로직이 안보인다면, 사후조건 검증을 가져간 상태로서, 이미 계약관계라고 판단한다
  • precondition은 인자에 대한 precondition조건을 명시적으로 호출하는 것이 필요하지만, postcondition은 위임한 메소드안에 지 스스로 보장해서 줘야한다.
precondition은 내가(받은쪽이) 발동할게, postcondition은 니(위임객체)가 처리해와
  • 처음에는 Plan에 precondition과 postcondition을 모두 두었다가, precondition도 옮기고, postcondition도 옮겼지만 둘의 옮긴다는 같은 의미가 아니다
    • preconidtion의 책임은 받은쪽이 호출해야하는 책임을 가지며
    • postcondition의 책임은 위임받아 주는쪽이 책임을 가져서 다 처리하고 준다.
  • **일반적으로 calc를 받아들인 쪽인 plan이 precondition을 명시적으로 호출한다. 그에 비해 postcondition은 위임받은 쪽이 검증까지 다 처리하고 주는 책임이 있다. **

    • precondition은 내가(받은쪽)이 처리할게, postcondition은 니(위임객체)가 처리해와
  • 회사에서 룰로 짜야한다.
    • postcondition은 return하는 쪽이 다 책임지고 줘야해
    • precondition은 잘못된 값줬다고 뭐라하지말고, 받은놈이 검증해
      • 이것을 안지키면, 제품이 어떤쪽에선 되고, 어떤쪽에선 안되고가 발생한다
      • 일관성있게 검사해야한다.
    • 70%가 validation코드가 되기 때문에, 스프링 등에서 애노테이션으로 제공해준다.

협력을 통한 책임분할

  • 지금까지는 precondition은 받은쪽이 발동, postcondition은 return주는 놈에게 정도로만 나눴지만, 더 세밀하게 나눠야한다.

postcondition검증인 결과값 검증에 외부인자 검증이 포함되어있는데, 검증 통과못하면 계산의 의미가 없는 경우, 애초에 오염되지 않은 인자만 넘어오는게 더 좋다.

image-20220712182851586

  • precondition으로서 내부에서 책임진다는원칙을 적용할 수도 있겠지만
  • calls는 외부에서 인자로 오는데, 왜 내부에서 검사하지?
    • 내 책임이 아니라서 외부에서 넘어올때부터 검증된 것만 넘어오게 한다.
  • 메세지에 white list만 흐르게 하면, 내부 검증 로직이 제거된다.

  • 외부 재료call이 1개이상일 때, 결과값이 0원이나 음수면 -> 결과값 반환없이 스스로 죽는다.

    • 외부 재료call이 0개일 땐? 0원인데, 그대로 두어도 0원으로 반환된다. 하지만, 1개이상이 아니라면, 계산의 의미가 없어진다.
  • 애초에 외부재료call(인자)이 1개 이상일때(특정 검증 통과해야)만 결과값계산이 의미를 가지는 경우라면, 외부에서 인자를 넘길 때, 미리 검증하고 넘기고, 아니라면 아예 안넘어오도록 삼항연산자를 활용해서 검증을 옮긴다

    image-20220712185231191

  • 계약관계에 있더라도, 위임된객체.메서드()의 시그니쳐가 보여주는 것은 아주 일부분이며, 코드로 계약관계를 명시한다.

    • 결과값 검증없이 책임객체의 결과값 반환하는 계야관계 -> 계약서 중 제목밖에 차지 안하는 부분이다.

    • 나머지 계약내용들은 위의 삼항연산자 코드처럼 계약내용이 명시되는 것이다.

      image-20220712185844782

      image-20220712190149813

  • 계약서의 내용으로 인해 계약의 의미가 달라졌다.
    • plan은 나의 calls을 보내는 것이 아니라 내것(this)와 다른 1건이상의 calls을 보낸다.
      • 더이상 plan의 calls이 아닌 calls.
      • 코드만 보고 계약내용을 모를 경우, OneMoreCalls라는 형을 만들어서 인자로 보내기도 한다.

런타임에 의한 계약조건

  • 방금 봤던, size가 0보다 더 큰 것을 검증하는 것도 섬세한 문제였지만, 런타임은 더 큰 문제

setter가 있어 생성시점에 불변을 보장못하는, 상태변화하는 객체 인자는, 인자의 상태검증 발동을 precondition에 위치시켜선 안되고, 실제 계산에 사용될 때마다 매번 스스로 검증하도록 메세지 처리전, invariant를 스스로 걸어야한다.

  • 즉, setter를 가진 객체는, pre나 post가 아니라 메세지처리전, 실시간 자신의 불변성 검증으로 돌려야한다.

    image-20220713002340242

Calculator, Plan

image-20220713000622034

  • 전략객체들을 실시간으로 받는 calculator는
    • Plan에 들어가는 runtime(시점)에는1개이상인지 상태검증을 제공하는데
      • check()메서드의 가정: 생성시점에 가지고 있는 전략객체 컬렉션이 확정되어있으며, 내가 plan에 들어갈 때, 내가 전략객체가 없으면 잘못된 놈이야.
    • **runtime상에서 상태변화를 언제든지 변화시키는 setter받기기능이 존재하고 있다. **
      • 전략객체를 추가하는 것은 생성시점이 아니라 runtime시 자유롭게 추가할 수 있다.
  • 상태가 시간(타이밍)마다 달라지는 것이 어렵다
    • Calculator 생성시점에는 아무것도 없다
    • Plan에 집어넣을 때도 아무것도 없다. 근데 검사는 여기서 이루어진다.
    • 근데, plan에 들어가서 검사 이후에 얼마든지 setter로 전략객체를 추가할 수 있다
    • 나중에 추가될 수 있으면, plan에 집어넣을 때 검사하면 안된다.
  • 생성시점에 확정되지 않는 애들(setter를 가진 필드)은 runtime에 의존성이 있고, 시차가 생기는데, 특정시점(plan에 들어가는 시점)에 검사하고 마치면 안된다.

image-20220713001324325

  • calc.check()는, 나중에 계산시 전략객체가 없으면 안되니까 미리 검사하는 것이었다.

    1. calc.check()는 precondition으로서 들어갈 때 인자의 상태검사를 할 것이 아니다 -> plan에서 삭제한다.

      image-20220713001953348

    2. setter를 가져 상태변화하는 calc인자상태 검사는, 자신이 사용될 때 실시간 스스로 검증해야한다.

      image-20220713002054160

  • 다시 보니, 내가 메세지를 처리하기 전에 내 상태를 검증하고 있는 invariant검사다.
    • 메세지 처리전에, 상태변화가 가능한 자신부터 thr로 불변성 검증하는 01 불변성 검증이다.
    • precondition으로 봤던 calc인자의 상태 검증은 알고 봤더니, 상태가 수시로 변하므로, 계산할때 상태 검증 -> 스스로 불변성 검증하는 invariant였다
  • 상태가 변하는 객체는, 실행될 때마다 자신의 상태를 검증해야한다.

  • 불변식이라는 것은, 1번만 검사하고 끝나는 immutable이 아니라, 특정 조건하에서만 메세지를 처리하는 것이 invariant이며, 변하지 않는 필수조건을 의미한다

    • 메세지 처리를 하기 전에, 확인해야하는 필수조건 = invariant
    • 메서드내에 인자나 지역변수와는 상관없이 먼저 깔려야한다

    image-20220713002819826

  • 메서드호출인 실행시점보다 앞 시점은 생성시점밖에 없다.
    • 생성시점에 1번만 보장하면 되는 immutable이 아니라 상태변화를 하는 상태값이라면
    • 실행시점마다 == 메서드호출시마다 보장해야하는 invariant
  • 생성 -> initialize -> calculate 순으로 메서드를 생성했다.
    • client에서는 initialize를 호출해준다는 보장이 없다.
      • 컴파일러는 메서드 순서를 빼먹은 것을 걸러주지 못한다.
    • 런타임에 와서야 뭔가 부족해서 에러가 난다.
      • 런타임으로 올라간다면, 단위테스트로 모든 경우를 테스트할 수 밖에 없어진다.
    • 컴파일타임에 확정지으려면, 생성자에서 확인할 수 밖에 없다
    • 메서드의 순서로서 런타임으로 미뤄진다면, 호출시마다 invariant를 확정하는 방법 밖에 안남았다.
      • 스프링은 라이프사이클(순서)로 객체를 만들던데?
      • 스프링은 자동으로 자신이 순서대로 객체를 다 만들어서 빼틀일 일이 없다 = 상태가 에러나는 쪽으로 변하지 않는다 = 런타임 에러가 안난다.
  • 불변상태로서 생성시점에 검사를 한다면, 컴파일러에 걸려서 에러를 미리 잡을 수 있으나, setter로 변하는 상태를 가졌다면 -> 생성시점에 검사는 의미가 없고, 호출시점마다 매번 invariant를 확정하고 메세지를 처리하는 수 밖에 없다

다시 precondition

Calculator

image-20220713004119098

  • setter도 인자의 null검사를 precondition으로서 검사해줘야한다.

계약의 전파

image-20220713004238485

  • 지금까지 이것을 배우기 위해 공부했던 것
  • 계약은 전파된다.
    • 한번 앞에서 검증했다면, 또 검증해야할까?
    • 독립성을 위해서, 확인하기 귀찮으니, 무조건 다시 검사할까?
    • 갑-을만 계약할까? 어디에서 계약이 이어질까?

package(가시성)으로 precondition검증을 생략하기

좋은 인자들이 검증없이 전파 유지되기 위해서는,,, 이미 앞에서 검증이 끝난 인자라면, 호출하는 주체가 계약 맺는 놈만… public이 아니어야..

PricePerTime

image-20220713010412965

  • 1분당 18원을 계산하기 위해서, 1분을 60초 Duration으로 받고, 분당가격을 Money로 받는다.

  • 문제점: invariant / pre / post 검증을 아무것도 안하고 있는 전략객체

    image-20220713010825966

    • 생성자에 만능키인 null이 들어올 수 있으므로 invariant 만족이 안되는 상태다

    image-20220713011018995

    • contract의 문제때문에, 인자2개를 아무것도 안하고 있다.
      • 인자null/인자상태검사인 precondition도 검증안했고
      • 반환해줄 결과값인 result도 검증안했다.
  • 이렇게 짜면, 제품세계에서 장난감이 되는 것이다. 확정 버그 클래스로 보면 된다. pre/post condition/invaraint가 안걸린 class는 테스트도 필요없는 버그클래스다.

    • 보자마자 병균. 3가지를 만족하지 않으면, 아무리 단위테스트를 많이 짜도 101번째에 버그가 발생한다.

precondition(메서드 인자)이 검증이 없어서 찾아보는 원천 인자

image-20220713101323490

  • 전략객체calc의 전략메서드 calc를 호출하는 주체는 전략객체를 소유하고 있는 Calculator다

    • PricePerTime에 precondition이 되는 인자를 준 놈은 Calculator이고

    • calc.calc()의 인자는 Calculator의 calcCallFee( , )의 파라미터에서 받아온다.

      image-20220713101534094

    • 따라서, 넘어가는 파라미터에 대한 책임은 Calculator를 소유한 Plan에서 넣어주는 인자들이다.

      image-20220713101625017

    • 원천 인자를 찾아보니, Plan은 1건이상의 calls + 누적결과값변수의 초기값이라는 최초 인자가 계약을 잘지킨 좋은 인자Plan과 Calculator사이에는 확신할 수 있다.

      • 이렇게 되면, Calculator로 넘어간 파라미터는 precondition계약을 잘 지킨 좋은 인자라고 확신할 수 있다.

        image-20220713102128596

좋은 인자로 전파가 시작됬다면, 문제는 package와 접근제한자에 있다

  • Calculator의 calcCallFee()는 좋은 인자를 받았다고 했지만, public 메서드는 계약 맺은 Plan이 아닌 놈도 호출할 수 있는 문제점이 있다
    • precondition을, 앞에서 호출하는 놈이 계약내용을 코드로 책임을 지어주었는데, public 메서드라 precondition의 계약내용을 책임을 안지어주는 놈이 나쁜 인자를 넣어서 호출할 수 있는 문제가 생길 수 있다.
  • 우리가 계약을 믿으려면, Calculator의 메서드는 Plan만 호출할 수 있어야하고,Calc의 메서드는 Calculator만 호출할 수 있어야한다.
    • 이것을 확정지어야 좋은 인자를 처음에 검사했다면, 더이상 precondition검사를 할 필요가 없게 된다.
  • java의 package는 계약관계때문에 쓰는 것이다.
    • java같은 패키지언어에서는, 가상화경로를 가지는 package를 통해서 물리적인 위치의 불편함을 해소했다.
      • c의 manifest의 물리적인 파일위치용 package가 아니라 , 물리적 위치가 달라도 같은 패키지로 링킹
    • 계약관계로 인해서 package를 만들어 어떤애가 호출하지 못하게 만들려고 한다.
  • plan은 calculator호출시 안전하다. 왜? plan에 계약서 내용이 명시되어 있어서
    • calculator가 calc를 호출하는 것도 안전하다. 왜? 이미 안전한 인자를 받아서 내부에서 호출하기 때문에
  • 계약의 체인을 확정하기 위해서는 package 구성를 통해 외부호출을 차단하고, 내부에서 좋은 인자로 체인이 가능하도록 해야한다

계약체인을 보장하는 방법은 package구성을 통한 가시성 밖에 없다.

image-20220713104558837

  • 외부에서 간섭할 수 없게하려면, 오직 Plan만이 인식할 수 있는 영역안에서만 Calculator를 호출할 수 있게 하고 싶다

    image-20220713104639985

  1. plan을 일단 package plan에 집어넣는다.

    image-20220713104730411

  2. 계산위임을 받아 내부에서 호출되며, 계약된 인자를 받는 합성객체 Calculator도 package plan안으로 집어넣는다.

    image-20220713104901590

    • Calculator자체는 client코드에서 new때려서 만들어야하기 때문에, public class여야한다.
    • 하지만 계약된 메소드calcCallFee 는 접근제한자 없는 internal로서 패키지 안에서만 호출된다.
      • 이것으로 외부에서 더이상 계약된 메소드를 호출할 수 없음을 보장할 수 있다.
      • package내부에서는 plan밖에 호출하지 않도록, 우리가 통제하고 있다.
    • internal메서드를 통해서, 계약을 체결하고 있는 객체만 호출되도록 계약서에 추가명시된 것과 마찬가지다.
      • internal가시성을 통해 calcCallFee()메소드는Plan에 독점공급된다는 계약내용이 추가되었다.
  3. 문제는 Calculator에 주입되는 전략객체들을, Calculator만 호출할 수 있게 만드는 것이다.

    image-20220713105833512

    • 일반 구상클래스들 합성에서는 같은 package에 넣고 호출되는 쪽 메서드를 internal로 만들면, 외부호출을 막을 수 있었음.
    • 여러개의 합성객체들 및 전략메서드(인터페이스 오퍼레이터)를 internal로 막을 수 있을까?

    • 일단 같은 pakcage안에 위치시킬 것인데, plan안에는 calc를 호출할 순 없다. 우리 통제권안에 있는 상태 = calculator만 호출할 수 있는 상태인데

      image-20220713110041775

    • java의 인터페이스는 가시성 누수 때문에 무조건 public으로 만들어야한다는 제한이 생긴다.

      • 그렇다면, public외적인 가시성을 가지려면 인터페이스를 포기해야한다.
      • 인터페이스를 포기해도 두려워할 필요없다. 우리는 좋은 부모를 만드는 방법을 알기 때문에

    image-20220713110231819

    • 가시성 확보를 위해, internal Calc 추상클래스와 internal calc추상메서드를 만들었다.
      • 뿐만 아니라, Calc는 internal class기 때문에, 외부에서 상속도 할 수 없다.
    • 전략객체들도, 훅메서드 구현구상체들도 변경해준다.

    image-20220713110751843

    • 훅메서드구현시 internal로 만들어, calculator만 Calc의 구상체들을 호출할 수 있도록 바뀌었다.
      • 이렇게 calculator나 calc에서 precondition을 걸 필요가 없게 된다.
      • 처음에 계약된, plan이 주는 깨끗한 인자를 보장할 수 있게 되었으므로
  • 무조건적으로 precondition이 없다고 나쁜코드가 아니다. 계약만 잘되어있다면, precondition은 보장되어서 걸 필요가 없게 된다.

    • 계약내용으로 코드만 있는게 아니라 가시성 조정도 계약의 일부였다.

    • 짬이 생기면, package구성만으로도 어떤 계약을 맺고 있는지 보이게 된다.
      • my) 어?? precondition이 없네? -> 같은 package내에서 계약관계로 보장되는가보네?
      • 얘네들이 어떤 관계로 연관관계 계약관계를 맺고 있는지 보이네? 한쪽에만 집어넣으면, 나머지는 다 안전할 것이네
    • 정상적으로 설계를 잘했다면, package별로 역할이 분리되어있고, package에 public으로 노출되는 메서드 인터페이스가 1개씩으로 구성되고, 안에서는 체인으로 막아줘야한다.

가시성을 위해 포기한 인터페이스로 인해 등장한 안좋은 부모

image-20220713110751843

  • 좋은 부모클래스는 abstract가 아니라 템플릿메소드 +protected abstract 만 있어야한다.
  • 좋은 상속구상체는 @Override가 부모의 자체메소드가 아니라 protected만 있어야한다.

문제점과 해결

image-20220713115518883

  • 부모가 internal의 abstract만 가지는게 아니라 public템플릿처럼 (internal)템플릿을 가져야한다.

  • 그리고 그 내부에, 자식들이 개별구현해야할 로직이 전체로직이라고 할지라도 protected abstract 훅메서드로 빼서 줘야한다.

    image-20220713115739279

  • 자식은 부모의 abstract메서드를 오버라이딩하는게 아니라 protected abstract 훅메서드만 오버라이딩 해야한다.

    image-20220713120111039

image-20220713120124043

계약관계라 할지라도, 좋은부모자식의 protected관계라면, 패키지를 이동시킬 수 있다.

  • 계약관계로 internal로만 호출되게 하기 위해 같은 package에 위치시켰었는데

    • Calculator와 calc는 이제 인터페이스<->전략객체의 관계가 아닌 부모<->자식관계이므로 package의 위치와 상관없이 protected범위인 자식이 어디서든 호출될 수 있다.

    image-20220713120325865

    • plan <-> calculator: 같은package의 internal관계
    • calculator <-> calc구상체들: protected관계로서 아무package나 위치해도 가능

    • 부모만 바깥의 상투클래스로 나가있고, 자식들은 내부의 다른 패키지에 위치시켜놓는다.
  • package구성은 카테고리만 할 것이 아니라 계약관계에 달려있다.

    • 전략인터페이스였던, 구상체들의 부모 Calc추상클래스만 Calculator와 계약하는 것이고 구상클래스들은 Calc와 proected로서 따로 계약하여 따로 위치하고 있다.

계약관계에 의한 package 최종 구성

  • package구성을 끝내면, calc안에는 구상체들만 구성되어있고 나머지 계약관계의 주인공들은 밖에서 같은pacakge내 internal로 통신하는 병행package를 구성되어있다

    image-20220713120730090

    image-20220713121307458 image-20220713121342208

    • 노란색들은 외부호출이 안되서 보호됨. -> 따라서 앞에 계약만 확실하다면, 아래부분은 검증하지 않아도 된다.
      • precondition은 plan에서만 처리되고 다른 곳에서는 안했다.

postcondition검사는?

  • 아래 파란색 인자는, precondition검사는 더 상위레이어에서 계약을 체결해서 생략되었다.
    • precondition 검사는 반드시 명시적인 validation코드가 있어야만 하는 것이 아니었다. 가시성으로 계약을 체결해서 해결된다. 특히 condition체인들이 해결된다.

image-20220713121538506

condition검사할 때, 메세지 처리전 상태(필드)들의 invariant뿐만 아니라, postcondition검사 이전에, 로직에 사용된 보라색 필드들에도 주목해야한다.

  • 결과값 검증하기 전에, 내 필드값들이 눈에 밟혀서 invariant검증이 안된 것이 눈에 밟혀야한다.

    image-20220713121910224

  • result를 검증하기 전에, 사용되는 필드들 invariant부터 안정화되어야한다.

image-20220713122017278

  • **해당 invariant들을 보니 생성자 type이다 -> 컴파일타임에 1번 null + 상태검사해서 invariant를 1번에 확정지을 수 있겠다. **

    • setter로 상태가 변해서 런타임검사시 호출될때마다 매번 검사안해도 되겠다.
  • null체크 뿐만 아니라, 하한을 가지는 객체는 하한선도 같이 섬세하게 검사하는 도메인검사도 같이 한다

    • **price의 상태검증물어보면 변화의 여파가 오니, 스스로 죽도록 시켜서 price.check()로 변경할 수 있다. **

      • 게다가, ||로 연결되어야하는 상황이니 불린flag를 반환해주도록 .isChecked()로 만들어줘야한다?
    • Money라는 형은 언제든지 바뀔 수 있는 객체라서, 충격을 감당하려면, 처음부터 OverZeroMoney라는 형을 새로 만들어서 받아도 된다.

      • 검증만큼 형을 만들어주면 다 해결 된다. 형을 안받은 만큼, 형 안받은 포괄쪽인 곳에 코드로 나오게 되고 하드코딩이 되고 변화가 생기면 하드코딩해야한다. 그러나 형을 만들면, 그 형만 바꾸면 된다.

      image-20220713123110846

    • 하지만, Duration 타입은, 외부객체라서 내가 시킬수 없다.

      • Duration을 상속받은 && isChecked()를 가진 새로운형을 이용해서 처리해도 되긴한다.
      • 새로만든 형만 오면 검증을 안해도 되는 로직으로 만들어도 된다.

      • 형으로 해결하는 것이 코드가 줄기 때문에 더 우아하다

다시 postcondition검사로

image-20220713125344236

  • calls는 1건이상으로 들어도록 해놨고
    • call마다 먼저 계산을 한다.
      • 최종반환 전에, for문의 개별 결과값도 만든 뒤 postcondition검사를 한다.
  • sum을 다 계산한 나만의 계산이 맞는지 result이전에 지역변수 sum을 만들어 계산한다.
    • result는 건너온 메세지로서 객체context로, postcondition검증이 다 끝난 후에야 업데이트해줘야한다.
    • 그러기 위해 초기값 ZERO result를 바로 사용하지 않고, 지역변수에 새로 할당해서 나만의 계산을 먼저 한다.
  • 기존에는 그냥 객체context(파라미터 result)에 바로 더했던 것에 비해서, 나만의 계산부터 먼저할 수 있도록 한다.
    • postcondition을 고려하지 않고, 바로 context에 때려박으면, 디버깅도 X 추적도 X
  • 반드시 끊을 수 있게 지역변수를 선언해서 짜야한다.
    • sum단계
    • r단계
    • sum다시 검사(여긴 없는데 sum이 0이상인지도 검사해야하는데, 암묵지로 생략해버린 것은 문제점이다.)
  • **단계별로 postcondition검사를 할 수 있는 로직으로 짜야한다. **
    • 그렇게 짜지 않았더라면, 테스트를 안하고 짠 것이다.
    • 지역변수로 받아서, 중간상태를 볼 수 있는 상태/검사가능한 상태로 바꿔줘야한다.
  • 코드로 계약을 짜야하고, 단계별로 검사할 수 있도록 짜야한다.

    • 계약내용(pre, post, invariant) vs 로직을 구분해서 확인가능하도록 짜여져야함

    • 코드의 70%는 계약서내용이고, 중간에 지역변수 쓴 것도 다 계약내용 때문이다. 알고리즘은 아주조금만 들어가있다.
    • 코드로 명시하는 것은, thr의 제어문으로 만들 수 도 있지만, 형을 만드는 것으로 바꿔서, 그 코드를 형으로 그대로 옮겨줘도 된다.