이번 주차 과제는 개인적인 일정이 있어서 많이 공들이지 못했던 것이 아쉽다.
그러나 테스트 코드를 통과시키기 위해 기존 코드를 자주 수정하면서
테스트 코드가 애플리케이션의 안정성과 사용자 경험에 있어서 중요하다는 것을 느꼈다.
enum을 처음 제대로 사용해보면서 불변 데이터가 상태값을 가지고있을 때 편리한 기능이라는 것을 느꼈다.
가독성도 좋아지고 컴파일러에 의해 불변성도 보장받을 수 있어서 장점이 많은 기능인 것 같다.
그리고 이전 주차와 마찬가지로 Java 기본 API에 대해서도 조금씩 습득하고 있고 (특히 collection, stream)
immutable list의 필요성과 생성 과정이 너무 정리가 안 되어있어서 헷갈렸는데 이번 기회를 통해서 좀 정리가 된 것 같다.
다음 주차 때도 바쁜 시간을 쪼개서 구현을 해야 하는데 오픈 미션까지 있다니 정말 산 넘어 산이다!!!
그래도 화이팅해부자~~ 😄👊
enum
과제에서 로또의 등수를 표현해야 했다.
등수는 다음과 같은 상태를 가지고 있다.
- 일치하는 번호 개수
- 보너스 번호 일치 여부
- 상금 금액
다음과 같이 등수를 코드로만 표현하면 의미가 불분명하고, 여러 상태를 반영하기도 어렵다.
public static final int FIRST = 6;
public static final int SECOND = 5;
public static final int THIRD = 5;
// ...
if(number == FIRST){
} else if(number == SECOND){
} else if(number == THIRD){
} // ...
enum을 사용하면 다음과 같은 이점이 있다.
- 가독성 : 상수 집합을 하나의 타입으로 묶어 표현할 수 있고, Rank.FIRST 처럼 의미 있는 상수로 사용 가능
- 불변성 : enum의 각 항목들은 외부에서 생성 불가하므로 단일 인스턴스가 보장됨 (enum의 생성자는 private이 디폴트로 적용됨)
- 타입 안정성 : Rank 타입 안에 가능한 값은 FIRST ~ NONE 뿐이라는 것을 컴파일러가 보장
- switch문 지원 : Rank에 새 항목을 추가했는데 switch문에서 안 다뤘다면 컴파일 에러
switch(rank){
case FIRST -> System.out.println("1등");
case SECOND -> System.out.println("2등");
// ...
}
🙋🏻♀️ 🙋🏻♀️ 내 과제에 적용해보기
public enum Rank {
FIRST(6, false, 2_000_000_000),
SECOND(5, true, 30_000_000),
THIRD(5, false, 1_500_000),
FOURTH(4, false, 50_000),
FIFTH(3, false, 5_000),
NONE(0, false, 0);
private final int matchCount;
private final boolean bonusMatch;
private final int prize;
Rank(int matchCount, boolean bonusMatch, int prize) {
this.matchCount = matchCount;
this.bonusMatch = bonusMatch;
this.prize = prize;
}
public int getPrize() {
return prize;
}
// Lotto의 Rank를 판별
public static Rank of(int matchCount, boolean hasBonus) {
return Arrays.stream(values())
.filter(rank ->
rank.matchCount == matchCount &&
(rank.bonusMatch == hasBonus || !rank.bonusMatch))
.findFirst()
.orElse(NONE);
}
}
Rank.of()메서드에 대해서 조금 더 설명하자면
enum을 정의하면 컴파일러가 자동으로
public static Rank[] values(){
return new Rank[] { FIRST, SECOND, THIRD };
}
이런 유틸 메서드를 만들어주는데, 이것은 enum의 모든 상수들을 배열 형태로 반환하는 정적 메서드이다.
enum 밖에서는 Rank.values()로 쓸 수 있고, 내부에서는 바로 values()로 호출할 수 있다.
try-catch와 throw
try-catch
- 예외가 터져서 프로그램이 죽는 것을 방지
- 사용자에게 예외 메시지를 보여줌
throw
- 처리할 수 없는 예외를 호출한 쪽으로 전파
- 잘못된 입력이 들어온 경우 즉시 예외를 발생시킬 때
이 부분은 아직 코드를 짜면서 더 공부해봐야할 것 같다.
불변 List
- 한 번 만들어지면 바뀌지 않는 리스트
- add(), remove(), set() 등의 수정 메서드 사용 불가
- get()으로 조회만 가능함
가변, 불변 리스트를 만드는 방법은 다음과 같다.
// 가변
List<Integer> list = new ArrayList<>();
// 불변
List<Integer> list = List.of(1, 2, 3); // 값을 바로 불변 리스트로 만드는 정적 팩토리 메서드
List<Integer> list = list.stream().toList(); // stream 결과를 불변 리스트로 수집
List<Integer> list = List.CopyOf(list); // 기존 리스트를 복사해서 불변 리스트로 반환
가변 리스트를 반환할 때는 외부에서 수정하지 못하도록 하기 위해 방어 복사를 해야 한다.
하지만 리스트를 조회할 때마다 매번 복사하게 되면 리스트가 큰 경우 성능 오버헤드가 발생한다는 문제점이 있다.
public List<Integer> getNumbers() {
return new ArrayList<>(numbers); // 방어 복사
}
불변 리스트이면 수정이 불가하므로 원본을 그대로 반환해도 안전하다.
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = List.copyOf(numbers); // 불변 리스트로 저장
}
public List<Integer> getNumbers() {
return numbers; // 그대로 반환
}
}
🙋🏻♀️ 🙋🏻♀️ 내 과제에 적용해보기
로또를 생성하는 로직이다.
numbers가 불변 리스트라서 내부 요소의 변경이 허용되지 않기 때문에,
Collections.sort(numbers)를 호출해도 정렬되지 않았고 관련 테스트가 실패했다.
package lotto.domain;
import java.util.*;
import camp.nextstep.edu.missionutils.Randoms;
import java.util.stream.Collectors;
public class Lotto {
// 필드, 생성자 ...
public static Lotto createRandomLotto() {
List<Integer> numbers = Randoms.pickUniqueNumbersInRange(
LOTTO_NUMBER_MIN, LOTTO_NUMBER_MAX, LOTTO_NUMBER_COUNT);
Collections.sort(numbers);
return new Lotto(numbers);
}
// 기타 로직들 ...
}
외부 라이브러리인 Randoms.pickUniqueNumbersInRange()를 보았더니 다음과 같이 구현되어있었다.
public static List<Integer> pickUniqueNumbersInRange(int startInclusive, int endInclusive, int count) {
validateRange(startInclusive, endInclusive);
validateCount(startInclusive, endInclusive, count);
List<Integer> numbers = new ArrayList();
for(int i = startInclusive; i <= endInclusive; ++i) {
numbers.add(i);
}
return shuffle(numbers).subList(0, count);
}
shuffle()는 java.util.Collectons()에서 제공하는 메서드로, 가변 리스트를 받아서 요소 순서를 무작위로 섞어주고
subList()는 java.util.List()에서 제공하는 메서드로, 원본 리스트의 일부 구간을 view로 반환한다.
subList()의 반환값도 원본 리스트에 따라서 가변, 불변이 정해진다.
“The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa. The returned list supports all of the optional list operations supported by this list.”
- javadoc의 subList() 설명문
어디에도 불변 리스트로 반환하는 로직이 없는데
실제로 테스트해보면 Collections.sort()에서 numbers가 immutable list라는 에러 메시지가 뜬다.
java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
at java.base/java.util.ImmutableCollections$AbstractImmutableList.sort(ImmutableCollections.java:263)
at java.base/java.util.Collections.sort(Collections.java:145)
at lotto.domain.Lotto.createRandomLotto(Lotto.java:22)
디버깅 해봐도
public static Lotto createRandomLotto() {
List<Integer> numbers = Randoms.pickUniqueNumbersInRange(
LOTTO_NUMBER_MIN, LOTTO_NUMBER_MAX, LOTTO_NUMBER_COUNT);
System.out.println("numbers List : " + numbers.getClass());
// numbers List : class java.util.ImmutableCollections$ListN
Collections.sort(numbers);
return numbers;
}
이렇게 출력되어서 불변인 건 맞는데 내부의 어떤 로직 때문에 불변으로 반환되는 건지는 아직 모르겠다!
외부 라이브러리 내부 로직을 깊게 팔 필요는 없으므로 여기까지 알아보고 이제 불변 리스트를 정렬할 수 있게 가변으로 변환해보자.
방법 1 : 새로운 리스트로 복사하기
가장 직관적이고 간단한 방법이다.
public static Lotto createRandomLotto() {
List<Integer> numbers = new ArrayList<>(Randoms.pickUniqueNumbersInRange(
LOTTO_NUMBER_MIN, LOTTO_NUMBER_MAX, LOTTO_NUMBER_COUNT));
Collections.sort(numbers);
return new Lotto(numbers);
}

불변 리스트(randomNumbers)를 복제해서 새로운 가변 리스트(numbers)를 만들었다.
리스트 주소는 다르지만 내부 요소는 같은 곳을 참조하고 있으므로 얕은 복사이다.
방법 2 : stream api 활용하기
- sorted() : stream의 모든 요소들의 순서를 재정렬한 후 다시 흘려보냄
- collect() : stream의 모든 정제된 요소들을 모아서 어떤 자료구조로 반환할 것인지 결정
2-1 .collect(Collectors.toList())로 가변 리스트를 생성
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
2-2 Java 16 이상이면 .toList()를 써서 불변 리스트를 간단히 생성할 수 있다. - 내부적으로 List.copyOf()가 사용된다.
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.toList();

toList()도 마찬가지로 얕은 복사이다.
2-3 Java 10 이상에서 toUnmodifiableList()도 사용되는데 찾아보면 원본 객체를 참조하는 새로운 불변 변수를 만들 뿐이므로, 완전한 불변을 보장하지는 않는다고 한다. 그래서 딱히 좋은 방법은 아닌 것 같다.
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toUnmodifiableList());
결론적으로 불변 리스트를 반환하는 2-2 방법이 가장 안전하고 구현도 직관적인듯!
toList()와 toUnmodifiableList() 모두 얕은 복사이다.
toList()는 단지 Java 16 이상에서 Collectors.toUnmodifiableList()를 간단하게 사용하려고 만들어진 함수이다.

✅ 따라서 현재로서는 로또 번호를 변경하는 로직이 없고, 항상 deepcopy를 하면 리소스 낭비가 될 수 있으므로 toList()로 얕은 복사 + 불변 리스트를 생성하면 될듯!!
record 클래스
데이터 저장용 불변 클래스 (immutable data carrier)
즉 불변 DTO(Value Object)를 자동으로 만드는 문법이다.
public record Person(String name, int age) {}
record를 쓰면 컴파일러가 자동으로 만들어주는 것
- private final 필드 - 불변 객체 보장
- 모든 필드를 초기화하는 기본 생성자
- name(), age() 형태의 getter 메서드
- equals(), hashCode()로 필드 기반 비교 자동 생성
- toString()으로 포맷된 문자열 자동 생성
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
@Override
public boolean equals(Object o) {
// ... (생략)
}
@Override
public int hashCode() {
// ... (생략)
}
}
🙋🏻♀️ 🙋🏻♀️ 내 과제에 적용해보기
이번 과제에서 LottoResult를 record로 만든 친구가 있어서 한번 예시로 들어보겠다!
원래는 따로 LottoResult를 만들지 않고 아래처럼 바로 출력했었다.
WinningStats가 WinningNumber, Lottos객체에게 results와 profitRate를 구하도록 시킨 후
구해진 결과를 outputView에게 print하도록 시키고 있다.
package lotto.domain;
import java.util.Map;
import lotto.view.OutputView;
public class WinningStats {
private OutputView outputView;
public WinningStats(OutputView outputView){
this.outputView = outputView;
}
public void printStats(Lottos lottos){
Map<Rank, Integer> results = lottos.calcRank(WinningNumber.INSTANCE);
double profitRate = lottos.calcProfitRate(results);
outputView.printWinningStats(results);
outputView.printProfitRate(profitRate);
}
}
여기서 WinningStats가 OutputView로 results, profitRate을 넘기게 하는 방법 보다는
record LottoResult라는 하나의 불변 객체(DTO)로 감싸서 리턴하게 하는 방법이 좋다.
그러면 코드를 다음과 같이 정리된다.
WinningStats는 결과 생성에만 집중하게 되고, 출력 책임은 OutputView로 완전히 분리된다.
public record LottoResult(Map<Rank, Integer> result, Double profitRate){}
package lotto.domain;
import java.util.Map;
public class WinningStats {
public LottoResult calculate (Lottos lottos){
Map<Rank, Integer> results = lottos.calcRank(WinningNumber.INSTANCE);
double profitRate = lottos.calcProfitRate(results);
return new LottoResult(results, profitRate);
}
}
----
코드리뷰를 통해 추가로 고민한 부분
나중에 시간 되면 정리해야지
bigdecimal
커스텀 예외 처리
'우테코' 카테고리의 다른 글
| [우테코 프리코스] 2주차 미션 회고록 (0) | 2025.10.29 |
|---|---|
| [우테코 프리코스] 1주차 미션 회고록 (2) | 2025.10.21 |