프로그래밍/Java

[Java] JDK 부수기 - (2) java.lang.System - 1. err, in, out

Churnobyl 2023. 11. 23. 16:02
728x90
반응형

 


java.lang.System

The System class contains several useful class fields and methods. It cannot be instantiated. Among the facilities provided by the System class are standard input, standard output, and error output streams; access to externally defined properties and environment variables; a means of loading files and libraries; and a utility method for quickly copying a portion of an array.
System 클래스는 유용한 클래스 필드와 메서드들을 가지고 있다. 이 클래스는 인스턴스화할 수 없다. System 클래스가 제공하는 기능 중에는 표준 입력 스트림, 표준 출력 스트림, 그리고 예외 출력 스트림이 있다. 추가적으로 외부에서 정의된 properties들과 환경 변수에 대한 접근, 파일과 라이브러리를 로딩하는 수단, 그리고 배열의 일부를 빠르게 복사할 수 있는 유틸리티 메서드도 가지고 있다.

 

 System 클래스는 위의 설명처럼 in, out, err 표준 스트림들을 사용할 때 주로 사용하는 클래스다. System 클래스는 자바 프로그램에 걸쳐 단 하나만 존재할 수 있으며 인스턴스화할 수 없다. 따라서 모든 멤버 변수와 메서드는 static 키워드를 달고 있다.  그 밖에도 gc() 메서드 같이 가비지 컬렉터를 즉시 실행시키거나 currentTimeMillis()처럼 OS로부터 현재 시간을 밀리초 단위로 받아오는 메서드 등 유용한 메서드들이 있다. 추가적으로 Java 9에서 도입된 간소화된 로깅 API인 Logger와 LoggerFinder가 중첩 클래스로 구성되어 있다.

 

// src/java.base/java/lang/System.java

public final class System {
    /* Register the natives via the static initializer.
     *
     * The VM will invoke the initPhase1 method to complete the initialization
     * of this class separate from <clinit>.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    
    /** Don't let anyone instantiate this class */
    private System() {
    }
    
    public static final InputStream in = null;
    public static final PrintStream out = null;
    public static final PrintStream err = null;

    /* ... */
}

 

 System 클래스는 다음과 같이 시작한다. 우선 registerNatives()를 실행해 네이티브 메서드들을 JVM에서 찾을 수 있도록 매핑한다. 또한 생성자의 접근자를 private로 설정해 외부에서 접근할 수 없도록 막고 인스턴스화를 막는다. 그리고 in, out, err 멤버변수를 null로 초기화한다. 표준 스트림들을 미리 지정해 두지 않는 이유는 자바 프로그램이 실행되는 OS와 Console이 전부 다르므로 각각의 스트림를 다르게 설정해주어야 하기 때문이다. 이 부분은 네이티브 메서드인 setIn0(), setOut0(), setErr0() 메서드를 통해 스트림을 설정해준다. 더 자세한 내용은 아래에서 알아보자.

 


중첩 클래스 (Nested Classes)

제어자 및 타입
(Modifier and Type)
클래스(Class) 설명(Description)
static interface System.Logger Java 9에서 도입된 자바의 기본 로깅 프레임워크로 메세지를 log한다.
static class System.LoggerFinder LoggerFinder 서비스는 사용하는 기본 프레임워크에 대한 logger를 생성, 관리 및 구성을 담당한다. 즉, System.Logger 인스턴스를 제공하는 팩토리 역할을 한다.

*JDK17.0.9 (2023-10-17 LTS 기준)

 

 System.Logger와 Logger를 생성하는 팩토리 역할을 하는 System.LoggerFinder는 Java 9에서 도입된 로그 관련 API다. 전통적으로 자바는 로깅 API로 java.util.logging 내장 로깅 API를 사용하거나 외부 라이브러리인 SLF4J 프레임워크를 실질적인 구현체들인 logback, log4j 심지어 java.util.logging와 결합한 형태를 사용했다. 로깅 API는 이미 충분히 다양한 옵션들이 존재하지만 자바 개발자들은 System.Logger에서 로깅 API을 단순화, 표준화하고자 했다. 그래서 System.Logger는 자바의 java.base 모듈 내에 정의되어 외부 의존성을 피하도록 하며 아주 단순한 사용 방법을 가지고 있다. 하지만 전통적인 방법을 대체하기엔 너무 단순하고 세부적인 설정을 할 수 없다. 그냥 java.util.logging 모듈없이 기본적인 로깅을 할 수 있다 정도?

 

 System.Logger 인터페이스는 기존 로깅 라이브러리보다는 단순화되어 사용하기 간편한 로깅 API다. 단순화되었다는 뜻은 사용하기 쉽고 빠르다는 뜻도 되지만 그만큼 세부적인 설정을 할 수 없다는 단점이 있다. System.Logger 인터페이스는 내부에 로깅 수준을 나열한 Level이라는 enum 클래스를 가지고 있으며 log()메서드를 통해 로그 메세지를 출력한다. System.LoggerFinder 추상 클래스는 JDK 로깅 시스템 구현을 위한 jdk.internal.logger 패키지와 함께 동작해 안정적으로 Logger 객체를 생성하고 관리한다. jdk.internal.logger 패키지의 구현 방식을 보면 일단 java.util.logging 라이브러리가 존재하고 애플리케이션과 연결되어 있으면 Logger는 java.util.logging.Logger로 대체된다. 만약 없으면 내부 패키지를 이용해 아주 간단한 Logger 객체를 제공한다. 사실상 java.util.logging 라이브러리를 백엔드로 사용하므로 써야할 이유를 모르겠다?

 


필드 (Fields)

제어자 및 타입
(Modifier and Type)
필드(Field) 설명(Description)
static final PrintStream err 표준 에러 출력 스트림
static final InputStream in 표준 입력 스트림
static final PrintStream out 표준 출력 스트림

 

 System 클래스에는 각각 단일한 err, in, out 필드가 존재한다. 위에서 설명했다시피 null로 초기화된 뒤에 OS와 터미널에 따라서 적절한 스트림이 설정된다. 이 스트림을 통해 사용자로부터 입력을 받거나 터미널로 출력해 줄 수 있다. 스트림을 설정하는 코드는 다음 글에서 더 자세히 공부해보자.

 

 


메서드 (Methods)

제어자 및 타입
(Modifier and Type)
메서드(Field) 설명(Description)
static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length 배열을 복사하는 메서드. 기존 배열의 특정 시작점에서 length만큼 복사해 목표 배열에 복사해준다.
static String clearProperty(String key) 지정된 key가 나타내는 시스템 속성을 제거한다.
static Console console() 현재 JVM과 연관된 Console 객체가 있는 경우 이를 반환한다.
static long currentTimeMillis() OS로부터 현재 시간을 밀리초 단위로 받아온다
static void exit(int status) JVM의 종료 시퀀스를 수행한다. status는 상태 코드이며 0이 아닌 경우 비정상적인 종료로 간주된다.
static void gc() 현 시점에서 가비지 컬렉터(Garbage Collector)를 실행한다.
static Map<String, String> getenv() 현재 시스템의 환경변수(OS 수준)를 Map 자료형으로 리턴한다.
static String getenv(String name) 해당 name의 환경변수의 값을 리턴한다.
static System.Logger getLogger(String name) 해당 name의 Logger 인스턴스를 리턴한다.
static System.Logger getLogger(String name, ResourceBundle bundle) 언어 설정이나 일관성 있는 로그 메세지를 위해 bundle을 함께 제공하면서 해당 name의 Logger 인스턴스를 리턴한다
static Properties getProperties() JVM에 전달되는 시스템 속성에 관한 데이터를 담고 있는 Properties 객체를 리턴한다.
static String getProperty(String key), getProperty(String key, String def) 해당 name의 시스템 속성의 값을 리턴한다. def는 시스템 속성에 key가 name인 값이 없을 시 리턴하는 기본값.
static SecurityManager getSecurityManager() Deprecated 예정 (since=17)
자바 어플리케이션의 SecurityManager를 리턴한다.
static int identityHashCode(Object x) x의 클래스에서 hashCode() 메서드를 오버라이딩한 것과 관계없이 x 객체에 대한 JVM의 기본 hashCode()메서드의 결과를 리턴한다. 
static Channel inheritedChannel() 부모 프로세스에서 채널을 명시적으로 현재 프로세스에 전달한 경우 해당 채널을 리턴한다.
static String lineSeparator() OS에 따른 행 구분자 문자열을 리턴한다.
static void load(String filename) 특정 filename(절대경로) 경로에 있는 네이티브 코드 라이브러리를 로드한다.
static void loadLibrary(String libname) java.library.path 시스템 속성에 지정된 디렉토리들 중에서 해당 libname 이름의 라이브러리를 찾아 로드한다.
static String mapLibraryName(String libname) 라이브러리 이름 libname을 해당 OS의 네이티브 라이브러리 형식에 맞게 변환한다
static long nanoTime() 현재 시간을 나노초 단위로 리턴한다. 성능 측정이나 벤치마킹을 위해 시간 간격을 측정할 때 사용한다.
static void runFinalization() Deprecated 예정 (since=18)
pending 상태인 객체들에 finalize() 메서드를 실행하도록 한다.
static void setErr(PrintStream err) 표준 예외 출력 스트림을 다시 할당한다.
static void setIn(InputStream in) 표준 입력 스트림을 다시 할당한다.
static void setOut(PrintStream out) 표준 출력 스트림을 다시 할당한다.
static String setProperty(String key, String value) 지정된 key와 value로 시스템 속성을 설정한다.
static void setSecurityManager(SecurityManager sm) Deprecated 예정 (since=17)
자바 어플리케이션의 SecurityManager를 재할당한다.

*JDK17.0.9 (2023-10-17 LTS 기준)

 

 System 클래스의 메서드는 위와 같이 엄청 많지만 보안에 대한 변화로 인해 deprecated될 예정인 SecurityManager 관련 메서드와 finalize() 관련 메서드를 제외하면 우리가 평소에 사용할만한 메서드들은 많지 않다. 따라서 많이 쓸만한 메서드들만 모아서 글을 쓰려고 한다.

 


1. err, in, out

System 클래스의 표준 입출력, 에러 출력 스트림

 

JVM 뜯어보기

 System클래스에는 입출력 및 예외 출력을 담당하는 세 개의 스트림이 있다. 이 스트림들을 활용해 자바 애플리케이션과 사용자 간 상호작용이 가능하다. 특징적으로 이 세 스트림은 자바 애플리케이션의 시작부터 종료까지 계속 열려 있고 동작을 실행할 준비가 되어 있다. 그럼 차근차근 뜯어보자.

 

// src/java.base/java/lang/System.java

public final class System {

    public static final InputStream in = null;
    public static final PrintStream out = null;
    public static final PrintStream err = null;
    
    public static void setIn(InputStream in) {
        checkIO();
        setIn0(in);
    }
    
    public static void setOut(PrintStream out) {
        checkIO();
        setOut0(out);
    }
    
    public static void setErr(PrintStream err) {
        checkIO();
        setErr0(err);
    }
    
    private static void checkIO() {
        @SuppressWarnings("removal")
        SecurityManager sm = getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setIO"));
        }
    }

    private static native void setIn0(InputStream in);
    private static native void setOut0(PrintStream out);
    private static native void setErr0(PrintStream err);
    
}

 

 위의 코드에서 보다시피 System의 멤버변수 in, out, err는 1개의 InputStream과 2개의 PrintStream을 타입으로 가진다. 또한 초기에는 null로 초기화된다. 이제 JVM이 실행되면서 System 클래스를 로드할 때, 아래의 세 메서드 setIn, setOut, setErr가 실행되며 해당 OS와 콘솔에 맞는 적절한 인스턴스를 갖게 된다. 

 

 이제 각각의 메서드를 살펴보면 모두 checkIO() 메서드와 함께 각각의 이름에 0을 붙인 네이티브 메서드를 실행한다. checkIO() 메서드에서는 SecurityManager 객체를 sm 변수로 가져와서 setIO, 즉 해당 애플리케이션에게 입출력을 변경할 권한이 있는지 확인한다. I/O는 시스템 보안에 영향을 끼칠 수 있는 아주 중요한 리소스이므로 보안 상의 문제가 없는지 한번 더 체크하는 것이다. 만약 권한이 없다면 SecurityException이 발생한다.

 

 문제가 없다면 이제 각각의 멤버변수를 setIn0(in), setOut0(out), setErr0(err)로 각각의 메서드에 담아서 실행한다. 그리고 이 세 메서드는 네이티브 메서드로 구현되어 있다. 사실 이 부분은 JVM가 System 클래스를 초기화하는 initPhase1() 메서드에서 실행되지만 자세한 내용은 추후에 스레드를 공부하면서 함께 다루기로 하고 지금은 그냥 System 클래스가 호출될 때 함께 실행되는 것으로 이해하면 된다. 이제 JVM에서 해당 코드를 살펴보자.

 

// src/java.base/share/native/libjava/System.c

/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
JNIEXPORT void JNICALL
Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

JNIEXPORT void JNICALL
Java_java_lang_System_setErr0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"err","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

 

 자바 메서드 setIn0(), setOut0(), setErr0()는 JVM에서 다음과 같은 함수 이름으로 매핑되어 있다. 자바의 입출력과 관계된 in, out, err를 왜 네이티브 메서드로 구현했는지는 위의 주석을 보면 알 수 있다. in, out, err 이 셋은  final 변수로 선언되었고 null로 초기화 되었으므로 자바 수준에서는 새롭게 할당이 불가능하다. 이를 자바 수준에서 변경하는 것은 자바 언어의 규칙에 위배된다. 따라서 더 낮은 수준인 네이티브 메서드 수준에서 우회해서 새롭게 할당하도록 하는 것이다. "표준" 스트림이므로 final을 통해 자바 수준에서 쉽게 변경하는 것을 막는 방법으로 보인다.

 

 이제 각각 함수의 코드를 보면 GetStaticFieldId()메서드를 이용해 System 클래스에서 in, out, err 정적 변수에 해당하는 정적 필드 id값을 찾고 필드 식별자 fid에 저장한다. 여기서 정적 필드 id값은 JVM에서 JNI를 사용할 때, 필드를 참조하는데 사용하는 내부 식별자다. 만약 fid가 0이라면 (fid == 0) 적절한 필드 식별자를 찾지 못했음을 나타내며 빈 값을 리턴한다. 만약 적절한 필드 식별자를 찾았다면 새로운 InputStream 혹은 PrintStream 객체를 SetStaticObjectField함수로 넘겨서 해당 fid에 새롭게 객체를 설정해준다. 그럼 정적 필드를 설정하는 SetStaticObjectField함수를 다시 찾아보자.

 

// src/hotspot/share/prims/jni.cpp

JNI_ENTRY(void, jni_SetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fieldID, jobject value))
 HOTSPOT_JNI_SETSTATICOBJECTFIELD_ENTRY(env, clazz, (uintptr_t) fieldID, value);
  JNIid* id = jfieldIDWorkaround::from_static_jfieldID(fieldID);
  assert(id->is_static_field_id(), "invalid static field id");
  // Keep JVMTI addition small and only check enabled flag here.
  // jni_SetField_probe() assumes that is okay to create handles.
  if (JvmtiExport::should_post_field_modification()) {
    jvalue field_value;
    field_value.l = value;
    JvmtiExport::jni_SetField_probe(thread, nullptr, nullptr, id->holder(), fieldID, true, JVM_SIGNATURE_CLASS, (jvalue *)&field_value);
  }
  id->holder()->java_mirror()->obj_field_put(id->offset(), JNIHandles::resolve(value));
  HOTSPOT_JNI_SETSTATICOBJECTFIELD_RETURN();
JNI_END

 

 자바의 정적 필드를 설정하는 SetStaticObjectField함수는 다음과 같이 구현되어 있다. 넘겨받은 jfieldID타입 fieldID를 JNIid타입으로 변환해 id에 저장한 뒤 'id -> holder() ...' 라인에서 설정해준다. 저 부분을 간단하게 설명하면 JNIid 타입 id에서 holder()를 호출해 해당 id가 속한 클래스의 'Klass' 객체를 얻은 뒤 다시 java_mirror()를 호출해 자바 레벨의 'Class' 객체를 얻는다. 이 'Class'객체에서 obj_field_put()을 호출해 해당 'Class'객체의 id->offset(), 즉 해당 id의 위치에 새로운 value(새로운 Inputstream or PrintStream)를 설정해주는 것이다. 그럼 마지막으로 obj_field_put함수가 어떻게 구현되어 있는지 알아보자.

 

// src/hotspot/share/oops/oop.inline.hpp

inline void oopDesc::obj_field_put(int offset, oop value)
	{ HeapAccess<>::oop_store_at(as_oop(), offset, value); }

 

 obj_field_put 함수는 다음과 같이 구현되어 있다. obj_field_put이 호출되면 힙 영역에 Access해서 해당 'Class'객체 포인터 위치에서 offset 위치에 있는 필드에 value 즉 스트림을 새롭게 설정한다. 이 obj_field_put에서 실제적으로 메모리에 접근해 in, out, err에 스트림을 설정해주는 것이다. 이렇게 in, out, err 멤버변수에 스트림이 설정되는 과정을 살펴볼 수 있었다. 실제로 자바가 OS와 상호작용할 때 통로인 InputStream과 OutStream에 대해서는 별도로 다른 글에서 정리해보겠다.

 

사용해보기

 일반적인 상황에서 InputStream객체 만을 단독적으로 사용하지는 않는다. InputStream객체에서는 OS로부터 입력값을 1byte단위로 읽어오기 때문에 한글 입력값 같은 멀티바이트 문자를 제대로 처리할 수 없기 때문이다. 하지만 in, out, err만을 사용해서 간단한 예제를 만들어보자.

 

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        int data;

        while((data = System.in.read()) != -1) {
            if (data > 127) {
                System.err.println("ASCII 코드를 벗어났습니다.");
                break;
            }

            System.out.print((char) data);
        }
    }
}

 

 

 자바에서 IO 작업이 있을 때는 항상 예외를 처리해 주어야 한다. 위의 코드처럼 throws IOException을 통해 호출자에게 예외 처리를 위임시켰다. System.in.read()메서드는 OS로부터 입력받는 결과를 바이트 단위로 읽어 int형으로 리턴받고 스트림 끝에서 -1을 리턴한다. 따라서 1byte 범위인 127까지를 입력 받을 수 있고 그 이상의 임의의 입력값은 일부 데이터가 소실된 채로 받는다. 그래서 data에 127 이상의 값이 들어왔다면 System.err.println() 메서드를 이용해 입력값 범위를 벗어 났음을 출력하도록 하고, 일반적인 영문자와 숫자의 경우에는 data로 들어온 값을 char 자료형을 캐스팅해 출력하도록 했다. 자바 입출력에 대한 더 자세한 내용은 InputStream이나 PrintStream, 혹은 InputStreamReader와 같은 클래스를 공부할 때 심도 깊게 공부할 수 있을 것이다.

반응형