들어가며..

이렇게 내부를 확인하지 않고 라이브러리를 사용했다. 앞으로는 사용을 정확하게 하기 위해 내부를 분석하려 한다.
구조
camp.nextstep.edu.missionutils는 3개의 클래스로 이루어져 있다.

각각 알아보자
Console
package camp.nextstep.edu.missionutils;
import java.util.Scanner;
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 기본 생성자가 private로 선언되어 있다. 이는 외부에서 선언을 막기 위함이다. 생각보다 이렇게 선언하는 경우가 많더라. 이는 진입점을 하나도 둘 수 있는 장점이 있다. 또한 싱글톤 패턴에서 단일의 객체만을 사용하는 이점이 있다. (객체 생성 비용이 줄어든다)
이를 Utility class라고 한다. 자바 기본 문법인 Math도 이렇기 때문에 new Math()를 굳이 선언하지 않아도 사용이 가능하다.
음 그러면 utils는 뭘까?
갑자기 이런 고민이 생겼다. 나는 패키지에 자주 utils를 만든다. 내가 생각하는 utils는 서비스가 사용하는 보조 서비스이다. 서비스가 서비스를 의존하는 구조는 이상하니 대부분 그렇게 만들었다. 그런데 검색을 하다보니
utils는 비즈니스 로직을 담당하지 않고, 어떤 패키지에서 사용해도 무관한 정말 utils를 담는 패키지이다. 따라서 utils을 수정하게 되면 다른 클래스에 영향이 크니, 생성시 주의를 해야한다. 만일 클래스 1이 서비스 1만 사용하는 비즈니스 로직을 포함하지만, 외부에 선언하고 싶다면, service1Helper.java 이런식으로 선언하거나, service 내부에 internal이란 패키지를 다시 선언하고 클래스 1을 넣는 편이 좋다.
다시 주제로 돌아가서...
Console 내부에는 하나의 Scanner만 사용되고, 이는 getInstance로 불러와진다. 이를 가지고 readLine을 실행하고, close 메소드를 이용해서 명시적 닫기도 가능하다.
이 코드의 장점은 객체 재사용을 통해 리소스가 낭비되지 않는다는 점이다. 단점은 객체를 모든 Console에서 사용하기에 멀티스레드 상황에서 문제가 발생할 수 있다.
DateTimes
package camp.nextstep.edu.missionutils;
import java.time.LocalDateTime;
public class DateTimes {
private DateTimes() {
}
public static LocalDateTime now() {
return LocalDateTime.now();
}
}
코드가 생각보다 단순하다. 위 처럼 생성자를 private로 선언해 객체 생성을 막아주고, static으로 선언해 객체를 재사용한다.
음.. 근데 굳이 이렇게 사용해야할까? 어차피 메소드가 now() 하나인데.. 이렇게 패키지까지 선언해서 사용해야할까?
만일 위 메소드를 사용하지 않고 현재 시간 메소드를 사용하고 싶으면
LocalDateTime now = LocalDateTime.now()
이렇게 사용하고 위 메소드를 사용하면
LocalDateTime now = DateTimes.now()
이렇게 된다. 이게 뭐가.. 큰 장점인가? 고민을 많이 해봤지만 큰 장점이 생각나진 않았다. 다만, 만약 프로젝트가 커져서 LocalDateTime의 값을 일관되기 표현해야 할 필요가 있다면 변경 시 이점이 생길 것 같다.
프로젝트가 커져서 여러 지역의 시간을 불러와야한다. 이 경우 DateTimes 클래스의 메소드를
public static LocalDateTime JapenNow() {
return LocalDateTime.now(ZoneId.of("JST"));
}
public static LocalDateTime KoreaNow() {
return LocalDateTime.now(ZoneId.of("Asiz/Seoul"));
}
이렇게 수정하면 편리할 것 같다. 이 외의 현재 코드에서 장점을 아시는 분들은 댓글 부탁드려욤..
Randoms
package camp.nextstep.edu.missionutils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class Randoms {
private static final Random defaultRandom = ThreadLocalRandom.current();
private Randoms() {
}
public static int pickNumberInList(final List<Integer> numbers) {
validateNumbers(numbers);
return numbers.get(pickNumberInRange(0, numbers.size() - 1));
}
public static int pickNumberInRange(final int startInclusive, final int endInclusive) {
validateRange(startInclusive, endInclusive);
return startInclusive + defaultRandom.nextInt(endInclusive - startInclusive + 1);
}
public static List<Integer> pickUniqueNumbersInRange(
final int startInclusive,
final int endInclusive,
final int count
) {
validateRange(startInclusive, endInclusive);
validateCount(startInclusive, endInclusive, count);
final List<Integer> numbers = new ArrayList<>();
for (int i = startInclusive; i <= endInclusive; i++) {
numbers.add(i);
}
return shuffle(numbers).subList(0, count);
}
public static <T> List<T> shuffle(final List<T> list) {
final List<T> result = new ArrayList<>(list);
Collections.shuffle(result);
return result;
}
private static void validateNumbers(final List<Integer> numbers) {
if (numbers.isEmpty()) {
throw new IllegalArgumentException("numbers cannot be empty.");
}
}
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.");
}
}
private static void validateCount(final int startInclusive, final int endInclusive, final int count) {
if (count < 0) {
throw new IllegalArgumentException("count cannot be less than zero.");
}
if (endInclusive - startInclusive + 1 < count) {
throw new IllegalArgumentException("count cannot be greater than the input range.");
}
}
}
1. ThreadLocalRandom.current()
private static final Random defaultRandom = ThreadLocalRandom.current();
Random값을 ThreadLocalRandom.current()값으로 받는다.
추가 개념으로.. 컴퓨터는 기본적으로 난수를 생성하지 못한다. (난수? 그게 뭔데.. 랜덤? 그거 어떻게 하는건데..) 그래서 보통 시간을 가지고 난수를 생성한다. 이게 기본 시드값으로 들어간다. 여기서 Random은 동일한 인스턴스를 멀티 쓰레드에서 공유한다. 만약 동일한 시간에 하나의 인스턴스에 여러 멀티 쓰레드에서 요청이 들어오면 이후 쓰레드는 이전 쓰레드 종료시점까지 계속 도전을 한다.
이는 심각한 성능 이슈를 유발한다!
그래서 ThreadLocalRandom을 사용해야한다.
이는 Random을 상속받는다. (그래서 위 코드에서 Random으로 받는 것이 가능하다.) ThreadLocalRandom은 각 쓰레드마다 다른 난수를 반환한다. 이는 경합 문제가 발생하지 않아 성능적으로 안전하다.
Randoms 안에서는 static final로 Random 인스턴스인 defaultRandom이 선언된다. 이는 모든 Randoms 인스턴스가 동일한 시드값을 가짐을 알 수 있다. 즉 여기 저기서 Randoms를 선언해도 내부 Random은 동일하다.
그러면 여기서 질문!
동일한 Random 인스턴스를 공유하니 매번 동일한 값이 출력되지 않을까??
답은 아니다! 일단 출력해보면,


이렇게 다르게 출력된다. 왜 그럴까??
동일한 Random객체를 사용하지만, Random 객체는 난수를 생성할 때 마다 내부 상태가 계속 변한다. 첫번째 호출 이후 defaultRandom은 다음 호출을 위해서 내부 시드 값을 변경한다.
그러면 초기 시드는 시간을 기반으로 생성 + 호출 시점마다 내부 시드가 바뀜 + 초기 시드는 시간 기반이기 때문에 이전 시드와 동일할 수 없음
이 모든 것이 합쳐서 늘 다른 값이 나오는 것이다.
2. 여러 가지 메소드
외부에서 사용 가능한 함수는 총 4개가 있다.
- pickNumberInList
- pickNumberInRange
- pickUniqueNumbersInRange
- shuffle
이름이 직관적이라 메소드 명처럼 사용하면 된다. 옳지 못한 사용의 경우 내부 validate가 각각 사용 전 확인을 해준다.
참고
https://youngi2.tistory.com/15
[Java] Utility class란?
java를 쓰다보면 Math 함수를 한번쯤을 써봤을겁니다 근데 이런 생각 안해보셨나요?왜 Math는 new로 객체를 만들지 않고,클래스 이름만으로 기능을 사용할 수 있지...? 우테코 프리코스 1주차를 지나
youngi2.tistory.com
https://velog.io/@ozragwort/Util-%ED%81%B4%EB%9E%98%EC%8A%A4%ED%8C%A8%ED%82%A4%EC%A7%80
Util 클래스/패키지
Util 클래스와 패키지가 무엇이고 어떤 책임을 가질까?
velog.io
https://covenant.tistory.com/255
완벽정리! LocalDateTime을 살펴보자
시작하며Go 언어로 개발하다가 스프링으로 넘어와서 의외로 헷갈렸던 부분이 시간을 다루는 것이었습니다. Go에서는 time, Month 패키지명 부터 시작해서 함수 이름이 상당히 명확한데 LocalDateTime
covenant.tistory.com
Random 대신 ThreadLocalRandom을 써야 하는 이유
java.util.Random은 멀티 쓰레드 환경에서 하나의 인스턴스에서 전역적으로 의사 난수(pseudo random)를 반환한다. 따라서 같은 시간에 동시 요청이 들어올 경우 경합 상태에서 성능에 문제가 생길 수 있
velog.io
'우테코 끄적끄적' 카테고리의 다른 글
| [우테코] 8기 프리코스 자유주제 회고 (1) | 2025.11.29 |
|---|