profile image

L o a d i n g . . .

728x90

 

System.out.println() 메서드는 Java 프로그램에서 console로 출력이 필요할 때 가장 많이 사용하는 메서드 중 하나입니다. 하지만 정작 내부원리를 이해하려 노력해본 결과가 없었기에... 이번 포스팅을 통해 자세히 알아보고자 합니다. System.out.println("hello")에 breakpoint를 걸고 디버깅하며 차근차근 내부를 들여다보겠습니다. 

public class Main {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

 

System.out.println("hello")의 System.out은 PrintStream 클래스의 객체입니다. PrintStream은 다음과 같은 메서드를 통해서 인자로 전달받은 값을 console에 표시합니다. 

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

 

synchronized block에 의해 호출되는 객체는 thread-safe하게 동작합니다.

만약 System.out.println("hello")에 "hello"라는 String 대신 객체를 전달받았다면 객체를 String으로 변환하는 과정이 추가적으로 발생합니다(method overloading) 

더보기
public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

println() 메서드의 인자로 객체가 전달됐을 때 

 

다음은 public void println(String x) 메서드 내부에서 실행되는 print(x) 메서드입니다. 해당 코드에서 전달받은 인자값을 console에 표시합니다. private void write(String s) 가 실질적으로 console에 출력하는 코드와 연관이 있기에 자세히 알아보겠습니다. 

public void print(String s) {
    if (s == null) {
        s = "null";
    }
    write(s);
}

private void write(String s) {
    try {
        synchronized (this) { // 1
            ensureOpen(); // 2
            textOut.write(s); // 3
            textOut.flushBuffer(); // 4
            charOut.flushBuffer(); // 5
            if (autoFlush && (s.indexOf('\n') >= 0))
                out.flush(); // 6
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt(); // 7
    }
    catch (IOException x) {
        trouble = true; // 8
    }

1.  synchronized (this) 

synchronized block을 통해 PrintStream 객체를 thread-safe 하게 동작하도록 합니다. 

 

2. ensureOpen();

실행중인 Java 애플리케이션과 값을 표시하고자 하는 곳(여기서는 console)을 연결해주는 stream이 정상적으로 연결된 상태인지 확인하는 메서드입니다. 

private void ensureOpen() throws IOException {
    if (out == null)
        throw new IOException("Stream closed");
}

 

3. textOut.write(s); 

  • textOut 객체는 BufferedWriter 클래스의 객체입니다. BufferedWriter은 text를 character-output stream에 쓰기 위해 사용됩니다. 쓰고자 하는 text를 buffering 함으로써 성능을 향상시킵니다. 
  • write(s) 메서드는 다음과 같습니다. 
public void write(String str) throws IOException {
    write(str, 0, str.length());
}

public void write(String str, int off, int len) throws IOException {
    synchronized (lock) {
        char cbuf[];
        if (len <= WRITE_BUFFER_SIZE) { // WRITE_BUFFER_SIZE defaults to 1024
            if (writeBuffer == null) {
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            cbuf = writeBuffer;
        } else {    // Don't permanently allocate very large buffers.
            cbuf = new char[len];
        }
        str.getChars(off, (off + len), cbuf, 0);
        write(cbuf, 0, len);
    }
}

abstract public void write(char cbuf[], int off, int len) throws IOException;
  • WRITE_BUFFER_SIZE의 default값은 1024입니다.
  • writeBuffer는 string 또는 character를 출력하기 전 임시로 보관하는 buffer 입니다.
  • str.getChars(off, (off + len), cbuf, 0); 는 cbuf(character array) 에 str(출력 대상 String) 의 (off) ~ (off + len) 에 위치한 character을 복사하는 메서드입니다. 
  • 추상 메서드 abstract public void write(char cbuf[], int off, int len) throws IOException; 를 통해서 추상 클래스의 구현체에게 출력을 위임합니다. 추상 메서드인 write을 구현하는 클래스는 OutputStreamWriter입니다. 
  • OutputStreamWriter는 character stream -> byte stream 의 매개자 역할을 하는 클래스입니다. 특정 charset을 통해서 character을 byte로 변환합니다. 하단의 코드는 OutputStreamWriter이 구현한 write 메서드입니다. 
private final StreamEncoder se;

...

public void write(char cbuf[], int off, int len) throws IOException {
    se.write(cbuf, off, len);
}
  • OutputStreamWriter은 write를 다시한번 StreamEncoder에게 위윔합니다(se.write(cbuf, off, len);). 
public void write(char cbuf[], int off, int len) throws IOException {
    synchronized (lock) {
        ensureOpen();
        if ((off < 0) || (off > cbuf.length) || (len < 0) ||
            ((off + len) > cbuf.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        implWrite(cbuf, off, len);
    }
}

void implWrite(char cbuf[], int off, int len)
    throws IOException
{
    CharBuffer cb = CharBuffer.wrap(cbuf, off, len);

    if (haveLeftoverChar)
    flushLeftoverChar(cb, false);

    while (cb.hasRemaining()) {
    CoderResult cr = encoder.encode(cb, bb, false);
    if (cr.isUnderflow()) {
       assert (cb.remaining() <= 1) : cb.remaining();
       if (cb.remaining() == 1) {
            haveLeftoverChar = true;
            leftoverChar = cb.get();
        }
        break;
    }
    if (cr.isOverflow()) {
        assert bb.position() > 0;
        writeBytes();
        continue;
    }
    cr.throwException();
    }
}
  • CharBuffer cb = CharBuffer.wrap(cbuf, off, len) 
    • cbuf (char array)를 buffer로 wrap 함으로써(CharBuffer cb), cb 와 cbuf 가 서로 연동되도록 설정하는 코드입니다. 즉 한쪽에서 변경이 발생하면 다른쪽도 변경이 반영됩니다. 
  • 그 외의 코드는 bb(ByteBuffer) 에 char array를 encoding 해서 복사하는 코드입니다. 위의 메서드에서 writeBytes(); 메서드는 또다시 StreamEncoder의 메서드를 호출하며 우리가 인자로 전달한 String을 console에 출력합니다. 

StreamEncoder class 

private void writeBytes() throws IOException {
    bb.flip();
    int lim = bb.limit();
    int pos = bb.position();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);

    if (rem > 0) {
    	if (ch != null) {
        	if (ch.write(bb) != rem)
  				assert false : rem;
    	} else {
        	out.write(bb.array(), bb.arrayOffset() + pos, rem);
    	}
    }
    
    bb.clear();
}

out.write(bb.array(), bb.arrayOffset() + pos, rem);는 PrintStream 클래스의 메서드를 호출합니다 

 

PrintStream class 

public void write(byte buf[], int off, int len) {
    try {
        synchronized (this) {
            ensureOpen();
            out.write(buf, off, len);
            if (autoFlush)
                out.flush();
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

out.flush() 는 또다시 BufferedOutputStream 클래스의 flush 메서드를 호출합니다. 

public synchronized void flush() throws IOException {
    flushBuffer();
    out.flush();
}

flushBuffer() 메서드는 다음과 같습니다. 

private void flushBuffer() throws IOException {
    if (count > 0) {
        out.write(buf, 0, count);
        count = 0;
    }
}

out.write(buf, 0, count)가 최종적으로 console에 전달받은 buffer를 표시합니다. 

 

FileOutputStream write 메서드 

public void write(byte b[], int off, int len) throws IOException {
    writeBytes(b, off, len, append);
}
private native void writeBytes(byte b[], int off, int len, boolean append)
    throws IOException;

결국에는 native 메서드를 호출하여 console에 결과값을 표시하게 됩니다. 

 

jdk8u-dev-jdk/src/solaris/native/java/io/FileOutputStream_md.c

Native method의 구현은 다음과 같습니다. 더 깊게 들어가면 C 코드를 뜯어봐야 하기 때문에 여기서 마치겠습니다. 

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
    writeBytes(env, this, bytes, off, len, append, fos_fd);
}

 

4, 5, 6. textOut.flushBuffer(), charOut.flushBuffer() ... 

해당 Buffer(textOut, charOut)에 담긴 값을 underlying stream으로 flush합니다. 

 

결론 

System.out.println()의 내부 원리에 대해 분석하려하면 Buffer, Stream의 개념과 동작원리를 이해하고 있어야 합니다. 요약하자면 Buffer을 통해서 출력하고자 하는 데이터를 묶어서 한번에 Stream으로 전달하고 최종적으로는 Stream이 native 메서드를 호출해서 우리가 전달한 값을 console에 출력하게 됩니다. 

 

Reference 

https://www.youtube.com/watch?v=bNLj8zsXaRs

 

728x90
복사했습니다!