System.out.println() 왜 쓰면 안돼?
시무에 들어가기 전에 나는 콘솔에 출력할 때 System.out.println()를 썼다.
가장 처음 배운게 이거 였던 것 같다.
그런데 회사에 가니까 로그를 찍는다.
왜?? 왜?????
알아보자~~!
System.out.println()
System.out.println()은 Java 에서 콘솔에 출력할 때 사용되는 메서드로 주로 디버깅 목적으로 사용한다.
내부적으로 출력 버퍼링을 사용하기 때문에, 작은 양의 데이터를 출력할 때도 버퍼를 채우고 비운다. 따라서, 일정 시간이 소요되어, 오버헤드가 발생할 수 있고, 객체를 문자열로 변환하여 출력하기 때문에 문자열 작업이 추가적으로 필요하다.
System.out은 단일 인스턴스이며, 여러 스레드가 이 인스턴스에 동시에 접근할 경우, 출력 결과가 뒤섞일 수 있어, 이를 방지하기 위해 System.out.println은 동기화되어 있어, 한 번에 하나의 스레드만 출력 작업을 수행할 수 있다. 이로 인해 다른 스레드는 System.out.println이 완료될 때까지 대기해야 한다. 이 과정에서 스레드가 차단되고, CPU 자원이 낭비될 수 있다.
그리고 System.out.println은 실제로 콘솔에 출력을 하기 위해 I/O 작업을 수행하는데, 이 작업은 블로킹 방식으로 동작하기 때문에, 출력 작업이 완료될 때까지 스레드는 또 대기 상태가 된다. I/O 작업이 오래 걸리거나, 네트워크 연결 문제등으로 지연되는 경우, 출력 작업이 완료될 때까지 스레드가 차단되어 CPU 자원이 낭비된다.
이러한 두 가지 특성 (synchronized, Blocking I/O) 때문에 System.out.println을 호출하는 스레드가 많은 경우, 성능 저하가 발생할 수 있다. 대기 중인 스레드는 CPU를 사용하지 않기 때문에 전체 시스템의 CPU 사용률이 낮아질 수 있으며, 전체적인 성능에 악영향을 미칠 수 있다.
System.out.println은 synchronized로 동기화되어 있어, 한 번에 하나의 스레드만 출력 작업을 수행할 수 있고, Blocking I/O 방식으로 인해 스레드가 대기하게 되기 때문에 멀티스레드 환경에서는 이 메서드를 사용하는 것이 성능에 좋지 않은 영향을 미칠 수 있다.
이 때문에 현업에서는 System.out.println보다는 로깅 프레임워크를 사용하는 것이 좋다.
해결 방안
log4j, SLF4J, java.util.logging 등과 같은 로깅 프레임워크를 사용하면 로그 레벨을 조절하고, 파일에 로그를 저장하거나 비동기적으로 로그를 처리할 수 있다.
이는 콘솔 출력보다 효율적인 로깅을 제공한다.
로깅 시스템을 통해 출력을 관리하고, 디버깅과 모니터링이 가능하다.
문자열을 효율적으로 처리하기 위해 StringBuilder, String.format을 사용할 수 있다. 반복적인 문자열의 결합이 필요한 경우 성능 향상에 도움이 되며, 가변 크기의 문자열을 처리할 수 있고, 문자열을 효율적으로 조작할 수 있다.
출력 버퍼링을 활용하여 여러 작은 출력을 모아서 한 번에 출력하여 I/O의 작업 횟수를 줄이고, 오버헤드를 감소시킬 수 있다.
출력 빈도를 최소화하여 성능을 개선하는 방식
반복적인 출력을 피하기 위해 데이터를 한 번에 모아서 출력하거나, 조건을 걸어서 출력이 필요한 경우에만 출력할 수도 있다.
또는 로그 레벨을 설정하여 특정 레벨 이상의 로그만 출력하도록 할 수 있다.
출력 버퍼링을 사용할 경우, 적절한 버퍼의 크기를 선택해야 한다. 작은 크기는 자주 비워져야하기 때문에 입출력이 자주 발생할 수 있음며, 반대로 너무 큰 버퍼는 메모리를 많이 차지한다.
또한 멀티 스레드 환경에서 여러 스레드가 동시에 출력 버퍼에 접근할 수 있기 때문에 동기화 작업이 필요하다. 동기화를 통해 스레드간의 충돌을 방지한다.
그리고 출력 중에 발생할 수 있는 예외를 처리하는 방법이 필요하다.
멀티 스레드 환경에서 동기화를 통한 출력 버퍼 접근 방식
접근방식에는 synchronized 메서드나 블록을 사용하여 한 번에 하나의 스레드만 접근할 수 있도록 한다.
다른 방법은 ReentrantLock를 사용하여 락을 획득하고 해제하는 방법이 있다.
충돌을 방지하려면?
동기화를 구현하여 한 스레드가 공유 자원을 사용중일 때 다른 스레드에서 접근할 수 없도록 한다.
출력 중에 발생한 예외를 처리하려면
try-catch블록을 이용하여 예외를 처리할 수 있습니다. 출력하는 동안 발생할 수있는 예외를 해당 블록으로 감싸서 처리한다.
다른 방법은 메서드 내에서 예외를 직접 처리하지 않고 호출자에게 예외를 던지도록 할 수 있다. 예외 발생시 로그를 남김으로써 문제를 쉽게 발견할 수 있으며 예외 발생 시 이전 상태로 돌리는 방법을 고려해야 한다.
catch 블록에서 예외를 로그로 기록하고, 상태를 복구하기 위해 어떻게 로깅을 해야하는지
catch 블록에 예외 로그를 기록할 때 로그 레벨과 예외 메시지를 포함하여 기록한다.
상태를 복구하기 위해서는 상태 복구 메서드를 직접 생성하여 호출할 수 있다.
예로 데이터베이스 트랜잭션에서 예외 발생 시 이전 상태로 롤백하는 작업을 수행한다.
Blocking I/O 와 synchronized
Blocking I/O의 성능 저하
- 차단 상태: Blocking I/O 작업은 해당 작업이 완료될 때까지 스레드를 차단하므로, 스레드가 아무 작업도 수행하지 않고 대기하게 된다. 이로 인해 CPU 자원을 낭비하게 된다.
- I/O 대기: I/O 작업은 일반적으로 네트워크 요청, 파일 읽기/쓰기와 같은 외부 작업으로 인해 지연이 발생할 수 있으며, 이로 인해 스레드가 장기간 대기하게 된다.
synchronized는 항상 좋을까?
- 상호 배제: synchronized는 특정 블록의 코드에 대해 한 번에 하나의 스레드만 접근할 수 있도록 보장한다. 이로 인해 하나의 스레드가 블록에 진입할 때 다른 스레드는 대기해야 한다.
- 컨텍스트 스위칭: 스레드가 대기하는 동안 컨텍스트 스위칭이 발생하게 되는데, 이 과정에서 CPU 자원이 소모된다.
- 경합 상태: 여러 스레드가 synchronized 블록에 접근하려고 시도할 때 경합이 발생하여 추가적인 대기 시간이 생긴다.
synchronized와 Blocking I/O의 결합
이 두 가지가 결합되면 성능 저하가 심화된다.
- 스레드 대기: synchronized 블록에 진입하려는 여러 스레드가 대기하는 동안, 만약 이 블록 내에서 Blocking I/O 작업이 발생하면, 스레드는 해당 작업이 완료될 때까지 차단된다.
- 대기 스레드 증가: Blocking I/O가 발생하는 동안
더 많은 스레드가 synchronized 블록에 접근하기 위해 대기하게 되므로, 대기 스레드 수가 증가하게 된다.
이로 인해 CPU의 맥락 전환 비용이 늘어난다.
컨텍스트 스위칭은 CPU가 스레드나 프로세스를 전환할 때 발생하는 필수적인 작업이지만, 이 과정에서 CPU작업이 소모된며, 이는 성능 저하로 이어질 수 있다.
특히 블로킹 작업이 많아지면 CPU는 컨텍스트 스위칭을 자주 발생시켜야 하기 때문에 오버헤드가 커진다.
시뮬레이션: 스레드 동작 및 CPU 사용률
상황 설정:
- 스레드 수: 5개
- synchronized 블록: 데이터 처리 및 Blocking I/O가 포함된 작업
- Blocking I/O 대기 시간: 2초
- synchronized 블록의 처리 시간: 1초
시뮬레이션 동작:
- 스레드 1이 synchronized 블록에 진입하여 데이터 처리 후, Blocking I/O 호출. (총 3초 소요: 1초 처리 + 2초 대기)
- CPU 사용률: 1초 동안 100%, 이후 2초 동안 0%
- 스레드 2~5는 스레드 1이 Blocking I/O로 차단되는 동안 synchronized 블록에 진입하려고 시도하지만, 대기 상태이다.
- CPU 사용률: 0% (스레드 1을 대기하는 동안)
- 스레드 1이 Blocking I/O 작업을 마치고 블록을 나가면, 스레드 2가 진입한다. 스레드 2도 마찬가지로 데이터를 처리하고 Blocking I/O를 호출한다.
- CPU 사용률: 스레드 2가 처리하는 동안 1초 100%, 이후 2초 동안 0%
- 스레드 2가 대기하는 동안 스레드 3~5도 차례로 대기하며, 같은 과정을 반복한다.
각 스레드가 Blocking I/O를 호출하는 동안 스레드가 차단되고 CPU 사용률이 0%로 떨어진다. 전체 처리 시간은 스레드 수에 비례하여 증가하고, 전체적인 CPU 자원 활용이 비효율이다.
Blocking I/O와 synchronized가 함께 사용될 경우, 성능 저하가 심화되고 CPU 자원이 낭비될 수 있다.
특히 다수의 스레드가 Blocking I/O를 포함한 synchronized 블록에 접근할 때 크게 나타나며, 이런 상황을 피하기 위해 non-blocking I/O나 더 효율적인 동기화 메커니즘을 고려할 수 있다.