강의) 네오 2단계 로또 피드백
2단계에서 메서드부터 TDD + 리팩토링 일부
📜 제목으로 보기
- 세팅
- Service method TDD
- run()테메에서 static한 Service.start()메서드로 메인흐름 시작하기
- 요구사항에 따른 [정제 rawInput]을 Controller내 service가 가정 인자로 사용하여 -> Service 흐름 시작하기
- 가정인자에 따른 1 case만 1응답하도록 짠 뒤 테스트
- [테메] 복붙후 2case에 맞게 가정인자+예상값 바꾼 테스트 메서드 만들기
- [메서드 복붙]후 2case 예상값을 응답하도록 메서드2를 수정
- [기존 테메]도 [메서드2]로 돌아가는지 확인한다.
- [메서드2]를 [메서드]로 완전히 대체한다.
- [2case이후 테메]는 복붙후 한번에 처리해보자.
- service 메서드 완성후 회고
- 구현된 Service method의 가정인자 리팩토링
- 메서드인자 리팩토링1) 최소단위 값 검증 때문에 포장하도록 Service.메서드() 수정
- Service.메서드() [최소단위 값] 1개만 new 생성자()로 싸서, Class와 ClassTest를 정의
- Service.method()의 [포장]된 [입력인자]버전의 메소드를 만들기 위해 메소드1복붙
- 메소드 파라미터 정의를 원시형 -> 포장형으로 바꿔주기
- 포장용 Service메소드에서 사용되는 값들 모두 포장해서 전달해주기
- Exception 나는 것을 의도하여 포장했다면 에러가 나야한다 assertThat -> assertThatThrownBy()의 테스트 코드 바꾸기
- 포장한 도메인 생성자에 예외처리 검증 넣어주기
- [Service.method()에서의 검증]이 끝났으면 [단일 도메인에서 본격 검증]하러 classTest로 직접가서 [경계값 테스트]하기
- 예외 발생을 위한 포장 회고
- [정제된 rawInput을 받던 테매들이 사용하던 service.method()] 내부 개조로 한번에 포장해서 포장인자 메서드 호출해주기
- 메서드 인자 리팩토링2) 최소단위 [묶음]의 예외 처리 -> 일급 포장하여 검증
- 포장(단일)에 포장(일급) 후 client가 너무 불편? -> Client를 배려해줘라.
- 일급 테스트는 Service.Method()에서 완성된 [가변인자생성자] 시리즈가 완성된 후에 해주자.
- 일급으로 바꿨더니, 서비스 메서드 내부에 도메인 로직이? -> 메서드 분리로 역할 분리
- 응답값도 포장할 수 있다. 만약, 제한된 종류 + if분기마다 달라지는 응답값라면 ENUM응답 포장후 응답에 필요한 input들로 정펙매 를 호출한 뒤 내부로 분기 옮겨 응답한다. 이후 if분기를 처리해줄 < input->필드들 - 응답값>을 매핑한다.
- 제한된 종류의 응답 값포장은 Enum을 활용한다. 메서드 응답을 빨간줄로 바꿔 생성하자.
- 응답에 대한 분기로직을 어떻게 응답class/응답Enum으로 넘길까?
- 정팩메 내부로 옮긴 분기에 대해, 응답[값]들을 [Enum객체]로 수정해주기 -> 없는 값(0, None)에 대해서도 OUT,NONE객체 만들어서 응답해주기
- 메서드 [포장된 응답] 테스트 작성하기
- if분기를 enum 매핑으로 제거하기
- Enum정팩메 내 [제한된종류의 모든 if분기]를 values().stream + filter with input vs 매핑field로 분기 없애기
- Bi(비.아이.) Predicate< , >로 input된 2개 값을 다 사용하는 1개의 람다식필드 && filter속 예비 조건식(.test(a,b)로 실행)을 만들어 -> [enum의 1 필드 매핑]에 사용되는 Type을 함수형인터페이스로 사용하여 -> 정펙매 내부 들어오는 2개값 모두 사용하는 enum 만들기
- 응답값의 포장(리팩토링)이 다 끝났으면, 자체 도메인Test(Enum은 정펙매 테스트)
- 리팩토링(가변변수 0에 조건만족시 갯수 누적)
세팅
패키지 생성 (각종 설정 후)
-
프로젝트 생성 했다면
-
main>java
와 -
test>java
밑에 패키지 생성(lotto
)부터 해주기
-
시작점 고르기 (game, service)
-
시작점 찾기
-
로또번호
부터 구현할 수 있느냐? -
로또
부터? -
정제된 rawInput을 받은
Game, Service
부터 구현해서객체/도메인
도출을 천천히- 처음부터 객체 도출은 어려울 수 있다.
-
Service method TDD
run()테메에서 static한 Service.start()메서드로 메인흐름 시작하기
-
Lotto
ServiceTest
를 만들고 -
테스트 메소드명도 편하게
run
() -
Service클래스의 static메소드명도
run()start
()로 만들면서 시작한다.
요구사항에 따른 [정제 rawInput]을 Controller내 service가 가정 인자로 사용하여 -> Service 흐름 시작하기
-
테스트해야하므로 메인흐름을 가진 메서드는 controller로
응답
까지 해줘야한다.- service는 상태를 안가지므로 객체 - 예상객체 비교X, 응답값 - 예상값으로 비교해야한다.
public class LottoServiceTest { @Test void run() { //사용자의 로또번호 //당첨번호 // 보너스번호 // -> 당첨 등수 응답 LottoService.start(); } }
- 로직에 필요한 인자(정제된 input)를 가상으로 넣어주기
LottoService.start( List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6), 7 );
- 로직결과로 응답까지 해줘야 테스트가 된다.
가정인자에 따른 1 case만 1응답하도록 짠 뒤 테스트
가정인자( 6개 일치) = 1case를 넣어줬으니 -> 1응답을 반환
하도록 일단 1 case만 짠다.
-
테스트할 메서드는
처음에는 무조건 응답
되도록 void를 수정한다. 어떤 형으로 응답할지를 생각해서정의시 기입
한다. -
3개이상의
긴 인자는 콤마 다음부터 내려
서 이쁘게 정리해주자.public class LottoService { public static int start(final List<Integer> integers, final List<Integer> integers1, final int i) { } }
public class LottoService { public static int start(final List<Integer> integers, final List<Integer> integers1, final int i) { } }
-
메서드
파라미터 이름을 바꿔
준다. -
아직 도메인을 몰라서 클래스로 분리 못하겠다면, 메인 로직을 rawInput 가지고 짠다.
public static int start(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { } }
-
1case에 대한 1응답을 해주는 로직을 완성한다.
- if가 포함된 1응답이라면,
return 0; 같은 default 응답
도 넣어준다.
public static int start(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { // 몇개 일치하는지 돌면서 누적시켜야한다 // - 돌아가야하는 놈을 우측 파라미터+for문에 // - 전체를 가지고 contains 확인해야하는 놈을 좌변에 int matchCount = 0; for (Integer lotto : userLotto) { if (winningLotto. contains(lotto)) { matchCount += 1; } } // 일단 가정 인자에 따른 1 case통과 코드만 짠다. // 나중에는 matchCount 갯수에 따라 싹다 분기해서 응답해야한다.(리팩 대상) if (matchCount == 6) { return 1; } return 0; // 1case 응답인데, if를 포함해서 응답했다. -> 1외 default return 0;도 해주자. }
- if가 포함된 1응답이라면,
-
1응답이 나오는지 test한다.
public class LottoServiceTest { @Test void run() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6), 7 ); assertThat(rank).isEqualTo(1); } }
[테메] 복붙후 2case에 맞게 가정인자+예상값 바꾼 테스트 메서드 만들기
복붙 전, 각 case로 테메명 바꿔주고 난 뒤 복붙하기
-
현재
run
()으로 되어있는 테스트메서드명을 case를 반영해서 바꿔주고 난 뒤 복붙- 복붙하는 이유: 기존 테스트가 계속 돌아가도록 하기 위함.
@Test //void run() { void rank1() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6), 7 ); assertThat(rank).isEqualTo(1); }
복붙후 가정인자+예상값을 2case에 맡게 수정
-
1등 반환case
1개 이후에는 -> 복붙후가정인자를 바꾼 case변경
을 하면서 새로 짜야한다.- 테스트메서드 명을 case에 맡게 복사해준다.
- 가정인자 및 예상값을 -> 2case에 맞게 수정해준다.
@Test void rank2() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7), // 2등에 맞게 가정인자 수정 6 // 2등에 맞게 가정인자 수정 ); assertThat(rank).isEqualTo(2); // 2등에 맞게 예상값 수정 }
[메서드 복붙]후 2case 예상값을 응답하도록 메서드2를 수정
-
메서드2로 복붙한다.
- 단축키
shift + alt + →
및↓
를 활용해서 복사하고 - 단축니
shift + alt + F
로 정렬해서
public static int start2(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) {
- 단축키
-
메서드2가 case2예상값을 만들도록 짠다.
public static int start2(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { int matchCount = 0; for (Integer lotto : userLotto) { if (winningLotto.contains(lotto)) { matchCount += 1; } } if (matchCount == 6) { return 1; } if (matchCount == 5 && userLotto.contains(bonusNumber)) { return 2; } return 0; }
-
테메2가
메서드2를 사용하도록 수정한 뒤 테스트
한다.@Test void rank2() { final int rank = LottoService.start2(
[기존 테메]도 [메서드2]로 돌아가는지 확인한다.
@Test
void rank1() {
final int rank = LottoService.start2(
[메서드2]를 [메서드]로 완전히 대체한다.
-
기존 메서드
회색 비활성화된 메서드를 안전삭제(alt+del
)한다. -
메서드2 -> 메서드
(F2 2번)로 이름을 바꿔서 완전히 대체한다.
[2case이후 테메]는 복붙후 한번에 처리해보자.
[다case]테메명 + @ParaemeterizedTest를 통해 가정인자+ 예상값들을 변경시키며, 그에 맞는 응답을 하도록 [메서드 원본]을 수정해서 통과되도록 만들자.
-
가정인자가 2개, 예상값 1개, 총 3개 인자가 case에 따라 바뀐다.
List.of()
-
bonusNumber
expected
@CsvSource 혹은 @MethodSource를 통해 여러 파라미터를 받아야한다.
@ParameterizedTest @MethodSource("provideWinningLottoAndExpected") void rank3_to_5(){
public static Stream<Arguments> provideWinningLottoAndExpected() { return Stream.of( Arguments.of(List.of(1, 2, 3, 4, 5, 7), 8, 3), // 3등 Arguments.of(List.of(1, 2, 3, 4, 7, 8), 9, 4) , // 4등 Arguments.of(List.of(1, 2, 3, 7, 8, 9), 10, 5) // 5등 ); }
@ParameterizedTest @MethodSource("provideWinningLottoAndExpected") void rank3_to_5(final List<Integer> winningLotto, final int bonusNumber, final int expected) { // 파라미터 추가 + 가정인자/예상값 다 변경 -> 테스트시 에러나야 정상 final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, 6), winningLotto, bonusNumber ); assertThat(rank).isEqualTo(expected); }
-
바뀐 가정인자 + 예상값에 대응하도록 원본 메서드를 1,2case에 비추어 수정
if (matchCount == 6) { return 1; } if (matchCount == 5 && userLotto.contains(bonusNumber)) { return 2; } if (matchCount == 5) { return 3; } if (matchCount == 4) { return 4; } if (matchCount == 3) { return 5; }
service 메서드 완성후 회고
- 여기까지 통과시키고 난 뒤, 가정인자로 들어오는 놈들에 대해 도메인 지식이 쌓였을 것이다.
- 가정인자로 들어오는 List에 대해
개별Integer
는 개별 객체LottoNumber
라는 객체로도메인 분리할 수 있겠다. -
LottoNumber
List 6개를 모아서Lotto
라는 객체로 도메인 분리모아볼 수 있겠다.
- 가정인자로 들어오는 List에 대해
구현된 Service method의 가정인자 리팩토링
-
도메인 지식을 높이기 위해서,
가정
하고 넣어주는인자
가rawInput인체
로 빨리 돌아가기만 하는 코드를 짠 상태다. -
유지보수, 계속 봐야하는 코드라면
리팩토링
해야한다.
입력인자 예외처리에 의한 도메인 추출 인지
- 테스트코드가
가정인자+예상값
의 변화 없이개별 도메인 생성자 호출자체에서 문제
이기 때문에, 개별도메인으로 가서 테스트 된다.
[입력인자] [가장 작은 단위]의 숫자(로또번호)가 범위를 벗어나는 경우
-
기존 rank1~5 테스트라도 복붙해서 메서드명을
over
로 바꿔고 범위를 벗어나는 경우에 대한가정인자 + 예상값
이 아니라 user입력인자
를 바꿔서 만들어주자.-
사용자 입력에 대한 예외에 대한 테스트의 경우, service메서드의 가정인자 변화가 문제가 아니라
입력인자
에실수
가 반영된 것이기 때문에객체로 도메인 분리해서 도메인 검증
해야한다.
@Test void over() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, 100), // 사용자 입력인자만 예외인자로 변화 -> 100 입력 List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X 6 ); assertThat(rank).isEqualTo(2); // 예상값 변화X }
-
사용자 입력에 대한 예외에 대한 테스트의 경우, service메서드의 가정인자 변화가 문제가 아니라
[입력인자] 가장 작은 단위가 상호작용한 [일급]에서 중복 문제
-
테메 복붙후 이름변경
duplicate
-
입력인자
에서 중복이 되도록 입력- 예외 테스트 는 가정인자 + 예상값의 변화가 없는 상태임.
@Test void duplicate() { final int rank = LottoService.start( List.of(1,1,1,1,1,1), // 사용자 입력인자만 예외인자로 변화 -> 1이 list내 중복 됨. List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X 6 ); assertThat(rank).isEqualTo(2); // 예상값 변화X }
Service.메서드() 사용시 발견된 값들의 예외처리 -> 개별 도메인에서 테스트하면서 처리되어야함.
-
도메인을 풍부하게 ->
도메인으로 추출 분리
후도메인 내에서 검증
-
서비스메서드 사용하면서, 그 service(메인로직) 흐름내에서 if로 하지말자. 자기 도메인내에서 if없이 개별로 처리되어야한다.
- 잘못된 예(service에서 인자로 넘어온 도메인들의 예외처리)
public static int start(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { if (userLotto.get() > 45) { throw new IllegalArgumentException("범위 초과"); }
메서드인자 리팩토링1) 최소단위 값 검증 때문에 포장하도록 Service.메서드() 수정
Service.메서드() [최소단위 값] 1개만 new 생성자()로 싸서, Class와 ClassTest를 정의
-
Service.메서드() 로직을 사용하던 곳에서 예외발생 상황을 가정했던 곳에서
new 생성자()
를 씌워서 Class -> ClassTest까지 만들어보자.@Test void over() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, new LottoNumber(100)), // 검증이 필요한 곳 -> class로 도메인분리 -> 생성자부터 씌워서 만들어나가기
-
값을 1개만 포장하는 경우,
변수명을 value
로 잡아서 getValue()등 이름을 어색하지 않게 하자.- 생성자로 클래스 생성후, 상태값 직접 지정해줘야한다.
package lotto; public class LottoNumber { public LottoNumber(final int value) { } }
public class LottoNumber { private final int value; public LottoNumber(final int value) { this.value = value; } }
-
classTest도 바로 만들기
class LottoNumberTest { }
Service.method()의 [포장]된 [입력인자]버전의 메소드를 만들기 위해 메소드1복붙
-
기존테스트를 통과하는
메소드2
의 개발이 아니라인자가 달라지는 동일명의 메소드
를 개발하는 것이므로 메소드명 동일하게 복붙한다.- 기존 포장안한 테스트가 그대로 통과한 상태를 유지할 수 있다.
- 포장이 실패할시 바로 돌아갈 수 있다.
- 메소드2로 지어도 상관없을 것 같긴하다.
메소드 파라미터 정의를 원시형 -> 포장형으로 바꿔주기
-
메소드의 파라미터 정의부
를 원시형을 모두포장 형
으로 바꿔준다.- int, Integer ->
LottoNumber
public static int start(final List<LottoNumber> userLotto, final List<LottoNumber> winningLotto, final LottoNumber bonusNumber) {
- int, Integer ->
-
내부도 다 타입을 바꿔준다.
- 에러가 안나는 듯 하지만, 포장class에서도 기본 제공되는
contains()
는 비교/포함여부 되려면 equals/hashCode가 오버라이딩 되어있어야 에러가 안난다. -
최소단위 포장은
equals/hashCode생각
public class LottoService { public static int start(final List<LottoNumber> userLotto, final List<LottoNumber> winningLotto, final LottoNumber bonusNumber) { int matchCount = 0; for (LottoNumber lotto : userLotto) { if (winningLotto.contains(lotto)) { matchCount += 1; } }
- 에러가 안나는 듯 하지만, 포장class에서도 기본 제공되는
포장용 Service메소드에서 사용되는 값들 모두 포장해서 전달해주기
-
현재 생성을 위해 1개만 포장된 상태다. 수동으로 다 포장해줘야한다.
@Test void over() { final int rank = LottoService.start( List.of(1, 2, 3, 4, 5, new LottoNumber(100)), // 사용자 입력인자만 예외인자로 변화 -> 100입니다. List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X 6 ); assertThat(rank).isEqualTo(2); // 예상값 변화X }
- 포장 후
@Test void over() { final int rank = LottoService.start( List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5),new LottoNumber(100)), List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5),new LottoNumber(7)), new LottoNumber(6) ); assertThat(rank).isEqualTo(2); // 예상값 변화X }
Exception 나는 것을 의도하여 포장했다면 에러가 나야한다 assertThat -> assertThatThrownBy()의 테스트 코드 바꾸기
-
응답 vs 예상값
비교 구조에서() -> exception발생
구조로 테스트메서드를 바꿔주자.@Test void over() { assertThatThrownBy(() -> LottoService.start( List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(100)), List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(7)), new LottoNumber(6) )).isInstanceOf(IllegalArgumentException.class); }
포장한 도메인 생성자에 예외처리 검증 넣어주기
-
예외 발생 안했다 -> 예외발생시키는 검증을 도메인에 넣어준다.
public LottoNumber(final int value) { validate(value); this.value = value; } private void validate(final int value) { if (value < 1 || value > 45) { throw new IllegalArgumentException("로또 번호의 범위가 아닙니다."); } }
[Service.method()에서의 검증]이 끝났으면 [단일 도메인에서 본격 검증]하러 classTest로 직접가서 [경계값 테스트]하기
- 시간 관계상 Service.method()에서 통채로 테스트하지만, 포장ClassTest에서 직접 경계값 주위로 테스트해야한다.
class LottoNumberTest {
@ParameterizedTest
@ValueSource(ints = {0, 46, -1, 100})
void range(final int value) {
assertThatThrownBy(() -> new LottoNumber(value))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("범위");
}
}
예외 발생을 위한 포장 회고
- Service.메서드()에서 테스트했지만, LottoNumber로는 더이상 over되는 번호는 오지않겠구나.
- 심리적 안정감을 얻었다.
포장 입력인자 메소드 사용시 애로사항
-
Service.메서드() 사용시 모든 원시값을
new 포장( )
으로 일일히 감싸줘야한다.LottoService.start( List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(100)), List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(7)), new LottoNumber(6) ));
-
포장된 입력인자가 아니라 -원시값 입력인자를 그대로 사용하되,
메소드 내부에서 한꺼번에 포장객체로 변환
시키자
[정제된 rawInput을 받던 테매들이 사용하던 service.method()] 내부 개조로 한번에 포장해서 포장인자 메서드 호출해주기
기존에 작성된[ 정제된 rawInput을 받던 메소드]의 내용물은 삭제
-
기존 테스트를 위해
정제된 원시값을 받던 메소드
public static int start(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { int matchCount = 0; for (Integer lotto : userLotto) { if (winningLotto.contains(lotto)) { matchCount += 1; } }
원시값 인자 네이밍 -> 내부stream으로 한번에 포장 -> return 포장 인자 메서드() 호출
-
**기존 내용물은 삭제(포장된 것으로 처리되도록 통일해야됨) **
public static int start(final List<Integer> userLotto, final List<Integer> winningLotto, final int bonusNumber) { }
-
**원시값 인자 +
raw
네이밍 붙이기 **- 각 원시인자 기존 네이밍 -> 포장인자로 갈 때
기존 네이밍
으로 받아줘야 -> ``포장메서드 호출시 기존 네이밍으로 가기 때문에 **원시 인자 네이밍 앞에
raw`를 붙혀준다**
public static int start(final List<Integer> rawUserLotto, final List<Integer> rawWinningLotto, final int rawBonusNumber) { }
-
return
포장된 인자를 받는 새 메소드()
호출- 응답은 그대로 나가야하므로 return 유지
public static int start(final List<Integer> rawUserLotto, final List<Integer> rawWinningLotto, final int rawBonusNumber) { final List<LottoNumber> userLotto = rawUserLotto.stream() .map(LottoNumber::new) .collect(Collectors.toList()); final List<LottoNumber> winningLotto = rawWinningLotto.stream() .map(LottoNumber::new) .collect(Collectors.toList()); return start(userLotto, winningLotto, new LottoNumber(rawBonusNumber)); }
- 각 원시인자 기존 네이밍 -> 포장인자로 갈 때
의의: Service를 사용하는 client를 편안하게 해줘야한다.(내부로 가기전에 포장해서 호출해주는 놈이 있다면) = client는 원시값도 입력받아 도메인은 모르게 해주는게 낫다
-
client쪽, view쪽, 중요하지 않은쪽, 잘변하는 쪽
은 도메인, 포장값 사용없이 원시값으로만 service.메서드()를 호출하고, 내부에서 포장 좋다.- 포장을 서비스내에서 한번 거쳐서 하는 방법을 여기선 안내해줬다.
- 원래 중요하지 않은쪽이 -> 중요한 것을 생성/사용/리턴하는 방향은 허용된다.
- 생각이 다를 수 있다.
- 외부에서 무조건 감싸서 로직에 보내줘야한다라고 생각할 수도 있다.
- but 사용하는 client입장에서는.. 내부포장으로 편안한게 좋다
- my) controller에서 도메인 사용해도 되지만
- 내부 포장만 해주는 로직이 따로 존재한다면, 원시값을 넘겨도 될 듯?
메서드 인자 리팩토링2) 최소단위 [묶음]의 예외 처리 -> 일급 포장하여 검증
초과된 [포장 최소단위의 갯수 문제] -> [포장인자 메소드]를 가진 [테매]를 복붙하여 Exception 발생시키기
-
현상황에서 포장인자를 받던 메소드를 복붙하여
포장객체의 갯수 문제를 발생
시켜보자.- 아무래도.. 최소단위객체의 범위문제에서 작성된 메소드를 복붙해서 고쳐야함…
@Test void lotto7() { assertThatThrownBy(() -> LottoService.start( List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(6), new LottoNumber(7)), // 갯수 초과 List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(6)), new LottoNumber(7) )).isInstanceOf(IllegalArgumentException.class); }
-
갯수를 초과했음에도 불구하고 아직 묶음에 대한 검증이 안되기 때문에 예외발생이 안된다.
-
앞에서도 그렇고, 예외처리를
Service.method() 내부에서 if로 편하게 해주고 싶은데, 여기서 하면 모든 인자들에 대한 예외처리가 if로 한곳에 쌓일 것이다.
- **각 검증은
도메인 추출된 class를 만들어서 그곳에서 한다.
**
public static int start(final List<LottoNumber> userLotto, final List<LottoNumber> winningLotto, final LottoNumber bonusNumber) { // 여기서 갯수처리를 해주고 싶으나.. if (userLotto.size() != 6) { throw new 예외 } // 2번째 인자도 예외처리를 로직실행 메서드 내부에서 인자를 받아서 처리하고 싶으나.. if (winningLotto.size() != 6) { throw new 예외 } // 인자들마다 모든 검증들이 if로 여기(사용메서드)에 쌓여갈 것이다. 따른 검증 역시 여기서 해야한다.
- **각 검증은
List<도메인>
을 포장하는 일급포장의 시작
[객체의 갯수or중복 등 복수 검증]을 위한 복수검증 문제가 발생하는 곳에가서 new 일급()부터 시작
-
Service.method()
에서 원시값 List ->단일객체 List
가 인자로 들어가는 곳에서 **new 일급( ) ** -> Class -> ClassTest까지 생성해준다.@Test void lotto7() { assertThatThrownBy(() -> LottoService.start( new Lotto(List.of(new LottoNumber(1), new LottoNumber(2),
-
생성자로 들어오는 이름도
value
로 네이밍하여 생성자에서 초기화-
ClassName.
가 PREFIX인 것 및.getValue()
생각하기
public class Lotto { public Lotto(final List<LottoNumber> lottoNumbers) { } }
public class Lotto { private final List<LottoNumber> value; public Lotto(final List<LottoNumber> value) { this.value = value; } }
-
-
나머지도 List -> 일급으로 들어가도록 service.method수정단일>
List<단일>
) 메서드 복붙 백업후 -> 일급
인자로 받도록 method 파라미터 정의부 개조
들어오는 포장인자(-
기존 List<단일>으로 들어오던 메서드
전체를 복붙한다.- 기존 테스트 유지?를 위해, 일급으로 바꾸기전 List도 들어올 수 있도록 백업해둔다. ![image-20220305003021893](https://raw.githubusercontent.com/is3js/screenshots/main/image-20220305003021893.png)단일>
-
기존
List<단일>
으로 들어오던 메서드의파라미터 정의부
만정의해준 일급
들어오도록 수정한다.public class LottoService { public static int start(final List<LottoNumber> userLotto, final List<LottoNumber> winningLotto, final LottoNumber bonusNumber) {
- 수정후
public static int start(final Lotto userLotto, final Lotto winningLotto, final LottoNumber bonusNumber) {
-
내부 빨간줄 수정 ( 단일List -> 일급으로 인한 -> 기본적으로 앞에
.getValue()
의 getter가 요구됨.)-
일단 getter로 다 처리해놓자. 리팩토링 요구됨. 객체 메세지 던져서 통신.
-
상태값을
value
로 네이밍해줬어도,.getValue()
의 빨간줄을리팩토링으로 생성 후 return this.value;까지 직접 입력해줘야
된다. -
앞에 getter를 붙이면 빨간줄이 사라진다.
-
- 일급용 메서드가 완성되었으면
도메인
내부 생성자
에 단일List간 복수검증]을 넣어준다.
[일급 인자를 받는 Service.method()]가 완성되었으면, [일급이라는 -
일급 도메인인
Lotto의 생성자
에서 상태값 초기화 전에검증
을 넣어준다.public class Lotto { private final List<LottoNumber> value; public Lotto(final List<LottoNumber> value) { this.value = value; }
- 도메인 검증 추가
public class Lotto { private final List<LottoNumber> value; public Lotto(final List<LottoNumber> value) { if (value.stream().distinct().count() != 6) { throw new IllegalArgumentException("로또 번호는 6개입니다."); } this.value = value; }
-
도메인 생성자에 넣어준 검증(중복) 을, Service.method()의 인자검증으로 통채로
@Test void duplicate() { assertThatThrownBy(() -> LottoService.start( new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))), new Lotto(List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(6))), new LottoNumber(7) )).isInstanceOf(IllegalArgumentException.class) }
포장(단일)에 포장(일급) 후 client가 너무 불편? -> Client를 배려해줘라.
List<단일>
)의 인자를 -> 단일... 가변인자
로 받고 내부에서 List.Of대신 모아주는 포장용 생성자 추가
일급 생성자가 받는 new일급(-
현재 일급까지 포장해야하는 Service메서드 테매
@Test void duplicate() { assertThatThrownBy(() -> LottoService.start( new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))), new Lotto(List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(6))), new LottoNumber(7) )).isInstanceOf(IllegalArgumentException.class) }
- 인자 1개만 떼서 보기
new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))),
[Client] 일급 생성자에 들어가는 List.of() 혹은 Set.of() 제거해주는 -> [메서드내부] 가변인자(알아서 배열) -> this + Arrays.stream().collect(Collectors.toList or toSet ())) )
-
client에서 List.of(단일,객체)나 Set.of(단일, 객체)대신
단일객체를 콤마로 나열
만 해서 넘겨주면,알아서 List or Set으로 내부에서 모아주는
-
내부에서 포장해준 뒤,
포장 인자용 메서드를 다시 한번 호출
해주는메서드 파라미터 -> 인자만 복사해서 껍데기 메서드 생성 -> 내부에서 포장후 호출 다시 포장인자메서드를 재호출
일급 생성자 -> List.Of, Set.of를 제거하여 생성자를 추가 생성 -> 가변인자를 받도록 정의 -> 배열을 stream collect로 List나 set으로 포장만해서 this( , )로 미리 정의해둔 && 뒤에 존재하는 포장인자용 생성자 호출
public class Lotto { private final List<LottoNumber> value; public Lotto(final LottoNumber... value) { this(Arrays.stream(value).collect(Collectors.toList())); } public Lotto(final List<LottoNumber> value) { if (value.stream().distinct().count() != 6) { throw new IllegalArgumentException("로또 번호는 6개입니다."); } this.value = value; }
- 이제
new Lotto() 일급의 생성자에서
2가지 다 받을 수 있게 된다. List.of( , ,, ) 대신 ->그냥 , , ,의 가변인자를 넣어줘도 알아서 List로 포장하게 된다.
@Test void duplicate() { assertThatThrownBy(() -> LottoService.start( new Lotto(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1), new LottoNumber(1)), new Lotto(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(6)), new LottoNumber(7) )).isInstanceOf(IllegalArgumentException.class); }
-
내부에서 포장해준 뒤,
일급 내부에 2번의 포장 생성자
도 추가해서 client를 편하게 해주자.
가변인자(List.of제거) + 단일객체생성자 제거의 -
new 일급( ) 생성자에 client에서 List.Of() 제거하고 가변인자만 받도록 내부에서 포장해주는 생성자를 추가해줬었다.
//1. new Lotto( 단일객체, 가변으로) -> List.Of제거 public Lotto(final LottoNumber... value) { this(Arrays.stream(value).collect(Collectors.toList())); }
-
이번에는 client 단일객체 생성자 제거하고
단일객체 생성에 필요한 raw원시값을 가변인자
로만 받도록 수정해보자.- stream에서
mapToObj
가 뜨는 이유는 integer가 아닌int -> 객체
로 가기 때문 -> 애초에 파라미터로 들어오는 원시값들을 int… 대신Integer...
로 box된 타입으로 받을 수 있다.
public class Lotto { private final List<LottoNumber> value; //2. new Lotto( 원시값, 가변으로) -> List.Of() + new단일()생성자 제거 public Lotto(final Integer... value) { this(Arrays.stream(value).map(LottoNumber::new).collect(Collectors.toList())); } //1. new Lotto( 단일객체, 가변으로) -> List.Of제거 public Lotto(final LottoNumber... value) { this(Arrays.stream(value).collect(Collectors.toList())); } public Lotto(final List<LottoNumber> value) { if (value.stream().distinct().count() != 6) { throw new IllegalArgumentException("로또 번호는 6개입니다."); } this.value = value; }
- stream에서
-
이제 단계별로 생성자를 만들어서, 기존 테스트의 인자들이 모두 돌아가게 만들었다.
- 이제 원시값으로 일급을 만들 수 있게 된다.
@Test void duplicate() { assertThatThrownBy(() -> LottoService.start( new Lotto(1, 2, 3, 4, 5, 6), new Lotto(1,2,3,4,5,6), new LottoNumber(7) )).isInstanceOf(IllegalArgumentException.class); }
- client입장에서 코드를 사용하게 엄청 편해진다.
my) 메서드의 [인자만 포장후 호출용 메서드] or 일급 등의 생성자를 [인자만 다르게 정의하고 내부는 포장해서] client를 편하게 만들어줬다.
-
가변인자 사용시 주의해야한다.
-
가변인자는 배열을 내부적으로 만들어서 사용하기 때문에, 메모리 잇슈가 있을 수 있다.
-
그래서
.of()
메서드들은 11개부터 가변인자를 사용하고 10개까지는 인자를 하드코딩으로 받는다.
-
일급 테스트는 Service.Method()에서 완성된 [가변인자생성자] 시리즈가 완성된 후에 해주자.
[Service.method()에서의 단일/일급 퉁치는 검증] + [일급의 포장용 가변인자 생성자] 끝났으면 [단일 도메인Test]에서 본격 [경계값 테스트]하기
-
Lotto라는 일급도메인 테스트를 만든다.
-
Service.method()에서 퉁치며 만들던 갯수/중복 테스트를 case별로 테매명에 반영하면서 만들어본다.
- 가변인자는 parameter로 내려보내질 못하네
-
갯수먼저 검사 -> 중복 검사
- 중복부터하면? distinct.count() != 6에서 list의 size검사도 같이 일어나 중복에 걸리게 된다.
@Test void count_valid() { assertDoesNotThrow(() -> new Lotto(1, 2, 3, 4, 5, 6)); } @Test void count_under() { assertThatThrownBy(() -> new Lotto(1, 2, 3, 4, 5)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("6개"); } @Test void count_over() { assertThatThrownBy(() -> new Lotto(1, 2, 3, 4, 5, 6, 7)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("6개"); } @Test void duplicate() { assertThatThrownBy(() -> new Lotto(1, 1, 3, 4, 5, 6)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("6개"); }
Service.메서드의 이름 수정
- start ->
match
일급으로 바꿨더니, 서비스 메서드 내부에 도메인 로직이? -> 메서드 분리로 역할 분리
- getter가 있으면 도메인내부로직임을 의심한다.
도메인 로직-특히 getter써놓은부분
)이 Service
.method()내부에 있는지 확인한다. getter가 있으면 의심해본다.
일급vs일급 or 일급vs단일의 로직(-
Lotto와 Lotto를 비교하는 로직이 보인다. 같은 도메인끼리의 비교를 서비스내에서 할 필요가 없다. 같은 놈끼리는 더더욱 메세지를 보내서 getter의 존재없이 비교되도록 + 도메인을 풍부하게 해준다.
- 비교로직을 도메인으로 넣고, matchCount만 받아오자.
public class LottoService { public static int start(final Lotto userLotto, final Lotto winningLotto, final LottoNumber bonusNumber) { int matchCount = 0; for (LottoNumber lotto : userLotto.getValue()) { if (winningLotto.getValue().contains(lotto)) { matchCount += 1; } }
-
matchCount는 서비스가 가지고 있어야하는데, 비교해서 누적하는 부분만
도메인 로직
이다.
서비스내 도메인 로직을 도메인 내부 메서드로 옮긴다.
-
getter로 꺼내서 억지로 하던 것이, 파라미터로 서로를 비교해도 바로 상태값을 갖다 쓸 수 있다. 일단 옮길 도메인내 메서드부터 만든다.
- 서비스에서 갖다쓸 예정이니 서비스에서 빨간줄로 만들어도 될듯?
-
같은 것끼리의 비교는
other
로 파라미터를 네이밍해주자- 파라미터 other(일급)이
other.value(같은도메인 getter)
로 list가 되면서 찢어진다.
- 파라미터 other(일급)이
- 돌면서 누적하는 것은
초기화 후 누적하는 폼
의가변변수
를 쓸 수 밖에 없다?
public int match(final Lotto other) { int matchCount = 0; for (LottoNumber lotto : other.value) { if (value.contains(lotto)) { matchCount += 1; } } return matchCount; }
포장.getter.메서드()
가 서비스메서드내 보인다면, 도메인로직으로 (내부 value.메서드()
가 되도록)넘겨준다.
포장과정에서 빠르게 처리하여 생긴 -
서비스메서드내 포장(도메인).getter가 보인다.
- 넘겨줄 수 없을까?
getter()를 지워
getValue().메서드를포장.메서드()
로 빨간줄을 생성하여 대체한다.
- 파라미터(매개변수) 이름으로
bonusNumbe
는 너무 구체적이다. 팀원입장에서 시그니쳐보고 쓸건데, 좀더 추상적이면서, 넓게…메서드 입장에서는 구체적인 bonus라는 것을 모른다.
LottoNumber 역시 클래스랑 완전 동일해서.. value를 일급이 이미썼으니 -> number로 네이밍 해준다.
- 바깥에서
포장.getValue().contains
로 사용됬던 것을 ->포장.contains
메서드 -> 도메인 내부에서 getter없이value.contains
로 사용하도록 만든다.
-
서비스로직에 geValue()를 달고 있는 것ㅇ 도메인 로직으로 옮겨가며되면,
포장(도메인).getValue()
가 비활성화 되며 -> alt+del로 안전삭제 해준다.
일급과 단일의 메서드가 정의됬다면 -> 일급vs일급에서도 사용될 가능성이 있다.
-
서비스 로직에 있던
일급.contains(단일)
의 메서드가 정의되었다면-
일급vs단일
은 일급내에서파라미터/우항이 찢어지는 일급vs일급 비교메서드
에서 쓰일 가능성이 높다.
- 정의했으면, 찾아서 대체해준다.
-
-
객체없이 호출되는 메서드 -> 순수묶음 or
[내부 포장된 변수](현재는 일급을 get한 단일List)
가 쓰이는 내부 메서드contains( xxx)
(현재 도메인의 포장하는 변수)가 생략된체로
cotains()
하고 있구나 생각
public int match(final Lotto other) { int matchCount = 0; for (LottoNumber lottoNumber : other.value) { //value:[일급] lotto:찢어진 일급이 돌고 있는 [단일] // [일급 내부] 메서드 contains는 // [일급의 getter(단일List)된 value가 내부에서 쓰이는] 메서드로서, // 들어오는 단일에 대해 [현재 일급의 단일List]가 <파라미터 단일>을 포함하는지 // my) 객체없는 메서드 -> 순수묶음 or [내부 포장된 변수](현재는 일급을 get한 단일List)가 쓰이는 내부 메서드 // if (value.contains(lotto)) { // 객체없이 호출된다? 내부상태가 쓰이는 내부 정의 메서드구나~! if (contains(lottoNumber)) { matchCount += 1; } } return matchCount; } public boolean contains(final LottoNumber number) { return value.contains(number); }
응답값도 포장
할 수 있다. 만약, 제한된 종류 + if분기마다 달라지는 응답값
라면 ENUM응답 포장
후 응답에 필요한 input들로 정펙매
를 호출한 뒤 내부로 분기 옮겨 응답한다. 이후 if분기를 처리해줄 < input->필드들 - 응답값>을 매핑
한다.
-
int
를 포장한 뒤 -> 관련된 if분기 로직을 포장한 도메인으로 넘기면,메서드 if문이
사라진다.,
제한된 종류의 응답 값
포장은 Enum을 활용한다. 메서드 응답을 빨간줄로 바꿔 생성하자.
-
만약, 제한되지 않았다면?
class Rank
로, 현재는제한된 종류
의 값 응답이므로Enum
으로 매핑해서if를 대신
하게 해보자. -
연속된 상수라면,
Enum명_ 순서
를 활용하면 된다.- 1,2,3,4,5등 ->
등
을 Enum으로 만들고 등_1,2,3,4,5; 로 선언해보자.
package lotto; public enum Rank { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5; }
- 1,2,3,4,5등 ->
응답에 대한 분기로직을 어떻게 응답class/응답Enum으로 넘길까?
메서드 응답부
에서 원하는 포장된 응답값인 Enum객체을 뽑아 응답해주는 return Enum.정펙매of (,)
완성하기
-
결과적으로
정펙매
인Enum.from()
orEnum.of( , , )
로 Enum객체를 응답해야한다.- 원하는 Enum객체가 응답될 수 있도록 필요한 인자들을 생각해서 받아들인다.
- 실제로
입력값이 될 것들을 가지고 있는 사용부인 메서드 응답부에서 부터 시작
-
응답부에서 Enum.정팩메로 해당하는
포장응답값
을return
해 줄 예정이다.- 필요한 input들이 뭐가 있는지 살펴본다.
- 메서드호출의 결과값이 있다면,
input
이 명확하도록 따로 변수로 뽑아주자.
-
정팩메로는
Enum
이라는포장된 응답값
을 가져올 것이기 때문에return
과 같이 작성한다.
값 응답 로직을 -> 포장응답값 정펙매로 잘라내서 처리하기
-
이제 값 응답로직을 그대로 잘라붙혀넣기 한다.
- 이동후
정팩메 내부로 옮긴 분기에 대해, 응답[값]들을 [Enum객체]로 수정해주기 -> 없는 값(0, None)에 대해서도 OUT,NONE객체 만들어서 응답해주기
-
아직 바꾸기 전 응답로직
if (matchCount == 6) { return 1; } if (matchCount == 5 && matchBonus) { return 2; } if (matchCount == 5) { return 3; } if (matchCount == 4) { return 4; } if (matchCount == 3) { return 5; } return 0;
-
수정 + NONE객체 추가후
package lotto; public enum Rank { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, OUT; public static Rank of(final int matchCount, final boolean matchBonus) { if (matchCount == 6) { return RANK_1; } if (matchCount == 5 && matchBonus) { return RANK_2; } if (matchCount == 5) { return RANK_3; } if (matchCount == 4) { return RANK_4; } if (matchCount == 3) { return RANK_5; } return OUT; } }
public static Rank match(final Lotto userLotto, final Lotto winningLotto, final LottoNumber bonusNumber) { final int matchCount = userLotto.match(winningLotto); final boolean matchBonus = userLotto.contains(bonusNumber); return Rank.of(matchCount, matchBonus); }
메서드 [포장된 응답] 테스트 작성하기
여러분기므로 1case에 대한 -> 1예상값 마다 테매를 작성해야한다.
@Test
void match_1() {
//given, when
final Rank rank = LottoService.match(
new Lotto(1, 2, 3, 4, 5, 6),
new Lotto(1, 2, 3, 4, 5, 6),
new LottoNumber(7)
);
//then
assertThat(rank).isEqualTo(Rank.RANK_1);
}
if분기를 enum 매핑으로 제거하기
들어올 데이터
들 中 1개를 골라 (매핑 예정인) Enum객체의 필드
로 넣어준다.
정팩메 input으로 -
정펙매로
여러 데이터
가 들어와 사용가 들어오지만of( matchCount, matchBonus)
매핑에 사용될 쉬운 변수 1개
만 선택하여,정펙매 내부 values().stream.filter 분기
에서 비교할 것이다. Enum객체가 필드로 가지고 있으면 되며, 그 필드 기준으로 매핑값도 차후 배정된다.
-
쉽게 Enum의 필드에 넣기 위해서는 일단 ENUM
(1)
객체 옆에 빨간줄로원시형 예제
작성해준다. -
생성자를 생성
해주면서input과 똑같이 네이밍
후빨간줄 this.필드명 = 필드; 초기화
를 이용해서 필드를 생성한다. -
나머지 객체도 데이터를 넣어준다.
public enum Rank { RANK_1(6), RANK_2(5), RANK_3(5), RANK_4(4), RANK_5(3), OUT(0); private final int matchCount; Rank(final int matchCount) { this.matchCount = matchCount; } public static Rank of(final int matchCount, final boolean matchBonus) { if (matchCount == 6) { return RANK_1; }
Enum정팩메 내 [제한된종류의 모든 if분기]를 values().stream + filter with input vs 매핑field로 분기 없애기
특수한 경우, 정팩매에서 ENUM매핑값 뽑기도 전에, early return해준다.
-
특수한 경우로서 2등과 3등을 먼저 필드는 안됬지만 사용해야하는 변수 matchBonus로 홀로 true인 2등을 early return한다.
public static Rank of(final int matchCount, final boolean matchBonus) { if (matchBonus && matchCount == 5) { return RANK_2; }
배열.stream자동완성
사용하여 EnumStream 만들어 객체전체를 돌리기
Enum내에서 values()는 Enum객체배열을 응답하며 거기에
제한된 종류의 if분기
를 values().stream (for) + filter(it -> input vs 매핑field 비교)
대체하기
public static Rank of(final int matchCount,
final boolean matchBonus) {
if (matchBonus && matchCount == 5) {
return RANK_2;
}
Arrays.stream(values())
.filter(it -> matchCount == it.matchCount)
.findAny()
.orElse(OUT);
- 기존 if분기들 이제 삭제
enum sense: 각 enum객체끝은 마지막도 콤마(,) 그 다음라인(;) + .findAny()후 .orElse( NONE객체 )
public enum Rank {
RANK_1(6),
RANK_2(5),
RANK_3(5),
RANK_4(4),
RANK_5(3),
OUT(0),
;
return Arrays.stream(values())
.filter(it -> matchCount == it.matchCount)
.findAny()
.orElse(OUT);
Bi(비.아이.) Predicate< , >
로 input된 2개 값을 다 사용하는 1개의 람다식필드 && filter속 예비 조건식(.test(a,b)로 실행)
을 만들어 -> [enum의 1 필드 매핑]에 사용되는 Type을 함수형인터페이스
로 사용하여 -> 정펙매 내부 들어오는 2개값 모두 사용하는 enum 만들기
-
BiPredicate Type은
1개 함수를 가진 인터페이스 = 함수형 인터페이스
중2개 인자를 받아 실행대기 중인 조건식
을 제공해주고 ->.test( a, b)
를 통해 외부인자를 받아서 조건식을 실행한다.-
.test(a, b)
에 정팩메로 들어온 input2개를 넣어주면 된다.
-
아직 BiPredicate 적용안한 기존 코드 (상금도 매핑 안된 상태)
public enum Rank {
RANK_1(6),
RANK_2(5),
RANK_3(5),
RANK_4(4),
RANK_5(3),
OUT(0),
;
private final int matchCount;
Rank(final int matchCount) {
this.matchCount = matchCount;
}
public static Rank of(final int matchCount,
final boolean matchBonus) {
if (matchBonus && matchCount == 5) {
return RANK_2;
}
return Arrays.stream(values())
.filter(it -> matchCount == it.matchCount)
.findAny()
.orElse(OUT);
}
}
Enum필드에 2인자 람다식인 BiPredicate를 넣어서 생성하기
- 이번엔
Enum()
객체안에 input 중 1개 6,5,4.. 대신에input 2필드 다 써서 1개 람다식
을 만들어 예제로 넣어주자.- 이왕이면 다중커서로 한번에 다 입력해주자.
직접 필드입력 -> private final 필드 변수 생성 -> 생산자 자동완성
로 가야한다. (원래는 예제 -> 생산자 -> 빨간줄 this.필드 생성)
아쉽게도 BiPredicate는 - 원래 객체(
원시값 등 쉬운 Type
)을 빨간줄 필드로 넣으면, 생성자에 파라미터 공짜 -> this.로 필드 공짜 생성
-
BiPredicate 작성
-
받은 인자 2개는
if분기에 쓰였던 input값 2개
다. ->원래 Enum의 매핑 쉬운 상수 응답값 6, 5, 4...
와 비교해야한다.
public enum Rank { RANK_1(((matchCount, matchBonus) -> matchCount == 6)), RANK_2((matchCount, matchBonus) -> matchCount == 5 && matchBonus), RANK_3((matchCount, matchBonus) -> matchCount == 5 && !matchBonus), RANK_4((matchCount, matchBonus) -> matchCount == 4), RANK_5((matchCount, matchBonus) -> matchCount == 3), OUT((matchCount, matchBonus) -> matchCount < 3), ;
-
받은 인자 2개는
-
필드 직접 입력
private final
- 제네릭 타입은 들어올 인자들의 Type(정팩메로 들어올 Type)을 Boxing한 타입 +
네이밍은 condition
으로 실행될 조건식임을 인지시킴
private final BiPredicate<Integer, Boolean> condition;
- 제네릭 타입은 들어올 인자들의 Type(정팩메로 들어올 Type)을 Boxing한 타입 +
-
생성자 자동완성
.test( , )로 정펙매 인자
를 받아 조건식 실행
values().stream + filter분기에서 public static Rank of(final int matchCount,
final boolean matchBonus) {
return Arrays.stream(values())
.filter(it -> it.condition.test(matchCount, matchBonus))
.findAny()
.orElse(OUT);
}
응답값의 포장(리팩토링)이 다 끝났으면, 자체 도메인Test(Enum은 정펙매 테스트)
@Test
void rank_1() {
final Rank rank = Rank.of(6, false);
assertThat(rank).isEqualTo(Rank.RANK_1);
}
@Test
void rank_2() {
final Rank rank = Rank.of(5, true);
assertThat(rank).isEqualTo(Rank.RANK_2);
}
@ParameterizedTest
@CsvSource({"5,false,RANK_3", "4, false, RANK_4", "3, false, RANK_5"})
void rank_3_5(final int matchCount, final boolean matchBonus, final Rank expected) {
final Rank rank = Rank.of(matchCount, matchBonus);
assertThat(rank).isEqualTo(expected);
}
리팩토링(가변변수 0에 조건만족시 갯수 누적)
직전까지의합 0가변변수에 조건 만족시 add 1 -> stream + filter + count
- 변경 전: 0이라는
int 가변변수
를 먼저 선언해놓고 -> 돌면서 조건만족시 ->가변변수에 += 1;
하는 행위
// 일급 Lotto
public int match(final Lotto other) {
int matchCount = 0;
for (LottoNumber lottoNumber : other.value) {
if (contains(lottoNumber)) {
matchCount += 1;
}
}
return matchCount;
}
- stream의 filter + count로 원하는 조건의 갯수를 누적된 상태로 바로 뽑을 수 있다.
// 미리 가변 변수 int = 0;을 선언하지말고 조건만족시를 다 count()로 갯수만 반환받는다.
// - long이 기본 타입이라.. 캐스팅 해야한다.
public int match(final Lotto other) {
// 우항, 파라미터를 찢는 놈이라고 했으니, other.value.stream()으로 찢었다.
return (int) other.value.stream()
.filter(it -> contains(it))
.count();
}
stream없이 for+if 에서 if를 메서드분리 -> 누적변수없이도 응답 가능=갯수니까 return 1 or 0;
-
변경 전
public int match(final Lotto other) { int matchCount = 0; for (LottoNumber lottoNumber : other.value) { if (value.contains(lottoNumber)) { matchCount += 1; } } return matchCount; }
for문 내부 if는 메서드 추출
로 통채로 빼기 -> if가 바깥쪽 가변변수 조작한다면? return = 메서드추출()로 받아진다.
-
원래 if 내부에서 바깥의 가변변수를 건들였으니
if가 있던 자리
->바깥 가변변수 = 메서드추출()
로 return받도록 짜여졌다.- 메서드추출했는데 변수에 리턴받는다? -> 내부에서 바깥의 가변변수를 건들였던 것을 뱉어준 것
public int match(final Lotto other) { int matchCount = 0; for (LottoNumber lottoNumber : other.value) { matchCount = getMatchCount(matchCount, lottoNumber); } return matchCount; } private int getMatchCount(int matchCount, final LottoNumber lottoNumber) { if (value.contains(lottoNumber)) { matchCount += 1; } return matchCount; }
가변 = 메서드()
형태다
메서드추출로 튀어나온 (내부에서 사용되던 바깥선언의) 가변변수는 무조건 -
원래
if문 내부에서 가변변수++, 가변변수+=1
를 해줬었는데… 추출된 것은 무조건변수 = 메서드()
형태인 이유는-
추출된 메서드의 파라미터로 가변변수가 들어간다.
for (LottoNumber lottoNumber : other.value) { matchCount = getMatchCount(matchCount, lottoNumber); }
-
가변변수가
파라미터에서 선언되는 이름만 똑같은 지역변수
가 되는데, 이름은 같아도바깥 가변변수와는 전혀 다른 것
이다.
-
갯수 증가
의 가변++/가변+=1
은 메서드추출시 파라미터(=지역변수)로 안가도 return 1 or 0
만 해주면 되므로 [추출메서드에서 파라미터 삭제] -> 이후 가변 =
이 아닌 가변 +=
으로 바꿔주면 된다.
-
파라미터=지역변수를 없애고,
지역변수++
자리를return 1
로 아니면return 0;
으로 대체하자.private int getMatchCount(int matchCount, final LottoNumber lottoNumber) { if (value.contains(lottoNumber)) { matchCount++; } return matchCount; }
// 지역변수 지우고, 걸리면 갯수증가용 1 , 안걸리면 0 을 반환 private int getMatchCount(final LottoNumber lottoNumber) { if (value.contains(lottoNumber)) { //matchCount++; return 1; } return 0; }
-
메서드 추출 기능 자체의 문제가
가변+= 메서드리턴()
이 안되서 ->가변을 파라미터로 받은 뒤, 지역변수++, 지역변수+= 후 return
되는 문제였다.가변 = 메서드 추출()
부분을가변 += 메서드 추출()-1or0리턴;
으로 바꿔주자.
public int match(final Lotto other) { int matchCount = 0; for (LottoNumber lottoNumber : other.value) { //matchCount = getMatchCount(matchCount, lottoNumber); //matchCount = getMatchCount(lottoNumber); matchCount += getMatchCount(lottoNumber); } return matchCount; }