어느덧, 벌써 3주 차 미션이 끝났네요.
2주 차 미션과 비교하면 훨씬 어려운 수준이긴 했지만, 그만큼 구현하는 재미가 있었습니다.
제출하고 나면, 부족했던 부분들을 정리하는 시간을 가지는데, 이 시간이 이번 미션에 많은 도움이 되었습니다.
정말 돌아보는 시간이 중요하다는 것을 다시 한 번 깨닫게 되는 것 같아요.
이번 미션은, TDD를 도전해 봤습니다. 2주 차 때, 운좋게 박재성님의 TDD 리팩토링 강의를 접하게 되었는데, 이번 프로그램 요구사항에 "테스트 공간을 확보하라" 라는 요구사항을 보며 TDD를 해봐야겠다고 생각했습니다.
못했던 점
정말 TDD를 하면서, 도메인을 분석하는 시간이 정말 중요하다는 것을 느꼈습니다.
그냥 도메인을 나누는 것이 아니라, 정말 정말 정말 세세하게 해당 도메인이 무슨 역할을 가져야 할 지, 어떤 것을 원시값 포장해야 할 지 모두 나눠야 합니다. 처음에 도메인 분석을 제대로 하지 않고 시작했다가, 3번 갈아엎었습니다..... 정말 막막했어요......
도메인 분석
* RandomNumberGenerator: 숫자 생성을 위한 인터페이스
* LottoNumberGenerator: 로또 숫자 생성을 위한 구현체
* Lotto: 로또 숫자를 관리하기 위한 일급 컬렉션 객체
* Lotos: 로또들을 관리하기 위한 일급 컬렉션 객체
뭐 이것 말고도 Number, CarName 도 하나 하나 원시값 포장하였고, 이런 식으로 도메인 분석을 하였습니다.
그리고 위에 보이시는 그림이 최종적으로 제가 작성한 도메인입니다.
이렇게 도메인 분석을 하고 나니, 확실히 TDD를 통해 구현하는 것이 쉬웠습니다. 분석 조차 제대로 하지 않고 코드를 작성할 때랑 확연히 달랐습니다.
좋았던 점
TDD를 구현하며 가장 좋았던 점은, 확실하고 반복적인 예외 테스트를 통해 코드가 견고해진다는 점입니다. 이전 미션이나 제가 해왔던 작성했던 코드들은, 어느 부분에서 오류가 발생할지 확실히 몰랐기에, “오류가 발생하면 리팩토링 해야겠다”라는 마음으로 구현했던 것 같습니다.
하지만 TDD를 통해 미리 오류가 발생할 것 같은 부분을 미리 방지하며 코드를 작성하니, “어떻게 구현해야 할지”에 중점을 두며 작성할 수 있었습니다. 이렇게 구현 방식에 집중하며 구현하니, 코드가 자연스레 견고해졌고, 리팩토링의 본질은 오류 처리가 아니라는 것을 자연스레 깨달았습니다. 이미 오류가 발생할 것 같은 부분은 TDD를 통해 미리 예방하니, 코드의 가독성과 객체지향 설계에 집중을 할 수 있었고, 해당 과정을 통해 박재성 님이 말씀하신 리팩토링의 의미를 조금은 이해하게 되었습니다.
실제로 TDD를 통해, 예외 처리를 미리 해결하니, 리팩토링에 많은 시간을 쏟을 수 있었습니다. 이번 미션에서는 객체의 책임을 작은 단위로 나누기 위해 노력하였고, 해당 객체가 어떤 책임을 가져야 할지에 대해 고민하며 로직을 작성하였습니다. 따라서, 완벽하지는 않지만 작은 단위로 객체를 나누며 리팩토링할 수 있었습니다.
또한, 가독성 부분에서도 많은 신경을 썼습니다. camp.nextstep.edu.missionutils 패키지의 코드 스타일을 참고하여, 매개 변수가 많은 경우 가로로 나열하기보다는 세로로 배치하여 가독성이 좋게 하였습니다. 이처럼 TDD를 통해 이전에는 경험하지 못했던 심도 있는 리팩터링을 할 수 있었습니다. 다만, 기능 구현 목록대로 커밋 하라는 프로그램 요구사항이 있어, 세세한 리팩터링 과정을 커밋 내역으로 드러내지 못한 점이 굉장히 아쉬웠습니다.
TDD를 도전해보며 테스트 코드에 대한 생각을 바꾸다.
TDD를 통해 테스트 코드를 작성하는 이유에 대해서도 다시 한번 생각해 볼 수 있었습니다.
사실 저에게 있어서 테스트 코드는 예외 처리를 검증하는 용도로만 사용하였습니다. 주로 구현을 끝마치고, 단위 테스트를 통해 여러 예외 상황을 테스트하여 리팩토링하고, 마지막으로 통합 테스트를 통해 실제로 잘 작동하는지 검증하였습니다. 하지만 로또같이 복잡하고 다양한 예외 상황이 주어지는 문제를 마주하니, 제가 했던 방식에는 한계가 있다는 것을 느꼈습니다.
구현을 끝내고, 단위 테스트로 예외 처리를 검증하여 리팩토링 하는 것은 불가능에 가까웠습니다. 구조가 복잡했기에, 어느 하나가 바뀌면 연관된 모든 부분이 바뀌어야 했습니다. 또한, 제가 기대한 반환값과 실제 반환값이 일치하지 않는 경우도 많았습니다. 만일 작은 단위부터 테스트하지 않고, 객체를 구현했다면, 모든 부분을 수정해야 했을 것입니다.
해당 경험을 통해, 작은 단위부터 테스트 코드를 구현하여 검증하는 것이 필수라는 것을 뼈저리게 느꼈고, 왜 TDD를 해야 하는지 느낄 수 있었습니다. 로또 미션을 통해, 테스트 코드 작성이 더 많은 시간과 비용을 아껴준다는 점을 깊이 체감하게 되었고, 꼭 제대로 배워보고 싶다는 생각이 들었습니다.
지금까지 너무 TDD에 관한 이야기만 한 것 같아요. 그만큼 정말 TDD 해보려고 목숨 걸었습니다.
그리고 TDD 이외에도 정말 느끼고 배운점이 많았습니다.
항상 긍정적으로 생각하자.
지난주 PR을 보며, 많은 생각이 들었습니다. 1주 차 보다 어려운 미션을 진행함에 따라, 정말 잘하시는 동료분들의 코드가 돋보였고 개중에는 제가 이해할 수 없는 방식이 정말 많았습니다. 부족한 제가 보았을 때도 '이분들은 이미 취업하신 게 아닐까?’하는 생각이 들 정도로 잘하시는 분들이 많았고, 그런 분들을 보며 자신감도 떨어지고 다소 우울했습니다.
그러나 2주 차 피드백인 “조바심을 버리고 어제의 나와 비교하며 발전하라”라는 문구를 보며 우울감을 떨쳐 내고 긍정적으로 생각하고자 하였습니다. 이렇게 뛰어난 동료들 사이에서 학습하는 환경이야말로 제가 진정으로 원하는 환경이고, 이 속에서 더 나아지기 위해 꾸준히 정진해야겠다는 결심이 생겼습니다. 또한, 실력 있는 동료들이 공유해 주시는 자료와 지식을 배우고 적용할 수 있는 기회는 정말 흔치 않은 경험이라는 것을 깨달았습니다. 실제로 디스코드 채널을 통해 static 키워드를 통해 상수를 선언하는 이유나 객체 의존 다이어그램을 쉽게 그리는 법 등 여러 유익한 자료를 공유 받으며 한층 더 성장하였습니다. 그렇기에, 의기소침하기보다는 적극적으로 학습해야겠다는 생각이 들었습니다.
이렇게 긍정적으로 생각하고 나니, 우울감보다 의욕이 더 앞섰습니다. 비단 실력을 발전시키는 것 뿐만 아니라, 저 역시 동료들처럼 다른 이들에게 도움이 되고 싶다는 생각도 들었습니다. 이처럼 간접적이지만, 디스코드 채널을 통해 우아한테크코스가 지향하는 “협력” 중심의 학습 환경을 체험하고 나니, 자신의 발전뿐만 아니라 여러 방면에서 느끼는 것이 많은 것 같습니다. 꼭 이 과정에 참여하여 함께 성장하고, 나눌 줄 아는 개발자가 되고 싶다는 목표가 생겼습니다.
Getter 를 지양하기 위해 노력하다.
2주 차 코드를 보니, getter 사용을 지양하라는 원칙을 제대로 지키지 않았습니다. 해당 원칙에 대해서 간과하고 있었고, 왜 지양해야 하는지조차도 제대로 몰랐습니다. 그래서 이번 주차에는 해당 부분을 완벽하게 이해하고자 하였습니다.
로또의 상태를 출력하기 위해, List<Lotto> lottos를 필드로 가지고 있는 Lottos 클래스에서 해당 값을 반환받아야 했습니다. 하지만 원본 값을 그냥 반환할 경우, 외부에서 값을 조작하여 출력을 조정할 수 있었기에 많은 고민이 들었습니다. 해당 고민을 해결하기 위해 방어적 복사를 학습하여 적용하였습니다.
먼저, new ArrayList<>(lottos) 형태로 감싸서 복사본을 제공하도록 하였습니다. 이 경우, 원본 값을 외부에 노출시키지 않기에 안정성을 보장할 수 있었습니다. 다만 복사본을 조작할 수 있었고 또한, 원본 데이터가 달라지면 값이 같이 달라져야 하는데, 이 복사본은 원본과 연결이 끊긴 별도의 객체이기에, 달라진 데이터를 반영할 수 없었습니다.
해당 문제를 해결하기 위해, 불변 리스트를 학습하여 적용하였습니다. Collections.unmodifiableList(lottos) 형태로 불변 리스트를 반환하니, 원본 값을 수정할 수 없어서 안정성이 유지되었습니다. 또한, 원본 데이터에 변화가 생기면 그대로 반영되기 때문에, 해당 방식이 getter를 사용하면서도 getter를 지양해야 하는 이유를 해결한 가장 좋은 방식이라고 생각하여 적용하였습니다.
또한, DTO를 활용하여 원시값을 보호하도록 하였습니다. 원시값 포장 객체의 경우, 로직 구현을 위해 어쩔 수 없이 getter를 사용하여 원시값을 반환하여야 했습니다. 따라서, DTO와 Mapper를 사용하여 컨트롤러 단에 생기는 원시값들을 모두 포장하여, 객체의 의미를 명확하게 하고 객체를 보호하도록 하였습니다.
마지막으로, 캡슐화할 수 있는 로직은 모두 캡슐화하였습니다. 당첨 번호를 맞추는 로직에서, 같은 숫자를 가지는지 확인해야 할 필요가 있어서 Number 객체에 getter를 구현하였습니다. 하지만 리팩토링을 통해, equals 와 hashcode 메서드를 Number 객체에 구현하였습니다. 이를 통해 객체가 올바른 책임을 가지게 하고, 원시 값의 불변성을 보장할 수 있었습니다.
이처럼 이번 미션에서 무작정 getter를 지양하는 것이 아니라, 지양하는 근본적인 이유를 파악하였습니다. 그리고, 꼭 필요할 때 getter를 활용하되, 원본 값의 불변성을 보장하여 사용할 수 있도록 많은 노력을 기울였습니다.
어느덧 4주 차입니다. 지난 3주가 시험 기간이랑 겹쳐 정말 바빴지만, 시험 하루 전 날에도 프리코스를 하는 제 자신을 보며, 제가 진정으로 재밌어하고 좋아하는 것이 프로그래밍이라는 것을 느꼈습니다. 앞으로 마지막 일주일이 남았는데, 정말 불태워보겠습니다.
https://github.com/woowacourse-precourse/java-lotto-7/pull/483
'우아한테크코스[프리코스]' 카테고리의 다른 글
[4주 차] 회고 (1) | 2024.11.12 |
---|---|
[3주 차] 정적 팩토리 메서드 (4) | 2024.11.05 |
[3주 차] TDD (1) | 2024.11.05 |
[2주 차] 상수에 static 선언을 하는 이유에 대하여 - 1주 차 코드리뷰 (3) | 2024.11.05 |
[2주 차] 회고 (1) | 2024.11.05 |