이것 저것 하다보니 미션 리뷰 받은 것을 정리할 틈이 전혀 없었다..
다행히 두번째 미션부터는 2주씩이라.. 1단계 머지를 하고 2단계 들어가기 전에 ~
더 이상 늦기 전에 ~ 진짜 까먹기 전에 ~ 피드백 받은 것에 대해 정리를 해보고자 한다.
내 첫 리뷰어는 레아였다.
이번 5기에 새로 오신 안드 코치님이신데.. 굉장히 꼼꼼하게 봐주셔서.. 글이 길어질지도 ❔❗
(일단 내 원픽이다..왜냐면은 예쁘구 착하구 귀엽구 잘알려주시구 말도 걸어주시고 암튼 체고체고)
(아 그리구 빅스도 레아 진짜 친절하구 좋다구 했다.. 어쩌면 나.. #동동-레아팬클럽 만들게 될지도..? EGR ㅋㅋㅋㅋㅋㅋㅋ 제성합니다. 🙇♀️)
2단계
기타
첫 미션이어서 그런지 일주일짜리 미션이기도 했고, 페어와의 첫 합동 프로그래밍이어서인지 추가 요구사항은 MVC 패턴을 적용하는 것과 랜덤함수를 어떻게 테스트 할 지였다. 페어와 (아 내 첫 페어는 코건이었다! 기타치는 코건 🎸) 나는 객체 지향 체조 원칙을 따라보자고 했고, 그 중 일급 컬렉션과 원시값 포장에 대해 고민하는 시간을 가졌다.
try {
World()
} catch (e: Exception) {
when (e) {
is IllegalArgumentException, is IllegalStateException -> {
println("[ERROR]: " + e.message)
}
}
}
catch (e: Exception)문으로 모든 에러를 catch하고 있는데요, 이러한 코드는 프로그램이 커질수록 예상치 못한 버그를 발견하기 어렵게 만들 수 있어요. (예를 들어 에러가 발생했는데 해당 에러를 catch문이 먹어서 프로그래머가 인지하지 못하는 일이 생길 수 있습니다) 나중에 미션을 진행하면서 조금이라도 구체적인 catch문을 작성하여 이러한 문제를 예방하는 편이 좋습니다.
코건과 나는 코드가 길어지는 것이 싫어 when 문으로 처리를 했었는데 레아가 위와 같은 문제가 발생할 수 있음을 인지시켜주었다. catch문 사용 시에는 구체적으로 !!
class World {
private val outputView = OutputView()
private val inputView = InputView()
}
OutputView와 InputView 인스턴스를 World가 꼭 들고 있을 필요가 있을지 의문이 드네요 🤔
OutputView와 InputView와 같이 꼭 인스턴스화할 필요가 없는 클래스라면 object 키워드를 활용해서 선언하는 것을 고려해보면 어떨까요?
Util과 같이 상태를 가지지 않고 companion object만 들고 있는 클래스도 object로 만드는걸 고려해보면 어떨까요?
View와 관련된 클래스들은 인스턴스화를 할 필요가 없다.
object 키워드를 사용하여 클래스를 선언하게 되면 객체를 싱글톤패턴으로 한 번만 생성하여 메모리 소요를 줄일 수 있게 해준다.
object 클래스의 특징
class Name(private val name: String) {
init {
require(name.isNotEmpty()) { "차 이름은 1글자 이상이어야 합니다." }
require(name.length <= Values.MIN_CAR_NAME_LENGTH) { "차 이름은 5글자 이하여야 합니다." }
}
override fun toString(): String {
return name
}
override fun equals(other: Any?): Boolean {
return this.name == other.toString()
}
}
이번 기회에 Kotlin의 data class가 무엇인지 찾아보고 적용해보면 어떨까요?
(equals()를 오버라이드 하는 다른 코드도 한번 점검해보세요)
data class 데이터 보관 목적으로 만든 클래스. 생성자부터 getter & setter 프로퍼티에 대한 toString(), hashCode(), equals(), copy() 메소드를 자동으로 만들어 주기 때문에 boilerplate code를 만들지 않아도 된다.
Canonical Method (캐노니컬 메소드) : Any에 선언된 메소드. (참고로 Any는 자바의 Object처럼 코틀린에서 모든 객체의 조상이 되는 객체이다.) 따라서 코틀린의 모든 인스턴스가 갖고 있는 메소드를 뜻한다. 메소드의 종류에는 toString(), hashCode(), equals() 이 있다.
제약 사항
링링의 많은 고민이 느껴집니다! Util 클래스, Util 함수에 대해 객체지향 관점에서 어떤 시각이 있는지 찾아보면 더욱 재밌으실거예요. 🙂
레아가 첨부해준 글 :: "Are Utility classes Evil?"
정의 : 프로젝트 전역에서 사용되고 특정 로직이나 독립적인 기능을 하는 클래스, 매개 변수에 대해 작업을 수행하는 정적 메서드만 있는 클래스
특징 : 유틸리티 클래스는 Static 이고 Stateless로 사용하여 멀티스레드 환경에서 Thread-safe하다. 보통 final로 선언되어 상속될 수 없고 생성자는 private으로 선언되어 초기화 가능성을 막아버리며 다른 클래스에 의해 생성될 수 없다.
장점 : 모듈화
상태를 갖지 않고 다른 객체에 의존하지 않으므로 하나의 모듈로서 작동하고 연계된 피해를 끼칠 일이 없다. 다른 객체에서 공통적으로 참조하는 값을 유틸클래스에 넣음으로서 오히려 객체지향을 지키기가 쉬워진다.
단점 : 강한 결합
유틸클래스를 사용하는 클래스는 유틸클래스에 강한 의존성을 갖게 된다. 추상화를 진행하지 않았기에 유틸클래스의 변화에 해당 객체는 많은 영향을 받게 된다. 유틸 클래스에 대한 변경이 생길때마다 직접 해당 클래스에서 수정을 가해야 하는 상황을 맞이 한다. -> 유연한 객체지향에 따른 프로그래밍을 하지 못하게 된다.
찾아보니 유틸리티 클래스는 객체지향의 관점에서 '악'이라는 관점이 정말 많았다.
시간이 난다면 레아가 첨부해준 글과 플러스 자료 : "객체지향 프로그래밍으로 유틸리티 클래스를 대체하자" 를 확인해보자.
오랜만에 공식자료 !

참고자료1, 참고자료2 runCatching 블록 안에서 성공/실패 여부가 캡슐화된 Result 형태로 리턴된다.
val colorName: Result<String> = runCatching {
when (color) {
Color.BLUE -> "파란색"
Color.RED -> "빨간색"
Color.YELLOW -> "노란색"
Color.ARMARNTH -> throw Error("처음 들어보는 색")
}
}.onSuccess { it:String ->
//성공시
}.onFailure { it:Throwable ->
//실패시, catch와 유사
}.also {
// finally와 유사
}
플러스 자료 : 에러 핸들링을 다른 클래스에게 위임하기 (Kotlin 100% 활용)
... 아직 적용을 안해서 제대로 이해를 못하겠다. 언젠가의 다음 포스팅에서 꼭 예제를 가져와 보겠다. 😅
랜덤 함수 테스트는 대체 어떻게 해야하는 걸까?
fun attempt() {
for (i in 0 until cars.size) {
step(i, Util.generateRandom())
}
}
// Util 클래스
fun generateRandom(): Int {
return Random().nextInt(10)
}
이 로직은 랜덤한 함수를 의존하고 있는데요, 어떻게 테스트해볼 수 있을까요? 해당 로직이 잘 작동하는지 어떻게 검증할까요? 🙂
바아로 ~ interface를 활용하여 테스트를 해볼 수 있다. (인터페이스는 바로 아래서 정리!)
아래는 내가 수정한 코드이다.
// NumberGenerator
interface NumberGenerator {
fun generate(): Int
}
// NumberGenerator
class RandomNumber : NumberGenerator {
override fun generate(): Int = Random().nextInt(10)
}
//CarManager
class CarManager(names: List<Name>, private val numberGenerator: NumberGenerator) {
fun attempt(): List<Car> {
cars.forEach {
it.forward(numberGenerator.generate())
}
return cars
}
}
// CarManagerTest
@Test
fun `랜덤한 값 테스트`() {
val numberGenerator = RandomNumberTest(listOf(1, 3, 5))
val carManager = CarManager(listOf(Name("test1"), Name("test2"), Name("test3")),
numberGenerator)
carManager.attempt()
val winners = carManager.determineWinner()
Assertions.assertThat(winners[0].name.toString()).isEqualTo("test3")
}
NumberGenerator를 interface로 생성 후 CarManager 생성 시 RandomNumber: NumberGenerator를 넘겨준다. 이런식으로 코드를 작성하게 된다면 추후 테스트에서는 내가 직접 생성한 NumberGenerator를 넘겨주어 코드를 테스트 해 볼 수 있다.
이거 이해하는데 나는 정말.. 꽤나 오랜 시간이 걸려서 마지막에 머지할 때 코드를 구현 할 수 있었다.... ㅜ_ㅜ
=> 상속은 부모 클래스의 메소드를 오버라이딩한 후, 그대로 사용하거나 조금 변경하여 사용하게 되지만, 인터페이스는 안에 포함된 미완성 메소드를 처음부터 끝까지 모두 구형해야 한다.
상속받거나 인터페이스를 받는 메소드 자체가 완성시켜야 되는 부분이 많다면 인터페이스를 쓰는 것이 좋다. 반대로, 부모 클래스 또는 인터페이스로부터 받게 되는 메소드 자체가 별로 완성지을 것이 없다면, 상속으로 처음부터 정의하는 것이 좋다.
interface Vehicle {
fun move()
fun stop()
fun park() {
println("주차")
}
}
class Car: Vehicle {
override fun move() {
println("이동")
}
override fun stop() {
println("정지")
}
override fun park() {
println("주차")
}
}
위의 코드에서 move()와 stop()은 미완성 코드이고, park()는 완성된 코드이다.
상속과 다르게 인터페이스를 받을 때는 타입으로 Vehicle()이 아닌 Vehicle로 받게 된다. (인터페이스는 생성자의 개념이 없기 때문!!)
인터페이스를 타입으로 받은 후 마우스 우클릭 -> Generate -> Implement를 눌러 구현 가능
class CarManager() {
private lateinit var cars: MutableList<Car>
fun init(names: List<Name>) {
cars = mutableListOf()
for (name in names) {
cars.add(Car(name))
}
}
}
init 블럭에서 초기화할 때를 제외하고 cars가 Mutable한 List 타입이 될 필요가 있을까요? List 타입으로도 충분히 구현 가능해보입니다.
힌트: 이런 상황에서 강의에서 배운 부생성자를 활용해보세요!
class CarManager(names: List<Name>, private val numberGenerator: NumberGenerator) {
private val cars = names.map { Car(it) }
}
클래스에서 굳이 fun init이라는 함수를 사용해야할까? 부생성자를 사용한다면 초기화를 가능하다.
for문을 사용하지 않고 리스트를 어떻게 형변환 해주냐고? 그럴때는 map을 사용하면 된다.
private fun initCars(): List<Name> {
OutputView.printLnMessage(OutputView.MSG_INPUT_CAR_NAME)
}
컨트롤러에서 OutputView에 MSG_INPUT_CAR_NAME라는 상수가 존재한다는 것을 알 필요가 있을까요~? 외부에서 객체의 구체적인 구현을 참조하기보다 메세지만 던져보는 구조로 개선해보세요 🙂
OutputView.자동차 이름을 받을 때 사용할 안내메세지를 출력해줘()
다른 코드에서도 객체 내부의 구체적인 구현을 참조하는 곳이 있나요? 메세지를 던지는 구조로 바꾸면 어떤 점이 좋을까요?
같은 함수를 재활용하는 것이 나은가 아니면 상황 별로 분리하여 함수를 만드는 것이 나을까에 대해 코건과 고민했었고, 결국 전자를 선택했었다.
후자를 선택했을 때의 장점은 무엇이 있을까? 내가 생각했을 때는
레아는 가장 큰 장점으로 출력 요구사항이나 출력 방법이 바뀌더라도 해당 변경사항이 호출부까지 전파되지 않는다는 것을 꼽았다. (호출부에서 구체적인 요구사항에 의존하게 되면 해당 요구사항을 변경할 때 변경점이 늘어나게 되기 때문)
앞으로도 객체 지향 관점에 대해 고민하며 미션을 진행하실 텐데요, OOP의 가장 중요한 본질 중 하나는 프로그램이 커졌을 때 유지 보수와 변경사항의 전파를 막기 위함에 있다는 것을 기억하며 메세지를 던져보세요 🙂
👻 runCatching 적용해보기
👻 부생성자
👻 Factory Method
미션을 마무리하며..
👼 레아가 읽어보라며 추천한 글이다. 객체지향
👼 레아의 피드백에는 자잘한 칭찬이 많았는데,
혹시 본인이 칭찬을 많이 받고 있다면 아래 짤과 같이 의심을 해보는 것이 좋을지도 ❔❗
