이번 3주차 미션은 한마디로 정의할 수 있을 것 같다. "어렵다."
이전에 한 적이 있었지만, 지금처럼 깊게 설계를 하고 들어간 적은 없었던 것 같다.
저번주에 커스텀 예외를 구현해보자! 라고 생각했지만, 3주차 요구사항에서 자바 기본 예외를 사용을 하라는 것이 있어서 머리가 띵했다.
그 이유는 자바 기본 예외도 잘 구현할 수 있을까? 라는 생각이 먼저 들었기 때문이다. 자바 기본 예외도 잘 하지 못하면서 커스텀 예외부터 하는 것은 잘못된 순서라고 생각되었다. 그래서 이번주는 자바 기본 예외를 잘 활용 하자로 목표가 변경되었다.
1. 코드 회고
mvc 나누는 부분이 제일 어려웠다. 다른 패턴도 있었지만, 이것부터 잘 해야 다른 패턴도 잘 적용할 수 있다는 생각이 들어 mvc부터 잘 잡고 들어가자는 생각을 했다. 각 도메인이 해야하는 일을 정의하고, 상태도 정의하고, 이전처럼 서비스 계층 없이는 컨트롤러가 너무 무거워 질 것 같다는 생각이 들었다. 그래서 전주에 설정했던 목표 대로 서비스를 설정해 주었다.
package lotto.model.service;
import lotto.controller.dto.LottoResult;
import lotto.exception.ErrorMessage;
import lotto.model.domain.*;
import lotto.model.domain.vo.BonusNumber;
import lotto.model.domain.vo.Money;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class LottoServiceImpl implements LottoService{
private static final int PERCENT_UNIT = 100;
private static final double ROUND_UNIT = 100.0;
private final LottoMachine lottoMachine;
public LottoServiceImpl(LottoMachine lottoMachine) {
Objects.requireNonNull(lottoMachine, ErrorMessage.NULL_EXCEPTION.getMessage());
this.lottoMachine = lottoMachine;
}
public Lottos buy(Money money) {
Objects.requireNonNull(money, ErrorMessage.NULL_EXCEPTION.getMessage());
return lottoMachine.publishLottos(money.returnAmount());
}
public LottoResult matchWith(Lottos lottos, Lotto winningLotto , BonusNumber bonusNumber) {
WinningLotto winning =
WinningLotto.from(winningLotto, bonusNumber);
List<Rank> ranks = lottos.values().stream()
.map(winning::calculateRank)
.toList();
Map<Rank, Long> rankCounts = ranks.stream()
.collect(Collectors.groupingBy(rank -> rank, Collectors.counting()));
return LottoResult.from(rankCounts);
}
public double calculateEarningRate(LottoResult result, Money spentMoney) {
double rate = (double) result.totalPrize() / spentMoney.getInputMoney() * PERCENT_UNIT;
return Math.round(rate * PERCENT_UNIT) / ROUND_UNIT;
}
}
service를 좀더 세분화 할 순 없었을까요 ? 물론 지금도 생각하기 나름이지만 로또관련 서비스를 갖고있다고 저도 생각합니다 !
하지만 더 세분화해서 나누었다면 객체간의 분리와 책임이 더 명확해지지 않았을까요 ?
감사하게도 이런 리뷰를 달아주셨다. 로또 머신이 한 메서드에서만 쓰이기 때문에 다른 곳에서 해도 된다고 말씀해주셔서 다른 서비르를 생성하는 것도 괜찮은 선택이었을 것 같다.
개선
package lotto.model.service;
import lotto.exception.ErrorMessage;
import lotto.model.domain.LottoMachine;
import lotto.model.domain.Lottos;
import lotto.model.domain.vo.Money;
import java.util.Objects;
public class LottoPurchaseService {
private final LottoMachine lottoMachine;
public LottoPurchaseService(LottoMachine lottoMachine) {
Objects.requireNonNull(lottoMachine, ErrorMessage.NULL_EXCEPTION.getMessage());
this.lottoMachine = lottoMachine;
}
public Lottos buy(Money money) {
Objects.requireNonNull(money, ErrorMessage.NULL_EXCEPTION.getMessage());
return lottoMachine.publishLottos(money.returnAmount());
}
}
package lotto.model.service;
import lotto.controller.dto.LottoResult;
import lotto.model.domain.*;
import lotto.model.domain.vo.BonusNumber;
import lotto.model.domain.vo.Money;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class LottoResultService {
private static final int PERCENT_UNIT = 100;
private static final double ROUND_UNIT = 100.0;
public LottoResult matchWith(Lottos lottos, Lotto winningLotto, BonusNumber bonusNumber) {
WinningLotto winning = WinningLotto.from(winningLotto, bonusNumber);
List<Rank> ranks = lottos.values().stream()
.map(winning::calculateRank)
.toList();
Map<Rank, Long> rankCounts = ranks.stream()
.collect(Collectors.groupingBy(rank -> rank, Collectors.counting()));
return LottoResult.from(rankCounts);
}
public double calculateEarningRate(LottoResult result, Money spentMoney) {
double rate = (double) result.totalPrize() / spentMoney.getInputMoney() * PERCENT_UNIT;
return Math.round(rate * PERCENT_UNIT) / ROUND_UNIT;
}
}
이렇게 나눌 수 있을 것 같다.
package lotto.model.domain;
public enum Rank {
FIRST(6, false, 2_000_000_000, "6개 일치"),
SECOND(5, true, 30_000_000, "5개 일치, 보너스 볼 일치"),
THIRD(5, false, 1_500_000, "5개 일치"),
FOURTH(4, false, 50_000, "4개 일치"),
FIFTH(3, false, 5_000, "3개 일치"),
NONE(0, false, 0, "0개 일치");
private final int matchCount;
private final boolean requiresBonus;
private final int reward;
private final String description;
Rank(int matchCount, boolean requiresBonus, int reward, String description) {
this.matchCount = matchCount;
this.requiresBonus = requiresBonus;
this.reward = reward;
this.description = description;
}
public static Rank of(int matchCount, boolean bonusMatched) {
if (matchCount == 6) {
return FIRST;
}
if (matchCount == 5 && bonusMatched) {
return SECOND;
}
if (matchCount == 5) {
return THIRD;
}
if (matchCount == 4) {
return FOURTH;
}
if (matchCount == 3) {
return FIFTH;
}
return NONE;
}
public int calculatePrize(int count) {
return reward * count;
}
public int getReward() {
return reward;
}
public String getDescription() {
return description;
}
}
이렇게도 로또의 순위를 나타낸 Enum 클래스를 생성하였다.
정적 팩토리 메서드를 보면 if 문 안에 matchCount == 6 이렇게 되어있는데 6 부분을 순위 열거 중 matchCount를 적어 두었는데 이걸 사용했어야 할 것 같다.
개선코드
public static Rank from(int matchCount, boolean matchBonus) {
if (matchCount == Rank.FIRST.getMatchCount()) {
return Rank.FIRST;
}
if (matchCount == Rank.SECOND.getMatchCount() && matchBonus) {
return Rank.SECOND;
}
if (matchCount == Rank.THIRD.getMatchCount()) {
return Rank.THIRD;
}
if (matchCount == Rank.FOURTH.getMatchCount()) {
return Rank.FOURTH;
}
if (matchCount == Rank.FIFTH.getMatchCount()) {
return Rank.FIFTH;
}
return Rank.NONE;
}
package lotto.controller.dto;
import lotto.model.domain.Rank;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public record LottoResult(List<LottoRankResult> results, long totalPrize) {
private static final Long DEFAULT_VALUE = 0L;
public static LottoResult from(Map<Rank, Long> rankCounts) {
return new LottoResult(getLottoRankResults(rankCounts), getTotalPrize(rankCounts));
}
private static long getTotalPrize(Map<Rank, Long> rankCounts) {
return rankCounts.entrySet().stream()
.mapToLong(
entry -> entry.getKey()
.calculatePrize(entry.getValue().intValue()))
.sum();
}
private static List<LottoRankResult> getLottoRankResults(Map<Rank, Long> rankCounts) {
return Arrays.stream(Rank.values())
.filter(rank -> rank != Rank.NONE)
.map(rank -> new LottoRankResult(
rank.getDescription(),
rankCounts.getOrDefault(rank, DEFAULT_VALUE),
rank.getReward()
))
.toList();
}
}
Dto 사용한 부분인데, 코드 리뷰를 아래와 같이 받았다.
LottoResult가 담당하는 계산 및 집계 행위를 도메인 모델로 격상시키고, 현재의 LottoResult 레코드는 이 도메인 모델로부터 최종 결과만 받아 전달하는 순수한 DTO로 분리하면 어떨까요?
맞는 말이라 할말이 없었다. 다른 dto는 순수하게 하려고 노력했는데, 이런 부분에서 어떻게 보면 비즈니스 로직을 같고 있다고 생각했다. 이렇다면 개선을 서비스에서 하는 방법을 갖고 있으면 좋을 것 같다는 생각을 했다.
개선 코드
package lotto.controller.dto;
import java.util.List;
public record LottoResult(
List<LottoRankResult> results,
long totalPrize
) {
public static LottoResult from(List<LottoRankResult> results, long totalPrize) {
return new LottoResult(results, totalPrize);
}
}
package lotto.model.service;
import lotto.controller.dto.LottoRankResult;
import lotto.controller.dto.LottoResult;
import lotto.model.domain.Rank;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class LottoResultService {
private static final long DEFAULT_VALUE = 0L;
public LottoResult createResult(Map<Rank, Long> rankCounts) {
List<LottoRankResult> rankResults = getLottoRankResults(rankCounts);
long totalPrize = calculateTotalPrize(rankCounts);
return LottoResult.from(rankResults, totalPrize);
}
private long calculateTotalPrize(Map<Rank, Long> rankCounts) {
return rankCounts.entrySet().stream()
.mapToLong(entry ->
entry.getKey().calculatePrize(entry.getValue().intValue()))
.sum();
}
private List<LottoRankResult> getLottoRankResults(Map<Rank, Long> rankCounts) {
return Arrays.stream(Rank.values())
.filter(rank -> rank != Rank.NONE)
.map(rank -> new LottoRankResult(
rank.getDescription(),
rankCounts.getOrDefault(rank, DEFAULT_VALUE),
rank.getReward()
))
.collect(Collectors.toList());
}
}
이렇게 dto는 순수하게 변경하고 서비스나 domain으로 객체 계산 책임을 변경할 수 있을 것이다. 다만 지금 내 코드는 서비스를 구현하고 있기에 서비스로 변경하였다.
하지만 이것도 문제가 있다고 보는데 get을 사용하고 있다는 점이다. 근데 Enum에서 가져오는것이니 괜찮은 것인가??라는 생각이 들었다. getter에 대한 생각도 정리해야 될 것 같다.
2. 예외 처리
package lotto.model.domain;
import lotto.exception.ErrorMessage;
import java.util.List;
import java.util.Objects;
public class Lottos {
private final List<Lotto> lottos;
public Lottos(List<Lotto> lottos) {
this.lottos = lottos;
}
public static Lottos of(List<Lotto> lottos) {
Objects.requireNonNull(lottos, ErrorMessage.NULL_EXCEPTION.getMessage());
return new Lottos(lottos);
}
public List<Lotto> values() {
return List.copyOf(lottos);
}
}
NullPointerException 을 null 체크를 하기위해 사용하였다. 기존에는 if(lottos == null) 과 같은 방법으로 널체크를 했다면, 아예 내부에서 throw new NullPointerException(message) 수행하는 게 어떨까 하고 짠 코드였다. 나쁘지 않은 의도였던 것 같다.
그리고 요구사항에서
Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리한다
라는 항목이 있어서 IllegalStateException 에 대해서 고민했다.
이미 메서드 호출 순서가 명확하게 보장되는 구조라 실제로 잘못된 상태가 발생할 가능성은 거의 없었기에 굳이 예외를 던져야 할 필요가 있을지에 대한 고민이 있었다.
그럼에도 불구하고 코드의 안정성과 의도를 명확히 드러내기 위해 IllegalStateException을 사용하였다.
public class Controller {
private final InputView inputView;
private final OutputView outputView;
private final LottoService lottoService;
public Controller(InputView inputView, OutputView outputView, LottoService lottoService) {
this.inputView = Objects.requireNonNull(inputView, ErrorMessage.NULL_EXCEPTION.getMessage());
this.outputView = Objects.requireNonNull(outputView, ErrorMessage.NULL_EXCEPTION.getMessage());
this.lottoService = Objects.requireNonNull(lottoService, ErrorMessage.NULL_EXCEPTION.getMessage());
}
public void run() {
Money money = getInputMoney();
Lottos lottos = showLottos(money);
showResult(lottos, money);
}
private Money getInputMoney() {
return retryInput(() -> {
int parsedMoney = MoneyParser.parse(inputView.inputPurchaseMoney());
Objects.requireNonNull(parsedMoney, ErrorMessage.NULL_EXCEPTION.getMessage());
return Money.from(parsedMoney);
});
}
private Lottos showLottos(Money money) {
if (money == null) {
throw new IllegalStateException(ErrorMessage.STATE_EXCEPTION.getMessage());
}
Lottos lottos = lottoService.buy(money);
LottoPurchaseResult lottoPurchaseResult = LottoDtoConverter.toDto(lottos);
outputView.printLottos(lottoPurchaseResult);
return lottos;
}
대신, 해당 검증 로직은 순서가 이미 구현되어 있는 컨트롤러에서 수행하도록 구현하였다.
학교와 학교 과제, 팀플, 등등이 모조리 겹쳐서 정신이 없다.. 시간도 너무 촉박하다고 생각이 든다ㅠㅠ
그래도 미션을 끝까지 끝내고 테스트까지 통과 했을 때 쾌감이 정말 좋았다.

다만 과제 제출할때 회고를 작성하는데, 코드 구현에 너무 신경 쓴 나머지 회고를 너무 짧게 쓰는것 아닌가 하는 생각이 들었다ㅠㅠ 분명 한건 많은데, 이걸 시간에 쫒겨서 다 적지 못해 너무 아쉬운 마음이 크다.. 항상 그런 것 같다..ㅠㅠ
다음 미션은 시간이 기니까 회고를 잘 적으면서 기록해나가야 될 것 같다. 노션과 블로그를 잘 활용해야지!!!
그래도 벌써 절반을 끝냈다. 잘 끝내고 후회 없이 해내고 싶다.
마지막. 우테코 오픈 미션?!
처음으로 이전에는 없던 오픈 미션을 한다고 한다.
이전에는 프리코스로만 이루어졌다고 하면 이번엔 본인이 자기주도 적으로 할 수 있는 것을 정하라고 한다.
극단적으로는 바리스타 자격증을 2주만에 딴것도 도전이라고 할 수 있다고 하는데, 그만큼 도전 정신을 높게 사는게 아닌가 라는 생각이 든다.
이전 코드를 디벨롭해서 다른 방식으로 해야할지, js로 해봐야 하는지. 어떤걸 해야하는지 정말로 고민이 된다.
이전에는 없던 것이라 블로그를 찾기도 힘들고 완전히 자기 주도적으로 해야하는 것 같다. 해보자!!
당황감도 있지만, 이걸 어떻게 시작해야 할지 이상하게 설레는 마음도 드는것 같다.
진짜 우테코 너무너무너무너무너무 가고싶다.. 열심히 해서 도전해보자!!!!!!!!! 파이팅!!!!!
'알고리즘' 카테고리의 다른 글
| 프로그래머스 알고리즘 (2) | 2025.06.24 |
|---|---|
| 프로그래머스 문제(JAVA) / 피자 나눠 먹기(2) (0) | 2025.06.23 |
| 프로그래머스 문제(JAVA) / 최빈값 구하기 (0) | 2025.06.20 |
| 알고리즘프로그래머스 문제(JAVA) / 분수의 덧셈 / 유클리드 호제법 (0) | 2025.06.18 |
| 프로그래머스 문제(JAVA) / 배열 두 배 만들기 (0) | 2025.06.16 |