[우테코 6기 프리코스] 2주차 고민 해결 과정

별의개발자커비·2023년 11월 1일
0

우테코 도전기

목록 보기
27/37
post-thumbnail

개요

2주차 미션을 진행하면서 그 때 그 때 궁금한 점과 고민을 해결하는 과정을 정리해보려고 한다.

🔎 validate 범위를 처음부터 정해놔야할까?

사실 이 중에서 PlayerName 객체 자체에서 검증할 것은 validateLength만이고 validateFormat은 InputValidator에서 검증을 할 예정이다.

그런데 PlayerName 에 대한 테스트를 한 클래스에서 하려다 보니 일단 PlayerName 객체에 검증을 모두 넣어 테스트하게 되었다.

이렇게 하다보니 나중에 InputView가 만들어지면 그 때 메소드와 테스트 코드를 다 이동시켜야하는 것이다.

현재 방식은, PlayerName에 대한 검증을 한 테스트 코드에서 끝내고 넘어가기 때문에 필요한 검증을 빠뜨릴 위험은 덜하겠지만 메소드를 이동시키는 것은 꽤나 귀찮은 작업이다.

이제 어느정도 객체와 InputValidator에서 형식 선에서 검증해야하는 내용과 객체에서 검증해야하는 내용이 구분이 되기 때문에 다음부터는 처음부터 둘을 구분해서 테스트, 구현해야겠다!


🔎 필요한 건 다 외부에서 생성해서 생성자 주입하자!

MoveFactory는 랜덤으로 생성된 숫자 크기에 따라 전진 여부 boolean값을 반환하는 객체이다. NumberGenerator는 이 객체에서만 필요하기때문에 MoveFacory 객체가 생성될 때 생성자 안에서 함께 생성하면 되지 않을까? 하는 생각이 잠깐 들었다.

하지만 1주차 미션의 PR리뷰를 보고 공부하면서 이내 이것은 객체지향의 관점에서 권장되는 방식이 아니라는 것을 깨닫는 데에는 오래 걸리지 않았다!

결국, 랜덤 숫자를 지정해서 테스트 해야하는 경우만 생각해봐도 외부에서 NumberGenerator를 생성해서 생성자 주입하는 것이 더 맞다는 결론을 내렸다!


🔎 테스트의 given, when, then 패턴?

1주차의 다른 분들의 코드에서 발견한 방식으로 다음주에 한 번 적용해봐야지!라고 생각했던 것 중 하나다!

given(준비): 어떠한 데이터가 준비되었을 때
when(실행): 어떠한 함수를 실행하면
then(검증): 어떠한 결과가 나와야 한다.

테스트의 준비, 실행, 검증의 3단계를 내가 구분하여 작성하는 것도 중요하고, 테스트 코드의 가독성을 위해서도 적용해볼만한 방식이라는 생각이 들었다!

그동안 테스트 코드 작성이 익숙하지 않아서 구현한 메소드에 일치하는 테스트를 작성하는데 급급했다면, 이번에는 테스트 코드 역시 누군가가 이해하면서 읽어야하는 문서라는 점을 생각하며 작성해보려고 했던 것 같다!


🔎 에러 메시지, 리터럴 상수 객체화 시도!

그동안 사용되는 객체에 상수를 위치시켰던 것과 다르게 1주차 피드백을 반영해 상수 모음 객체를 만들어 분리하는 것을 시도해보았다!

그랬더니 확실히 View에는 출력 흐름에 맞는 상수만 남게 되어 가독성도 더 좋아진 느낌을 받았다!


🔎 단, 무작정 방식을 통일하는 자세 경계

위를 적용해, 입력에 필요한 문구들을 모두 상수화하니 이렇게 되었었다.

하지만, 이렇게 printf의 format에 해당하는 부분을 상수화해버리니,
format의 각 자리에 어떤 타입을 파라미터로 넣어줘야하는지 상수로 올라가서 체크해서 비교해봐야하는 상황이 되었다.

따라서, 이런 경우는 오히려 상수로 관리하지 않고 직접 메시지를 보여주는 것이 가독성에 좋다고 생각한다!

역시 무지성(?)으로 하나의 방식으로 통일하려는 자세는 계속해서 경계해야할 것 같다고 느낀 부분!


🔎 View로 값을 보낼 때 도메인 객체를 그대로 보내도 될까?

1주차에는 객체 자체를 보내서 view에서 필요한 값을 꺼내서 출력하곤 했다.
하지만 기존 방식에는 이런 문제점들이 있다는 생각이 들었다.

  1. view가 다른 정보들 까지 있는 도메인 객체 전체 데이터를 알 필요가 없다.
  2. 도메인 객체 속성이 외부에 노출되면 보안 문제가 발생할 수 있다.

그렇다면 view가 알아야하는 범위의 데이터만 보내는 방법에는

  1. 컨트롤러에서 포장을 풀어서 필요한 데이터만 보내거나

  2. 필요한 데이터만 뽑아 담은 Dto를 보낼 수 있겠다.

라고 정리했고 위와 같이 적용해보았다!


🔎 뚝딱거리는 DTO 사용기

view가 알아야하는 범위의 데이터만 보내기 위해 DTO를 사용해보기 시작했다.

DTO란?

계층 간 데이터 전송을 위해 도메인 모델 대신 사용되는 객체

DTO의 역할 제한

  1. 어떠한 비즈니스 로직을 가져서는 안된다.
  2. 데이터에 대한 getter, setter만을 가져야 한다.
  3. 저장, 검색, 직렬화, 역직렬화 로직만을 가져야 한다.

처음에는 DTO로 만들었다가 비즈니스 로직이 포함되어야해서 도메인 객체로 변경하기도 하면서
뚝딱거리면서 DTO를 사용해보았다..!


🔎 유지보수가 쉽게 메시지 생성!

1주차 때는 출력 예시를 그대로 복사해서 메시지값으로 넣어주는 방식이었다면,
이번주에는 현재 프로그램 요구 수준에서 필요하지는 않더라도 유지 보수를 생각하며 메시지를 작성해보고자 했다!

예를 들어,
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)에서
이름 구분자를 슬래시 /로 변경하고 싶다면,
원래는 출력문, 에러메시지 등이 있는 클래스들을 다 다니면서 변경해줬어야 했을 것이다.

하지만 이 부분을 처음부터 상수 객체 Constant에 있는 구분자 값을 결합한 코드로 작성한다면
Constant에 있는 구분자만 변경해주면 될 것 이다!


🔎 생성자를 깨끗하게! 맑게! 자신있게!

아래는 [우테코 6기 프리코스] 1주차 피드백 정리 - 📌 생성자에서 검증하지 말라고?를 반영해보고자 한 과정이다.

  1. 생성자에 있던 validate 메소드들을 부생성자로 옮겼다.
  2. from과 같은 부생성자는 static이므로, validate 메소드들과 그 메소드가 호출하는 isNumber와 같은 메소드들도 다 static으로 바꿔줘야했다.
  3. validate 메소드와 그것들이 호출하는 메소드들이 static이 되도 괜찮은가? 하는 생각이 들었다.
  • 이 메소드들은 객체의 생성, 상태와는 상관이 없는 메소드로
  • 입력에 대한 검증만 해주면 되는 메소드들이다.
  • 즉, 이 메소드들을 static으로 만들어 클래스에서 꺼내면 객체들은 입력에 대해 신경쓰지 않을 수 있다.

오케이! 검증 메소드 static 가능! 땅땅!

함께 생각해본 것들

  1. validate 메소드들을 하나의 메소드에 담기
  • 부생성자가 여러개면 그 때마다 validate 메소드들을 다 넣는 것이 중복되므로,
  • validate 메소드들을 담는 메소드를 만들어 넣어줄 수 있겠다.
  1. convert 메소드도 객체 생성시 작동되어야한다면 validateAndConvert라는 메소드로 묶을 수 있을까?
  • 하나의 메소드에서 검증과 변환, 이 두가지 기능을 해도 될까?
  • 이전에 알게된 응집성을 떠올려 봤을 때, 유효성 검사와 변환, 이러한 작업은 서로 밀접하게 관련되어 있으므로 이 메소드가 두 가지 작업을 수행해도 될 것 같다.
  1. 부생성자와 validate 메소드들의 위치는 생성자 바로 아래!
  • 생성자, 부생성자, 생성시 필요한 검증 유틸성 메소드 들을 그룹화해 상단에 위치시킴으로써
  • 외부에서 볼 때, 객체를 생성할 때 일어나는 과정을 이해하기 쉽게 할 수 있겠다.

잠깐, static 메소드와 변수는 클래스 레벨?

이 뜻을 여러 문장으로 표현해보자면

static 메소드와 변수는
= 객체(인스턴스)와 관련이 없으며,
= 클래스 자체에 속하고 클래스가 로딩될 때 생성된다.
= 클래스 레벨에 해당하기 때문에, 객체 인스턴스와 무관하게 클래스 이름으로 호출할 수 있다.
= 클래스가 로드될 때 한 번 초기화되며, 모든 객체 인스턴스에서 공유된다.

잠깐, 객체와 클래스의 차이는?

난 그동안 객체와 클래스를 혼용하여 쓰고 있었던 것 같은데, static 메소드에 대해 알아보면서 두개가 다른 것임을 느끼게 되었다. 정리해보자면!

  • 클래스

    • 객체를 만들기 위한 설계 도면 또는 템플릿 같은 것
    • 객체의 구조를 정의하고 객체가 가져야 하는 속성(필드)과 동작(메소드)를 포함함
  • 객체

    • 클래스를 기반으로 실제로 생성된 인스턴스
    • 객체는 클래스의 특정 인스턴스라고 할 수 있고, 클래스에서 정의한 속성과 메서드를 가지고 있음

🔎 view 메소드, 인스턴스? static?

1주차 때에는 View의 메소드를 호출할 때, View 객체를 생성해 그 속의 메소드를 호출하는 방식을 사용했다.
하지만, 과연 View가 생성자를 갖는 객체일 필요가 있을까? 하는 생각이 들었다.

  1. 인스턴스 메소드 접근 방식 (기존방식)
    : View는 생성자를 갖는 객체로, 인스턴스를 통해 View 메소드를 호출한다.

  2. static 메소드 접근 방식 (고민방식)
    : View는 생성자가 없어 객체로 생성하지 않는 유틸리티 클래스로, static 메소드로 View 메소드를 호출한다.

인스턴스 메소드 접근 방식

  • View 클래스의 캡슐화
  • 객체 지향적
  • 스레드 안전성 : 여러 곳에서 동일하게 다중 스레드가 생기는 것 방지, 상태 유지 가능

static 메소드 접근 방식

  • 객체 생성을 하지 않을 수 있음
  • 객체 생성 없이 어디서든 접근할 수 있음
  • 객체 생성 및 메모리 할당이 없어서 성능에 유리함

성능의 차이는?

결론부터 말하자면, 객체를 생성해 메서드를 사용하는 것와 static 메서드의 성능 차이는 크지 않다.

미세하게 차이가 있다면 이렇게지만 무시할 정도인 것으로 보인다!

  • 인스턴스 메소드
    • 객체를 생성하고 메서드를 호출해야 하므로 일반적으로 더 높은 메서드 호출 오버헤드가 발생
  • 정적 메서드
    • 객체 생성 없이 호출할 수 있으므로 메서드 호출의 오버헤드가 낮아 실행 속도가 빠름

결론

  1. 캡슈화가 크게 필요 없는 클래스
  2. 유틸리티 메소드의 모음 역할

인 경우 static 메소드 접근 방식을 사용하는 것이 적절하다. 라고 할 수 있다!
따라서 View 클래스 출력 메소드들의 경우 여기에 해당하므로
static 메소드 방식! 너로 정했다!🎱


🔎 너무 복잡하면 밀고, 다른 관점에서 생각

처음에는 플레이어 중 최대 거리값을 구할 때 getter를 안쓰고 플레이어들의 거리값들을 하나하나 비교하기 위해서,
메소드를 2개 이용해 이중 반복문을 돌려서 플레이어 거리값이 원래 값보다 크면 최댓값을 반환하게 할 생각이었는데! 에고고... 쓰다보니 말로만 해도 복잡하다.
그럴 때는 싹 밀고 다른 관점에서 과감하게 생각해야하는 것 같다.

그렇게 플레이어 끼리 비교하는 방식에서 벗어나 새로 생각해본 건
기존 최댓값을 0으로 설정해놓고 기존 최댓값을 들고 플레이어들을 돌며 더 큰 값을 반환하게 하는 방식으로 구현하게 되었다!

뭔가 복잡하게 풀리고 있는 것 같으면 아까워하지 말고 밀자!🛺🛺


🔎 .이 많은 건 왜 문제일까? 디미터의 법칙!

결과 출력을 위한 DTO의 생성자 구현하면서 처음에 이렇게 짜보았다.

하지만, 연결된 수많은 ...를 보면서 든 생각

  1. 디미터의 법칙을 위반했군. 근데 디미터의 법칙이 정확히 뭐고 .가 왜 안되는 거지?
  2. DTO에서 값을 가져오기 위한 getter의 사용은 괜찮은 걸로 알고있는데 그렇다면 어떻게 해야하지?

디미터의 법칙

  • 객체와 그 객체와 상호작용하는 객체와의 메소드만을 호출해야한다.
  • 객체 간의 결합도를 낮추기 위해 중간 단계의 객체에 직접 접근하지 말아라

장점 (아직 이해 안감)

  • 객체간의 결합도를 낮추고
  • 유지 보수성을 향상시킬 수 있다

지키지 않는 지금과 같은 경우의 문제점

  • 호출자가 Player과 PlayerName의 내부 메소드까지 알게된다.

이렇게 타고 타고 들어가게 짜는 방향으로 개선해보았다!

이 과정에서 이렇게 getter를 써도 될까하는 생각이 잠시 들었는데, 이미 해당 객체를 속성으로 갖고 있기때문에 괜찮지 않을까라고 결론을 내렸다. 추후 보완!


🔎 불변을 최대한으로 적용해보자! unmodifiableList, final

우테코 6기 프리코스 1주차 피드백 정리 - 📌리스트-레퍼런스를-그대로-반환할-때의-문제점을 반영해서
이번주 미션에는 불변성을 최대한 보장하기 위해,
unmodifiableList와 파라미터에 final 키워드를 적용해보려고 했다!

물론 원본이 변경되는 경우를 보장할 수 없다는 점이 남아있기 때문에,

Collections.unmodifiableList(new ArrayList<>());

Immutable 즉, 진정한 불변 컬렉션으로 만들기위한 방법인 복사 생성자 + unmodifiableList()를 활용해보는 것도 차차 적용해보고자 한다!

파라미터의 final 키워드?

받은 파라미터를 수정해서 쓰지 못하게 하는 효과!

참고해볼만한 글

함수 파라미터의 final 키워드
IntelliJ에서 메소드 추출한 메소드의 파라미터에 final 키워드 자동 추가하기


🔎 priavte 메소드의 테스트

현재 getMaxDistance 메소드는 checkWinner 안에서 쓰이기 때문에 private 메소드이다.
하지만, 해당 기능은 테스트 대상이라고 생각하는데... 이런 경우 어떻게 해야할까?

공개 모듈 동작을 고민하라.
공개된 인터페이스만 신경써라.
private을 테스트해야하는 상황이면 테스트 코드든 뭔가 잘못된 상황이라는 뜻이다.

찾아보니 이런 단호박이 얘기가 많아서,
private 메소드를 테스트하려고 하는 방향에서
public 메소드로 해당 기능을 포함해서 테스트 할 수 있게 테스트 코드를 짜는 방향으로 틀어보았다!

추후 좀 더 알아보기 위해 참고할 포스팅을 남긴다.

🔎 컨트롤러 없이 메인 로직 구현 완성하기!

이번주는 처음에 컨트롤러를 구현하지 않고, 필요한 객체와 메소드들을 tdd로 구현하고,
필요한 기능이 다 완성되었을 때, 마지막에 컨트롤러로 연결만 하는 방식으로 구현해보았다.

사실 생성자에 어떤 객체를 주입할지만 결정해놓는다면 컨트롤러를 구현하지 않고 필요한 도메인과 그 속의 로직을 구현을 끝까지 다 할 수 있다고 생각했고 무사히 완료했다!

이렇게 했을 때의 장점은,

  1. 컨트롤러가 중개 역할 이상을 하는 것을 방지할 수 있다.
    : 컨트롤러 없이도 기능이 다 돌아가게 완성을 해놓았기 때문에
  2. 테스트가 쉬워진다.
    : 컨트롤러가 필요한 테스트라면 객체 지향에 어긋나게 짠 것이라고 할 수 있기 때문!

단점은,
컨트롤러 연결에 시간을 투자해야한다는 것인데,
객체를 구분하여 구현을 잘 해놓으면 연결이 크게 어렵지 않다!


🔎 부생성자 테스트, assertNotull로 충분할까

이렇게 파라미터가 원시값으로 있는 부생성자 테스트는 값을 비교하면 되지만,

이렇게 파라미터가 조금 복잡한 객체인 경우의 부생성자 테스트는 값 비교가 어려워
assertNotNull로 객체가 잘 생성되었는지, isInstanceOf로 해당 클래스의 객체가 생성되었는지 정도만 테스트해서 걸리는 점이 있었다.

검색해봤을 때는 부생성자 메소드는
1. 단순히 객체를 생성하고 반환하는 것이 목적이고
2. 별도의 비즈니스 로직이 있는 것이 아니기 때문에
이 정도의 테스트가 적절하다.는 의견이 있는 것 같고, 나도 동의하기 때문에 우선은 이렇게 구현해놓고 추후 이야기 나눠보려고 한다!


이제 여기서부터는 아쉬웠던 점!

🥲 네이밍.. 너 참 어렵다...

구현하다보니 심심치 않게 이런 네이밍을 하게된다.

playerMoveResultDtoList
이게 뭐야... 베이컨크림해물로제마라탕도 아니고...🥲

구글링도 하고 영어로 번역도 하면서 개선해보긴 했지만 아직도 부족한 점이 많은 것 같다.
찰떡인 네이밍이 딱 하고 떠오르는 능력이 있으면 얼마나 좋을까...?

🥲 이거저거 다 담아 commit...

이 부분을 리팩토링하다가 아!! 맞다 여기 리팩토링해야지 하면서 여기저기 손보다 보니
한 commit에 여러 변경사항을 포함하게 되는 경우가 왕왕 있었다.

🥲 직관적이게 커밋메시지를 써야할텐데...

3주차 미션을 시작하기 전에 다른 분들의 커밋 메시지를 보고 틀을 잡고,
3주차 미션 때는 커밋을 하기 전에 메시지 예시를 충분히 보고 공들여서 커밋을 해봐야겠다! 꼭!!

🥲 커밋메시지의 본문을 작성해야할까?

이 부분은 내가 그동안 커밋메시지의 본문의 존재를 인지하고 있지 못하다 갑자기 인식이 되어서 든 궁금증이었다.
이번 PR 리뷰 올리면서 질문글을 함께 올려봐야겠다!!

🥲 리스트의 csv 테스트 적용 문제

위처럼 CsvSource를 사용해보려고 했으나 win1 첫번째 객체만 리스트에 담기고 나머지는 담기지 않는 결과가 나온다. 일단은 아래처럼 중복되는 버전으로 구현했지만 해당 문제를 해결할 수 있는 방법이 있는지 더 찾아봐야겠다!

0개의 댓글