본문 바로가기
가치관 쌓기/개발 돌아보기

[우아한테크코스Pro]로또 구현(테스트 주도 개발) - 못다한 이야기

by simplify-len 2021. 6. 3.

이전에 로또 TDD 에 대한 내용을 블로깅한 적이 있습니다.

https://happy-coding-day.tistory.com/170

들어가기

모든 구현을 마치고, PASS 까지 받은 상황에서 미쳐 생각하지 못했던 내용에 대해서 이야기해볼까 합니다.

이전 블로그에서도 말했다시피, 아래 생활 체조 원칙을 명확히 지키면서 코딩해야 합니다. 원시값도 포장한다는 말은 int 값 또한 인스턴스로 만들어 개발해야 한다는 의미입니다.

경험할 객체지향 생활 체조 원칙

  • 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  • 규칙 2: else 예약어를 쓰지 않는다.
  • 규칙 3: 모든 원시값과 문자열을 포장한다.
  • 규칙 5: 줄여쓰지 않는다(축약 금지).
  • 규칙 8: 일급 콜렉션을 쓴다.

캐싱인스턴스

클래스 중에 LottoNumber 에 대한 생각을 해봅시다.
LottoNumber는 말 그대로, 로또 티켓에 적혀있는 int or long 번호 하나를 이야기합니다.

//before
int number = 10;
//after
LottoNumber.of(10);

정적 팩토리 메소드를 사용함으로써, 무엇이 좋아졌다고 이야기할 수 있을까요? Effective Java Item 1번에서 나오는 내용은 다음과 같습니다.

정적 팩토리 메소드 내용

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

  • 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다.
  • 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
public static Boolean valueOf(boolean b) {
  return (b ? TRUE : FALSE);
}

장점 1. 이름을 가질 수 있다.

  • 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못하지만 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
  • 하나의 시그니처로는 생성자를 하나만 만들 수 있지만, 이름을 가질 수 있는 정적 팩터리 메서드에는 이런 제약이 없다.
    (e.g. BigInteger(int, int, Random) vs BigInteger#probablePrime(int, Random))

장점 2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

  • 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
  • 대표적인 예인 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.
  • 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷한 기법이라 할 수 있다.
    (e.g. java.lang.Integer#valueOf(int))

tips)

플라이웨이트 패턴?

final Integer n1 = 1;
final Integer n2 = 1;
System.out.println(n1 == n2); // true

final Integer n3 = 128;
final Integer n4 = 128;
System.out.println(n3 == n4); // false

왜?

장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

  • 반환할 객체의 클래스를 자유롭게 선택할 수 있는 '엄청난 유연성'을 선물한다.
  • API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
  • API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.

장점 4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

  • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다.
    (e.g. EnumSet#noneOf(Class<E>))
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
      throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
      return new RegularEnumSet<>(elementType, universe);
    else
      return new JumboEnumSet<>(elementType, universe);
}
  • EnumSet.nonOf 의 인자로 무엇을 받는가에 따라 반환되는 값이 달라진다.

장점 5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

  • 이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다.
  • 대표적인 서비스 제공자 프레임워크로는 JDBC(Java Database Connectivity)가 있다.
  • JDBC의 getConnection() 메서드에서 나오는 Connection 객체는 DB 마다 다르다.

장점 2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다. 를 코드로 이해볼 수 있는 기회가 되었다. LottoNumber는 45개 중 6개가 뽑히는 역할을 가졌다. 매번 new LottoNumber 를 하게 된다면, 한 사람이 만들 수 있는 LottoNumber에 대한 인스턴스를 무수히 많아 시스템에 지장을 줄 수 있습니다.

그러므로 수정된 코드는 아래와 같습니다.

public class LottoNumber implements Comparable<LottoNumber> {

    private int value;

    public static Map<Integer, LottoNumber> numberSet;

    static {
        numberSet = new LinkedHashMap<>();
        IntStream.range(1, 46).forEach( i -> numberSet.put(i, LottoNumber.of(i)));
    }
    public static LottoNumber of(int value) {
         return numberSet.get(value);
    }

    public static LottoNumber of(String value) {
        return numberSet.get(Integer.parseInt(value));
    }

매번 새로운 객체를 생성하는 것이 아니라,캐싱 인스턴스를 사용해 매번 만들어지는 LottoNumber의 메모리를 최적화 시킬 수 있습니다.

댓글