[우아한테크코스] 1주차 후기 - 단위테스트에 대해서
우아한테크코스 1주차 후기
날짜 | TODO | 추가 공부 |
---|---|---|
5/7 (화) | OT | |
5/8 (수) | 강의 (단위 테스트란 무엇인가) | 어떻게 공부할 것인가 고민... |
5/9 (목) | 페어 프로그래밍 (자동차 경주 게임 구현, 계산기 프로그램 구현 -> 단위테스트 중점) | JUnit5 공식 문서 정리 |
5/10 (금) | 강의 (피드백), 페어 프로그래밍 (과제 마무리) | AssertJ 공식 문서 정리 |
1주차 진행 테이블이다. 내용은 통합하여 정리하려고 한다.
OT 후기
새로 지은 교육장에 처음 간날 오티가 진행이 되었다. 조끼리 앉았지만 그 날만 해당이 되는 부분이었다. 아이스 브레이킹 시간을 갖고 교육이 어떻게 진행되는지 전반적인 설명과 Q&A를 가지는 시간이었다. 다들 열정이 가득해보였다.
첫 강의: 단위 테스트란 무엇인가
첫 강의 부터 테스트에 대해 논하였다. 박재성 이사님께서 직접 강의를 해주셨다. (이하 '포비'님) 우리는 닉네임으로 소통하기 때문에 닉네임으로 칭하는것이 좋겠다. 첫 강의 였지만 매우 임팩트 있는 강의였다. 여타 다른 강의들 (학교, 학원, 인강, 과외 등등)과는 확연이 차이가 느껴졌다. 나는 학교, 학원, 인강, 과외 등 해볼 수 있는거 거의 다 해보았지만 이것과 감히 견줄 수 없었다. (아 대학교는 논외로 하자. 학교는 학문을 닦는 곳이기 때문이다. 지식의 상아탑도 무시할 수 없다) 어쨌든 돈 주고 받는 강의나 과외, 학원과는 비교하는 것이 미안할 정도의 퀄리티 였다. 내가 예전 부터 듣고 싶었던 포비님의 강의였기 때문에 한 껏 기대에 부풀어 있기도 하였다. 어느 교육 기관에서도 소프트웨어의 품질에 대해서 논했던 곳은 없었다. 다들 언어나 프레임워크의 사용법을 가르치는데 혈안이 되어 있었기 때문이다. 하지만 여기서는 그런 습관들을 모조리 버려야 했다. 언어나 프레임워크는 소프트웨어를 만드는 하나의 보조 수단일 뿐, 그 본질은 다른 곳에 있다. 안정적이고 클린한 소프트웨어를 만들기 위해 단지 그것들은 거들 뿐 이다. 포비님의 강의는 그것들을 명확히 짚고 있었다.
우리가 단위 테스트를 하는 이유는 개별적으로 가장 작은 단위 모듈에 대한 테스트를 진행할 수 있기 때문이다. 단위 테스트들을 연습해보면서 하는 이유를 느껴볼 수 있었다. 내가 테스트를 작성한 이후 실제 코드에 손을 댈 때 더욱 자신있게 수정할 수 있었다. 내가 잘못 손을 대면 테스트가 통과하지 않기 때문이다. 프로덕션 코드를 조금 씩 수정 후 단위 테스트를 바로바로 빠르게 돌리면서 나아갔다. 그랬더니 조금 더 적극적으로 코드를 수정할 수 있었다. 기존에 테스트를 작성하지 않을 때는 코드를 수정할 때 맞게 되었는지 예외 처리가 제대로 되는지 알 길이 없었다. 물론 값을 찍어보며 확인할 수는 있지만 그 많은 경우에 대해서 일일이 하는것은 시간낭비이며 코드를 바꿀 때마다 그작업을 되풀이하는것은 매우 끔찍한 일이었다.
페어 프로그래밍: 자동차 경주 게임
자동차 경주 게임이라는 단순한 자바 게임을 단위 테스트를 하며 구현하는 과제가 나왔다. 우아한테크코스의 선발과정 중 프리코스라는 과정이 있는데 그 과정에서 나온 과제 중 하나 이다. 내용 자체는 단순하지만 클린한 아키텍쳐를 만들면서 개발을 해야 하기 때문에 시간이 매우 오래걸렸다. 그리고 각 미션은 짝이 랜덤으로 배정된다. 그래서 그 짝과 함께 페어 프로그래밍이라는 것을 한다. 페어 프로그래밍은 대학교때 소프트웨어 공학이라는 과목에서 이론적으로 접한 내용이다. 둘 이 하나의 컴퓨터를 가지고 코딩을 하는 일종의 방법론이다. 말로만 듣던 페어 프로그래밍을 직접 해 본 느낌은 매우 신선했다. 먼저 시간이 매우 빨리 지나간다. 그 만큼 집중하며 서로의 작업에 대해서 피드백을 하기 때문이다. 그러면서 서로에게 배우는 점도 있었고 과제도 수월하게 할 수 있었다. 하고 나면 체력소모가 있긴 하다. 하지만 굉장히 좋은 방법론임에 틀림없다.
JUnit5 문서 정리
그리고 JUnit5에 대해 정리를 해보았다. 물론 공식 문서가 최고 :)
https://pjh3749.tistory.com/240?category=783717
강의 플랫폼에서는 키워드를 많이 던져 준다. 그래서 알아서 찾아서 공부할 것들이 많다. 혼자 찾아가며 하는 공부를 좋아하는 나에게 더할 나위없이 좋은 방식이었다. 오히려 하루종일 앉아서 수동적으로 강의를 듣는 방식은 효율이 나지 않을 뿐더러 힘들다. 힘들 때 조금씩 알아서 쉬어가면서 공부를 하는 방목(?) 교육 스타일이 맘에 든다.
아직 JUnit4를 쓰는 곳이 많다지만 그래도 새로 나온 JUnit5 (새로 나왔다기엔 시간이 많이 됐지만)에 대해서 느껴 볼 수 있는 시간이었다.
AssertJ 문서 정리
AssertJ는 많은 assertion(직역: 주장, 행사)을 제공하는 자바 라이브러리이다. 에러 메세지와 테스트 코드의 가독성을 매우 높여주고 각자 좋아하는 IDE에서 쓰기 굉장히 쉽다.
https://pjh3749.tistory.com/241?category=783717
단위 테스트: 자동차 경주 게임
내가 나눈 패키지 구조이다. 여러 테스트들을 작성해 보았는데 눈여겨 볼 부분이 몇 가지 있다. 게임 프로그램의 핵심은 랜덤 숫자를 통해 자동차의 전진 유무를 만드는 것 이었다. 랜덤 클래스를 쓰게 되면 테스트 시 자기 멋대로 값을 반환하므로 테스트를 하는게 사실상 불가능하다. 페어와 함께 이 문제에 대해서 논의 해 보았다. 페어의 아이디어로 이 문제를 해결하기로 하였다. 바로 랜덤 하게 동작하는 부분을 인터페이스로 분리하고 프로덕션 코드와 테스트 코드에서 그것을 generate하는 클래스들을 각각 작성하는 것이 었다.
public interface NumberGenerator {
int MAX = 9;
int MIN = 0;
int generateNumber();
}
이런식으로 간단히 인터페이스를 정의하고 generateNumber()를 오버라이딩하게 강제한다.
그렇다면 저것을 구현하는 실제 프로덕션 코드의 클래스는 이런식이 될 것이다.
public class RandomNumberGenerator implements NumberGenerator {
@Override
public int generateNumber() {
return new Random().nextInt(MAX + 1);
}
}
그리고 저것을 구현하는 테스트 코드의 클래스는 이런식이 될 것이다.
public class TestNumberGenerator implements NumberGenerator {
private int[] numbers;
private int index;
public TestNumberGenerator(int[] numbers) {
this.numbers = numbers;
}
@Override
public int generateNumber() {
return numbers[index++];
}
}
내 임의대로 랜덤 값이 어떻게 나올것이다 라고 하는 것을 정하고 그것을 생성자 인자로 넘겨주어 그대로 반환하게 만든다. 즉 랜덤 함수를 흉내내게 구현할 수 있다. 당연히 객체 생성은 저 인터페이스의 자료형으로 생성을 할 것이다. 같은 참조형으로 다른 일을 하게 되는 것이다. 이 주제에 대해서는 앞으로 진행될 코스의 내용에 들어있다. (인터페이스오 상속)
예외 테스트를 어떻게 할 것인가
테스트 함수를 작성한 일부분을 보자.
@Test
void 비정상_이름_입력_5자초과() {
// given
String input = "pobi,crong,honuxxx";
// when then
assertThrows(IllegalArgumentException.class, () -> RacingCarUtil.isValidNameInput(RacingCarUtil.splitIntoNames(input)));
}
초기에는 이런식으로 작성을 했었다. 각 이름은 5글자를 넘을 수 없었는데 넘는다면 예외를 던지는 부분이다.
최종적으로 나는 이런식으로 구현을 했다.
@Order(3)
@ParameterizedTest
@ValueSource(strings = {"pobi,crong,honuxxx",
"pobiiiiiiiiii,crong,honux",
"thelongestnameintheword"})
void 비정상_이름_입력_5자초과처리(String inputs) {
assertThatIllegalArgumentException().isThrownBy(() ->
RacingCarUtil.checkValidNameInput(RacingCarUtil.splitIntoNames(inputs)))
.withMessage("이름은 5자를 넘을 수 없습니다");
}
물론 직접 AssertJ를 공부하고 나서 리팩토링을 한것이다. AssertJ에 대한 내용은 상단의 AssertJ에 대한 정리 링크를 눌러서 보자.
@Order를 쓴 이유는 밑의 사진 처럼 테스트 막대들을 볼 때 정렬되게 보이게 끔 하기 위해서 썼다. 정상과 비정상을 구분하기 위해서 쓴 것이다. 피드백을 어떻게 받을지는 잘 모르겠지만 일단은 가독성이 좋아지게끔하는것이 좋아보여서 저런식으로 했다.
@ParameterizedTest는 여러개의 테스트를 한 번에 할 수 있도록 파라미터들을 지원하는 애노테이션이다. 보는 것처럼 @ValueSource에 테스트 string들을 명시해 놓았다.
assertThatIlegalArgumentException()은 AssertJ에서 자주 발생하는 예외를 다루기 위해 특별히 맞춤 서비스를 한 함수이다. 문서에서 보고 신기해서 써 보았다. 그리고 예외 처리 메세지를 withMessage로 검증까지 할 수 있다. AssertJ 의 강력함을 볼 수 있다 :)
강의: 종합 피드백
컨벤션은 매우 중요하다.
컨벤션의 중요성을 강조했다. 코드의 첫 인상은 컨벤셔이라고 한다. 공백 까지도 컨벤션이다.
"문맥이 바뀔 때 공백을 띄워놓으면 나중에 메서드를 분리하라는 힌트로 쓸 수 있다." - 포비
이번 강의 때 깨달은 중요한 점 들 중 하나이다. 개행을 적절히 해야 나중에 메서드 분리의 힌트로 쓸 수 있다는 것이다.
이름 짓기의 팁
예를들어, 클래스의 리스트를 만들 때를 생각해보자. Animal이라는 클래스에서
List<Animal> animalList = new ArrayList<>();
이런식의 코드를 작성했다고 가정하자. 이 코드의 문제점이 있다.
바로 나중에 자료형이 바뀌게 되면 변수명까지 수정해야 한다. List가 아닌 Set이 된다면 animalSet으로 바꿀것인가? 그러지말고 animals라는 복수형으로 이 변수를 표현하는게 훨씬 낫다.
final 사용해 변수의 값 변경 막기
최근 언어들은 변수의 불변이 기본이라한다. 그래서 메서드의 인자를 final로 구현하는 습관이 좋다고 한다.
테스트에 관한 팁
- 경계 값을 기준으로 테스트하는 것이 좋다.
- 도메인 테스트용 클래스를 따로 만들어서 할 수도 있다.
다수의 생성자
다수의 생성자를 구현할 때에는 인자가 적은쪽에서 많은쪽으로 한다. 그리고 초과인자들은 디폴트 값을 넘긴다. 예를들어 내 과제의 코드를 보면,
public Car(String name) {
this(name, 0);
}
public Car(String name, int position) {
if (!isProperName(name)) {
throw new IllegalArgumentException("자동차 이름이 적절하지 않습니다");
}
this.name = name;
this.position = position;
}
이런식으로 생성자의 인자가 많은 쪽을 적은쪽에서 불렀다. 그리고 position의 초기값인 0을 넘겨준다.
왜 자꾸 작게 쪼개는가?
메서드를 하나로 길게 쓰는 것이 여러 개로 쪼갰을 때보다 성능이 좋다. 그런데 왜 자꾸 나누는것인가? 성능이 좋다는게 아주아주 미미한 차이이다. 쪼갰을 때의 이점이 (클린코드, 유지보수의 장점, 가독성 등등) 훨씬 많다.
"작은 클래스가 의미가 있을 수 있다. 하지만 성능이 떨어질 수 있는데 매우 미미하다. 예전에는 하드웨어가 비쌌기 때문에 요즈음은 그렇지 않다. 그래서 작게 쪼개는 작업이 의미 있다" - 포비
변수이름의 중복이 생긴다면 쪼개는 신호로 받아들여라
아래는 나의 코드였다. 보는 것처럼 이름들을 받아서 쪼개면 그것이 String 배열에 들어가고, 결국에는 List에 담아져서 반환하게 된다.
@Override
public List<String> promptUserNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
String input = scanner.nextLine();
String[] splitNames = RacingCarUtil.splitIntoNames(input);
if (!RacingCarUtil.isValidNameInput(splitNames)) {
return onInvalidUserNames();
}
List<String> splitNameList = new ArrayList<>();
Collections.addAll(splitNameList, splitNames);
return splitNameList;
}
String[] splitNames와 List splitNamesList 를 보자. 처음에는 splitNames를 썼고 그 다음 List 변수의 이름도 splitNames를 쓰려고 했지만 중복이라 어쩔 수 없이 splitNameList를 썼다. 이 처럼 변수의 이름이 중복이 생길거 같으면 분리의 신호로 받아들이고 메서드를 분리해야 한다.
@Override
public List<String> promptUserNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
String input = scanner.nextLine();
String[] splitNames = RacingCarUtil.splitIntoNames(input);
return parseStringArrayToList(splitNames);
}
결국 각 기능을 메서드들도 분리하였다.
클래스내 메서드 배치 순서는 public, private (public이 사용하는 함수) 순으로 해보자
public 만 몰아넣고 private만 몰아넣게 되면 가독성이 떨어질 수 있다. private 메서드를 그것을 사용하는 public 메서드 바로 밑에 위치시키면 가독성이 증가한다. 그리고 둘 다 쓰이는 곳이 있어서 배치가 애매해질 경우는 더 분리하라는 신호이기 때문에 코딩에 더 도움이 된다.
강의 내용중 제일 좋았던 부분
한 가지 형태를 정답이라고 간주하지 마라. - 포비
정답이 하나라고 접근하는 것을 지양했다. 많이 나아졌긴해도 여태 대부분 주입식 교육을 받아서 자꾸 답을 찾으려는 습관이 알게모르게 남아 있는 것 같다. 여러가지 해결책을 찾을 수 있게 이끌어줘서 너무 좋았다.