TDD 총정리2-로또 구현을 통한 랜덤로직 전략패턴
TDD학습 내용 압축 정리
📜 제목으로 보기
- 분기문 자체를 enum에 [찾을 때 지연실행될 로직]으로서 매핑할 수 있으며, 이미 시그니쳐가 정해진 함수형인터페이스를 이용해 돌면서(values().stream.filter내부) 지연 수행(.test)될 분기문을 [가상인자+람다식으로 실시간 외부 구현]으로 매핑해야한다
로또 구현
-
도메인 및 객체 산출이 어렵다면 controller부터 정제된 input을 받고, output도 받환해야하는 service부터 출발한다.
- 로또부터? 로또번호부터? 로또 게임부터? 로또 서비스부터?
- 정제된 입력을 받아 구현할 수 있다면 서비스부터
- 블랙잭 카드부터? 블랙잭 게임부터? 블랙잭 서비스부터?
- 정제된 입력은 이름 밖이고 카드를 사람당 랜덤 2장 뽑아야해서.. 그걸 지서
- 2장 받는 카드부터
- 정제된 입력은 이름 밖이고 카드를 사람당 랜덤 2장 뽑아야해서.. 그걸 지서
- 로또부터? 로또번호부터? 로또 게임부터? 로또 서비스부터?
-
서비스부터 짠다면, input -> output까지 메인 흐름을 생각
해야한다.- 입력1: 사용자의 로또번호 + 보너스번호 입력
- 입력2: 당첨번호 입력
- 출력: 당첨등수 응답
-
정제된 input -> string을
split등 전처리를 다 끝낸 원시형
+컬렉션(List.of() - 어차피 불변객체 넣어줄테니까, 조작불가능 얕은복사 사본)
을 사용해서 만들어준다.-
아직 도메인 객체 추출을 못했으니,
원시형 + 컬렉션으로 작성
한다
-
-
테스트할 메서드는
무조건 응답하도록 먼저 작성
하며 이 때,응답값은 [넣어준 인자에 대한 case값을 응답]
을 해줘야한다- 만약, 로또번호 vs 당첨번호 인자 입력을 1등 번호로 예시case로 넣어줬다면, 그 인자 case에 맞는 1등이 응답값으로 반환해야한다(아무거나 반환X)
- 인자가 3개이상이면, 엔터쳐서 줄바꿈해준다.
-
테스트 성공시 refactor -> 다음case (2번째부턴 일반화함)를 준비한다.
-
2번째 case를 넣기 전에,
1case 인자를 그대로 사용한 체 2메서드를 복붙 생성
하고 이 상태로 로직을 짜면서if등을 활용해 1case 통과
하도록 짜도 된다.1case통과 시
하면2case에 맞는 인자로 변경
하여서 본격적으로 짜도 된다. -
새로운 case(2번째라서 일반화)를 추가하되,
2번째 case추가
== only 1case -> 일반화로서일반화 로직 추가
->2메서드로 작성
한다- 서비스는 상태값이 없으므로 지역변수에 결과값이 나올 것인데,
1case가 통과하도록 if를 걸어주며 짠다
. - 컬렉션 vs 컬렉션 비교는 반복문 + 요소vs컬렉션 비교 + 업데이트 지역변수가 사용된다.
- 서비스는 상태값이 없으므로 지역변수에 결과값이 나올 것인데,
-
1case가 통과되도록 짠 뒤 -> 2case(일반화 케이스)로 짠다
- 이 때, ValueSource(고정 값 1개)나
- CSVsource(고정 값 2개이상)나
- MethodSource(객체 이상)을 활용해서 경계값 케이스 위주로 확인한다.
- 통과되면 2메서드로 기존테스트 돌린 뒤 대체
-
매칭5개가 2등이 아니라, 매칭 5개 + 보너스볼이 2등이고, 5개는 3등이다.
- 만약, 6-5-4 순으로 내려온다면 (7-매칭갯수)만 return하고 분기는 없어질텐데
- 예외적인 상황이 발생하므로 if분기를 가지고 가야한다.
- 중간에 예외를 처리하기 위해 if분기로 보너스볼 매칭까지 확인해야한다.
- 만약, 6-5-4 순으로 내려온다면 (7-매칭갯수)만 return하고 분기는 없어질텐데
-
service 메서드를 통해
정제된input
에 도메인 지식이 쌓였으면,SERVICE에서 input중 가장작은 단위의 input부터 예외발생해야하는 case
를 작성하고 ->도메인에서 검증로직
을 작성한다.이후 그다음 input단위인 컬렉션에서 예외발생 case
를 작성하고 ->도메인에서 검증로직
을 작성한다.-
service에서
가장작은단위 input에 대한 예외 case
를 작성한다. -
원시값에 대한 검증을 위해
검증이 필요한 원시값
을new 때려서 도메인객체로 포장
한다. -
현재 1개인자만 포장해줬기 때문에 컴파일에러가 뜬 상황임. 먼저 수정해준다.
-
-
인자를 1개만 포장했다면 ->
전체 인자 다 포장
한 뒤,메서드의 파라미터도 변경
해줘야한다.-
이 때 쓰는 것이 오버로딩
이다.- 메서드의 파라미터 타입이나 시그니쳐가 달라졌을 때
- 기본값을 입력해야할 때 ex> 꼬리재귀 재귀함수 최초호출 기본값 인자
-
기존의 원시값컬렉션인자 -> 객체 컬렉션인자로 가려면,
기존 메서드 내부에서 포장처리
를 한번 해줘야 한다.-
원시값컬렉션 파라미터 메서드 내부에서 -> 도메인 컬렉션으로 변경하여, 도메인컬렉션 파라미터 메서드를 호출하도록 한다.
-
원시값 로직은
아직 일급컬렉션을 도입안했다면, 로직을 그대로 도메인컬렉션 메서드 내부로 옮길 수 있다
-
다만, 다만
도메인 컬렉션(List)가 contains( 단일도메인 )처럼 원시값처럼 도메인을 사용하려면, eq/hC재정의
를 해줘야한다. -
다시 원시값으로 인자 입력이 가능
해진 상태이므로서비스 호출시 에러가 나도록, 원시값 인자 입력으로 바꿔준다
- 만약, 도메인 인자를 입력하면 service메서드 진입전 도메인 자체에서 에러가 난다.
-
테스트가 통과되었으면 리팩토링해준다.
-
테스트에 도메인인자가 모두 사라졌다면,
오버로딩으로 내부 호출되는 도메인 파라미터 메서드는 private
화 해준다.
-
-
서비스에서 원시값의 도메인 포장 로직이 반영되었다면, 도메인 자체의 검증Test도 시행해준다.
-
-
이후
도메인 컬렉션 인자 포장
시 [이미 원시값 인자 존재 + 1개의 오버로딩 존재 + 도메인컬렉션 파라미터는 필요없을 때]- public 원시값 컬렉션 파라미터 메서드가 있는 상태에서
- 원시값 -> 도메인변환 -> 오버로딩 private도메인컬렉션 파라미터 메서드
-
오버로딩 private도메인 파라미터메서드만
수정대상으로 삼아2메서드
를 만들고- 도메인 컬렉션 파라미터 -> 내부에서
일급컬렉션 변환
후 -
오버로딩 private일급컬렉션 파라미터 메서드
대신, 내부에서 만든p->변환메서드->p2(일급컬렉션)
자체를파라미터 추출
로 올려- 도메인컬렉션 -> 일급으로 변환을
오버로딩 내수용 메서드의 인자에서 호출
+내부는 변환로직의 return값인 p2(일급컬렉션)이 파라미터
가 되게 한다.
- 도메인컬렉션 -> 일급으로 변환을
- 도메인 컬렉션 파라미터 -> 내부에서
-
적용해보기
-
수정대상 메서드는
private내수용 오버로딩 [도메인 컬렉션 파라미터] 메서드
이다. -
수정 대상을 2메서드로 복사한 뒤, 내수용 오버로딩 메서드라서 여기서 테스트는 못한다.
public 메서드로 테스트할껏이므로 상위메서드는 2메서드를 사용하도록 일단 바꾼다.
-
2메서드의 파라미터를 변환하여
파라미터가 되길 원하는 값을 return
하는 변환메서드(or 생성자호출)를 작성한다.-
p1 -> 변환메서드(p1) 추출 -> 예비 p2 상태에서
-
변환메서드를 파라미터 추출하면
- 외부메서드호출부( 변환메서드(p1) ) -> 내부는 (p2)가 파라미터가 된다.
- 파라미터 변환 적용하기 작전이다.
-
변수명은 똑같이 추출해서, 잠시는 에러나더라도
같은이름으로 변경하여, 내부로직에서 변경사항 파악이 쉽게 빨간줄
들어오게 해놔야한다.
-
-
-
파라미터 변환(도메인 컬렉션 -> 일급컬렉션) 변경에 따라
컬렉션 에서 물어보던 것들을, 일급컬렉션 내부러 던져
야한다.- 일단은 빨간줄을 없애도록 작성한다.
- 파라미터 변경메서드로 기존테스트가 잘돌아가는지 확인한다.
-
일급컬렉션에 원하는 검증로직(중복검사 by distinct.count vs size, 갯수)를 확인한다. -> 예외발생 테스트 통과시 2메서드를 반영한다
-
이제, 도메인 객체(Lotto, 일급컬렉션)의 경계값 테스트도 작성해야햔다.
-
현재는 도메인 컬렉션 생성자만 있다.
원시값 컬렉션 파라미터 생성자
를 추가하고 싶다.파라미터 추가
는인자 그대로 작성후 -> 변환 -> 오버로딩
의 과정으로 추가한다고 했다.
- 만약, 원시값 배열로 입력하면 가변인자(배열)로 받는다.
-
-
서비스 start~end까지 로직이 짜여졌으면
해당 로직에 맞는 메서드명
으로 변경한다.- start() -> match()
-
서비스
내getter
가 보이면,도메인 내부 로직
으로서캡슐화로 감춰야하는 로직
임을 100% 생각한다.- 출력을 제외하고 getter는 없다고 보자.
- getter이후가 같은형의 비교면 ->
해당형으로 책임을 위임해 옮긴다
- getter이후가 다른형의 비교면 ->
제3형을 만들어 책임을 위임한다.
- 같은형의 비교시 -> 내부 메서드로 돌아갈 때,
하나는 other라는 파라미터
명으로 잡아서 처리해준다. - 일급컬렉션엔 일반 컬렉션이 못했던 책임위임을 할 수있다.
- 내부에서 알아서 하도록 / 출력할때 빼곤, 객체에 getter를 쓰지 않고 위임한다.
-
같은 형 2개의 비교로직 위임이면,
getter를 쓰는 하나만 타겟팅해서 위임받을 context
로 잡아야한다.-
위임받지 않는 녀석만 파라미터에 포함
되도록 하려면-
static에서 static으로추출된 상태로 옮기면 위임받는 객체가 this등으로 리팩토링 안되게 된다.
-
객체에 위임하는 로직은
메서드 추출후, static있다면 삭제
해야한다.static내부 nonstatic메서드로 빨간줄이 떠도 참아야함
- static은 공용, 상태없는 유틸메서드이므로.. 파라미터 input -> output형태라서 this등 리팩토링 안됨
-
같은형의 객체 2개가 파라미터
에 있으면,타겟팅할 변수를 선택
하라고 인텔리제이가 알려준다.- private변수를 객체에 위임할 땐,
Escalate -> Public
으로 바꿔서 이동시켜준다.
- private변수를 객체에 위임할 땐,
-
같은형의 비교에서
파라미터에 있는 같은형은 other로 네이밍
해주자.
-
-
메서드 추출로 위임되었으면
getter()호출부를 삭제
한다.
-
-
제한된 종류의 상수가 보이면
enum
값객체로 대신할 수있다.-
이 때, 네이밍은
의미_원래값
형태로 해주면 된다.- 1 -> RANK_1
- 0 -> RANK_NONE
-
제한된 종류의 상수를 작성한 뒤 원래값을 매핑해둔다.
-
응답값이 상수에서 -> 값객체enum으로 변경되었으니 테스트도 다 수정해준다.
-
이 때, 네이밍은
-
**enum은 **
- **
{}
:값객체가 외부에서 파라미터로 입력
되면추상클래스로서 추상메서드를 이용해 [가상인자+람다식]에의해 수행되도록 전략객체로서 행위를 구현
해놓을 수 있지만, ** ()
:분기별 값객체 반환
시분기문 자체를 값객체에 매핑
해놓고,values()를 통해 매핑된 정보를 바탕으로 해당 값객체를 반환
해주는정적팩토리메서드
가 될 수 있다.
- **
-
현재 분기별로 생성된다. ->
정적 팩토리 메서드로서 [분기를 함수형인터페이스로 매핑하여 돌면서 지연실행될] 생성메서드를 만들고 위임
해야한다.- 기존 : 분기별 생성을 정적팩토리메서드에 위임하기 전 확인사항이 있다.
-
전체 값객체(enum)생성 분기문을 위임
해야한다. 그전에 해야할 것이 있다.-
위임의 첫단추는
내부context로 사용하는 값은 위임객체에선 외부context
가 되도록위임객체context외에 모든 context값들을 변수로 만들어서 추출
해야한다. -
특히 파라미터가 제일 많은 부분을 확인해야하며,
조건식 내에서 메서드호출된 것도 값이다!!
-
boolean문안에서
여러객체를 이용한 메서드호출() -> 1개의 응답값
을 가지는값(파라미터) 1개
로서 ->1개의 외부context로 위임
될 수 있도록추출될 로직보다 더 위쪽에 미리 1개의 지역변수로 빼놔야한다.
-
badCase:
로직 위임전 [내부 여러객체.메서드호출()]부를 지역변수로 안빼놨을 때
- 여러객체를 사용한 메서드호출은 어차피 1개의 값으로 사용되는데, 연관된 객체가 모두 변수로 뽑힌다.
-
GoodCase:
로직 위임전 [내부 여러객체.메서드호출()]부를 위임로직 더 위쪽에 지역변수 1개로 응답값을 받았을 때
-
-
-
위임할 로직 전체보다 더 위쪽에서, 위임 로직 내부
객체.메서드호출()
부를 지역변수 1개로 빼놓고 메서드추출한다.-
정팩메 위임의 메서드명은
of
라고 지으면 된다.
-
-
위임할 정팩메of를 enum에 위임한다.
-
enum은
내부 분기문
들을인스턴스에 ()매핑후 돌면서 찾기로 제거
가 가능하다.-
각 분기문들을 values() -> filter에 걸릴 수 있게
각 인스턴스에 지연될 실행될 로직으로서 매핑
해야한다.- 지연실행될 로직은
함수형 인터페이스(or전략인페.전메())으로 지연호출부 정의 -> 가상인자 람다식에서 외부구현(or전략객체로 생성)
의 방법이 있다
- 지연실행될 로직은
-
각 분기문들을 values() -> filter에 걸릴 수 있게
-
지연실행될 로직은 전략패턴이 아니라면
미리 시그니쳐가 정의된 boolean을 반환하는 Predicate함수형인터페이스
를 사용하여 정의한다.-
매핑될 boolean반환형 함수형인터페이스의 변수명은 condition으로 편하게 지어주자.
-
enum에 매핑되어있는 condition을 찾아서 .test()로 지연실행할 것이다.
-
이 때,
람다식의 가상인자로 구성되는 분기식에 쓰이는 실제 인자들
을지연실행 메서드
에 넣어줘야한다.
-
-
매핑할 함수형인터페이스는, enum의 필드로 선언해줘야한다.
- 빨간줄 생성하면 filter속 .test()로 사용될것을 인식하여 BiPredicate변수로 만들어준다.
-
값매핑 필드 삭제,
함형 필드는 생성자에서 추가
한 뒤 각 분기문들을가상인자 람다식으로 구현
해야한다.- 이 때, 분기문이 없는 enum필드도 매핑되어야하므로 분기문을 만들어주고 매핑한다.
-
Bi로 정의했기 때문에, 1개의 파라미터만 쓰더라도, 파라미터가 많은 것을 따라야한다.
-
enum도 메서드 생성시 도메인 테스트를 해줘야한다.
-
정적팩토리메서드로 class를
객체찍는템플릿
에서능동적인 객체관리자
로 승격시킨다.- new 생성자 : 100% 객체를 생성해야함(캐싱못함)
- 정펙매
- 캐싱(재사용) 가능해짐.
-
인자의 갯수에 따라 .of or from으로 public static 메서드를 생성한다.
-
기존 생성자와 동일한 형으로 key / 재사용할 객체형을 value로 해서 HashMap을 만든다. / 캐싱할 인스턴스 객체수를 알고 있다면 capacity를 저적어준다.
- 변수는 static변수로서 상수의 map으로서 jvm돌때 미리 생성된다.
-
캐싱의 핵심은,
존재하면 map에서 get
으로 꺼내고,없으면 생성
이다- map의 초기화도 미리 이루어진다.
-
없으면 key-value 넣어주기, 있으면 꺼내기는
map.computeIfAbsent( key, 람다식으로 value생성식)
을 넣어주면 된다.- 람다식은 가상인자로 작성해야해서, key값을 그대로 넣어주면 에러남.
-
정적팩토리메서드가 완성되었으면,
기본생성자는 private으로 막아
두고각종 검증방법은 정팩메로 이동
시킨다. -
기존 new때려서 생성한 객체들을 of로 다 수정해준다.
-
캐싱의 테스트는 식별자 일치를 확인하는
.isSameAs
를 사용해서 테스트한다. -
여러 쓰레드에서 접근한다면,
미리 모두 static생성 + ConcurrentHashMap
으로 변경하고 getter만 하게 한다.static 컬렉션 변수
의 요소들은staitc블록으로 요소들 미리 생성
하게 할 수있다.- static블럭이 늘어나면, 해석하는데 힘이쓰여 좋지 않ㄴ다.
-
생성할 수 있는 파라미터를 추가해
생성자가 많아 견고한 클래스
를 만들 수 있다.- 일반 생성자든 정펙매(캐싱 등) 생성자든
정팩메or생성자에 또다른 파라미터를 추가
할 수 있다. - 상위 파라미터(더 원시) 추가는 변환 후 오버로딩을 하면 된다고 했다.
- 참고) 더 하위 파라미터를 추가해도 마찬가지지만,
더 하위 파라미터(기존 파라미터 변환해서 만들어지는)로 변환
은 변환후 -> 메서드추출 -> 파라미터로 추출로 바깥에서 일하게 만들어서 파라미터를 바꾼다고 했음.
- 참고) 더 하위 파라미터를 추가해도 마찬가지지만,
- 일반 생성자든 정펙매(캐싱 등) 생성자든
-
불변 일급컬렉션을 만드는 과정
- 멋도 모르고
컬렉션<단일객체>을 생성자에 통채로 집어넣어 만든 일급컬렉션은
사실상태 변화(setter/add 등) -> [변화된 상태의 새 객체]를 생성해서 반환
하기 위해 생겨난 생성자임을 생각해야한다.
-
단일객체에 대한 복수형으로
final 불변의 일급컬렉션 클래스
를 생성하고,Set으로 단일객체를 보유할 빈 컬렉션을 조합
한 필드를 가지고 있는다. -
필드에 대해서 setter(상태변화) / getter를 고민해야한다.
- 단일객체가
외부에서 변수로 받아져 조작되는 객체
라면, 객체 자체를 파라미터로 받는 setter로 만든다. 컬렉션에 대한 setter는 add/remve등
이다.add로 단일객체 파라미터
를 받되- 상태변화의 setter(add)는
상태변화된 새 객체를 반환
하여 불변성을 유지한다.
- 2번에서
상태변화된 컬렉션으로 새 객체 생성해서 반환
을 하기 위해컬렉션을 통째로 받아 생성하는 생성자
가 필요했음을 생각한다. - 즉, 필요에 의해 객체컬렉션을 받는 생성자가 생겼음을 인지한다.
- 단일객체가
-
컬렉션 필드의 상태변화는
기존 컬렉션필드가 변하지 않도록 사본으로 복사한 다음 상태변화
시킨 후새객체 생성
을 해줘야한다.-
컬렉션필드를 수정할 땐, 항상 사본복사후 연산
하자.- 생성자를 통한 복사는
객체요소는 같은 주소를 바라봐서 요소변화가 가능한
상태로 복사이다.
- 생성자를 통한 복사는
-
-
기본생성자외
컬렉션 필드의 생성자가 생성되는 순간, 빈컬렉션의 필드초기화
는 의미가 없어진다.최초의 시작은 빈컬렉션으로 될 수 있도록 부생성자로서 추가
해줘야한다. -
파라미터가 없는 생성자는
부생성자
이므로파라미터가 있는 주생성자를 this로 사용
해줘야한다. -
불변의 일급컬렉션에 요소를 add하는 과정은 매번 재할당해줘야한다.
-
불변 객체를 받는
변수
는 재할당이 운명이다. -
일급컬렉션은 getter().size()가 아니라
.size()
메서드를 정의해서 쓴다.
-
- 멋도 모르고