[우테코 프리코스] 4주차 미션 회고 (java)

3Beom's 개발 블로그·2022년 11월 27일

본 글은 우아한 테크코스 4주 차 미션을 수행하면서 기록했던 회고록이다.


지난 3주 차 미션에 이어 마지막 4주 차 미션까지 정말 열심히 했다 ㅎ..
-> 3주 차 미션 회고

4주 차 미션


-> 우아한 테크코스 프리코스 4주 차 미션 java-bridge
-> 필자가 제출한 코드

합격 여부를 떠나(사실 떠날 수 없다..ㅠ) 프리코스를 통해 정말 정말 많이 성장한 것 같다..!

처음엔 Java 언어를 활용한 프로그래밍이 거의 처음이라 많이 헤매기도 하고 막막했었는데 4주 차 미션까지 열심히 열심히 하고 나니 지금은 Java가 제일 익숙한 것 같다 ㅎ 뿌듯하다..!

4주 차 미션의 목표는 다음과 같았다.

  1. 클래스(객체)를 분리하는 연습
  2. 리팩터링

4주 차 미션도 3주 차와 똑같이 미션의 목표와 3주 차의 공통 피드백 내용을 토대로 '나만의 미션'을 설정해서 진행했다.

본 회고록도 '나만의 미션'을 혼자서 실천해 보며 느낀 점을 기록한 내용이다.

나만의 미션과 회고


1. 미션 회고 목록

3주 차 개선, 유지

  • TDD 방식 유지하기
    • 아주 작은 단점 해결
    • 개발 중 경험한 테스트 코드의 장점
  • MVC 패턴 유지하기
    • MVC 패턴을 활용한 내용
    • 주어진 5개의 클래스에 대한 고민
  • 메소드, 클래스 분리에 대해 고민하기
    • 클래스 분리 과정에서 느낀 점
    • 메소드 분리 과정에서 느낀 점
  • 기능 목록 양식 활용, 완성
    • 3주차에 만든 '나만의 기능 목록' 양식 활용 및 최종 완성

4주 차의 새로운 시도 (개발 전 설정)

  • 변경되지 않는 필드에 final 키워드 붙이기
    • final 키워드 활용시 얻을 수 있는 장점
  • 필드(인스턴스 변수) 수 줄이기
    • 필드 수 줄이기 위한 노력과 결과
  • 테스트 코드 리팩토링
    • 인터페이스+익명클래스의 또다른 기능
    • @ParameterizedTest

4주 차의 새로운 시도 (개발 중 설정)

  • Model static화
    • static으로 설정하게 된 이유
    • static으로 설정하여 느끼게 된 장단점
  • Model 내에서 처리 가능한 로직 고려
    • 객체는 객체스럽게
    • Model 내에서 스스로 처리한 로직
  • Enum 적극 활용
    • Enum 활용 내용
    • Enum의 장점

2. 3주 차 개선, 유지

2.1 TDD 방식 유지하기

  • 2, 3주차에 이어 4주차까지 TDD 개발 방식을 유지하기 위해 노력했다.
  • 지금까지 느꼈던 TDD 방식의 단점 중 아직 구현되지 않은 기능에 대한 테스트 코드를 작성하다 보니 빨간 줄이 계속 떠서 거슬렸다는 아주 작은 문제가 있었는데, 이를 다음과 같이 해결하였다.
    • 테스트 코드를 먼저 작성하기 전, 기능이 구현될 클래스와 메소드를 미리 선언만 해두는 것이다.
    • 그 전까지는 테스트 코드를 작성하면서 네이밍도 생각했었는데, 이렇게 미리 선언을 해두고 테스트 코드를 작성하니 네이밍 과정이 좀 더 수월해지는 느낌을 받았다.
    • 빨간 줄도 없어져서 마음이 편안했다.
  • 미션의 난이도가 올라가며 TDD 방식의 또다른 장점을 찾을 수 있었다.
    • 개발 초반에는 기능 목록에 맞추어 개발을 진행하다 보니 기능 목록을 보며 테스트 코드를 작성했지만, 개발이 거듭될 수록 기능 목록 내용과 달라진 방향으로 구현되는 경우가 잦았다.
    • 하지만 한창 개발에 몰입해 있는 중에 기능 목록을 수정하기가 어려워 갈 수록 테스트 코드를 즉흥적으로 작성하게 되었고, 곧 테스트 코드가 기능 목록의 역할도 수행하게 되었다.
    • 물론 개발 중 기능 목록을 수시로 최신화 하는게 가장 좋겠지만, 테스트 코드도 하나의 기능 목록 역할을 수행할 수 있다는 장점을 느낄 수 있었다.
    • 테스트 코드도 기능을 나타내는 문서의 역할을 한다는 말이 사실이었고, 이를 몸소 느껴볼 수 있어 기분이 좋았다.

2.2 MVC 패턴 유지하기

  • 3주차에 이어 4주차 미션에서도 MVC 패턴을 적용해 보았고, 이번에는 좀 더 서버 개발 개념(?)에 맞추어 구성해 보았다.

    (특히 Model의 경우, DB 성격을 넣으려고 static으로 설정했는데, 이에 대한 자세한 내용은 4.1 Model static화에 기술된다.)

    • Model
      • 생성된 다리를 저장
      • 건넌 기록을 저장
      • 게임 결과를 저장
    • Controller
      • 게임 운영
      • 게임 운영에 필요한 로직
    • View
      • 사용자 입력 기능
      • 출력 기능
  • 4주차 미션에서는 주어진 클래스의 개수가 더욱 늘어나고 각각의 요구 사항도 서로 달라 이를 염두하며 각자의 역할을 설정하는 과정에서 매우 많은 고민이 동반되었다.

    • 주어진 클래스들의 요구 사항들을 보며 '왜 이렇게 설정하셨을까'를 계속 고민하며 의도를 파악해보려 했다.
  • 내가 개발한 구성이 코치님들의 의도에 맞았는지는 모르겠지만, 나름대로 최선의 로직을 구성하려 많은 노력을 기울였다.

    • 특히 BridgeGame에서 InputView와 OutputView를 활용하면 안된다는 요구 사항이 가장 어려웠다.

      (내 생각엔 게임 운영에 필요한 로직과 게임을 운영하는 로직을 분리하라는 의도였던 것 같다.)

  • 각 클래스마다 주어진 요구 사항과 나의 생각을 종합하여 미션을 해결하였고, 해결하는 과정에서 동반되었던 수많은 고민들이 클래스 분리의 필요성과 MVC 패턴, 메소드 분리 등 이해도를 크게 높여주었다고 생각한다.

2.3 메소드, 클래스 분리에 대해 고민하기

  • 4주차 미션에서 가장 어려웠던 과정이었고, 특히 메소드 10줄 요구 사항 덕분에 메소드 분리가 굉장히 어려웠다.
  • 클래스 분리는 오히려 3주차 보다 좀 더 수월했던 것 같다.
    • 주어진 5개의 클래스에 대해 파악하며 내 생각에 맞추어 로직을 구성하는 과정에서 오히려 더 많은 고민이 되었다.
    • 고민을 마친 후에는 이미 클래스들이 역할에 따라 나눠져 있는 듯한 느낌이 들어 고민을 덜어주었던 것 같다.
    • 하지만 주어진 클래스들 외에도 Model, Exception 등은 하나의 클래스 안에 적은 메소드가 들어가도, 내가 생각했을 때 분리되어야 개념적으로 올바르다고 판단한 기능들은 모두 클래스를 분리하였다.
    • 클래스를 분리하며 '유지 보수에 굉장히 큰 도움이 될 것이다.' 라는 생각을 가장 많이 하였다.
      • 지금은 주어진 기능 요구 사항에 해당하는 기능들만 갖고 있지만, 만약 미션이 아니라 실제 게임 프로그램이라면 이후에 어떤 기능이 추가될지는 아무도 모르는 것이다.
      • 따라서 아무리 적은 필드와 메소드를 갖고 있더라도 개념적으로 분리되어 있으면 클래스를 분리하여 관리하는 것이 더 유리할 것이라고 생각했다.
  • 메소드 분리는 정말 많은 고민이 필요했다.
    • 이유는 위에도 기재해 두었지만, 10줄을 넘어가면 안된다는 요구 사항 때문이었다.
    • 아무리 생각해도 분명 한가지 일만 하는 것 같은데 10줄이 넘어갔던 메소드들의 경우, 이를 분리하려고 더 고민해보니 한가지 일만 하는게 아니었다.
    • 요구 사항에 맞추려 계속 고민을 하다보니 안에 숨겨진 여러 기능들이 보이기 시작했고, 이를 한번 두번 분리해 보니 이후에는 오랜 고민을 하지 않아도 분리할 수 있는 기능들이 빠르게 보일 수 있었다.
    • 저번 피드백에서 함수가 한가지 일만 하고 있는지 판단하는 기준에 대해 메소드의 최대 라인을 설정하는 방안이 예시 중에 있었는데, 이번 미션을 통해 확실히 효과를 느낄 수 있었다.
    • 클래스와 마찬가지로 메소드 내에 한 줄이 있더라도 개념적으로 분리되어야 한다고 판단되면 분리했고, 개발을 마친 후에 다시 확인해 보니 깔끔히 정돈된 느낌을 받을 수 있었다. (굉장히 뿌듯했다.)

2.4 기능 목록 양식 활용, 완성

  • 3주차에서 처음 만들어 본 '나만의 기능 목록 양식'을 4주차에도 적용해 보았다.
  • 확실히 양식을 정해두고 기능 목록을 작성해 보니 놓치는 내용을 줄일 수 있었다.
  • 이번 미션에서는 구현을 모두 마친 후, 각 기능들이 구현된 클래스와 메소드 이름을 함께 기재하였다.
  • 후에 내가 아닌 다른 개발자가 기능 목록을 참고하며 내가 쓴 코드를 보면 이해도에 도움이 될 것이라 생각되었다.

3. 4주 차의 새로운 시도 - 개발 전 설정

3.1 변경되지 않는 필드에 final 키워드 붙이기

  • 3주차 공통 피드백 내용 중, final 키워드를 활용해 값의 변경을 막는 내용이 있었다.
  • 혼자서 개발하다 보니, 변경될 수 있는 값과 변경되면 안되는 값에 대해 나는 알고 있으니 무의식적으로 final 키워드를 생략해도 될 것이라 생각했었다.
  • 해당 피드백 내용을 보고 이번 미션에서는 final 키워드를 적극 활용해 보고자 하였다.
  • final 키워드를 계속 붙이다 보니, 이는 다른 개발자가 내 코드를 봤을 때 의도를 파악하는 과정에서 도움이 될 수 있겠다는 생각을 하였다.
  • 변경되면 안되는 값에 모두 final을 붙이고, 변경될 수 있는, 혹은 변경 되어야 하는 값에는 final을 안붙이니 각 필드에 대한 이해도를 높여줄 수 있겠다는 생각을 하였다.
  • 이 외에도 개발 중 의도치 않게 변경되면 안되는 값을 변경하여 문제가 될 수도 있는데, 이러한 문제를 차단하는 역할도 할 수 있을 것이라 생각되었다.
  • 이후에도 final 키워드를 적극 활용할 것이다.

3.2 필드(인스턴스 변수) 수 줄이기

  • 3주차 공통 피드백에서 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다 의 내용을 확인하고, 본 미션에서 필드의 수를 줄이기 위해 꽤 노력을 기울였다.

  • 그 결과, Model, View를 활용하는 게임 운영 클래스(BridgeGameOperator)와 데이터를 저장하는 Model의 클래스 외에는 필드가 하나도 없다.

    • 게임 운영 클래스의 경우, Model과 View 클래스의 객체를 활용해야 하므로 각각의 객체를 필드로 두어야 했다.
      • 물론 이들도 메소드 내에서 지역 객체로 선언되어 파라미터로 전달하는 방식으로 줄일 수 있었겠지만, 필드로 두는 것이 의도를 드러내기에 더 적합할 것이라 생각하였다. (메소드 10줄 요구 사항도 한 몫 했다.)
    • Model값을 저장해야 하므로 필드가 필요했다. 너무 많은 필드들이 포함되면 클래스를 분리해야겠지만, 각각 2개씩만 갖고 있어 괜찮다고 판단하였다.
  • 필드의 수를 줄이기 위해 최대한 지역 변수를 활용하고자 했고, 다른 필드로부터 알아낼 수 있는 값은 알아내는 과정을 메소드로 구현하여 활용하였다.

  • 또한, Model 의 클래스들을 모두 static으로 설정한 것도 도움이 되었다.

    (static으로 설정한 것에 장단점이 있었는데, 이는 4.1 Model static화에서 다룬다.)

  • 이전까지 필드가 늘어날 수록 해당 필드가 어떤 용도인지, 무슨 의도를 갖고 있는지 헷갈릴 수 있겠다는 생각을 했었는데, 확실히 필드의 수를 최소화하니 복잡도를 낮출 수 있었고 가독성이 좋아지는 효과를 볼 수 있었다.

3.3 테스트 코드 리팩토링

  • 본 시도도 3주차 공통 피드백에서 테스트 코드도 코드다의 내용을 통해 적용하게 되었다.
  • 사실 3주차 미션까지도 테스트 코드를 '잘' 쓰기 위해 리팩토링을 해왔었는데, 4주차에서 특히 더 신경을 썼던 것 같다.
<인터페이스 + 익명 클래스>
  • 3주차 미션에서 하나의 메소드만 달라지고 그 외 과정은 모두 동일한 테스트에 대해 인터페이스+익명 클래스 방식으로 중복 코드를 줄였었는데, 4주차에서도 동일하게 적용하였다.

  • 적용하는 과정에서 정말 놀라운 기능을 확인했는데, 이는 바로 하나의 메소드만 갖는 인터페이스의 익명 클래스를 생성할 때, 람다식을 통해 매우 간단하게 나타낼 수 있다는 것이었다.

    • 예를들어 다음과 같이 Example 인터페이스가 정의되어 있다.
    public interface Example {
      void exampleMethod();
    }
    • 이를 익명 클래스로 활용할 때, 보통은 아래와 같이 생성한다.
    Example anonymous = new Example() {
      @Override
      void exampleMethod(String input) {
        System.out.println(input);
      }
    };
    • 하지만 Example 인터페이스는 하나의 메소드만 가지므로, 이를 상속받은 클래스는 하나의 메소드만 재정의 하면 된다.
    • 따라서 이는 다음과 같이 람다식으로 표현할 수 있다.
    Example anonymous = (input) -> System.out.println(input);
    • 위 람다식은 아래와 같이 더 간편하게 표현할 수 있다.
    Example anonymous = System.out::println;
    • 처음 재정의한 과정은 6줄에 걸쳐 표현되는데, 위 코드는 1줄로 매우 간단하게 표현할 수 있는 것이다.
  • 비록 이번 주차에서 활용한 인터페이스+익명 클래스 방식은 인터페이스가 여러 개의 메소드를 포함하고 있어 위 방식대로 구현되지 못했지만, 이 방식을 알게 되었을 때 정말 경이롭고 감탄스러웠다. (나중에 꼭 활용해 볼 것이다..!)

<@ParameterizedTest>
  • 2주차 미션에서 JUnit5와 AssertJ에 대해 공부할 때, @ParameterizedTest 어노테이션을 확인했었지만, 직접 활용해 보진 못했었다. 이번 기회에 이를 활용하여 불필요한 코드들을 줄여보고자 했다.
  • 직접 활용해 보니 지금까지 왜 안쓰고 있었는지 의문이 들 정도였다.
  • 매번 테스트 메소드 초반에 예시 값들을 설정하는 코드들이 있었는데, 이들을 모두 지울 수 있었다. 심지어 여러 개도 가능했다.
  • 비록 이번 미션에서는 @ValueSource 어노테이션을 활용하여 primitive 타입 위주로 활용해 보았지만, 이후에 @CsvSource, @MethodSource 등 다양한 어노테이션들을 활용해 볼 생각이다.

4. 4주 차의 새로운 시도 - 개발 중 설정

4.1 Model static화

  • 앞에서도 기술했지만, 본 미션에서는 Model을 DB처럼 활용해 보기 위해 static으로 설정해 보았다.

    • Model은 View와 Controller에서 모두 활용되며, 특히 이번 미션에서 Model로 설정한 클래스의 데이터들은 모두 공용으로 활용되어야 하는 것들이었다. (다리, 건넌 기록, 게임 결과)

    • 따라서 일일이 객체를 만들고 그 객체를 서로 주고 받는 것이 아니라, 아예 클래스 필드와 클래스 메소드로 설정하여 어디서든 바로 접근할 수 있도록, 그리고 모두 동일한 값이 보장되도록 설정하려 했다.

      (Model이라는 창고를 두고 모두가 공용으로 쓰는 느낌?)

  • static으로 설정했을 때의 장단점에 대해 정말 많은 것을 느낄 수 있었는데, 이는 다음과 같다.

<장점>
  • 첫번째 장점은 클래스마다 필드가 줄었다.
    • static으로 설정되지 않으면 Model이 활용되는 클래스마다 객체를 선언하여 해당 객체를 주고받아야 한다.
      • 필드로 선언하지 않고 메소드 내에서 지역으로 생성하여 주고 받아도 되지만, 다수의 메소드에서 활용되는 경우 필드로 선언되어야 할 수 있다.
    • 하지만 static으로 설정하면 클래스 필드와 클래스 메소드에 바로 접근이 가능하여 객체를 선언할 필요가 없다.
  • 두번째 장점은 메소드 파라미터가 줄었다.
    • 첫번째 장점과 이어지는 내용인데, 객체 생성이 필요 없으니, 메소드 간에 객체가 전달되는 과정이 제거될 수 있었다.
  • 세번째 장점은 개인적으로 느낀 점인데, 로직이 정돈된 느낌이 들었다.
    • 프로젝트 내에서 공용으로 활용되어야 하는 데이터들을 static이라는 공용 메모리 공간에 저장해두고 모두가 활용하고 있으니, 뭔가 개념적으로 마치 DB에서 꺼내다 쓰는 듯한 느낌을 받았다.
<단점>
  • 첫번째 단점은 매우 치명적인 단점인데, 테스트 과정이 매우 불편하다.

    • JUnit5는 메소드마다 객체를 새로 생성하며 테스트가 수행되는데, static이다 보니 객체 생성과 무관하여 static 필드 초기화가 안된다.
    • 따라서 @BeforeEach 어노테이션에 다음 내용을 담아야 했다.
      • Field 객체를 생성하여 Model의 필드에 접근 후, Accessible을 true로 바꾸어 값을 초기화 한 다음 다시 false로 수정한다.
    • 해결은 할 수 있었지만, 애초에 다른 테스트에 영향을 주는 것 자체가 매우 치명적인 단점이며, 해결한 방법도 정석적인 방법이 아닌 것 같았다.
  • 두번째 단점은 구글링을 통해 알게된 사실인데, 메모리 릭(Memory Leak) 현상이 발생할 수 있다고 한다.

    • static은 Garbage Collector의 영향 밖에 있어, 프로그램이 종료될 때까지 메모리가 할당된 채로 존재하게 된다.
    • 만약 static으로 선언한 필드에 데이터가 지속적으로 쌓이게 되어 메모리가 꽉 차게 될 경우, 더이상 사용 가능한 메모리가 없어져(Memory Leak) OutOfMemorryError가 발생하게 된다.
    • 이렇게 되면 더이상 서비스를 제공할 수 없게 된다.
    • 본 미션에서는 다리 길이의 범위가 정해져 있어 문제없이 활용될 수 있었지만, 만약 길이의 범위가 매우 크거나 다른 데이터들이 다량으로 추가될 경우, static은 활용될 수 없다.
  • 본 경험을 통해 static은 많은 장점을 갖추고 있지만, 활용하기 전에 충분히 많은 고민이 필요하다는 것을 깨달을 수 있었다.

    (두번째 단점을 보고 static이 조금 무서워졌다.)

4.2 Model 내에서 처리 가능한 로직 고려

  • 본 시도는 3주차 공통 피드백의 객체는 객체스럽게 사용한다.를 통해 추가하게 되었다.

  • Model의 클래스가 단순히 데이터 저장만을 위한 역할이 아닌, 자신이 갖고 있는 데이터를 가공하여 필요한 새 데이터를 제공할 수 있도록 기능을 심어주고자 하였다.

  • 따라서 다음과 같이 Model의 각 클래스에 기능을 심어주었다.

    • 다리 모델 (Bridge)
      • 이동 가능한지 여부를 반환한다.
      • 다리 길이 반환한다.
    • 다리 건넌 기록 모델 (CrossedRecord)
      • 건넌 기록을 자체적으로 추가한다.
      • 건넌 기록 리셋도 자체적으로 수행한다.
    • 게임 결과 모델 (GameResultInformation)
      • 시도 횟수를 set하는게 아닌, 본인이 직접 하나씩 올린다.
  • 이렇게 설정해 보니, 데이터를 저장만 하고 Getter, Setter만 가진 클래스가 아니라, 데이터를 저장도 하고 관리하는 역할도 수행하는 클래스가 된 것 같아 객체는 객체스럽게 사용한다.의 개념에 좀 더 가까워진 느낌이 들었다.

    (Model의 클래스들이 스스로 해내는 모습을 보며 기특하다는 생각도 했다.)

4.3 Enum 적극 활용

  • 1주차부터 지금까지 필요한 상수들에 대해 Enum을 항상 사용해왔다.

  • Enum은 쓰면 쓸수록 정말 좋은 기능이라는 것이 느껴져 이번 미션에서 적극적으로 활용해 보았다.

    • 다리 그리기 기호 Enum
    • 방향 Enum
    • 게임 결과 Enum
    • 안내 문장 Enum
    • 건너기 가능 여부 Enum

    (재시작 여부도 Enum으로 설정할까 하다가 비교적 적게 쓰일 것 같아 선언하지 않았다.)

  • 이렇게 Enum을 활용했을 때, 다음 두가지 장점을 느낄 수 있었다.

<Enum의 장점>
  • 첫번째 장점은 코드 정리와 가독성에 도움이 된다는 것이다.

    • Enum을 사용하지 않고 상수를 활용할 경우, 멤버 필드로 선언하거나 지역 상수로 선언되어야 한다.
    • 일회성 상수의 경우 괜찮을 수 있지만, 여러번 활용되는 상수의 경우 가독성을 해칠 수 있다.
    • 하지만 Enum으로 같은 범주의 상수들을 한 곳에 모아두면 각 상수의 역할을 빠르게 파악할 수 있으며, 코드가 정리된 느낌을 줄 수 있다.
    • 결국 가독성에 큰 도움이 된다.
  • 두번째 장점은 Enum 클래스 메소드를 활용할 수 있다는 것이다.

    • 정말 많이 활용하는 메소드가 있는데, values() 메소드이다.
    • 개발 중에 Enum의 상수들을 차례대로 탐색해야 하는 경우가 있는데 values를 활용하면 간단하게 탐색할 수 있다.
    • 이 외에도 name()이나 ordinal() 등 활용하여 개발 과정에 도움이 될 수 있는 메소드들을 갖추고 있다.
  • 세번째 장점은 예외처리를 생략할 수 있다.

    • 이번 미션에서 특히 느꼈던 장점인데, 파라미터로 Enum 타입이 전달되면 예외처리를 생략할 수 있었다.
    • 물론 해당 메소드를 활용하는 측에서 Enum 타입으로 변환하는 과정에 예외처리가 필요할 수 있지만, 이는 Enum 타입이 아니더라도 필요한 것들이다.
    • Enum 내에 정의되어 있는 열거 상수들 중 하나를 전달받기 때문에 예외가 발생하지 않을 것이라는 믿음을 챙길 수 있다.
  • 본 미션을 통해 다시 한번 Enum의 장점에 대해 느껴볼 수 있었고, 앞으로도 계속해서 애용할 예정이다.


profile
경험과 기록으로 성장하기

0개의 댓글