저번에 메서드 네이밍에 대한 피드백을 받았는데, 이번에도 이 부분에 대한 피드백이 가장 많이 들어왔다.
아직 근거있는 네이밍이 부족하다고 느꼈다. 각 클래스의 관심사에 집중해서 네이밍할 수 있도록 더 신경써야겠다.
이번에도 어김없이 클래스 분리에 대한 피드백이 많이 들어왔다.
위 코멘트와 같이 클래스 분리가 적절하지 않은 수준으로 이뤄졌다는 피드백이 들어오기도 했다.
지난 과제에서는 원시타입으로는 표현하기 힘든 구체적인 데이터를 출력해야 했는데, View에 도메인 객체를 그대로 넘기고싶지 않아서 DTO를 적용했다. 그리고 DTO의 생성 위치는 큰 고민없이 도메인 클래스에 정의했다. DTO에 도메인 객체를 넘기는 방향으로 작성한다면 DTO에게 도메인 객체에 영향을 줄 수 있는 권한이 생기는 것이 우려되었다.
이렇게 DTO를 도메인 클래스에서 생성하도록 작성한 것에 대해 피드백이 들어왔다. 도메인 클래스에서 DTO를 생성한다면 입출력 요구사항이 변경될 경우 View의 변경이 DTO에 영향을 미치고, 이게 다시 도메인 클래스에 영향을 미칠 수 있다는 이야기였다.
확실히 지양해야 하는 방향이라고 생각하고, 이번주에는 이를 어떻게 해결할 수 있을지 고민해보는 시간을 가졌다.
TDD를 진행하면서 최소한으로 동작하는 프로그램을 빠르게 완성한 후 리팩토링을 하다보니 어떤 예외를 던져야 하는지 명시하는 것을 잊어버리기도 했다.
앞으로는 리팩토링을 더 꼼꼼히 해야겠다... 😢
여기까지가 여러 리뷰어분들께 중복해서 받은 피드백들이고, 아래는 1회씩 받은 피드백들이다. 관심이 간다면 학습해보자.
유익한 피드백을 많이 받을 수 있었지만 다른 사람의 코드를 리뷰하는 과정에서도 많은 것들을 배울 수 있었다.
불변 객체를 도입한 분들이 계셨다. 불변 객체를 활용하면 객체 내부 상태의 불변을 보장하여 안정성을 챙길 수 있기에 메리트있어 보였다. 무작정 모든 객체들을 불변으로 만드는 것은 바람직하지 않겠지만 최소한 원시타입 포장을 위한 일급 객체 정도는 불변을 적용해보는 것도 좋겠다고 느꼈다.
객체 내부의 컬렉션을 그대로 외부에 반환하게 되면 외부에서 이 컬렉션에 직접적으로 관여할 수 있는 문제가 발생한다. 이를 개선하기 위해 불변 컬렉션으로 바꿔 반환하는 등 방어적 복사를 활용해볼 수 있다. 다만 컬렉션을 방어적복사한다고 해도 컬렉션 내부의 객체 상태까지는 불변을 보장할 수 없기 때문에 유의해야 한다.
기존에는 입력 형태 검증을 InputView에서 진행했는데, InputView의 크기가 점점 커지면서 이제는 분리를 고민해야 하는 상황이 되었다. 이 검증 절차를 어디로 옮겨야 할 지 열심히 고민중이었는데, 어떤 분은 DTO 내부에서 입력 형태 검증을 진행하고 있었다. 확실히 Input보다는 특정 형식을 만족해야 하는 DTO의 관심사에 가깝다고 생각하고, DTO로 변환하는 과정에서 자연스럽게 검증까지 이루어지는 로직의 방향성이 내가 추구하던 것과 일치하기 때문에 남은 프리코스에서는 이를 활용해보려고 한다.
I/O는 일반적으로 메모리에서 처리하는 작업에 비해 비용이 많이 비싸다. 그래서 System.out.println()
과 같은 작업은 최소한으로 요청하는 것이 좋다고 한다.
나는 출력 메시지나 예외 메시지를 enum으로 관리하면서 String.format을 적극적으로 활용하고 있다. 그러다 보니 .getMessage()
메서드의 인자로 int value
나 String value
, int intValue, String strValue
와 같이 다양한 정보를 받아와야 하는 경우가 생겼고, 이 때마다 메서드 오버로딩을 통해 구현했다. 하지만 이렇게 작성하다보니 같은 로직의 코드가 무의미하게 반복되는 상황이 발생했고, 코드리뷰 중 가변인자를 활용해 이를 해결한 코드를 접할 수 있었다.
가변인자를 적용하니 확실히 무의미한 메서드 오버로딩이 필요없어지고 간결하면서도 직관적인 코드로 개선되는 것 같았다. 꼭 기억해두고 필요할 때 사용해봐야겠다.
@FunctionalInterface
public interface LottoRandom {
List<Integer> getLottoNumbers();
}
랜덤 값 추출 로직을 전략 패턴으로 분리하고, 인터페이스를 함수형 인터페이스로 활용했다. 여기에 람다표현식을 활용한 익명 함수를 통해 테스트용 클래스를 따로 작성하지 않고도 원하는 값을 간편하게 주입할 수 있었다.
class LottoRandomTest {
private LottoRandom lottoRandom;
@BeforeEach
void setup() {
lottoRandom = () -> List.of(1, 2, 3, 4, 5, 6);
}
}
스프링에서는 Controller나 Service가 스프링 컨테이너에 의해 싱글톤으로 관리된다. 하지만 지난 주 과제까지는 Service에서 도메인들을 직접 관리했기에 Service 객체가 생태를 유지해야 했다.
Service가 상태를 유지하지 않도록 만들기 위해 Repository 계층을 추가하여 상태를 유지하는 주체를 변경했다.
public class WinningLottoRepository {
private WinningLotto winningLotto = new WinningLotto();
public WinningLotto get() {
return winningLotto;
}
}
public class LottoService {
private final LottoRandom lottoRandom = new LottoRandomStrategy();
private final PlayerRepository playerRepository = new PlayerRepository();
public LottosResponse buyLottos() {
Player player = playerRepository.get();
player.buyLottos(lottoRandom);
return LottosResponse.of(player.getLottos());
}
}
코드리뷰 과정에서 배운 점을 토대로 InputView에서 입력받은 데이터를 가공하고 검증하는 주체를 DTO로 변경했다. 개인적으로 InputView는 입력에만 집중하고 입력 형식과 검증은 DTO에서 진행하는 것이 스프링과도 닮아있고 적절한 책임 분리라고 생각한다. 하지만 try-catch
를 사용해서 검증과 가공을 동시에 진행하며 이 로직이 DTO에 들어있는 방향성이 올바르다고 확신하지는 못하겠다. 다음주에 코드리뷰를 받아보며 이 부분에 대한 의견을 들어보고 싶다.
public class InputView {
public static MoneyRequest money() {
return MoneyRequest.from(input());
}
}
public record MoneyRequest(long money) {
public static MoneyRequest from(String input) {
try {
return new MoneyRequest(Long.parseLong(input));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(NOT_NUMBER_MONEY.getMessage());
}
}
}
코드리뷰 과정에서 입출력 비용이 매우 크다는 점을 알았기에 어떻게 이를 줄일 수 있을지 고민해보았다. 기존에는 문자열 출력 시 System.out.print()
를 매번 바로 호출했다면 이번에는 버퍼 공간에 누적시켜두고 필요할 때만 flush()
할 수 있도록 작성해보았다.
public class OutputView {
private StringBuilder buffer;
public OutputView() {
setupBuffer();
}
public void inputMoney() {
write(INPUT_MONEY.getMessage());
flush();
}
// ...
private void write(String content) {
buffer.append(content);
}
private void flush() {
System.out.print(buffer);
setupBuffer();
}
private void setupBuffer() {
buffer = new StringBuilder();
}
}
System.out과 BufferedWriter, StringBuilder의 출력 속도가 궁금하다면 아래 게시글을 확인해보자.
[JAVA] BufferedReader, BufferedWriter, StringBuilder
안정성을 높이기 위해 변경이 적은 객체에 대해 불변성을 적용해보았다. 변경이 필요한 경우 변경된 필드를 지닌 새로운 객체를 만들어 반환하는 형태로 구성했다.
public class Wallet {
private final long initialMoney;
private final long money;
public Wallet(long money) {
this(money, money);
}
private Wallet(long initialMoney, long money) {
validate(initialMoney);
this.initialMoney = initialMoney;
this.money = money;
}
public Wallet useMoney(long money) {
return new Wallet(this.initialMoney, this.money - money);
}
}
이번에는 출력 메시지 반환 시 가변인자를 사용할 수 있도록 적용해보았다. 이전에 비해 훨씬 코드가 간결해진 것을 확인할 수 있었다.
public enum OutputMessage {
INPUT_MONEY("구입금액을 입력해 주세요.%n"),
BUY_LOTTO_TICKETS("%n%d개를 구매했습니다.%n"),
RANK_RESULT("%d개 일치 (%s원) - %d개%n"),
RANK_RESULT_SECOND("%d개 일치, 보너스 볼 일치 (%s원) - %d개%n"),
RETURN_RESULT("총 수익률은 %.1f%%입니다."),
EXCEPTION("%n[ERROR] %s%n%n"),
;
private final String message;
OutputMessage(String message) {
this.message = message;
}
// ...
public String getMessage(Object... args) {
return String.format(message, args);
}
}
이번 주 과제는 지난 과제들과 달리 새로 추가된 요구사항이 있다.
사용자가 잘못된 값을 입력할 경우
IllegalArgumentException
을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
이를 위해 입력 로직에 대해 매번 반복문과 조건문을 사용해야 할지 고민되었다. 아무리 봐도 너무 코드가 못생겨서 다른 방법은 없을지 고민해보았다. 아래 게시글에 정리해두었으니 관심이 생겼다면 한 번 읽어보자!
이번 과제에서는 몇 등 복권이 몇 개 당첨되었는지 출력해야 한다. 그래서 각 등수별 정보를 Rank
열거형 클래스에 담고, 이 등수의 복권이 몇 개 당첨되었는지를 Map<Rank, Integer>
로 함께 저장하도록 구현했다. 그런데 IDE에서 갑자기 신경쓰이는 노란 줄을 띄워줬다.
HashMap
대신 EnumMap
을 사용하라는 내용이었다. EnumMap
은 Key로써 특정 Enum 클래스 객체만을 가질 수 있는 특별한 Map
자료구조였다.
private final Map<Rank, Integer> rankCounts = new EnumMap<>(Rank.class);
EnumMap
은 Map
의 구현체이기 때문에 사용법은 동일하고 생성할 때만 위와 같이 Enum 클래스 정보를 제공하면 된다.
EnumMap
을 사용하면 Key에 특정 Enum 객체만이 들어갈 수 있음을 보장하고, 이를 통해 Map에 들어갈 수 있는 요소의 최대 개수를 예측할 수 있으므로 내부적으로 성능이 최적화되며 해시 충돌 예방과 같은 부가 요소를 고려하지 않아도 된다는 장점이 있다.
[JAVA] EnumMap 을 사용합시다.
A Guide to EnumMap - Baeldung
지난주에 이어 이번에도 코드리뷰를 최선을 다해 진행했다. 정말 3일동안 코드리뷰만 했다... 프리코스 커뮤니티에도 PR 리뷰 글을 올리고, 교내 프리코스 참여자분들과 스터디원분들, 코레아(6기 코드리뷰 인원 모집 서비스) 리뷰이분들을 리뷰해드리니 지난주보다 훨씬 많은 리뷰를 진행하게 되었다.
그 결과 이번주에는 총 22명의 코드를 리뷰했고, 내 PR에서는 15명의 참여자분들과 총 171개의 코멘트를 주고받았다. 코멘트를 많이 모으겠다는 생각으로 참여한 것은 아니었지만 코드리뷰를 열심히 진행하다 보니 내 PR에도 많은 분들이 리뷰를 남겨주신 것 같다.
무려 전체 PR 중 코멘트 수 1등...?! 많은 관심과 성원에 감사드립니다 🥰
리뷰 과정에서 새로운 코드 방향성도 접하고 여러 이야기를 주고받으며 많이 성장함을 느끼고 있다. 다른 사람에게 내 의견을 설득할 수 있는 능력도 중요하지만 특히 다른 사람의 의견을 열린 자세로 경청하며 내 생각이 틀렸음을 인정할 수 있을 때 더욱 성장하는 것 같다.
다음주에도 코드리뷰 열심히 해봐야겠다!!
지난주 과제를 진행하면서 가장 고민했던 내용에 대해 정리해서 블로깅을 했다. 그리고 다른 분들에게도 도움이 되었으면 하는 마음으로 프리코스 커뮤니티에 글을 올렸는데 생각보다 훨씬 많은 분들이 관심을 주시고 칭찬해주셔서 너무 감사하다. 🥰
이번주에도 고민한 내용에 대해 꼼꼼히 정리해서 공유해야겠다!
이번주 스터디에서는 목표를 달성했는지 점검하고 학습한 내용과 회고를 공유, 라이브 코드리뷰를 진행했다. 다들 코드 품질이 굉장히 좋아진 게 느껴져서 깜짝 놀랐다. 함께 스터디를 진행할 때마다 더 성장 욕구가 샘솟고 동기부여가 되는 것 같아서 너무 좋다.
사실 이번 스터디의 메인디쉬는 따로 있는데, 바로 몹 프로그래밍이다!
나는 동아리 사람들과 함께 종종 페어 프로그래밍을 진행하고 있는데, 스터디원분들은 이러한 경험이 없었다. 그래서 이 경험이 얼마나 유익한지 공유해드리고자 하는 마음으로 몹 프로그래밍을 준비해갔다.
프리코스 6기 숫자야구게임 과제를 가지고 2시간동안 몹 프로그래밍을 진행했다. TDD 적용 여부부터 시작해서 변수이름까지 하나하나 논의하며 진행했다. 당연히 혼자 개발할 때에 비해 훨씬 오래걸렸지만 서로 의견을 공유하며 각 코드에 근거를 가지기 위해 고민하는 과정이 유익하고 즐거웠다. 다른 스터디원분들도 이 활동이 재미있었다고 말씀해주셔서 좋았다. 비록 이번주에는 시간이 부족해서 구조를 크게 고려하지 못하고 구현까지만 진행했지만, 다음주 몹 프로그래밍 시간에는 리팩토링하는 시간을 가져보기로 했다. 이번주보다도 훨씬 더 재미있겠지?! 😆
스터디가 끝나고는 커피챗 시간을 가졌다. 백엔드에서 유익하게 다뤄볼 수 있는 여러 주제에 대해 이야기했고, 백엔드 개발자는 무엇을 중요하게 여겨야 하는가, 어떤 색깔을 가진 개발자가 되고 싶은가(이 질문은 너무 어려워서 대답하지 못했다.. 🥲) 등 프리코스 외의 다양한 이야기를 주고받았다. 앉은지 얼마 되지도 않은 것 같은데 벌써 돌아갈 시간이 되버린 게 너무 아쉬웠다.
이번주 스터디도 시간가는 줄 모르고 끝나버린 것 같다. 더 이야기하고 싶은 마음에 아쉬우면서도 많은 이야기를 주고받고 고민하는 시간을 가졌음에 뿌듯하다. 남은 프리코스 기간동안 스터디에서 또 어떤 재밌는 일을 해볼 수 있을지 고민해봐야겠다. 🤭
열심히 달리다 보니 어느샌가 프리코스의 끝이 다와간다. 나는 지금까지의 프리코스 기간을 유익하게 보냈을까? 당당하게 그렇다고 말할 수 있다. 지난 3주를 유익하고 재밌는 프리코스 과정으로 즐겼고, 남은 기간에도 어떤 재밌는 일이 있을지 기대하고 있다.
나는 다른 사람과 코드리뷰를 주고받는 것을 즐기며, 매주 배운 내용을 공유하고 있다. 회고를 꾸준히 작성하여 프리코스에 임하는 내 자세를 점검하며, 다른 사람은 어떻게 프리코스를 보내고 있는지 회고를 읽어보기도 한다. 스터디를 주도하며 다함께 유익한 시간을 보낼 수 있도록 준비하기도 하고, 구성원들에게 어떤 도움을 줄 수 있을지 항상 고민한다.
그리고 그 속에서 성장하는 나를 볼 수 있었다.
프리코스 기간이 얼마 남지 않았지만, 그렇기에 더욱 열심히 달려봐야겠다.
다들 남은 프리코스도 재밌게 해보자!! 화이팅!!!
[JAVA] BufferedReader, BufferedWriter, StringBuilder
[JAVA] EnumMap 을 사용합시다.
A Guide to EnumMap - Baeldung
벨로그 진짜 잘 쓰는 것 같아욥!
꾸준히 진행한 회고의 힘인 것 같아요. 저도 벨로그를 활용하면서 회고를 최근에 시작했는데, 회고의 매력에 빠져들었습니다. 열심히 써서 선권씨 처럼 이쁘게 생각을 정리하고 싶네요.
그리고 몹 프로그래밍은 저도 꼭 참여해보고 싶네요! 정말 매력적인 공부 방법인 것 같아요!