티스토리 뷰

프리코스 2주차를 11/2 ~ 11/8 진행하였습니다. 여러 개의 문제를 해결해야 했던 1주차와 달리 2주차는 숫자 야구 게임을 구현하는 것이 과제로 주어졌습니다. 개인적으로 1주차보다 2주차가 더 재밌었던 것 같습니다! 남은 기간 어떤 과제를 줄지 벌써 기대하고 있습니다.

요구 사항 분석

미션은 기능을 구현하기 전 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행해야 합니다. 문제를 해결하기 위해 문제를 먼저 파악하듯 구현 기능을 위해 먼저 요구사항을 자세히 읽고 분석해 보았습니다.

분석한 요구사항을 바탕으로 구현해야 할 기능에 대해 생각해보았습니다. 추상적인 기능에서부터 구체적인 기능으로 좁혀갔습니다. 아래의 사진은 제가 구현하고자 했던 기능의 목록입니다.

숫자 야구 게임이 아닌 게임이라는 명칭을 사용한 이유는 다른 게임 구현체가 들어와도 게임만 변경한다면 코드의 변경을 최소한으로 동작시키고자 하였습니다. 열려 있는 설계를 통해 확장성 있는 코드에 대해 생각해볼 수 있었던 것 같습니다. 큰 기능에서 작은 기능으로 좁혀가며 구현해야 할 기능들을 작성할 수 있었습니다.

처음에 추가로 사용되는 라이브러리에 대한 정보를 읽지 못해서 입력 기능과 테스트도 구현했지만 제공해주는 입력 라이브러리가 존재해서 수정을 진행하였습니다. 글을 꼼꼼히 읽는 습관을 들여야겠습니다.

저번 과제와 다르게 추가적인 요구사항도 주어졌습니다. 요구사항을 보고 메서드를 분리하여 작게 만들어 메서드가 하는 역할이 무엇인지 분명하게 드러내고자 하는 의도를 확인할 수 있었습니다. 또한 테스트 코드를 직접 작성하며 메서드가 정상적으로 동작하는지 확인하는 것도 포함하고 있었습니다.

기능 구현

클래스 분리

정리한 기능을 바탕으로 기능에 알맞은 클래스를 분리해보았습니다. 숫자 야구 게임에 있어 핵심적인 기능을 수행하는 클래스는 domain 패키지에 게임과 관련 없이 유틸리티 기능을 수행하는 클래스는 util 패키지에 위치하도록 했습니다.

Game
  • 게임 동작을 위한 인터페이스로 동작하고자 하는 게임은 이를 구현합니다.
GameManger
  • 게임을 실행시키고 게임이 종료된 후 재실행/종료를 결정합니다.
  • 실행하고자 하는 게임을 받아 생성합니다.
Baseball
  • 숫자 야구 게임을 위한 구현체입니다.
  • 숫자 야구 게임 로직을 실행합니다.
BallCount
  • 플레이어와 컴퓨터 비교 결과를 저장합니다.
  • 저장된 결과로 게임이 종료되는지 알려줍니다.
  • 저장된 결과로 알맞은 출력을 보여줍니다.
BaseballGuide
  • 숫자 야구 게임의 가이드 역할을 수행합니다.
  • 필요한 정보를 상수로 가지고 있습니다.
  • 플레이어 입력을 검증합니다.
GamePrinter
  • 게임에서 발생하는 출력을 담당하는 역할을 수행합니다.

역할에 따라 클래스를 적절히 분리하였더니 오류가 발생했을 때 바로바로 고칠 클래스를 찾을 수 있었고 클래스 간 의존성도 줄어들고 코드의 변화가 발생했을 때 해당 클래스 내에서만 수정이 발생하여 다른 클래스까지 영향을 주지 않았습니다. 책을 읽으며 이론적으로만 학습했던 내용을 제 코드에 적용해볼 수 있어서 즐겁게 할 수 있었습니다.

조건은 긍정으로

//#1
if (!isValidLength(input)) {
    ...
}

//#2
if (isInvalidLength(input)) {
    ...
}

//#3
if (hasLongLength(input)) {
    ...
}

#1#2 중 어느 것이 더 직관적으로 의미를 파악하기 쉬운가요? Not 연산자를 통해 한번 더 생각해야하는 #1과 달리 #2이 조금 더 직관적이라고 판단했고 #2처럼 코드를 작성해서 과제를 진행했습니다.

 

하지만 회고를 작성하며 #1#2Not 연산자를 사용하지 않았을 뿐 생각하는 관점이 비슷한 것을 느끼게 되었습니다. 유효한 길이가 아닐 때, 그럼 유효한 길이는 뭘까라고 다시 한번 생각되는 것 같습니다. 이는 부정 조건문을 사용함으로 인해 발생하는 문제임을 알게 되었습니다. 실제로 클린 코드라는 책에서도 부정 조건문을 지양하도록 권장하고 있습니다. #3과 같이 긍정 조건문을 사용하며 명확한 의도를 드러내는 메서드가 가장 직관적임을 생각해낼 수 있었습니다. 다음 과제부터는 조금 더 긍정 조건문을 지향하고자 합니다.

불변 객체

public class BallCount {

    private int strike;
    private int ball;

    public BallCount() {
        reset();
    }

    ...

    public void reset() {
        strike = 0;
        ball = 0;
    }

    public void strike() {
        strike += 1;
    }

    public void ball() {
        ball += 1;
    }

    ...

}

처음 BallCount 클래스를 작성했을 때 위와 같이 작성했습니다. 이처럼 객체 내부 상태를 변화할 수 있게 만든 이유는 입력마다 플레이어와 컴퓨터의 숫자를 비교한 결과를 저장하는 BallCount 인스턴스를 새롭게 생성하기보다는 1번의 게임당 1개만 생성하고 내부 상태를 초기화해서 생성 없이 진행하고자 하였습니다. 입력이 반복적으로 발생하는데 계속해서 인스턴스를 생성하는 비용이 많이 든다고 생각했기 때문입니다.

 

리팩토링 과정을 통해 코드를 다시 살펴보던 도중 이펙티브 자바에서 읽었던 내용이 떠올랐습니다.

클래스는 꼭 필요한 경우가 아니면 불변인 것이 좋다.

위처럼 내부 상태를 변경할 수 있게 설계한다면 해당 객체를 언제든 변경할 수 있기에 side-effect가 발생할 수 있고 불변이 보장되지 않기에 다른 사람이 코드를 보았을 때 해당 객체의 변경 상태를 예측하기 어렵게 만듭니다.

 

불변 객체로 만들면 위의 단점을 해결할 수 있습니다.

public class BallCount {

    private final int strike;
    private final int ball;

    public BallCount(int strike, int ball) {
        this.strike = strike;
        this.ball = ball;
    }

    ...

}

불변 객체로 만들기 위해 아래의 방법을 사용했습니다.

  • 객체의 상태를 변경하는 메서드를 제공하지 않습니다.
  • 모든 필드를 final로 선언합니다.
  • 모든 필드를 private로 선언합니다.
  • 자신 외에는 내부 필드에 접근할 수 없도록 합니다.

불변 객체의 장점은 다음과 같습니다.

  • 상태 변화가 발생하지 않기에 side-effect가 없습니다.
  • 불변이 보장되기에 객체의 변경 상태를 쉽게 예측할 수 있습니다.
  • 스레드 안전하여 동기화가 필요하지 않습니다.

불변 객체의 단점도 존재합니다.

  • 값이 다르면 새로운 객체를 만들어야 하기에 비용이 발생합니다.

해당 과제에서는 불변 객체의 장점을 포기할 만큼 생성 비용이 많이 들지 않다고 판단하였고 불변 객체로 변경하기로 하였습니다. 불변 객체에 대해 다시 한번 공부할 수 있었고 성능과 장점 사이의 trade-off가 발생하는데 판단하여 잘 결정하는 것도 좋은 프로그래머가 가져야 할 역량이라는 생각이 들었습니다.

테스트

코드의 변경이 발생했을 때 테스트 코드가 존재함으로 오는 안정성을 테스트 코드를 공부하며 직접 느낄 수 있었고 테스트 코드 작성에 많은 관심을 가지게 되었습니다.

 

평소에도 고민이었던 단위 테스트의 범위는 어디까지인가에 대해 생각해볼 수 있었습니다. 테스트 코드를 작성하며 모든 메서드를 테스트하는 것이 필요할까? 라는 생각이 들었습니다. 모든 메서드를 테스트하는 것은 물론 중요하지만, 테스트를 위해 모든 메서드가 노출되게 된다면 추상화를 어렵게 만들기 때문입니다.

 

추상화와 테스트 사이 적절한 접점을 찾는 것이 중요했고 public 메서드들은 최대한 테스트할 수 있는 코드로 작성하기 위해 노력했습니다.

20여 개의 테스트 코드를 직접 작성해볼 수 있었고 다시 한번 테스트 코드의 중요성을 느낄 수 있었습니다. 단위 테스트에 대한 고민은 남은 프리코스 기간 동안 계속해서 이어 나가고자 합니다.

Stream

이번 과제에서 해보고 싶은 가장 큰 목표는 Stream을 적극적으로 사용하는 것이었습니다. Stream을 정확한 판단 없이 남발하는 것도 문제가 되지만 이번 과제에서는 Stream 사용으로 인해 발생하는 성능상의 문제가 없다고 판단하였기에 Stream을 사용할 수 있는 부분에 대해서는 모두 적용하여 기능을 구현하였습니다.

검증이 완료된 입력받은 숫자 문자열을 Integer 리스트로 만드는 메서드입니다. String 자체적으로 chars() 메서드를 제공하여 IntStream으로 쉽게 변환할 수 있었고 중간 연산을 통해 Integer 값으로 변경하여 최종 연산으로 List<Integer> 결과를 얻을 수 있었습니다.

플레이어와 컴퓨터 숫자를 비교해서 결과인 BallCount를 만드는 메서드입니다. 인덱스 사용을 위해 숫자 길이만큼의 IntStream을 생성하였고 filter를 적용하여 strike, ball인 경우만 데이터가 최종 연산으로 옮겨져 개수를 구할 수 있었습니다. for문을 사용했을 때 비해 한눈에 무슨 일을 수행하는지 확인할 수 있었습니다.

플레이어의 입력 중 숫자가 아닌 것이 있는지 판별하는 메서드입니다.

플레이어의 입력 중 범위에 속하지 않는 숫자가 있는지 판별하는 메서드입니다.

플레이어의 입력 중 중복되는 문자가 있는지 판별하는 메서드입니다.

게임 재실행/종료 상태를 Enum으로 관리했는데 플레이어의 재실행에 대한 입력이 들어왔을 때 입력이 Enum에 존재하는 상태면 해당 값을 반환하고 없다면 예외를 발생시키는 메서드입니다. 체인 형식으로 함수가 잘 드러나 있어 무슨 일을 하는지 직관적으로 확인할 수 있습니다.

 

하나하나 조립해가는 과정도 너무 재밌었고 코드 가독성 효과도 확인할 수 있었기에 앞으로도 적극적으로 사용할 것 같습니다.

마치며

평소 궁금했던 내용이나 더 알아보고 싶은 내용을 과제 구현을 통해 학습하니 효율적으로 공부할 수 있었던 2주차였습니다. 3주차에서는 2주차에서 학습한 내용을 조금 더 다듬어서 적용해보고자 합니다. 3주차도 열심히 해보겠습니다! '^'

 

자세한 코드는 아래에서 확인할 수 있습니다.

https://github.com/woo-chang/java-baseball/tree/woo-chang

댓글
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday