🏁 서론
이번에 다시 한 번 느낀 건, 배우는 것만큼 복습이 정말 중요하다는 점이다. 특히 한 번 배운 개념은 꼭 내 것으로 만들기 위한 반복 학습과 이해가 필요하다. 2주차 자동차 경주에서 VO와 DTO 개념을 배웠을 때, 의미를 문자 그대로만 외우고 제대로 이해하지 못한 채 넘겼는데, 이번 미션에서도 그 부분이 다시 막히는 걸 보며 뼈저리게 느꼈다.
다행히 이번엔 PR에서 이 부분에 대해 의견을 나눠보면서, 좀 더 깊이 이해할 수 있었다.
앞으로는 배운 걸 그냥 넘기지 않고, 꼭 정리하고 복습하면서 완전히 내 것으로 만들자!
📚 이번 주 학습 내용
https://github.com/izzy80/java-lotto-clean-playground
GitHub - izzy80/java-lotto-clean-playground: 초록 스터디 자바 기초 과정 로또 미션을 위한 저장소
초록 스터디 자바 기초 과정 로또 미션을 위한 저장소. Contribute to izzy80/java-lotto-clean-playground development by creating an account on GitHub.
github.com
🛠️ 단계별 PR 회고
1. DTO vs VO vs DAO
추가로 리뷰어의 의견
단순히 값만 가지는 VO는 데이터를 전달하는 목적을 가지는 DTO로써 활용하기 좋은 객체다.
DTO : 원본인 Entity대신에 사용되는 객체, 로직이 없고 값만 존재. getter/setter 가능
VO : 불변, getter만 존재, 간단한 로직(검증 등) 존재, 원본으로 이용될 수 있음
DAO : repository 같은 역할
2. for vs stream
📎 참고 링크: Baeldung - Java Streams vs Loops
구분 | for문 | stream |
반복자 유형 | 외부 반복자 | 내부 반복자 |
제어권 | 개발자에게 있음 -> `continue`, `break`, `return` 자유롭게 사용 가능 | 라이브러리에 위임되어 반복 흐름에 대한 직접 제어 불가 |
예외 처리 | 세밀한 흐름 제어 가능 | 예외 발생 시 즉시 종료되며, 내부에 `try-catch` 를 넣으면 가독성이 떨어질 수 있음 |
어떤 리뷰어 분은 “stream을 쓸 때 의도가 명확하지 않다면 for문을 선호한다”고 하셨다.
상황에 따라 명확성과 가독성을 기준으로 선택하는 것이 중요하다고 느꼈다.
3. 후위 증감 연산자와 멀티스레드
멀티스레드 환경에서 다음 코드는 어떤 문제가 발생할 수 있을까?
public Lotto generate() {
return fixedLottos.get(index++);
}
- 멀티스레드 환경에서는 index++가 원자적 연산이 아니기 때문에, 여러 스레드가 동일한 index를 읽는 문제가 발생할 수 있다.
- 처음엔 ++ 연산자 때문인 줄 알았는데, 사실 index += 1도 마찬가지로 thread-safe하지 않다.
- 해결 방법:
- synchronized 블럭으로 동기화 처리
- AtomicInteger 사용하여 원자성 보장
4. findFirst() vs findAny()
메서드 | 설명 |
`findFirst()` | 스트림의 순서를 보장하며 가장 앞의 요소 반환 |
` findAny()` | 순서 보장 X, 병렬 스트림에서 더 나은 성능 기대 가능 |
조건을 만족하는 값이 1개로 보장되면 findFirst()도 문제가 되지 않음.
다만, 내부적으로는 findFirst()가 순서를 보장하기 위해 정렬 등의 오버헤드가 발생할 수 있음.
정렬이 필요 없는 상황이라면 findAny()가 더 가볍고 적절할 수도 있음.
5. Stream 람다 변수명
stream 사용 시 람다 표현식 안의 변수명을 너무 축약하지 말고, 의미 있는 이름으로 작성하는 것이 가독성에 도움이 된다.
✅ 예: user -> user.getName()
❌ 예: u -> u.getName()
6. static 메서드를 도메인 객체 내부에 둘 것인가?
package domain;
import java.util.List;
import java.util.stream.Stream;
public class Lottos {
//구매한 로또들의 내역
//일급 컬렉션
private final List<Lotto> lottos;
public Lottos(List<Lotto> lottos) {
this.lottos = List.copyOf(lottos);
}
public List<Lotto> getLottos() {
return lottos;
}
public int size() {
return lottos.size();
}
public static Lottos merge(Lottos manual, Lottos auto) {
List<Lotto> combined = Stream.concat(
manual.getLottos().stream(),
auto.getLottos().stream()
).toList();
return new Lottos(combined);
}
}
📌 상황 정리
- Lottos.merge()는 두 Lottos 객체를 합쳐 새로운 Lottos를 반환하는 정적 메서드다.
- 이 메서드는 상태 변화 없이 새로운 객체만 생성하며, 의미적으로 Lottos와 밀접하게 연결되어 있다.
- 따라서 도메인 클래스 내부에 있는 것도 자연스럽다고 판단했다.
- 다만 일반적으로 static 메서드는 유틸리티 클래스로 분리하는 경우도 많다 보니 의문이 생겼음.
🧠 리뷰어 피드백 요약
- static 메서드는 객체지향의 핵심 개념인 다형성을 위반할 수 있음
- “두 Lottos를 합치는 행위는 Lottos의 책임이 아니라 별도의 객체에게 부여하는 게 적절할 수도 있다”는 의견 제시 → 예: LottoCombiner 같은 조합 객체
📎 참고 링크 : 정적 메소드, 너 써도 될까?
🧩 내가 정리한 결론
블로그 글 요약
static 메서드
- 반복적으로 자주 사용되는 기능에 효율적
→ 하지만 merge는 프로젝트 내에서 auto + manual을 한 번 묶는 용도로만 사용됌.
- 객체 지향에서 멀어진다.
→ 하지만 복잡한 로직 없이 단순히 두 개의 Lottos를 합쳐 새로운 객체를 반환하는 역할만 해서, 객체지향의 본질을 크게 해치진 않는다고 생각
결국, Lottos.merge()는 의미상 도메인 객체 내부에 있어도 자연스럽고, LocalTime.of(...)처럼 정적 팩터리 메서드의 형태로 이해할 수 있기 때문에 이 경우에는 static 메서드로 구현하는 것이 적절하다고 판단했다.
실제로 리뷰어님도 같은 의견을 주셨고, 이 방향으로 진행해도 무방하다는 확신을 얻었다!
여기서부터는 다른 분의 리뷰를 보면서 배운 점이다!
7. 자바 숫자 _(언더바) 표현
private static final int LOTTO_PRICE = 1_000;
자바에서는 숫자 리터럴에 밑줄(_)을 사용할 수 있다. 이 기능은 사람이 읽기 쉽게 숫자를 표현하기 위한 문법으로, 실제 값에는 영향을 주지 않는다.
즉, 컴파일러는 _를 무시하지만, 개발자는 읽기 쉬운 숫자 표현이 가능해진다.
8. 개행문자 버퍼
`nextInt()`에서 `nextline()`으로 호출하면, nextInt() 이후의 개행문자(\n)가 버퍼에 남아서 바로 빈 문자열로 읽는다.
9. 정적 팩토리 메소드 네이밍 컨벤션 : of vs from
📎 참고 링크: 정적 팩토리 메서드는 왜 사용할까?
of : 여러 값을 조합해서 객체를 만들 때
리뷰어 의견) 파라미터 2개 이상일 때
from : 하나의 값에서 다른 객체로 변환할 때
리뷰어 의견) 파라미터 1개 일때
valueOf : 문자열이나 숫자에서 Enum을 얻을 때
10. EnumMap
Enum을 key값으로 이용하고 Map을 사용한다면 EnumMap을 사용하는 것이 좋다.
HashMap과 비교했을 때 EnumMap이 순서를 보장하고 성능도 더 좋다.
11. static 메소드로 구성된 class는 기본 생성자 private으로 막아두기!
- 불필요한 객체 생성을 방지하기 위해
- 클래스의 의도를 명확하게 전달하기 위해
- 정적 팩토리 클래스, 유틸 클래스, 상수 모음 클래스 등에서 자주 사용
12. 상속은 언제 했을 때 효과적일까?
https://github.com/next-step/java-lotto-clean-playground/pull/111#discussion_r2097391410
좋은 의견인 것 같다.
🧩 정리
- "~은 ~이다" 관계가 명확할 때
- 같은 종류면서 기능을 확장 또는 세분화할 때, 그리고 코드 중복을 줄이기 위해 공통 기능을 공유할 때
13. View에서 바로 Domain을 반환하게 하지 말자
- View와 Domain 간의 불필요한 결합이 생김
-> Domain 생성 방식이나 내부 구현이 바뀌면 View도 바뀌게 된다.
- 해결방안으로는 DTO가 있다.
14. final
📎 참고 링크: final 키워드
class
- 클래스를 상속하는 것을 막을 수 있다.
- 유틸성 클래스는 인스턴스를 만들거나 상속해서 쓸 일이 없어서 final로 선언해서 의도를 명확하게 드러내고 불필요한 확장을 방지하는 효과가 있다.
매개변수
의도치 않게 값을 변경하는 실수를 막을 수 있고, 의도를 명확히 전달할 수 있다.
15. 0으로 나눈다
https://github.com/next-step/java-lotto-clean-playground/pull/121#discussion_r2102633430
📝 마무리 회고
이번 미션은 단순히 기능을 구현하는 것을 넘어, 도메인과 역할의 분리, 객체지향적인 설계, 그리고 구조에 대한 고민까지 해볼 수 있었던 아주 좋은 경험이었다.
특히 VO와 DTO, static 메서드의 위치, 예외 처리 방식 등 그동안 그냥 써왔던 것들에 대해 “왜 이렇게 쓰는지”를 한 번 더 고민해보는 계기가 되었다.
리뷰어의 피드백을 통해 "정답은 하나가 아니라, 상황에 따라 더 적절한 선택이 있다는 것"도 다시금 느꼈고, 내가 선택한 방향이 적절한지 설명할 수 있도록 근거와 의도를 가지고 코드를 작성하는 습관이 정말 중요하다는 걸 배웠다.
앞으로도 “일단 동작하는 코드”가 아니라 “의도를 설명할 수 있는 코드”, “구조적으로 자연스러운 코드”를 고민하면서 성장해나가고 싶다 💪
🔗 Pull Request 기록
'🌤️일상 > 초록 스터디' 카테고리의 다른 글
Spring 1주차) Spring MVC 회고 (0) | 2025.06.19 |
---|---|
Java 4주차) 사다리 회고 (0) | 2025.06.04 |
Java 2주차) 자동차 경주 회고 (2) | 2025.05.13 |
Java 1주차) 계산기 회고 (0) | 2025.05.03 |