java를 쓰다보면 Math 함수를 한번쯤을 써봤을겁니다
근데 이런 생각 안해보셨나요?
왜 Math는 new로 객체를 만들지 않고,
클래스 이름만으로 기능을 사용할 수 있지...?
우테코 프리코스 1주차를 지나오면서 코드를 뜯어보다가 Console을 utility class로 선언한 것을 발견해서, 2주차인 지금 이에 대해서 글을 작성해보려고 합니다!
🤔 일반 클래스 vs 유틸리티 클래스
가장 큰 차이는 객체를 만드느냐, 안 만드느냐입니다.
일반적인 클래스는 이렇게 씁니다👇
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
즉, new 키워드로 객체를 만들어야만 그 안의 기능(nextLine)을 사용할 수 있습니다
이런 클래스들은 인스턴스마다 독립적인 상태(state)를 가지게 됩니다.
반면 유틸리티 클래스는 이렇게 사용합니다 👇
String input = Console.readLine();
객체를 만들지 않았는데도 메서드를 바로 사용할 수 있죠?
이게 바로 유틸리티 클래스의 핵심입니다!
🌱 Console 코드 분석
우테코 1주차에서 사용한 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;
}
}
이 코드를 하나씩 뜯어보면서 설명드리겠습니다
1) 생성자
private Console() {
}
생성자 선언은 여러분도 많이 접해봤을꺼라 생각합니다
다만 중요한 점은 private으로 선언에서 외부에서 new Console()을 사용하지 못하게 막아놨다는 점입니다.
유틸리티 클래스는 인스턴스를 만들 이유가 거의 없으니 일부러 못 만들게 합니다
이렇게 하면 클래스가 오직 정적(static) 매서드만으로 사용된다는 의도가 명확해집니다
2) 정적필드
private static Scanner scanner;
static이 붙은 필드는 클래스 단위로 하나만 존재합니다.
즉, Console 클래스 전체에서 공유되는 Scanner 객체가 하나만 유지되는 겁니다.
처음엔 null로 시작하고, 필요할 때 생성하는겁니다
3) readline()
public static String readLine() {
return getInstance().nextLine();
}
외부에서 입력을 받을 때 Console.readLine()처럼 클래스 이름으로 바로 호출할 수 있게 해주는 메서드입니다.
내부적으로 getInstance()를 통해 Scanner를 얻고 nextLine()을 호출해서 입력 라인을 반환합니다.
4) getInstance(지연초기화)
private static Scanner getInstance() {
if (scanner == null) {
scanner = new Scanner(System.in);
}
return scanner;
}
여기가 핵심입니다.
코드를 읽어보면, scanner가 아직 없을 때(즉 첫 호출 시)만 new Scanner(System.in)으로 생성하고 그걸 재사용하고 있습니다.
이 방식 덕분에 프로그램 시작 시점에 불필요하게 자원을 미리 쓰지 않고, 실제로 필요할 때만 생성할 수 있습니다.
이를 Lazy Initialization(지연 초기화)라고 부릅니다
5) 정리 메서드
public static void close() {
if (scanner != null) {
scanner.close();
scanner = null;
}
}
이는 입력이 끝났을 때 자원을 정리해주는 메서드입니다.
✨ 그렇다면 객체는 언제 생성되고 언제까지 유지될까?
- 생성 시점: Console.readLine()(또는 내부적으로 getInstance()가 호출될 때) 처음으로 scanner == null이면 new Scanner(System.in)가 실행되어 생성됩니다.
- 유지 기간: 생성된 Scanner는 Console 클래스의 정적 필드로 계속 유지됩니다. (프로그램이 끝나거나 Console.close()가 호출될 때까지)
- 소멸 / 정리: Console.close()를 호출하면 scanner.close()가 실행되고 scanner는 null로 바뀝니다.
정리하자면 아래와 같습니다.
| 생성 시점 | Console.readLine() 첫 호출 시 |
| 재사용 | 그 후 모든 Console.readLine()은 동일한 Scanner 사용 |
| 자원 해제 | Console.close() 호출 시 (또는 프로그램 종료 시 JVM 종결) |
🐢 utility class vs 일반 클래스
🚫 유틸리티 클래스를 사용하지 않은 버전
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// 매번 Scanner 객체를 직접 생성해야 함
Scanner scanner = new Scanner(System.in);
System.out.print("이름을 입력하세요: ");
String name = scanner.nextLine();
System.out.println("안녕하세요, " + name + "님!");
// 사용이 끝나면 객체 자체를 반드시 닫아줘야 함
scanner.close();
}
}
✅ 유틸리티 클래스를 사용한 버전
public class Main {
public static void main(String[] args) {
// Scanner 객체를 생성할 필요 없음!
System.out.print("이름을 입력하세요: ");
String name = Console.readLine();
System.out.println("안녕하세요, " + name + "님!");
Console.close(); // 사용 끝나면 클래스 자체에서 정리
}
}
💪 마무으리
유틸리티 클래스는 “객체를 매번 만들 필요 없는 도구상자”입니다.
Console은 내부에서 필요한 객체(Scanner)를 필요할 때 한 번 만들어 재사용하고, 끝나면 정리해 주는 전형적인 패턴이라 이해하면 좋습니다.
다만, 전역처럼 공유되는 static 자원이기 때문에 동시성 문제(멀티스레드) 가 발생할 수 있습니다.
싱글스레드에서의 간단한 콘솔 입력용이라면 상관없지만, 멀티스레드 환경에서는 동기화를 고려해줄 필요가 있습니다!
또한 입력 스트림이 닫히고 다른 곳에서 다시 사용하려면 Console.getInstance()로 새로 생성되긴 하지만, System.in 자체를 닫으면 다시 열 수 없는 경우가 종종 발생하기도 하니 close() 호출 시점에 주의해야 합니다!
'Develop note' 카테고리의 다른 글
| [트러블슈팅] 공백 입력 테스트코드, NoSuchElementException (1) | 2025.10.28 |
|---|---|
| [Pattern] 예외 팩토리(Exception Factory) 패턴: 깔끔하고 유연한 예외 처리 (1) | 2025.10.23 |
| [Git] Git alias 설정은 어떻게 돌아가는가? (0) | 2025.10.20 |
| [Git] Hooks 적용으로 커밋 메세지 형식 강제하기 (0) | 2025.10.15 |
