저는 우테코를 시작하기 전 매일 알고리즘문제를 풀고 있었습니다. 덕분에 우테코 1주차 과제를 받고, 기능 요구사항을 읽었을 때 제가 이전에 풀던 알고리즘문제와 다를 게 거의 없다는 걸 느꼈습니다. 오히려 조금 쉬운 편에 속했고, 어떤 식으로 풀어야할 지 바로 머릿속에 그려졌습니다. 어렵지 않게 미션을 수행할 수 있을 것 같다는 희망을 가지고 readme에 기능리스트를 완성한 후, 커밋하기 직전 멈칫했습니다. 커밋메시지를 이렇게 작성하는 게 맞는지 의문이 들었습니다. 자바컨벤션과 마찬가지로 커밋에도 컨벤션이 있다는 것을 들은 적이 있었기 때문에 커밋메시지를 찾아보았고, 제가 컨벤션을 지키지 않았다는 걸 알게되었습니다. 공부한 김에 제대로 공부하자는 생각에 블로그에 Angular commit메시지를 정리했습니다.
우테코 과제가 나오기 전 날 자바 컨벤션을 정리했고, 이번에 commit 메시지 컨벤션도 정리했기 때문에 이젠 자신감을 가지고 커밋을 하기 시작했습니다.
안좋은 습관1
저는 알고리즘 문제를 풀 때 시간내에 돌아가는 코드를 짜고 통과만 하면 끝이라고 생각했고, 제출 후 제 코드를 다시 볼 일이 거의 없었습니다. 남이 읽기 쉬운 좋은 코드구조에 대해 딱히 생각한 적이 없었습니다. 이번 과제를 하면서도 제가 편한 방식으로 코드구조를 짜서 커밋 직전에 발견하고 급하게 고친 적이 꽤 많습니다. 습관이 무섭다는 걸 알게되었고, 이번 기회에 안좋은 습관을 고치고 구글 컨벤션에 익숙해져야겠다고 생각했습니다. 또한 기능을 만들 때마다 커밋하는 게 습관이 되어있지 않아 작성한 코드를 없애기도 하면서 기능마다 커밋하려고 노력하였습니다. 이 또한 잘못된 습관이라고 생각하여 이번기회에 고치고자 하였습니다.
안좋은 습관2
컨벤션을 신경쓰느라 시간이 꽤 들었지만 결국 기능을 모두 완성하게 되었습니다. 하지만 다른 곳에 신경을 쓰다보니 브랜치를 만드는 걸 깜빡하였고, 이를 알게된 건 기능을 다 개발하고 main에 push를 한 이후였습니다. 저는 이전 프로젝트에서 항상 main에 푸쉬했는데, 이 습관이 나와버린 것이었습니다. main에 푸쉬한 기록이 있으면 안될 것 같아 결국 fork한 프로젝트를 삭제하였고, 다시 처음부터 개발하기로 하였습니다.
처음부터 개발해야한다는 사실에 의욕이 조금 떨어졌지만, 다시 개발하면서 더 좋은 구조로 개발할 수 있다는 생각으로 다시 개발을 시작하였습니다. 처음에는 커밋도 조심스럽게 하고, 자바컨벤션도 맞추느라 머리가 아팠는데 2번째 개발을 하니 좀 더 수월하게 했던 것 같습니다. 한 번 개발해보는 사이에 컨벤션에 조금 익숙해진 것 같았습니다. 덕분에 기능개발에 조금 더 초점을 맞출 수 있었고, 이전보다는 더 나은 코드를 만들었다고 생각합니다. (컨벤션에 익숙하지 않다면 2번 짜보는것도 나쁘지 않은 것 같습니다)
두 번째 개발을 모두 끝낸 후 코드를 봤을 때 가독성이 좋지 않은 것 같다는 생각이 들었습니다.
조금 더 객체지향적으로 만들어보고 싶다는 생각이 들었고, BaseballGame이라는 클래스를 만들어 객체지향적으로 만들고자 하였습니다.
application에서는 BaseballGame을 만들어 start()만 하면 게임을 진행할 수 있게 하였고, 다른 코드는 BaseballGame이라는 클래스 안으로 넣어 캡슐화를 했다고 생각했습니다. 하지만 이렇게 만드는 게 객체지향적이 맞는지 의문이 들었습니다. 이전에 친구가 "객체지향의 사실과 오해"라는 책을 같이 읽어보자고 하였는데, 지금이 읽어야할 때라는 걸 느끼고 책을 읽어보기로 하였습니다.
객체지향 공부하기
이 책을 읽고 제가 객체지향에 대해 전혀 몰랐다는 걸 알게되었습니다.
그냥 클래스를 만들고 인터페이스를 만들어 다형성만 구현하면 객체지향적인 줄 알았는데, 책을 읽으면서 계속 반성하게 되었습니다. 물론 책 한 권을 읽었다고 객체지향적으로 프로그래밍 할 수 있는 건 아니겠지만, 객체지향적 프로그래밍을 하기 위한 발판은 밟았다고 생각합니다.
다시 제 코드로 돌아와서 보니 굉장히 부끄러웠습니다. 그래서 제 코드를 리팩토링하기보단, 아예 새로 협력을 그려보기로 하였습니다. (책임주도 설계를 해보려고 노력하였습니다)
협력과 메시지에 초점을 맞춰 적당히 자율적이면서, 명확한 책임을 할당하도록 신경쓰며 도메인을 그려보았습니다.
- 시스템의 책임 = 사용자의 책임(게임을 한다)
- 게임 = 게임을 진행하는 책임
- 랜덤 = 랜덤넘버를 만드는 책임
- 비교자 = 스트라이크 수와 볼 수를 비교하여 각각의 개수를 출력하는 책임
- 입력 = 사용자에게 입력을 받는 책임
- 입력 검증자 = 입력이 올바른지 검증하는 책임
- 출력 = 사용자에게 결과, 안내메시지 등을 출력하는 책임
이제는 좀 객체지향적으로 설계한 것 같아 코드를 그대로 옮겨보았습니다.
처음에는 각각의 메시지를 포함한 공용인터페이스를 만들고, 메시지 구현을 위한 private 메서드와 필드를 만들어갔습니다. 구현은 미리 해놓은 상태였기 때문에 그대로 옮겨 붙였고, 객체간 관계에만 최대한 초점을 맞춰서 구현하였습니다.
main에서는 BaseballGame 객체를 만들어 start만 호출하고
여기서 여러 객체들이 상호작용하면서 협력이 진행됩니다.
이렇게 작성한 코드가 좋은 설계인지는 잘 모르겠지만, 이전보다는 나은 코드일 것 같다고 생각합니다.(얼른 코드리뷰를 받아보고 싶습니다)
그리고 이렇게 계속 책임주도설계를 연습하여 익숙해지면 나중에 tdd도 가능하다고 하였습니다. 이전에는 tdd를 어떻게 시작해야할 지 감도 잡히지 않았고, 너무 어렵다고 생각했는데 알고보니 객체지향과 연관이 깊었습니다! 1주차 과제를 객체지향적으로 설계해봤으니 아예 tdd로도 구현해보고 싶다고 생각하였습니다. 그래서 이제는 tdd를 위해 테스트에 대하여 공부해보기로 하였습니다.
이전에 단위테스트를 보던 시각
저는 이전에 spring으로 간단한 프로젝트를 진행하며 JUnit을 이용하여 테스트를 작성해본 경험이 있었습니다. 하지만 단위테스트의 필요성을 느끼지 못하여 항상 데이터베이스까지 다녀오는 통합테스트를 진행하였습니다. 제가 단위테스트의 필요성을 느끼지 못한 이유는 다음과 같습니다.
- 나는 "게시판 글쓰기"라는 하나의 큰 기능을 테스트하고싶은데, 왜 함수단위로 하나하나 테스트를 진행해야하지? 통합으로 한 번에 테스트하는 게 더 빠르고 정확하게 기능을 테스트 할 수 있을 것 같다! 함수 하나하나를 테스트하는 건 시간낭비 아닌가?
- 함수는 리팩토링 할 때마다 변하는데, 왜 매번 변하는 함수에 대해 테스트를 작성해야하지? 리팩토링 할 때마다 테스트 코드도 변경해줘야하는 게 너무 번거로운 것 같다.
저는 항상 api 기능을 모두 개발한 후, 기능검증을 위해 테스트코드를 작성하려고 하였습니다. 하지만 이렇게 하니 기능검증을 위한 하나의 통합테스트를 작성하는 게 더 빠르게 테스트할 수 있을 것 같았고, 단위테스트를 진행하는 건 시간낭비라고 생각했습니다. (실제로 단위테스트를 하려면 mock객체를 하나하나 생성해줘야했기 때문에 시간이 굉장히 오래걸렸습니다)
하지만 객체지향에서는 "메시지"라는 게 정말 중요하기 때문에 메시지를 검증하기 위해서는 단위테스트가 꼭 필요했습니다. tdd에서 이야기하는 테스트도 단위테스트를 의미했습니다. 또한 각자의 캡슐화를 생각한다면, 독립적인 메서드에 대한 단위테스트도 필수적이였습니다. 그래서 객체지향을 알게된 이번 기회에 단위테스트도 공부해보기로 하였습니다.
단위테스트에 대한 정리글
단위테스트 공부하기 전, NsTest에 대한 궁금함
JUnit으로 단위테스트를 공부하려고 했는데, applicationTest에 있는 NsTest가 궁금해졌습니다.
이 때가 일요일이었기 때문에 제출까지 시간이 꽤 넉넉했습니다. 그래서 단위테스트 코드를 작성하기 전 NsTest를 상속받아 미리 작성된 테스트코드가 무엇을 테스트하는지 분석하고 정리해보았습니다.
게임종료_후_재시작() 코드만 분석해봤을 때는 Randoms를 mock으로 하여 전체를 통합테스트하는 느낌이었습니다. 제가 원하는 단위테스트 느낌은 아니었던 것 같지만, 새로운 자바문법들도 알게되어 도움이 되었던 것 같습니다. 시간이 더 있었다면 끝까지 분석해보고 싶었지만 시간이 부족하여 일단 제 코드에 대해 단위테스트를 해보기로 하였습니다.
일단 많은 게 연결되지 않아 비교적 테스트가 쉬운 3가지 클래스에 대해 단위테스트 코드를 작성했습니다.
class ComparatorTest {
@DisplayName("3스트라이크 테스트")
@Test
void threeStrikeTest() {
//given
Comparator comparator = new Comparator();
//when
String computer = "345";
String user = "345";
//then
assertThat(comparator.getResult(computer, user).get("strike")).isEqualTo(3);
assertThat(comparator.getResult(computer, user).get("ball")).isEqualTo(0);
}
...
}
객체를 만들고, assert만 잘 활용하면 어렵지 않게 테스트를 수행할 수 있었기 때문에 비교적 쉽게 테스트를 할 수 있었습니다. 좋은 단위테스트에 대해 생각하며 assert를 웬만하면 최소화하여 만들었고, 한 가지 개념만을 테스트하려고 노력하였습니다.
3가지를 테스트한 후, 다음은 입력과 출력, 게임에 대한 테스트를 하려고 하였습니다. 하지만 연결된 클래스가 많아 mock객체를 만드는 게 필수적이어서 쉽게 손을 대지 못했습니다. 고민 끝에 생성자에 mock객체들을 넣는 방법을 떠올렸습니다.
class BaseballGameTest {
@DisplayName("정상적인 게임동작 테스트")
@Test
void goodGame() {
Input input = Mockito.mock(ConsoleInput.class);
OutPut outPut = Mockito.mock(ConsoleOutput.class);
Comparator comparator = Mockito.mock(Comparator.class);
RandomGenerator randomGenerator = Mockito.mock(DiffNumberRandomGenerator.class);
BaseballGame baseballGame = new BaseballGame(input, outPut, comparator, randomGenerator);
Mockito.when(randomGenerator.make(3)).thenReturn("123");
Mockito.when(input.getGameInput()).thenReturn("123");
Map<String, Integer> map = new HashMap<>();
map.put("strike", 3);
map.put("ball", 0);
Mockito.when(comparator.getResult("123", "123")).thenReturn(map);
Mockito.when(input.getRestartInput()).thenReturn("2");
baseballGame.start();
}
}
- BaseballGame 클래스는 내부필드값으로 input, output, comparator, randomGenerator를 가지고 있어서 이들과 독립적으로 BaseballGame 의 동작을 테스트하기 위해 이 방법을 택했습니다.
- 테스트는 통과했지만, 이 테스트 방법이 맞는방법인지는 잘 모르겠습니다.
mock객체와 테스트에 익숙하지 않아 좋지 않은 테스트코드를 짠 것 같습니다. 그래서 mock객체와 테스트에 대ㅐ 1주차와 2주차 사이에 확실히 공부하여 2주차에는 제대로된 테스트를 작성하고 싶습니다. 아쉬운 점도 많았지만 배운 것도 많았던 1주차였습니다. 2주차에는 건강관리도 잘하며 더 열심히 공부해보고자 합니다!
(이 게시글의 작성일자는 10월 25일이지만, 10월 26일 자정 12시에 공개로 전환하였습니다. 1주차 미션 진행중에는 다른 분들이 원치않게 제 글을 읽게될 수 있어서 비밀글로 유지하였습니다.)
'기타 > 우테코' 카테고리의 다른 글
[우테코] 테스트를 작성하는 이유에 대한 생각 (0) | 2023.11.08 |
---|---|
[우테코] 2주차 소감문 (0) | 2023.11.01 |
[우테코] 1주차 숫자야구게임 강의 배운내용 정리 (0) | 2023.10.27 |
[우테코] 1주차 ApplicationTest의 코드분석 (0) | 2023.10.25 |