우아한테크코스를 시작한지 벌써 1달이 됐습니다!
어느새 과정 10%를 지나고 있네요!
1레벨의 절반이 지났고, 자동차 경주와 로또 미션을 진행했습니다.
지난 두 가지 미션에서 배운 것들을 요약해봅니다.
🏎️ 자동차 경주 - TDD
https://github.com/woowacourse/java-racingcar/tree/hj-rich
제목에서 알 수 있듯, 자동차 경주는 TDD에 익숙해지는 과정입니다.
우테코에서 처음으로 하게되는 미션이자, 첫 페어 프로그래밍을 경험하는 미션이기도 했어요.
그리고 본격적으로 TDD를 처음으로 경험하는 시간이기도 했습니다.
간단히 돌아보자면..
미션 제목대로 TDD에 상당히 많이 익숙해지는 시간이었던 것 같아요.
요구사항 분석 -> 도메인을 설계 -> 테스트코드 작성 -> 리팩토링
이 사이클이 이제 더 익숙해졌어요!
그럼 자동차 경주 미션을 통해 제가 알게된, 혹은 생각하게된 것들을 정리해볼게요!
리뷰어분들의 공통 피드백을 수집한 것에 추가적으로 제 생각도 들어있으니 참고해주세요~
1. 테스트 코드 작성 시, 메서드명을 한글로 적기보단 @DisplayName을 활용하면 어떨까요?
한글로 작성하면 아스키문자가 아니라는 경고가 발생해요.
경고 억제를 위해서는 해당 경고를 무시시키는 애너테이션을 추가해야 합니다.
@DisplayName 애너테이션을 사용하면, 경고를 발생시키지 않으면서도 한글로 표기할 수 있어요.
경고를 발생시키지 않으면서도 필요를 충족시킬 수 있는 방법이 있는데
굳이 경고를 무시하도록 처리하는 것 보다는 한 줄 더 작성하는 게 저는 더 좋은 것 같아요.
추가적으로 @ParameterizedTest 애너테이션을 사용하는 경우에도,
@DisplayName과 더불어
속성값으로 name : "자동차 전진 테스트 : {0}" 와 같이 전달하여 상세 케이스에 대한 네이밍도 함께 해주면 좋아요.
2. 하나의 테스트 메서드엔 하나의 assert문만 사용해요
여러 개의 assert문이 필요한 경우, 테스트 메서드를 분리해야 하는 상황이 아닌지 확인해야 해요.
만약 꼭 여러 개의 확인이 동시에 필요할 경우, assertAll을 사용하면 됩니다.
3. 가급적 junit 4와 5중 하나를 통일감 있게!
assertThat과 assertTrue 를 혼용하기 보다는,
assertTrue를 assertThat().isTrue()로 사용하여 통일감을 유지하면 어떨까요?
개인적으로 assert core j 의 문법이 가독성이 제일 좋은 것 같아요.
4. 애플리케이션 Flow 순으로 구현할지, 핵심 비즈니스 로직에 바로 접근할지?!
핵심 비즈니스로직을 먼저 할 수 있다면 그걸 권장한다.
단, 그러기 어렵다면 작게 쪼개어 접근하거나, 아니면 순차적으로 해도 된다.
by 네오
위 내용은 네오 코치의 이야기였는데, 추후 로또 피드백 강의중 관련 언급이 한 차례 더 있었어요.
바로 핵심 비즈니스 로직에 바로 접근하는 것이 도메인 지식을 높여준다는 점!
자동차 경주 이후 로또를 들어가며, 클래스 다이어그램을 페어와 그리고,
설계를 공유한 뒤에 코딩을 시작하면 중간에 설계 이해 차이로 인한
비효율을 줄일 수 있을 거라 기대했었어요.
그런데 실제 구현 과정에서 어떤 형태의 반환 값을 주고 받을 지에 대해
구현 과정에서 어려움이 있었어요.
어떤 메시지를 보내고 어떤 반환값을 받을지에 대해서까지 미리 설계하는 것은
다소 어려움이 있지 않을까 생각했었고, 그러나 그 흐릿함이 내키지 않았었는데..
테스트코드 자체를 그 코드를 사용하는 클라이언트 입장에서 테스트코드를 top-down으로 작성해버리니
난 이런 파라미터를 전할 거고, 이렇게 응답해줘 라고 아주 간단히 설계가 진행됐어요.
물론 항상 통용되는 건 아니겠지만, 이러한 클라이언트 관점에서의 TDD 접근은
분명 도메인에 대한 지식을 높여준다는 점에서 엄청난 장점이 있다고 생각해요.
5. private 메서드 테스트 필요성에 대해
테스트는 구현이 아닌 의도를 검증해야 한다
by 네오
일단 이 사이트 참고... ㅋㅋ 개발자도구로 주석도 참고하세욤!ㅋㅋ
private 메서드는 클래스 내부에서만 사용되고, 이는 결국 public 메서드에서 호출되어 사용하게 됩니다.
외부로 오픈되어있는 public 이 정상 동작한다면 private에 대한 테스트도 포함된 것으로 이해해도 될 것입니다.
반면 package-private, reflection, mock 등을 사용해 일부 우회하여 private 메서드를 테스트하는 것은
필요 여부에 대해서도 의문이지만 그 자체로도 득 보다 실이 크다고 생각해요.
6. for-loop와 stream을 적절히 !
반복 횟수가 적고, 단순 작업을 반복할 경우에는 for-loop이 가독성이나 성능에서 유리할 수 있습니다.
반복 이후 최종 연산이 추가적으로 필요할 때에는 stream을 사용하는 것이 간결하고 가독성이 좋습니다.
7. 생성자에서는 방어적 복사를, getter에서는 unmodifiable 컬렉션 반환을
생성자에서는 new ArrayList<>() 또는 copyOf 를 이용해서 원본과 연결을 끊어냅니다.
getter에서는 이미 unmodifiable 하다면 그대로 반환해도 되고, 아니면 상황에 맞는 메서드로 반환합니다.
8. Random이나 Pattern 과 같이 생성 비용이 큰 객체는 상수화를 고려!
9. 비교, 정렬이 필요한 도메인 객체는 equals, hashCode, toString 오버라이드 필수!
10. 기본 정렬 기준이 필요할 땐 Comparable, 아니라면 Comparator를 사용해요~
11. 유틸성 클래스는 private 생성자를 통해 외부 인스턴스화를 방지해요~
12. FunctionalInterface 에는 꼭 @FunctionalInteface 애너테이션을 추가해서 안전하게!
13. 배열 보다는 List 를 활용해요!
14. getter 보다는 메시지를 보내서 객체에 능동성을 부여해요!
15. 컨트롤러는 상태값을 갖지 않아야 스레드 세이프할 수 있어요
16. 정규식은 해석에 어려움이 있을 수 있어요. 아주 복잡한 경우가 아니라면 다른 방법을 찾아봐요!
17. 유효성 검증은 어디에서 해야할까요?
저는 웹을 기준으로 생각하면 기준이 잘 떠올랐어요.
사용자에게 빠른 피드백을 줌과 동시에 불필요한 트래픽을 발생시키지 않기 위해,
프론트에서 기본적인 유효성 검사를 해요.
마찬가지로 컨트롤러에 입력값을 전달하기 전에,
최소한의 유효성 검사는 적절하게 View에서 가져가는 게 맞는 것 같아요.
그러나 도메인은 무조건 유효성 검사를 예외없이 수행해야 한다고 생각해요.
18. 상수를 몰아서 한 곳에 몰기 보다는 사용되는 클래스 내부에 배치해요
대부분의 리뷰어분들이 한 곳에 모으기 보다는 사용되는 곳에 배치하라고 조언하셨어요.
저 역시 사용되는 곳에서 가지고 있는게 관리에 용이할 거라고 생각해요.
그러나 중복 방지 등 특수한 상황에서 필요한 경우에는 물론 한 곳에서 관리할 수도 있어요.
19. static 은 유틸성 클래스에만 사용해보면 어떨까요?
static 으로 선언하면 객체지향의 장점을 활용하지 못하게 됩니다.
모든 곳에서 공통적으로 사용 가능하며 도메인 로직이 아닌 기능이 곧 유틸인데요,
유틸에 대해서 private 생성자로 인스턴스화를 막고, static 메서드를 사용하는 게 좋을 것 같아요.
💸 로또 - OOP
https://github.com/woowacourse/java-lotto/tree/hj-rich
1. 재사용 가능한 객체라면, 캐싱 처리를 생각해볼까요?
1에서 45까지의 값을 포장하는 VO LottoNumber는 매번 새로 생성되지 않아도 돼요.
Map<Integer, LottoNumber> 자료구조를 이용해서
int로 요청을 보내면 그 숫자를 포장한 LottoNumber로 반환하는 방식을 생각해볼 수 있어요.
추가적으로 미리 모두 만들어두는 게 아니라,
요청에 오면 그때 만들고 그때부터 캐싱을 하는 방식도 생각해볼 수 있어요.
2. 자료 구조의 특성을 잘 이용해보아요!
HashSet, TreeSet은 순서를 보장하지 않고 LinkedHashSet은 순서를 보장해요. 대신 메모리가 더 필요해요.
그러나 로또 번호 출력 시, 무조건 오름차순 정렬해서 보여주게 되어 있으므로, 순서 보장은 불필요해요.
TreeSet은 요소 추가시마다 오름차순으로 자동정렬하기 때문에 TreeSet<LottoNumber> 로 구성하려면,
LottoNumber는 반드시 Comparable을 구현해야 해요.
3. 핵심 비즈니스 로직에 대해서는 가능하다면 모든 케이스를 테스트해봐야 해요
가령 로또 미션이라면, 낙첨부터 1등까지 모든 케이스를 테스트 해봐야해요.
특히 2등과 3등 구분은 약간 미묘한데, 테스트 케이스 작성을 통해 오류를 미리 확인할 수 있어요.
4. Key가 Enum인 Map 자료구조가 필요하다면, 꼭 EnumMap을 사용해요
EnumMap은 Enum을 Key로 사용하기 위해 특화되어 있어요. HashMap에 비해 성능적 이점이 큽니다.
5. stream으로 결과를 집계할 때 groupBy, LongSummaryStatistics를 고려해봐요
조건을 전달하면 조건에 맞춰서 그룹화, 통계화를 자동으로 처리해줘요.
6. Objects.requireNonNull 메서드는 추후 수정에 유연하지 못할 수도 있어요.
단순히 NPE를 던지고 메시지를 전달하는 역할밖에 수행하지 못해요.
나중에 단순히 NPE를 던지는 게 아니라 다른 처리를 하려면 결국 해당 코드를 아예 제거하고
새로 코드를 작성해야만 해요.
아예 null을 검증하는 메서드를 분리하는게 추후 수정에 유연할 수 있어요.
7. 큰 수에 대해서는 세자리 마다 언더스코어를 활용해서 가독성을 높여요! 1_000 !
8. MVC 패턴에서 핵심은 도메인은 View를 모르고, View도 도메인을 모르는 거에요
의존이 생기면 수정에 유연하지 못하게 돼요.
컨트롤러가 중간에서 데이터를 전달하도록 구성함으로써
도메인은 View를 의존하지 않게 구성해야 해요.
View 역시 도메인을 직접 받기 보다는 DTO나 원시값을 받아야 해요.
그러나 도메인이 DTO를 의존하는 건 좋지 않아요.
컨트롤러 혹은 제 3의 객체가 도메인의 반환값을 DTO로 변환해주는 게 좋을 것 같아요.
9. 현재 시점에 필요한 비즈니스를 표현해요! - by 루피
현재 시점에 필요한 비즈니스에 집중해요. 미리 나중에 생길 변경을 구현할 필요가 없어요.
유연성은 설계로만 대응하는 것이고, 실제 구현은 현재의 필요에 집중해요.
루피로부터 이 이야기를 듣기 전까지 저는 설계와 구현을 분리해서 생각하지 못했어요.
미래의 변경은 설계로 대응한다 라는 말이 현재의 비즈니스에 더욱 집중하게 해주는 것 같아요.
10. 향후 변경이 예상되는 부분에 대해서는 인터페이스 적용을 고려해봐요
가령 로또 생성 로직을 예로 들면,
수동로또는 사용자로부터 로또 숫자 6개를 입력 받아 처리하게 되고
자동로또는 아무것도 받지 않아도 돼요.
그러나 그 이후 프로세스는 동일해요
이 경우 변경이 일어나는 곳과 아닌 곳을 분리할 수 있어요.
또한 이미 이렇게 변경이 일어나는 곳이 한 번 감지되었다면, 이곳은 추후 또 변경될 수 있음을 의미해요.
따라서 숫자를 입력 받는 로직만 추상 메서드로 갖는 인터페이스를 정의하고,
나머지는 이미 구현부를 가진 static 메서드로 구현할 수 있어요.
그리고 구현체에서는 6개의 숫자를 사용자에게 전달받을지, 랜덤 생성할지만 선택하면 돼요.
이 때, 랜덤생성은 매개변수가 불필요하고, 사용자에게 전달받을 땐 필요하게 되는데요,
메서드 시그니처에서는 메서드를 명시하지 않더라도 이를 전달할 수 있어요.
구현체의 생성자에 파라미터로 받아 필드에 주입한 뒤,
오버라이드 한 메서드에서 그 필드 값을 사용해서 이후 로직을 수행하면 돼요.
11. 도메인으로 분류되는 객체들에 대해서는 빌더 패턴은 지양해요
자동차 경주 -> 로또 개선 시도 회고
1. 클래스 다이어그램으로 설계를 합의한 이후에 코드 작성하기
설계에 대해 먼저 협의한 이후에 코드를 작성해야,
구현 중간에 생기는 서로간의 설계 이해 차이로 야기되는 비효율을 줄일 수 있을 거라 생각했어요.
추가적으로 함께 자라기에서도 중간 매개를 두고 소통할 때
추상화된 규칙을 발견할 확률이 증가한다고 했고요.
결론적으로 이 시도는 매우 성공적이었어요.
뿐만 아니라 후디가 아주 꼼꼼해서 중간중간 설계 변경시마다 클래스 다이어그램도 즉시 수정했어요.
다음 페어 프로그래밍에도 꼭 설계 관련 공유를 먼저 진행할 수 있으면 좋을 것 같아요.
2. 충분한 아이스 브레이킹, 오프라인 페어 프로그래밍
후디와는 정말 더이상 맞을수가 있나 싶을 정도로 스타일이 잘 맞았어요.
저는 완성도를 너무 쫓기 보다는 1차 PR의 완성도를 다소 낮추더라도,
즐겁게 함께 코딩하고, 1차 PR 이후에도 지속적으로 상호 공유하며 학습하길 선호했는데요,
감사하게도 후디도 저의 뜻과 일치해서 충분한 아이스 브레이킹 타임을 가질 수 있었어요.
덕분에 그 이후 시간이 더 즐겁게 진행될 수 있었습니다.
앞으로도 충분히 아이스 브레이킹을 할 수 있으면,
더 나아가 후디와의 로또 미션 때처럼 오프라인 페어 프로그래밍도 할 수 있으면 좋을 것 같아요.
3. 마감 시한 및 완성도에 대한 합의
시작할 때부터 후디와 1차 PR 희망 시점에 대해 협의했고,
이를 위해 어느 시점에 헤어질지, 헤어지기 전까지 어느정도 완성도로 구현할지를 협의했어요.
언제까지 헤어진다, 어느 정도 완성도까지 구현한다 라는 협의를 먼저 진행하고 나서,
그 다음에 코딩 시간에 대해 협의했어요.
물론 모든 요구사항을 만족할 때까지 완성하는 건 기본이지만,
리팩토링은 끝이 없고 또 개인차가 크기 때문에,
굳이 100% 상호 동의한 최고의 코드를 끝까지 지향하며 전력질주 하기보다는
함께 즐겁게 코딩하고 싶었어요.
이 부분도 후디와 협의가 잘 되었고 결과적으로 아주 잘한 결정이었어요.
다음에도 시도해보려고 합니다.
4. 리뷰 요청 시, 코드와 함께 질문 드리기!
5. Top - down TDD를 통한 도메인 지식 높이기
후디와 설계를 마친 뒤 가장 먼저 시작한 게 LottoNumber 였어요. 가장 작은 단위였죠.
구조상 로또 넘버 -> 로또 -> 로또스 이렇게 순차적으로 만들어질 수 있기 때문이었어요.
그런데 네오의 로또 피드백 강의를 보니 네오는 즉시 숫자를 던져서 결과를 받는 테스트 코드로 시작했어요.
어느 쪽이 항상 맞는 정답이다 라는 건 없겠지만 저에겐 충격이었어요. 상상도 못한.. ㄴㅇㄱ
로또 페어 프로그래밍 중 가장 어려웠던 부분이 당첨 결과 집계값을 어떤 자료형으로 반환할 것이냐였는데
만약 네오처럼 Top-down 으로 클라이언트 입장에서 테스트코드를 작성하면서 시작한다면,
어떤 반환값이 나오면 사용자 입장에서 편한지 처음부터 정하고 내려갈 수 있었어요.
아마 앞으로도 작은 단위부터 TDD를 하는 게 아무래도 도 많은 경우에 자연스러울 것 같긴 해요.
그치만 Top-down, 클라이언트 입장에서의 테스트 코드 작성은 다음 미션에서 꼭 유념하고 시도해보고 싶어요.
6. 예습....
이건 장단점이 있긴 한데..
미리 해보지 않고 들어가서 부딪히면서 현재의 내가 작성할 수 있는 코드를 모두 한 차례 쏟아내고 싶다.
그 과정에서 페어 프로그래밍도 미리 해보지 않고 경험해보고 싶다.
이런 생각이었었는데요, 학습 효율을 봤을 때 아무래도 다른 방법을 시도해봐야할 것 같아요.
미션 시작전에 혼자서 한 번 구현을 미리 해봐야할 것 같아요.
그리고 고민도 해보고, 이전 기수분들의 PR도 보면서 공부한 뒤에,
그다음 페어 프로그래밍을 들어간다면 학습효율이 더 좋지 않을까 생각해봤어요.
어느새 우테코를 시작한지 1달.. 10% 경과했습니다!
남은 9달도 화이팅!!
'우아한테크코스 4기' 카테고리의 다른 글
객체의 행동으로 표현되는 책임과 역할 (객체지향의 사실과 오해) (2) | 2022.03.25 |
---|---|
💻 코딩을 지탱하는 기술을 읽었습니당 ! (4) | 2022.03.08 |
컬렉션의 복사 방법을 정리해봅시다! (unmodifiable view / list) (6) | 2022.02.28 |
for-loop vs IntStream, 그리고 멀티 스레딩 (4) | 2022.02.28 |
EnumMap, 니가 그렇게 빨라?? (4) | 2022.02.26 |