대외활동/우아한 테크코스 6기

[우아한 테크코스 6기 - 프리코스] 1주차 회고

oxdjww 2023. 10. 23. 03:53
728x90
반응형

Overview

우아한 테크코스 6기 백엔드에 지원하게 되었다.
이번 6기는 서류를 지원하기만 하면 4주간 진행되는 프리코스에 참여할 기회를 얻게 된다.
기존에는 프리코스만 가볍게 체험해볼 생각으로 원서를 썼으나, 내게 필요한 교육과정이라는 생각이 들어 지원서도 열심히 적고, 프리코스에 임하고 있다.

본 포스팅에서는 1주차 미션 숫자 야구를 수행하는 과정과 회고를 다룬다.
Github Link

미션 수행 절차

첫 주차 미션인만큼 메일과 과제 명세서를 자세히 읽어보았다.
fork, git clone을 받은 후에 트러블 슈팅을 해주었다.

요구사항 분석

먼저 전체적인 flow를 작성하였다.

그리고 구현할 기능 목록을 작성했다.



0. 들어가며

처음에 숫자야구라는 주제를 보고, 친숙한 게임이기도 하고, 규칙을 모두 알고 있어서 어렵지 않아보였다.

기능을 구현하는 것은 손쉽게 할 수 있을 것 같았고, 클래스를 분리하지 않고도 main에서 절차지향적으로 구현할 수 있었다.
하지만 객체지향적으로 최대한 코드를 구현하는 것에 초점을 두었다.

클래스를 분리하고, 기능단위로 메서드를 하나하나 분리하여 구현하고자 하였다.

1. 첫 구현 (ver 1.0)

첫 구현때는 위의 기능 목록에 충실하게 구현하였다.

크게 메서드를 트리구조로 나누자면 다음과 같다.

- BaseballGame.class
  - createBaseballGame()
  - run()
  - getUserInput()
  - resetUserInput()
  - checkBalls()
  - checkStrikes()
  - generateRandomNum()
- BaseballGameCounts.class
  - BaseballGameCounts()
  - createBaseballGameCounts()
  - isWinCondition()
  - showCounts()
  - editMessage()
  - resetCounts()

BaseballGame 객체를 생성하여 해당 객체에서 run() 메서드를 통해 게임을 진행한다.

computer의 숫자를 생성하고, 사용자로부터 userInput을 입력받아 이를 통해 게임을 진행한다.

1.1 Concerns


처음에는 우선 모듈을 분리하고, 클린 코드를 작성하는 것보다는 최소한의 지킬 것을 지키며 우선 돌아가는 쓰레기라도 만들고자 하였다.

최소한의 지킬 것들을 위해 고민을 하였던 사항들은 다음과 같다.

절차지향적(명령적)이 아니라, 객체지향적(선언적)으로 구현하려면 설계를 어떻게 해야할까?



1.1.1 객체를 객체답게

다양한 자료를 찾아보던 중, Tecoble의 한 포스팅을 읽을 수 있었다.

getter사용을 최대한 지양하며, 필요한 곳에서 쓸 수 있도록 한다.

객체에 메시지를 담는다. 즉, 메서드를 객체의 기능으로 분리하여 객체가 행동을 하게 한다.

이런 생각은 코드를 선언적으로 구현하여 이를 다른 곳에서 끌어다쓸 수 있도록 구현하고자 하였다.
그것이 바로 진정하게 객체를 객체답게 쓸 수 있는 방법이라고 생각되었다.
하향식 접근으로, Application.class(main)에서 게임을 진행하기 위한 전체적 flow를 작성하였다.

그 후, 실질적으로 게임을 진행하는 run() 메서드에서는 객체의 행동을 구현한 메서드들을 하나하나 호출하게끔 하였다.

BaseballGame은 하나의 게임이고, 이는 사용자에게 할당되는 것으로 정의하였다.

무분별한 생성자 사용을 막기 위해 생성자는 protected으로 지정하고, 정적 팩토리 메서드를 생성하였다.

1.1.2 하나의 클래스에는 하나의 책임만

게임 진행을 위한 메서드를 구현하며, strikes수와 balls수를 BaseballGame에서 관리하기엔 너무 많은 책임을 가진다고 생각되었다.

이에, BaseballGameCounts 클래스도 필요하다고 생각되었다.

BaseballGameCounts에는 무엇이 필요할까? 에 대한 고민을 해결하기 위해 이 클래스의 기능을 우선으로 생각하였다.

  • 사용자의 한 입력에 대한 strikesballs를 저장해야 한다.
  • 입력에 대해 정답인지, 오답인지 판별할 수 있어야 한다.
  • 정답이든 오답이든 strikesballs에 대한 메시지(n스트라이크 k볼)를 갖고 있어야 한다.
  • 한 게임 내에서 이 객체를 재사용할 것이기에, reset을 하는 기능이 필요하다.

기능을 모두 구현하고, 기본 테스트를 모두 통과하여 ver1.0을 완성했다.

그리고 각 클래스의 기능 별 테스트 코드를 추가적으로 개발하고자 하였다.

1.1.3 캡슐화

  • 캡슐화
    • 데이터와 코드의 형태를 외부로부터 알 수 없게 하고, 데이터의 구조와 역할, 기능을 하나의 캡슐 형태로 만드는 방법
  • 추상화
    • 클래스들의 공통적인 특성(변수, 메소드)들을 묶어 표현하는 것
  • 상속화
    • 부모 클래스에 정의된 변수 및 메서드를 자식 클래스에서 상속받아 사용하는 것
  • 다형화
    • 다양한 형태로 표현이 가능한 구조를 말한다.

객체지향의 4가지 특징 중 캡슐화를 생각해보았다.

연관있는 속성과, 기능들을 하나의 캡슐로 묶도록 하였다. 이를 통해 결합도는 낮추고, 응집도를 높이는 것에 초점을 두었다.

  • 결합도 : 다른 모듈간의 의존성 정도이다. 한 모듈을 수정하면 다른 모듈에게 전파가 되는 것을 최대한 낮추는 것
  • 응집도 : 한 모듈 내에 요소들이 하나의 목표를 위해 밀집되어 있는 정도를 나타내는 것.

BaseballGame에서는 게임의 진행 을 위한 요소만을 포함해야 하고,

BaseballGameCounts에서는 스트라이크/볼을 판별하는 등 공을 카운트하는 것 에 대한 요소들만 포함하도록 한다.

이에 BaseballGame에서는 게임의 진행을 위한 메서드를 구현하고,

BaseballGameCounts에서는 카운트 객체가 행동해야할 기능인 해당 카운트의 승리조건 판별, 해당 카운트에 해당하는 메시지 등에 초점을 맞추었다.

그렇게 ver1.0을 구현하였다.

2. 리팩토링 (ver1.1)

ver1.1의 주 변경사항은 다음과 같다.

  • 리팩토링
  • 테스트 코드 추가

2.1 Concerns

리팩토링을 위해 다음과 같은 사항들을 고려했다.

2.1.1 우아한 테크코스 코드 컨벤션 준수

우아한 테크코스 깃허브 코드 컨벤션을 intellij ide에 적용시켰다.

Mac OS 기준으로 Command + Option + L 을 사용하면 일괄 적용할 수 있었다.

나중에 팀프로젝트에서 코드 컨벤션을 정하게 된다면 이를 XML 문서로 정의하여 활용할 수 있겠다는 생각을 했다.

2.1.2 클래스 세분화를 통해 기능을 더 나누자.

ver1.0에서, 각 클래스가 가지고 있는 책임을 리스트업 해보았다.

BaseballGame

  • 게임을 진행한다.
    • 게임 데이터(볼, 스트라이크)의 처리를 다룬다.
  • 게임 화면을 보여준다.
    • 입력을 받고, 결과도 보여준다.
  • 게임 객체를 다룬다.g

BaseballGameCounts

  • 게임 데이터(볼, 스트라이크)의 기능을 다룬다.
    • 볼, 스트라이크의 개수를 설정한다.
    • 볼, 스트라이크의 개수를 출력한다.
  • 데이터에 따른 출력 메시지를 다룬다.

각각의 클래스가 너무 많은 책임을 갖고 있는 것을 파악하였다.

이에, MVC 패턴으로 이를 해결하고자 하였다.

2.1.3 일급 컬렉션화 하자

이번 미션을 수행하며 일급 컬렉션이라는 개념을 처음 접했다.

일급 컬렉션이란 아래와 같은 조건을 만족한다.

  • CollectionWrapping하면서, 그 변수가 유일한 멤버변수여야 한다.

또, 다음과 같은 특징을 지닌다.

  • 비즈니스에 종속적인 자료구조를 가질 수 있다.
  • Collection의 불변성이 보장된다.
  • 상태와 행위를 한 곳에 관리한다.
  • 컬렉션에 이름을 부여할 수 있다.

일급 컬렉션을 사용하면 한 자료구조에 대해 응집성을 보장하고, 캡슐화를 함으로서 객체지향적 특징을 잘 드러낼 수 있다.

또한, 컬렉션의 특성상 자료구조의 모음으로 이루어지는데, 이 자료구조의 모음에 유효성 검증이 필요하다면 유효성 검증을 일급 컬렉션 내에 구성할 수 있다.

이런 장점은 내가 구현하고자 하는 선언적 프로그래밍의 특성에도 걸맞고, 이 클래스를 사용하는 사람은 마음 편히 객체를 생성하여 쓸 수 있다.

그렇기에 일급 컬렉션화 가능한 클래스들을 최대한 일급 컬렉션화 하였다.

본인은 BaseballCounts, ComputerNumber, UserNumber 클래스를 일급 컬렉션으로 리팩토링하였고, 이유는 다음과 같다.

BaseballCountsstrikeball은 비즈니스 로직적으로 연관성이 있어, 응집성을 보장하고자 하였다.

또, BaseballCountsstrikeball의 수를 체크하고, 승리 조건을 확인하는 등 다양한 행위를 하기 때문이다.

ComputerNumberUserNumber는 일련의 IntegerList로 가진다.

각 자리수를 검증하는 로직이 매우 중요하며 필수적이다. 또, List로 한번 정의되면 불변적인 특징도 가진다.

또한 List보다 일급 컬렉션을 활용하여 이름을 지정하면 readable하다는 장점도 가진다.

일급 컬렉션화하는 과정에서 내가 작성하는 코드 한줄을 적을 때에도 깊은 생각을 했다.

원래는 단순히 코드를 구현할 때 막연하게 '남들도 다 이렇게 하니까'라는 이유가 대부분이였다.

하지만 이런 이유 때문에 이렇게 구현하고, 이를 활용한 장점은 무엇이 있을지 고민하는 것 자체가 너무 값진 경험이였다.

앞으로도 이렇게 내 판단에 이유를 찾는 습관을 통해 더 readable하고 clean한 코드를 작성할 수 있도록 할 것이다.

2.1.4 의미를 한눈에 파악할 수 있게 매직 넘버를 정의하자.

본 과제를 수행하며 STRIKE, INT를 일급 컬렉션의 인덱스로 받아 처리하였는데 이 인덱스들을 상수화하였다.

private static final int로 선언하였으며, 한눈에 이 변수가 무슨 의미를 가지는지 파악할 수 있도록 하였다.

private static final int BALL = 0;
private static final int STRIKE = 1;

2.1.5 유효성 검증을 따로 처리하자.

리팩토링 하는 절차에서, Validator 클래스를 따로 만들어 유효성을 검증할 때 쓸까 고민했다.

각 모델에 대한 Validator를 따로 만들어, 객체 생성시 마다 검증을 하게끔 말이다.

하지만 내가 구현한 프로그램에서는 오버엔지니어링이라는 생각이 들었다.

일급 컬렉션으로 구성한 클래스들이 있기 때문에, 일급 컬렉션의 의미를 다시 생각해보았다.

'유효성 검증 코드를 내부에 구현하여 사용자들이 유효성을 검증하는 수고를 덜 수 있다'


그렇기에 일급 컬렉션의 특징을 더 잘 지키고자 Validator를 따로 구현하지 않았다.

2.1.6 자바 코드 컨벤션을 지키자.

우아한 테크코스 문서 저장소에서 컨벤션 문서를 발견하였다.

따라서, 리팩토링을 하는 과정에서 참고할 수 있었다.

하지만 이렇게 무작정 가이드를 따라가기 전에, 이렇게 정형화된 컨벤션이 만들어진 이유가 궁금하였다.

이에 이유를 찾고자 하였다.

2.1.7 한 메서드에 오직 한 단계의 들여쓰기(indent)만 허용했는가?

인덴트 여러개로 함수를 구현하게 되면, 함수로서의 의미가 떨어진다. 함수는 기능별로 프로그램을 쪼개기 위해 있다고 생각한다.

하지만 함수가 여러개의 인덴트로 복잡하게 구성되어 있다면 어떻겠는가? 아무리 함수명을 함수의 기능을 식별하기 편하게 작성하였다고 해도, 세부 로직을 한 눈에 알아보기 어렵다. 또한, 인덴트를 줄이는 과정에서 함수를 더 함수답게 사용할 수 있었다. 기능을 더 세분화하였고, 이는 함수를 유지보수 할 때도 용이하였다. 어느 곳에서 버그가 발생했는지 파악하기 쉽기 때문이다.

2.1.8 else 예약어를 쓰지 않았는가?

사실 이 부분은 좀 의아했다. if-else 문의 궁극적인 목표가 상황을 가정하고 그 상황이 아닐 경우 예외를 다루어야 하는데, else를 쓰지 않는 것을 권장하기 때문이다. 하지만 else를 쓰지 않고 if문을 구성하니, 코드가 한결 깔끔해지는 것을 볼 수 있었다. 또, 분기문의 수를 줄여 메서드를 분리하는 과정도 더 편해질 수 있었다.

그렇다고 무작정 else문은 쓰면 안 될까? 그것은 아니라고 생각한다. 불가피한 경우에는 else문을 사용하되, 가급적 사용을 덜 하는 것이 좋다.

2.1.9 모든 원시값과 문자열을 포장했는가? 콜렉션에 대해 일급 콜렉션을 적용했는가?

이 규칙들은, 의의가 비슷하다고 생각되어 묶었다.
원시값이나, 원시값 혹은 wrapper class로 구성된 컬렉션의 경우는 변수의 자료형 자체만으로 용도를 파악하기 어렵다.
또, 각 값들에 대해서 유효성 검증이 필요하고, 비즈니스 로직을 담은 메서드를 필요로 할 경우 이를 하나로 묶어 캡슐화를 해야 한다.

이를 통해 더 객체지향적인 프로그래밍을 할 수 있다고 생각한다.

2.1.10 3개 이상의 인스턴스 변수를 가진 클래스를 구현하지 않았는가? 클래스를 작게 유지하기 위해 노력했는가?

이 규칙들을 통해 SRP를 지킬 수 있다고 생각된다. 클래스 내에 많은 변수를 할당하게 되면, 자연스럽게 클래스가 2개 이상의 책임을 질 가능성이 높아진다.
이 컨벤션에 대해 리팩토링을 거치며 고민하는 과정을 통해 클래스를 더 분리하고, 응집도를 더 높이고, 결합도를 낮출 수 있었다.

2.1.11 메소드가 한가지 일만 담당하도록 구현했는가?

위의 클래스에 대한 노력이 메서드에도 동일하게 적용되어야 한다.

미션을 수행할 때 리팩토링 하는 과정에서 UserInput에 대한 유효성 검증을 하는 메서드에서 UserInput을 검증하는 동시에 List.add()를 하는 것을 확인할 수 있었다.
메서드는 기능을 분리하는 것에 초점을 두기 때문에, 한 가지 역할을 수행하도록 List.add()의 책임을 생성자로 위임했다.

2.1.12 getter/setter 없이 구현했는가?

비즈니스 로직을 구현할 때 있어 무분별한 getter, setter 사용을 방지하기 위한 규칙이라 생각한다.

객체지향적 프로그래밍의 가장 핵심적인 가치는 '객체가 행위를 한다'라고 생각한다.

이를 잘 지키기 위해, 비즈니스 로직을 담당하는 클래스(예를 들어 service)에서 직접 변수를 호출하고, 로직을 구현하는 것은 이 가치에 위배된다 생각한다.
getter/setter를 줄이는 과정을 통해 변수를 호출하는 것이 아니라, 변수에 메서드로 구체화된 행위를 구현하여 비즈니스 로직에서 효과적으로 호출하는 코드를 구현할 수 있다.

2.1.13 메소드의 인자 수를 제한했는가?

한 메서드에서 너무 많은 객체에 의존하게 되면 수정이 너무 어려워진다.

디버깅을 할 때, 원인을 더 잘 파악하고, 코드의 유지보수성을 높이기 위해 파라미터를 최소한으로 제한한다.

2.1.14 코드 한 줄에 점(.)을 하나만 허용했는가?

물론 코드에 점이 적으면, readable한 코드가 될 수 있지만, 이의 진정한 의도는 캡슐화나 결합도와 관련되어 있다고 생각한다.

점이 많아질 수록 객체에 더 직접적인 접근을 한다는 의미기에, 모듈간의 의존성을 높여 결합도를 높인다.

또, 캡슐화를 통해 기능별로 분리하였어도 타 메서드/클래스에서 다른 메서드/클래스에 깊은 접근을 한다면 캡슐화의 의미가 옅어질 수 있다.

3. 단위 테스트

단위 테스트를 실제로 적용해본 것은 이번이 처음이기에 깊은 고민을 통해 이해를 하고, 선택하고자 하였다.

테스트 코드를 작성할 때, @Test를 사용하니 두 가지 선택지가 있었다.

import org.junit.Test; // JUnit4
import org.junit.jupiter.api.Test; //JUnit5

첫 번째 고민 : 어떤 테스트 라이브러리를 사용해야 하는가?

사용한지 오래되어 안정화된 JUnit4와 JUnit4를 개선한 최신 JUnit5가 있었다.

하지만 결정적으로 Springboot 2.2.0버전부터 JUnit5가 기본으로 채택되었기에 JUnit5를 사용하기로 결심했다.

두 번째 고민 : 어디까지 테스트 케이스를 작성해야 하는가?

기본으로 제공된 테스트 케이스는 Application에 대한 테스트한다. 게임이 제대로 진행되는지, 4 자리수 입력했을 때 예외가 발생하는지만을 체크한다.

전체적인 테스트는 우테코에서 제공해준 테스트를 이용하고, 내가 구현한 메서드에 대해서만 단위테스트를 진행했다. 하지만 어디까지 세분화하여 테스트를 진행할지 고민했다.

내가 구현한 코드의 구조에서는 public에서 private 메서드들을 이용하며 비즈니스 로직을 수행하기에 public 메서드 테스트만으로 충분하다고 판단되었다.

세 번째 고민 : 테스트 메서드의 구조는 어떻게 구성해야 하는가?

값의 유효성을 체크함으로서 비즈니스 요구사항을 만족하고, 이를 통해 유연한 소프트웨어 개발 및 유지보수를 가능케 하는 것이 테스트의 주 용도라 생각한다. 이에 인프런 강의를 통해 수강한 given/when/then 구조를 적극 활용하고자 하였다.

예외를 체크할 때, @Testexpected를 활용하여 예외를 체크하지 않고, assertThrows()를 통해 한줄에 검증하니 정말 편리하였다.

then에서 예외가 발생하는 상황을 만들고, then에서 fail()메서드를 통해 검증해야 하는 절차를 한 줄로 줄여버린 것이다.

개발자는 한 줄의 코드를 줄이기 위해 수많은 노력을 하는데, 이 때 JUnit4보다 발전된 JUnit5의 성능에 감사했다.

네 번째 고민 : 테스트 코드를 위해 프로덕션 코드를 추가/수정해도 되는가?

BaseballGameCounts 클래스에 대한 단위 테스트 코드를 만들 때였다.

UserNumber는 자체적으로 생성자를 이용해 임의로 만들 수 있지만 내가 구현한 ComputerNumber 클래스는 자체적으로 랜덤수를 generate해서 수를 생성하기 때문에 임의의 스트라이크/볼 카운트에 대한 테스트를 하기가 까다로웠다. 물론, ComputerNumber를 생성한 후 그 값을 UserNumber 생성자에 그대로 넣어주면 3스트라이크의 경우는 테스트가 가능했다.

다양한 테스트를 위해 원하는 수로 ComputerNumber를 만들 수 있는 생성자를 추가하는 것을 고려해보았다.

테스트 코드의 용도는 프로덕션 코드를 테스트하기 위함인데, 테스트 코드를 위해 프로덕션 코드를 추가/수정하는 것은 주객이 전도되는, 조금 웃긴 상황이라 생각했다.

하지만 확실한 검증을 위해 테스트 코드가 필요하니, 어떻게 해야할지 고민을 많이 하고 결론을 다음과 같이 내릴 수 있었다.

  • 우선적으로 테스트를 위한 프로덕션 코드의 수정/추가는 최대한 지양한다.
  • 하지만 다음과 같은 상황을 만족하는 예외적인 경우에서는 허용한다.
    • 비즈니스 로직에 혼동을 줄 수 있는 경우가 아니다.
    • 테스트하려는 기능이 핵심적인 기능이고, 반드시 테스트를 통해 검사가 필요하다.
    • 프로덕션 코드를 수정/추가 함으로서 기능에 사이드 이펙트를 가져오지 않는다.

감사합니다.

728x90
반응형