[내일배움캠프] 숫자 야구 게임 구현 - 리팩토링(1)

2024. 9. 20. 17:51내일배움캠프

 현재 'Level01' 부터 'Level02' 까지의 요구사항을 반영하였는데, 코드를 한 번 다시 확인해보고 '리팩토링(refactoring)' 해 보았다. 수정된 코드는 여기서 확인이 가능하다.

 

1. application 수행 흐름

 반영된 요구사항에 따르면 사용자가 게임을 '시작 - 종료' 할 때, application 은 아래와 같은 수행 흐름을 가지게 된다.

  1. 프로그램 시작
  2. 시작 메뉴 노출 : 사용자는 메뉴를 보고 게임을 시작 또는 종료 할 수 있음
  3. 게임 시작 선택 : 게임이 시작되면 application 은 현재 게임의 '정답' 을 생성
  4. 입력 : 사용자는 정답이라 생각하는 값을 입력, application 은 '정답' 과 '입력' 을 비교하고 결과를 사용자에게 알려줌
  5. 정답 입력 : 사용자의 입력이 정답이면 축하 메시지와 함께 다시 '시작 메뉴' 를 사용자에게 노출함
  6. 메뉴 선택 : 사용자는 다시 한 번 게임을 진행하든 프로그램을 종료할 수 있음
  7. 종료 선택 : application 이 '정상' 종료됨

하지만 현재 작성한 코드를 보니 좀 어색한(?) 부분이 존재해 수정하고자 한다.

Main.main() 메서드

 

현재 프로젝트의 'main()' 메서드인데, 요구사항을 반영하다 보니 위의 이미지와 같은 형태를 가지게 되었다. '시작 메뉴' 를 출력해주는 것 또한 'App' 에서 수행될 로직이란 생각이 들어, 현재 처럼 'main()' 메서드에서 시작 메뉴를 출력해주는 것은 구현 의도를 벗어났다고 느껴졌다. 그래서 아래와 같이 리팩토링을 진행했다.

 

참고 : 애초에 'application' 으로 생각하고 생성한 클래스인 만큼 'NumbersBaseball → NumbersBaseballApp' 으로 클래스명을 수정하였다.

수정된 Main.main() 메서드

 

먼저 'Main.main()' 메서드에서는 정말 application 객체만을 다루도록 수정하였다. 기존의 입력에 따른 수행흐름은 전혀 없으며 application 에 해당하는 'NumbersBaseballApp' 객체를 생성, 'NumbersBaseballApp.start(Scanner)' 를 호출한다. 반환 값(boolean)에 따라 'start()' 메서드를 재호출 하거나 실행을 종료하는 로직만을 가지게 되었다.

 

'Main.main()' 메서드를 수정함에 따라 'NumbersBaseballApp.start()' 메서드도 아래와 같이 수정하였다.

수정된 NumbersBaseballApp.start() 메서드
분리한 NumbersBaseballApp.play() 메서드

 

가장 큰 변화는 'NumbersBaseballApp.start()' 메서드는 'boolean' 타입을 반환하는 메서드가 되었으며 기존의 로직을 분리하였다. 로직의 경우 '시작 메뉴' 를 '게임 시작' 과 '게임 재시작(이전 게임 클리어 후)' 시에만 사용자에게 노출하기 위해서 분리하게 되었는데 코드의 'depth' 가 너무 깊어지는 것도 있고 로직을 파악하기 어려워지는 것을 고려한 것도 있다.

 

게임이 시작된 후의 실행 흐름을 나타내는 로직을 분리하고자 'NumbersBaseballApp.play()' 메서드를 추가했으며 해당 메서드는 'NumbersBaseballApp' 클래스 내에서만 사용되기에 'private' 로 접근을 제한하였다. 결과적으로 'NumbersBaseballApp.start()' 메서드는 메뉴 선택에 따른 '실행 흐름(메뉴 노출 - 메뉴 선택 - 선택에 따른 수행)' 을 나타내는 로직을 가지게 되었다.

 

또한 '메뉴 번호' 를 사용자가 입력하면 해당 입력 값이 유효한지 검증을 수행하는 메서드를 'InputValidator' 클래스에 아래의 이미지와 같이 추가했다.

InputValidator.isValidMenuNum() 메서드

 

해당 메서드는 입력 값이 '유효한 메뉴 번호' 인지 검증한다. 정확히는 enum 클래스를 통해 상수에 지정한 필드(정규식)를 만족하는 입력인지 확인하는 'checkValidMenuNum()' 을 호출한다. 해당 메서드는 입력 값이 지정한 정규식을 만족하는지 확인 후 만족하지 못 한다면 'BadInputException' 을 발생시킨다. 여기서 발생된 예외를 'isValidMenuNum()' 에서 처리하면서 입력 값의 유효성에 대한 결과를 'boolean' 으로 반환하게 된다.

 

이렇게 해서 'Main' 클래스에서는 application 만을 호출하게 하였고 'NumbersBaseballApp' 에서 application 의 동작 흐름에 대한 모든 로직을 다루게 되었다.

 

 

2. enum 클래스

 프로젝트의 모든 클래스의 문자열을 좀 더 편하게 관리할 수 있게 enum 클래스가 존재했는데, 새로운 요구사항을 반영하면서 해당 부분을 신경쓰지 못해서 현재까지 작성한 코드 기준, 모든 클래스의 문자열을 enum 클래스에 상수 필드에 지정 또는 필요에 따라 enum 클래스를 추가 생성하였다.

SystemMessage(enum) 클래스

 

먼저 사용자에게 노출될 '시스템 메시지' 를 필드로 갖는 상수를 열거한 'SystemMessage(enum)' 클래스이다. 수행 흐름에 따라 노출되는 시스템 메시지를 관리하고자 생성하였고 요구사항에 따라 필요한 시스템 메시지를 필드로 갖는 상수들이 추가 되었다.

ValidCriteria(enum) 클래스

 

입력 값의 유효성을 검증할 때 사용되는 '기준' 을 필드로 갖는 상수를 열거한 'ValidCriteria(enum)' 클래스이다. 시작 메뉴에 대한 요구사항이 추가 됨에 따라 'MENU_NUM' 상수와 각 메뉴의 번호 문자열을 필드로 갖는 상수들이 추가 되었다. 'MENU_NUM' 의 경우 현재 '1,3' 번 메뉴에 대한 요구사항만이 반영되었기에 '2' 번 메뉴에 대한 접근을 막기 위해 위 이미지와 같이 정규식을 지정했다. '2' 번 메뉴에 대한 요구사항 반영시 "[1-3]" 으로 정규식을 수정할 것이다.

ExceptionMessage(enum) 클래스

 

예외 발생시 사용될 '예외 메시지' 를 필드로 갖는 상수를 열거한 'ExceptionMessage(enum)' 클래스이다. 예외 객체 생성시 사용할 예외 메시지들을 관리하기 위해 추가 생성하였다.

 

 

3. Exception

 기존에 사용자가 정답을 맞추기 위해 'BadInputException' 을 구현하였는데, '시작 메뉴' 요구사항이 반영됨에 따라 '메뉴 번호' 의 입력에 대한 유효성 검증이 필요하게 되었다. 유효성 검증 메서드를 추가 구현하면서 해당 메서드에서도 예외를 발생시키는 방식을 활용했는데, 추가 'CustomException' 을 생성할까 고민이 되었다. 결과적으로 아래와 같이 'BadInputException' 의 생성자를 수정해 예외 객체 생성시 '예외 메시지' 를 주입받도록 수정하였다.

수정된 BadInputException 클래스

 

이런 방식을 택한 이유는 단순히 귀찮아서가 아니다. 따지고 보면 '정답을 맞추기 위한 입력', '메뉴 선택을 위한 입력' 이든 예외가 발생하면 '잘못된 입력' 이 그 원인이다. "원인이 같은데 검증 대상이 다르다는 이유로 분리하는 것이 맞을까?" 라는 생각이 들어 결국 위 이미지와 같이 수정하게 된 것이다. 물론 원인이 명확하게 다른 경우 예외 클래스를 추가 생성할 것이다.

 

 

4. 마무리

 이렇게 해서 'Level01-02' 요구사항을 모두 반영한 'NumbersBaseball' 프로젝트는 application 수행 로직은 'NumbersBaseballApp' 에서 다루고, 사용되는 모든 문자열은 enum 클래스를 통해 관리하며, 예외 객체는 '예외 메시지' 를 주입해 생성할 수 있게 되었다.