Post

[우테코 프리코스] 1주차 - 제공된 라이브러리 분석

camp.nextstep.edu.missionUtils를 뜯어보며

[우테코 프리코스] 1주차 - 제공된 라이브러리 분석

들어가며

우테코 프리코스 1주차 미션을 개발하기 전에 외부 라이브러리를 사용하지 않고 제공된 라이브러리를 사용해야한다는 요구사항이 있었다. 제공된 라이브러리는 우테코에서 직접 개발한 코드이기 때문에 배울 점이 많다고 생각하여 뜯어보고자 한다. 모든 걸 이해할 순 없겠지만 하나씩 살펴보면서 내가 이해할 수 있는 부분까지 이해해보고 새롭게 알게 된 점을 정리해보려 한다.

alt text 인텔리제이에서 camp.nextstep.edu.missionUtils를 확인할 수 있었다. 테스트와 관련된 AssertionsNsTest, 유틸성의 클래스인 Console, DateTimes, Randoms로 분류 되어있다.

먼저 Scanner 대신 readline()을 사용하라는 요구사항이 있었기에 Console을 먼저 확인해보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Console {
    private static Scanner scanner;

    private Console() {}

    public static String readLine() {
        return getInstance().nextLine();
    }

    public static void close() {
        if (scanner != null) {
            scanner.close();
            scanner = null;
        }
    }

    private static Scanner getInstance() {
        if (scanner == null) {
            scanner = new Scanner(System.in);
        }
        return scanner;
    }
}

Console

Console 클래스는 생성자가 private으로 되어있다. 그래서 이렇게 사용할 수 없다

1
Console console = new Console(); 

의미 없는 객체들이 계속 만들어지는 것을 방지하기 위해 인스턴스화 방지의 역할로 private 생성자로 막아둔 거다.

또한 여기서 Scanner를 필요할 떄만 만드는 것을 확인할 수 있는데 getInstance() 메서드를 보면

1
2
3
4
5
6
private static Scanner getInstance() {
    if (scanner == null) {  // Scanner가 없으면
        scanner = new Scanner(System.in);  // 그때 만든다
    }
    return scanner;
}

프로그램이 시작하자마자 Scanner를 만드는 게 아니라, readLine()이 처음 호출될 때 만든다. 필요하지 않으면 만들지 않아도 되니 메모리의 절약의 의도로 하지 않았을까 생각해본다.

1
2
3
4
5
6
public static void close() {
    if (scanner != null) {
        scanner.close();
        scanner = null;  
    }
}

Scanner는 사용이 끝나면 닫아줘야 한다. 안 닫으면 리소스 누수가 생길 수 있다고 한다.

null로 만드는 이유는 단순히 close()를 하게 된다면

1
2
3
4
Console.readLine();  
Console.close();     

Console.readLine();  

이와 같은 예시가 있을 때 null로 만들지 않으면 getInstance()if (scanner == null) 체크가 작동하지 않아서, 이미 닫힌 Scanner를 또 사용하려다 에러가 난다.

1
2
3
4
5
6
private static Scanner getInstance() {
    if (scanner == null) {  
        scanner = new Scanner(System.in);
    }
    return scanner;
}

즉, 재사용 가능하게 만들기 위한 장치다.

실제로 close()가 사용된NsTest 클래스를 보면 이렇게 사용한다

1
2
3
4
5
6
7
8
protected final void run(final String... args) {
    try {
        command(args);
        runMain();
    } finally {
        Console.close();  
    }
}

finally 블록에 넣어서 예외가 발생하든 안 하든 무조건 정리되도록 한다.

Randoms

Randoms 클래스 또한 인스턴스화를 막기위해 private으로 감싸서 일종의 유틸리티 클래스로 사용한다.

그 중 pickNumberInRange()validateRange()를 보면

1
2
3
4
public static int pickNumberInRange(final int startInclusive, final int endInclusive) {
    validateRange(startInclusive, endInclusive);  // 검증부터!
    return startInclusive + defaultRandom.nextInt(endInclusive - startInclusive + 1);
}
1
2
3
4
5
6
7
8
9
10
11
private static void validateRange(final int startInclusive, final int endInclusive) {
    if (startInclusive > endInclusive) {
        throw new IllegalArgumentException("startInclusive cannot be greater than endInclusive.");
    }
    if (endInclusive == Integer.MAX_VALUE) {
        throw new IllegalArgumentException("endInclusive cannot be greater than Integer.MAX_VALUE.");
    }
    if (endInclusive - startInclusive >= Integer.MAX_VALUE) {
        throw new IllegalArgumentException("the input range is too large.");
    }
}
  • endInclusive == Integer.MAX_VALUEnextInt()에서 예외 발생
  • endInclusive - startInclusive를 계산할 때 예외 발생

이런 엣지 케이스까지 고려해서 미리 막아둔 것이다. 나는 이런 부분까지 생각하지 못했는데 정말 꼼꼼하게 검증하도록 되어 있는 것을 확인할 수 있다.

또한 shuffle()을 보면

1
2
3
4
5
public static <T> List<T> shuffle(final List<T> list) {
    final List<T> result = new ArrayList<>(list);  // 복사본 만들고
    Collections.shuffle(result);  // 복사본을 섞는다
    return result;
}

shuffle() 메서드가 원본 리스트를 직접 섞지 않고 새로운 리스트를 만들어 반환한다.

만약 원본을 직접 섞었다면

1
2
3
List<String> names = Arrays.asList("최", "장", "우");
Randoms.shuffle(names);  
System.out.println(names); 

함수를 호출한 쪽에서 예상치 못한 변화가 생긴다. 새 리스트를 반환하면 원본은 안전하다.

DateTimes

1
2
3
4
5
6
7
public class DateTimes {
    private DateTimes() {}

    public static LocalDateTime now() {
        return LocalDateTime.now();
    }
}

이 클래스는 정말 단순하다. 그냥 LocalDateTime.now()를 감싼 것뿐이다.

처음엔 이 코드를 작성한 이유를 잘 몰랐지만 테스트 코드를 보고 이해가 됐다. 이렇게 감싸두면 테스트할 때 특정 시간을 반환하도록 만들 수 있다고 한다.

테스트 관련 클래스들 (NsTest, Assertions)

테스트 코드 쪽은 아직 잘 모르겠지만, 대략적으로 어떤 역할인지는 파악할 수 있었다.

NsTest - 테스트 편하게 만들기

1
2
3
4
5
6
@BeforeEach
protected final void init() {
    standardOut = System.out;
    captor = new ByteArrayOutputStream();
    System.setOut(new PrintStream(captor));
}

System.outByteArrayOutputStream으로 바꿔치기 한다. 그러면 화면에 출력되는 내용을 문자열로 받을 수 있다.

1
2
3
protected final String output() {
    return captor.toString().trim();
}

이렇게 output()으로 출력된 내용을 가져와서, 내가 원하는 문자열이 출력됐는지 확인할 수 있다.

finally에서 리소스 정리

1
2
3
4
5
6
7
8
protected final void run(final String... args) {
    try {
        command(args);
        runMain();
    } finally {
        Console.close();  // 무조건 실행됨
    }
}

위에서 언급한 run()에서 try-finally 구조를 사용해서 예외가 발생하든 안 하든 Console.close()를 무조건 실행한다. 리소스를 최적화 시킨 방법이지 않을까 생각해본다.

Assertions - 랜덤 테스트?

1
2
3
4
5
6
7
8
9
10
11
12
public static void assertRandomNumberInRangeTest(
    final Executable executable,
    final Integer value,
    final Integer... values
) {
    assertRandomTest(
        () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
        executable,
        value,
        values
    );
}

랜덤은 실행할 때마다 다른 값이 나오니까 테스트하기 어렵다. 그런데 이 클래스는 “이번엔 3을 반환해, 다음엔 7을 반환해”라고 미리 정해둘 수 있게 해주는 것 같다.

정확히 어떻게 동작하는지는 아직 모르지만, 랜덤을 테스트 가능하게 만드는 도구라는 건 알겠다.

코드에서 발견한 패턴들

final 키워드가 많다

1
2
3
4
5
6
7
public static String readLine() {
    // ...
}

public static void assertSimpleTest(final Executable executable) {
    // ...
}

거의 모든 파라미터에 final이 붙어있다. 이러면 메서드 안에서 파라미터 값을 바꿀 수 없다.

1
2
3
public void example(final int number) {
    number = 10;  
}

실수로 값을 바꾸는 걸 방지할 수 있다. 우테코에서는 객체지향을 끝까지 고민해야하기 떄문에 적극적으로 활용해봐야겠다.

private 생성자 패턴

1
2
3
private Console() {}
private DateTimes() {}
private Randoms() {}

유틸리티 클래스는 모두 생성자가 private이다. 인스턴스를 만들 수 없고 정적 메서드만 사용하라는 의미이다.

메서드 이름이 명확하다

  • pickNumberInList: 리스트에서 하나를 뽑는다
  • pickNumberInRange: 범위에서 하나를 뽑는다
  • pickUniqueNumbersInRange: 범위에서 중복 없이 여러 개 뽑는다

Unique라는 단어 하나로 중복이 없다는 걸 명확하게 알 수 있다. 메서드명만 봐도 뭘 하는지 알 수 있게 짓는 게 중요한 것 같다.

배운 점과 적용할 점

리소스는 반드시 정리하기

Console.close()try-finally로 감싸는 패턴을 보고 배웠다. 예외가 발생해도 리소스를 정리해야 한다.

1
2
3
4
5
try {
    // Scanner 사용
} finally {
    Console.close();  // 무조건 실행
}

앞으로 Scanner나 파일 관련 작업을 할 때 꼭 이 패턴을 사용해야겠다.

유틸리티 클래스

정적 메서드만 있는 유틸리티 클래스를 만들 때

  • 생성자를 private으로 만들기
  • 필요한 경우 Lazy Initialization 고려하기
  • 메서드 이름을 명확하게 짓기

테스트를 고려한 설계

DateTimesRandoms처럼, 테스트하기 어려운 부분(시간, 랜덤)을 별도 클래스로 감싸두면 나중에 테스트하기 쉽다고 한다. 테스트 코드를 조금 더 배우고 적용할 수 있는 부분들은 적용해봐야겠다.

마치며

우테코에서 제공한 라이브러리가 정말 깔끔하고 명확하게 작성된 것을 확인할 수 있었고 일종의 제공된 가이드라인이라고 생각한다. 개발하면서 따라할 수 있는 부분은 최대한 따라해보고 사용해야 할일이 생길 때 작은 근거를 들어가며 사용해봐야겠다.

This post is licensed under CC BY 4.0 by the author.