본문 바로가기

etc/우테코 6기 프리코스

3주차 프리코스 미션(로또)

기능 요구 사항 & 프로그램 요구 사항 & 과제 진행 요구 사항


GitHub : https://github.com/JunTaeINC/java-lotto-6/tree/JunTaeINC

 

GitHub - JunTaeINC/java-lotto-6: 우아한 테크코스 6기 프리코스 3주차

우아한 테크코스 6기 프리코스 3주차. Contribute to JunTaeINC/java-lotto-6 development by creating an account on GitHub.

github.com

 

 

 

문제 해결


1. 무분별한 Getter 사용 지양

Setter의 사용은 외부에서 의도와 다르게 값이 수정되는것을 방지하고, 변경되지 않는 인스턴스에 대해서 접근이 가능해져 객체의 일관성, 안정성을 보장받기 힘들기 떄문에 지양해야한다. Getter를 사용해서 필드의 값을 가져왔었다. 하지만 다른 사람들의 코드리뷰를 보면서 Getter의 사용의 지양하라는 말을 봤다. 그래서 Getter를 왜 지양을 해야하는지 알아봤다.

 

1.1 캡슐화 위반 (은닉화 위배)

OOP에서 객체의 상태를 직접 조작하는 대신 객체에 메시지를 보내서 원하는 결과를 얻는 것을 지향한다.

캡슐화를 위해 private로 선언해도 Getter로 내부의 상태를 외부로 노출시키므로 캡슐화가 깨진다.

public class Student {

    private final String name;
    private final int age;
    
    public Student(String name, int age){
    	this.name = name;
        this.age = age;
    }
    
    public String getName(){
    	return this.name;
    }
    
    public int getAge() {
    	return this.age;
    }
}

 

1.2 강한 결합도

객체가 다른 객체의 상태에 직접 접근하거나 의존하게  되면, 두 객체는 강하게 결합된다.
이럴경우 한 객체의 상태가 변경되었을때 다른 객체도 영향을 받게 되어 코드의 유연성, 재사용성이 떨어진다.

 

1.3 Getter 사용 지양에 대한 해법

위에서 살펴봤듯이 객체의 값을 가져오는 것이 아닌 객체에게 메시지를 줘서 원하는 값을 가져오는 방식으로 처리한다.

boolean 성이 김씨니? = 학생.이름가져오기().성이뭐야?(김) -> 잘못됨
boolean 김씨로시작하니? = 학생.성이뭐야?(김) -> 이런식으로 구현한다.
// Getter 사용
public boolean isBonusMatch(Lotto lotto) {
	return lotto.getNumbers().contains(bonusNumber.getBonusNumber());
}
// 객체에 메시지 전달
public class Lotto {

	private final List<Integer> numbers;

	public Lotto(List<Integer> numbers) {
		validate(numbers);
		this.numbers = numbers;
	}
    
    	public boolean contains(Integer number) {
		return numbers.contains(number);
	}
}

// 메서드 활용
public boolean isBonusMatch(Lotto lotto) {
	return lotto.contains(bonusNumber.getBonusNumber());
}

 

2. Enum 적극 활용

Enum은 상수로 이루어진 데이터들의 집합이라고 생각하면 된다.

이번주차에서는 오류 메시지, 게임안내 메시지, 로또의 기본설정, 로또 랭킹에서 Enum을 활용했다.

 

2.1 Enum의 장점

2.1.1 유지보수성

Enum을 사용해 관련된 상수들을 보관 하므로 만약 상수의 값을 변경해야할 경우, Enum에서 정의한 값만 바꿔주면 된다.

 

2.1.2 싱글턴 패턴

Enum 값은 해당 클래스의 단일 인스턴스를 나타내므로, 이를 이용하면 쉽게 싱글턴 패턴을 구현할 수 있다.

 

2.2 Enum의 메서드

public enum Color {
    RED, BLUE, GREEN
}
메서드 반환 타입 출력 예시
name() String Color.Red.name() -> "RED"
ordinal() int Color.Blue.orinal() -> "1"
valueOf(String name) enum Color.valueOf("GREEN") -> GREEN
values() enum[] Color.values() -> [RED, BLUE, GREEN]

 

2.3 Enum 직접 사용예시

로또 순위에 따른 상수를 관리하는 Enum이다.

번호의 일치수, 상금, 출력메시지를 관리한다. 또한 랭킹순위를 반환하는 메서드도 구현을 했다.

package lotto.config;

import java.util.EnumSet;

public enum LottoRank {
	FIFTH(3, 5_000, "3개 일치 (5,000원) - %d개"),
	FOURTH(4, 50_000, "4개 일치 (50,000원) - %d개"),
	THIRD(5, 1_500_000, "5개 일치 (1,500,000원) - %d개"),
	SECOND(5, 30_000_000, "5개 일치, 보너스 볼 일치 (30,000,000원) - %d개"),
	FIRST(6, 2_000_000_000, "6개 일치 (2,000,000,000원) - %d개");

	private final int countOfMatch;
	private final long winningMoney;
	private final String winningMessage;

	LottoRank(int countOfMatch, int winningMoney, String winningMessage) {
		this.countOfMatch = countOfMatch;
		this.winningMoney = winningMoney;
		this.winningMessage = winningMessage;
	}

	public long getWinningMoney() {
		return winningMoney;
	}

	public String getWinningMessage() {
		return winningMessage;
	}

	public static LottoRank findRank(int matchCount, boolean matchBonusNumber) {
		if (matchCount == 5) {
			if (matchBonusNumber) {
				return SECOND;
			}

			return THIRD;
		}

		return EnumSet.allOf(LottoRank.class).stream()
			.filter(rank -> rank.countOfMatch == matchCount)
			.findFirst()
			.orElse(null);
	}
}

 

 

 

3.  try, catch 문을 활용한 재입력  리펙토링

우선 기능이 정상적으로 작동을 하기위해서 재귀를 활용해 예외발생시 에러메시지를 출력하고 메서드를 호출했다.

하지만 이렇게 구현을 하다보니 각 메서드에서 중복이 발생되었다. 그래서 try, catch문의 중복을 제거하기 위해 고민했다.

객체를 생성할때 예외처리를 해줬다. 그래서 지금 중복되는 부분이 try, catch, 객체생성 이렇게 있다.

private void purchaseLotto() {
	try {
		OutputView.askPurchaseAmount();

		String money = InputView.getUserInput();

		PurchaseAmount purchaseAmount = new PurchaseAmount(money);

		lottoPlayer = lottoMachine.purchaseLotto(purchaseAmount);

		OutputView.printPlayerLottos(lottoPlayer);
	} catch (IllegalArgumentException e) {
		System.out.println(e.getMessage());
		purchaseLotto();
	}
}

private void createWinningLotto() {
	try {
		WinningNumbers winningNumbers = createWinningNumbers();
		BonusNumber bonusNumber = createBonusNumber();

		winningLotto = new WinningLotto(winningNumbers, bonusNumber);
	} catch (IllegalArgumentException e) {
		OutputView.printExceptionMessage(e.getMessage());
		createWinningLotto();
	}
}

private BonusNumber createBonusNumber() {
	try {
		OutputView.askBonusNumber();
		String number = InputView.getUserInput();

		return new BonusNumber(number);
	} catch (IllegalArgumentException e) {
		OutputView.printExceptionMessage(e.getMessage());
		return createBonusNumber();
	}
}
private WinningNumbers createWinningNumbers() {
	try {
		OutputView.askWinningNumbers();

		String numbers = InputView.getUserInput();

		return new WinningNumbers(numbers);
	} catch (IllegalArgumentException e) {
		OutputView.printExceptionMessage(e.getMessage());
		return createWinningNumbers();
	}
}

 

3.1 자바 함수형 인터페이스 Function 도입

String input을 입력받아 input -> 생성자로 생성이 가능하면 return input;

만약 정의한 예외 상황에 예외가 발생하면 다시 input을 입력받는다.

생성자는 각기 다르기 때문에 <T> 제네릭 타입으로 정의했다.

이로써 기본 코드의 중복을 제거할 수 있었다.

// constructor는 String 타입의 입력을 받아 T 타입의 결과를 반환하는 메서드
private <T> String getValidInput(Function<String, T> constructor) {
	String input;
	do {
		input = InputView.getUserInput();
		try {
			constructor.apply(input);
		} catch (IllegalArgumentException e) {
			OutputView.printExceptionMessage(e.getMessage());
			input = null;
		}
	} while (input == null);

	return input;
}

 

private WinningNumbers createWinningNumbers() {
	OutputView.askWinningNumbers();

	String numbers = getValidInput(WinningNumbers::new);

	return new WinningNumbers(numbers);
}

 

4. 객체 생성시 검증 해결

기존 WinningLotto 객체를 생성할때 당첨번호와 보너스 번호의 중복이 있는지 검증하는 식으로 설계를 했다.

하지만 만약 예외가 발생할 경우 당첨번호부터 다시 재입력 받는 오류가 발생했다. 내가 원하는건 보너스 번호만 다시 재입력 받는 것이다.

private void createWinningLotto() {
	WinningNumbers winningNumbers = createWinningNumbers();
	BonusNumber bonusNumber = createBonusNumber();

	winningLotto = new WinningLotto(winningNumbers, bonusNumber);
}
private BonusNumber createBonusNumber() {
	OutputView.askBonusNumber();

	String number = getValidInput(BonusNumber::new);

	return new BonusNumber(number);
}

4.1 리펙토링

기존 WinningLotto에서 진행되던 검증을 WinningNumbers로 옮겼다.

checkDuplicationBonusNumber() 메서드를 구현 함으로써 이 문제를 해결했다.

보너스 번호객체를 생성할 경우 검증을 통해 true, false 값에 따라 재입력 받도록 구현했다.

그럼 보너스 번호는 객체가 생성되는 검증, 당첨번호와 중복이 있는지 검증을 통과하면 그제서야 객체를 생성한다.

private BonusNumber createBonusNumber(WinningNumbers winningNumbers) {
	BonusNumber bonusNumber;

	do {
		OutputView.askBonusNumber();
		String number = getValidInput(BonusNumber::new);
		bonusNumber = new BonusNumber(number);
	} while (!isValidBonusNumber(winningNumbers, bonusNumber));

	return bonusNumber;
}

private boolean isValidBonusNumber(WinningNumbers winningNumbers, BonusNumber bonusNumber) {
	try {
		winningNumbers.checkDuplicationBonusNumber(bonusNumber);
		return true;
	} catch (IllegalArgumentException e) {
		OutputView.printExceptionMessage(e.getMessage());
		return false;
	}
}

 

 

미션 진행 후기


객체에 대해 더 깊이 이해할 수 있는 시간이 었다.객체의 정의를 명확히 파악하고, 객체 지향적인 설계를 위해 SOLID 원칙을 철저히 학습하는 시간을 가졌다. 기존에는 SOLID에 대한 지식이 부족했지만, 이번에 SOLID 원칙을 확실히 이해하고, 이를 토대로 설계하고 수정하면서, '객체'가 무엇인지, 그리고 '객체 지향적 설계'가 무엇인지를 더욱 명확히 이해하게 됐다.

 

또한, 자바에 대해 이미 충분히 알고 있다고 생각했지만, 자바의 다양한 인터페이스와 메서드를 보고 난 후에는 아직 배워야 할 것이 많다는 사실을 깨달았다. 이번에 예외가 발생했을 때 사용자로부터 재입력을 받는 기능을 구현하면서, 객체 생성 시 검증을 수행하도록 설계했다.

그래서 while문과 try-catch문으로 예외가 발생하지 않을 때까지 재입력받는 기능을 구현했다. 하지만 이 방식으로는 코드의 가독성도 떨어지고 코드의 중복이 많아 리펙토링의 필요성을 느꼈다. 그래서 Function 인터페이스의 apply 메서드를 활용하여 재입력을 받는 방식으로 코드를 개선하여 중복코드를 제거를 했다.

 

기존에는 테스트 코드의 필요성에 대해 의문을 가지고 있었다. "왜 굳이 테스트 코드를 작성해야 할까? 그냥 System.out.print로 확인하며 기능을 구현하면 되지 않을까?"라는 생각을 갖고 있었다. 이번 미션을 진행하면서 System.out.print를 사용하지 않고, 오로지 테스트 코드에만 의존하여 미션을 완수하는 것을 목표로 진행했다.

그 결과, 제 생각이 틀렸다는 걸 깨달았다. System.out.print를 사용하는 방식은 수동적인 부분이 많다. 반면, 테스트 코드를 한 번 작성하면 그 코드를 계속 재사용할 수 있었다. 기능을 수정했을 때도, 테스트 코드를 전체적으로 한 번 실행해 보면 어떤 부분이 잘못되었는지 바로 파악할 수 있었다. 작은 단위의 테스트 코드 작성의 중요성을 알게 된 시간이었다.