[1단계 - 사다리 생성] 도라(추서연) 미션 제출합니다.#315
Conversation
Hyunta
left a comment
There was a problem hiding this comment.
안녕하세요 도라!
사다리 미션으로 만나게 되서 반갑습니다, 아서입니다👋🏻
페어와 미션을 진행하면서 나눈 내용들을 같이 공유해주니 어떻게 미션을 진행하셨는지 보여서 좋았습니다. 토론을 해서 개발 방향을 정하고 진행한 내용은 인상적이었습니다.
전반적으로 잘 짜주셨는데 몇가지 개선사항 커멘트 남겼습니다.
확인부탁드릴게요!
|
|
||
| private Players preparePlayers() { | ||
| List<String> playerNames = InputView.askPlayerNames(); | ||
| return Players.create(playerNames); |
There was a problem hiding this comment.
| return Players.create(playerNames); | |
| return Players.from(playerNames); |
정적 팩토리 메서드를 쓸 때 컨벤션을 따라지으면 더 좋습니다.
There was a problem hiding this comment.
이전엔 컨벤션 관련해서 사실 create과 from/of 중 뭘 쓰던 상관 없다 생각했었습니다.
말씀해주신 덕에 한 번 더 제대로 찾아보니 제가 잘못 알고 있던 부분을 캐치했습니다.
우선 이펙티브 자바 책에서 정적 팩토리 메서드 네이밍 부분의 설명을 찾아 읽어보니 아래와 같이 나와있었습니다.
from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
- ex.
Date d = Date.from(instant);of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
- ex.
Set<Rank> faceRecards = EnumSet.of(JACK, QUEEN);valueOf: from 과 of 의 더 자세한 버전
- ex.
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
- ex.
StackWalker luke = StackWalker.getInstance(options)create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
- ex.
Object newArray = Array.newInstance(classObject, arrayLen)
생각 전환 1. create의 재발견: 클래스 리터럴을 매개변수로 받음
이전엔 from/of도 인스턴스를 반환하는 거고, create도 인스턴스를 반환하는 거니 별다를게 없다 생각했습니다.
여기서 create의 설명을 다시 제대로 읽어보니 instance 혹은 getInstance와 같지만 이란 말이 있었습니다.
instance의 (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만 이란 부분이 create에도 적용된단 의미였습니다.
주어진 예시론 잘 와닿지 않아서 spring-project에 검색해보았습니다.
수많은 예시들이 나왔는데, 전부 통일되게 매개변수가 있는 경우 클래스 리터럴을 매개변수로 받는 걸 확인할 수 있었습니다.
RepositoryService service = factory.createClient(RepositoryService.class);
private static final BeanCopierKey KEY_FACTORY = (BeanCopierKey)KeyFactory.create(BeanCopierKey.class);
따라서, create을 지금처럼 from/of와 구분없이 남발하면 안되겠단 인사이트를 얻을 수 있었습니다.
생각 전환 2. from/of의 재발견: 둘을 구분하는 의의
이전엔 from/of 사용을 통해 매개변수를 하나를 받는 것과 여러 개를 받는 것을 구분해 얻는 이득이 무엇인지 의문이었습니다.
의문을 해소하기 위해 위 설명 중 형변환 과 집계 라는 키워드에 주목해 일단 둘의 차이점을 인지했습니다.
보통 컴퓨터과학에서 집계란 여러 개의 데이터 항목을 하나의 결과로 결합하는 과정을 의미하고,
형변환은 한 개의 데이터를 다른 타입으로 변환하는 과정을 의미합니다.
여러 개를 사용할 때와 한 개를 사용할 때는 그 목적이 형변환과 집계로 다르기에 구분하는 데에 의미가 있다고 납득할 수 있었습니다.
from 과 of 라는 두 전치사가 자연어로 사용될 때도 생각해본다면,
from은 주로 단일 출처를 가리킬 때 사용되고,
of는 주로 여러 조각 중 하나와 같은 복수의 구성 요소를 가리킬 때 사용됩니다.
from/of 컨벤션을 따르면 자연어처럼 읽혀 개발자가 코드를 더 직관적으로 이해하는 데에도 도움이 될 것입니다.
생각 확장. createXXX
지금과 같이 from, of, create만 단일로 사용하는 경우엔 위 컨벤션을 그대로 따르면 되겠지만,
실제로 코드를 짜다보면 아래와 같은 상황도 마주합니다.
메서드 이름으로 생성하는 목적 별로 구분하고 싶고,
정적팩토리메서드가 두 개 이상이라 그 의미를 구분하고 싶을 때란 특징이 있습니다.
Lottos createSlipLottos(List<Integer> numbers) {} // 수동 로또
Lottos createQuickpickLottos(NumberGenerator numberGenerator) {} // 자동 로또
아서는 혹시 위 상황을 경험해본 적이 있으신지와
이 경우 정적팩토리메서드의 네이밍컨벤션과 상충한다는 점에 대해 어떻게 생각하시는지 궁금합니다! (❓❓❓)
생각 정리
제 생각엔 createSlipLottos과 createQuickpickLottos의 경우 컨벤션을 따라간다면
클래스 리터럴을 인자로 받지 않기에 fromNumbers와 fromNumberGenerator를 사용하는 게 더 타당할 수도 있습니다.
허나, 팀의 컨벤션으로 팀원들이 모두 동의했다면 createSlipLottos, createQuickpickLottos를 사용하는 것이
때론 더 가독성을 높일 수 있다 생각합니다.
There was a problem hiding this comment.
사고의 흐름을 잘 정리해주셨네요👍🏻
작성해주신 from, of, create 모두 저자인 조슈아 블로크가 제안한 이름이고 컨벤션이지 정답이 아닙니다.
마지막에 남겨주신 것처럼 팀의 컨벤션이 존재한다면 우선순위가 가장 높겠죠.
그런데 팀의 컨벤션을 정할 때 되도록이면 통용되는 이름을 사용하려고 합니다.
저는 of, from 만 사용하고 있습니다.
그밖의 네이밍에 대해서는 도라가 정리해주신 것처럼 사람마다 기준이 너무 다르기 때문에
그런 디테일을 고려해야하는 컨벤션은 오히려 독이 된다고 생각합니다.
제안을 드렸던건 create 를 사용하는 예제를 거의 못봐왔었어서, 조슈아 블로크가 제시한 리스트에 있다는 사실 조차 이번에 다시 보면서 알게 됐네요ㅎㅎ
| @@ -0,0 +1,16 @@ | |||
| package model.player; | |||
|
|
|||
| public record Player(String name) { | |||
There was a problem hiding this comment.
페어와 토론을 통해 이러한 시도를 하는 건 정말 좋은 도전인 것 같습니다!
앞으로도 계속 이렇게 해주세요ㅎㅎ
그런데 record를 도메인 객체로 사용하기에는 제한 상황들이 많아서 쓰시면 안됩니다.
가장 먼저 떠오른 것은 equals and hashcode가 자동으로 생성되는 점입니다. 동등성 비교를 위해서 record는 모든 필드의 값을 비교하도록 자동으로 생성해줍니다. 그런데 만약에 Id 값으로만 동등성을 비교하고 싶으면 어떻게 해야할까요?
그리고 또한 상속을 못하기 때문에 도메인 객체로 사용하기에 제한적인 상황이 생길 수도 있습니다.
이렇게 클래스안에서 자동으로 만들어주는 기능들은 사용할 때는 좋지만, 정작 유지보수해야할 때는 발목을 잡는 경우가 발생합니다.
openJdk 공식문서 에서 record의 목적이 불변 데이터를 전달하기 위한 캐리어임을 드러내고 있습니다. 그말은 지금 당장 사다리 미션에서 사용할 때는 특별히 제약이 없어서 사용할 수 있지만 언젠간 업데이트를 통해서 record의 본목적에 맞게 수정될 수도 있다는 말입니다. 또한 다른 개발자가 봤을 때 해당 클래스를 데이터를 전달하기 위해 만들었다고 생각했는데 안에 도메인 로직이 있다면 파악하는 비용이 발생합니다.

이러한 이유로 저는 model에 record를 사용하지 않을 것 같습니다.
이와 별개로 도라와 페어분이 새로운 개념을 학습하고, 우리는 적용해도 좋아 까지 도출한 과정은 적절했다고 생각합니다!👍🏻
There was a problem hiding this comment.
상세한 답변 감사합니다! 생각하지 못했던 부분들을 많이 짚어주셨네요:)
가장 먼저 떠오른 것은 equals and hashcode가 자동으로 생성되는 점입니다. 동등성 비교를 위해서 record는 모든 필드의 값을 비교하도록 자동으로 생성해줍니다. 그런데 만약에 Id 값으로만 동등성을 비교하고 싶으면 어떻게 해야할까요?
이 부분은 레코드가 equals and hashcode를 자동으로 오버라이드 하지만,
직접 오버라이드 하는 것도 가능하기에 해결 가능하다 생각했습니다!
openJdk 공식문서 에서 record의 목적이 불변 데이터를 전달하기 위한 캐리어임을 드러내고 있습니다. 그말은 지금 당장 사다리 미션에서 사용할 때는 특별히 제약이 없어서 사용할 수 있지만 언젠간 업데이트를 통해서 record의 본목적에 맞게 수정될 수도 있다는 말입니다. 또한 다른 개발자가 봤을 때 해당 클래스를 데이터를 전달하기 위해 만들었다고 생각했는데 안에 도메인 로직이 있다면 파악하는 비용이 발생합니다.
말씀해주신 부분에 구구절절 공감합니다.
record의 목적을 페어와의 토론 내내 중요치 않게 생각하고 넘긴 경향이 컸는데,
'공식문서가 목적을 명시해줬기에' 그에 맞게 사용하잔 의견은 토론을 종결시킬 만큼 합리적인 의견인 것 같습니다!
데이터를 전달하기 위한 캐리어라는 점에서 DTO(데이터 전송 객체)를 표현하기에 매우 적합하단 생각에 더 확신을 가질 수 있었네요.
말씀해주신 점들을 생각하다보니 추가로 궁금한 점이 생겼습니다! (❓❓❓)
그리고 또한 상속을 못하기 때문에 도메인 객체로 사용하기에 제한적인 상황이 생길 수도 있습니다. 이렇게 클래스안에서 자동으로 만들어주는 기능들은 사용할 때는 좋지만, 정작 유지보수해야할 때는 발목을 잡는 경우가 발생합니다.
이에 대해 생각해보다가 든 생각입니다.
record는 상속 기능을 제공하지 않기에 도메인 객체로 사용하기에 제한적인 상황이 생길 수 있단 의견은 자명한 사실입니다.
그렇다면 반례로써, 상속할 일이 없을 도메인 객체는 어떨까?란 궁금증이 생겼습니다.
생각 흐름 1. 모든 원시 값과 문자열을 포장한다
이번 미션의 요구사항이자 객체지향생활체조원칙 중 모든 원시 값과 문자열을 포장한다 라는 말이 있었습니다.
원시값과 문자열을 포장한 객체는 하나의 값만을 필드로 가집니다.
제 코드 상에서 원시값을 포장한 객체론 Player.java와 LadderHeight.java가 있습니다.
Player는 name(String) 이란 문자열을 포장하고 있고,
LadderHeight은 value(int) 란 원시값을 포장하고 있지요.
생각 흐름 2. 원시값을 포장한 객체를 VO로 표현 가능하다
이러한 원시값을 포장한 객체를 VO로도 표현할 수 있습니다. 참고
VO의 제약사항을 살펴보면 아래와 같습니다.
- Immutable: VO는 Setter를 가지지 않고, 불변함
- Equality: 두 객체가 실제로 다른 객체이더라도, 논리적으로 표현하는 값이 같다면 동등성 가짐
- Self-Validation: 원시타입을 사용했을 때 값의 유효성을 사용하는 측에서 검사했던 것과 달리, VO는 자가 유효성 검사라는 특징 가짐
제가 원시타입을 포장하기 위해 만든 Player와 LadderHeight은 위 제약사항을 만족하기에 VO 로 사용 가능합니다.
생각 흐름 3. VO를 record로 표현 가능할까? (Can I use record for value object in java?)
도메인 객체를 Entity 와 VO로 나눠 보기에 VO 역시 도메인 객체입니다.
결론적으로 도메인 객체를 VO로 표현했다면 이땐, class가 아닌 record를 사용해도 될까? 란 의문에 종착했습니다.
즉, Player와 LadderHeight이 VO라면, 지금처럼 record로 표현해도 괜찮지 않을까? 란 궁금증이었습니다.
VO의 제약사항들은 언뜻 보면 record로 표현하기 딱 좋아 보입니다.
허나, VO의 특성을 하나하나 뜯어 보면 record의 특성과 딱 맞아떨어지지 않음을 확인할 수 있습니다.
VO는 DTO와 엄연히 다른 개념이라 개념적으로 따지면 DTO처럼 데이터 전송 용도를 가진다 보기 어렵습니다.
VO는 말그대로 값 자체를 표현하는 객체이기 때문입니다.
또한, VO는 DTO와 다르게 비즈니스 로직을 포함할 수 있습니다.
아서가 말씀하신 record는 데이터 전송 캐리어이기에 record로 도메인 객체를 표현 시 얻는 단점들(목적성, 통용성)이
도메인 객체의 일종인 VO에도 똑같이 적용됩니다.
생각 정리
구글에 value object를 record로 표현해도 되는가에 대해 검색해보면, 관련 레퍼런스가 꽤 뜹니다.
record 사용 시 value object에 완벽히 fit 하게 적용할 수 있다고 표현
record의 사용 예시로 value object 언급
value object라는 직접적인 언급은 없이 simple value types(원시값)라고 언급
결론적으로 저는 VO를 record로 표현할 순 있지만,
아서 말씀대로 record의 본질적인 목적이 VO의 목적과 다르기에(이게 너무 크게 설득됐습니다)
VO를 포함한 도메인 객체는 모두 record가 아닌 class로 표현해야겠단 결론에 도달했습니다.
혹시 아서님께선 VO 관련해서도 동일한 의견이신지 궁금합니다! (❓❓❓)
There was a problem hiding this comment.
상속할 일이 없을 도메인에 대해서도 사용하지 않을 것 같습니다.
관리포인트를 두개로 나눠서 가는 비용이 꼭 필요하다고 보이지는 않아서요.
VO에서도 마찬가지 이유로 record를 사용하지는 않을 것 같습니다.
그리고 record 자체를 DTO로 쓰는 것 조차 지양합니다.
아직 라이브러리 중에 잘 호환되지 않는 경우가 있더라구요.
저는 웬만하면 class로 만들어서 사용하는게 유지보수 측면에서 제일 낫지 않을까 생각합니다.
| import model.bridge.Bridge; | ||
| import model.ladder.Ladder; | ||
| import model.line.Line; | ||
| import model.player.Player; | ||
| import model.player.Players; |
There was a problem hiding this comment.
model 쪽에 있는 객체들을 많이 사용하고 있네요.
레이어 분리를 위하여 해당 부분을 제거해보도록 합시다.
There was a problem hiding this comment.
레이어 분리를 위해선 DTO를 사용해야 할 것입니다.
현재 구조 상 모든 원시값과 문자열을 포장했기에
한 모델을 전달했을 때 그 내부의 여러 모델들이 함께 사용되는 걸 피할 수 없기 때문입니다.
이는 결국 view에 DTO 넘기기 vs view에 모델 넘기기 와 관련해 고민해보아야 합니다.
MVC 패턴에선 view 패키지의 객체가 domain 패키지 객체에 의존할 수 있지만,
domain 패키지의 객체는 view 패키지 객체에 의존하지 않는다고 말합니다.
따라서, 현재와 같이 view가 model 쪽에 있는 객체들을 사용하는 것 자체는 가능하지요.
이때 고려해보아야 하는 건 view에 DTO 넘기기 vs view에 모델 넘기기 두 방식의 장단점입니다.
view에 DTO 넘기는 방식의
장점으론 model과 view 레이어 간 결합도를 줄여준단 점이 있고,
단점으론 도메인과 유사한 코드를 중복으로 개발해야 한단 점과 변환 로직이 추가로 필요하단 점이 있습니다.
view에 모델 넘기기방식의
장점으론 코드 복잡성 감소가 있고,
단점으론 model과 view 레이어 간 결합도가 높아진단 점이 있습니다.
결국 trade-off 관계인데,
아서는 어떤 포인트에서 기존의 뷰가 모델에 의존하는 방식(뷰의 매개변수로 모델 받음)을
dto를 사용해 뷰와 모델 간의 결합도를 줄인 방식으로 바꿔야 할 임계점을 넘어섰다 판단하셨는지 궁금합니다!
저는 개인적으로 DTO 사용을 선호하는 편입니다
그럼에도 지난 미션에선 DTO 사용보단 모델에 의존하는 편이 낫다고 결론지었었습니다.
model 한 개만 import하면 되는 상황이었기 때문입니다.
이번 미션은 모델을 더 많이 import 하고 있지요.
아서의 말씀을 듣고 서비스 볼륨에 따라 이 임계점이 바뀌는 건가 싶은데,
아서도 같은 맥락에서 말씀하신게 맞는지 궁금합니다! (❓❓❓)
There was a problem hiding this comment.
우선 첫번째 가정(레이어 분리를 위해선 DTO를 사용해야 할 것입니다)에 동의하기가 어렵습니다.
DTO의 목적 자체가 레이어의 분리에 있지 않습니다. 참고
DTO는 더 큰 규모에서 말 그대로 데이터를 실어나르기 위한 패턴 중 하나인 것이죠.
DTO가 하는 역할은 Java에서 반환하는 타입으로 전달하기에는 너무 복잡한 데이터를 전달하기 위함입니다.
그런데 지금 당장 출력해야하는 정보를 나열해봤을 때 복잡한가? 저는 아니라고 생각합니다
이름 , 층별 사다리 두가지는 모두 List<String>, List<Boolean> 으로 충분히 대체 가능하다고 생각합니다.
그래서 view에 DTO 넘기기 vs view에 모델 넘기기 에 하나 추가될 수 있죠 view에 Collection 넘기기

마틴 파울러가 말하는 MVC 패턴의 구조입니다 참고
MVC 패턴의 핵심 개념은 view을 model에서 분리하고, controller를 view에서 분리하는 두가지 분리입니다.
그림에서 보이는 것처럼 view에서 model을 단방향으로 의존을 하고 있긴합니다.
왜 각 레이어는 분리되어야 할까요?
view와 model은 기본적으로 다른 관심사를 갖고 있습니다. view를 개발할 때는 UI의 메커니즘을 고려하면서 화면을 구성하는데 있죠. model은 비즈니스 정책이나 데이터베이스와 상호작용을 고려합니다. 그 사이의 간극을 메꿔주기 위해서 controller를 이용합니다. 사용자로부터 입력 받고, 모델을 조작하고, 뷰를 적절하게 업데이트 해줍니다. 그러면 Model을 통해서 가져온 정보를 가공해서 view 단에 전달해주는 책임을 가지는 곳은 view가 아니라 controller 여야할 것입니다.
현재 도라가 작성해준 코드에서는 객체를 통으로 넘겨서 view 단에서 객체들의 메서드를 이용해서 출력해주고 있습니다. 만약 그럼 Bridge.isConnected() 라는 메서드에 변경이 일어나면 Bridge 출력하는데 당장 변화가 생길 것입니다. 그럼 Model 개발하는 사람이 view단에서도 우리 로직을 사용하고 있으니까 함부로 바꾸면 안된다는 제약사항이 생기겠죠. 점점 프로젝트의 규모가 커지면 어떻게 될까요? model의 메서드를 유지보수하는데 비용이 점점 증가하게됩니다.
서비스 볼륨에 따라서 임계점이 바뀐다고 생각합니다. 지금 당장은 와닿지 않을 규모의 프로젝트라고 생각합니다.
그런데 저는 규모와 상관없이 레이어를 의식적으로 독립시키는 것이 중요하다고 생각합니다.
원시값과 래퍼타입에서의 속도는 무시해도 괜찮을 것 같습니다. 남겨주신 포인트 중에 null 과 관련된 사항만 고려해서 선정하면 될 것 같아요. 저는 개발할 때 항상 WorstCase를 기준으로 잡고 예외처리를 합니다. 원시값으로 사용했는데 null이 들어오면 어떻게 될까요? not null 인 상황은 없다고 가정을 하고 개발을 해서 의도치 않은 상황을 발생시키지 않기 위해서 래퍼타입을 사용할 것 같습니다.
저는 Controller를 인스턴스로 만들어서 호출할 것 같습니다. 메모리에 대한 이야기는 지금 하기에는 프로그램 생명주기와 Controller 생명 주기가 동일하기 때문에 할 말이 없습니다. 내부 구현에서 각각 어떻게 구현되는지를 봐야할텐데, 현재 Controller 메서드에 static을 쓰게되면 this를 참조하는 람다식을 쓸 수 없게 됩니다. 그러면 가독성이 떨어지니 구현 방식을 변경해야합니다. 지금 Controller를 외부에서 호출해야하는 경우는 Application에서 시작하는 트리거를 발동시킬 때만 필요하니 굳이 현재 구조에서 static으로 가져가서 누릴 이점은 없어보입니다. |
|
안녕하세요 아서:) 좋은 리뷰 정말 감사합니다!!
감사합니다:) |
|
리뷰 주신 부분 중 아직 반영 안한 부분은 아래와 같습니다!
리뷰 주신 부분 중 반영한 부분은 아래와 같습니다!
리뷰 주신 부분 중 학습한 내용은 아래와 같습니다!
|
Hyunta
left a comment
There was a problem hiding this comment.
안녕하세요 도라!
사고의 흐름을 하나하나 작성해주고, 정말 열심히 하시는게 많이 느껴지네요ㅎㅎ
덕분에 저도 많이 배운 것 같습니다.
남겨주신 커멘트에 답변 남겼는데 확인해보시고 이해가 안가는 내용은 DM이나 커멘트로 남겨주세요.
1단계 요구사항은 모두 충족하신 것 같아, 2단계로 넘어가시죠!
고생하셨습니다👍🏻
안녕하세요 아서, 6기 도라입니다☺️
이번 자동차 경주 미션은 #318 와 페어로 진행한 미션입니다.
잘부탁드립니다!
본인의 상황
자바
디자인
테스트
신경 쓴 부분
play(), play2()->play())궁금한 부분
(추가로 몇 개의 질문이 있는데 리뷰 시 참고해주시면 감사하겠습니다! 더 자세하겐 README의 부록에 적어두었습니다!)
null이 들어올 일이 없고, Collection이나 Generic으로 쓰는 상황도 아닐 땐 Wrapper보단 Primitive 타입을 사용하는 게 낫다라는 의견에 대해 어떻게 생각하시는지 궁금합니다!model을 record 로 표현할 수 있다면 record로 표현하는 게 낫다라는 의견에 대해 어떻게 생각하시는지 궁금합니다!이번 미션의 경우 controller를 stateless 하게 사용해도 된다라는 의견에 대해 어떻게 생각하시는지 궁금합니다!