예외처리
프로그램 오류
- 프로그램 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우에 이런 결과를 초래하는 원인
- 발생 시점에 따라 '컴파일 에러(compile-time error)'와 '런타임 에러(runtime error)'로 나눔
- 컴파일 에러는 컴파일 할 때 발생하는 에러
- 런타임 에러는 프로그램 실행시 발생하는 에러
- '논리적 에러(logical error)'는 실행은 되지만 의도와 다르게 동작하는 것
런타임 에러
- 런타임 에러로 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)'으로 나눔
- 에러는 메모리 부족(OutOfMemoryErro)이나 스택오버 플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류. 프로그램이 비정상적으로 종료됨
- 예외는 발생하더라도 비교적 덜 심각한 오류. 코드를 통해 비정상적인 종료를 막을 수 있음
컴파일러 기능
- 구문체크, 문법체크
- 번역
- 최적화
예외처리의 정의와 목적
- 정의 : 프로그램 실행시 발생할 수 있는 예외의 대해서 대비한 코드를 작성 하는 것
- 목적 : 프로그램의 비정상 종료를 막고 정상적인 실행상태 유지 하는 것
예외클래스 계층도
- 모든 오류의 조상은 Throwable 클래스
- 모든 예외의 최고 조상은 Exception 클래스
- Exception 클래스와 그 자손들
예외 클래스는 2가지로 나눌 수 있음
① Exception클래스와 그 자손들 (RuntimeException과 자손들 제외) (≒ Exception 클래스들)
② RuntimeException와 그 자손들 (≒ RuntimeException 클래스들)
Exception 클래스들
- 사용자의 실수, 외적인 요인에 의해 발생되는 예외
- 예외처리 선택
- try-catch문 필수
- Exception 클래스들 종류
- IOException (Input/Output Exception) : 입출력 예외
- ClassNotFoundException : 클래스가 존재하지 않을 때 예외
- FileNotFoundException : 파일이 존재하지 않을 때 예외
- DataFormatException : 입력한 데이터 형식이 잘못됐을 때 예외
RuntimeException 클래스들
- 프로그래머의 실수로 발생하는 예외
- 예외처리 필수
- try-catch문 선택
- RuntimeException 클래스들 종류
- ArithmeticException : 산술계산 예외. 정수를 0으로 나누려고 하는 경우
- ClassCastException : 형변환 예외
- NullPointException : 값이 null인 참조변수의 멤버를 호출하려 했을 때 예외
- IndexOutOfBoundsException : 배열 범위 벗어났을 때 예외
예외처리하기 try ㅡ catch 문
- 'Exception클래스'는 모든 예외의 최고 조상이므로 'Exception'이 선언된 catch블럭은 모든 예외처리 가능. 그러므로 'Exception'이 선언된 catch블럭은 가장 마지막에 쓸 것
- 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예의(uncaught exception)는 JVM의 '예외처리기(UncaughtExceptionHandler)'가 예외의 원인을 화면에 출력
예제1
class prac{
public static void main(String[] args){
System.out.println(1);
try{
System.out.println(2);
System.out.println(0/0);
System.out.println(3);
} catch (ArithmeticException ae) {
if (ae instanceof ArithmeticException) {
System.out.println("true");
}
System.out.println("ArithmeticException");
} catch (Exception e) {
System.out.println("Exception");
} // end of try-catch
System.out.println(4);
}
}
/* 출력값
1
2
true
ArithmeticException
4
*/
예제2
class prac{
public static void main(String[] args){
System.out.println(1);
try{
System.out.println(args[0]);
System.out.println(2);
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("ArrayIndexOutOfBoundsException");
}
System.out.println(3);
}
}
/* 출력값
1
ArrayIndexOutOfBoundsException
3
*/
ArrayIndexOutOfBoundsException은 IndexOutOfBoundsException의 자손 클래스
printStackTrace(), getMessage(), e.toString()
- 예외 발생 원인을 알 수 있는 메소드들
- 예외가 발생했을 때 예외 클래스의 인스턴스가 생성됨
- 예외가 발생 했을 때 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 남겨 있음. printStackTrace(), getMessage() 를 통해서 이 정보를 얻을 수 있음
- catch블랙의 괄호()에 선언된 참조변수를 통해 예외클래스의 인스턴스에 접근할 수 있음. 이 참조변수는 선언된 catch 블럭 내에서만 사용 가능
■ '예외클래스참조변수명.printStackTrace()'
예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력
에러의 발생근원지를 찾아서 단계별로 에러 출력
■ '예외클래스참조변수명.toString()'
에러의 Exception 내용과 원인을 출력
■ '예외클래스참조변수명.getMessage()'
발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있음
에러의 원인을 간단하게 출력
예제1
class Calculator{
int left, right;
public void setOperands(int left, int right){
this.left = left;
this.right = right;
}
public void divide(){
try {
System.out.print("계산결과는 ");
System.out.print(this.left/this.right);
System.out.print(" 입니다.");
} catch(Exception e){
System.out.println("\ne.getMessage()\n"+e.getMessage());
System.out.println("\ne.toString()\n"+e.toString());
System.out.println("\ne.printStackTrace()");
e.printStackTrace();
}
}
}
class prac {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.setOperands(10, 0);
c1.divide();
}
}
/* 출력값
계산결과는
e.getMessage()
/ by zero
e.toString()
java.lang.ArithmeticException: / by zero
e.printStackTrace()
java.lang.ArithmeticException: / by zero
at Calculator.divide(prac.java:10)
at prac.main(prac.java:26)
*/
멀티 catch 블록
- catch 블록을 '|' 기호를 이용해서 하나의 catch 블럭으로 합칠 수 있음
- 멀티 catch 블록에 사용되는 '|'는 논리연산자가 아니라 기호임
- 중복 코드를 줄일 수 있음
- '|' 기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없음
- 예외 클래스를 '|' 기호로 연결할 때 예외 클래스들이 서로 조상과 자손 관계면 안됨
- '|' 기호로 연결된 예외 클래스의 참조변수는 예외 클래스의 공통된 멤버만 사용할 수 있음
- 멀티 catch불록에 선언된 참조변수는 상수이므로 값을 변경할 수 없음
예외 발생시키기
// 예외 발생시키기
Exception e = new Exception("에러 에러");
throw e;
// 위의 2줄을 한줄로 줄일 수 있음
throw new Exception("에러 에러");
예제1
dd
checked 예외, unchecked 예외
- checked 예외 : 컴파일러가 예외 처리 여부를 체크 (예외 처리 필수)
- Exception과 그 자손들
- try-cathc문 필수
- unchecked 예외 : 컴파일러가 예외 처리 여부 체크 안함 (예외 처리 선택)
- RuntimeException과 그 자손들
- try-cathc문 선택
- RuntimeException과 그 자손들
checked 예외
class prac {
public static void main(String[] args) {
// checked 예외
throw new Exception(); // 컴파일러가 에러 표기함
}
}
/* 출력값
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Unhandled exception type Exception
at prac.main(prac.java:3)
*/
unchecked 예외
class prac {
public static void main(String[] args) {
// unchecked 예외
throw new RuntimeException(); // 컴파일러가 에러를 안냄
}
}
/* 출력 값
Exception in thread "main" java.lang.RuntimeException
at prac.main(prac.java:3)
*/
checked 예외와 unchecked 예외 둘 다 프로그램 실행시 에러가 나지만 checked 예외는 프로그램 실행 전 컴파일러가 에러 표시를 하지만 unchecked 예외는 프로그램 실행 전 컴파일러가 에러 표시 안함
메서드에 예외 선언하기
- 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws 사용해서 메서드내에서 발생할 수 잇는 예외를 적어주면 됨
- throws를 사용한다는 것은 해당 메서드에서 예외가 나올 수 있음. 그러나 해당 메서드에 예외처리 하지 않을거니깐 해당 메소드를 호출한 곳에서 예외처리를 할거라는 의미
- 예외가 여러 개일 경우에는 쉼표(,)로 구분
- 메서드 선언부에 예외를 적을 때 적어도 상관없으나 일반적으로 unchecked 예외(RunTimeException 과 그 자손들)는 적지 않음
- 메서드에 throws를 통해 예외를 적는것은 예외를 처리하는것이 아니라 자신을 호출한 메서드에게 예외를 전달하는 것
- 예외 처리는 try-catch문으로 처리 해야함
예제1
class prac{
public static void main(String[] args) throws Exception{
method1();
}
static void method1() throws Exception{
method2();
}
static void method2() throws Exception{
throw new Exception("에러 에러"); // 예외 발생시킴
}
}
/*
Exception in thread "main" java.lang.Exception: 에러 에러
at prac.method2(prac.java:12)
at prac.method1(prac.java:8)
at prac.main(prac.java:5)
*/
method1() 호출 → method2() 호출 → method2()에서 예외 발생 → method1로 예외 전달 → method1()에서 try-catch문이 없으므로 예외처리 못하고 프로그램 비정상 종료
예제2
class prac{
public static void main(String[] args){
method1();
}
static void method1() {
try{
throw new Exception("예외");
} catch(Exception e){
System.out.println("method1 메서드에서 예외가 처리");
e.printStackTrace();
}
}
}
/*
method1 메서드에서 예외가 처리
java.lang.Exception: 예외
at prac.method1(prac.java:7)
at prac.main(prac.java:3)
*/
method1에서 예외가 발생하고 처리함
예제3
class prac{
public static void main(String[] args){
try{
method1();
} catch(Exception e){
System.out.println("main 메서드에서 예외 처리");
e.printStackTrace();
} // end of try-catch
} // end of main
static void method1() throws Exception{
throw new Exception("예외 예외");
}
}
/*
main 메서드에서 예외 처리
java.lang.Exception: 예외 예외
at prac.method1(prac.java:12)
at prac.main(prac.java:4)
*/
method1에서 예외가 발생했지만 main메서드에서 예외를 처리함
예제4
import java.io.*;
class prac{
public static void main(String[] args){
File f = createFile(args[0]);
System.out.println(f.getName() + "파일 생성 완료");
}
static File createFile(String fileName){
try{
if(fileName == null || fileName.equals(""))
throw new Exception();
} catch (Exception e) {
fileName = "제목없음.text";
} finally{
File f = new File(fileName);
createNewFile(f);
return f;
}
}
static void createNewFile(File f) {
try{
f.createNewFile();
} catch(Exception e){
e.printStackTrace();
}
}
}
- 실행 시 커맨드라인에 파일이름을 입력하지 않으면, args[0]이 유효하지 않으므로 ArrayIndexOutOfBoundsException 발생
- createFile 메서드와 createNewFile 메서드에서 예외 처리
- String[] args 배열에 인자 넣는 방법 : 이클립스 상단 카테고리 → Run → Run Configurations → Arguments → 인자 넣기 → Run 버튼 클릭
- createFile 메서드에서 fileName 인자 값이 유효하지 않을 때 특정 인자값으로 지정해서 메서드 자체에서 예외 처리
예제5
import java.io.*;
class prac{
public static void main(String[] args){
try{
File f = createFile(args[0]);
System.out.println(f.getName() + "파일 생성 완료");
} catch(Exception e) {
e.printStackTrace();
}
}
static File createFile(String fileName) throws Exception{
if(fileName == null || fileName.equals(""))
throw new Exception("파일 이름 유효 x");
File f = new File(fileName);
f.createNewFile();
return f;
}
}
- 실행 시 커맨드라인에 파일이름을 입력하지 않으면, args[0]이 유효하지 않으므로 ArrayIndexOutOfBoundsException 발생
- main 메서드에서 예외 처리
- String[] args 배열에 인자 넣는 방법 : 이클립스 상단 카테고리 → Run → Run Configurations → Arguments → 인자 넣기 → Run 버튼 클릭
- createFile 메서드에서 fileName 인자 값이 유효하지 않을 때 fileName 인자 값을 사용자에게 다시 받아오기 위해서 main 메서드에게 예외를 던짐
finally 블럭
- finally 블럭은 예외의 발생 여부에 상관없이 실행되어야할 코드를 적음
- try-catch=finally 순서로 구성되지만 finally 블럭은 선택적으로 사용 (필수사항 아님)
- 예외 발생시 : try → catch → finally
- 예외 미발생시 : try → finally
- try 또는 catch 블럭에서 return문을 만나도 finally 블럭은 수행됨
예제1
class prac{
public static void main(String[] args){
method1();
System.out.println("method1() 수행 끝. 메인 메서드 도착");
}
static void method1() {
try{
System.out.println("method1 호출");
return;
} catch(Exception e) {
e.printStackTrace();
} finally{
System.out.println("finally 블럭 싱행");
}
}
}
/* 출력값
method1 호출
finally 블럭 싱행
method1() 수행 끝. 메인 메서드 도착
*/
method1메서드에서 try블럭에 return문이 실행되어도 finally 블럭은 실행됨
자동 자원 반환 ㅡ try - with - resources문
- 입출력(I/O) 관련 클래스를 사용할 때 유용
- 입출력에 사용되는 클래스는 사용한 후에 꼭 닫아줘야함. 그래야 사용했던 자원(resources)이 반환됨
- try 괄호()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출됨
- try괄호()안에 선언된 변수는 try 블럭 내에서만 사용 가능
class prac{
public static void main(String[] args){
try (CloseableResource cr = new CloseableResource()){
cr.exceptionWork(false); // 예외 발생x
} catch(WorkException e) {
e.printStackTrace();
} catch (CloseException e) {
e.printStackTrace();
}
System.out.println();
try (CloseableResource cr = new CloseableResource()){
cr.exceptionWork(true); // 예외 발생
} catch(WorkException e) {
e.printStackTrace();
} catch (CloseException e) {
e.printStackTrace();
}
}
}
class CloseableResource implements AutoCloseable{
public void exceptionWork(boolean exception) throws WorkException{
System.out.println("exceptionWork(" + exception + ") 호출");
if (exception)
throw new WorkException("WorkException 발생");
}
public void close() throws CloseException {
System.out.println("close() 호출");
throw new CloseException("CloseException 발생");
}
}
class WorkException extends Exception{
WorkException(String msg) {super(msg);}
}
class CloseException extends Exception{
CloseException(String msg){super(msg);}
}
/*출력값
exceptionWork(false) 호출
close() 호출
CloseException: CloseException 발생
at CloseableResource.close(prac.java:33)
at prac.main(prac.java:5)
exceptionWork(true) 호출
close() 호출
WorkException: WorkException 발생
at CloseableResource.exceptionWork(prac.java:28)
at prac.main(prac.java:13)
Suppressed: CloseException: CloseException 발생
at CloseableResource.close(prac.java:33)
at prac.main(prac.java:14)
*/
- main메서드에 두개의 try-catch문 존재
- 첫 번째 것은 close()에서만 예외 발생 시킴
- 두 번째 것은 exceptionWork()와 close()에서 모두 예외 발생
- exceptionWork()예외가 출력된 후에 '억제된(Suppressed)' 의미의 머리말과 함께 close() 예외가 출력
- 두 예외가 동시에 발생할 수 없기에 실제 발생한 예외를 WorkException으로 하고 CloseException은 억제된 예외로 다룸
- 억제된 예외에 대한 정보는 실제로 발생한 예외인 WorkException에 저장
- 만약 기존의 try-catch문을 사용했다면 먼저 발생한 WorkException은 무시되고, 마지막에 발생한 CloseException에 대한 내용만 출력
Throwable의 억제된 예외 메서드 종류
void addSuppressed(Throwable exception) // 억제된 예외 추가
Throwable[] getSuppressed() // 억제된 예외(배열)를 반환
사용자정의 예외 만들기
- 보통 Exception 클래스 또는 RuntimeException 클래스로부터 상속 받아 사용자정의 예외 클래스를 만듦
- 가능한 새로운 예외 클래스를 만들기보단 기존의 예외 클래스 활용할 것
- 예전에는 주로 Exception을 상속받아서 checked예외로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아 작성하는 쪽을 권장
- checked예외 클래스로 상속 받으면 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해짐
예제1
class prac{
public static void main(String[] args){
try{
startInstall(); // 프로그램 설치 준비
copyFiles(); // 파일 복사
} catch(SpaceException se) {
System.out.println("에러 메세지: " + se.getMessage());
se.printStackTrace();
System.out.println("공간 확보 후 다시 설치할 것");
} catch(MemoryException me) {
System.out.println("에러 메시지:" + me.getMessage());
me.printStackTrace();
System.gc(); // Garbage Collecetion을 수행하여 메모리를 늘림
System.out.println("다시 설치할 것");
} finally{
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일 삭제
}
}
static void startInstall() throws SpaceException, MemoryException{
if(!enoughSpace())
throw new SpaceException("설치 공간 부족");
if(!enoughMemory())
throw new MemoryException("메모리 부족");
}
static void copyFiles() {/* 파일 복사하는 코드 */}
static void deleteTempFiles() {/* 임시파일 삭제 코드 */}
static boolean enoughSpace(){
return false;
}
static boolean enoughMemory(){
return true;
}
}
class SpaceException extends Exception{
SpaceException(String msg){
super(msg);
}
}
class MemoryException extends Exception{
MemoryException(String msg){
super(msg);
}
}
/* 출력값
에러 메세지: 설치 공간 부족
SpaceException: 설치 공간 부족
at prac.startInstall(prac.java:22)
at prac.main(prac.java:4)
공간 확보 후 다시 설치할 것
*/
예외 되던지기(exception re-throwing)
- 한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리함으로써, 양쪽에서 나눠서 처리할 수 있음
- 예외 되던지기란, 예외를 처리한 후에 인위적으로 다시 예외를 발생시키는 방법
- 먼저 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외를 처리해주고 catch문에서 필요한 작업을 행한 후 throw문을 사용해서 예외를 다시 발생시킴. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리
예제1
class prac{
public static void main(String[] args){
try {
method1();
} catch (Exception e) {
System.out.println("main 메서드에서 예외 처리");
}
}
static void method1() throws Exception{
try {
throw new Exception();
} catch(Exception e) {
System.out.println("method1 메서드에서 예외 처리");
throw e; // 다시 예외 발생
}
}
}
/* 출력값
method1 메서드에서 예외 처리
main 메서드에서 예외 처리
*/
예제2
class prac{
public static void main(String[] args){
method1();
}
static int method1() {
try {
System.out.println("method1() 호출");
return 1; // 실행중인 메서드 종료
} catch(Exception e) {
e.printStackTrace();
return 0; // catch문에도 return문 필요
} finally {
System.out.println("method1()의 finally 블럭 실행");
}
}
}
/* 출력값
method1() 호출
method1()의 finally 블럭 실행
*/
- try-catch문에서 try 블럭안에 return문이 있을경우, catch문에도 return문이 있어야함
- try 블럭안에 return문이 있어도 catch블럭에서 예외 던지기를 해서 호출한 메서드로 예외를 전달한다면, catch문에 return문 필요 없음
- finally 블럭에서 return문 사용 가능
- try블럭이나 catch 블럭의 return문 다음에 수행되고 최종적으로 finally 블럭의 return문의 값이 반환됨
연결된 예외(chained exception)
- 연결된 예외란, 어떤 예외를 다른 예외로 감싸는 것
- 한 예외가 다른 예외 유발 가능. 예외 A가 예외 B를 발생시켰다면, A는 B의 '원인 예외(cause exception)'
- 원인 예외 A를 예외 B에 포함 시킴
- ex) '예외B.initCause(예외A)'
사용 용도
- 여러 예외를 하나로 묶어서 다루기 위해
- 만약 조상으로 묶어버리면 실제로 발생한 예외가 어떤것이 알 수 없는 문제가 생김
- 예외가 원인 예외를 포함할 수 있게하면 두 예외는 상속 관계가 아니여도 됨
- catch 블럭을 너무 많이 사용하면 가독성이 떨어지기 때문에 여러 예외를 하나로 묶음
- checked 예외를 unchecked 예외로 바꾸기 위해
- 의미없는 try-catch문을 사용안해도 됨
연결 예외 메소드
Throwable initCause(Throwable cause) // 지정한 예외를 원인 예외로 등록
Throwable getCause() // 원인 예외 반환
RunTimeException(Throwable cause) // 원인 예외를 등록하는 생성자
여러 예외를 하나로 묶어서 다룰 때
try{
startInstall();
copyFiles();
} catch(SpaceException se){
InstallException ie = new InstallException("설치중 예외발생");
ie.initCause(se);
throw ie;
} catch(MemoryException me) {
InstallException ie = new InstallException("설치중 예외발생");
ie.initCause(me);
throw ie;
}
SpaceException 예외와 MemoryException 예외가 원인 예외로서 InstallException 예외에 포함됨
checked 예외를 unchecked 예외로 바꾸기 위해
static void startInstall() throw SpaceException {
if(!enoughSpace())
throw new SpaceException("설치할 공간 부족");
if(!enoughMemory())
throw new RuntimeException(new MemoryException("메모리 부족"));
}
사용자 정의 예외 만들기에서 예제1번에 MemoryException은 Exception 자손임. 그래서 반드시 예외 처리를 해야함. 근데 이 예외를 RuntimException으로 감싸버려서 unchecked 예외가 됨. 그래서 startInstall() 메서드 선언부의 throw에 MemoryException을 안적어도 됨. 참고로 여기서는 initCause() 대신 RuntimeException의 생성자를 사용
예제1
class prac{
public static void main(String[] args){
try{
install();
} catch(InstallException e){
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
static void install() throws InstallException {
try{
startInstall();
copyFiles();
} catch(SpaceException se){
InstallException ie = new InstallException("설치 중 예외발생");
ie.initCause(se);
throw ie;
}
}
static void startInstall() throws SpaceException{
if(!enoughSpace()){
throw new SpaceException("설치할 공간 부족");
}
if(!enoughMemory()) {
throw new RuntimeException(new MemoryException("메모리 부족"));
}
}
static boolean enoughSpace(){
return false;
}
static boolean enoughMemory(){
return true;
}
static void copyFiles(){/* 파일 복사 코드 */}
static void deleteTempFiles(){/* 임시 파일 삭제 코드 */}
}
class SpaceException extends Exception{
SpaceException(String msg) {super(msg);}
}
class MemoryException extends Exception {
MemoryException(String msg) {super(msg);}
}
class InstallException extends Exception{
InstallException(String msg) {super(msg);}
}
/* 출력값
InstallException: 설치 중 예외발생
at prac.install(prac.java:17)
at prac.main(prac.java:4)
Caused by: SpaceException: 설치할 공간 부족
at prac.startInstall(prac.java:25)
at prac.install(prac.java:14)
... 1 more
*/
출력값에 발생한 예외는 InstallException이 뜨지만 세부정보로 원인예외 SpaceException이 나옴
'[자바] > 자바의 정석 - 3판' 카테고리의 다른 글
Chapter 10 날짜와 시간 & 형식화 (0) | 2021.11.08 |
---|---|
Chapter 09 java.lang 패키지와 유용한 클래스 (0) | 2021.10.30 |
Chapter07 객체지향2 (0) | 2021.10.17 |
Chapter06. 객체지향 (0) | 2021.09.25 |
Chapter.05 배열 (0) | 2021.08.29 |