2주차 미션이 끝난 후 유독 빠뜨린 점이 많이 보였던 이번주였다. 이 덜렁이..🥲
그래 할 수 있는 실수를 지금 미리 다 하고 있는 거지💪 앞으로 안하면 된다!
그렇게 어떤 아쉬운 점과 피드백이 있었고 어떻게 반영했는지 정리해보려고 한다!
띵동. 2주차 행운의 메일이 도착했습니다!
해당 프로젝트가 어떠한 프로젝트이며, 어떤 기능을 담고 있는지 기술하기 위해서 마크다운 문법을 검색해서 학습해 보고 적용해 본다.__피드백 문서 중
너무 세세한 부분까지 정리하기보다 구현해야 할 기능 목록을 정리하는 데 집중한다.
클래스 이름, 함수(메서드) 시그니처와 반환값은 언제든지 변경될 수 있기 때문이다.__피드백 문서 중
class A {
상수(static final) 또는 클래스 변수
인스턴스 변수
생성자
메서드
}
앗 2주차의 내 프로젝트에서 본 것 같은데... 여기 이렇게 썼었네...ㅎㅎ
왜 안되는 걸까? 그리고 그렇다면 어떻게 대체할 수 있을까? 고민 필요!🚨
PlayerMoveGroup
, PlayerMoveCollection
등을 쓸 수 있겠다!
단지 기능을 점검하기 위한 목적으로 테스트를 작성하는 것은 아니다. 테스트를 작성하는 과정을 통해서 나의 코드에 대해 빠르게 피드백을 받을 수 있을 뿐만 아니라 학습 도구(학습테스트를 통해 JUnit 학습하기.pdf)로도 활용할 수 있다. 이런 경험을 통해 테스트에 대해 어떤 유용함을 느꼈는지 알아본다.__피드백 문서 중
나의 경우 이번주차에서는 철저히 TDD 기반으로 시도해보기로 한 경우이기에,
테스트를 작성하는 이유는 필요한 기능이 있을 때 테스트 코드를 우선 작성한 후 그것이 통과하는 프로덕션 코드를 만드는 순서이기 때문이다.
테스트의 중요한 목적 중 하나는 내가 작성하는 코드에 대해 빠르게 피드백을 받는 것이다. 시작부터 큰 단위의 테스트를 만들게 된다면 작성한 코드에 대한 피드백을 받기까지 많은 시간이 걸린다. 그래서 문제를 작게 나누고, 그 중 핵심 기능에 가까운 부분부터 작게 테스트를 만들어 나간다.__피드백 문서 중
이 부분은 나름 지키고 있다고 생각한다! 이번주에도 놓지 않고 가야겠다!
- controller
- domain
- constant: RaceConstant, RandomConstant, DistanceConstant, PlayerNameConstant
- dto
- util :
- validator : ValidatorFactory, Validator(I), CarValidator, PlayerNameValidator
- convertor: Converter
- RegexPattern, Seperator
- view: InputView, outputView
그동안은 인텔리제이의 add는 자동 add를 썼고, commit은 git 메뉴탭을 이용해서 즉, GUI 방식으로 했었다.
그러다보니 한 commit에 포함되지 않아야하는 여러 변경사항을 한꺼번에 commit하는 일도 많이 있었다.
이렇게 실수를 줄이고 더 작은 단위의 add, commit을 하려면 CLI 방식으로 해볼 필요가 있다고 느꼈다! 다음주는 저 git 메뉴탭은 없는 기능이다.. 생각하고 CLI 방식으로 해보자!
- git status
: 변경된 파일 확인- git add -A(또는 add .)
: 변경된 전체 파일을 한번에 반영- git commit -m "메시지"
: 작업한 내용을 메시지에 기록- git add -p
: 부분별 add, 더 작은 단위 hunk는 's
이 코드는 디미터의 법칙을 위반한 코드지만
이 코드는 위반하지 않은 코드다. 왜 아래는 위반하지 않은 것일까?
아하! 이런 방식이었구나!
이렇게 comparable를 상속받으면 getter 없이도 비교하기가 쉬워진다!
피드백을 받지는 않았지만 개인적으로 1주차에 비해 개선이 없던 것 같아 상당히 아쉬웠던 부분이었다. 두 번 실수는 없다! 이 기회에 제대로 정리해서 다음주에는 꼭 적용해보려고 한다!
정리해본 포스팅 : 링크텍스트
오호 이런 어노테이션도 있구나!
레코드란? 불변의 데이터를 쉽게 생성할 수 있는 새로운 유형의 클래스!
'불변의 데이터'가 딱 필요한 DTO에 적용하면 되겠다!
단, 리스트를 getter로 반환할 때 unmodifiableList 적용이 자동으로 되지는 않기때문에 overide 해줘야 할 것 같다!
원래 이랬던 dto가 이렇게 간단해졌다!
이렇게 예외를 던지는 메소드를 분리하는 방법을 발견해서 나도 적용해보았다.
다만 걸리는 점은,
1. assertJ를 최대한 활용하고 싶은데 Throwable 타입은 assertThrow로 구현이 되어서 그럴 수 없다는 점
2. 여러 메소드의 exception이 아니라 한 메소드씩 exception 메소드가 만들어진다.
앞으로 적용해볼지는 고민을 해봐야겠다.
다른 분들의 코드를 보는데 테스트 코드가 꽤나 길었지만 생각보다 가독성이 괜찮았다!
given - when - then의 형식에 의해 잘 갖춰져있다면 괜찮겠다는 생각이 들었다!
테스트를 위해 자주 쓰일 것 같은 상수부터 메소드, 함수형 인터페이스의 람다식까지 중복되는 부분을 상수, 메소드로 분리해준 것을 발견했다! 유레카!
나도 적용해보았다!
assertThat
과 함께 쓸 수 있는 extracting
이라는 메소드를 발견했다.
다음에 유용하게 써봐야겠다!
Console.readLine().trim()
이렇게 입력된 값에 대해 공백을 제거하여 받는 코드도 간간히 있었다.
그런데 여기에는 고민이 필요할 것 같다.
왜냐하면 공백을 포함해서 받는 것을 예외 상황으로 볼 수도 있기에 일단 이런 방법이 있다는 것만 기록해두려고 한다!
1번. (내 방식) 컨트롤러에서 dto를 받아 거기어 get으로 풀어서 Player를 생성하고 List<Player>
로 담아서 Players 객체로 생성하게 보낸다.
2번. dto를 get으로 풀어서 Players 객체로 보낸다. Players 객체의 부생성자 부분에서 Player를 생성하고 List<Player>
로 담아서 주생성자로 보낸다.
적어놓고 보니 2번 방법이 의존성 문제도 더 추가되지 않으면서 컨트롤러의 중개역할에 충실할 수 있기에 적용해보아야겠다는 생각을 했다! 리팩토링 가자!
위의 방식처럼 컨트롤러의 의존성을 줄여보려했으나 역시 문제가 있었다.
그래서 3가지 방식으로 해보고 장단점을 비교해보고 결론을 내려봤다.
public class GameController {
private final MoveDecider moveDecider;
public GameController(final MoveDecider moveDecider) {
this.moveDecider = moveDecider;
}
public void start() {
Players players = createPlayers();
}
private Players createPlayers() {
PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
List<Player> players = playerNamesDto.playerNames().stream()
.map(Player::from)
.toList();
return Players.from(players);
}
}
----
public record PlayerNamesDto(List<String> playerNames) {
public static PlayerNamesDto from(final List<String> playerNames) {
return new PlayerNamesDto(playerNames);
}
@Override
public List<String> playerNames() {
return Collections.unmodifiableList(playerNames);
}
public class GameController {
private final MoveDecider moveDecider;
public GameController(final MoveDecider moveDecider) {
this.moveDecider = moveDecider;
}
public void start() {
Players players = createPlayers();
}
private Players createPlayers() {
PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
return Players.from(playerNamesDto);
}
}
----
public class Players {
private final List<Player> players;
private Players(final List<Player> players) {
this.players = players;
}
public static Players fromPlayerNames(PlayerNamesDto playerNamesDto) {
List<Player> players = playerNamesDto.getPlayers();
return new Players(players);
}
}
----
public record PlayerNamesDto(List<String> playerNames) {
public static PlayerNamesDto from(final List<String> playerNames) {
return new PlayerNamesDto(playerNames);
}
@Override
public List<String> playerNames() {
return Collections.unmodifiableList(playerNames);
}
public List<Player> getPlayers() {
return playerNames.stream()
.map(Player::from)
.toList();
}
}
public class GameController {
private final MoveDecider moveDecider;
public GameController(final MoveDecider moveDecider) {
this.moveDecider = moveDecider;
}
public void start() {
Players players = createPlayers();
}
private Players createPlayers() {
PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
List<Player> players = DtoConverter.getPlayers(playerNamesDto);
return Players.from(players);
}
----
public class DtoConverter {
public static List<Player> getPlayers(PlayerNamesDto playerNamesDto) {
return playerNamesDto.playerNames().stream()
.map(Player::from)
.toList();
}
}
방법 별 장,단점 비교
이 경우에는 외부 변환 클래스를 쓰는 것이 최선이다!
상수, 구분자, 예외메시지 등을 객체화해서 모아 놓았었는데 (물론, 이 방식의 개선이 필요하다)
이렇게 상수 모음을 객체에 모아넣을 때 2가지 방식을 쓰곤했다.
1번은 static 변수로 모아놓고 바로 불러쓰는 방식
2번은 아래처럼 enum으로 만들어서 getMessage해서 쓰는 방식
두 방식은 어떤 장단점이 있고, 각각 언제 써야할까?
어차피 둘 다 프로그램이 실행될 때 생성되고 그 후로는 재생성되지 않고 항상 동일한 인스턴스를 참조한다. 즉, 성능과 메모리 측면에서 둘은 유사한 동작을 하며 클래스 레벨에서 공유됨.
그렇다면 용도에 맞게 쓰면 되겠다!
이러한 코멘트를 다른 분의 리뷰에서 발견하고 나도 고민이 들었다.
그러게 왜 나는 원시 타입이 아닌 DTO로 보내는 거지?
그리하여 이번 기회에 정리해보았다!
이 메소드는 printf와 같이 줄바꿈이 안되었을 때 사용 가능하다!(빈 라인을 삽입하는 것과는 다름)
따라서, 기존에 빈 라인을 삽입하는 메소드를 만들어 쓰는 것과 혼용하여 적절하게 쓰면 될 것 같다!
skip이라는 메소드는 또 처음 보는데 한 번 알아봐야겠다!
생각해보니 그동안 outputView에 대한 검증을 안했다. 출력 검증하는 좋은 방법을 리뷰다니다 발견하여 적용해보았다!
작동 방식은 아래와 같다!
보다보니 정규식을 사용한 경우와, 간간히 세세한 예외처리로 대체한 경우 이렇게 나뉘었다.
그러다보니 생각이 들었다. 각각의 장단점이 뭐고, 난 왜 정규식 사용으로 통일했을까?
2주차의 검증 방식에서는 정규식 하나로 형식에 대한 검증을 모두 마치게 구현했는데 다시 돌아보니,
정규식 하나가 너무 많은 일을 하게 했던 것 같다는 생각이 든다.
만약 한글, 영어, 숫자, 공백은 허용하고 특수문자만 허용하는 등의 세세한 예외를 처리하기 어려운 경우, 큼직큼직한 형식 검증은 검증 메소드를 생성하고, 이러한 작은 단위의 형식 검증(문자 타입 등)은 정규식을 활용하는 등의 적절한 혼용이 필요하겠다!
아래의 순서를 프리코스 커뮤니티에서 찾게되었고 거기에 내가 조금 더해서 정리해보려고 한다!
아래의 형식이 기본이나, 서로 호출하는 메소드는 가까이에 둔다!
- static 변수
- 멤버변수
- 주생성자
- 부생성자
- static 메소드
- public 메소드
- private 메소드
- overide hashcode, equals
- getter를 가장 아래
함수형 인터페이스에는 어노테이션 @FunctionalInterface을 붙여서 해당 타입에 어긋난 부분 확인 등을 컴파일러의 도움을 받을 수 있다고 한다!
마지막에 남은 정규식은 numeric 확인 정도였는데 아니나 다를까 해당 부분의 리뷰를 통해 질문을 던지게 되었다. 이게 객체화해서 필요한 정도인가?
다시 돌아본 내 답은, 이번 프로그램 사이즈 정도에서는 그렇지 않다. 였다.
더 많은 입력이 있고, 중복된다면 객체화가 맞겠지만
(예: 숫자가 여러번 입력되는데 다 다른 Validator 에서 검증이 일어나 숫자 확인 정규식이 여러번 필요할 때)
모든 문자열을 포장해야한다는 생각이 있었고, 에러 메시지는 당연히 포장해야한다고 생각했다. 하지만 이 피드백을 보고 머리를 댕- 하고 맞은 것 같았다.
스스로 생각해보았다.
객체화가 가독성에 도움이 되는가? NO
객체화가 유지보수에 도움이 되는가? NO
객체화를 안하고, 그렇다면 에러메시지의 상수화 자체는 가독성에 도움이 되는가? NO
상수화가 유지보수와 관리에 도움이 되는가? 재사용성이 높은가? YES or NO
다시 요렇게 객체화, 상수화를 풀어서 넣어주니 보기 편하다! 클래스 구조도 복잡하지 않다!
그래서 이렇게 바꿨다! 생각도 못한 부분!
이번 미션 중 랜덤 번호를 받아 이동여부를 반환하는 클래스명을 MoveFactory
라고 지었는데
클래스명의 suffix로 Factory를 사용하는 케이스에 대해 찾아보면 좋을 것 같다는 피드백을 받고 찾아보았다.
그랬군... 객체 생성 로직이 캡슐화하는 역할이 있는 경우를 지칭하는데 난 막 썼군..!
변경 완료!
이러한 변수명과 클래스명의 중복이 간간히 있었던 것 같다.
중복 확인하기!
사실 완전히 이해가 가지는 않아서 추천해주신 강의를 추후에 들어보면서 이해해야할 것 같다!
책임을 나눌 때 의존관계를 살펴보는 것과 함께 이렇게 리뷰어분이 이야기하신 것 처럼 의인화해서 얘기해보면 부자연스러운 관리인지를 확인할 수 있을 것 같다!
InputView 나 유틸 클래스 등 객체를 생성하지 않고 사용하는 클래스들은,
생성자를 추가하지 않으면 되는 걸까? NO!
그러면 암시적 생성자가 어차피 존재하여 객체를 생성할 수 있게된다.
즉, 위와 같은 클래스들에는 private 생성자 추가하는 것을 잊지 말기!
🎯 프로그래밍 요구 사항에 보면
JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
라고 적혀있다.
사실 이에 대한 리뷰를 받기 전까지는 jupiter와 AssertJ를 크게 구분하지 않고 사용하고 있었는데 이에 대한 리뷰를 받으면서 명확히 구분되어 인식하게 되었다.
나는 좀 더 짧으니까! 더 가독성이 좋겠지! 하는 생각으로 jupiter를 많이 사용하고 있었는데
리뷰어님이 언급한 이유
1. AssertJ 가 jupiter 구현 보다 가독성이 높고, 2. 더 보편적인 방식임
를 생각해보니 요구 사항에서 괜히 권장한 게 아니었겠구나 라는 생각을 하게되었다.
assertThat()을 보다 사용하고 assertEqauls 등을 불필요하게 사용하는 것 줄여보자!
stream.iterate라는 걸 쓴 코드를 봤는데 흥미로워서 일단 검색 결과를 첨부해놓는다.
다음에 더 찾아보고 써봐야지!
선호님