1주 차가 미션이 끝나고, 공통 피드백과 크루원들의 코드 리뷰를 통해 자아성찰을 할 수 있는 부분이 많았다.
Getter의 사용, 알고 보니 2가지 이상의 일을 담당했던 메서드, 유틸리티 클래스 등 스스로 놓친 부분과 개념들이 의외로 많았고, 2주 차 미션에서는 실수를 반복하지 않기 위해 회고하는 시간을 가졌다.
자세한 내용은 아래 링크를 참고해주세요.
2주 차 미션도 1주 차와 마찬가지로 https://github.com/woowacourse-precourse/java-racingcar-6 를 fork/clone 한 후, 요구사항을 만족해 미션 코드를 작성하는 것이었다.
n대의 자동차를 전진 조건을 통해 전진해서 우승자를 맞추는 것이 핵심 기능이었고,
함수 분리(작은 단위), 각 기능 별 테스트 코드 작성 능력을 키워주려는 의도가 보인 미션이었다.
제 코드는 https://github.com/woowacourse-precourse/java-racingcar-6/pull/1570 에서 볼 수 있습니다.
1주 차와 마찬가지로 스스로 고민했던 부분과 그 과정에서 공부한 내용을 기록하고자 한다.
유틸리티 클래스는 언제 사용해도 될까?
1주차 미션에서는 입력 클래스(+검증)를 new로 생성해서 사용했다.
크루원들의 1차 미션 코드를 보면서 검증, 입력 클래스를 유틸리티 클래스로 사용한 것을 보았고, 어떤 이유로 사용했는 지를 알기 위해 static 메서드에 대해 집중적으로 찾아봤다.
static 메서드의 장점도 많지만, 단점도 많아 그다지 권장하는 방법은 아니었다.
하지만, 결과적으로 2주 차 미션 때는 유틸리티 클래스를 사용했다.
아래는 static 메서드를 권장하지 않는 이유와 그럼에도 불구하고 내가 사용한 이유를 정리한 내용이다.
1. 프로그램이 종료될 때까지 Garbage Collector가 회수하지 않아, 메모리에 계속 쌓인다.
기본적으로 static 키워드를 붙인 변수/메서드는 힙 메모리 영역이 아닌 스택 영역에 할당되기 때문에 GC의 탐색 대상에서 제외된다.
2주 차 미션은 처음 입력, 출력, 검증 클래스를 생성해서 사용하면 됐지만, 1, 2주차 모두 미션 특성상 사용자 입력을 필수적으로 필요로 한 게임이라고 생각했다. 즉, 전체 프로그램의 라이플 사이클 동안 입력, 출력, 검증 클래스는 필수적으로 사용해야 했다.
따라서, 자동차 레이싱 게임 진행 간, 지속적으로 GC 탐색 대상이 되는 것은 오히려 성능을 떨어뜨릴 수도 있다고 생각했다.
결과적으로 유틸리티 클래스로 만들어 프로그램 진행 간, GC 탐색 대상에 제외시켜 성능을 향상 시 킬 수 있다고 생각했다.
2. Thread Safe 하지 않다.
입력, 출력, 검증 클래스는 단순히 전달받은 값을 리턴, 검증, 출력하는 역할만 수행한다.
그래서 하나의 스레드의 변화가 다른 스레드에게 영향을 미치지 않는다고 생각했다. 또한, 해당 클래스들은 인스턴스 변수가 존재하지 않아 문제가 발생하지 않을 거라 판단했다.
3. 오버라이딩이 불가능하다.
static 메서드는 오버라이딩이 불가능하다. 이 내용을 보았을 때, 약간의 고민을 했다. 오히려 해당 클래스들을 추상화시키고, 여러 구현체를 만들어서 사용하는 게 더 좋지 않을까?라는 생각을 했었다.
현재 검증 클래스는 입력에 대한 검증 클래스와 이름에 대한 검증 클래스로 분리되어 있다.
그래서 만약, 검증 인터페이스를 만들고 여러 구현체 정의한다면 해당 클래스에서 사용되지 않는 메서드까지 구현해야 하는 단점이 발생한다고 생각했다.
추가로, Math.max()와 같은 메서드를 사용할 때 new로 Math를 생성하지 않고 사용한다. 이처럼 명확한 이름을 가질 경우에 사용자에게 큰 편의성을 제공해 준다. (이펙티브 자바 내용)
그러니 이름도 명확한 입력, 출력, 검증 클래스를 유틸리티 클래스로 사용하는 것이 괜찮지 않을까?
자동차 이름 길이, 중복 검증은 누구의 책임일까?
1주 차 미션에서는 입력 메서드에서 검증도 함께 진행했었지만, 메서드가 한 가지 일만 담당하고 관심 사항을 분리하고자 2주 차 미션에는 입력 클래스와 검증 클래스를 분리했다.
초기 검증 메서드는 사용자가 콤마(,)를 기준으로 여러 이름을 입력하기 때문에, 빈/공백 이름 검증에 더불어 중복이름, 길이까지 검증했다.
하지만, 자동차 이름 길이, 중복은 특정 클래스에 종속된 제약조건이 아닐까?라는 생각이 들었다.
현재는 자동차 게임만 존재하지만, 다른 게임이 생겼을 때 이름의 조건(중복, 길이) 등이 다를 수 있다고 생각했다.
그래서, 입력 검증 메서드는 단순히 사용자가 구분자를 이용해 이름을 입력할 때, 이름의 기본검증(빈/공백)만 수행하도록 했고, 길이/중복 등의 검증은 클래스의 책임으로 넘기는 것으로 결정했다.
이로 인해, 입력 검증 메서드의 재사용성이 증가될 것이라 생각했다.
하지만 위 코드는 하나의 메서드는 하나의 기능 수행해야 한다는 것을 어겼다. 생성자 메서드에서 2가지 기능(생성, 검증)을 수행하기 때문이다.
생성자 메서드는 인스턴스 필드를 초기화하는 역할도 있겠지만, 인스턴스 변수를 올바른 값으로 초기화하고, 객체의 일관성과 안정성을 보장해줘야 한다고 생각했다.
이런 측면에서 생성자에서 검증을 진행해도 될 거 같다고 생각했다.
그러나 결국 생성자에서 2가지 기능을 수행하는 것은 변함없다. 만약 생성자에서 검증을 하더라도, 검증해야 할 부분이 많아지면 가독성과 유지 보수성이 확실히 떨어진다고 생각했다.
따라서 별도의 이름 검증 유틸리티 클래스를 도입했고, 이름에 관한 검증을 아래와 같이 분리했다.
입력 메서드에서 유효성 검사를 해야 할까?
1주 차 미션 때는 입력 메소드에서 유효성 검사를 동시에 진행 했지만, 현재 미션에서는 입력 클래스와 검증 클래스를 분리했다.
(참고로 분리한 이유는 1주차 추가 회고록에 작성했습니다.)
그렇기 때문에 초기에는 아래와 같이 두 메서드를 각각 호출해서 사용했다.
하지만, 2가지 의문점이 생겼었다.
- 입력 메서드는 사용자로부터 입력받은 값을 반환해 주는 데, 기본적인 타입, 값 안정정 보장해줘야 하지 않을까?
- 만약 내부적으로 검증 메서드를 사용한다면, 입력 메서드가 특정 제약 조건에 종속되는 건 아닐까?
결론으로 유효성 검사를 진행했다.!
왜냐하면, 이름은 빈값/공백이 되면 안 되고, 시도 횟수는 양수이어야 한다는 것은 기본 제약조건에 해당될 것이다.
또한, 현재 기본 제약 조건을 검증하는 클래스와 길이/중복 와 같이 특정 클래스에 특화된 제약조건을 검증하는 클래스가 분리되어 있다. 그러므로, 입력 클래스는 기본 제약 조건만 검증하므로, 재사용성도 떨어지지 않을 거라 판단했다.
추가로, 입력 메서드를 사용하는 클라이언트는 반환해주는 값은 안정성이 확보됬다고 판단할 수 있기 때문에, 필요한 검증 메소드를 호출하지 않을 수도 있을 것이다.
결과적으로 아래와 같이 코드를 변경했다.
Getter 메소드를 사용을 지양하자
클린 코드의 기본 규칙이자, 1주 차 미션 때 지키지 못한 부분이다. (이 부분을 코드 리뷰 해주신 크루님 감사합니다.ㅎ)
이 메시지는, 객체지향 프로그래밍의 정보은닉을 지키면서 객체에 메시지를 보내, 상태 처리의 주체를 객체가 하도록 옮기라는 의미를 내포하고 있다.
이 규칙은 사실 처음 본 것이 아니라, 이전에 몇 번 봐왔었다. 이전까지는 Getter를 사용하지 않아야 할 이유가 크게 와닫지 않았었다. 단순히 필드 값을 응답해 주는 역할을 할 뿐일 텐데.
하지만, 이번 미션을 진행하면 Getter 메서드를 지양하는 이유인 상황을 직접 마주쳤고, 확실히 느낄 수 있었다.
아래는 내가 Getter 메소드를 닫게 된 이유이다.
Getter는 조회로 끝나지 않는 경우가 많다.
보통 Getter로 상태 값을 조회하면, 그 값이 조건에 맞는지 비교하는 등의 작업이 동반되는 경우가 많다.
위 코드는 자동차 경주에서 자동차들 중, 우승자를 찾는 메서드이다.
보시다시피 위 코드에서는 Getter로 조회 한 총 이동 횟수를 최대 총 이동 횟수와의 비교에 사용된다. 즉, 총 이동 횟수를 Car에게 전달해서 물어보면 되는데, 직접 이를 외부에서 확인해 보는 꼴이 된 것이다.
그래서 아래와 같이, Car 클래스의 책임으로 넘겨 Getter를 제거할 수 있었다.
그럼, Getter는 아예 사용하면 안 될까?라는 생각이 들었다.
그거는 아니라고 생각한다. 필드에 직접 접근해서 값을 변경할 수 있는 Setter는 무조건 지양하는 것이 맞을 수 있지만, 객체의 값을 외부로 단순히 표현해줘야 하는 상황에서는 사용해도 된다고 생각한다.
위 코드에서 Car::getName 이 그런 상황이라고 생각한다.
마치며
이번 주는 우당탕탕 지나간 거 같다. 😂
인턴 간, 새로운 외부 API 연동 및 레거시 코드에 교체하는 일을 맡아서, 레거시 코드를 분석하느라 정신이 하나도 없었다.
하지만, 그 와중에서 우테코 미션을 진행하면서 부족했던 부분을 스스로 발견할 수 있었고, 무엇보다도 미션이 너무 재미있었다. :)
이번 미션 진행 간, 1주 차 공통 피드백에도 있듯이 자바 API를 적극 사용하기 위해 노력했다. 이펙티브 자바를 읽으면서 익힌 스트림, 람다를 사용해 볼 수 있는 좋은 기회였고, 확실히 반복문/조건문을 사용했을 때 보다 코드 양이나 가독성이 훨씬 상승했던 거 같다.
또, 테스트 코드를 작성하면서, 하나의 기능과 테스트를 묶어서 커밋을 해야 하는지, 별도로 커밋을 해야 하는지에 대한 고민도 있었다.
당연한 말이지만 작동하지 않은 코드를 커밋하는 것은 무의미하다고 생각했다.
그래서, 구현한 기능에 대한 테스트도 함께 커밋 단위로 묶이는 것이 맞다고 생각했고, 테스트 코드 추가가 필요할 시에는 test 커밋 컨벤션을 사용하도록 했다.
참고자료
'우아한테크코스' 카테고리의 다른 글
우아한테크코스6기 3주차 미션 회고록 (0) | 2023.11.08 |
---|---|
우아한테크코스6기 프리코스 1주차 미션 후기 (1) | 2023.10.24 |