[우아한테크코스 백엔드 4기]프리코스 3주차 "자판기" 회고

23958 단어 우테코우테코

미션 - 자판기

🚀 기능 요구사항

반환되는 동전이 최소한이 되는 자판기를 구현한다.

  • 자판기가 보유하고 있는 금액으로 동전을 무작위로 생성한다.
    • 투입 금액으로는 동전을 생성하지 않는다.
  • 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려준다.
  • 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.
  • 상품명, 가격, 수량을 입력하여 상품을 추가할 수 있다.
    • 상품 가격은 100원부터 시작하며, 10원으로 나누어떨어져야 한다.
  • 사용자가 투입한 금액으로 상품을 구매할 수 있다.
  • 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.
  • 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
    • 반환되지 않은 금액은 자판기에 남는다.
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다.
  • 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.

✍🏻 입출력 요구사항

⌨️ 입력

  • 상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어 세미콜론(;)으로 구분한다.
[콜라,1500,20];[사이다,1000,10]

🖥 출력

  • 자판기가 보유한 동전
500원 - 0개
100원 - 4개
50원 - 1개
10원 - 0개
  • 잔돈은 반환된 동전만 출력한다.
100원 - 4개
50원 - 1개
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 [ERROR]로 시작해야 한다.
[ERROR] 금액은 숫자여야 합니다.

🔗 관련 링크

3주차 프리코스 깃허브 링크
작성한 코드
프리코스 참고자료 노션 정리


구현 기능 목록

✅ 숫자 셋팅(예외처리) 기능

  • 숫자 문자열을 예외처리 해야한다.
    • [예외] 인풋이 없는 경우
    • [예외] 인풋에 숫자 외에 다른 것이 들어온 경우
    • [예외] 인풋의 첫 글자가 0인 경우

✅ 돈 셋팅(예외처리) 기능

  • 받은 돈 문자열을 숫자 셋팅(예외처리) 해야한다.
  • 숫자화 후 금액관련 예외처리를 해야한다.
    • [예외] 금액이 100 미만인 경우
    • [예외] 금액이 10으로 나누어 떨어지지 않는 경우

✅ 이름 셋팅(예외처리) 기능

  • 상품 이름을 예외처리 해야한다.
    • [예외] 이름이 비어있는 경우
    • [예외] 이름에 문자 외에 다른 것이 들어온 경우
    • [예외] 중복된 이름이 있는 경우

✅ 상품 이름&가격&재고 세팅(예외처리) 기능

  • 상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어 세미콜론(;)으로 구분한다.
  • 받은 상품들 문자열 포멧에 맞게 예외처리 해야한다.
    • [예외] 인풋이 없는 경우
    • [예외] 인풋에 문자,숫자,쉼표,세미콜론 외에 다른 것이 들어온 경우
    • [예외] 인풋에 대괄호 쌍이 없는 경우
    • [예외] 인풋의 대괄호 쌍들 사이에 세미콜론 이외의 것이 들어온 경우
    • [예외] 인풋의 대괄호 쌍 사이에 문자,쉼표,숫자 이외의 것이 들어온 경우
    • [예외] 인풋의 대괄호 쌍 사이에 2개의 쉼표가 없는 경우
  • 상품 이름을 이름 셋팅(예외처리) 기능으로 예외처리 한다.
  • 상품 가격을 돈 셋팅(예외처리) 기능으로 예외처리 한다.
  • 상품 개수를 숫자 셋팅(예외처리) 기능으로 예외처리 한다.

✅ 자판기 보유금액 입력 받기&저장

  • "자판기가 보유하고 있는 금액을 입력해 주세요."을 출력해야 한다.
  • 보유금액 입력을 받아야 한다.
  • 입력을 돈으로 셋팅(예외처리)한다.
  • 입력받은 문자열에서 자판기 보유 금액을 추출한다.
  • 자판기 보유 금액을 저장한다.

✅ 보유금액을 보유 동전 개수로 변환

  • 동전 무작위 생성
    • 보유금액에서 동전 개수를 하나 늘린다
    • 동전 금액만큼 보유금액에서 뺀다.

✅ 자판기 안 동전 개수 출력

  • "자판기가 보유한 동전"을 출력해야 한다.
  • "500원 - n개"을 출력해야 한다.
  • "100원 - n개"을 출력해야 한다.
  • "50원 - n개"을 출력해야 한다.
  • "10원 - n개"을 출력해야 한다.

✅ 상품 가격&재고 입력 받기&세팅

  • "상품명과 가격, 수량을 입력해 주세요."을 출력해야 한다
  • 상품 가격&재고 입력을 받아야 한다.
  • 상품 셋팅(예외처리) 기능으로 예외처리 한다.
  • 입력받은 문자열에서 개별 상품을 추출한다.
  • 개별 상품에서 이름,가격,수량을 추출한다.
  • 예외발생 시 에러메세지 출력해야 한다.
  • 예외발생 시 다시 입력을 받아야 한다.

✅ 투입금액 입력 받기&세팅

  • "투입 금액을 입력해 주세요."을 출력해야 한다
  • 투입금액 입력을 받아야 한다.
  • 입력을 돈으로 셋팅(예외처리)한다.
    • [예외] 투입금액이 상품최소금액보다 적은 경우
  • 입력받은 문자열에서 투입 금액을 추출한다.
  • 투입 금액을 저장한다.
  • 예외발생 시 에러메세지 출력해야 한다.
  • 예외발생 시 다시 투입금액 입력을 받아야 한다.

✅ 투입금액 출력

  • "투입 금액: n원"을 출력해야 한다.

✅ 자판기 반복 실행

  • 구매할 상품명 입력 받기&세팅 & 투입금액 출력을 반복해야 한다.

✅ 구매할 상품명 입력 받기&세팅

  • "구매할 상품명을 입력해 주세요."을 출력해야 한다.
  • 사용자 입력을 받아야 한다.
  • 상품 이름을 이름 셋팅(예외처리) 기능으로 예외처리 한다.
    • [예외] 인풋값의 상품명을 보유하고 있지 않은 경우
  • 입력받은 문자열을 상품명으로 저장한다.
  • 빈 줄을 출력해야 한다.
  • 예외발생 시 에러메세지 출력해야 한다.
  • 예외발생 시 다시 사용자 입력을 받아야 한다.

✅ 상품 구매

  • 다음 경우에 반복을 종료한다.
    • 상품금액이 투입금액보다 많은 경우
    • 해당 상품이 소진된 경우
  • 상품금액 만큼 자판기의 돈을 뺀다.
  • 상품 개수를 하나 줄인다.
  • 다음 경우에 반복을 종료한다.
    • 남은 금액이 상품의 최저 가격보다 적은 경우
    • 모든 상품이 소진된 경우

✅ 잔돈 출력

  • "투입 금액: n원"을 출력해야 한다.
  • 남은 금액으로 반환해야 할 동전종류&개수를 계산한다.
  • 동전 종류마다, 자판기 안 보유 동전으로 전부 반환이 가능한지 확인한다.
    • 반환이 가능할 시, 반환한다.
    • 반환이 불가능할 시, 줄 수 있는 가장 높은 동전종류&개수를 반환한다

🔍구현 로직

  • Main diagram

  • Utils diagram

  • View diagram


⛳ 후기

🗒 원칙

  1. jdk 버전 8, intellij IDEA로 세팅했습니다.
    • checkstyle 등을 이용해 자바 코드 컨벤션을 꾸준히 확인했습니다.
    • crlflf로 전환했습니다.
  2. 커밋 메세지 컨벤션을 지키려 노력했습니다.
    • "타입(스코프): 내용" 형식으로, 내용은 한글로 작성했습니다.
    • 스코프에는 바뀐 파일/클래스 명을 썼고, 바뀐 파일이 3개 이상일 경우 비워뒀습니다.
  3. README.md 속 사항을 구현할 때마다 커밋을 남겼습니다.
    • 개괄적인 구현 이후엔 리팩토링 마다 커밋을 남겼습니다.
  4. MVC 구조를 사용했습니다.
    • /model : 데이터 저장 및 처리
    • /view : 인풋/아웃풋 관리
    • /controller : 프로그램 실행 로직 클래스
    • /Utils : 프로그램 비즈니스 로직(유틸)
  5. 객체지향 생활 체조 원칙을 지키려 노력했습니다.
    • 메소드 당 기능 하나를 담당하도록 설계했습니다.
    • 메소드/변수명을 축약없이 설명적으로 적었습니다.
    • getter/setter 대신 상수 & 생성자를 쓰려 했습니다.
    • 필요한 곳에만 주석을 적었습니다.
    • 하드코딩 대신 상수 클래스를 사용했습니다.
    • else/switch 대신 if/continue를 사용했습니다.
    • 일급객체/일급 컬렉션을 적극적으로 사용했습니다.
  6. 코드를 압축적으로 짜려 했습니다.
    • for/while 대신 람다/stream을 사용했습니다.
    • for/while 대신 재귀를 사용했습니다.
    • 객체 검증(예외처리) 시 정규식을 사용했습니다.
    • indent = 1을 지키려 노력했습니다.
    • 메소드 길이가 10줄 넘지 않게 했습니다.
    • 클래스 길이가 50줄이 넘지 않게 했습니다.
    • 클래스 속 인스턴스 변수 개수를 2개 까지만 허용했습니다.(자판기 클래스 제외)
  7. SOLID 원칙을 지키려 노력했습니다.
    • 단일 책임 원칙(SRP)을 적용시켜, 기능 별로 클래스를 분리시켰습니다.
    • 상위 클래스와 하위 클래스 사이의 일관된 책임관계를 주려 했습니다.
    • 응집도/결합도를 고려했습니다.
  8. TDD 방법론을 적용했습니다.
    • 실패테스트 생성 후 테스트코드가 돌아가는 레거시 코드를 구현했습니다.
    • 테스트코드 성공 후 리팩토링을 진행했습니다.
  9. LinkedHashMap을 사용했습니다.

🖋 소감

1. TDD

이번 과제는 기존의 관계들보다 복잡한 로직과 많은 클래스를 요구했습니다.

그래서 (드디어!)
TDD를 적용해보기로 했습니다.

- 구현기능 목록 작성

클래스 단위보단 기능단위 별로 적으니 지난 2주차보다 빠르게 작성할 수 있었습니다.
(1시간 내외로 걸린 것 같습니다)
이후 나눠진 구현기능 별로 TDD를 적용했습니다.

- 테스트 코드

기존 AplicationTest 안에 작성된 코드를 분석한 후 테스트 코드에 적용했습니다.
추가적으로 Junit5와 assertJ API 역시 공부했습니다.

참고 링크 1 : Junit5
참고 링크 2 : assertJ

코드는 뷰의 인풋/아웃풋 값들을 기준으로 작성했습니다.

처음엔 구현한 클래스의 기능이 정상적으로 작동되는지 테스트하려 했지만,
이후 코드의 구현로직이 바뀔 때마다 테스트 코드를 같이 수정해야 하는
무한 지옥 루프에 빠질 수 있음을 알았습니다.

웬만하면 구현된 테스트 코드가 범용적으로 사용되었으면 했고,
이를 위해 뷰 단위의 테스트 코드를 작성했습니다.

물론, 객체 안의 값들에 대한 검증 코드를 짜기 어렵다는 것이 단점이었습니다.
이번 과제는 나눠진 객체들의 값이 예측가능한 형태였기 때문에 지금의 테스트 코드만으로도 잘 돌아갔습니다.

이후 더 복잡하고 굵직한 클래스들이 추가되는 시점에서는
클래스 인스턴스를 검증하는 테스트 코드를 짜며 TDD를 실시해야 할 것 같습니다.
API도 더 적극적으로 이용하면서!

- 리팩토링

리팩토링을 구현기능 별로 실시할지,
전체 기능의 레거시코드 구현 이후 실시할 지 역시 고민했습니다.
결론적으론 두 방법 모두 사용했습니다.

기능 단위 구현 때엔 간단한 부분들로 1차 리팩토링을 진행했습니다.
이후 모든 기능들을 TDD 과정으로 구현해낸 후엔 전체 코드를 보며 2차 리팩토링을 진행했습니다.

2. MVC 패턴

지난 1주차 때 MVC 패턴을 알게 된 이후 지금까지 계속 사용했습니다.

- Utils 패키지 생성

항상 고민이었던 것은 유틸적 성격이 강한 클래스들의 위치였습니다.

예를 들어, 인풋값을 Model 객체에 넣을 때 값을 검증(예외처리)하는 validator 클래스들을 어디에 넣어야 할까요?

  1. 객체를 이용하는 로직이니 Controller?
  2. 객체에 관련된 로직이니 Model(Domain)?
  3. 인풋 값을 검증하는 것이니 View???

하지만 Controller에 넣기엔 사이드 로직이었습니다. (심지어 Model 객체가 사용함)
Model에 넣기에는 객체 그 자체는 아니었습니다.
View에 넣기에는.. View에는 비즈니스 로직이 들어가면 안된다는 피드백이 떠올랐습니다.

결국 이런 사이드 비즈니스 로직들을 저장하는 패키지 Utils를 추가로 생성했습니다.

이후 해당 패키지에 과제에서 언급된 추가 API를 저장하는 Util 클래스, String 인풋을 올바른 형태로 전환해주는 Converter 클래스, Constants(상수) 클래스를 넣었습니다.

MVC 패턴이 깨진걸까?
막상 찾아보니 MVC 패턴이 꼭 3개의 패키지로 구성되어야 한다는 뜻은 아닌 것 같았습니다.
사실 이전 과제에도 Util, Constants 클래스를 만들고선 위치를 고민했습니다.
앞으로는 사이드 비즈니스 로직과 유틸들을 패키지로 따로 나눠 보관하면 좋을 것 같습니다.

패키지명은 좀 더 고민해봐야 할 것 같다.

서치해보며 Service 패키지로
사이드 비즈니스 로직만 따로 넣어두는 케이스를 찾았지만
해당 방법에 대한 확신이 없어서 이번 코드에 적용하지 않았습니다.

앞으로 MVC 모델에 대한 이해도가 높아지면
이런 의문들도 자연스레 풀릴 것이라고 생각합니다.

- 객체 저장 클래스 (VendingMachine)

이번 미션 제목이 "자판기"인 만큼, 자판기 객체를 만드는 건 필수적이었습니다.

그래서 root 컨트롤러인 VendingMachineController 안에
자판기 객체 VendingMachine을 넣었습니다.

이 자판기 클래스는 서브 클래스(보유한 동전들, 제품들, 유저가 넣은 돈) 객체들을 저장하는 것이 주된 기능이었습니다. 이 클래스를 MVC 중 어디에 넣을지도 고민했습니다.

결국 이 역시 Model 객체라는 생각에 Model 카테고리에 넣었습니다.

그런데 다른 사람들 코드를 보니 따로 config 패키지로 분할한 경우도 있었습니다.
그 편이 클래스 간 결합도를 낮추는 걸 수도 있겠다는 생각은 들었습니다.

하지만 VendingMachine 역시 하나의 큰 Model이고, Model의 최상위 클래스로 기능하는 상황인데 따로 패키지를 분할하는 것은 Model 클래스 간 응집도 역시 낮추는 결과를 가져오지 않을까 싶어 적용하지 않았습니다.

3. 일급 객체 생성

자판기 작동에 필요한 변수들을 모두 일급 컬렉션이나 일급 객체로 묶었습니다.

일급 컬렉션은 객체지향 생활체조에서 언급된 개념에 기초해 wrapping 했고,
primitive 타입 변수들(money, coin 등) 역시 일급 객체로 묶었습니다.
객체들 안에 해당 객체의 상태와 행위를 저장했습니다.

또한 View에 보낼 때 toString() 메소드로 바로 프린트 될 수 있게 했습니다.

- 변수의 final화

모델들끼리 수직적 연결관계가 강했습니다.
그래서 컨트롤러에서 명령을 보낼 때 자판기 객체 속 서브 객체를 꺼내야 할 때가 많았습니다.
이때 getter를 사용하며 .을 2개 이상 찍게되는 경우가 많았습니다.

예를 들어, 자판기 속 특정 코인 개수를 찾을 때

VendingMachine.getCoinGroup().getCoin(이름)

과 같은 방식을 쓰게 되었습니다.

캡슐화를 위반하는 것 같아 마음이 불편해 방법을 찾아봤습니다.
생각해보니 애초에 일급 컬렉션을 만드는 것이 객체의 불변성을 보장하기 위한 것이었습니다.

따라서 불변성이 보장된 객체는 상수화하고 생성자를 통해서만 설정하면,
public으로 사용할 수 있습니다.

VendingMachine.coins.getCoin(이름)

와 같은 방식으로...

이 경우에도 .을 두번 쓰는 것은 똑같지만, 함수가 아닌 내부 상수로의 접근이니 괜찮지 않을까요? 이러한 방식이 맞는지는 이후 공부해나가며 알게될 것 같습니다.

4. enum

이번 과제에선 Coin 클래스를 통해 enum의 사용을 강제했습니다.
지금까지 enum을 사용한 적이 많이 없었거니와, 사용하더라도 따로 클래스를 만드는 방식이었던 적이 없었습니다.
일단 해당 Coin 클래스부터 이해가 되지 않았습니다.

enum 클래스를 구글링해 공부했습니다.

참고 링크 : tcpschool

막상 찾아보니 겁먹을 만큼 어렵지 않았습니다.
일반 객체와 큰 차이는 없고, 단지 열거체를 정의하고 상수값을 추가하면 됐습니다.

이후 Coin 이외의 Model 클래스에서도 enum을 적용할 수 있을까 고민했는데, 딱히 다양한 열거체를 정의할 필요성을 못 느꼈습니다.

5. 검증(예외처리)

이번엔 받는 인풋 값이 많았습니다.
그만큼 검증과정도 복잡했고 예외처리해야 할 요소들도 많았습니다.

- 정규식

지난 주에 공부했던 정규식을 사용했다. 정규식 테스트 사이트의 도움을 많이 받았습니다.
참고 링크 : regexr.com

이번엔 사용해야 할 패턴이 지난 번보다 훨씬 복잡했습니다.
예를 들어 상품 명을 입력받을 때

  1. ;로 상품 나눔
  2. 대괄호를 제거
  3. 쉼표로 상품 내용들을 나눔

의 각 요소들의 검증이 이뤄져야 했습니다.

- 검증 위치

처음에는 위 요소들을 한개의 정규식 패턴으로 해결하려 했다.

	public static final Pattern PRODUCT_PATTERN = Pattern.compile(
		"^" + "(" + DELIMITER_PRODUCT_BOXER[0] + PRODUCT_REGEX + DELIMITER_PRODUCT_BOXER[1] + ")"
			+ "(" + DELIMITER_PRODUCTS
			+ DELIMITER_PRODUCT_BOXER[0] + PRODUCT_REGEX + DELIMITER_PRODUCT_BOXER[1] + ")*" + "$");

(아이고 복잡해)

이것도 나름 코드 중복을 줄이려고 세부 정규식들을 스트링 상수로 선언한 것이었습니다.
인풋을 받을 때 바로 모든 요소를 검증하는 방식이었습니다.
비교적 간편했습니다.

하지만 이후 Model 속에 값이 들어갈 때 검증이 이뤄지지 않았습니다.

이것이 객체를 불안정하게 만드는 것 같습니다.
지금으로썬 코드가 잘 돌아가지만, 이후 다른 방식으로 객체값이 들어간다면 검증할 방법이 없습니다.

또한 에러 메세지를 세분화해 보낼 수도 없었습니다.

사용자가 값을 입력했을 때, 단순히 값이 잘못되었다는 메세지만 출력해야 했습니다.
사용자는 양식이 잘못된건지, 상품 내용이 잘못된건지 등 구체적인 오류 메세지를 알 수 없었습니다.
저 역시 테스트코드를 짤 때 불편했습니다.

보다 범용적인 프로그램을 위해, 검증 위치객체에 값을 넣을 때의 시점으로 옮겼습니다.

상품리스트 인풋을 받을 땐, ;'][ 의 양식이 맞는지만 확인했고,
이후 상품의 이름, 가격,개수를 넣을 땐
Beverage 클래스 안에서 Validator 객체를 호출했습니다.

이를 통해 예외처리를 보다 엄밀하게 진행할 수 있었습니다.

7. 상수

이번에도 (당연히) 상수들을 상수 클래스에 저장했습니다.
그런데 모든 하드코딩 된 값들을 상수클래스로 이동했더니 (당연히) 상수클래스의 길이가 길어졌습니다.

이에 문제에 정의되어 불변할 뷰 관련 상수들은 사용처 클래스에 정의해줬습니다.

옮기고 나니 나머지 상수들도 사용되는 클래스로 이동시켜야 할지 고민이 됐습니다.

실제로 다른 분들의 코드를 보면 클래스 안에 상수 선언을 해주는 경우가 많았습니다.
모두 한 클래스에 모아두는 것과 각자 클래스로 상수를 이동시키는 것은 각각의 장점이 있습니다.

상수 클래스를 선언 : 모든 상태값을 한눈에 확인, 쉽게 수정
각각의 클래수 내부 상수로 선언 : 응집도 증가?

전 일단 상수 클래스에 대부분의 상수 값들을 넣었습니다.
그래야 클래스 길이가 50줄을 넘지 않기로 한 나 스스로의 조건을 지킬 수 있어서 였습니다.
(물론 위의 장점 때문이기도 합니다.)

8. 재귀

지난 주차에 사용했던 재귀 방식을 보다 적극적으로 사용했습니다.

while이 들어갈 수 있는 모든 곳을 재귀로 교체했습니다.

이번 과제 역시 반복 횟수 자체는 많지 않고(1000 이하) 함수 내부 루프로 인한 위험을 줄이고 싶었습니다.
누구나 그렇듯 while문에 대한 안좋은 기억이 있기도 합니다...

재귀 구현 시 빌드 및 실행 속도가 더 느려지는 것은 생각해봐야 할 문제입니다.
Python 같은 경우에는 재귀를 통한 루프 수에 내부적인 제약을 걸어놓은 것으로 알고 있습니다.
자바 역시 그런 제약들이 있는지는 아직 찾아보지 못했는데, 앞으로 계속 고민해볼 생각입니다.

9. LinkedHashMap

이번 과제에선 동전의 종류별 개수를 판단하기 위해 HashMap을 사용할 필요성을 느꼈습니다.
그런데 HashMap은 각각 동전 종류의 순서가 보장되지 않습니다.
따라서 출력 시 500-100-50-10의 순서가 지켜지지 않습니다.
대안을 구글링을 통해 찾아봤습니다.

LinkedHashMap을 사용하면 처음 넣었던 값 순서가 유지된 HashMap을 만들 수 있음을 알았습니다.

HashMap보다 검색 속도는 느립니다.
하지만 어차피 들어가는 값 개수가 4개뿐이었어서 상관 없었습니다.
앞으로도 내장된 모듈들 중 원하는 기능이 있는지 찾아보는게 좋다는 교훈을 얻었습니다.

10. pull request 충돌 해결

지난 주차엔 풀리퀘스트 시 수정된 ApplicationTest로 인한 conflict를
미처 알지 못한채 제출했습니다.
관련 문의 결과 다행히 사정을 감안한다는 얘기를 들었습니다.

이번에도 테스트 코드 수정 메일이 왔는데,
conflict를 해결할 방법을 고민해보라는 이야기가 있었습니다.

열심히 구글링해본 결과
(당연히도) 바뀐 우테코 main 브렌치 코드가 로컬에 적용되지 않아 생긴 문제였습니다.

아래는 해결방법 입니다.

1.개인 main repository 브렌치에서 Fetch Upstream을 클릭
-> woowacourse의 변경사항을 개인 repository의 main 브랜치에 반영
2.local의 main 브렌치에 반영

git checkout main
git pull

3.프로젝트(BETTERFUTURE4) 브렌치와 main 브랜치를 merge

git checkout BETTERFUTURE4
git merge main

4.merge 결과를 push

git push origin BETTERFUTURE4

😓 아쉬운 점

1. 비즈니스 로직과 UI로직의 분리

2주차 피드백 때 이러한 내용이 있었습니다.

비즈니스 로직과 UI 로직을 분리해라

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다.
단일 책임의 원칙에도 위배된다.

public class Car {
  private int position;
    // 자동차 이동 여부를 결정하는 비즈니스 로직
    public void move(int randomValue) {
    }
    // UI 로직
    private void print(int position) {
      StringBuilder sb = new StringBuilder();
      ...
    }
}

처음엔 단순히 MVC 모델을 적용하라는 이야기로 들었습니다.
View가 UI로직, Controller-Model(추가로 Utils)이 비즈니스 로직이라고 생각했습니다.
그래서 컨트롤러 패키지로 View 로직을 생성하는 방식으로 분리가 완료된다고 생각했습니다.

그런데 코드를 다 짠 이후 예시코드 속 StringBuilder가 눈에 밟혔습니다.
찾아보니 String에 값을 추가할 때 처리속도를 빠르게 할 수 있는 String의 아종 객체였습니다.

그렇다면 print에서 String이 추가되는 방식을 고려하면 좋다는 힌트인가?
또한 그렇다면, UI 로직에서 필요한 모든 값들을 String에서 입력받고, 한꺼번에 출력해야 분리가 제대로 이뤄진 것은 아닐까?

그래서 OutputView에서 아예 모든 controller로 받은 값들을 프린트하는 방식의 구현을 시도해봤었습니다.

그러나...

실행 시 터미널에서 실시간으로 인풋이 보이고 값 또한 보여야 하는데
그리고 변화한 StringBuilder 값이 실시간으로 터미널에 적용되어야 하는데
그 방법을 도저히 몰랐습니다.

해당 영역은 이번주 안 해결 불가라는 판단에 빠르게 포기했습니다.
계속 방법을 찾아봐야 할 것 같습니다.

2. 함수를 인자로 받는 함수?

프로그램의 input 영역을 예외처리하는 클래스 InputController에서
자세히 보면 코드의 반복이 눈에 띕니다.

public static Money getMachineMoney() {
		try {
			return new Money(InputView.machineMoneyInput());
		} catch (IllegalArgumentException e) {
			OutputView.printError(e.getMessage());
			return getMachineMoney();
		}
	}

	public static Money getUserMoney(int minPrice) {
		try {
			return new Money(InputView.userMoneyInput(), minPrice);
		} catch (IllegalArgumentException e) {
			OutputView.printError(e.getMessage());
			return getUserMoney(minPrice);
		}
	}
    ...

안의 모든 메소드들은
예외가 있을 시 해당 메소드를 재귀로 반복하는 형태입니다.

만약 메소드의 인자로 메소드가 들어오고
이를 try문 안에 실행한다면 반복을 줄일 수 있을 것 같았습니다.

처음에는 enum 클래스를 이용해 반복을 줄이려 했습니다.
열거체 이름을 정하고, 해당 값에 함수를 추가하는 방식이었습니다.

그런데 테스트 코드를 돌리면서 예외처리가 제대로 안되고 있다는 것을 알았습니다.

Function 객체를 활용했는데,
인자 속의 예외가 InputControllertry 문까지 닿지 않는 것 같았습니다.
(그냥 제가 코드를 잘못 짠 걸 수도 있습니다..)

구글링 결과 각각의 고유의 영역을 클래스 객체로 싸서 인자로 주면 된다는 이야기를 들었습니다.
하지만 그만큼 클래스 수가 늘어나는 것은 부담스러웠습니다.
몇 줄 반복 줄이려다 배보다 배꼽이 더 커지는 격이었습니다.

결국 위의 반복은 줄이지 못했습니다.
앞으로 보다 복잡하거나 반복이 많은 경우에는 클래스로 분리하는 방법을 생각해봐야 할 것 같습니다.

3. 작명을 돕는 사이트

변수 작명 사이트

해당 사이트는 클래스/변수/함수 영어 작명을 추천해줍니다.
이 사이트를 알게 된 것이 바로 어제였네요.

사이트를 통해 모든 요소들을 다시 작명할까도 생각했습니다.
리팩토링 이후 예상치 못한 오류가 생길 수도 있고
인자로 받는 변수명을 일일히 변경하기 귀찮아 냅뒀습니다.

이후 코딩 시 작명이 고민될 땐 해당 사이트의 도움을 받으면 어떨까 싶습니다.

4. 상수 코딩 컨벤션

이건 이번 주차의 아쉬운 점은 아니지만 말하자면

상수는 모두 대문자와 밑줄로 쓴다고 생각했는데
자꾸 checkstyle이 오류를 뿜어냈습니다.

이번에서야 이유를 알았습니다...

컨벤션에서 말한 constant_uppercase 적용은 static final에만 적용되었습니다...
인스턴스 final에는 적용되지 않았습니다.

이번 과제부터는 해당 코딩 컨벤션을 지켰습니다!


📚 TODO

  • 테스트 코드 작성 연습(Junit5, assertJ)
  • TDD 기능단위별 적용 연습
  • MVC 모델 심화 탐구, 적용 연습
  • 인자에 함수를 넣는 방법 연구/연습
  • 자바에서 자주 쓰이는 유틸 함수 정리(된다면 포스팅도!)
  • 최종 코딩테스트 준비!

좋은 웹페이지 즐겨찾기