[우테코/5기/안드로이드] 3주차 프리코스 톺아보기

Falco·2022년 11월 20일
0

우테코 프리코스

목록 보기
3/5
post-thumbnail
post-custom-banner

3주차 공통 피드백

함수(메서드) 라인에 대한 기준

프로그래밍 요구사항을 보면 함수 15라인으로 제안하는 요구사항이 있다. 이 기준은 main() 함수에도 해당된다. 공백 라인도 한 라인에 해당한다.

15라인이 넘어간다면 함수 분리를 위한 고민을 한다.

발생할 수 있는 예외 상황에 대해 고민한다

정상적인 경우를 구현하는 것보다 예외 상황을 모두 고려해 프로그래밍하는 것이 더 어렵다.
예외 상황을 고려해 프로그래밍하는 습관을 들인다. 예를 들어 로또 미션의 경우 아래와 같은 예외 상황을 고민해 보고 해당 예외에 대해 처리를 할 수 있어야 한다.

  • 로또 구입 금액에 1000 이하의 숫자를 입력
  • 당첨 번호에 중복된 숫자를 입력
  • 당첨번호에 1~45 범위를 벗어나는 숫자를 입력
  • 당첨 번호와 중복된 보너스 번호를 입력

테스트 케이스를 만들 때 도 예외적인 상황을 고려하여 테스트를 작성한다.

비즈니스 로직과 UI 로직을 분리한다

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.

class Lotto(private val numbers: List<Int>) {
   // 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
   fun contains(number: Int): Boolean {
      ...
   }

   // UI 로직 단일 책임의 원칙 위배 !!!
   private fun print() {
      ...
   }
}

현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()을 통해 구현한다.

override fun toString(): String = // ...

View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.

// getter 메서드
fun getLottoNumber() = numbers

연관성이 있는 상수는 전역으로 선언하는 대신 enum을 활용한다.

enum class Rank(
   private val countOfMatch: Int,
   private val winningMoney: Int
) {
   FIRST(6, 2_000_000_000),
   SECOND(5, 30_000_000),
   THIRD(5, 1_500_000),
   FOURTH(4, 50_000),
   FIFTH(3, 5_000),
   MISS(0, 0);
}

val 키워드를 사용해 값의 변경을 막는다

최근에 등장하는 프로그래밍 언어들은 기본이 불변 값이다. 코틀린은 val 키워드를 활용해 값의 변경을 막을 수 있다.

class Money(private val amount: Int)

프로퍼티의 변경에 있어 가장 예민한건 콜렉션이다.
mutable한 리스트를 사용함에 있어 mutable한 프로퍼티인 var을 쓰는것은 자제하도록 하자.

private var list = mutableList() // Do not use like this!!

객체의 상태 접근을 제한한다

프로퍼티의 접근 제어자는 private으로 구현한다.

class WinningLotto(
    private val lotto: Lotto,
    private val bonusNumber: Int
)

객체는 객체스럽게 사용한다

Lotto 클래스는 numbers를 상태 값으로 가지는 객체이다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, numbers를 외부에 드러내는 함수만을 가진다.

class Lotto(private val numbers: List<Int>) {
   fun numbers() = numbers
}

class LottoGame {
   fun play() {
       val lotto = Lotto(...)

       // 숫자가 포함되어 있는지 확인한다.
       lotto.numbers().contains(number)

       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       lotto.numbers().count()...
   }
}

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

객체에 메시지를 보내 객체가 로직을 수행하도록 하자

class Lotto(private val numbers: List<Int>) {
   fun contains(number: Int): Boolean {
       // 숫자가 포함되어 있는지 확인한다.
       return ...
   }

   fun matchCount(other: Lotto): Int {
       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       return ...
   }
}

class LottoGame {
   fun play() {
       val lotto = Lotto(...)
       lotto.contains(number)
       lotto.matchCount(...)
   }
}

참고. getter를 사용하는 대신 객체에 메시지를 보내자

필드의 수를 줄이기 위해 노력한다

필드의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인해 필드의 수를 최소화한다.
예를 들어 총 상금 및 수익률을 구하는 다음 객체를 보자.

class LottoResult(
   private val result: Map<Rank, Int>,
   private val profitRate: Double,
   private val totalPrize: Int
)

위 객체의 profitRatetotalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

class LottoResult(private val result: Map<Rank, Int>) {
   fun calculateProfitRate(): Double {
       ...
   }

   fun calculateTotalPrize(): Int {
       ...
   }
}

성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다

테스트를 작성하면 성공하는 케이스에 대해서만 고민하는 경우가 있다. 하지만 예외에 대한 부분 또한 처리해야 한다. 특히 프로그램에서 결함이 자주 발생하는 부분 중 하나는 경계값이므로 이 부분을 꼼꼼하게 확인해야 한다.

@Test
fun `보너스 번호가 당첨 번호와 중복되는 경우에 대한 예외 처리`() {
   assertThrows<IllegalArgumentException> {
       WinningLotto(
           lotto = Lotto(listOf(1, 2, 3, 4, 5, 6)),
           bonusNumber = 6
       )
   }
}

테스트 코드도 코드다

테스트 코드도 코드이므로 리팩터링을 통해 개선해나가야 한다. 특히 반복적으로 하는 부분을 중복되지 않게 만들어야 한다. 예를 들어 단순히 파라미터의 값만 바뀌는 경우라면 아래와 같이 테스트할 수 있다.

@ValueSource(ints = [999, 0, -123])
@ParameterizedTest
fun `천원 미만의 금액에 대한 예외 처리`(input: Int) {
   assertThrows<IllegalArgumentException> {
       Money(input)
   }
}

테스트를 위한 코드는 구현 코드에서 분리되어야 한다

테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.

  • 테스트를 위해 접근 제어자를 바꾸는 경우
  • 테스트 코드에서만 사용되는 메서드

단위 테스트하기 어려운 코드를 단위 테스트하기

아래 코드는Random 때문에 Lotto에 대한 단위 테스트를 하기 힘들다. 단위 테스트가 가능하도록 리팩터링한다면 어떻게 하는 것이 좋을까?

import camp.nextstep.edu.missionutils.Randoms

class Lotto {
   private val numbers: List<Int> =
       Randoms.pickUniqueNumbersInRange(1, 45, 6)
}

——————

class LottoMachine {
   fun execute() {
       val lotto = Lotto()
   }
}

올바른 로또 번호가 생성되는 것을 테스트하기 어렵다. 테스트하기 어려운 것을 클래스 내부가 아닌 외부로 분리하는 시도를 해 본다.

import camp.nextstep.edu.missionutils.Randoms

class Lotto(private val numbers: List<Int>)

class LottoMachine {
   fun execute() {
       val numbers: List<Int> = Randoms
           .pickUniqueNumbersInRange(1, 45, 6)
       val lotto = Lotto(numbers)
   }
}

위 코드는 A 상황을 B로 바꾼 것이다.

A.

main(테스트하기 어려움)
     ⬇️
LottoMachine(테스트하기 어려움)
     ⬇️
Lotto(테스트하기 어려움) ➡️ Randoms(테스트하기 어려움)
B.
 
main(테스트하기 어려움) 
     ⬇️
LottoMachine(테스트하기 어려움) ➡️ Randoms(테스트하기 어려움) 
     ⬇️
Lotto(테스트하기 쉬움)

참고. 메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기

이처럼 단위 테스트를 할 때 테스트하기 어려운 부분은 분리하고 테스트 가능한 부분을 단위 테스트한다. 테스트하기 어려운 부분은 단위 테스트하지 않아도 된다. 남은 LottoMachine은 어떻게 테스트하기 쉽게 바꿀 수 있을지 고민해 본다.

private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다.
public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다.
다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.

What I Learn? 🤔

일급콜렉션이란?

일급 컬렉션을 사용하는 이유

Collection을 Wrapping하면서 그 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 한다.

자바에서의 일급 콜렉션의 선언은 다음과 같다.

public class Lotto {
    // 멤버 변수가 하나밖에 없다는게 중요!!
    private List<LottoNumbers> numbers;

    public Lotto(List<LottoNumbers> numbers) {
        this.numbers = numbers;
    }
}

코틀린에서는 좀 더 간단하게 선언할 수 있다.

class Lotto(private val numbers: List<LottoNumber>) : List<LottoNumber> by numbers {
	
}

위의 소스에서는 Lotto객체의 타입을 List<LottoNumber>으로 상속받고 인자로 받은 numbers를 이용하여 프로퍼티 위임(by)을 받은 것을 볼 수 있다.

코틀린의 클래스는 기본적으로 상속할 수 없도록 정의되어 있다. (Open 어노테이션을 붙여야함) 이때 위임을 사용하면 상속하는 클래스의 모든 기능을 구현하는 동시에 추가 기능도 구현할 수 있다. 실제로 상속받는건 아님!
위임 : 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라.

이렇게 위임받으면 포워딩 메서드들을 자동으로 생성할 수 있다.
List<LottoNumber>콜렉션에 대한 모든 확장함수가 Lotto에서도 제공된다.

일급 콜렉션을 사용함으로써

  • 비지니스에 종속적인 자료구조
    해당 로또에서는 모든 로또 번호에 대한 도메인 로직, 밸리데이션 처리 등의 기능을 한 모델에서 수행할 수 있다. 특정 조건으로 만들 수 있는 자료구조를 생성하는 것이다.

  • MutableList가 아닌 List를 사용함으로써 Collection의 불변성을 보장

  • 이름이 있는 컬렉션으로 가독성 증가

// 멤버 변수가 numbers밖에 없다.
class Lotto(private val numbers: List<LottoNumber>) : List<LottoNumber> by numbers {
    init {
        checkValidation()
    }

    override fun toString(): String {
        return numbers.toString()
    }

    fun containsNumber(number: Int) = numbers.contains(LottoNumber.valueOf(number))

    private fun checkValidation() {
        require(numbers.size == LOTTO_LENGTH) {
            LOTTO_LENGTH_MUST_SIX_TEXT
        }
        require(numbers.distinct().size == LOTTO_LENGTH) {
            LOTTO_NOT_DUPLICATE_TEXT
        }
    }

}

Enum 클래스에 대해

Enum클래스 완벽 정복
Enum 클래스(열겨형 클래스)란 상수를 집합으로 관리하기 위해 만들어진 클래스이다.

enum class Rank(private val correctCnt: Int) {
    CORRECT_ZERO(0) {
        override val prize: Int = ZERO
    },
    CORRECT_THREE(3) {
        override val prize: Int = PRIZE_THREE
    },
    CORRECT_FOUR(4) {
        override val prize: Int = PRIZE_FOUR
    },
    CORRECT_FIVE(5) {
        override val prize: Int = PRIZE_FIVE
    },
    CORRECT_FIVE_BONUS(5) {
        override val prize: Int = PRIZE_FIVE_BONUS
    },
    CORRECT_SIX(6) {
        override val prize: Int = PRIZE_SIX
    };

    abstract val prize: Int
  
}
  • 열거형 클래스는 아규먼트를 가질 수 있으며, 상위 클래스(Rank)의 함수 및 파라미터를 모두 상속받는다.

  • 인터페이스를 상속받을 수 있다.

  • when문을 사용할 때 가능한 모든 컨디션을 체크할 수 있다. (else 브런치가 필요없음)

  • values() : 모든 enum 상수 클래스를 반환한다.

  • valueOf(value: String) : value라는 이름을 가진 enum class를 반환한다. 만약 이러한 클래스가 없다면 IllegalArgumentException을 반환한다.

  • 동반 객체 생성자를 활용해 해당 객체를 반환할 수 있다.

	companion object {

        fun valueOf(correctCnt: Int, checkBonus: Boolean): Rank {
            if (correctCnt < MIN_CORRECT_COUNT) {
                return CORRECT_ZERO
            }
            if (CORRECT_FIVE.compareCnt(correctCnt) && checkBonus) {
                return CORRECT_FIVE_BONUS
            }

            for (rank in values()) {
                if (rank.compareCnt(correctCnt) && rank != CORRECT_FIVE_BONUS) {
                    return rank
                }
            }

            throw IllegalArgumentException(LOTTO_CORRECT_LENGTH_TEXT)
        }
    }
    
 // Useage
 Rank.valueOf(count, lotto.contains(bonus))

비즈니스 로직과 UI 로직의 분리 🤽‍♂️

피드백 예시로 주어진 코드이다. Lotto 클래스 내에서 도메인 로직 및 UI 로직을 동시에 수행하고 있다.

class Lotto(private val numbers: List<Int>) {
   // 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
   fun contains(number: Int): Boolean {
      ...
   }

   // UI 로직 단일 책임의 원칙 위배 !!!
   private fun print() {
      ...
   }
}

이를 방지하기 위해선 디자인패턴에 대해 알고 가면 좋을 것 같다.

디자인 패턴이란
반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 일종의 솔루션 같은 것

Model - View - Controller로 이루어지며

사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만든다.

  • 모델은 애플리케이션의 정보(데이터)
  • 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소
  • 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.

비즈니스 로직과 UI로직을 분리하기 위해 몇가지 규칙이 있다.

  1. Model과 View는 서로의 존재를 몰라야 한다.

  2. Model은 컨트롤러가 요청한 내부 비즈니스 로직을 처리해야한다.

  3. Model은 뷰나 컨트롤러에 의존해서는 안된다.

  4. View는 데이터를 저장하면 안된다.

  5. Model 입력 값에 대한 밸리데이션 체크를 진행하면 안된다.

    • 해당 Model 객체가 View에 의존하게 됨
  6. Controller는 모델과 뷰를 이어주어야 한다.

  7. Controller는 Model에게 getter를 보내 데이터를 가져오는 것이 아닌 모델에게 메시지를 보내 객체가 로직을 수행하게 한다.
    getter를 사용하는 대신 객체에 메시지를 보내자

다음 소스는 당첨 복권 모델 내에서 로또비교 로직을 수행한다. (controller에서 비교해달라는 메시지를 보내 이를 모델에서 처리)

class WinningLottery(private val userWinningNum: String, private val userBonus: String) {

    private val winningNum = mutableListOf<Int>()
    private val bonus: Int
    fun compareLotto(lotto: Lotto): Rank {
        var count = ZERO
        winningNum.forEach {
            if (lotto.contains(it)) count++
        }
        return Rank.valueOf(count, lotto.contains(bonus))
    }
}

인터페이스를 분리하여 테스트하기 좋은 메서드로 만들기

다음과 같은 로또가게 모델이 있다고 해보자.

class LottoStore() {

    private fun generateLottoNum(): List<Int> = Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUM, MAX_LOTTO_NUM, LOTTO_LENGTH)
            .sorted()
            
    //.. makeLotto

}

해당 로또 모델에서 로또를 생성할 때에는 generateLottoNum()함수를 호출하여 랜덤 6개 번호를 생성한 후 그에 맞는 로또 객체를 반환할 것이다.

하지만 해당 함수가 제대로된 로또번호를 반환하는지 테스트를 진행하고싶다.
그러나 해당 generateLottoNum()는 private으로 선언되어 있음으로 테스트하기가 어렵다. 피드백 중 private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다가 있음으로 객체를 분리해보자.

class LottoNumberGenerator {

    fun generate(): List<Int> =
        Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUM, MAX_LOTTO_NUM, LOTTO_LENGTH)
            .sorted()
}
class LottoStore(private val numberGenerator: LottoNumberGenerator = LottoNumberGenerator()) {

    private fun generateLottoNum(): List<Int> = numberGenerator.generate()

다음과 같이 로또 번호 생성기를 만듦으로 인해 해당 로직을 분리 및 테스트가 가능해 졌다.

메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기

이렇게 수정했다고 모든 문제가 해결된 것은 아니다.
랜덤 숫자 생성기를 만들어 로또 번호에 대한 랜덤 값 테스트는 가능해 졌다. 하지만 이는 테스트를 어렵게 만드는 의존 관계를 분리한 것에 불과하다.

로또를 생성할 때에 generateLottoNum()은 다시한 번 랜덤한 값에 의존하게 된다. 이러한 의존을 조금 더 줄일 수 있는 방법으로 인터페이스를 분리하여 테스트하기 좋은 메서드로 변활할 수 있다.

인터페이스를 분리하여 테스트하기 좋은 메서드로 만들기

  • 번호를 생성하는 로직을 인터페이스로 분리한다.
interface GenerateLottoNumber {
    fun generate(): List<Int>
}
  • 해당 로직을 구현하는 구현체를 선언한다. (인터페이스와 구현체를 분리함으로 인해 해당 구현체가 랜덤값을 생성할지, 특정 값을 생성할지 유저가 설정할 수 있다.)
class LottoNumberGenerator : GenerateLottoNumber {

    override fun generate(): List<Int> =
        Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUM, MAX_LOTTO_NUM, LOTTO_LENGTH)
            .sorted()
}
  • 해당 인터페이스를 상속받는 구현체를 사용한다.
class LottoStore(private val numberGenerator: GenerateLottoNumber = LottoNumberGenerator()) {

    private fun generateLottoNum(): List<Int> = numberGenerator.generate()
    
    fun generateLotto(): Lotto = Lotto(generateLottoNum().map {
        LottoNumber.valueOf(it)
    })

이러하게 인터페이스를 분리한다면 번호를 생성하는 다양한 구현체를 생성할 수 있음으로 로또를 만드는 것에 있어 여러 테스트 케이스를 지정할 수 있게 되었다.

    class CustomGenerator : GenerateLottoNumber {
        override fun generate(): List<Int> {
            return listOf(1, 3, 5, 6, 7, 8)
        }
    }
@DisplayName("로또 상점 테스트")
class LottoStoreTest {

   private lateinit var lottoStore: LottoStore

   @BeforeEach
   fun setup() {
       lottoStore = LottoStore(CustomGenerator())
   }

   @Test
   @DisplayName("정확한 로또를 반환하는지 테스트한다.")
   fun generateLottoTest() {
       assertSimpleTest {
           val lottoNumbers =
               listOf(
                   LottoNumber.valueOf(1),
                   LottoNumber.valueOf(3),
                   LottoNumber.valueOf(5),
                   LottoNumber.valueOf(6),
                   LottoNumber.valueOf(7),
                   LottoNumber.valueOf(8)
               )
           assertThat(lottoStore.generateLotto())
               .containsAll(lottoNumbers)
       }
   }
}

🍏 캐싱 적용하기

Lotto미션을 진행하면서 더욱 더 리팩토링할 수 있을 것이 무엇이 있을지, 찾아보고 공부해 보았는데 그 중 하나가 캐싱적용이 가능하다는 것이였다.

기존의 Lotto인스턴스를 생성하는 소스이다.


class Lotto(private val numbers: List<Int>) {
    init {
        checkValidation() // numbers에 대한 validation 체크
    }
    
    companion object {

        fun newInstance(): Lotto {
            return Lotto(makeRandomNum())

        private fun makeRandomNum(): List<Int> =
                Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUM, MAX_LOTTO_NUM, LOTTO_LENGTH).sorted()
	}
}

Companion 객체 팩토리 함수를 사용하여 로또를 생성할 때 마다 새로운 instance를 생성하여 반환하고 있다.

만약 사용자가 로또를 100개, 1000개, 10000개를 만들 때 이러한 인스턴스가 100개, 1000개, 10000개 만들어 지는 것이다. 🥲

로또 리스트마다 로또 객체를 계속하여 생성하는 것을 볼 수 있다.

  • Lotto@1128 ... Lotto@1327

그렇다면 인스턴스 캐싱을 사용해 인스턴스의 중복 생성을 방지해보자.

class LottoNumber(private val number: Int) {

    init {
        validationNumber(number)
    }

    companion object {
        private val CACHED_LOTTO_NUMBER = List(MAX_LOTTO_NUM) { idx ->
            LottoNumber(idx + 1)
        }

        fun valueOf(number: Int): LottoNumber {
            validationNumber(number)
            return CACHED_LOTTO_NUMBER[number - 1]
        }

        private fun validationNumber(number: Int) {
            require(number in MIN_LOTTO_NUM..MAX_LOTTO_NUM) {
                LOTTO_NUM_IN_RANGE_TEXT
            }
        }
    }
}

로또번호는 어짜피 1~45의 숫자로 이루어져 있음으로 Lotto라는 객체를 새로 만드는 것이 아니라 LottoNumber라는 더 작은 객체를 사용하여 List<LottoNumber>가 Lotto의 역할을 대체수행하도록 하였다.

CACHED_LOTTO_NUMBER라는 변수를 동반객체로 생성하여 각각의 리스트에 LottoNumber의 인스턴트를 저장해 놓는다.

  • valueOf함수에서는 저장해 둔 인스턴트를 재사용하여 반환해 준다.
    companion object {

        fun List<LottoNumber>.contains(number: Int) = this.contains(LottoNumber.valueOf(number))
        fun newInstance(): List<LottoNumber> {
            return makeRandomNum().map {
                LottoNumber.valueOf(it)
            }
        }

        private fun makeRandomNum(): List<Int> =
            Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUM, MAX_LOTTO_NUM, LOTTO_LENGTH).sorted()
    }

로또의 생성 함수에서 makeRandomNum으로 생성된 랜덤 숫자와 valueOf 팩토리 함수를 이용하여 이미 캐싱되어 있는 LottoNumber의 주소를 반환한다.

이렇게 사용하면 1000개의 로또를 생성해도 인스턴트가 1000개가 생기는 것이 아닌, 6개의 주소를 가진 1000개의 리스트가 만들어짐으로 메모리를 절약할 수 있다.

매모리 프로파일러를 활용하여 메모리 사용량을 직접 측정해보았다.

  • 다음은 캐싱을 적용하지 않은채 로또 200,000만개를 만들었을 때 사진이다.


Lotto객체가 200,000개 생성되어 약 124 * 200,000B = 24.8Mb의 크기를 차지

Lotto 객체 내의 Arrays$ArrayList 즉 로또 번호의 어레이리스트가 200,000개 생성되어 2Mb크기 차지

Comparable객체는 정렬 수행 시 기본적으로 적용되는 정렬 기준이 되는 메서드를 정의하는 인터페이스로 ArrayList안의 Integer가 생성될 때 자동 생성 된 것 같다. -> 약 72 * 200,000 = 1.4Mb 차지
왜 생성되는지 못 찾아냄


  • 다음은 캐싱을 적용한채 로또 200,000개를 만들었을 때이다.

Lotto객채 내에서는 size가 포함되어 128 * 200,000 = 25.6Mb

LottoNumber의 주소를 가지는 ArrayList가 200,000개 생성
약 104 * 200,000B = 2Mb

캐싱을 적용하지 않았을 때의 Comparable객체는 생성되지 않음

결과로 캐싱 적용 전 총 : 28.2Mb, 캐싱 적용 후 총 : 27.7Mb

0.5Mb를 더 적게 사용했다. 눈에 뛰는 결과 값은 아니지만 전에는 Integer을 직접 생성하였지만, 캐싱 후 LottoNumber레퍼런스를 재사용하여 새로 생성되는 메모리가 없는 것을 볼 수 있다.


🤔 우테코에서 무엇을 가르치려 하는가? && 느낀점

이번 과제를 진행하면서 우테코에서 내가 무엇을 배우길 원하는가, 어떤 것을 가르치려고 하는 것인가에 대해서 다시 한번 생각해 보았다.
내가 이번에 배운 점은 다음과 같다.

1. 함수(메서드)를 간략하게 작성하라.

2주 차에서와 동일하게 요구사항에 인덴트 3을 넘어가는 것뿐만 아니라 "15줄을 넘어가지 않게 한다.", "함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다."를 요구하고 있습니다. 이러한 간략한 함수(메서드)는 가독성을 증가시킵니다. 2주가 지난 지금 1주 차 온보딩, 2주 차 야구게임의 소스를 다시 볼 때 함수 하나하나가 간단한 일만 수행하기에 코드를 읽기 쉬움을 경험했습니다. 또한 테스트코드의 작성을 편리하게 해줍니다. 개별 함수가 한가지의 일만 수행하기에 이에 따른 테스트코드의 작성이 쉽습니다. 👍

2. 보기 좋게 코드를 작성하라.

"else를 지양한다"와 "Enum클래스"를 사용한다. 라는 요구사항을 보았을 때 같은 기능으로 작동하는 코드여도 읽기 쉬운 코드를 작성하라는 것을 알았습니다. 이펙티브 코틀린을 읽으며 알게 된 점은 코틀린이 좋은 언어라고 하는 이유 중 하나가 바로 가독성이라는 것입니다. 그러기에 짧은 코드보다 개발자가 읽기 쉬운 긴 코드가 더 좋은 코드라고 판단하게 되었고, "When", "if"를 상황에 맞추어 사용하거나 else 구문이 오기 전에 return을 수행하거나, "?:"엘비스 연산자를 활용하여 Null처리를 간편하게 하는 가독성이 좋은 코드를 작성하는 방법을 알게 되었습니다.

3. 기능을 분리하라.

"핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다."가 무엇을 의미하는가에 대해 대부분의 소스를 작성한 후 알게 되었습니다. 기능을 구현하고 단위테스트를 작성하면서 하나의 함수가 입/출력과 도메인 로직에 대한 처리를 동시에 한다던가, 입력받는 뷰 로직에서 밸리데이션 체크를 진행하는 등의 문제가 발생했습니다. 이는 함수(메서드)를 간단하게 작성하지도 않았으며, 기능을 분리하지도 않았기 때문에 소스를 거의 갈아엎어서... 다시 구현했습니다.🥲 로직을 분리하는 데에 있어 디자인 패턴(MVC, MVVM....)의 여러 방법이 있을 수도 있지만 지금 단계에서는 뷰와 도메인 로직을 분리하는 데에만 집중했습니다!

4. 단위테스트를 작성하라. 👏(집중!)

"단위 테스트 작성이 익숙하지 않다면 test/kotlin/lotto/LottoTest를 참고하여 학습한 후 테스트를 구현한다." 우테코에서 따로 학습을 권한만큼 중요한 것이 이 단위테스트의 작성이라고 생각합니다. 이는 우테코에서 TDD(테스트 주도 개발)을 지향하고 있고, 이를 우테코의 교육생이 경험해보았으면 함을 알 수 있었습니다! JUnit 5와 AssertJ의 활용 방법과 제공하는 메소드가 매우 유용하기에 이를 따로 정리해 공부할 수 있었습니다!
이번에 로또게임을 작성하며 A기능을 하는 함수를 만들어야지 생각하고 함수부터 작성하는 것과 함수의 입출력을 테스트코드로 작성한 후 그다음 기능을 구현하는 TDD기반의 코드 작성을 할 때 제가 느낀 차이점은 다음과 같습니다.

  • 깔끔한 코드 작성이 가능합니다.
  • 코드의 수정 및 변경(리팩토링)이 쉽습니다.
  • 로직을 분리할 때 편리합니다. (기능이 작아서 해당 메서드만 복붙해서 다른 클래스로 옮기는게 쉽다.)

5. README (기능 목록)을 업데이트 하라.

함수(메서드)의 네이밍, 클래스 이름으로 기능을 모두 알 수 있다면 좋겠지만 그렇지 않았습니다..! (1주차 온보딩의 함수명을 보고 처절하게 느꼈습니다.) 기능 목록을 정기적으로 업데이트하면 메서드가 무슨 일을 하는지 빠르게 알 수 있고 이러한 기능 목록은 개발자의 가독성을 증가시킵니다. 가독성을 위해서라도 기능 목록을 정기적으로 업데이트해야 함을 알 수 있었습니다.

profile
강단있는 개발자가 되기위하여
post-custom-banner

0개의 댓글