코테 문제를 풀던 중 시간 초과 에러가 발생하였는데,
아무리 찾아봐도 어디서 많은 시간이 소요됐는 지 알 수 없었습니다.
그러던 와중 결과를 출력하는 메서드인 System.out.println()
대신 StringBuilder
객체를 사용하는 것이 더 효율적이라는 정보를 발견했습니다. 그래서 두 방법의 차이점을 한 번 알아보기로 했습니다.🤔
제가 풀던 문제는 콘솔에 출력을 반복적으로 요구하는 문제였습니다.
기존에는 System.out.println()
메서드를 사용했었는데, 시간 초과가 발생하였고, StringBuilder
로 문자열들을 조립해 한 번에 출력하니 제한 시간 내에 통과가 되는 것을 확인할 수 있었습니다.
알고 보니, 이 두 방법은 I/O 작업과 문자열 처리 방식에서 속도 차이가 발생하는 것이었습니다.
먼저 System.out.print()
은 기본적으로 콘솔에 출력을 하기 위한 메서드입니다. 사실 콘솔에 데이터를 출력하는 것은 상대적으로 느린 작업입니다. 이는 디스크 I/O와 유사하게, CPU가 아닌 외부 장치 즉, 콘솔과의 통신이 필요하기 때문입니다.
또한, 이 메서드는 전달된 객체를 문자열로 변환하기 위해 toSting()
메서드를 호출하게 되며, 이로 인해 추가적인 메모리 할당과 객체 생성이 발생합니다. 결국엔 추가적인 오버헤드를 발생시킬 수 있는 것이죠.
여기서 오버헤드(Overhead)란, 특정 작업이나 프로세스가 수행될 때 발생하는 추가적인 비용이나 자원을 의미합니다.
이는 주로 성능 저하를 초래할 수 있습니다.
이제 StringBuilder
에 대해서 알아보겠습니다.
StringBuilder
는 문자열을 출력하기 위한 클래스이기 보다는 문자열을 효율적으로 조작하기 위한 클래스입니다. 목적이 다르긴 하지만, 문자열을 출력해야 하는 상황에서 이를 활용하면 다양한 장점이 있습니다.
StringBuilder
는 내부적으로 가변 길이의 배열을 사용하여 문자열을 저장하는데, 문자열을 추가하거나 수정할 때마다 새로운 문자열 객체를 생성하는 것이 아니라, 기존의 배열을 수정하기 때문에 성능적인 면에서 효율적인 모습을 보여줍니다.
내용을 정리하자면 아래와 같습니다.
System.out.print()
- 콘솔에 출력하는 기능을 제공하는 메서드
- 호출할 때마다 문자열을 매번 새로 생성하고 콘솔에 출력
- 이 과정에서 I/O 작업들이 발생하여 성능 부담 발생
StringBuilder
- 문자열을 합치거나 변경할 때 사용하는 클래스
- 가변 길이의 배열 사용
- 문자열을 반복적으로 추가하거나 수정할 때 효율적
간단한 문제를 예시로 들어 성능 차이를 비교해 보겠습니다.
이 문제는 콘솔에 출력을 최대 1,000,000번 해야 하는 문제입니다.
import java.util.*;
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int i = 0; i < n; i++) {
int order = sc.nextInt();
switch(order) {
case 1:
int x = sc.nextInt();
stack.push(x);
break;
case 2:
if (!stack.isEmpty()) {
System.out.println(stack.pop());
}
else {
System.out.println(-1);
}
break;
case 3:
System.out.println(stack.size());
break;
case 4:
if (stack.isEmpty()) {
System.out.println(1);
}
else {
System.out.println(0);
}
break;
case 5:
if (!stack.isEmpty()) {
System.out.println(stack.peek());
}
else {
System.out.println(-1);
}
break;
}
}
sc.close();
}
}
System.out.print()
메서드를 사용하였더니, 역시나 시간 초과가 발생하는 모습입니다.
그렇다면 이제 StringBuilder
를 사용해 문자열을 조합해 한 번에 출력하는 방식을 사용해 보겠습니다.
import java.util.*;
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
Scanner sc = new Scanner(System.in);
StringBuilder output = new StringBuilder();
int n = sc.nextInt();
for (int i = 0; i < n; i++) {
int order = sc.nextInt();
switch(order) {
case 1:
int x = sc.nextInt();
stack.push(x);
break;
case 2:
if (!stack.isEmpty()) {
output.append(stack.pop() + "\n");
}
else {
output.append(-1 + "\n");
}
break;
case 3:
output.append(stack.size() + "\n");
break;
case 4:
if (stack.isEmpty()) {
output.append(1 + "\n");
}
else {
output.append(0 + "\n");
}
break;
case 5:
if (!stack.isEmpty()) {
output.append(stack.peek() + "\n");
}
else {
output.append(-1 + "\n");
}
break;
}
}
System.out.println(output.toString());
sc.close();
}
}
StringBuilder
를 사용하니 시간 초과 문제가 해결된 것을 확인할 수 있습니다.
결론적으로, 콘솔에 출력을 반복적으로 해야 하는 상황이라면, System.out.print()
를 반복적으로 사용하는 것보다 StringBuilder
를 사용하여 문자열을 생성하고, 마지막에 toString()
메서드를 호출하여 문자열을 한 번에 출력하는 것이 더 효율적입니다.