OBJECT 17 정책적용 객체들의 계약관계
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/
- 참고 유튜브 : 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가지
- 객체지향의 세계에서는 메세지를 통해 계약한다.
- 메세지를 보내는 sender
- 메세지를 받는 receiver사이에 더 상세한 내용이 계약으로 있다.
- 범위, 시간, 등 다 계약으로 정해놔야한다.
- 전달받은 메세지의 규격 = precondition을 지켜줘야한다.
- sender가 규격을 지켜서 보내진 않는다.
- precondition검증은 receiver가 한다.
- 우리는 receiver가 메세지를 전달받아 규격(precondition)을 검증하는 것을 기본정책으로 하고 있다. 안맞으면 안받는다.
- 전달할 메세지의 규격(postcondition)
- receiver가 받아 검증했지만, 자기가 보낼 것도 receiver 안에서 검증해야한다.
- receiver는 내 결과물을 상대방에 전달하기 전에 검증해야한다.
- 객체 자신의 규격(class invariant)
- 객체는 원래 상태를 가지는데, 메세지를 받았을 때, 특정 필드값을 만족할 때만 제대로 메세지가 처리된다.
-
메세지를 받기 이전에, 불변성invariant 검증이 되어있어야한다.
- 내 상태조건이 변하지 않도록 확정되어서 메세지 처리가 가능한지 검증
- 위임된 책임의 context
- 내가 하는 짓이 이 context가 맞는지
- 4가지 조건이 다 만족되어야 계약이 가능하다.
01 불변성(invariant) 검증
- 메세지를 주고 받기 전에, (setter등으로 상태가 변화한다면 thr로) 객체 자신은 불변성 검증을 통해 불변성을 확보해야 메세지 처리가 가능하다
-
객체 초기상태, 객체 메세지 수신 전 상태가 확립되어야한다.
-
메세지와 무관한 객체의 상태는 필드값이다.
- 메세지는 인자값(pre) or 리턴값(post) or 지역변수들(post)로 이루어진다.
- 필드값들을 제대로 갖고 있는지 상태 점검 해야한다
-
초기화 할당해서 확보 or DI로 위임해서 꽂아서 확보하던지 해야한다.
- 객체 초기화는 프로그래머 역량에 달려있어서 믿을 수 없다. solution, computer에게 DI로 맡기자
-
메세지와 무관한 객체의 상태는 필드값이다.
Plan
-
기존
- 하나의 요금제를 감당하고 있는 Plan은 set으로 calls통화기록를 받아서 가지고 있다.
- 이 calls를, 전략주입 구상클래스에게 넘겨줘서 계산책임을 위임하고, 결과를 받는다.
- private필드, final을 준수하고 있으니 확장가능(상속을 해줘도) 안전하다
문제점과 해결(객체 필드 상태null -> setter호출 이전에 빈값으로 초기화)
-
주입받을 calculator필드는 초기값이 null이다.
- setter를 호출하지 않았따면, calculateFee호출시 NP에러가 날 것이다.
- 그러나 대외적인 인터페이스(class signature)를 보면, thr가 안달려있어서, 예외가 발생할 것이라 예상도 할 수 없다.(보고서 정상작동 하지 않을 것이라는 생각을 할 수도 없다)
- Calculator 내부를 보면,
- 부분적인 계산체인을 위한 calcs라는 전략객체 컬렉션 필드를 소유하고 있고
- 위임받은 책임인 calcCallFee를 보면, calls는 한꺼번에 받고, 초기값을 받아서, 누적해서 결과물을 낸다.
- 즉, 자신이 계산하는 것이 아니라, 자신 소유 전략객체들을 계산합으로 결과물을 내어준다.
-
calculator는
생성
자에 받아야하는조건
이 없으므로null초기화로 인해 없는 상태를 방지
하기 위해서객체를 미리 넣어주는 방법으로 안정성을 확보
할 수 있다.- 또한, calculateFee()의 calls 역시 미리 초기화된 상태라, 에러날 일이 없다
-
필드에 대해 유효한 초기화 방법은
필드 초기화 or 생성자
로 할당하여NULL대신 NULL을 대신할 수 있는 empty객체를 할당
해주는 것이다.
02 사전조건(precondition) 검증 - 인자값 검증(전처리)
- 사전조건은 일반적으로 validation이라 부르며, 다른 언어들에서는 required라고도 부른다.
- 넘어가기전에 위에서 검증하고 가
- white list 이론: 38선을 경계로 위에서 방역/검증하고, 밑에서는 valdiation로직 없이 깨끗한 white list영역으로 취급한다.
- 모든 인자들이나 검증되지 않는 값들을 위에서 검증하여 white list를 만든 뒤, 로직을 전개한다.
- if로 return으로 짤라버리는 쉴드패턴
- 메세지로 받는 값을 스스로 검증한다.
- 내가 직접검증안하고, 형으로도 검증할 수 있다.
- VO Money가 대표적인 예. Money클래스를 브릿지로 써서, integer를 중간에 형을 더 만들어서 안정성/불변성을 확보하는 형태
- 검증때문에 한번더 감싸는 것
plan
문제점과 해결(인자null or 인자의 상태null -> if로무시With불린flag토하기with기존값사용가능확인 or if thrWith메세지로통보)
-
java는 모든 객체형에 대해 null이 가능하므로
NULL인자로 호출가능한 메서드
들의 인자로 넘어온 객체메세지를 null검사안하는 것이 맘에 안든다.- 제임스고슬링이 java에서 null을 만든 것을 실수라 한 것처럼, null은 모든 메서드의 시그니쳐를 무력화시킨다.
- null이 값이 없다는 것이 문제가 아니라, null이 만능키라서 문제
- 제임스고슬링이 java에서 null을 만든 것을 실수라 한 것처럼, null은 모든 메서드의 시그니쳐를 무력화시킨다.
-
첫번째 해결책:
언어나 컴파일러의 기능
을 이용하여인자에 @NonNull
같은 어노테이션을 쓰자- 컴파일시, 컴파일러가 모든 코드를 조사해서 null이 생기면 컴파일에러로 거른다.
- 컴파일 때가 아닌, 런타임상에서 null이 아닌 실제 인스턴스가 들어갈 수 있으며, 컴파일러는 런타임시점을 모르므로 제한이 있다.
- 다른 필드나 지역변수들은 null이 허용되기 때문에, @NonNull인자외에 다른변수들이 runtime시 null을 만들어서 인자에 null이 들어갈 수 있다.
- typescript, swift, kotlin 등과 같이 애초에 null이 올수 없는 변수를 쓰는 경우에는 해결이 되는 방법이다.
- 컴파일시, 컴파일러가 모든 코드를 조사해서 null이 생기면 컴파일에러로 거른다.
-
두번째 해결책:
null이면 if로 무시 with 외부에서 알수 있는 boolean flag + 무시하고 기존값 써도되는지 확인
- null이면 if로 무시하고 해당로직은 슬그머니 넘어가되 최소한 boolean값으로 결과를 응답해준다. 우리가 많이 하는 선택지
- 나빠보이지만, 자바컬렉션들은 add나 put시 if로 슬그머니 null을 넘어가며, 넘어간 여부를 boolean으로 대답을 해준다.
- 그렇다면,
void가 아니라 최소한 boolean형으로 return하도록 수정
해야만한다.외부에서 알수 있는 flag를 세워줘야한다.
-
또한,
if로 무시할 수 있는 상태 == 실패시 기본값(add시 빈컬렉션) 이미 존재하며 그대로 사용해도 되는 상태인지 확인
도 해야한다.- 실패해도 기본값이 이미 초기화되어있으며, 그 것을 사용해도 되는지
- 예를 들어, setCalculator에서 if로 무시해서, 기본calc를 사용하게 되는데, 요금이 250만원 넘게 나와버리면, 기본calc는 사용하면 안되는 상황이다. 이럴 경우, if로 무시하고 넘어가면 안된다.
- 근데, boolean으로 응답해줬다면, 밖에서 기존값 쓰면 안되어서 따로 처리하게 될 것이다.
- 그렇다면,
-
세번째해결책:
예외로 처리
-
if로무시하며 boolean flag하는 것보다 더 강력하게,
null이면 예외메세지로 통보
하고 있다.- 참고로
if로무시하며 불린flag를 바깥에 토하는 것
과if thr로 멈추는 것
둘다 runtime에서 조치할 수 있는 2가지 모든 것이다.
- 참고로
-
한번
null에 대한 정책
을 thr로 태웠으면, if무시with불린flag토하기는 더이상하지말고, 다른 로직도 모두 thr를 태우도록 통일해야한다.- 어떤 것은 불린flag토하고, 어떤 것은 thr토하면 괴롭다
- 정책을 세우면, 통일해서 일관성을 만들어야한다.
-
if로무시하며 boolean flag하는 것보다 더 강력하게,
사전조건 검증(null인자)에 대한 정책 3+1가지 정리
-
언어를 바꾸거나 애노테이션을 쓴다.
-
if를 통해 불린flag를 리턴하거나
-
명시적인 thr로 멈춘다.
-
우리는 3번을 중심으로 한다.
-
모든 인자를 가진 함수에 다 써야한다. 그래서 인자가 많은 함수가 귀찮다.
-
-
객체 메세지라면, 객체의 사용될 상태 검사(null/empty)
- Calculator는 전략객체들을 컬렉션필드에 담고 있으므로, 컬렉션 필드가 비었으면, null이나 마찬가지인 놈으로서 해당필드 empty검사를 해줘야한다.
-
객체는 자기자신에 대한 validation의 책임을 외부에 제공해야하므로
객체가 메세지로 사용된다면, 책임수행 중에 자기자신의 상태에 대한 validation책임도 늘어난다
- validation때문에 main/client에서 보이지 않는 메서드들이 늘어나는 경우로서,
메세지로 들어가는 타class의 메서드 내부의 인자 사전조건 검사에 쓰이는 자기 상태검증용 메소드
들이 늘어난다.
- validation때문에 main/client에서 보이지 않는 메서드들이 늘어나는 경우로서,
03 사후조건(postcondition) 검증 - (받아온) 결과값 검증(후처리)
- 일반적으로 결과값 검증이라고 한다.
- 재고처리 시스템 등은 사후조건 검증을 빡빡하게 찬다.
- 보내줄 값이 올바름을 검증한다.
- return하는 형에 대해, 그 형을 만드냐 못만드냐로 검증을 미룰 수도 있음.
plan
문제점과 해결(결과값 검증 -> 도메인의 영역이다.)
-
사후검증 대상은 calculateFee()이다.
- 일반적으로 객체는 자기의 일을 직접수행하지 않고,
메세지를 통해 책임수행이 적합한 객체에게 위임
해서 결과값을 받아온다.받아온 결과값은 내가 한 것이 아니므로 반드시 검증한 뒤 반환한다
- 계산책임을 위임받은 calculator가 결과를 반환해주는데, 받아서 다른 데 반환하기 전에 다시, 내가 검증을 해야하는데
사후조건 검증은 로직이 아니라 도메인의 영역
이다.- Money를 반환할 예정인데, 0원이나 음수를 보내줘도 되는지 확인 -> call이 있다면 0원이하일 수 없다.
-
위임해서 받아온 값은 반환하기 전에 내가 검증한다.
-
계산재료인 call이 없었으면 모르겠는데,
call이 있는데도 불구하고 && 돈이 0원이나 음수가 되면 thr
-
도메인지식을 if에 올려 죽여버린다
-
- 일반적으로 객체는 자기의 일을 직접수행하지 않고,
계약별 책임할당
- Plan이 현재 모든 계약조건을 다 검증하고 있다.
- 하지만 Plan은 Calculator에게 계산책임은 위임한 상태이므로 같이 일하고 있는데, 모든 계약조건을 Plan이 다 검증해야할까?
사전계약조건 책임할당
- 인자로 넘어왔을 때, 인자가 null인지 체크하는데, 인자의 상태체크는 물어보지말고 스스로 죽도록 위임할 수 있음.
precondition1) 자신의 상태null, 인자null체크는 상대방에게 위임할 수 없다.
-
참고로, 인텔리제이는 precondition으로 인자 null체크를 넣어둔 메서드에 대해, null을 넣으면, 코드힌트에서 알려준다.
precondition2) 인자의 상태null/emtpy체크는? 해당 객체인자가 자신상태validation메소드를 제공해주는데?
isEmpty()는 간접적으로 내장을 까는 메소드. <물어보고 행동하면, 변화(시)의 여파가 찾아온다. 검증도 스스로 죽어라고 시킨다(위임)>
-
간접적으로 내 안의 컬렉션필드의 size를 알려주는 코드라서 getter나 마찬가지다.
-
메소드가 캡슐화하는데 실패한 코드다
- if로 물어보고 어떤 행동을 했기 때문이다.
- 물어보고 check를 시켜서 스스로 thr로 죽여야한다.
-
-
my)
인자로 넘어온 특정 객체에 대한 상태검사
는내가 하지말고 시켜서 스스로 죽게 만들자
- 자신에 대한 검증메소드가 추가될 때, 스스로 죽도록 하는 메소드로 만들 것
-
각 객체의 상태검증은 본인이 하도록 위임해서 스스로 죽게하고 나는 호출만 한다.
- 객체도 구조가 바뀌면 다 바뀔텐데, 니가 알아서 검증하고 스스로 죽고 오세요.
if로무시with불린Flag보다 thr로 멈추게하면 좋은 점
- 만약 if로무시했다면 불린flag를 받아서 내가 thr를 처리해줘얗나다.
- thr로 처리했다면, 스스로 죽게 검증을 완벽하게 위임할 수 있다.
사후계약조건 책임할당
postcondition1) this가 필요없는 결과값 검증(this조차 받아가는 객체)은, 결과계산 위임객체에게 계산후 검증도 위임한다.
-
객체는 적합한 객체에게 책임을 위임하고 결과값을 받아오며, 그 결과값을 반환하기 전에 내가 직접 검증해야한다고 했다.
-
하지만,
메소드 내부에서 결과값(result) 검증 로직
을 보면Plan 자신의 의존성(this)
이 존재하질 않아, plan이 검사할 이유가 1개도 없다- this가 존재하는지 유무는 보라색 변수를 확인하면 되는데, 사용되는 보라색 변수는
calls
하나가 있긴 하지만,이미 계산메서드의 인자로 던져준 상태
이므로,계산로직 위임 객체calc가 이미 가진 의존성
이다.
- this가 존재하는지 유무는 보라색 변수를 확인하면 되는데, 사용되는 보라색 변수는
-
빨간 박스안에는
this가 나올 요소가 하나도 없는 상태
이다.this가 안나오면 나의 메서드가 아니다
라는 증거이다.
-
즉, 쉽게말하면 result는 내가 가진 정보(this)로 와서 검증받을 대상이 아니라, result를 반환해주는 책임객체에서 검증을 끝내고 와야하는 대상이다.
- 위임한 객체 calc에서 알아서 결과값을 검증하고, thr로 스스로 죽어라고 검증책임을 위임한다.
-
계약조건의 준수는, 한쪽에 모는 것이 아니라 섬세하게 누구에게 책임이 있는지 판단해서, 그쪽에 위임해야한다.
사전/사후계약조건 검증 중에 내 context(this, 보라색변수)를 통한 검증이 아니면, 검증도 물어보지말고 스스로 죽도록 시킨다.(위임)
-
인자 calc의 상태검증을, isEmpty()로 물어보고 검증한다면? calc가 변할 때, 변화의 여파가 여기까지 온다.
- 내 정보this가 필요한 것도 아닌데, 물어보고 행동하면, 변화의 여파가 온다.
-
결과값 result의 검증을, 0이냐/음수냐를 물어보고 행동한다면, 결과값계산로직이 바뀐다면 변화시 여파가 온다.
- 내 정보(this)를 이미 결과계산로직에서 받아갔다면, 결과값 검증은 물어보지말고 결과값 계산하는 객체가 스스로 죽도록 시킨다.
pre/postcondition 검증책임은 대부분 위임받은 객체들이 가져가야하며, 인자/결과값로직의 검증 코드가 안보인다면, 이미 계약관계라고 판단한다.
- 상태값이나 결과값에 대한 검증을 물어보지말고, 위임된 객체에게 시킨다.
- 인자 객체의 검증코드없이
나는 검증된 값임을 메소드로 증명
하고있다면이미 계약관계
라고 판단한다 결과값에 대한 검증로직이 안보인다
면, 사후조건 검증을 가져간 상태로서,이미 계약관계
라고 판단한다
- 인자 객체의 검증코드없이
- 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검증인 결과값 검증에 외부인자 검증이 포함되어있는데, 검증 통과못하면 계산의 의미가 없는 경우, 애초에 오염되지 않은 인자만 넘어오는게 더 좋다.
- precondition으로서 내부에서 책임진다는원칙을 적용할 수도 있겠지만
-
calls는 외부에서 인자로 오는데, 왜 내부에서 검사하지?
- 내 책임이 아니라서
외부에서 넘어올때부터 검증된 것만 넘어오게
한다.
- 내 책임이 아니라서
-
메세지에 white list만 흐르게 하면, 내부 검증 로직이 제거된다.
-
외부 재료call이 1개이상일 때, 결과값이 0원이나 음수면 -> 결과값 반환없이 스스로 죽는다.
- 외부 재료call이 0개일 땐? 0원인데, 그대로 두어도 0원으로 반환된다. 하지만, 1개이상이 아니라면, 계산의 의미가 없어진다.
-
애초에 외부재료call(
인자
)이 1개 이상일때(특정 검증 통과해야
)만결과값계산이 의미를 가지는 경우
라면,외부에서 인자를 넘길 때, 미리 검증하고 넘기고, 아니라면 아예 안넘어오도록 삼항연산자를 활용해서 검증을 옮긴다
-
계약관계에 있더라도, 위임된객체.메서드()의 시그니쳐가 보여주는 것은 아주 일부분이며, 코드로 계약관계를 명시한다.
-
결과값 검증없이 책임객체의 결과값 반환하는 계야관계 -> 계약서 중 제목밖에 차지 안하는 부분이다.
-
나머지 계약내용들은 위의 삼항연산자 코드처럼 계약내용이 명시되는 것이다.
-
-
계약서의 내용으로 인해
계약의 의미
가 달라졌다.- plan은 나의 calls을 보내는 것이 아니라
내것(this)와 다른 1건이상의 calls
을 보낸다.- 더이상 plan의 calls이 아닌 calls.
- 코드만 보고 계약내용을 모를 경우,
OneMoreCalls
라는 형을 만들어서 인자로 보내기도 한다.
- plan은 나의 calls을 보내는 것이 아니라
런타임에 의한 계약조건
- 방금 봤던, size가 0보다 더 큰 것을 검증하는 것도 섬세한 문제였지만, 런타임은 더 큰 문제다
setter가 있어 생성시점에 불변을 보장못하는, 상태변화하는 객체 인자는, 인자의 상태검증 발동을 precondition에 위치시켜선 안되고, 실제 계산에 사용될 때마다 매번 스스로 검증하도록 메세지 처리전, invariant를 스스로 걸어야한다.
-
즉, setter를 가진 객체는, pre나 post가 아니라 메세지처리전, 실시간 자신의 불변성 검증으로 돌려야한다.
Calculator, Plan
- 전략객체들을 실시간으로 받는 calculator는
-
Plan에 들어가는 runtime(시점)에는1개이상인지 상태검증을 제공하는데
- check()메서드의 가정: 생성시점에 가지고 있는 전략객체 컬렉션이 확정되어있으며, 내가 plan에 들어갈 때, 내가 전략객체가 없으면 잘못된 놈이야.
- **runtime상에서 상태변화를 언제든지 변화시키는 setter받기기능이 존재하고 있다. **
- 전략객체를 추가하는 것은 생성시점이 아니라 runtime시 자유롭게 추가할 수 있다.
-
Plan에 들어가는 runtime(시점)에는1개이상인지 상태검증을 제공하는데
- 상태가 시간(타이밍)마다 달라지는 것이 어렵다
- Calculator 생성시점에는 아무것도 없다
- Plan에 집어넣을 때도 아무것도 없다. 근데 검사는 여기서 이루어진다.
- 근데, plan에 들어가서 검사 이후에 얼마든지 setter로 전략객체를 추가할 수 있다
- 나중에 추가될 수 있으면, plan에 집어넣을 때 검사하면 안된다.
- 생성시점에 확정되지 않는 애들(setter를 가진 필드)은 runtime에 의존성이 있고, 시차가 생기는데, 특정시점(plan에 들어가는 시점)에 검사하고 마치면 안된다.
-
calc.check()는,
나중에 계산시
전략객체가 없으면 안되니까 미리 검사하는 것이었다.-
calc.check()는 precondition으로서 들어갈 때 인자의 상태검사를 할 것이 아니다 -> plan에서 삭제한다.
-
setter를 가져 상태변화하는 calc
인자
의상태 검사
는,자신이 사용될 때 실시간 스스로 검증
해야한다.
-
-
다시 보니, 내가
메세지를 처리하기 전에 내 상태를 검증하고 있는 invariant검사
다.-
메세지 처리전에,
상태변화가 가능한 자신부터 thr로 불변성 검증
하는 01 불변성 검증이다. - precondition으로 봤던 calc인자의 상태 검증은 알고 봤더니, 상태가 수시로 변하므로, 계산할때 상태 검증 -> 스스로 불변성 검증하는
invariant
였다
-
메세지 처리전에,
-
상태가 변하는 객체는, 실행될 때마다 자신의 상태를 검증해야한다.
-
불변식이라는 것은, 1번만 검사하고 끝나는 immutable이 아니라,
특정 조건하에서만 메세지를 처리하는 것이 invariant
이며,변하지 않는 필수조건
을 의미한다메세지 처리를 하기 전에, 확인해야하는 필수조건 = invariant
메서드내에 인자나 지역변수와는 상관없이 먼저 깔려야한다
-
메서드호출인
실행시점
보다 앞 시점은생성시점
밖에 없다.생성시점에 1번만 보장하면 되는
immutable이 아니라 상태변화를 하는 상태값이라면실행시점마다 == 메서드호출시마다 보장
해야하는invariant
다
- 생성 -> initialize -> calculate 순으로 메서드를 생성했다.
-
client에서는 initialize를 호출해준다는 보장이 없다.
- 컴파일러는 메서드 순서를 빼먹은 것을 걸러주지 못한다.
-
런타임에 와서야 뭔가 부족해서 에러가 난다.
- 런타임으로 올라간다면, 단위테스트로 모든 경우를 테스트할 수 밖에 없어진다.
컴파일타임에 확정
지으려면,생성자에서 확인
할 수 밖에 없다-
메서드의 순서로서
런타임으로 미뤄진다면
,호출시마다 invariant를 확정
하는 방법 밖에 안남았다.- 스프링은 라이프사이클(순서)로 객체를 만들던데?
- 스프링은 자동으로 자신이 순서대로 객체를 다 만들어서 빼틀일 일이 없다 = 상태가 에러나는 쪽으로 변하지 않는다 = 런타임 에러가 안난다.
-
client에서는 initialize를 호출해준다는 보장이 없다.
- 불변상태로서 생성시점에 검사를 한다면, 컴파일러에 걸려서 에러를 미리 잡을 수 있으나, setter로 변하는 상태를 가졌다면 -> 생성시점에 검사는 의미가 없고, 호출시점마다 매번 invariant를 확정하고 메세지를 처리하는 수 밖에 없다
다시 precondition
Calculator
- setter도 인자의 null검사를 precondition으로서 검사해줘야한다.
계약의 전파
- 지금까지 이것을 배우기 위해 공부했던 것
- 계약은 전파된다.
- 한번 앞에서 검증했다면, 또 검증해야할까?
- 독립성을 위해서, 확인하기 귀찮으니, 무조건 다시 검사할까?
- 갑-을만 계약할까? 어디에서 계약이 이어질까?
package(가시성)으로 precondition검증을 생략하기
좋은 인자들이 검증없이 전파 유지되기 위해서는,,, 이미 앞에서 검증이 끝난 인자라면, 호출하는 주체가 계약 맺는 놈만… public이 아니어야..
PricePerTime
-
1분당 18원을 계산하기 위해서, 1분을 60초 Duration으로 받고, 분당가격을 Money로 받는다.
-
문제점: invariant / pre / post 검증을 아무것도 안하고 있는 전략객체
- 생성자에 만능키인 null이 들어올 수 있으므로 invariant 만족이 안되는 상태다
- contract의 문제때문에, 인자2개를 아무것도 안하고 있다.
- 인자null/인자상태검사인 precondition도 검증안했고
- 반환해줄 결과값인 result도 검증안했다.
-
이렇게 짜면, 제품세계에서 장난감이 되는 것이다. 확정 버그 클래스로 보면 된다. pre/post condition/invaraint가 안걸린 class는 테스트도 필요없는 버그클래스다.
- 보자마자 병균. 3가지를 만족하지 않으면, 아무리 단위테스트를 많이 짜도 101번째에 버그가 발생한다.
precondition(메서드 인자)이 검증이 없어서 찾아보는 원천 인자
-
전략객체calc의 전략메서드 calc를 호출하는 주체는 전략객체를 소유하고 있는 Calculator다
-
PricePerTime에 precondition이 되는 인자를 준 놈은 Calculator이고
-
calc.calc()의 인자는 Calculator의 calcCallFee( , )의 파라미터에서 받아온다.
-
따라서, 넘어가는 파라미터에 대한 책임은 Calculator를 소유한 Plan에서 넣어주는 인자들이다.
-
원천 인자를 찾아보니, Plan은 1건이상의 calls + 누적결과값변수의 초기값이라는
최초 인자가 계약을 잘지킨 좋은 인자
를Plan과 Calculator사이에는 확신
할 수 있다.-
이렇게 되면, Calculator로 넘어간 파라미터는 precondition계약을 잘 지킨 좋은 인자라고 확신할 수 있다.
-
-
좋은 인자로 전파가 시작됬다면, 문제는 package와 접근제한자에 있다
-
Calculator의 calcCallFee()는 좋은 인자를 받았다고 했지만,
public 메서드는 계약 맺은 Plan이 아닌 놈도 호출
할 수 있는 문제점이 있다- precondition을, 앞에서 호출하는 놈이 계약내용을 코드로 책임을 지어주었는데, public 메서드라 precondition의 계약내용을 책임을 안지어주는 놈이 나쁜 인자를 넣어서 호출할 수 있는 문제가 생길 수 있다.
-
우리가 계약을 믿으려면, Calculator의 메서드는 Plan만 호출할 수 있어야하고,Calc의 메서드는 Calculator만 호출할 수 있어야한다.
- 이것을 확정지어야 좋은 인자를 처음에 검사했다면, 더이상 precondition검사를 할 필요가 없게 된다.
-
java의 package는 계약관계때문에 쓰는 것이다.
- java같은 패키지언어에서는, 가상화경로를 가지는 package를 통해서 물리적인 위치의 불편함을 해소했다.
- c의 manifest의 물리적인 파일위치용 package가 아니라 , 물리적 위치가 달라도 같은 패키지로 링킹
- 계약관계로 인해서 package를 만들어 어떤애가 호출하지 못하게 만들려고 한다.
- java같은 패키지언어에서는, 가상화경로를 가지는 package를 통해서 물리적인 위치의 불편함을 해소했다.
- plan은 calculator호출시 안전하다. 왜? plan에 계약서 내용이 명시되어 있어서
- calculator가 calc를 호출하는 것도 안전하다. 왜? 이미 안전한 인자를 받아서 내부에서 호출하기 때문에
- 계약의 체인을 확정하기 위해서는
package 구성
를 통해 외부호출을 차단하고, 내부에서 좋은 인자로 체인이 가능하도록 해야한다
계약체인을 보장하는 방법은 package구성을 통한 가시성 밖에 없다.
-
외부에서 간섭할 수 없게하려면, 오직 Plan만이 인식할 수 있는 영역안에서만 Calculator를 호출할 수 있게 하고 싶다
-
plan을 일단
package plan
에 집어넣는다. -
계산위임을 받아 내부에서 호출되며, 계약된 인자를 받는 합성객체 Calculator도 package plan안으로 집어넣는다.
- Calculator자체는 client코드에서 new때려서 만들어야하기 때문에, public class여야한다.
-
하지만 계약된 메소드calcCallFee 는
접근제한자 없는 internal
로서패키지 안에서만 호출
된다.- 이것으로 외부에서 더이상 계약된 메소드를 호출할 수 없음을 보장할 수 있다.
- package내부에서는 plan밖에 호출하지 않도록, 우리가 통제하고 있다.
-
internal메서드를 통해서, 계약을 체결하고 있는 객체만 호출되도록 계약서에 추가명시된 것과 마찬가지다.
internal
가시성을 통해 calcCallFee()메소드는Plan에 독점공급
된다는 계약내용이 추가되었다.
-
문제는 Calculator에 주입되는 전략객체들을, Calculator만 호출할 수 있게 만드는 것이다.
- 일반 구상클래스들 합성에서는 같은 package에 넣고 호출되는 쪽 메서드를 internal로 만들면, 외부호출을 막을 수 있었음.
-
여러개의 합성객체들 및 전략메서드(인터페이스 오퍼레이터)를 internal로 막을 수 있을까?
-
일단 같은 pakcage안에 위치시킬 것인데, plan안에는 calc를 호출할 순 없다. 우리 통제권안에 있는 상태 = calculator만 호출할 수 있는 상태인데
-
java의 인터페이스
는 가시성 누수 때문에무조건 public으로 만들어야한다는 제한
이 생긴다.- 그렇다면, public외적인 가시성을 가지려면 인터페이스를 포기해야한다.
- 인터페이스를 포기해도 두려워할 필요없다. 우리는
좋은 부모를 만드는 방법
을 알기 때문에
-
가시성 확보를 위해, internal Calc 추상클래스와 internal calc추상메서드를 만들었다.
- 뿐만 아니라, Calc는 internal class기 때문에, 외부에서 상속도 할 수 없다.
- 전략객체들도, 훅메서드 구현구상체들도 변경해준다.
-
훅메서드구현시 internal로 만들어, calculator만 Calc의 구상체들을 호출할 수 있도록 바뀌었다.
- 이렇게 calculator나 calc에서 precondition을 걸 필요가 없게 된다.
- 처음에 계약된, plan이 주는 깨끗한 인자를 보장할 수 있게 되었으므로
-
무조건적으로 precondition이 없다고 나쁜코드가 아니다. 계약만 잘되어있다면, precondition은 보장되어서 걸 필요가 없게 된다.
-
계약내용으로 코드만 있는게 아니라 가시성 조정도 계약의 일부였다.
-
짬이 생기면, package구성만으로도 어떤 계약을 맺고 있는지 보이게 된다.
- my) 어?? precondition이 없네? -> 같은 package내에서 계약관계로 보장되는가보네?
- 얘네들이 어떤 관계로 연관관계 계약관계를 맺고 있는지 보이네? 한쪽에만 집어넣으면, 나머지는 다 안전할 것이네
- 정상적으로 설계를 잘했다면, package별로 역할이 분리되어있고, package에 public으로 노출되는 메서드 인터페이스가 1개씩으로 구성되고, 안에서는 체인으로 막아줘야한다.
-
가시성을 위해 포기한 인터페이스로 인해 등장한 안좋은 부모
- 좋은 부모클래스는 abstract가 아니라
템플릿메소드
+protected abstract
만 있어야한다. - 좋은 상속구상체는 @Override가 부모의 자체메소드가 아니라
protected
만 있어야한다.
문제점과 해결
-
부모가 internal의 abstract만 가지는게 아니라 public템플릿처럼
(internal)템플릿
을 가져야한다. -
그리고 그 내부에, 자식들이 개별구현해야할 로직이
전체로직이라고 할지라도 protected abstract 훅메서드
로 빼서 줘야한다. -
자식은 부모의 abstract메서드를 오버라이딩하는게 아니라 protected abstract 훅메서드만 오버라이딩 해야한다.
계약관계라 할지라도, 좋은부모자식의 protected관계라면, 패키지를 이동시킬 수 있다.
-
계약관계로 internal로만 호출되게 하기 위해 같은 package에 위치시켰었는데
- Calculator와 calc는 이제 인터페이스<->전략객체의 관계가 아닌
부모<->자식
관계이므로package의 위치와 상관없이 protected범위인 자식이 어디서든 호출될 수 있다.
- plan <-> calculator: 같은package의 internal관계
-
calculator <-> calc구상체들: protected관계로서 아무package나 위치해도 가능
- 부모만 바깥의 상투클래스로 나가있고, 자식들은 내부의 다른 패키지에 위치시켜놓는다.
- Calculator와 calc는 이제 인터페이스<->전략객체의 관계가 아닌
-
package구성은 카테고리만 할 것이 아니라 계약관계에 달려있다.
- 전략인터페이스였던, 구상체들의
부모 Calc추상클래스만 Calculator와 계약
하는 것이고구상클래스들은 Calc와 proected로서 따로 계약하여 따로 위치
하고 있다.
- 전략인터페이스였던, 구상체들의
계약관계에 의한 package 최종 구성
-
package구성을 끝내면,
calc안에는 구상체들만 구성
되어있고나머지 계약관계의 주인공들은 밖에서 같은pacakge내 internal로 통신하는 병행package를 구성
되어있다
- 노란색들은 외부호출이 안되서 보호됨. -> 따라서 앞에 계약만 확실하다면, 아래부분은 검증하지 않아도 된다.
- precondition은 plan에서만 처리되고 다른 곳에서는 안했다.
- 노란색들은 외부호출이 안되서 보호됨. -> 따라서 앞에 계약만 확실하다면, 아래부분은 검증하지 않아도 된다.
postcondition검사는?
- 아래 파란색 인자는, precondition검사는
더 상위레이어에서 계약을 체결
해서 생략되었다.- precondition 검사는 반드시 명시적인 validation코드가 있어야만 하는 것이 아니었다. 가시성으로 계약을 체결해서 해결된다. 특히 condition체인들이 해결된다.
condition검사할 때, 메세지 처리전 상태(필드)들의 invariant뿐만 아니라, postcondition검사 이전에, 로직에 사용된 보라색 필드들에도 주목해야한다.
-
결과값 검증하기 전에,
내 필드값들이 눈에 밟혀서 invariant검증이 안된 것이 눈에 밟혀야한다.
-
result를 검증하기 전에, 사용되는 필드들 invariant부터 안정화되어야한다.
-
**
해당 invariant들을 보니 생성자 type
이다 ->컴파일타임에 1번 null + 상태검사해서 invariant를 1번에 확정
지을 수 있겠다. **setter로 상태가 변해서 런타임검사시 호출될때마다 매번 검사
안해도 되겠다.
-
null체크 뿐만 아니라,
하한을 가지는 객체는 하한선도 같이 섬세하게 검사하는 도메인검사
도 같이 한다-
**price의
상태검증
은물어보면 변화의 여파가 오니, 스스로 죽도록 시켜
서 price.check()로 변경할 수 있다. **- 게다가,
||로 연결되어야하는 상황이니 불린flag를 반환해주도록 .isChecked()로 만들어줘야한다?
- 게다가,
-
Money라는 형은 언제든지 바뀔 수 있는 객체라서, 충격을 감당하려면, 처음부터 OverZeroMoney라는 형을 새로 만들어서 받아도 된다.
- 검증만큼 형을 만들어주면 다 해결 된다. 형을 안받은 만큼, 형 안받은 포괄쪽인 곳에 코드로 나오게 되고 하드코딩이 되고 변화가 생기면 하드코딩해야한다. 그러나 형을 만들면, 그 형만 바꾸면 된다.
-
하지만, Duration 타입은, 외부객체라서 내가 시킬수 없다.
- Duration을 상속받은 && isChecked()를 가진 새로운형을 이용해서 처리해도 되긴한다.
-
새로만든 형만 오면 검증을 안해도 되는 로직으로 만들어도 된다.
- 형으로 해결하는 것이 코드가 줄기 때문에 더 우아하다
-
다시 postcondition검사로
- calls는 1건이상으로 들어도록 해놨고
- call마다 먼저 계산을 한다.
- 최종반환 전에, for문의 개별 결과값도 만든 뒤 postcondition검사를 한다.
- call마다 먼저 계산을 한다.
-
sum을 다 계산한
나만의 계산
이 맞는지 result이전에지역변수 sum을 만들어 계산
한다.- result는
건너온 메세지로서 객체context로, postcondition검증이 다 끝난 후에야 업데이트
해줘야한다. - 그러기 위해 초기값 ZERO result를 바로 사용하지 않고, 지역변수에 새로 할당해서 나만의 계산을 먼저 한다.
- result는
-
기존에는 그냥 객체context(파라미터 result)에 바로 더했던 것에 비해서, 나만의 계산부터 먼저할 수 있도록 한다.
- postcondition을 고려하지 않고, 바로 context에 때려박으면, 디버깅도 X 추적도 X
-
반드시 끊을 수 있게 지역변수를 선언해서 짜야
한다.- sum단계
- r단계
- sum다시 검사(여긴 없는데 sum이 0이상인지도 검사해야하는데, 암묵지로 생략해버린 것은 문제점이다.)
- **단계별로 postcondition검사를 할 수 있는 로직으로 짜야한다. **
- 그렇게 짜지 않았더라면, 테스트를 안하고 짠 것이다.
- 지역변수로 받아서, 중간상태를 볼 수 있는 상태/검사가능한 상태로 바꿔줘야한다.
-
코드로 계약을 짜야하고, 단계별로 검사할 수 있도록 짜야한다.
-
계약내용(pre, post, invariant) vs 로직을 구분해서 확인가능하도록 짜여져야함
- 코드의 70%는 계약서내용이고, 중간에 지역변수 쓴 것도 다 계약내용 때문이다. 알고리즘은 아주조금만 들어가있다.
- 코드로 명시하는 것은, thr의 제어문으로 만들 수 도 있지만, 형을 만드는 것으로 바꿔서, 그 코드를 형으로 그대로 옮겨줘도 된다.
-