강의 링크
https://edu.nextstep.camp/c/9WPRB0ys/
깃 허브 저장소
학습 전
https://github.com/ljw1126/my-java-baseball-playground/blob/myself/src/main/java
학습 후
https://github.com/ljw1126/my-java-baseball-playground/tree/practice/src/main/java
학습 전 직접 작성한 코드 (펼처보기)
반성)
① 객체 지향적 언어를 사용하면서도, 거리가 먼 절차적 코드를 작성
② 절차적 코드로 인해 한 눈에 로직을 파악하기 힘듦 (가독성 저하)
③ 테스트 코드 미작성
④ 클래스 미분리 - view, model, controller
⑤ 의미 불명의 매직넘버/스트링 값을 사용하고 있어 수정 범위를 좁히기 힘듦
public class BaseBallGame {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String actual = getRandomNumber();
String next = "";
while(!"2".equals(next)) {
System.out.println("숫자를 입력해 주세요 : ");
StringBuilder sb = new StringBuilder();
String expected = br.readLine();
// 비교 해서 출력
Map<String, Integer> resultMap = getResult(actual, expected);
if(resultMap.isEmpty()) {
System.out.println("낫싱");
continue;
}
if(resultMap.getOrDefault("스트라이크", 0) == 3) {
System.out.println("3스트라이크");
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임종료");
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요");
next = br.readLine();
continue;
}
for(String key : resultMap.keySet()) {
sb.append(resultMap.get(key)).append(key).append(" ");
}
System.out.println(sb);
}
}
public static Map<String, Integer> getResult(String actual, String expected) {
Map<String, Integer> resultMap = new HashMap<>();
for(int i = 0; i < 3; i++) {
if(expected.charAt(i) == actual.charAt(i)) {
resultMap.put("스트라이크", resultMap.getOrDefault("스트라이크", 0) + 1);
continue;
}
if(actual.indexOf(expected.charAt(i)) != -1) {
resultMap.put("볼", resultMap.getOrDefault("볼", 0) + 1);
}
}
return resultMap;
}
public static String getRandomNumber() {
return new Random()
.ints(1, 9)
.distinct()
.limit(3)
.boxed()
.map(String::valueOf)
.collect(joining(""));
}
}
복습. 숫자 야구 게임
- 1~9까지 서로 다른 수로 이루어진 3자리 수를 맞추는 게임이다
- 같은 수가 같은 자리에 있으면 STRIKE(스트라이크), 다른 자리에 있으면 BALL(볼), 같은 수가 전혀 없으면 NOTHING(낫싱) 힌트를 얻고 컴퓨터의 수를 맞추면 승리한다
컴퓨터 123 일때
-입력 123 의 경우 3 STRIKE
-입력 213 의 경우 1 STRIKE 2 BALL
-입력 456의 경우 NOTHING
1. Random 볼 번호 생성
[기능 요구사항]
중복 없이 3개의 숫자를 가진다
각 볼 번호는 1 ~ 9 사이 숫자 중 하나이다
RandomBallNoGenerator 클래스
- 유틸 패키지/클래스 분리한다
- 반환 타입을 List<Integer> 으로 변경하여 문자열 처리 과정을 줄인다
public class RandomBallNoGenerator {
public static List<Integer> generateRandomBallNo() {
return Collections.emptyList();
}
}
테스트 코드 작성
@Test
void 볼번호는_중복없이_3개가되야한다() {
List<Integer> randomBallNo = RandomBallNoGenerator.generateRandomBallNo();
assertThat(new HashSet<>(randomBallNo)).hasSize(3);
}
@Test
void 볼번호는_1에서_9사이_숫자이다() {
List<Integer> randomBallNo = RandomBallNoGenerator.generateRandomBallNo();
for(int ballNo : randomBallNo) {
assertThat(ballNo).isBetween(1, 10); // 1 ~ 9
}
}
- assertj 의 Assertions 기능을 활용한다
-빨간 불을 보았으니 빠르게 기능 구현 한다
기능 구현
public class RandomBallNoGenerator {
public static List<Integer> generateRandomBallNo() {
return new Random()
.ints(1, 10)
.distinct()
.limit(3)
.boxed()
.collect(Collectors.toList());
}
}
테스트 수행
추가적으로 특별히 할 게 없는거 같다.
테스트에서 반복문으로 랜덤 볼 번호가 1 ~ 9 사이인지 확인하는 횟수를 늘려보고 넘어간다
2. Ball 클래스 기능 구현 및 테스트
- 기존 코드는 컴퓨터 볼번호(문자열)과 사용자 볼번호(문자열)을 비교하여 결과를 구했었다
- Ball 클래스 생성하고, Ball을 비교하는 방법으로 구현해본다
[기능 요구사항]
Ball 클래스는 위치와 볼 번호 정보를 가진다
위치와 볼을 비교하여 다음 결과를 반환한다
위치와 볼 번호 둘다 같으면 STRIKE
볼 번호만 같으면 BALL
둘 다 다르면 NOTHING
Ball 클래스 생성
public class Ball {
private int position;
private int ballNo;
public Ball(int position, int ballNo) {
this.position = position;
this.ballNo = ballNo;
}
}
enum BallStatus 생성
볼과 볼을 비교시 결과값을 문자열로 반환하는 것보다 enum 클래스 생성하여 활용하도록 하였다
public enum BallStatus {
STRIKE("스트라이크"),
BALL("볼"),
NOTHING("낫싱");
private String name;
BallStatus(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
테스트 코드 작성
class BallTest {
private Ball computer = new Ball(1, 1);
@Test
void strike() {
Ball user = new Ball(1, 1);
assertThat(user.compare(computer)).isEqualTo(BallStatus.STRIKE);
}
@Test
void ball() {
Ball user = new Ball(2, 1);
assertThat(user.compare(computer)).isEqualTo(BallStatus.BALL);
}
@Test
void nothing() {
Ball user = new Ball(3, 3);
assertThat(user.compare(computer)).isEqualTo(BallStatus.NOTHING);
}
}
빨간 불을 보았으니 빠르게 기능 구현 한다
Ball 클래스 - compare() 기능 구현
public class Ball {
// position, ballNo, 생성자
public BallStatus compare(Ball other) {
if(this.ballNo == other.ballNo && this.position == other.position) {
return STRIKE;
}
if(this.ballNo == other.ballNo) {
return BALL;
}
return NOTHING;
}
}
테스트 실행시 초록불을 확인했다
리팩토링
"객체 필드에 직접 접근하는 것보다 메시지를 보내라" - 자바지기
import static model.BallStatus.*;
public class Ball {
// ..
public BallStatus compare(Ball other) {
if(this.equals(other)) { //* 여기
return STRIKE;
}
if(isSameBallNo(other)) { //* 여기
return BALL;
}
return NOTHING;
}
private boolean isSameBallNo(Ball other) {
return this.ballNo == other.ballNo;
}
@Override
public boolean equals(Object o) {
//..
}
}
개인적으로 이 부분에서 처음으로 신선함을 얻었다
보통 실무에서도 조건문에 논리 연산자가 많으면 읽기 불편한 적도 많았고, 무슨 의미를 나타내는지 이해하는데 시간 소모 했던 적이 많았다. (더군다나 시간에 쫓기기 때문에 리팩토링은 하지 못하고 넘어가는 경우가 대다수라 다시 보면 .. 😅)
"메시지를 보내라" 는 말처럼 객체의 행동 호출하여(=메시지) 조건문이 간소화가 되는 것을 확인할 수 있었다.
작은 부분이지만 객체 지향적인 코드의 느낌을 처음으로 받았던 부분이었다
3. Balls 클래스 기능 구현 및 테스트
[기능 요구사항]
사용자와 컴퓨터의 Balls를 비교하고 결과를 반환한다
NOTHING인 경우
3 STRIKE 경우
3 BALL인 경우
1 STRIKE, 2 BALL 인 경우
테스트 코드 작성
컴퓨터와 사용자 Balls를 비교시 ResultData 결과값을 반환하도록 코드 작성하였다
class BallsTest {
private Balls computerBall = new Balls(Arrays.asList(1, 2, 3));
@Test
void equals() {
Balls expectedBall = new Balls(Arrays.asList(1, 2, 3));
assertThat(computerBall).isEqualTo(expectedBall);
}
@Test
void nothing() {
Balls userBall = new Balls(Arrays.asList(4, 5, 6));
ResultData actual = userBall.match(computerBall);
ResultData expected = new ResultData(0, 0);
assertThat(actual.isNothing()).isTrue();
assertThat(actual).isEqualTo(expected);
}
}
Balls 클래스
public class Balls {
private List<Ball> ballList;
public Balls(List<Integer> ballNumbers) {
this.ballList = mapBallList(ballNumbers);
}
private static List<Ball> mapBallList(List<Integer> ballNumbers) {
List<Ball> balls = new ArrayList<>();
for(int i = 0; i < ballNumbers.size(); i++) {
balls.add(new Ball(i + 1, ballNumbers.get(i)));
}
return balls;
}
public ResultData match(Balls otherBalls) {
ResultData resultData = new ResultData();
return resultData;
}
// equals, hashCode 생략..
}
ResultData 클래스 생성
strike와 ball 카운팅하는 책임을 맡기도록 하였다
public class ResultData {
private int strikeCount;
private int ballCount;
public ResultData() {
this.strikeCount = 0;
this.ballCount = 0;
}
public ResultData(int strikeCount, int ballCount) {
this.strikeCount = strikeCount;
this.ballCount = ballCount;
}
public boolean isNothing() {
return this.strikeCount == 0 && this.ballCount == 0;
}
// equals, hashCode 생략
}
초록불 확인
특별히 할게 없어 다음 테스트 코드를 추가 후 기능 구현/리팩토링을 진행하였다
@Test
void result_3strike() {
Balls userBall = new Balls(Arrays.asList(1, 2, 3));
ResultData actual = userBall.match(computerBall);
ResultData expected = new ResultData(3, 0);
assertThat(actual).isEqualTo(expected);
}
빨간 불을 보았으니 빠르게 기능 구현 한다
Balls 클래스 기능 구현 및 리팩토링
"객체 필드에 직접 접근하는 것보다 메시지를 보내라" - 자바지기
- 자바지기님의 말씀대로 메세지를 보냄 > 비교 > BallStatus 반환 > 결과 데이터 저장하는 형태로 기능 구현해보았다
- 처음 2중 for문을 활용해서 결과값을 구하였는데, 코드 가독성이 좋지 않아 Stream API를 사용하였다
- 메소드 내에서 멤버 변수와 로컬 변수를 구분하기 위해 this.* 키워드를 붙였다
- private 메소드의 경우 public 비즈니스 로직을 테스트하면 검증이 되기 때문에 단위 테스트 생략
public class Balls {
// 멤버 변수, 생성자 생략
public ResultData match(Balls otherBalls) {
ResultData resultData = new ResultData();
for(Ball ball : this.ballList) {
resultData.addResult(otherBalls.judgement(ball));
}
return resultData;
}
private BallStatus judgement(Ball other) {
return this.ballList.stream()
.map(ball -> ball.compare(other)) // Stream<BallStatus>
.filter(ballStatus -> !ballStatus.isNothing())
.findFirst() // Optional<BallStatus>
.orElse(BallStatus.NOTHING);
}
// equals, hashCode 생략
}
ResultData 에서는 아래와 BallStatus에 메시지를 보내는 방식으로 변경하고, 상태값을 갱신시켜줬다
public class ResultData {
// 필드, 생성자 생략..
public void addResult(BallStatus ballStatus) {
if(ballStatus.isStrike()) { // 변경전 ballStatus == BallStatus.STRIKE
this.strikeCount += 1;
}
if(ballStatus.isBall()) { // 변경전 ballStatus == BallStatus.BALL
this.ballCount += 1;
}
}
}
테스트 케이스 추가 후 동작시 정상적으로 초록불 확인할 수 있었다
테스트 코드 리팩토링
- 아래와 같이 케이스별 테스트 구조가 반복되는 것을 확인할 수 있었다
- @ParamiterizedTest + @MethodSource를 활용하여 간소화하도록 하였다
변경 전)
public class BallsTest {
private Balls computerBall = new Balls(Arrays.asList(1, 2, 3));
@Test
void nothing() {
Balls userBall = new Balls(Arrays.asList(4, 5, 6));
ResultData actual = userBall.match(computerBall);
ResultData expected = new ResultData(0, 0);
assertThat(actual.isNothing()).isTrue();
assertThat(actual).isEqualTo(expected);
}
@Test
void result_3strike() {
Balls userBall = new Balls(Arrays.asList(1, 2, 3));
ResultData actual = userBall.match(computerBall);
ResultData expected = new ResultData(3, 0);
assertThat(actual).isEqualTo(expected);
}
// 이하 생략..
}
변경 후)
public class BallsTest {
private Balls computerBall = new Balls(Arrays.asList(1, 2, 3));
@ParameterizedTest
@MethodSource("getBallsTestDataProvider")
void ballsMatchTest(List<Integer> givenBallList, ResultData expected) {
Balls userBall = new Balls(givenBallList);
ResultData actual = userBall.match(computerBall);
assertThat(actual).isEqualTo(expected);
}
public static Stream<Arguments> getBallsTestDataProvider() {
return Stream.of(
Arguments.arguments(Arrays.asList(4, 5, 6), new ResultData(0, 0)),
Arguments.arguments(Arrays.asList(1, 2, 3), new ResultData(3, 0)),
Arguments.arguments(Arrays.asList(2, 3, 1), new ResultData(0, 3)),
Arguments.arguments(Arrays.asList(3, 2, 1), new ResultData(1, 2))
);
}
}
테스트 통과
회고
처음 과제를 접하고 구현했을 때가 아직도 생생하다.
객체 지향 코드를 작성할 줄 몰라 일단 구현을 하고 뿌듯함을 느꼈지만,
강의를 보고 난 후 쓰레기 같은 코드를 작성했음에 부끄러움이 몰려왔었다.
"객체 필드에 직접 접근하는 것보다 메시지를 보내라" - 자바지기
자바지기님의 말씀을 귀담아 듣고 어떻게 하면 메시지를 보낼 수 있을지 고민하고, 학습한 덕분에 이전보다 나은 코드를 작성할 수 있었던게 아니었나 싶다. (3주가 지나 복습하는데도 잊혀지지 않는다 😭)
국비 교육으로 시작하다보면 대부분 SI로 가서 게시판만 주구장창 만들다보면 객체 지향과는 거리가 멀게 된다는 느낌을 한번은 받을 것이라고 생각한다. 나 또한 그랬고, 쓰레기 같은 코드가 아닌 객체 지향 언어를 좀 더 객체 지향 언어 답게 사용할 수 있을지 고민했지만 어떻게 할 지 몰라 방황을 많이 했었다.
만약 이러한 방황을 하고 있는 개발자가 있다면 이 강의를 보는 것을 추천하고 싶다.
'공부 > Java' 카테고리의 다른 글
[넥스트스탭] 좌표계산기 - 자바 플레이 그라운드 (3) | 2024.04.30 |
---|---|
[넥스트스탭] 자동차 경주 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[Java] Reflection API 테스트 학습(with Baeldung) (0) | 2023.11.20 |
[Java] Stream Quiz 개인 풀이 (출처. 망나니 개발자 기술 블로그) (0) | 2023.08.15 |
[Java] Stream 최종 연산(Terminal Operation) (0) | 2023.08.05 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!