합격도 되지 않았으면서 우아한테크코스 6기 카테고리를 만드는 게 조금 웃기지만.. 프리코스를 하면서 어떤 것들을 배웠고 고민했었는지 남기기 위해 블로그에 적어봅니다.
사실 1주차는 이미 벨로그에 작성했어서, 그대로 복붙이 될 것 같네요.
클린 코드 책 내용만이 무조건 정답이라고 생각하지 말자.
코드를 그렇게 완벽히 작성하지 못하기도 해서 과감한 말일 수도 있지만, 클린 코드에서도 이와 비슷한 내용이 작성되어 있습니다.
실제로도 이 책에서 주장하는 기법 다수는 논쟁의 여지가 있다. 여러분도 모든 기법에 동의하지 않으리라. 어떤 기법은 격렬히 반대하리라. 그래도 괜찮다. 우리 생각이 무조건 옳다고 주장할 의도는 없으니까. 하지만 다른 한편으로 이 책은 우리가 오랫동안 고민하고 숙고한 교훈과 기법을 권고한다. - Clean Code, 17p
개발을 하며 느낀 점은, 그간 클린 코드 등과 같이 매우 권위적인 책들은 여기에 적힌 내용이 무조건 정답이라는 식으로 공부했었다는 점입니다.
진정한 성장을 하려면, 꼭 이게 정답인 내용일지 되뇌어보는 시간이 필요할 것 같습니다.
대표적인 예시는 다음과 같습니다.
지나친 함수 분할은 오히려 가독성을 해친다.
클린 코드에서 제공된 코드 중 일부입니다.
private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
이렇게 분할한 의도는 이해가 됩니다. 각 함수의 추상화 수준을 맞추기 위해서입니다.
이와 비슷한 방식으로, 이번 미션을 하며 저는 처음에 아래처럼 작성했었습니다.
private void play() {
int computerNumber = RandomNumber.pickNumber();
while (true) {
...
int strike = countGameStrike();
...
}
printGameEnd();
askResumeInput();
}
private int countGameStrike() {
return umpire.countStrike();
}
private static void printGameEnd() {
EndView.end();
}
private static void askResumeInput() {
AskController.askResumeInput();
}
이 코드는 과연 좋게 작성된 코드일까요?
이 방식의 단점으로는 클래스에 너무 많은 함수가 존재함에 따라 함수가 필요 이상으로 많아져, 오히려 가독성이 불편해질 수 있다는 점입니다.
따라서 저는 이렇게 바꾸었습니다.
private void play() {
int computerNumber = RandomNumber.pickNumber();
while (true) {
...
int strike = umpire.countStrike();
...
}
EndView.end();
AskController.askResumeInput();
}
정확히 말하면, 다른 객체에게 메서드를 실행해 달라고 요청하는 부분은 그 메서드의 이름으로 충분히 유추할 수 있다면 굳이 별도의 함수로 감싸지 않아도 될 것 같다는 결론을 내렸습니다. 함수가 하나의 기능을 함에 있어 다른 객체에게 메서드로 요청을 하는 것은 자연스러운 부분이기 때문입니다.
그러나, 함수화를 통해 쉽게 코드 표현을 줄일 수 있다면 그때는 함수화를 진행해도 괜찮을 것 같습니다. 아래 또 다른 클린 코드의 예시가 그 예입니다.
// includeSetupAndTeardownPages 함수를 통해 아래 네 개의 작업이 단축된다
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
또 다른 예로는, 아래와 같이 바꿀 수도 있습니다.
// BEFORE
public static void assertNumberValue(final String input) {
if (!isInputValidPositiveNumber(input)) {
throw new IllegalArgumentException();
}
}
private static boolean isInputInvalidPositiveNumber(final String input) {
return input.matches("^[1-9]+$");
}
// AFTER
public static void assertNumberValue(final String input) {
if (!input.matches("^[1-9]+$")) {
throw new IllegalArgumentException();
}
}
하지만 아래 경우는 분리할 필요가 있어 보입니다. 함수 호출을 넘어 특정 작업을 실행할 경우입니다.
public static int pickNumber() {
StringBuilder numberBuilder = new StringBuilder();
while (!isBuilderEnoughPicked(numberBuilder)) {
saveNewNumber(numberBuilder);
}
...
}
// 값이 같은지 "비교" 행위를 한다.
private static boolean isBuilderEnoughPicked(final StringBuilder numberBuilder) {
return numberBuilder.length() == PLAY_NUMBER_DIGIT.getValue();
}
어떨 때 함수 분할을 해야 할까에 대한 고민은 계속되어야 할 내용 같습니다. 함수는 한 가지만을 잘해야 하면서도, 너무 지나친 분할로 인해 가독성을 해치면 안 된다고 생각합니다. 이 둘 사이의 적절한 조화를 찾을 수 있을 때까지 연마해야겠습니다.
지나친 클래스 분할을 하지 말자.
클린 코드를 잘못 해석해서인지, 여러 converter들을 만들곤 하였습니다. 형 변환과 같은 것들조차 하나의 기능으로 바라봤었습니다.
public class StringInputConverter {
public static String[] toArray(final String input) {
return input.split("");
}
}
public class IntegerInputConverter {
public static toString(final int number) {
return String.valueOf(number);
}
}
이렇게 하다 보니, 아래와 같은 괴상한 코드가 나오기도 했습니다.
public class BallRule implements GameRule {
@Override
public int calculate(final int hitter, final int pitcher) {
String[] origin = StringInputConverter.toArray(IntegerInputConverter.toString(hitter));
String[] test = StringInputConverter.toArray(IntegerInputConverter.toString(pitcher));
boolean[] match = recordMatchedPositions(origin, test);
...
}
}
도대체 이것을 따로 만들어놓은 게 어떤 이점이 있는 것일까요? 지금 다시 보면 오히려 쓸데없이 의존성이 늘어난다는 문제가 있을 것 같습니다.
따지고 보면 String을 String [] 배열로 만드는 것은 String 객체에게 기본적으로 요청할 수 있는 것이며, int를 String으로 만드는 것 또한 가능합니다. 즉, 이러한 클래스는 작성하지 않기로 하였습니다.
public class BallRule implements GameRule {
@Override
public int calculate(final int hitter, final int pitcher) {
// 표현이 조금 비효율적일수도 있습니다.
// 말하고 싶은 것은 기본 메서드로 절약할 수 있는 것들을 절약하자는 것이니 코드 효율은 차치해주세요.
String hitterValue = String.valueOf(hitter);
String pitcherValue = String.valueOf(pitcher);
String[] hitterNumbers = hitterValue.split("");
String[] pitcherNumbers = pitcherValue.split("");
boolean[] match = recordMatchedPositions(hitterNumbers, pitcherNumbers);
...
}
}
다른 사람이 쉽게 이해할 수 있는 이름을 쓰자.
이름 짓기는 진짜 어려운 것 같습니다.
처음에는 숫자 야구와 관련하여 야구다 보니, 심판 객체 이름을 야구 + 심판을 나타내는 Umpire를 썼었습니다.
그동안 Referee는 들어봤어도, Umpire에 대해서는 처음 들었습니다. 그럼에도 사전적 의미에 더 집중하기 위해 Umpire로 사용했었습니다.
그러다 클린 코드에 있는 다음 내용을 떠올렸습니다.
해법 영역에서 가져온 이름을 사용하라
모든 이름을 문제 영역 (도메인)에서 가져오는 정책은 현명하지 못하다. 같은 개념을 다른 이름으로 이해하던 동료들이 매번 고객에게 의미를 물어야 하기 때문이다.
이에 누구나 쉽게 읽을 수 있도록 하려면 클린 코드에 있는 내용처럼 같은 개념을 다른 이름으로 이해하지 않도록 더 보편적인 이름을 사용해야겠다고 결론 내렸습니다. 향후 실무 개발을 하면서 다른 사람과의 협업은 필수적으로 이루어질 텐데, 그때 다른 분이 읽을 때 오해가 없도록 해야 할 테니까요.
원시값 포장 객체의 검증 로직은 원시값 포장 객체에게 맡기자.
이전에 미션을 연습 삼아 미리 해 보며 원시값 포장에 대해 알아봤었습니다.
원시값 포장의 장점 중 하나는 일반적인 원시값으로 썼을 때보다 예외처리를 해당 객체에서 해 주어, 사용되는 클래스 (ex: Name과 User가 있다면 User)에서는 원시값에 대한 예외처리를 해주지 않아도 된다는 것입니다.
그런데 저는 책임을 분할해야 한다는 것에 집착해서, 처음에는 Validator를 만들곤 하였습니다.
public class NumberValidator {
public static void assertInputNumberWithLength(final String input, final int length) {
assertNumberValue(input);
aasertDigitLength(input, length);
assertEachNumberUnique(input);
}
private static void assertNumberValue(final String input) {
...
}
...
}
실제 사용은 이렇습니다. 이 때는 원시값을 포장하지 않았으며 (만들었어도 검증을 Validator에게 맡겼을 것 같습니다.), 직접적으로 입력을 받았을 때 매번 검증이 일어나도록 설정했습니다.
public class ConsoleInputView implements InputView {
@Override
public int readPlayNumber() {
String number = Console.readLine();
NumberValidator.assertInputNumberWithLength(number, PLAY_NUMBER_DIGIT.getValue());
return Integer.parseInt(number);
}
}
하지만 PlayNumber를 원시값 포장 객체로 만들고, 예외처리를 PlayNumber에서 직접 하도록 바꿨습니다.
public class PlayNumber {
private final int number;
private PlayNumber(final String number) {
validateNumber(number);
this.number = Integer.parseInt(number);
}
}
NumberValidator를 사용하지 않고 직접 원시값 포장 및 해당 객체에서 예외처리를 하도록 하면, 응집도가 올라간다는 장점이 있습니다.
이는 오브젝트 (Object) 책에도 있는 내용입니다.
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도 (cohension)가 높다고 말한다. 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있을뿐더러 응집도를 높일 수 있다.
객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다. - 오브젝트 (Object), 26p
만약 굳이 NumberValidator를 만들었다면 PlayNumber 객체 입장에서는 자신의 데이터에 관해 다른 객체가 관여하게 되는 것입니다.
PlayNumber에서 예외 처리를 하게 하면, 테스트 코드도 더 명료하게 작성할 수 있게 됩니다.
@Test
void 플레이_숫자는_세자리여야만_한다() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
PlayNumber playNumber = PlayNumber.from("1234");
});
}
만약 NumberValidator를 만들었다면 PlayNumber에 대한 테스트가 아니라 NumberValidator에 대한 테스트였겠죠?
공용 상수는 전용 클래스에 담기보다는 Enum으로 관리하자.
객체에 private 하게 사용되는 상수는 제외하고, 여러 곳에서 사용되는 상수들은 상수 전용 클래스 (ex: Constants 등)에 담기보다는 Enum을 쓰는 것이 더 좋음을 알게 되었습니다. 이는 이펙티브 자바에도 있는 내용입니다.
Enum을 사용함에 따라 얻을 수 있는 장점 및 이전의 단점은 다음과 같습니다.
- 정수 상수는 문자열로 출력해도 의미가 아닌 단지 숫자로만 보입니다.
- 같은 정수 열거 그룹에 속한 모든 상수를 순회할 수도 없습니다. 개수 또한 파악할 수 없습니다.
- 열거 타입으로 하면 타입 안전성이 보장됩니다. 특정 열거 타입의 인스턴스를 의존하는 메서드는 그와 다른 열거 타입의 인스턴스를 받았을 시 컴파일 오류를 던집니다.
- 열거 타입으로 하면 임의의 메서드나 필드를 추가할 수 있으며, 임의의 인터페이스를 구현할 수도 있습니다.
따라서 이전에는 아래와 같이 사용했었지만,
public class Constants {
public static final int PLAY_NUMBER_DIGIT = 3;
public static final int RESTART = 1;
public static final int END = 2;
}
이후 다음과 같이 바꾸었습니다.
public enum Constant {
PLAY_NUMBER_DIGIT(3),
PLAY_WANT(1),
END_WANT(2);
private final int value;
Constant(final int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
Enum에 대해서는 우아한 형제들 기술블로그 등을 보면서 더 익혀보도록 하겠습니다.
커밋 컨벤션 준수
커밋 컨벤션을 정리하면서 그동안과 다르게 의식적으로 커밋 컨벤션을 지키려는 노력을 할 수 있었습니다.
뿐만 아니라 무조건적으로 따르기보다는, 제 생각에 맞춰 일부 수정한 부분이 있기도 합니다. (자연스럽게 읽힐 수 있도록 하는 것 등)
물론 팀 컨벤션이 있다면 그것을 지키는 게 최우선이지만, 일단은 많은 사람들이 보편적으로 알고 있는 커밋 컨벤션이 습관에 배이게 된 것 같아 좋은 것 같습니다.
결론
공통적으로 느낀 것은, 실제 지원을 하고 미션을 스스로 하니 제 주관에 맞게 이 지식이 왜 그런지 탐구해 보는 훈련을 할 수 있었다는 점입니다.
1주 차만 해도 이렇게 배우고 느낀 게 많았는데, 나머지 주차들에는 어떤 것들을 배울 수 있는지 등에 대해서도 기대가 됩니다.
한편으로는 새로운 미션이 주어졌을 때 또 똑같은 실수를 하게 되는 건 아닌지 걱정이 들기도 합니다. 그럴 때마다 이때 느끼고 배운 점들을 복기해 보고, 같은 실수를 반복하지 않도록 더 점검해야겠습니다.
향후 있을 공통 피드백에서도 제가 어떤 점을 놓쳤었는지 점검해보기도 할 생각입니다.
최종 제출
이곳에서 확인하실 수 있습니다.
'🚀 우아한테크코스 6기 지원 기록' 카테고리의 다른 글
[프리코스] 프리코스 4주차 후기 (크리스마스 프로모션 🎄) (1) | 2023.11.17 |
---|---|
[프리코스] 프리코스 3주차 후기 (로또 🎱) (0) | 2023.11.14 |
[프리코스] 프리코스 2주차 후기 (자동차 경주 🚗) (0) | 2023.11.06 |