이번 미션은 🔥TDD🔥 가 가장 중점적인 학습 목표이다.
사다리 타기 미션을 함께하게 된 레디와 페어 프로그래밍을 진행하고, 리뷰어 현구막에게 리뷰를 받고 리팩토링한 후기를 작성하려 한다.
TDD가 학습 목표라며 운을 뗐지만 사실 이번 미션을 진행하면서 TDD의 장점에 대해 깊이 이해하지 못하였다. 페어 프로그래밍 때는 우선 아래와 같은 TDD의 사이클을 지키면서 TDD를 경험해보았다.
TDD cycle
1. 요구사항 정의하기
2. 실패하는 테스트 케이스 생성하기
3. 테스트가 성공하도록 구현하기
4. 리팩토링하기
그리고 시간 안에 기능을 구현하는 것에 집중하였고, 생각보다 사다리 출력 기능을 구현하는 것에서 많은 시간을 소비하였다. 그리고 리팩토링을 할 때는 객체의 역할 분리와 추상화, 가독성에 대해서 많이 고민하고 적용해보았다.
그 결과 위와 같은 구조의 프로그램을 설계하였고, 나름 가독성 좋은 코드를 만들어낼 수 있었다.
하지만 여전히 TDD를 이용해서 좋은 프로그램을 만들어 내는 것은 어렵다고 생각한다. 특히 1단계 미션처럼 시간적 여유가 없는 상황에서는 특히 그랬다. 왜냐하면, TDD를 적용해 보기 위해서는 어느 정도 객체의 협력 구조를 알고 있어야 하고 초기 설계가 필요하기 때문이다. 즉 비즈니스 도메인에 대한 이해가 충분히 되어 있는 상태여야 한다.
하지만 일반적으로 TDD를 할 때는 초기설계에 대한 최소한의 구상만을 한 다음 바로 테스트 코드를 작성 한다고 한다. 지금까지 오랜 시간 구상을 하고 프로그램에 옮긴 나로써는 최소한의 구상이라는 것을 지키기가 어려웠다
은총알은 없다.
항상 완벽한 것은 없다는 뜻이다. 돌다리도 두들겨보고 건너는 것이 중요하기 때문에 TDD를 하는 것인데, 내가 1단계 미션을 수행하면서 TDD가 불필요하고 불편하다고 느낀 것은 내가 설계하고 있는 프로그램이 학습용이며 작은 규모이기 때문이라고 생각한다. 나중에 큰 기업에 가거나 많은 사용자가 사용하고 안정성을 보장할 필요가 있는 서비스를 만들게 된다면 TDD는 필수 중의 필수가 되겠지? (이것도 물론 팀 바이 팀이 되겠지만 ......)
1단계 미션을 수행하면서 고민한 이러한 내용들을 현구막과 나눴는데, 아래와 같은 답변이 왔다.
결과적으로 어쩔 수 없다는 것이다. 비즈니스 이해도가 낮은 사람들은 바로 코드를 설계하기 어렵다. 프로그램에 협력 구조 중 가장 간단한 것, 내가 알기 쉬운 것을 시작으로 해서 차근차근 쌓아 가는 느낌으로 테스트 코드를 작성 하는 것도 방법이다. 다양한 도메인을 기반으로 한 프로그램을 개발하면서 TDD를 적용해보는 경험을 통해 이런 부분을 개선 시킬 수 있을 것이라고 생각했다.
사다리 미션을 진행하며 고민한 것들
출력문을 가공하는 로직의 위치
사다리 미션에서는 사다리를 출력 하는 기능과 참여자를 사다리의 위치와 맞게 공백을 조정하여 출력 하는 것이 특히나 어려웠다. 그래서 Formatter 라는 클래스를 만들어서 OutputView가 Formatter로부터 출력을 위한 가공된 문자열을 받는 것으로 하였다.
그러나 이렇게 Formatter를 View의 내부적으로 호출하게 되면 뷰가 너무 큰 책임을 지고있다는 생각이 들었고, 이를 DTO로 분리하려고 하였다. 그러나 한편으로는 DTO에 출력로직이 들어가도 괜찮을까라는 의문이 들었다.
고민한 결과 뷰에 로직을 넣는 것으로 결정하였고, 리뷰를 통해 이 판단이 틀리지 않았다는 확신을 가질 수 있었다.
RandomGenerator를 도메인 로직과 분리
마지막으로 생각한 것은 랜덤 테스트에 대한 고민이었다.
사다리를 생성하는 과정에서 무작위로 생성한 boolean 값으로 사다리의 다리를 생성할 지, 생성하지 않을 지 결정하였다. 그러나 사다리를 생성 하는 메인 로직 중 하나인 앞의 다리가 존재한다면 현재 다리를 놓지 않는다라는 것이 아래와 같이 랜덤 생성 로직에 포함되면서 검증이 어려워지는 문제가 있었다.
public class RandomStepGenerator implements StepStatusGenerator {
@Override
public StepStatus generate(final Step previous) {
Random random = new Random();
if (isNotOverlapped(previous)) {
return StepStatus.from(random.nextBoolean());
}
return StepStatus.DISCONNECTED;
}
private boolean isNotOverlapped(final Step previous) {
return previous.isEmpty() || previous.isDisconnected();
}
}
Generator가 Generator의 역할만 하지 않고 분기를 통해 메인 로직을 처리하고 있다. 이에 대해 현구막이 리뷰를 남겨주었고, 해당 로직이 테스트 가능하도록 분리할 수 있었다.
public class RandomStatusGenerator implements StepStatusGenerator {
private static final Random random = new Random();
@Override
public StepStatus generate() {
return StepStatus.from(random.nextBoolean());
}
}
리뷰를 통해 리팩토링하여 Generator는 랜덤 값을 생성하고 StepStatus를 반환하는 역할만 수행하도록 수정하였다. 이렇게 하여 분기 로직을 도메인 로직으로 위치시키고, 도메인 테스트 시 함께 테스트할 수 있도록 하였다.
Generator 구현 클래스 생성 위치
처음에는 Generator 인터페이스에 대한 구현 클래스의 인스턴스를 다리 하나를 은유하는 Step 클래스가 만들어질 때마다 생성하여 사용하도록 하였다.
public static Line from(final Width width) {
List<Step> steps = new ArrayList<>();
Step previous = Step.from(StepStatus.EMPTY);
while (steps.size() < width.size()) {
Step step = Step.of(previous, new RandomStatusGenerator()); // 구현체 생성
steps.add(step);
previous = step;
}
return new Line(steps);
}
이에 대한 리뷰를 받아보니, 모든 Step을 생성 할 때 구현체를 일일이 생성 하는 것이 적합하지 않다는 피드백을 받았다. 아, 물론 적합하지 않다는 의견의 이유는 내가 마음대로 생각한 것이다. 😅 즉, 성능상 여러번 구현체를 생성 하는 것이 메모리 상에서도, 시간 상에서도 큰 단점이라고 생각했다.
그래서 사다리 전체 (LadderController) 선에서 구현체를 생성하여 파라미터를 통해 구현체를 전달하는 것으로 로직을 바꿨다.
하지만 사실 이에 대한 궁금증은 여전히 풀리지 않았다. Controller에서 구현체를 생성 하는 것이 더 부적합 하지 않을까, 하는 개인적인 의문이다. Step을 생성하는 객체에서 Step을 어떻게 생성할지 전략을 알고 있는 것 또한 적합한 선택일 수 있을 것 같다.
셀프 피드백
사다리 타기를 TDD로 진행하였는가? 그 과정에서 어려웠던 점은 없는가?
사다리 타기를 TDD로 진행해 보았다. 그런데 TDD로 진행 하는 것이 처음에는 큰 효과가 없다고 생각했다. 오히려 테스트 코드를 먼저 작성하는 것이 불필요한 작업이라는 생각이 되었고 왜 TDD가 좋다고 생각하는 거지 하는 의문이 들었다.
그래서 켄트 백의 책을 읽기 시작하였다. 내가 왜 TDD를 하고 있는지 그리고 리뷰어와 TDD 에 대해서 깊이있는 논의를 하고 싶어서 책을 읽었다. 비록 꼼꼼하게 읽지는 못했지만 책에서 나온 예제를 따라해 보면서 TDD의 싸이클을 배우고 어떤 기법들을 사용해서 TDD를 할 수 있는지 배울 수 있었다.
2단계 미션에서는 책에서 배운 내용을 토대로 조금 더 효과적으로 TDD를 적용해보자는 목표를 세웠다.
본인이 정한 개발을 잘한다는 기준은 무엇인가? 그 기준을 지키기 위해 노력한 부분은 무엇인가?
개발을 잘 한다는 것은 내가 작성한 코드의 먼 미래까지 볼 수 있는 능력이라고 생각한다. 물질적인 성능이든, 코드를 읽는 동료의 편안함이든 내가 작성한 코드가 먼 미래에 회사에 어떤 영향을 끼칠 수 있는 지 고민해보고 더 최선의 방향으로 설계할 수 있어야 한다.
어쨌든 나는 사람들이 사용할 프로덕션을 만들기 위해 코드를 작성하고 있고, 이 프로덕션은 한 번 만들어지면 오랜 기간 회사가 살아있는 동안 사용되고 유지보수될 것이기 때문에 효율적이고 효과적인 코드를 작성할 의무가 있다고 판단했다.
나는 사다리 타기 미션을 수행하면서 이 기준을 지키기 위해 전략 패턴을 사용해 사다리를 생성하는 로직을 분리하였고, 객체의 역할 분리를 통해 각 객체가 자신의 책임을 다하도록 설계하였다.
너무 뻔한 대답같지만, 이렇게 설계하는 것에 나름의 노력과 시간을 사용하였기 때문에 자신있게 말할 수 있다.
피드백을 받으며 리팩토링하는 과정에서 어려움을 느낀 부분이 있는가? 그 문제를 어떠한 방식으로 해결했는가?
정답이 정해져있지 않고 트레이드 오프라는 관점에서 내가 선택해야하는 사소한 부분이 많았기 때문에, 리뷰어의 피드백을 받았더라도 바로 적용하기 힘든 경우가 있었다. 가령 나는 객체의 책임 상 Step이 사다리 생성 전략을 가지고 있는 것이 맞다고 생각했으나, 그렇게 되면 Step이 생성될 때마다 각기 다른 구현체가 생성되고 메모리 및 시간 성능을 많이 잡아먹는 다는 것이 문제가 되었다.
객체의 역할 분리를 통한 추상화냐, 메모리 및 시간 성능이냐를 따져 보았을 때, 작은 규모의 프로젝트이므로 추상화를 선택할 수 있었다. 성능은 지금 고려할 부분이 아니라고 생각했기 때문이다. 그러나 실제 현업에서는 성능을 고려할 필요가 있다고 생각하였고, 이 내용에 대해서 토론하면서 커비가 알려준 오버 엔지니어링 개념인 YAGNI에 대해서도 새로 알게 되었다.
https://ko.wikipedia.org/wiki/YAGNI
YAGNI - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. YAGNI[1](You aren't gonna need it[2][3])는 프로그래머가 필요하다고 간주할 때까지 기능을 추가하지 않는 것이 좋다는 익스트림 프로그래밍(XP)의 원칙이다.[4] 익스트림
ko.wikipedia.org
해당 미션에서 작성한 본인의 코드가 만족스러운가? 다음 미션에선 어떠한 목표로 코드를 작성할 예정인가?
만족스럽지 않다 😥😥😥
특히 출력 로직은 더 그렇고, 코드가 마냥 깔끔하다는 생각이 들지 않아 더 개선하고 싶었다.
2단계 미션에서는 1단계 미션을 크루들이 어떻게 수행했는 지 확인해보고 코드 가독성과 여러 사소한 사항들 (정적 팩토리 메서드에서 검증하는 지, 생성자에서 검증하는 지 등) 의 장단점을 비교해보며 나만의 결론을 내리고 코드에 적용해보고 싶다.
'우아한테크코스 > 레벨1' 카테고리의 다른 글
[회고] 우아한테크코스 6기 백엔드 레벨1 4주차 회고 (4) | 2024.03.11 |
---|---|
[회고] 우아한테크코스 6기 백엔드 레벨1 3주차 회고 (0) | 2024.03.03 |
[회고] 우아한테크코스 6기 백엔드 레벨1 2주차 회고 (0) | 2024.02.25 |
[UnitTest] 🚗자동차 경주🚗 페어 프로그래밍 & 리팩토링 회고 (4) | 2024.02.23 |
Random 기능이 포함된 메서드를 어떻게 테스트할 수 있을까? (feat. 전략 패턴) (0) | 2024.02.22 |