Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[부나] 코틀린 자동차 경주 제출합니다. #47

Merged
merged 58 commits into from
Feb 11, 2023

Conversation

tmdgh1592
Copy link
Member

첫 페어 프로그래밍이라 낯설었지만 우테코에서 처음 하는 활동인 만큼 의미있는 시간이었습니다.
혼자보다는 두 명이서 코드를 작성하다보니 집단지성이 생기는 느낌이었습니다..!

저희 페어는 특히 주어진 요구사항 외에도 중복되는 자동차명이 입력되는 경우 예외가 발생하도록 하였는데요!
여기서 중복에 대한 검증을 InputView에서 하기 보단, 가능한 domain에서 처리하는 것이 좋지 않을까 라는 생각이 들었습니다.

이럴 때에는 service layer에서 repository를 통해 데이터베이스처럼 관리하는 것이 좋을까요?
만약, 값을 insert할 때 이미 값이 있는지 확인하기 위해 기존 car 객체들을 모두 불러와 확인하는 방식처럼요!

좋은 방법이 있다면 의견을 듣고 싶습니다 :)

@namjackson
Copy link

저희 페어는 특히 주어진 요구사항 외에도 중복되는 자동차명이 입력되는 경우 예외가 발생하도록 하였는데요!
여기서 중복에 대한 검증을 InputView에서 하기 보단, 가능한 domain에서 처리하는 것이 좋지 않을까 라는 생각이 들었습니다.
이럴 때에는 service layer에서 repository를 통해 데이터베이스처럼 관리하는 것이 좋을까요?
만약, 값을 insert할 때 이미 값이 있는지 확인하기 위해 기존 car 객체들을 모두 불러와 확인하는 방식처럼요!

Car에 대한 정보가 1회성 사용으로 한정된 미션이기 때문에,
현재 미션에서는 repository를 통해 관리하는 방법은 좋은방법은 아닌거 같단 생각이 들어요!
현재미션에서는 List를 관리하는 책임이 있는 클래스에서 입력된 names로 cars를 생성할때만 체크하도록하면 어떨까요?

Copy link

@namjackson namjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 부나님! 자동차 경주 미션을 함께하게된 잭슨(조남재)라고 합니다
잘부탁드립니다! 😃
첫번째 미션 고생하셨습니다!
질문 주신 부분과 몇가지 고민해볼만한 포인트들 코멘트 남겼으니, 확인해주세요!

.editorconfig Outdated
[*.{kt,kts}]
disabled_rules=import-ordering
# java.* 패키지를 의존하는 경우 IntelliJ의 Orgarnize Import 기능으로는 알파벳 순서대로 import 구문을 정렬할 수 없다.
# 이는 ktlint의 import-ordering 규칙과 맞지 않는다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 시원시원하네요😭👏
마지막에 개행 문자를 추가하는 것에 대해 의문이 있었는데 일부 환경에서는 EOF이 없으면 컴파일 에러가 발생하는군요!

추가로 .editorconfig를 수동으로 작성해서 추가해야 하는 줄 알았는데 Cmd + N 단축키로 쉽게 생성할 수 있다는 것도 알게 되었습니다!
감사합니다~🙏

class RacingController(
private val inputView: InputView = InputView(Validator()),
private val outputView: OutputView = OutputView(),
private val racingService: RacingService = RacingService(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자 주입방식과 기본값 👍

}
}

private fun createCars(carNames: List<String>) =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반환값이 있는경우에는, 반환타입을 명확하게 표기해주는 습관이 좋습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반환값이 있는 경우에는 가독성 측면에서 항상 반환 타입을 작성해주는 것이 좋을까요??
어떤 글에서는 '안 쓰는 것이 좋다', '메서드를 보고 알 수 있다면 작성하지 않아도 된다' 등 다양한 의견이 있어 혼동이 생깁니다..!

현업자의 관점에서 보셨을 때 추천하시는 방식이 있으실까요??🙂

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 스타일의 차이가 있을수 있지만,
저는 간단한 함수라도 모두 작성하는 편입니다.

코드가 많아지다보면, 해당 함수에 대한 확인을 위해 구현부까지 보지않고, 함수명, 파라미터, 리턴값 정도만 확인하는데,
어떤 타입이 반환되는지 직관적으로 알수있는 구조가 아니라면, 가독성 측면에서는 명시적으로 써주는것이 좋을것 같아요!
또한 Int로 생각했는데 실제 값은 double로 반환된다거나하는 실수도 사전에 막을수 있을거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변해주신 글을 읽어보니 공감되는 부분이 많습니다!
다들 스타일이 다르겠지만, 가독성 높은 코드라면 대강 함수명, 인자, 반환타입만 보고도 제 3자가 봤을 때 대강 파악할 수 있어야 한다고 생각하는데 그러기 위해서는 작성해주는 것이 좋은 방법이라고 생각됩니다👏

return false
}

override fun toString() = name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kotlin에서는 따로 getter함수를 생성해주지 않으셔도 됩니다.
name이 필요한경우, name은 불변변수이기 때문에, 외부에서 바로 접근해도 괜찮을거같아요

한번 공식문서를 참고하시면 좋을거같아요!
https://kotlinlang.org/docs/properties.html

Copy link
Member Author

@tmdgh1592 tmdgh1592 Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 이 부분도 헷갈리던 점이었는데 명쾌한 답변 감사합니다.
굳이 toString() 으로 재정의할 필요 없이 val 키워드로 불변을 보장하니 name에 바로 접근해도 되는군요!
'getter setter 를 지양하라.' 라는 글을 보고 getter는 어디까지 가능한 것인가에 의문이 있었는데 조금 더 고민해봐야겠습니다🥲

return result
}

fun getPositionAsDash() = "-".repeat(position)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"-" 문자열같은 매직넘버는 상수로 관리해보면 어떨까요?
관리측면이나, 가독성 측면에서 좋을거 같아요!
+
해당 코드는 View와 관련된 코드라고 볼수도 있을거 같아요!
한번 고민해보셔도 좋을거 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매직 리터럴, 매직 넘버를 제거하는 것이 좋아보이네요!
중요한 것을 놓치고 있었는데 알려주셔서 감사합니다!
+
다시 살펴보니 비즈니스 로직보다는 View에 보여주기 위한 메서드로 보이네요..!
OutputView에서 처리하도록 리팩토링 해보겠습니다~

@@ -0,0 +1,32 @@
package racingcar.utils

class Validator {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validator을 분리하셨군요!
목적에 맞게 검증관련 코드를 모았지만,

클래스이름과 역할이 너무 광범위해지는 단점도 있는거 같아요!
또한 해당 클래스를 사용하는곳에서는 Validator에 대한 강한 의존이 생기게되는 단점도 있을수 있을거같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validator에 대한 의존도가 높아지는 것을 고려하지 못했던 것 같습니다 🥲
단순히 Validator를 사용하는 것보다 각 model에서 init을 통해 객체가 생성될 때 검증하는 방향으로도 생각해보겠습니다~!

}

@Test
fun testCarMovement() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구체적인 테스트 케이스명을 작성해주시면 좋을거같아요!
프로젝트 규모가 커질수록, 테스트코드가 많아질수록,
구현부를 살펴보기가 힘들기 때문에, 테스트 케이스명으로도 어떤 테스트인지 파악할수 있으면 좋을거 같아요!
또한 테스트 케이스가 구체적으로 작성된다면, 요구사항 명세서처럼 확인할수도 있을거같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변해주셔서 감사합니다!
말씀해주신대로 테스트 케이스명이 모호하네요..!
구체적인 네이밍을 통해 어떠한 상황에 대한 테스트인지 수정해보겠습니다!


@BeforeEach
fun beforeEach() {
car = Car("otter")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Car객체가 파라미터에 따라 원하는 에러가 발생하는지,
객체가 잘생성되는지 등의 테스트 코드가 추가되면 좋을거같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체 생성시 검증 코드를 추가하고 자동차 객체 생성에 대한 테스트 코드를 추가로 작성해보겠습니다 :)


@ParameterizedTest
@ValueSource(strings = ["soooodal", "buuuuuuuna", ""])
fun `자동차 객체 생성 예외 테스트`(carName: String) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공백이나 5글자이상의 이름인 경우, createCar시 , IllegalArgumentException가 발생한다와 같이 구체적이면 좋을거같아요!
부나님만의 규칙을 만들어보세요!
저는 Given-When-Then표기법을 즐겨 사용합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 예외 상황에 대한 테스트인지 명확히 작성해주는 것이 좋을 것 같군요~
추가로, Given-When-Then 표기법에 대해서 공부해봐야겠습니다 ㅎㅎ!

this.racingService = RacingService()
}

@ParameterizedTest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParameterizedTest 👍

@tmdgh1592
Copy link
Member Author

tmdgh1592 commented Feb 10, 2023

저희 페어는 특히 주어진 요구사항 외에도 중복되는 자동차명이 입력되는 경우 예외가 발생하도록 하였는데요!
여기서 중복에 대한 검증을 InputView에서 하기 보단, 가능한 domain에서 처리하는 것이 좋지 않을까 라는 생각이 들었습니다.
이럴 때에는 service layer에서 repository를 통해 데이터베이스처럼 관리하는 것이 좋을까요?
만약, 값을 insert할 때 이미 값이 있는지 확인하기 위해 기존 car 객체들을 모두 불러와 확인하는 방식처럼요!

Car에 대한 정보가 1회성 사용으로 한정된 미션이기 때문에,
현재 미션에서는 repository를 통해 관리하는 방법은 좋은방법은 아닌거 같단 생각이 들어요!
현재미션에서는 List를 관리하는 책임이 있는 클래스에서 입력된 names로 cars를 생성할때만 체크하도록하면 어떨까요?

제가 코드리뷰에 대한 경험이 많이 없어서 여기에 답을 남겨주신건 미처 보지 못하고 repository를 추가한 채로 코드리뷰를 요청드렸습니다.. 죄송합니다.

다시 살펴보니 Cars 리스트에 대해 일급 컬렉션을 만들어 관리하고, 멤버 프로퍼티들도 래퍼 클래스로 관리한다면 조금 더 비즈니스에 종속적인 자료구조로 만들 수 있지 않았을까 싶습니다..!

그렇게 하면 Service layer나 Controller에서 도맡던 로직 또한 각 클래스의 역할에 맞춰 분리할 수 있어 보입니다.. 🥲

List를 관리하는 클래스가 일급컬렉션을 의미하시는게 맞나요..?!

@namjackson
Copy link

namjackson commented Feb 11, 2023

제가 코드리뷰에 대한 경험이 많이 없어서 여기에 답을 남겨주신건 미처 보지 못하고 repository를 추가한 채로 코드리뷰를 요청드렸습니다.. 죄송합니다.
다시 살펴보니 Cars 리스트에 대해 일급 컬렉션을 만들어 관리하고, 멤버 프로퍼티들도 래퍼 클래스로 관리한다면 조금 더 비즈니스에 종속적인 자료구조로 만들 수 있지 않았을까 싶습니다..!
그렇게 하면 Service layer나 Controller에서 도맡던 로직 또한 각 클래스의 역할에 맞춰 분리할 수 있어 보입니다.. 🥲
List를 관리하는 클래스가 일급컬렉션을 의미하시는게 맞나요..?!

일급컬렉션은 구현에 관한 이야기이고, List를 관리하는 클래스는 객체지향의 책임에 대한 부분을 말씀드린부분이긴합니다!
일급컬렉션은 다음 미션의 요구사항이라 따로 말씀드리진 않았는데, 기회가 된다면 적용해보셔도 좋을거같아요! 😄

Copy link

@namjackson namjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경사항 적용 잘해주셨네요!
몇가지 코멘트들 남겼으니 확인해주세요!
변경사항은 다음미션 진행하면서 적용해주시면 좋을거 같아요 😄

Comment on lines +8 to +10
require(name.length in MIN_CAR_NAME_LENGTH..MAX_CAR_NAME_LENGTH) {
throw IllegalArgumentException(CAR_NAME_LENGTH_OVER_BOUNDARY_ERROR_MESSAGE)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require 내부적으로 throw하기 때문에
따로 에러를 throw해줄필요가 없을것 같아요!
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. 이런 실수를..!
캐치해주셔서 감사합니다 ㅎㅎ
수정하겠습니다!

class RacingService(
private val carRepository: Repository<Car> = CarRepository()
) {
fun getAll(): List<Car> = carRepository.selectAll()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 외에는 외부에서사용되지 않는 함수같아요!
private으로 유지해주세요!

테스트를 위한 함수나, 테스트를 위한 코드는 지양해주세요!
해당 함수를 공개하지않고 테스트할수 있는 방법을 고민해보시면 좋을것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멘토님께서 말씀해주신 것처럼 Car에 대한 정보가 1회성으로 사용되기 때문에, Repository의 필요성이 낮다고 생각이 들었습니다!
그래서 Repository 레이어를 없애고 Car List를 Service 레이어에서 관리하도록 수정하여 해당 메서드를 제거하였습니다 :)

Comment on lines +15 to +16
val cars = racingService.createCars(readCarNames())
racingService.insertCars(cars)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

racingService에서 createCars를 한후,
racingService에 insertCars를 하는 구조네요!
racingService에게 메세지를 주어 해당 기능을 하도록 책임을 위임해보면어떨까요?

Copy link
Member Author

@tmdgh1592 tmdgh1592 Feb 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 리뷰를 읽고 스스로 고민하고 공부해보는 시간을 가졌습니다.
DTO와 도메인 Model의 사용 범위에 대해 공부해보다가, View와 Controller에서 Model을 참조하는 것이 좋지 않을 수도 있다고 생각했습니다.

이렇게 생각한 이유는 아래와 같습니다.

  1. Controller가 Model에 직접적으로 의존하면, Model이 변경되었을 때 Model을 참조하고 있는 모든 Controller에서 변경이 발생합니다.
  2. Service가 Model을 캡슐화하는 역할을 하는데, Controller에서 Model에 직접 접근이 가능하다면, Model의 비즈니스 로직을 의도치 않게 수행하여 예기치 못한 오류가 발생할 수 있습니다.
  3. 값을 입출력만 하면 되는 View에서 굳이 비즈니스 로직을 담고 있는 Model에 직접 의존할 필요가 없습니다.

아래 코드는 View에서 값을 읽어 DTO 객체로 만들고 Controller -> Service 로 전달하여 Service에서 model로 변환해주는 형식으로 코드를 수정해보았습니다.

View

fun readCarNames(): CarsDto = CarsDto(
        readln()
            .split(CAR_NAME_DELIMITER)
            .removeBlank()
            .map { carName ->
                CarDto(CarNameDto(carName))
            }
    )

    fun readRound(): RoundDto {
        val number = readln().toIntOrNull()
        requireNotNull(number) { NOT_NUMERIC_ERROR_MESSAGE }

        return RoundDto(number)
    }

Controller

racingService = RacingService(readCarNames(), readRound())

Service

class RacingService(_cars: CarsDto, _round: RoundDto) {
    private val cars = _cars.toModel()
    private val round = _round.toModel()

글이 길어 죄송합니다.. 🥲
해당 코드에서 일반적이지 않거나 문제가 될 수 있는 부분이 있다면 바로 잡고 싶습니다..!
추가로.. 잭슨님께서 평소에 생각하시던 의견이 있으시다면 공유 부탁드려도 될까요?
정말 많은 도움이 될 것 같습니다 :)

Comment on lines +44 to +46
repeat(roundCount) {
runRound(cars)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller의 역할은 무엇인지, 다음 미션의 MVC적용을 하시면서 고민해보시면 좋을거같아요!

Comment on lines +21 to +26
fun moveRandomly(car: Car) {
car.move(getRandomProbability())
}

private fun getRandomProbability(): Int =
(START_RANDOM_MOVEMENT_PROBABILITY..END_RANDOM_MOVEMENT_PROBABILITY).random()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random 기능은 테스트하기 까다롭습니다!
해당 기능의 테스트를 고민해보시면 좋을거같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

car.move()는 외부에서 랜덤 조건을 주입받기 때문에 테스트가 가능해졌지만,
moveRandomly(car: Car) 메서드는 테스트가 불가능하군요..!
캐치하지 못하고 있었는데 다뤄주셔서 감사합니다!


@ParameterizedTest
@ValueSource(strings = ["buna", "otter", "jack", "son"])
fun `1글자 이상 5글자 이하의 이름이 주어졌을 때, 자동차 객체 생성시, 예외가 발생하지 않는다`(name: String) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구체적인 테스트 케이스 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

칭찬은 부나를 춤추게 합니다 😆

Comment on lines +51 to +54
racingService.insertCars(cars)
racingService.getAll().forEachIndexed { index, car ->
repeat(moveCounts[index]) { car.move(ABSOLUTE_MOVE_CONDITION) }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 테스트 코드는 getWinners에 대한 테스트 코드이지만,
insertCars, getAll등의 함수에 의존성이 있어서,
해당 함수들에 문제가 있으면, 테스트가 정상적으로 동작하지 않는 구조입니다!

racingService 생성시 cars를 주입하는 방법을 사용해보면 어떨까요?
다른 함수에 의존성이 낮아질거 같아요!

Copy link
Member Author

@tmdgh1592 tmdgh1592 Feb 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새로운 지식을 하나 더 얻어갈 수 있었습니다!
이렇게 보니 의존하고 있는 대상이 많아지면, 대상이 수정될 경우 테스트 코드도 번거롭게 수정을 해주어야 하는군요..!

@beforeeach에서 미리 racingService를 생성하면서 필요한 정보를 주입하는 방법으로 시도해보겠습니다!

}
}

private fun createCars(carNames: List<String>) =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 스타일의 차이가 있을수 있지만,
저는 간단한 함수라도 모두 작성하는 편입니다.

코드가 많아지다보면, 해당 함수에 대한 확인을 위해 구현부까지 보지않고, 함수명, 파라미터, 리턴값 정도만 확인하는데,
어떤 타입이 반환되는지 직관적으로 알수있는 구조가 아니라면, 가독성 측면에서는 명시적으로 써주는것이 좋을것 같아요!
또한 Int로 생각했는데 실제 값은 double로 반환된다거나하는 실수도 사전에 막을수 있을거 같아요!

@namjackson namjackson merged commit 3e4ee27 into woowacourse:tmdgh1592 Feb 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants