사용자
가 로또
를 구매하고 싶은 만큼 금액
을 입력당첨번호와 보너스번호
를 입력 받는다. (잘못 입력 시 예외)IllegalArgumentException
를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료사실 우테코 프리코스 기능 요구사항은 어떤 코드를 짜기 위한 최소한의 목표에 가까운 것 같고 주로 해결할 문제는 프로그래밍 요구사항에 있는 것 같다.
뷰
와 비즈니스 로직을 담당하는 모델
의 관심사를 분리 시키고핸들러
↔ 서비스
↔ 레파지토리(DAO)
를 통하여 DB에 접근하며 필요한 로직을 처리프론트 컨트롤러
에서 적절한 핸들러
를 호출해서 결과를 적절한 뷰
로 데이터를 넘겨준다.모델
영역은 비교적 명확했다.뷰
를 어떻게 구현해야되는지 헷갈렸다.컨트롤러
가 어떤 형태로 존재해야되는지가 제일 와닿지가 않았다.입력
→ 모델의 로직 처리
→ View 호출
단위로 메소드를 나눴다.├── controller
│ └── LottoHandler
├── model
│ ├── GameMoney.java
│ ├── Judgement.java
│ ├── Lotto.java
│ ├── Rank.java
│ ├── RankCounter.java
│ ├── User.java
│ └── WinningNumbers.java
├── utils
│ ├── Errors.java
│ ├── Input.java
│ ├── LottoGenerator.java
│ └── Rules.java
├── view
│ └── Print.java
└── Application.java
클래스 | 내용 |
Lotto | 로또 번호를 가지고 있는 클래스 |
WinningNumbers | 당첨번호(Lotto)와 보너스 번호를 가지고 있는 클래스 |
GameMoney | 사용자가 투입할 금액을 가지고 있는 클래스 |
Rank | 로또 순위에 대한 정보를 갖고 있는 enum |
RankCounter | 순위에 대한 집계를 하는 클래스 |
User | 로또 게임을 진행하는 사용자 정보(GameMoney, List< Lotto>)를 가지고 있는 클래스 |
Judgement | 사용자(User)와 당첨번호(WinningNumbers)를 가지고 결과를 판단하는 클래스 |
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) throws IllegalArgumentException {
validateLength(numbers);
validatedLottoRange(numbers);
validateDuplicate(numbers);
this.numbers = numbers;
}
private void validateLength(List<Integer> numbers) throws IllegalArgumentException {
if (numbers.size() != Rules.LOTTO_SIZE) {
throw new IllegalArgumentException(Errors.ERROR_LOTTO_NUMBER_SIZE.getValue());
}
}
private void validatedLottoRange(List<Integer> numbers) throws IllegalArgumentException {
for (Integer number : numbers) {
validateNumberRange(number);
}
}
private void validateNumberRange(int number) throws IllegalArgumentException {
if ((number < Rules.LOTTO_MIN_NUMBER) || (number > Rules.LOTTO_MAX_NUMBER)) {
throw new IllegalArgumentException(Errors.ERROR_LOTTO_NUMBER_RANGE.getValue());
}
}
private void validateDuplicate(List<Integer> numbers) throws IllegalArgumentException {
HashSet<Integer> checkDuplicate = new HashSet<>(numbers);
if (checkDuplicate.size() != Rules.LOTTO_SIZE) {
throw new IllegalArgumentException(Errors.ERROR_LOTTO_NUMBER_DUPLICATE.getValue());
}
}
public int findLottoNumber(int index) {
return numbers.get(index);
}
public boolean containNumber(int number) {
return numbers.contains(number);
}
}
MVC패턴에서 View와 Model은 독립적이어야 한다.
View를 바꾼다고, Model의 로직이 바뀌면 안된다.
그 반대도..
로또 객체
를 가르키고 있는 참조변수
가 필요한 것이 아니라, 그 객체가 갖고있는 실질적인 필드 값
이 필요한 것이다.어떤 번호가 해당 로또 객체에 존재하는지 확인하는 메소드
를 구현해놓으면 된다.public boolean containNumber(int number) {
return numbers.contains(number);
}
public class Print {
// ...
private static String MESSAGE_LOTTO_COUNT = "개를 구매했습니다.";
private static String FORM_LIST_START = "[";
private static String FROM_LIST_END = "]";
private static String FORM_LIST_MIDDLE = ", ";
public static void printLottoList(List<Lotto> lottos) {
System.out.println(lottos.size() + MESSAGE_LOTTO_COUNT);
for (int lottoIndex = 0; lottoIndex < lottos.size(); lottoIndex++) {
printLotto(lottos.get(lottoIndex));
}
}
public static void printLotto(Lotto lotto) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(FORM_LIST_START);
stringBuilder.append(lotto.findLottoNumber(0));
for (int numberIndex = 1; numberIndex < Rules.LOTTO_SIZE; numberIndex++) {
stringBuilder.append(FORM_LIST_MIDDLE);
stringBuilder.append(lotto.findLottoNumber(numberIndex));
}
stringBuilder.append(FROM_LIST_END);
System.out.println(stringBuilder.toString());
}
// ...
}
List<Lotto>
반환 값을 받아 그를 출력하는 과정을 짜준 것이다.public class Lotto {
private final List<Integer> numbers;
// ...
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(numbers.get(0));
for (int lottoIndex = 1; lottoIndex < numbers.size(); lottoIndex++) {
sb.append(", ");
sb.append(numbers.get(lottoIndex));
}
sb.append("]");
return sb.toString();
}
}
Lotto
와 RankCounter
클래스에 위와 같이 toString()을 재정의하였는데, 모델에서는 데이터만 반환해주고 그를 뷰에서 형식에 맞게 출력해줘야한다.
public class LottoHandler {
private final User user;
private final WinningNumbers winningNumbers;
private final Judgment judgment;
public LottoHandler() throws IllegalArgumentException {
this.user = new User();
this.winningNumbers = new WinningNumbers();
this.judgment = new Judgment(user, winningNumbers);
}
public void sellLotto() throws IllegalArgumentException {
Print.printInsertMoney();
long money = Input.readLong();
user.payMoney(money);
user.buyLotto();
Print.printLottoList(user.getUserLottos());
}
public void pickWinningNumber() throws IllegalArgumentException {
Print.printWinningNumber();
List<Integer> newWinningNumbers = Input.readListInteger(Rules.SEPARATOR_WINNING_NUMBERS);
winningNumbers.newWinningNumbers(newWinningNumbers);
}
public void pickBonusNumber() throws IllegalArgumentException {
Print.printBonusNumber();
int newBonusNumber = Input.readInteger();
winningNumbers.newBonusNumber(newBonusNumber);
}
public void calculateResult() throws IllegalArgumentException {
RankCounter rankCounter = judgment.calculateRank();
Print.printRankCounter(rankCounter);
double yield = judgment.calculateYield();
Print.printYield(yield);
}
}
입력
→ 모델 로직 호출
→ 값을 받아 뷰 호출
하는 과정을 하나로 메소드로 묶었다.
테스트하려는 메소드
가 어떤input
에 의해 어떤output
이 나오는지 더 명확하여 테스트 코드를 짜는데 훨씬 용이했다.
@Test
void 예외_테스트() {
assertSimpleTest(() -> {
runException("1000j");
assertThat(output()).contains(ERROR_MESSAGE);
});
}
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.
throw new IllegalArgumentException("[ERROR] 에러메세지");
로 처리를 하였다.첫 줄
은 runException("1000j");
은 타고 들어가보면 다음과 같다. protected final void runException(final String... args) {
try {
run(args);
} catch (final NoSuchElementException ignore) {
}
}
protected final void run(final String... args) {
command(args);
runMain();
}
private void command(final String... args) {
final byte[] buf = String.join("\n", args).getBytes();
System.setIn(new ByteArrayInputStream(buf));
}
protected abstract void runMain();
2번째 줄
인 assertThat(output()).contains(ERROR_MESSAGE);
은 output()
에 ERROR_MESSAGE = "[ERROR]";
이 포함되어 있는지 확인해보는 테스트다.output()
을 들어가보면 다음과 같다. private PrintStream standardOut;
private OutputStream captor;
@BeforeEach
protected final void init() {
standardOut = System.out;
captor = new ByteArrayOutputStream();
System.setOut(new PrintStream(captor));
}
protected final String output() {
return captor.toString().trim();
}
System.out.print("이 값을 가져오겠다는 뜻");
"[ERROR] 어쩌구 저쩌구"
를 지정해 뒀을 뿐이지 그것을 출력하지 않았다.사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.
try {
validate(...);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
프로그램 종료 시 System.exit()를 호출하지 않는다.
에 의해 catch문 안에서 해당 메소드를 호출하여 프로그램을 종료할 수 없다.throws
하여 메인 전체 내용을 감싸 그 중 한 군데에서 IllegalArgumentException이 발생하면, main메소드가 끝나도록 하였다.public class Application {
public static void main(String[] args) {
try {
LottoHandler lottoHandler = new LottoHandler();
lottoHandler.sellLotto();
lottoHandler.pickWinningNumber();
lottoHandler.pickBonusNumber();
lottoHandler.calculateResult();
} catch(Exception e){
// 이 부분은 뷰에 에러메세지를 출력하는 기능 구현
Print.printExceptionMessage(e);
}
}
}
몇 번의 웹 프로젝트르 진행하고, MVC 패턴에 대한 이해도가 있다라고 생각한 자체가 오만한 생각이었다. 지금 생각을 해보니 단순히 Spring MVC에 대한 사용이나, 서블릿 MVC가 어떻게 동작하는 지 정도를 이해한 것이지 MVC로서 관심사를 분리하는 기본적인 설계법에 대해서는 이해를 제대로하지 못한 상태였던것 같다.
그래서 이번 과제를 진행하면서 관심사가 명확하게 나눠지니 특정 메소드에서 처리해야되는 기능이 명확해졌고, 메소드가 단 하나의 일을 할 수 있도록 하기 쉬워졌다.
그렇다 보면, 나중에 특정 기능을 고치거나, 리팩토링할 때도 고쳐야되는 부분이 명확하게 보였고, 단위 테스트 코드를 짜는 것도 쉬워졌다.
객체 지향적으로 코딩을 하는 것이 얼마나 중요한지 느낄 수 있던 일주일이었다.
도움 많이 받고 갑니다!! 로또 클래스 내부에서 정렬해서 출력하는 toString() 메소드를 구현했는데, 생각해보니까 정말 view에서 해야하는 일을 model에서 하고 있었던 것 같네요 자세하게 작성해주신 회고록 덕분에 견문을 넓혀가는 것 같습니다 ! !!