1주차 미션이 끝나고 사람들에게 코드리뷰를 받는 과정에서 내 코드에 대해 명쾌하게 설명할 수 없는 내 모습을 통해 아직 메타인지
가 부족함을 깨달을 수 있었다.
나름 왜
에 대해 고민하고 개발한다고 생각해왔지만 Deep
한 고민은 아직 부족했던 모양이다.
때문에 이번 미션에선 아주 진득하게 고민하고 개발해보고자 다짐하였다.
그렇게 다짐해서인지 코드 한 줄을 적을 때도 이런 저런 생각이 많이 들었고, 아직 내가 지식이 부족함을 다시 한 번 깨달을 수 있었다. 우테코는 프리코스만 참여해도 많이 배워간다고들 하는데, 나 또한 몰입하는 과정에서 정말 많이 배우고 성장해나가고 있는 중인 것 같다.
나는 이번 미션을 풀어나가면서 단지 미션을 수행하는 것에 목적을 두는 것이 아닌, 미션에서 마주하는 문제들을 해결하고 개선해나가는 것에 더욱 초점을 맞추었다.
“코드만 작성하는 코더가 되지 말고 문제를 해결하는 개발자가 되어라”
라는 말을 최근에 본 적이 있는데, 이 말을 점점 실천해나가고 있는 것 같아 정말 뿌듯했고 나에게 수고했다고 말해주고 싶었다.
정규표현식을 사용할 때 String.matcher()
를 사용하는 방식과 Pattern
객체를 생성해두고 이를 이용하는 방식을 고민하면서,
String.matcher()
를 사용하면 메서드 호출 시마다 Pattern
객체를 생성하기 때문에 메모리 낭비가 발생한다는 것을 학습할 수 있었고
Pattern
객체를 미리 만들어두어 사용하는 방식으로 이를 해결하여 필요없는 자원 낭비를 막는 경험을 할 수 있었다.
List<String>
객체를 입력받아 반복문을 통해 Car
객체 생성 작업을 반복하여 수행할 때, For-Loop
사용과 Stream
사용 중 어느 것이 성능면에서 우위에 있을 지를 직접 코드를 짜보며 비교하고 학습하였다.
그 과정에서 Stream
사용의 경우 최적화가 많이 진행되지 않아 최적화가 잘 되어있는 For-Loop
문을 사용하는 경우보다 동작 속도가 느리다.
하지만 Wrapped Type
의 경우 Stack
에 값이 바로 할당되어 빠르게 참조가 가능한 Primitive Type
과 다르게, Stack
에 저장된 Heap 메모리
의 주소를 통해 객체에 접근하기 때문에 성능이 저하되는 이슈가 있는데,
그러한 성능 저하로 인해 For-Loop
문을 사용해도 Stream
과의 성능 격차가 완화되거나 Stream
방식이 오히려 더 좋은 성능을 보이는 등, 비슷한 성능을 발휘하게 되기 때문에 가독성이 더 좋은 방법을 선택할 수 있다는 점을 직접 테스트하며 학습하였다.
이러한 검증 과정을 통해 코드가 짧고 간결한 Stream 방식을 채택하여 가독성있는 코드를 작성할 수 있었다.
어느정도의 책임부터 역할이라고 생각하고 객체로 생성할 지 정말 많이 고민했는데, 이때 테스트 코드 작성이 굉장히 많은 도움이 되었다.
테스트 로직은 실행 시에 자동으로 촤라락 실행되어야 하기 때문에 Console.readLine()
을 사용해서 입력을 받아 진행하는 테스트는 의도를 벗어나는 코드라고 생각이 들었다.
하지만 레이싱카 미션을 수행하며 이동 시도 횟수
를 InputView
에서 입력받아 NumberFormatException
에 대한 검증을 처리하고, int
값으로 파싱하여 사용하도록 코드를 작성하니
Console.readLine()
으로 입력받지 않는 이상 NumberFormatException
발생에 대한 검증 테스트를 수행하는 것이 불가능했다.
입력값을 객체를 통해 관리하지 않고 입력값을 그대로 사용하니, Console.readLine()
을 하지 않는 이상 값에 대한 테스트는 불가능했던 것이다.
때문에 시도횟수를 Turn
이라는 객체로 생성하여 몇 번의 이동 시도를 할 지 int
타입의 count
라는 필드에 담아 관리하고,
Console.readLine()
의 입력 값인 String을, Turn
객체에 생성자 주입하여 NumberFormatException
을 검증 후 Integer.parseInt
를 통해 얻은 시도 횟수를 사용하여 count
필드를 초기화하도록 구현하니
new Turn(”1”);
을 assertThatThrownBy
로 예외가 발생하는 지 검사해주면 되어
입력값에 대해 굉장히 간편하게 테스트 코드를 작성할 수 있게 되었다.
상태와 행위
를 객체를 만들어 관리했을 때 테스트 작성까지 간편해지는 것을 배웠고 더욱 깊이있게 이해하게 되었다.
상태와 행위
를 가질 수 있다면 역할로 생각하고 객체로 만들어 Testable
하도록 개발하는 것도 꽤나 괜찮은 방법이라는 것을 배울 수 있었으며,
생성자 주입 방식
을 사용하여 객체 생성 시, 객체 초기화 전 단계에 필수적으로 검증 로직 호출
이 가능함과 동시에 Testable
해지는 코드를 보며 생성자 주입 방식
을 지향하는 이유에 대해 몸소 느낄 수 있었다.
물론 생성자 주입을 사용하는 이유는 더욱 많지만 말이다.
지난 1주차에서 Java17에 대해 학습하며
Stream에서 collect(Collectors.toList());
가 아닌 toList()
메서드를 사용했을 때, unmodifiableList
를 return하기 때문에 사이드 이펙트
를 방지하고 List 내부의 값들을 안전하게 보관할 수 있어 이를 활용해보고자 다짐하였는데
이번 미션에서 기본적으로 변경이 일어나선 안되는 List에는 toList()
를 사용하였고, 순위 측정을 위해 정렬이 필요한 경우에는 collect(Collectors.toList());
을 사용하는 등 상황에 따라 적절히 활용하며 사이드 이펙트
를 최대한 방지하도록 유도하며 코드를 작성하였다.
지난 1주차에서 뉴라인이 필요한 문자열을 관리할 때, \n
기호를 사용하여 관리하였는데 문자열 중간중간에 \n기호가 추가되어 있어 가독성이 떨어지는 문제점이 있었다.
때문에 이번 미션에선 테스트 코드에서 실행 결과 출력 양식에 대해 테스트할 때 Text Blocks(“””)
를 활용하여 가독성있는 테스트 코드 작성을 할 수 있었다.
하지만 아쉬운 점은 \n
기호 활용 시, 1 line
만을 활용하여 변수를 선언할 수 있었는데 Text Blocks
를 활용하면 라인이 굉장히 늘어날 우려가 있다.
때문에 우선은 정말 명확하게 텍스트를 명시해야하는
테스트 환경
에서만 활용해보고자 한다.
main
메서드는 어느정도의 책임을 가질 수 있을까에 대해 많이 고민했다.처음엔 애플리케이션이 어떻게 동작하는지 명시해야하는 곳이 바로 Application
의 main
메서드이기 때문에
이곳에 의존성 주입에 필요한 객체 초기화
, 의존성 주입
, 메인 기능 start
정도의 책임을 부여해주었는데 이렇게 구현하는게 맞는지 틀린지 모르겠어서 고민이 정말 많았다.
하지만 결국 CarRacingManager
라는 객체를 생성하여 이곳에 의존성 주입에 필요한 객체 초기화
, 의존성 주입
, 메인 기능 start
라는 역할을 부여하였고, main
메서드에서는 CarRacingManager
객체를 생성하고 메인 기능 start의 역할만 수행하도록 변경하였다.
당장은 main
에 작성하는 것이 가독성이 괜찮았지만, 만약 이 프로그램의 기능과 동작 방식이 확장된다면 분명 더 많은 객체들이 필요하게 될 것이고 그것들을 모두 Application
객체에 구현하다보면 가독성이 낮고 지저분한 코드로 전락하게 될 것을 생각하였기 때문이다.
때문에 main메서드는 애플리케이션의 실행
만 담당하도록 하였고, 애플리케이션의 실행 로직과, 의존성을 관리하는 CarRacingManager
객체를 생성하여 IoC 컨테이너
와 같이 애플리케이션의 전체적인 흐름 컨트롤
이라는 역할을 부여했다.
carName
과 position
을 받아 출력을 수행하는 위 로직은 특정 객체에 의존하지 않는다.
public void printCarPosition(String carName, int position) {
StringBuilder positionText = new StringBuilder();
for (int i = 0; i < position; i++) {
positionText.append(CAR_POSITION_MARKER);
}
printMessage(String.format(CAR_POSITION_OUTPUT_MESSAGE, carName, positionText));
}
Car
객체를 직접 받는 출력 방식은 더 구체적인 입력 형태이며, 특정 클래스(Car
)에 의존한다.
이 메서드는 Car
객체에 의존하므로, Car
클래스의 내부 구현이 변경되면 이 메서드의 동작도 영향을 받을 수 있다.
public void printCarPosition(Car car) {
StringBuilder positionText = new StringBuilder();
for (int i = 0; i < car.getPosition(); i++) {
positionText.append(CAR_POSITION_MARKER);
}
printMessage(String.format(CAR_POSITION_OUTPUT_MESSAGE, car.getName(), positionText));
}
이 중에 뭐가 더 좋은 설계일까?
2번
은 출력 로직에서 특정 객체에 의존성을 갖기 때문에, 객체의 설계가 변경되는 경우 영향을 함께 받는다.1번
은 특정 객체에 의존성을 갖고 있지 않기 때문에, 객체의 변경에 영향을 받지 않는다.본인은 carName
과 position
을 활용하여 메시지를 만들어 출력한다는 것이 정해져있는 점을 주시하여,
객체가 변경됨에 따라 정해져있는 출력 로직도 변경되면 안된다는 생각에 UI 로직
이 도메인 로직
에 의존하지 않도록 설계해보았다.
출력 방식이 변경되면 UI 로직
만 손보면 되고, 객체의 설계가 변경되면 도메인 로직
만 손보면 되도록 의존성을 제거했었다.
하지만 두 로직 모두 UI로직
이 도메인 로직
에 의존성을 갖는 것은 매한가지라고 생각이 들었다.
때문에 MessageConverter
객체를 만들어 요구 사항에 맞는 형태의 메시지 String
을 생성하도록 하고,
OuputView
에는 단순히 출력
이라는 책임만을 쥐어주어 도메인 로직
에 대한 UI로직
의 의존성을 완전히 제거해보고자 한다.
이렇게 관리하면 OutputView
객체는 최소한의 출력 로직만을 가짐에도 불구하고 원하는 모든 메시지를 출력할 수 있는 유연성
과 확장성
을 가지게 되며, 도메인 로직
에 더는 의존하지 않는다.
뿐만 아니라 만약 출력 형태가 변경되면 MessageConverter
객체만 수정하면 되어 확실한 역할
, 로직
의 분리가 이루어지게 된다고 생각한다.
이번주 미션은 이미 종료되었으니, 다음 주 미션에서 이를 적용해보고자 한다!
테스트 코드를 먼저 작성함으로써 내가 작성할 코드에 대해 깊이 있게 생각해보고, 그 과정에서 예외 케이스를 발견하는 등의 이점을 얻기 위해 TDD
적용해보고자 하였지만, 아직 TDD
에 대한 지식이 깊지 않아 제대로 진행하고 있는 것이 맞는 지 판단이 서질 않았다.
물론 이번 미션에서는 TDD
를 통해 얻고자 하는 이점
을 모두 얻었다고 느꼈지만, 개인적으로 더 잘해낼 수 있을 거라고 생각이 들었고 더 깊이있게 학습하여 프리코스에서 최대한 많이 배워갈 수 있도록 노력해야겠다.
또한 테스트 코드 작성 자체가 익숙하지 않다보니, 어린아이의 낙서같은 테스트 코드를 작성한 것 같다.
반복되는 코드가 많았고 이로 인해 테스트 코드의 효율이 떨어진다고 생각됐다.
정말 꾸준히 학습하여 다음 미션에서는 더욱 좋은 코드를 작성해보고자 한다.
- 저번 미션보다 깊이있게 고민하고 생각하며 한 주 동안 정말 많이 성장했다고 느꼈다.
때문에 다음 미션에서도 근거있는 코드 작성을 하기 위해 정말 많은 시간을 투자해보고자 한다.Junit5
를 적극 활용하여 반복되는 코드를 제거하고 가독성 좋은 클린한 테스트 코드 작성하고자 한다.MessageConverter
객체를 두어도메인 로직
에 대한UI로직
의 의존성을 제거하고, 유연하고 확장성 있는UI로직
을 개발해보고자 한다.
I have learned many new and interesting things from reading your post slope and hope you will post more interesting information in the near future.
정말 많이 보고 배우고 있어요 !!!!!!!