콘솔 출력

자바에서는 지정한 형식에 맞춰서 문자열을 출력하는 System.out.printf() 메서드를 지원합니다. 이 메서드는 내부적으로 System.out.format() 메서드를 호출하며, 아래와 같이 사용할 수 있습니다.

System.out.printf(문자열);
System.out.printf(형식, 인수1, 인수2, ...);

인수(argument) vs 매개변수(parameter)

인수는 메서드를 호출할 때 전달되는 값, 변수, 참조를 말하며 매개변수는 함수 선언에서 정의할 수 있는 변수를 말합니다.

// 여기서 a와 b를 매개변수(parameter)라고 합니다.
static void func(int a, int b) {
	// ...
}

int num = 10;
// 여기서 num과 5를 인수(argument)라고 합니다.
func(num, 5);

아래 코드를 컴파일 후 한 번 실행해보세요.

public class JavaTutorial23 {
	public static void main(String[] args) {
		int a = 5;
		int b = 10;
		
		System.out.printf("%d + %d = %d\n", a, b, a + b);
		System.out.printf("%d - %d = %d\n", a, b, a - b);
		System.out.printf("%d * %d = %d\n", a, b, a * b);
		System.out.printf("%d / %d = %d\n", a, b, a / b);
	}
}

결과를 살펴보면 %d에 값이 대응되어 출력되는 것을 볼 수 있습니다. 문자열 뒤에 따라오는 인수가 순서대로 %d에 대응됩니다.

포맷 지정자(format specifiers)

여기서 %d는 포맷 지정자라고 하며, 포맷 지정자는 %로 시작하며 여러 종류가 있습니다. 포맷 지정자는 아래와 같이 작성합니다. 여기서 대괄호([])안에 있는 것은 필요에 따라서 적을 수도 있고 적지 않을 수도 있습니다.

%[플래그][너비][.정밀도]변환문자

플래그(flags)

플래그는 정렬이나 기호를 추가하는 등 포맷을 수정하는 문자들을 말합니다.

너비(width)

인수를 출력하기 위해 너비를 지정합니다. 이 너비는 출력할 최소 문자 수를 말합니다. 출력 길이가 너비 값보다 작으면 공백을 추가합니다.

정밀도(precision)

정밀도는 부동 소수점 타입과 함께 사용되며, 정밀도를 사용해서 출력될 소수점 수를 지정할 수 있습니다.

변환 문자(conversion character)

변환 문자는 제공한 인수에 대한 타입(문자열, 16진수, 부동 소수점 등)을 지정하는 문자이며 필수적으로 적어야 합니다. 변환 문자를 대문자로 입력하면 출력도 대문자로 출력됩니다.

예제 살펴보기

public class PrintFormattingExamples {
	public static void main(String[] args) {
		System.out.printf("문자: %c %c\n", 'a', 65);
		System.out.printf("10진법: %d %d\n", 2022, 100000000L);
		
		System.out.printf("앞의 빈 칸이 공백으로 채워짐: %10d\n", 56442);
		System.out.printf("앞의 빈 칸이 0으로 채워짐: %010d\n", 56442);
		
		System.out.printf("동일한 수 다양한 진수로 출력: %d %x %o %#x %#o\n", 50, 50, 50, 50, 50);
		System.out.printf("문자열: %s\n", "안녕하세요");
	}
}

이스케이프 시퀀스(escape sequence)

그리고 문자열 내에서 컴파일러에게 특별한 의미로 해석되는 문자들을 가리켜 이스케이프 시퀀스라고 합니다. 이스케이프는 모두 역슬래시 기호(\)로 시작합니다. 아래는 자바에서 사용되는 이스케이프 시퀀스를 정리한 표입니다.

아래 코드를 컴파일 후 실행해보세요. 몇몇 컴파일러는 다르거나 이상한 결과를 출력할 수 있습니다.

public class EscapeSequenceExamples {
	public static void main(String[] args) {
		// \n는 개행 문자를 삽입, 즉 다음 줄로 넘어가는 기능을 합니다.
		System.out.println("이스케이프 시퀀스를\n 이용한 예제입니다.");
		
		// \t이 입력된 자리에 탭을 삽입합니다.
		System.out.println("이스케이프 시퀀스를\t이용한 예제입니다.");
		
		// \r는 커서가 현재 줄의 처음으로 이동합니다.
		System.out.println("이스케이프 시퀀스를\r이용한 예제입니다.");
		
		// \f는 폼 피드를 삽입하는 기능을 합니다.
		System.out.println("이스케이프 시퀀스를\f이용한 예제입니다.");
		
		// \b는 문자를 하나 삭제하거나 커서를 한 칸 뒤로 이동시키는 기능을 합니다.
		System.out.println("이스케이프 시퀀스를\b이용한 예제입니다.");
		
		// \', \", \\는 각각 작은따옴표, 큰따옴표, 역슬래시를 출력하는 기능을 합니다.
		System.out.println("\"이스케이프\" \'시퀀스를\' \\이용한 예제입니다.\\");
	}
}

콘솔 입력

이제 출력에 대해 간단히 살펴봤으니, 이어서 입력에 대해서도 살펴보도록 하겠습니다.

java.util.Scanner

사용자에게 double, int 등 다양한 값을 입력받을 수 있는 기능을 자바에서 지원하고 있는데 java.util 패키지에 있는 Scanner 클래스를 import 해야 사용할 수 있습니다. Scanner를 아래와 같은 식으로 사용해서 입력을 받을 수 있습니다.

import java.util.Scanner;

public class ConsoleInputExamples {
	public static void main(String[] args) {
    	// System.in이나 스트림(stream)에 관한 부분은 파일 입출력 편에서 설명할 것임
		Scanner sc = new Scanner(System.in);
		int data = sc.nextInt();
		
		System.out.println("사용자가 입력한 값: " + data);
	}
}

6행처럼 타입 T의 값을 입력받기 위해서는 nextT() 메서드를 호출합니다. 예를 들어서, short형의 값을 받으려면 nextShort() 메서드를, double형이라면 nextDouble() 메서드를 호출합니다. 하지만 String형은 nextString()이 아니라 nextLine() 메서드를 호출해야 합니다.

텍스트 블록(Text Blocks)

텍스트 블록은 자바 13에서 프리뷰 기능으로 추가되었다가 자바 15부터 표준 기능으로 추가되었습니다. 텍스트 블록을 사용하면 이스케이프 시퀀스를 거의 사용하지 않고 여러 줄의 문자열을 손쉽게 작성할 수 있습니다.

기존의 문제점

자바에서 자바 외의 언어(JSON, HTML, SQL 등)의 코드를 문자열에 담을 때는 아래와 같이 큰따옴표(")로 각각 감싸고 문자열 연결 연산자(+)로 연결해야 합니다. 또 문자열 리터럴에 큰따옴표를 넣으려면 백슬래시(\)를 사용하여 이스케이프 처리(\")를 해줘야 합니다. 그리고 개행이 되지 않으므로 필요하다면 마지막에 개행 문자(\n)도 삽입해야 합니다. 내용이 길어지면 길어질수록 읽기가 힘들어지며 유지보수하기도 어려웠습니다.

String html = "<html>\n" +  
        "<body>\n" +  
        "   <h1>안녕하세요.</h1>\n" +  
        "   <p>이것은 문단입니다.</p>\n" +  
        "</body>\n" +  
        "</html>\n";  
  
String sql = "SELECT * FROM table1 t1\n" +  
        "JOIN table2 t2 ON t1.id = t2.id\n" +  
        "JOIN table3 t3 ON t1.id = t3.id\n" +  
        "ORDER BY t1.salary DESC\n" +  
        "LIMIT 10;\n";  
  
String json = "{\n" +  
        "   \"name\":\"김철수\",\n" +  
        "   \"age\":15,\n" +  
        "   \"address\":\"서울시\"\n" +  
        "}\n";

텍스트 블록

이를 텍스트 블록을 사용하면 아래와 같이 간편하게 작성할 수 있습니다. 텍스트 블록은 보는 바와 같이 세 개의 큰따옴표(""")로 시작하고 끝나야 합니다. 그리고 여는 구분자(delimiter)인 """에서 개행 후 내용을 작성하면 됩니다. 또, 닫는 구분자인 """에서 첫 번째 큰따옴표 앞에 있는 문자가 마지막 문자가 됩니다. 덧붙여서 들여쓰기는 문자열에 포함되지 않습니다.

String html = """
		<html>
		<body>
		   <h1>안녕하세요.</h1>
		   <p>이것은 문단입니다.</p>
		</body>
		</html>
		""";

String sql = """
		SELECT * FROM table1 t1
		JOIN table2 t2 ON t1.id = t2.id
		JOIN table3 t3 ON t1.id = t3.id
		ORDER BY t1.salary DESC
		LIMIT 10;
		""";

String json = """
		{
			"name":"김철수",
			"age":15,
			"address":"서울시"
		}
		""";

여기에서 컴파일러는 줄 뒤에 따라붙는 후행 공백이나 들여쓰기로 사용된 공통 공백 문자를 제거합니다. 이는 매우 높은 확률로 이런 공백이 문자열에 포함되는 것을 개발자는 원하지 않기 때문입니다. 아래의 예에서 점(.)이 이러한 공백을 시각화한 것이라고 생각해봅시다.

String json = """
........{..
........	"name":"김철수",..
........	"age":15,...
........	"address":"서울시"...
........}..
........""";

이를 출력해보면 앞에 있던 공통 공백 문자 8자가 각 행에서 제거되며, 후행 공백도 마찬가지로 제거된 상태로 출력될 것입니다.

{
	"name":"김철수",
	"age":15,
	"address":"서울시"
}

주의할 점은 텍스트 블록의 들여쓰기에 탭 문자와 공백 문자를 혼용해서 사용하면 우리가 기대하는 대로 공백이 제거되지 않으므로 탭 문자와 공백 문자 둘 중 하나만 일관되게 사용해야 합니다. 보통 대부분의 IDE(예: Eclipse, IntelliJ 등)는 자동 들여쓰기를 지원하고 탭(Tab)을 누르면 탭 문자 대신 4개(기본값)의 공백 문자를 삽입하므로 실수로 탭 문자와 공백 문자를 혼용해서 사용할 일이 별로 없으리라 생각합니다.

개행 문자

개행을 하고 싶다면 텍스트 블록 내에서 개행 문자(\n)를 추가할 필요 없이 엔터를 쳐서 다음 줄로 넘어가면 됩니다. 텍스트 블록 내에서 \n나 \"를 쓸 수는 있지만 그렇게 필요하지는 않으며 권장되지 않습니다. 이해를 돕기 위해서 아래와 같은 텍스트 블록이 있다고 해봅시다.

"""
줄 1
줄 2
줄 3
"""

이는 문자열 리터럴 "줄 1\n줄 2\n줄 3\n"과 같습니다. 따라서 마지막 개행이 필요하지 않다면 아래와 같이 써야 합니다.

"""
줄 1
줄 2
줄 3"""

세 개 이상의 큰따옴표를 연속해서 작성하기

그리고 텍스트 블록 내에 큰따옴표(")를 연속으로 3번 작성해야 하는 경우에는 구분자와 겹치므로 세 개의 큰따옴표 중 하나를 역슬래시로 이스케이프 처리해야 합니다.

String tripleQuotes = """  
      \"""안녕하세요.\"""  
      """;

개행 문자 삽입 막기

만약에 텍스트 블록 내에서 어떤 줄은 개행하길 원하지 않는 경우 그 줄의 마지막에 이스케이프 시퀀스 \를 입력하여 개행 문자 삽입을 막을 수 있습니다. 물론 이 이스케이프 시퀀스는 텍스트 블록에만 사용할 수 있습니다.

String literal = """  
      이 줄의 개행을 원하지 않는다면 줄 마지막에 이스케이프 시퀀스를 추가해보세요. \  
      개행이 필요하다면 입력하지 않을 수도 있습니다.  
      다시 한번 이스케이프 시퀀스를 사용해 보겠습니다. \  
      결과를 확인하면 첫째 줄과 세번째 줄은 개행 문자가 삽입되지 않은 것을 알 수 있습니다.  
      """;  
System.out.println(literal);

후행 공백 삽입하기

이번에는 후행 공백을 삽입해봅시다. 아래의 코드를 마우스로 드래그해서 잘보면 뒤에 공백이 있는 걸 볼 수 있습니다. 하지만 막상 출력해보면 후행 공백이 제거된 채로 출력됩니다.

String literal = """
	  10   
	  100  
	  1000 
	  10000
	  """;
System.out.println(literal);

그러면 어떻게 후행 공백을 살릴 수 있을까요? 바로 아래와 같이 새로운 이스케이프 시퀀스인 \s를 사용하면 됩니다. \s는 단순하게 하나의 공백 문자로 바뀌게 되는데, 후행 공백이 모두 제거된 후에 이스케이프 시퀀스가 번역되므로 \s는 후행 공백 제거를 막는 울타리 역할을 할 수 있습니다. 따라서 아래를 출력하면 각 행이 공백을 포함하여 정확히 6자인 것을 확인할 수 있습니다.

String literal = """
	  10   \s
	  100  \s
	  1000 \s
	  10000\s
	  """;
System.out.println(literal);

다양한 곳에서 텍스트 블록 사용하기

문자열 리터럴이 들어갈 수 있는 곳이면 어디든지 텍스트 블록도 들어갈 수 있습니다.

String literal = """
		이렇게 혼용해서""" + " 사용할 수 " + """
		있다.""";
System.out.println(literal);

String code = """
		public $type sum($type n1, $type n2) {
			return n1 + n2;
		}
		""".replace("$type", "int");
System.out.println(code);

code = String.format("""
		public %s sum(%s n1, %s n2) {
			return n1 + n2;
		}
		""", "int", "int", "int");
System.out.println(code);

code = """
		public %s sum(%s n1, %s n2) {
			return n1 + n2;
	   }
	   """.formatted("int", "int", "int");
System.out.println(code);

잘못된 예시 살펴보기

아래의 예시를 복사해서 편집기에 붙여 넣으면 a, b, c, d에서 모두 에러가 발생하는 것을 볼 수 있습니다. 원인을 차례대로 살펴보도록 하겠습니다.

// 에러: 텍스트 블록의 시작이 잘못되었습니다. 여는 따옴표(""") 뒤에 개행이 빠졌습니다.
String a = """""";
// 에러: 텍스트 블록의 시작이 잘못되었습니다. 여는 따옴표(""") 뒤에 개행이 빠졌습니다.
String b = """ """;
// 텍스트 블록이 닫는 구분자(""")로 닫히지 않았습니다. 따라서 닫아주지 않으면 소스 파일의 끝까지 텍스트 블록이 이어질 것입니다.
String c = """
           ";
// 에러: 알 수 없는 이스케이프 문자. '\ '라는 이스케이프 시퀀스는 존재하지 않습니다.
String d = """
           abc \ def
           """;

그리고 당연하게도 리터럴이 한 줄이거나 공백을 나타내는 경우에는 텍스트 블록은 적합하지 않으며 기존의 문자열 리터럴을 사용해야 합니다.

String blank = """  
   """;
String blank = "";

String oneLine = """  
   이것은 적합하지 않습니다.""";
String oneLine = "이렇게 써주세요.";

참고