처음 만나는 Java 오류
컴파일 오류, 클래스를 찾지 못하는 오류, 실행 중 예외를 일부러 일으켜 보고, 메시지에서 실패한 단계와 고칠 곳을 찾는 법을 익힙니다.
첫 프로그램에서 Hello를 작성하고 실행하는 데 성공했지만, 실제로 프로그램을 만들다 보면 성공보다 오류를 훨씬 자주 만납니다. 영어 메시지가 몇 줄씩 쏟아지면 처음에는 무엇부터 봐야 할지 막막합니다. 하지만 오류 메시지에는 생각보다 많은 정보가 들어 있습니다. 무엇보다 프로그램이 어느 단계까지 진행했고 어디에서 멈췄는지를 알려 줍니다.
앞 글에서 본 것처럼 Java 프로그램은 세 단계를 거쳐 실행됩니다. 먼저 javac가 소스를 컴파일해 .class 파일을 만듭니다. 다음으로 java 실행기가 그 클래스를 찾고, 마지막으로 JVM이 main부터 문장을 실행합니다. 오류도 이 가운데 한 단계에서 일어나므로, 어느 단계에서 멈췄는지부터 가려내면 고칠 곳이 크게 좁혀집니다. 예를 들어 컴파일에서 멈췄다면 소스 코드를 고쳐야 하지만, 클래스를 찾지 못했다면 소스는 멀쩡하고 실행 명령이나 폴더를 고쳐야 합니다.
아래 도표는 이 판단을 질문 순서대로 정리한 것입니다. 실패한 명령이 javac인지, java가 클래스를 찾았는지, 클래스에 main이 있는지, main에 들어간 뒤에 멈췄는지를 차례로 묻습니다. 마지막 갈래는 오류 없이 예전 결과가 나오는 경우입니다. 이 글에서는 갈래마다 오류를 일부러 하나씩 만들어 봅니다. 예제 파일은 모두 java-board/src에 저장하고, 명령은 java-board 폴더에서 실행합니다.
FLOWCHART · FAILURE STAGE
마지막 성공 경계로 Java 오류를 분류한다
실패한 명령과 마지막으로 확인된 성공을 차례로 묻고, 소스·class 탐색·main 계약·실행 문장 중 첫 조사 대상을 정합니다.
판정 기준 실패한 명령과 마지막 성공 경계를 함께 확인합니다.
javac가 실패했는가?
예 · COMPILE: 첫 오류의 파일·줄과 직전 괄호·세미콜론·이름을 고친 뒤 같은
javac명령을 반복합니다. 아니요: class 탐색 단계로 갑니다.java가 class를 찾지 못했는가?
예 · LOAD: class 존재, classpath root, 확장자 없는 binary class name을 확인합니다. 아니요: main 계약으로 갑니다.
class는 찾았지만 main이 없는가?
예 · ENTRY:
public static void main(String[] args)실행 진입점 계약을 확인합니다. 아니요: main 진입 뒤 실행으로 갑니다.main 진입 뒤 예외인가?
예 · RUNTIME: 마지막 성공 출력과 스택 추적의 첫 사용자 코드 줄을 봅니다.
before는 성공했고after는 도달하지 않았습니다. 아니요: 출력 신선도를 확인합니다.오류 없이 이전 출력인가?
마지막 성공 컴파일 뒤 소스만 바뀌었는지, 즉
javac를 생략해 오래된 class를 실행했는지 확인합니다.
- 질문과 명시된 예·아니요 분기
- 오류 메시지 없는 오래된 class 경로
오류 종류를 외우기보다 마지막으로 성공한 경계를 찾습니다. 그 다음 단계의 입력·계약·첫 사용자 코드 줄이 가장 작은 조사 범위입니다.
세미콜론이 빠졌을 때: 컴파일 오류
가장 흔한 실수인 세미콜론 누락부터 보겠습니다. 다음 내용을 src/BrokenSyntax.java로 저장하세요. 출력 문장 끝의 세미콜론 ;을 일부러 뺐습니다.
public class BrokenSyntax {
public static void main(String[] args) {
System.out.println("semicolon missing")
}
}평소처럼 컴파일하면 javac가 오류를 보고합니다. 아래 출력은 Windows에서 실행한 모습입니다.
javac -d out src/BrokenSyntax.javasrc\BrokenSyntax.java:3: error: ';' expected
System.out.println("semicolon missing")
^
1 errorjavac의 오류 메시지는 첫 줄에 핵심이 모여 있습니다. src\BrokenSyntax.java:3은 오류가 난 파일과 줄 번호이고, error: 뒤의 ';' expected는 '여기에 세미콜론이 와야 한다'는 뜻입니다. 둘째 줄은 그 소스 줄을 그대로 보여 주고, 셋째 줄의 ^는 컴파일러가 세미콜론을 기대한 위치인 닫는 괄호 바로 뒤를 가리킵니다. 마지막 줄의 1 error는 찾아낸 오류의 개수입니다. macOS와 Linux에서는 경로가 src/BrokenSyntax.java처럼 /로 표시될 뿐 나머지는 같습니다.
컴파일에 실패하면 javac는 .class 파일을 만들지 않습니다. BrokenSyntax는 아직 한 번도 컴파일에 성공한 적이 없으므로 out에는 BrokenSyntax.class가 없습니다. 그래서 이 상태에서 java -cp out BrokenSyntax를 실행하면 프로그램 대신 뒤에서 볼 '클래스를 찾을 수 없다'는 오류가 나옵니다. 3번째 줄 끝에 ;을 붙여 저장한 뒤 같은 javac 명령을 다시 실행하세요. 이번에는 아무 메시지 없이 끝나고, java -cp out BrokenSyntax를 실행하면 semicolon missing이 출력됩니다.
파일 이름과 클래스 이름이 다를 때
컴파일 오류는 문법 실수에서만 생기지 않습니다. 첫 프로그램에서 public class의 이름과 파일 이름이 같아야 한다고 했는데, 이 규칙을 어겨도 컴파일 오류가 납니다. 다음 내용을 src/WrongName.java에 저장하세요. 파일 이름은 WrongName인데 클래스 이름은 DifferentName입니다.
public class DifferentName {
public static void main(String[] args) {
System.out.println("not compiled");
}
}javac -d out src/WrongName.javasrc\WrongName.java:1: error: class DifferentName is public, should be declared in a file named DifferentName.java
public class DifferentName {
^
1 error메시지를 우리말로 옮기면 'DifferentName 클래스는 public이므로 DifferentName.java라는 파일에 선언해야 한다'입니다. 고치는 방법은 둘 중 하나입니다. 파일 이름을 DifferentName.java로 바꾸거나, 클래스 이름을 WrongName으로 바꾸면 됩니다. 어느 쪽이든 이 오류는 javac 단계에서 멈춘 것이므로 실행 명령은 아직 신경 쓸 필요가 없습니다.
이름과 관련해 자주 만나는 메시지가 하나 더 있습니다. 명령에 적은 파일 이름에 오타가 있거나 java-board가 아닌 다른 폴더에서 명령을 실행하면, javac는 error: file not found:로 시작하는 메시지를 출력하고 바로 끝납니다. 이 경우는 소스의 내용과 상관이 없으므로, 명령에 적은 경로와 현재 폴더부터 확인하세요.
컴파일은 됐는데 클래스를 찾지 못할 때
이번에는 javac는 성공했는데 java가 실패하는 경우입니다. 첫 프로그램에서 만든 src/Hello.java를 컴파일한 뒤, 실행 명령에서 -cp out을 빼 보세요.
javac -d out src/Hello.java
java HelloError: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: Hello첫 줄은 '메인 클래스 Hello를 찾거나 불러올 수 없다'는 뜻이고, 둘째 줄의 Caused by는 그 원인이 ClassNotFoundException, 즉 클래스를 찾지 못한 것이라고 알려 줍니다. 그런데 out/Hello.class는 분명히 만들어져 있습니다. 문제는 java가 그 파일을 어디에서 찾았는가입니다. -cp 옵션을 주지 않고 CLASSPATH 환경 변수도 설정하지 않았다면 java는 현재 폴더에서 클래스를 찾습니다. 지금 현재 폴더는 java-board이고 Hello.class는 그 아래의 out에 있으니 찾지 못한 것입니다.
클래스를 찾을 폴더를 -cp out으로 알려 주면 해결됩니다. 이번에는 Hello가 정상적으로 실행되어 앞에서 저장해 둔 문장이 출력됩니다.
java -cp out Hello이 오류는 소스 코드를 아무리 고쳐도 해결되지 않습니다. 실패한 곳이 컴파일이 아니라 클래스를 찾는 단계이기 때문입니다. ClassNotFoundException을 만나면 소스를 보기 전에 다음 네 가지를 확인하세요.
-cp out을 빠뜨리지 않았는지 봅니다.-cp src처럼 소스 폴더를 가리켜도 같은 오류가 납니다.- 클래스 이름의 대소문자가 맞는지 봅니다.
java -cp out hello는Hello를 찾지 못합니다. - 클래스 이름 뒤에
.class를 붙이지 않았는지 봅니다.java -cp out Hello.class는Hello.class라는 이름의 클래스를 찾다가 실패합니다. - 컴파일이 실제로 성공했는지 봅니다. 앞의
BrokenSyntax처럼 한 번도 컴파일에 성공하지 못한 클래스는.class파일이 없으니 찾을 수도 없습니다.
Windows나 macOS의 기본 설정처럼 파일 이름의 대소문자를 구분하지 않는 환경에서는, 대소문자를 틀렸을 때 둘째 줄이 Caused by: java.lang.NoClassDefFoundError: hello (wrong name: Hello)로 나올 수 있습니다. 파일은 찾았지만 그 안의 클래스 이름이 Hello라서 맞지 않는다는 뜻이고, 고치는 방법은 같습니다. 또 운영체제 언어가 한국어이면 JDK에 따라 java 실행기의 메시지가 오류: 기본 클래스 Hello을(를) 찾거나 로드할 수 없습니다.처럼 한국어로 나오기도 합니다. 여기서 '기본 클래스'는 main class를 옮긴 말입니다. 반면 javac의 메시지는 한국어 번역이 없어서 영어로 나옵니다.
클래스는 찾았지만 main이 없을 때
java가 클래스를 찾았다고 끝이 아닙니다. 실행기는 그 클래스에서 시작점인 main 메서드를 찾아 호출해야 합니다. 다음 클래스는 문법에 맞아서 컴파일은 되지만, main 대신 print라는 메서드만 가지고 있습니다.
public class NoMain {
public static void print() {
System.out.println("no entry point");
}
}javac -d out src/NoMain.java
java -cp out NoMainjavac는 조용히 성공하지만 java는 실패합니다. 메시지는 Error: Main method not found in class NoMain으로 시작하는데, 'NoMain 클래스에서 main 메서드를 찾지 못했다'는 뜻입니다. 한국어 메시지가 나오는 환경이라면 JDK 25의 한국어 번역이 이 메시지에 맞게 고쳐지지 않아서 오류: javafx.application.Application 클래스에서 기본 메소드가 NoMain이(가) 아닙니다.처럼 뜻이 통하지 않는 문장이 나올 수 있습니다. 이때도 원인은 main을 찾지 못한 것입니다. 이어지는 줄에는 main을 어떤 모양으로 선언하면 되는지가 나옵니다. 실행기는 이름이 정확히 main인 메서드만 시작점으로 쓰므로, print처럼 다른 이름의 메서드는 자동으로 실행되지 않습니다. Main처럼 대소문자가 하나만 달라도 다른 이름이니, 이 오류를 만나면 main의 철자부터 확인하세요.
main이 실행되다 멈출 때: 실행 중 예외
이번 오류는 컴파일도 성공하고 main도 시작된 뒤에 일어납니다. 다음 파일은 문법에 문제가 없어서 javac가 그대로 받아들입니다. 하지만 4번째 줄에서 정수 10을 0으로 나누는데, Java는 정수를 0으로 나누는 계산을 실행 중 오류로 정해 두었습니다.
public class RuntimeFailure {
public static void main(String[] args) {
System.out.println("before");
System.out.println(10 / 0);
System.out.println("after");
}
}javac -d out src/RuntimeFailure.java
java -cp out RuntimeFailure컴파일은 성공하고, java를 실행하면 다음과 같이 출력됩니다. 화면에는 println이 찍은 줄과 JVM이 찍은 오류 보고가 섞여 나옵니다. 소스의 println 세 개 가운데 몇 개가 실제로 출력됐는지 세어 보면서 읽어 보세요.
before
Exception in thread "main" java.lang.ArithmeticException: / by zero
at RuntimeFailure.main(RuntimeFailure.java:4)출력을 위에서부터 읽으면 프로그램이 어디까지 진행했는지 알 수 있습니다. 첫 줄의 before는 3번째 줄이 실행됐다는 뜻이므로, JVM이 클래스를 찾았고 main에도 들어갔다는 것을 알 수 있습니다. 둘째 줄부터가 오류 보고입니다. Java에서는 실행 중에 생긴 오류를 예외(exception)라고 부르며, java.lang.ArithmeticException이 예외의 종류이고 콜론 뒤의 / by zero는 '0으로 나눴다'는 설명입니다. 앞부분의 Exception in thread "main"은 main을 실행하던 흐름(스레드)에서 예외가 났다는 뜻입니다.
셋째 줄처럼 at으로 시작하는 줄은 예외가 일어난 위치를 알려 줍니다. RuntimeFailure.main은 클래스와 메서드의 이름이고, 괄호 안의 RuntimeFailure.java:4는 파일 이름과 줄 번호입니다. 이런 at 줄의 묶음을 스택 트레이스(stack trace)라고 하고, 글 첫머리의 그림에서는 스택 추적이라고 적었습니다. 지금은 main 안에서 바로 예외가 났으므로 한 줄뿐이지만, 표준 라이브러리의 메서드를 부르다가 그 안에서 예외가 나면 java.base/로 시작하는 at 줄이 위쪽에 여러 개 쌓입니다. 그럴 때는 위에서부터 읽어 내려가다가 처음으로 내가 작성한 파일 이름이 나오는 줄을 찾으면 됩니다.
after가 출력되지 않았다는 점도 보세요. 예외가 나면 그 문장에서 실행이 멈추므로 뒤의 문장은 실행되지 않습니다. 컴파일 오류와 달리 이번에는 out/RuntimeFailure.class가 정상적으로 만들어졌고, 실패는 실행 도중에 일어났습니다. 그러니 고칠 곳은 실행 명령이 아니라 스택 트레이스가 가리키는 4번째 줄과 그 줄에서 나누는 값입니다. 예외를 처리하는 방법은 12장에서 배웁니다.
더 깊이: javac는 왜 10 / 0을 미리 막지 않을까
10 / 0은 소스만 봐도 실패할 것이 뻔한데 javac는 이 줄을 오류로 보지 않습니다. Java 언어 명세(JLS)가 정수를 0으로 나누면 실행할 때 ArithmeticException이 발생한다고 정해 두었기 때문입니다(JLS §15.17.2). 즉 이 코드는 문법과 타입 규칙에 맞는 올바른 프로그램이고, 실행했을 때 예외를 던지는 것까지가 정해진 동작입니다.
컴파일러는 2 * 3처럼 값이 정해진 식을 미리 계산해 두기도 하는데, 이런 식을 상수 식(constant expression)이라고 합니다. 그런데 명세는 상수 식을 '예외로 끝나지 않는(does not complete abruptly)' 식으로 한정하므로(JLS §15.29), 예외를 던지는 10 / 0은 상수 식이 아닙니다. 그래서 javac는 이 나눗셈을 미리 계산하지 않고, 실행할 때 나누도록 컴파일합니다. 대신 javac -Xlint:divzero처럼 경고 옵션을 켜고 컴파일하면 warning: [divzero] division by zero라는 경고로 이 줄을 짚어 줍니다. 경고는 오류가 아니므로 .class는 그대로 만들어지고, 실행하면 똑같이 예외가 납니다.
고쳤는데도 예전 결과가 나올 때
오류 메시지가 전혀 없는데 결과가 이상한 경우도 있습니다. 소스를 고쳤는데 실행하면 고치기 전 문장이 그대로 나오는 경우입니다. 첫 프로그램에서 본 것처럼 java는 .java가 아니라 out에 있는 .class를 실행합니다. 그래서 마지막으로 컴파일에 성공한 뒤에 소스를 바꿨다면, javac를 다시 실행하기 전까지는 예전 .class가 실행됩니다.
알아차리기 더 어려운 경우도 있습니다. javac를 다시 실행하긴 했는데 컴파일이 실패한 경우입니다. 다음 파일을 src/Notice.java로 저장하고 컴파일한 뒤 실행해 version 1이 출력되는 것을 확인하세요.
public class Notice {
public static void main(String[] args) {
System.out.println("version 1");
}
}이제 문자열을 "version 2"로 고치면서 줄 끝의 세미콜론을 실수로 지웠다고 해 봅시다. 그 상태로 저장하고 두 명령을 이어서 실행합니다.
javac -d out src/Notice.java
java -cp out Noticesrc\Notice.java:3: error: ';' expected
System.out.println("version 2")
^
1 error
version 1javac가 실패했으니 out/Notice.class는 바뀌지 않았고, java는 남아 있던 예전 클래스를 실행해 version 1을 출력했습니다. 명령을 여러 개 이어서 실행하다 보면 javac의 오류를 못 보고 지나치기 쉽습니다. 실행 결과가 예상과 다르면 바로 앞의 javac가 아무 메시지 없이 끝났는지부터 확인하세요. 세미콜론을 붙여 다시 컴파일하면 version 2가 출력됩니다.
그래도 의심스러우면 앞 글에서 한 것처럼 out 폴더를 비우고 다시 컴파일하세요. 그러면 예전 .class가 남아 있을 수 없으므로, 컴파일이 실패했을 때는 예전 결과 대신 ClassNotFoundException이 나와서 문제가 바로 드러납니다.
오류 메시지를 읽는 순서
지금까지 본 오류를 바탕으로 메시지를 읽는 순서를 정리하면 다음과 같습니다.
- 방금 실행한 명령이
javac인지java인지 확인합니다.javac에서 났다면 소스 파일의 문제이고,java에서 났다면 클래스를 찾는 단계나 실행 중의 문제입니다. - 첫 번째 오류의 메시지를 읽습니다.
';' expected,ClassNotFoundException,ArithmeticException처럼 오류의 종류를 나타내는 말이 있습니다. - 메시지가 가리키는 파일과 줄 번호를 찾습니다. 실행 중 예외라면 스택 트레이스에서 내가 작성한 파일이 처음 나오는 줄을 봅니다.
- 그 줄만 보지 말고 바로 앞 줄의 괄호, 중괄호, 세미콜론도 함께 확인합니다.
- 하나를 고친 뒤 같은 명령을 다시 실행합니다.
4번째 단계가 필요한 이유는, 컴파일러가 실수한 자리가 아니라 더는 코드를 이해할 수 없게 된 자리를 보고하기 때문입니다. 예를 들어 main 줄 끝의 여는 중괄호 {를 빠뜨려 보겠습니다.
public class MissingBrace {
public static void main(String[] args)
System.out.println("hello");
}
}javac -d out src/MissingBrace.java중괄호 하나가 빠지면 컴파일러가 그 뒤의 코드까지 엉뚱하게 해석해서 오류가 여러 개 이어질 수 있습니다. 그래서 앞의 순서대로 첫 번째 오류만 봅니다. 실제로 빠뜨린 곳은 2번째 줄 끝이라는 것을 기억하고, javac가 보고하는 첫 번째 오류를 보세요.
src\MissingBrace.java:2: error: ';' expected
public static void main(String[] args)
^빠뜨린 것은 {인데 메시지는 ;을 요구합니다. 컴파일러는 main(String[] args) 뒤에 {가 없으니 이 줄을 본문 없이 끝나는 메서드 선언으로 읽었고, 그런 선언은 ;으로 끝나야 하기 때문입니다. 이처럼 메시지는 컴파일러가 그 자리에서 무엇을 기대했는지를 말해 줄 뿐, 내가 무엇을 빠뜨렸는지를 직접 알려 주지는 않습니다. 그래서 표시된 위치와 그 바로 앞을 함께 보는 것입니다.
이 파일을 컴파일하면 첫 오류 아래에 5번째 줄의 }를 가리키는 오류가 더 나옵니다. 컴파일러는 4번째 줄의 }에서 클래스가 끝났다고 이해했으므로, 5번째 줄의 }는 짝이 없는 괄호가 된 것입니다. 이 오류는 2번째 줄 끝에 {를 넣으면 함께 사라집니다. 이렇게 실수 하나가 오류 여러 개를 낳을 수 있으니, 오류가 여러 개 나와도 한꺼번에 고치려 하지 말고 첫 오류부터 고친 뒤 다시 컴파일하세요.
정리
- 오류 메시지는 프로그램이 어느 단계에서 멈췄는지 알려 줍니다.
javac가 실패하면 소스 코드를,java가 클래스를 찾지 못하면 실행 명령과 폴더를, 실행 중 예외가 나면 스택 트레이스가 가리키는 줄을 봅니다. - 컴파일에 실패하면
.class가 새로 만들어지지 않습니다. 예전.class가 남아 있으면java는 그것을 실행합니다. ClassNotFoundException은 소스가 아니라-cp, 클래스 이름, 컴파일 성공 여부를 확인할 오류입니다.- 실행 중 예외가 나면 그 문장에서 실행이 멈추고,
at줄의 파일 이름과 줄 번호가 실패한 문장을 가리킵니다. - 오류가 여러 개면 첫 오류부터 고치고, 표시된 줄과 그 앞 줄을 함께 확인합니다.
연습 문제
- 다음 증상이 어느 단계의 오류인지(컴파일, 클래스 찾기,
main찾기, 실행 중) 분류하고, 먼저 확인할 곳을 적으세요.';' expectedclass Member is public, should be declared in a file named Member.javaError: Could not find or load main class HelloError: Main method not found in class NoMainException in thread "main" java.lang.ArithmeticException: / by zero- 오류 메시지 없이, 소스를 고치기 전의 문장이 출력됨
RuntimeFailure에서System.out.println(10 / 0);을main의 첫 문장으로 옮기면 컴파일은 성공할까요? 실행 결과는 어떻게 달라질까요?- 친구가
src/Hello.java의 문자열을 고친 뒤javac -d out src/Hello.java와java -cp out Hello를 실행했더니, 고치기 전 문장이 나왔다고 합니다. 가능한 원인 두 가지와 확인 방법을 적으세요.
해설 보기
1번은 메시지를 낸 명령과 메시지의 첫 단어로 단계를 가릅니다.
';' expected: 컴파일 단계입니다. 표시된 줄과 그 앞 줄에서 빠진 세미콜론이나 괄호를 찾습니다.class Member is public, ...: 컴파일 단계입니다. 파일 이름과public class의 이름이 같은지 확인합니다.Could not find or load main class Hello: 클래스 찾기 단계입니다. 소스보다-cp out, 클래스 이름의 대소문자,.class를 붙였는지, 컴파일이 성공했는지를 먼저 확인합니다.Main method not found in class NoMain: 클래스는 찾았지만 시작점이 없습니다.main메서드가 있는지, 이름의 철자와 대소문자가 맞는지 확인합니다.ArithmeticException: / by zero: 실행 중 예외입니다.at줄이 가리키는 파일과 줄에서 나누는 값을 확인합니다.- 예전 문장이 출력됨: 오래된
.class를 실행한 경우입니다. 소스를 저장했는지,javac를 다시 실행했는지, 그javac가 오류 없이 끝났는지 확인합니다.
2번은 컴파일이 성공합니다. 나눗셈의 위치가 바뀌어도 문법과 타입에는 문제가 없기 때문입니다. 실행하면 나눗셈이 첫 문장이 되어 3번째 줄로 올라왔으므로, 아무것도 출력되기 전에 그 줄에서 실행이 멈춥니다.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at RuntimeFailure.main(RuntimeFailure.java:3)before와 after가 모두 출력되지 않고 줄 번호가 3으로 바뀐 것을 보면, 예외가 난 문장 뒤로는 아무것도 실행되지 않는다는 것을 다시 확인할 수 있습니다.
3번의 첫 번째 원인은 편집기에서 파일을 저장하지 않은 것입니다. 이때 javac는 디스크에 있는 예전 내용을 컴파일하므로 결과가 바뀌지 않습니다. 두 번째 원인은 javac가 실패한 것입니다. 고치는 과정에서 문법 오류가 생기면 .class가 갱신되지 않아 예전 클래스가 그대로 실행됩니다. 편집기에서 저장했는지 확인한 뒤 javac의 출력에 오류가 없는지 보고, 그래도 모르겠으면 out을 비우고 다시 컴파일해 봅니다.
다음 글
다음 글에서는 프로그램을 실행할 때 명령 뒤에 값을 붙여 main에 전달하는 방법을 배웁니다. 값을 전달하지 않았을 때 생기는 실행 중 예외를 이 글에서 익힌 방법으로 읽고, 그 예외가 나지 않도록 코드를 고쳐 봅니다.