일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- IT
- OS
- 백준
- 컴퓨터구조
- react
- Algorithm
- mongoDB
- C
- 운영체제
- c언어
- JPA
- spring webflux
- MySQL
- MSA
- redis
- Proxy
- 디자인 패턴
- Heap
- 알고리즘
- 자료구조
- JavaScript
- Spring
- Galera Cluster
- design pattern
- Java
- 파이썬
- 네트워크
- Kafka
- Data Structure
- 자바
- Today
- Total
시냅스
코드로 뜯어보는 System.out.println 대신 logger 를 사용해야 하는 이유 본문
System.out.println()
System 에서 out 은 static 변수로 PrintStream 클래스로 할당됩니다.
out 은 jvm 이 시작될 때 intializeSystemclass 함수를 사용하여
jvm 의 standard outputstream(os 로 부터 받은 standard output 을 PrintStream 으로 구현한 것) 으로부터 생성되어 할당됩니다.
고로, 우리는 PreamStream.println() 을 사용하고 있는 것이 됩니다.
혹시 initializeSystemclass() 함수가 궁금하다면 더보기를 참고해주세요...
private static void initializeSystemClass() {
props = new Properties();
initProperties(props);
String s = props.getProperty("line.separator");
if (s != null)
lineSeparator = s;
sun.misc.Version.init();
FileInputStream localFileInputStream = new FileInputStream(FileDescriptor.in);
FileOutputStream localFileOutputStream1 = new FileOutputStream(FileDescriptor.out);
FileOutputStream localFileOutputStream2 = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(localFileInputStream));
setOut0(new PrintStream(new BufferedOutputStream(localFileOutputStream1, 128), true));
setErr0(new PrintStream(new BufferedOutputStream(localFileOutputStream2, 128), true));
}
이 함수는 jdk 를 decompile 하여 확인하실 수 있습니다.
println 은 다음과 같이 구현됩니다.
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
// print
public void print(String s) {
write(String.valueOf(s));
}
// write
private void write(String s) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush && (s.indexOf('\\n') >= 0))
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
// textOut(BufferedStream).write
public void write(String s, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
int b = off, t = off + len;
while (b < t) {
int d = min(nChars - nextChar, t - b);
s.getChars(b, b + d, cb, nextChar);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}
뭔가 어지러운 느낌이 들지만 중요한 것은 곳곳에 lock 이 걸려 있다는 점입니다.
(아마도, PrintStream 이 BufferedStream 을 사용하고 BufferedStream 은 fd 의 standard output 을 사용하므로 결국 printstream 은 write system call 을 사용한다고 말할 수 있겠습니다.)
spring 을 실행하며 tomcat 은 max thread 200 개를 갖게 됩니다.
만약 thread 1 에서 lock 을 획득하여 print 하게 된다면,
나머지 199개는 print 를 하기 위해 lock 을 획득할 때 까지 기다려야 할 것입니다.
이러한 성능 저하 때문에 운영환경에서는 절대 사용해서는 안되며, logger 를 사용해야 할 것입니다.
Logger
대표적인 logging framework 인 logback 은 println 의 한계를 극복하기 위해 여러가지 기능을 제공합니다.
로그 레벨 설정에 대한 출력 형식 필터링이나 출력 자체를 제어하거나
logging 을 위한 스레드를 따로 두어 전체 애플리케이션의 성능이 저하되지 않도록 합니다.
내부적으로 ConsoleAppender 를 통해 특정 스레드에서만 log 를 쓰게합니다.
아래에서 ConsoleAppender 의 구현을 살펴보겠습니다.
public class ConsoleAppender<E> extends OutputStreamAppender<E> {
// ...
private Lock lock;
public ConsoleAppender() {
// ...
this.lock = new ReentrantLock();
}
// ...
private void writeWithJansi(E event) {
if (!this.lock.tryLock()) {
return;
}
try {
byte[] byteArray = this.encoder.encode(event);
int length = byteArray.length;
for (int i = 0; i < this.codeLength; i++) {
JansiOutput.write(this.codes[i]);
}
JansiOutput.write(byteArray, 0, length);
JansiOutput.write(0);
if (this.immediateFlush) {
JansiOutput.flush();
}
} catch (IOException ioe) {
this.inError = true;
} finally {
this.lock.unlock();
}
}
}
멀미가 날 것 같은 기분입니다만…
중요한 것은 내부적으로 reentrantlock 을 사용하여 동기화 하여
기존 println 에서 synchronized block 보다 동기화의 cost 를 낮추고,
OutputStreamAppender 를 extends 한다는 점입니다.
public void start() {
// ...
started = false;
OutputStream os = openOutputStream();
try {
setOutputStream(os);
} catch (IOException e) {
// ...
}
if (encoder == null) {
addError("No encoder set for the appender named \\"" + name + "\\".");
return;
}
encoderInit();
writeHeader();
synchronized (this) {
this.outputStreamReady = true;
this.notifyAll();
}
super.start();
started = true;
if (immediateFlush) {
encoderInit();
writeFooter();
}
// ...
}
위의 코드는 OutputStreamAppender 의 start 메서드의 일부이고,
outputstream 을 할당한 후 encoder 라는 인스턴스를 가지고 thread 시작하게 됩니다.
실제로 스레드가 시작되는 메서드가 encoderInit() 내부에 있습니다.
protected void encoderInit() {
if (encoder == null) {
addWarn("Encoder has not been set. Cannot invoke its init method.");
return;
}
if (encoderStarted) {
return;
}
// ...
encoder.start(outputStream, this);
// ...
}
encoder.start() 가 스레드의 시작부입니다.
따라서 logger 는 println 에서의 잦은 I/O 작업을 제어하거나
한 스레드에서 lock 을 독점하는 것을 막아 시스템의 전체 응답 속도를 낮추는 것을 피하기 위해
하나의 스레드에 I/O 작업을 독점적으로 하게 하여 성능적 저하를 회피할 수 있게 됩니다.
끝!
'Java, Spring' 카테고리의 다른 글
Java 참조 유형 과 GC (strong, soft, weak, phantom reference) (0) | 2023.03.26 |
---|---|
Java8 Parallel Stream 과 성능, 동시성 문제에 대해 (0) | 2023.03.25 |
Spring 예제로 보는 보안을 위한 HMAC (2) | 2023.03.11 |
Java 코드 예시로 보는 CSRF 설명과 예방 (4) | 2023.03.09 |
JNI, Java의 Native Code 사용 설명 및 예제 (0) | 2023.03.05 |