1주차에는 오버엔지니어링하지 않고 직관적이고 간결한 코드를 가져가고자 했다. 그 결과 다른 사람들에 비해 짧은 208 line이 나왔다.
다른 분들은 500줄~1000줄, 길면 1500줄까지도 작성하셨던데 상대적으로 초라하게 느껴지기도 한다. 그래도 라인 수가 전부는 아니라고 생각하고, 코드가 간결하고 직관적이어서 좋았다는 리뷰도 종종 달렸기에 스스로 칭찬해주고 싶다.
그래도 여기에 안주하지 않고 더 성장하기 위해 1주차 과제에서 받은 피드백들을 정리해보려 한다.
이번 과제에서 가장 많이 받은 피드백은 메서드나 클래스 분리가 미흡하다는 부분이었다. 아무래도 간결하게 작성하고자 한 내 목표와 상충하는 내용이기 때문에 관련 피드백이 많이 나온 것 같다.
특히 특정 클래스가 너무 많은 책임을 지고있는 것 같다는 피드백이 주로 나왔다. 여기서 내가 관심사에 따른 분리를 적절하게 해내지 못했다는 점을 체감했다.
과제가 간단했던 만큼 테스트에는 큰 신경을 쓰지 않았다. 필요한 최소한의 예외처리만을 추가 테스트로 작성해 진행했고, 이마저도 메서드를 분리하지 않고 케이스 종류를 주석으로 명시한 채 간략하게 작성했다.
@Test
void 정상_케이스() {
assertSimpleTest(() -> {
// 일반적인 입력
run("1,2,3");
assertThat(output()).contains("결과 : 6");
// 구분자가 연속으로 주어지는 경우
run("1::2::3");
assertThat(output()).contains("결과 : 6");
// 숫자 없이 구분자만 연속으로 주어지는 경우
run(",,,,");
assertThat(output()).contains("결과 : 0");
// 커스텀 구분자와 기본 구분자를 혼용하는 경우
run("//;\\n1,2:3;4");
assertThat(output()).contains("결과 : 10");
// 숫자를 커스텀 구분자로 사용하는 경우
run("//1\\n21212");
assertThat(output()).contains("결과 : 6");
});
}
여기에도 많은 피드백을 받았다.
@ParameterizedTest
활용하여 여러 인자에 대해 테스트해보기@DisplayName
)에 테스트 정보 명시하기과제 진행 과정에서 String을 합쳐야 하는 일이 있으면 간단하게 +
연산자를 통해 진행했다. 하지만 문자열의 덧셈은 큰 비용이 들기 때문에 StringBuilder
를 활용해보라는 피드백을 받았다(참고 자료). 어렴풋이 알고 있었는데 이렇게 큰 차이가 날 줄은 몰랐다. 하지만 리뷰어들끼리도 의견이 갈려서 어떤 분은 간단한 경우 +
를 사용해도 무관하다거나, String.format
을 사용해도 좋겠다는 이야기도 나왔다. 다들 정말 유익한 인사이트를 제공해줘서 이 부분은 상황에 따라 잘 활용해봐야겠다.
그 외에도 수많은 피드백을 받았기에 그 내용을 간단하게 정리하면 다음과 같다.
문자열 계산기
와 계산기
는 다른 것!)많은 사람들의 코드를 리뷰하면서 생각해보지 못한 유익한 인사이트도 많이 얻을 수 있었다.
과제 수행에 들인 시간을 기록한 사람이 있었다. 과제 해결까지 얼마나 긴 시간이 걸렸는지 객관적으로 확인할 수 있는 좋은 방법이라고 생각한다.
예외 메시지나 출력 메시지를 상수화하는 것뿐 아니라 enum화까지 진행한 사람들이 많았다. 확실히 관심사가 특정 클래스에 밀집되어 훨씬 보기 좋았던 기억이 있다.
불변 객체에 대해 record를 활용한 사람도 있었다. 간단한 불변 객체에 대해서는 record를 활용해보는 것도 좋아보인다.
객체 생성 비용을 고려하여 일반 생성자를 제한하고 싱글톤 패턴으로 작성한 사람이 있었다. 싱글톤 패턴에 단점이 많다고는 하지만 명확한 근거에 기반해 활용한다면 좋은 패턴이 될 것 같다.
2주차에는 1주차에서 받은 피드백을 적극적으로 반영해보기 위해 노력했다.
관심사에 맞게 여러 클래스로 분리하고자 했다. 사소한 것도 놓치지 않기 위해 원시값 사용을 최대한 지양하고 별도 클래스로 wrapping해 사용했다. 이 과정은 각 클래스가 자신의 관심사에 집중하는 데 큰 도움이 되었다.
테스트를 꼼꼼하게 진행하기 위해 이번 과제에서는 TDD를 적용해보았다! 여기에 대한 후기는 잠시 후에 다시 다루겠다.
이번 과제에서는 메시지에 특정 변수 값을 포함해야 하는 경우 String.format
을 적극적으로 활용했다.
String.format("자동차 이름은 %d자 이하만 가능합니다.%n", carNameLength)
이번 과제를 진행할 때는 요구사항에 따라 구현해야할 리스트를 README에 기록해두고, 하나씩 구현하며 커밋했다. 그리고 각 작업단위를 완료할 때마다 시간이 얼마나 걸렸는지 구체적으로 명시했다. 시간을 기록하면서 진행하니 내 작업 속도에 대해 메타인지를 할 수 있어서 좋았다.
## 구현할 기능 목록
- [x] 테스트 작성
- [x] 자동차 이름 구분 (5분)
- [x] 랜덤 값에 따른 전진 여부 확인 (10분)
- [ ] ...
- [x] 기능 구현
- [x] 자동차 이름 구분 (5분)
- [x] 랜덤 값에 따른 전진 여부 확인 (5분)
- [ ] ...
각종 메시지를 enum으로 분리하면서 이전보다 훨씬 유연하고 확장에 열린 구조가 된 것 같다.
public enum ExceptionMessage {
CAR_NAME_NOT_ALLOW_SPACE("자동차 이름에는 공백이 올 수 없습니다."),
CAR_NAME_LENGTH_MUST_BE_LESS("자동차 이름은 %d자 이하만 가능합니다.%n"),
TRY_COUNT_MUST_BE_NUMBER("시도 횟수는 숫자여야 합니다."),
TRY_COUNT_MUST_BE_POSITIVE("시도 횟수는 양수여야 합니다."),
;
private String message;
ExceptionMessage(String message) {
this.message = message;
}
public String getMessage() {
return String.format(message);
}
public String getMessage(int carNameLength) {
if (this.equals(CAR_NAME_LENGTH_MUST_BE_LESS)) {
return String.format(message, carNameLength);
}
return String.format(message);
}
}
이번 과제에서는 Car
객체를 View로 넘겨주고 싶었는데, 도메인 객체를 그대로 넘기기에는 우려되는 점이 많았다. 그래서 DTO를 만들었는데 record가 불변 객체로써 잘 활용되었다.
public record CarDto(String name, int position) {
}
이번 과제를 진행하면서 가장 큰 목표로 잡은 점은 개발과정에 TDD를 적용해보는 것이었다. 작년 프리코스 때도 TDD를 적용해본 적이 있었는데, 개발 속도가 현저히 느려지고 그 장점을 찾기가 힘들었다.
우아한테크코스에서 제공받은 프리코스 1주차 공통 피드백을 확인해보니 우아한테크코스 내에서 코치님께서 진행한 강의 동영상이 수록되어 있었다. 이 영상을 시청하면서 아 테스트는 그렇게 대단한 게 아니구나..!
라고 느꼈다.
영상 속의 코치님은 테스트 작성을 깊게 고민하기보다는 실제 구현이 잘 이루어졌을 때 기대하는 바를 확인해보는 정도로만 작성하셨다. 비즈니스 로직을 먼저 구현하고 이후에 테스트 코드를 작성하셨지만 비즈니스 로직을 작성하면서 유기적으로 테스트를 추가하는 모습이 나에게는 TDD처럼 느껴졌다.
제일 먼저 구현할 기능 목록을 리스트업했다. 그리고 각 기능의 구현을 커밋 단위로 가져갔다. 또한 각 커밋마다 걸린 시간을 기록하여 내가 어떤 기능을 구현하는 데 긴 시간이 소모되었는지, 과제 해결까지 총 걸린 시간은 얼마나 되는지 정량적인 지표로 확인할 수 있었다.
## 구현할 기능 목록
- [x] 테스트 작성
- [x] 자동차 이름 구분 (5분)
- [x] 랜덤 값에 따른 전진 여부 확인 (10분)
- [x] 승자 판정 (15분)
- [x] 잘못된 값 입력 시 예외 처리 (10분)
- [x] 기능 구현
- [x] 자동차 이름 구분 (5분)
- [x] 랜덤 값에 따른 전진 여부 확인 (5분)
- [x] 승자 판정 (5분)
- [x] 잘못된 값 입력 시 예외 처리 (35분)
- [x] 입출력 적용 및 로직 연결 (35분)
최소 기능 제품(MVP) 완성까지 걸린 총 시간: 130분
다른 사람들의 블로그를 통해 이전 기수의 우아한테크코스 최종 테스트 메일을 보면 이런 내용이 있다. (출처. 우테코 5기 AN(안드로이드) 최종코딩테스트 후기)
안 돌아가는 프로그램보다 돌아가는 쓰레기를 만들어라.
그런 다음 클린 코드, 리팩터링, 테스트를 챙기는 것이다.
이번 과제 구현은 철저히 위 내용을 지키고자 했다. TDD로 요구사항을 명시하고, 이를 구현하기 위한 최소한의 코드만을 작성하고, 이 과정을 반복하여 구현을 끝냈다. 작성하면서도 코드가 너무 쓰레기같다고.. 스스로 생각하면서 웃음이 나오기도 했다. 하지만 왠지모를 자신감이 들었다. 리팩토링에 대한 고민없이 구현에 온전히 집중하니 술술 풀리듯이 풀어나갈 수 있었다.
작년에는 TDD를 적용하면서 개발 시간이 굉장히 길어졌는데, 이번에는 130분이라는 짧은 시간만에 요구사항을 충족하는 최소한의 구현을 끝낼 수 있었다.
과제 해결 과정에 TDD를 적용해보니 TDD의 장점이 정말 크게 체감되었다. 내가 느낀 장점은 크게 두 가지가 있다.
이번에 TDD를 진행할 때는 기능 하나를 테스트로 만들고, 이게 동작하도록 만들고, 이 과정을 반복했다. 그랬더니 테스트를 통과시키기 위해 정말 최소한의 코드를 작성할 수 있었고, 왔다갔다 생각나는 대로 구현하던 이전과 달리 각 기능의 구현에 온전히 집중할 수 있었다.
하나의 기능을 완성하면 기능 구현 목록을 확인하며 다음 기능의 테스트를 작성하면서 개발을 진행하니 지금까지 어떤 기능들을 구현했는지, 앞으로 어떤 기능을 구현해야 하는지 방향을 잡아나가는 데 큰 도움이 되었다.
최소 기능 제품(MVP)을 완성한 후에는 리팩토링을 진행했는데, 리팩토링 후에 테스트 코드를 실행해보면 종종 실패하기도 했다. 이 때마다 테스트 코드의 소중함이 절실하게 느껴졌고, 리팩토링을 하는 데 있어 최소한의 안전장치 역할을 해준다는 것을 체감했다.
돌이켜보면 본격적으로 개발을 시작하기 전에 피드백 강의 영상을 시청한 것이 나에게 큰 도움이 되었다. TDD는 이름 그대로 테스트를 기반으로 개발을 진행해야 하기에 테스트를 어떻게 작성할지가 아주 중요하다고 생각한다. 테스트는 거창한 게 아니라 코드를 그저 테스트하려는 목적일 뿐이라는 걸 인지하고 개발하니 훨씬 부담없이 빠르고 수월하게 개발할 수 있었던 것 같다.
안돌아가는 프로그램보다 돌아가는 쓰레기를 만들자.
라는 생각을 가지고 개발에 임한 것도 큰 도움이 되었다. 위에서 언급한대로라면 테스트를 대충 짠 거 아니야?
라는 생각이 들 수 있지만 이 생각 덕분에 테스트를 편하게 작성할 수 있었다. 물론 테스트도 MVP가 완성된 이후 리팩토링을 거쳤지만 내가 하고싶은 말은 아래와 같다.
테스트는 테스트일 뿐 여기에 과한 시간을 투자하는 것은 좋은 방향이 아닐 수 있다.
TDD를 진행하다 보니 일부 메서드는 비즈니스 로직 상 클래스 내부에서만 사용되는데도 불구하고 테스트를 위해 public
으로 선언해야 했다. 하지만 찾아보니 테스트 클래스와 실제 클래스의 패키지 경로만 일치하면 protected
로 선언해도 정상적으로 접근이 가능했다.
private
메서드의 테스트 방법에 대해 찾아보다 보니 오히려 이 상황이 생긴 것 자체가 설계가 잘못되었을 가능성이 높다는 의견을 많이 접할 수 있었다. 하지만 내 경우에는 실제 Input과 직접적으로 연관되어 있는 메서드이기에 원래 테스트가 힘든 경우라고 생각해서 protected
로 처리하고 넘겼다. 그래도 대부분의 경우에는 private
메서드의 테스트가 부적절할 수 있다는 점에 동의하고, 앞으로 이 부분을 신경써야겠다고 느꼈다.
테스트가 비즈니스 로직에 영향을 미치는 것은 좋지 않다. 하지만 테스트를 작성하다 보면 부득이하게 테스트만을 위해 메서드를 작성하게 되는 경우가 생기는데, 이 상황을 마주해버렸다.
// 비즈니스 로직에서 사용되는 생성자
public Car(String name) {
this.name = new Name(name);
this.position = new Position(DEFAULT_RANDOM_UTIL);
}
// 테스트에서만 사용되는 생성자
public Car(String name, int position) {
this.name = new Name(name);
this.position = new Position(position, DEFAULT_RANDOM_UTIL);
}
위 코드를 보면 자동차 생성 시 최초 위치를 커스텀하기 위해 테스트에서만 사용되는 생성자를 별도로 정의했다. 이 문제를 근본적으로 어떻게 해소할 수 있을지 많이 고민해봤는데, 깔끔한 결론이 나오지 않았다.
결국 테스트만을 위한 생성자를 유지한 채로 2주차 과제를 제출해버렸다. 찜찜한 마음이지만 다음주에 이번 과제에 대해 다른 사람들과 이야기해보면서 이 코드를 개선할 방향을 찾을 수 있었으면 좋겠다. 😥
2주차 과제를 수행하면서 가장 고민한 부분은 랜덤 값을 어떻게 테스트할 수 있을까?
였다. 처음 구현할 때는 빠르게 최소한의 기능만 구현하는 것을 목표로 했기에 이 부분을 미뤄두었고 이후 리팩토링 과정에서 이를 어떻게 해소할 수 있을지 고민해보았다.
이 고민에 대해 관심이 생겼다면 아래 게시글을 읽어보자!
랜덤 값 테스트하기
유닉스 기반 운영체제와 윈도우 운영체제는 서로 줄바꿈 문자가 다르기 때문에 \n
을 사용하면 운영체제에 따라 예상치 못한 결과가 나올 수 있다. 하지만 String.format()
과 %n
을 사용하면 현재 실행 중인 운영체제에 맞는 줄바꿈 문자를 자동으로 삽입해주기 때문에 운영체제에 의존적이지 않은 줄바꿈을 사용할 수 있다.
Car
는 필드로 position
과 name
을 유지한다. 하지만 각 필드에 대한 예외처리나 로직을 작성하다 보니 Car
객체에 과하게 많은 책임이 부여된 것 같았다. 어떻게 분리해낼 수 있을지 고민하다가 원시값 포장
개념이 떠올랐다.
각 필드를 새로운 클래스로 분리하니 Car
나 Position
, Name
객체는 온전히 자신의 관심사에만 집중하는 형태가 되었다.
Car
객체를 List로 관리해야 하는데, 이걸 service
단에서 다루기에는 많이 무거워보였다. 그래서 해당 List만을 가지는 별도 클래스로 분리하여 관심사에 집중시키고자 했다(이를 일급 컬렉션
이라 한다). Car
List에 대한 로직을 별도 클래스로 분리하면서 service
에서 복잡한 비즈니스 로직을 더 추상적으로 사용할 수 있게 개선되었다.
public class Cars {
private final List<Car> cars = new ArrayList<>();
// ...
}
1주차 과제가 끝나면서 본격적으로 프리코스 커뮤니티가 활성화되었다. 나는 이 소중한 기회를 놓치지 않고 내 성장을 위해 적극적으로 활용하고자 한다.
1주차 과제 제출 기한이 끝난 후, 제일 먼저 한 행동은 커뮤니티에 PR 리뷰 요청 게시글을 올린 것이다. 이 게시글을 통해 다른 사람들과 상호리뷰를 진행했지만 아직 리뷰 활동을 더 하고 싶었다. 그래서 시간이 남을 때마다 다른 사람들이 커뮤니티에 올린 게시글들에도 찾아가 리뷰를 하고 다녔다.
다른 사람의 코드를 읽는 과정에서 내가 얻는 것도 많았고, 다른 사람들이 나에게 남겨준 피드백에서도 정말 많은 것을 얻을 수 있었다. 2주차가 시작되고 처음 2, 3일동안은 리뷰만 하고 다닌 것 같다. 2주차가 끝나가는 시점에서 나는 17명에게 리뷰를 마쳤고, 내 PR에서는 13명의 참여자분들과 총 80개의 코멘트를 주고받았다.
나는 1주차 과제를 진행하면서 고민한 내용이 다른 사람들에게도 도움이 되었으면 했다. 그래서 1주차 학습 내용 정리글(정규표현식)과 1주차 회고를 커뮤니티에 공유하기도 했다.
코드리뷰나 경험 공유도 좋지만 다른 사람들과 더 다양한 이야기를 주고받으며 긍정적인 상호작용을 이루고 싶었다. 마침 커뮤니티에서는 주변 지역에서 오프라인 스터디를 진행할 인원을 모집하고 있었다. 약간 멀긴 했지만 그래도 다양한 사람들과 서로의 성장을 위해 꼭 상호작용해보고 싶었기에 바로 신청해 참여할 수 있었다.
사실 전공 관련해서 학교 밖의 사람들과 만나본 적이 거의 없었기에 기대 반 두려움 반이었다. 스터디 채팅방은 조용했고, 약간 걱정되기 시작했다. 오프라인 모임 전날까지도 무엇을 할지 명확히 정해진 것이 없었다. 이대로는 유익하고 재밌는 이야기를 많이 주고받기가 힘들 것 같았다. 그래서 팀장 경험을 살려 스터디를 주도하기로 마음먹고, 모임에서 무엇을 할지 전체적인 방향성과 틀을 정하면서 회의록을 미리 작성해두었다.
오프라인 모임일이 되고, 유일하게 타 지역에서 오느라 가장 늦게 도착한 나는 도착하자마자 스터디를 주도하기 위해 노력했다. 조용한 분위기를 풀기 위해 바로 준비해온 아이스브레이킹을 시작하면서 서로에 대해 이야기하는 시간을 가졌다. 내 생각보다 정말 다양한 사람들이 모였고, 이 분들도 이번 주가 특히 바쁘고 조금 내성적이었을 뿐 다들 우테코에 몰입하며 성장 욕구가 큰 대단한 분들이었다. (자기소개 시간에 정말 많이 감탄했다..)
오프라인 스터디는 누구 한 명 빠지지 않고 의견을 주고 받으며 원활하게 진행되었다. 첫주차인 만큼 프리코스에 임하는 자세 메타인지하기, 목표와 다짐 정하기, 개발 공부 이력 공유하기와 같은 다양한 이야기를 주고받았다. 또한 1주차 과제를 해결하기 위해 학습한 내용과 회고를 공유하고, 서로의 코드를 다시 한 번 발표하고 질의응답하며 더 나은 코드에 대해 고민해보는 시간을 가졌다. 어찌나 유익하고 재밌게 이야기했는지 5시간이 시간가는 줄도 모르고 빠르게 지나가버렸다. 더 이야기하고 다양한 활동을 해보고 싶었는데 너무 아쉬웠다.
다음 주 오프라인 스터디에서는 지식 공유와 라이브 코드리뷰 후 몹 프로그래밍까지 시도해보려고 한다. 드라이버를 바꿔가며 서로가 생각하는 적절한 코드의 방향성에 대해 이야기하는 시간을 가져보고 싶다.
(TMI) 스터디 이름이 대전충청 스터디
여서 바꾸고 싶었는데, 다들 I여서 스터디 이름을 우아한 I들로 결정했다. 재밌는 이름이라 마음에 든다! ㅋㅋㅋㅋ
이번 주에는 1주차 피드백을 적극적으로 반영하면서도 TDD를 적용하기 위해 노력했다. 그 결과 1주차 코드에 비해 이번 2주차 코드는 품질이 몰라보게 좋아졌다! 하지만 코드리뷰를 받으면 여기서도 피드백이 많이 올라오겠지? 너무 재밌을 것 같고 하루빨리 상호리뷰가 하고 싶다. 프리코스를 하면서 정말 많은 것을 얻어가고 있다.
마지막으로 프리코스 스터디 경험이 너무 좋았다. 비록 오프라인 모임 장소가 멀어서 왔다갔다 하는 시간이 짧지는 않지만 전혀 후회되지 않고, 오히려 벌써부터 다음 모임이 기다려진다. 흔하게 경험해보지 못할 유익한 경험이라고 자신한다. 더 나은 코드에 대해 진지하게 논의해보는 것도 좋았고 멋있는 사람들도 만날 수 있는 소중한 기회였다. 덕분에 동기부여도 크게 되는 것 같다. 혹시 아직 스터디를 고민하고 있는 사람이 있다면 지금 바로 스터디 모집 글을 올리길 추천한다!
우아한 I들 화이팅! 프리코스 참여자분들 모두 화이팅!
[Java] 자바에서 '+' 연산을 통한 문자열 합치기를 지양하라
우테코 5기 AN(안드로이드) 최종코딩테스트 후기
private 메서드도 테스트를 해야 할까?
[Java] \n과 %n의 차이점은?
원시 타입을 포장해야 하는 이유 - Tecoble
일급 컬렉션을 사용하는 이유 - Tecoble
꽤 긴 글이지만, 몰랐던 단어가 존재하기도 했고 시간을 체크하면서 구현하는게 대단하다고 생각이 들어 금방 읽어버렸어요! 오프라인 스터디는 정말 재밌을거 같네요 ㅋㅋㅋㅋ