5. 자바 중급 활용-1
이번 강좌에서는 자바를 좀 더 제대로 사용하기위해 try~catch, throws, throw 등을 이용한 예외처리 기법을 살펴보고 사용자 정의 예외클래스를 만들고 활용해 봅니다. 또한 프로그램 개발시 많이 필요한 문자열의 동작구조를 배우고 주요 API들을 사용해 문자열을 다루는 방법을 배우게 됩니다.
이 강의를 통해 자바언어의 중급활용을 통해 기초를 다지고 보다 복잡한 자바프로그램 구조를 이해하고 개발 할 수 있습니다.
01: 예외처리
예외처리는 자바 프로그램에서 실행중 발생할 수 있는 예외적인 상황을 대비한 코드를 미리 만들어두는 것으로 문제 발생시 좀 더 안전한 구조를 제공하며 문제 해결에 필요한 정보등을 제공할 수 있습니다
일반적으로 프로그램에서 문제가 발생하는 것을 에러(error)라고 하며 보통 에러는 컴파일 과정에 발생하는 컴파일 에러와 실행중 발생하는 런타임 에러로 구분 합니다.
- 근래에는 개발도구의 지원으로 컴파일 에러는 소스코드 작성과정에서 대부분 발견됨.
- 런타임에러는 프로그램 로직상의 문제나 실행중 부적절한 데이터 혹은 객체 참조등으로 발생.
try ~ catch 블럭
자바에서 기본적인 예외처리 방법은 try ~ catch
블럭을 사용하는 것입니다. 그러면 언제 try ~ catch
를 사용해야 할까요? 다행이도 개발자가 굳이 신경을 쓰지 않더라도 특정 클래스의 메서드를 사용할때 컴파일러에 의해 try ~ catch
작성이 요구 됩니다. 물론 이클립스와 같은 개발도구는 컴파일 이전에 미리 관련해서 처리해야 한다고 알려주게 되고 기본 코드도 자동으로 생성해 줍니다.
그러면 컴파일러는 어떤 원리에서 try ~ catch
블럭을 사용해야 한다고 강제 할 수 있을까요? 이것은 좀 더 뒤에서 알아보도록 합니다.
우선 예외 상황이란 어떤 경우가 있을지 생각해 봅니다.
- 특정 파일을 로딩하려고 할 때 파일이 해당 경로에 없는 경우.
- 네트워크로 서버에 접속하는 경우 서버가 다운되었거나 네트워크 연결에 문제가 있는 경우.
- 문자열 데이터를 읽어 객체의 참조를 구하는 경우.
- 정수를 0으로 나누는 경우.
이처럼 정상적으로 프로그램이 동작할 수도 있지만 예외적인 상황이 발생하면 정상동작이 어려운 경우를 예외 상황으로 이해하면 됩니다. 이러한 예외 상황 환경에서 동작하는 클래스를 만들때 해당 기능을 수행하는 메서드에 throws 구문을 추가해 특정 예외 상황 처리를 강제하게 됩니다.
예외 처리 클래스는 java.lang.Exception
클래스를 부모로 하는 대표적인 클래스들이 있으며 사용자 정의 예외클래스를 만드려면 Exception
클래스를 상속받아 구현하면 됩니다.
예외처리 유형
자바의 예외처리는 Checked Exception 와 Unchecked Exception 이 있습니다.
Checked Exception
- Exception 클래스를 상속받아 구현된 클래스(RuntimeException 클래스 제외)
- 컴파일러에 의해 강제로 예외처리가 요구됨
Unchecked Exception
- RuntimeException 클래스를 상속받아 구현된 클래스
- 강제로 예외처리가 요구되지는 않지만 필요에 따라 예외처리가 가능
» 실습: 예외처리 기본 예제
실습개요
파일로 부터 데이터를 읽어 출력하는 프로그램을 구현하면서 기본적인 예외처리 과정을 살펴 봅니다.
앞에서 Scanner 클래스를 통해 키보드로 부터 입력을 받는 예제를 작성해 본적이 있습니다. 여기서는 파일을 이용해 입력을 받도록 다음과 같이 코드를 작성해 봅니다.
소스코드
import java.io.File;
import java.util.Scanner;
public class ExceptionTest {
public static void main(String[] args) {
File file = new File("test.txt");
Scanner scan;
scan = new Scanner(file);
while (scan.hasNext()) {
System.out.println(scan.next());
}
}
}
- test.txt 파일은 반드시 이클립스의 프로젝트 폴더 최상위에 생성해야 함(src 나 패키지 폴더 아님).
- File 클래스를 이용해 특정 파일을 읽을 수 있도록 스트림 생성.
- File 객체를 Scanner 클래스의 생성자 인자로 전달.
- 파일로 부터 데이터가 있을때 까지 읽어 출력.
소스를 작성하면 이클립스에서 다음과 에러가 있다는 표시를 만나게 되고 빨간 점을 살짝 클릭하면 try ~ catch
블럭을 추가해야 한다는 도움말을 볼 수 있습니다.
Surround with try/catch
를 선택하면 나오는 자동생성 코드를 다음과 같이 최종적으로 수정합니다.
public class ExceptionTest {
public static void main(String[] args) {
File file = new File("test.txt");
Scanner scan;
try {
scan = new Scanner(file);
while (scan.hasNext()) {
System.out.println(scan.next());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
test.txt
파일에 간단한 문장을 몇줄 입력한다음 프로그램을 실행해 정상 동작을 확인해 본 다음 파일 이름을 변경하거나 위치를 이동한 다음 다시 실행해 보도록 합니다.
분명 프로그램 코드에는 이상이 없지만 파일이 제위치에 없기 때문에 예외상황이 발생해 다음과 같은 예외 메시지를 볼 수 있습니다.
실행결과
java.io.FileNotFoundException: test.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.util.Scanner.<init>(Scanner.java:639)
at com.dinfree.java.part1.ExceptionTest.main(ExceptionTest.java:12)
중복 catch 블럭과 finally 구문
중복 catch 블럭 사용하기
기본적으로 예외는 메서드 호출시 발생하는 것이므로 사용하는 메서드들이 모두 서로 다른 예외 처리를 요구한다면 다음과 같이 코드가 작성 될 수 있습니다.
try {
method1();
}
catch (AAAEception e) {
...
}
try {
method2();
}
catch (BBBException e) {
...
}
그러나 이와 같이 작성하게 되면 코드가 매우 비효율적이므로 다음과 같이 try 블럭에 메서드들을 묶고 catch 블럭을 나열하는 방식을 주로 사용 합니다.
try {
method1();
method2();
}
catch (AAAEception e) {
...
}
catch (BBBException e) {
...
}
만일 두개의 예외에 대해 동일한 처리를 한다고 가정하면 다음과 같이 예외들을 묶을 수 있습니다.
catch (AAAEception | BBBException e) {
...
}
이경우에서 만일 두 예외가 부모 자식 관계라면 부모 클래스만 명시해 주어야 합니다.
혹은 여러 예외의 처리가 동일하다고 하면 그냥 최상위 클래스인 Exception 만 처리해도 같은 효과가 있습니다.
catch(Exception e) {
...
}
예외처리 흐름과 처리 내용
예외처리는 try 블럭
안에 있는 코드들이 차례로 실행되며 예외가 발생한 구문 이후의 코드들은 실행되지 않고 catch 블럭
에서 해당 예외처리를 따르게 됩니다. 앞의 예제에서 method1()
에서 예외가 발생했다면 method2()는 실행되지 않습니다.
보통 예외 처리 블럭에서는 예외상황을 추적하기 위해 e.printStackTrace()
메서드를 사용해 콘솔에 예외상황을 출력해 문제를 해결하기 위한 도움 메시지로 활용하거나 좀 더 체계적으로 관리하기 위해서는 앞에서 배운 로거를 통해 로그를 남기는 형태로 구현하는 것이 좋습니다.
프로그램의 중요도나 상황에 따라 예외 발생시 메모리 상의 데이터를 파일에 저장시킨다거나 새로운 데이터베이스로 연결을 전환하고 다시 시도한다거나 관리자에게 해당 상황을 알리는 코드등도 작성할 수 있습니다.
catch(Exception e) {
e.printStackTrace(e);
...
}
finally
finally
는 예외가 발생한 경우나 혹은 발생하지 않은 경우 모두 수행되는 블럭을 지정할 때 사용 합니다. 코드의 성공/실패 여부에 상관없이 반드시 실행되어야 하는 구문들이 있다면 finally
에 넣어줘야 합니다.
try {
method1();
method2();
}
catch (AAAEception e) {
...
}
catch (BBBException e) {
...
}
finally {
...
}
예외 던지기와 예외클래스 작성
지금까지는 예외를 처리하는 과정을 살펴보았는데 이번에는 우리가 만드는 메서드에 예외 처리를 강제하도록 하는 방법을 알아보도록 합니다.
메서드를 만들때 throws
구문을 넣어주면 현재 메서드에서 특정 예외 처리를 하지 않겠다는 의미가 됩니다. 이 말은 곳 해당 메서드를 호출하는 쪽에서 해당 예외 클래스에 대한 처리 즉 try ~ catch
블럭을 사용해야 한다는것을 말합니다.
이 방법은 우리가 만드는 메서드를 호출할때 특정 예외처리를 강제하도록 하는데에도 사용 되며 내가 만든 전용의 예외처리 클래스를 활용할때도 적용이 됩니다.
public void printData() throws IOException, MyException {
}
- printData() 메서드 안에서 IOException 을 발생시키는 메서드를 사용할 경우 자체 처리를 하지 않고 호출하는 쪽으로 예외처리를 넘김.
- 제공되는 예외클래스 이외에 내가 만든 예외클래스 역시 throws 할 수 있음.
직접 예외 클래스를 만드는 경우 Exception 혹은 RuntimeException 클래스를 상속받아 구현하면 됩니다. 해당 예외는 예를 들면 앞의 printData() 메서드내에서 특정상황에 throw MyException
코드를 작성함으로써 예외 코드를 동작시킬 수 있습니다.
public class MyException extends Exception {
...
}
» 실습: 사용자 예외 클래스 구현하기
실습개요
사용자 정의 예외 클래스를 구현하고 throws 와 throw 구문의 동작 과정을 살펴 봅니다.
사용자 정의 클래스는 MyException 실행클래스는 ExceptionTest2 로 합니다.
소스코드
public class MyException extends Exception {
String exMsg;
public MyException(String msg) {
exMsg = "MyException: " + msg;
}
@Override
public String getMessage() {
return exMsg;
}
}
- Exeption 클래스의 여러 메서드들 예를들어 printStackTrace() 등의 메서드를 오버라이딩 할 수 있음.
- 여기서는 getMessage() 메서드만 오버라이딩.
- 생성자에서 메시지를 받아 문자열 조합후 getMessage() 에서 리턴하도록 구현.
소스코드
public class ExceptionTest2 {
int num;
public void doExeption() throws MyException {
if (num == 1)
System.out.println("OK");
else
throw new MyException("doException");
}
public static void main(String[] args) {
ExceptionTest2 app = new ExceptionTest2();
app.num = 2;
try {
app.doExeption();
} catch (MyException e) {
e.printStackTrace();
}
}
}
- doException() 메서드에서 MyException 예외 클래스를 throws 처리 즉 호출하는쪽에서 예외처리 필요.
- num 변수값에 따라 OK 를 출력하거나 예외를 발생.
- main() 에서 인스턴스 생성후 doException() 메서드를 호출하고
try~catch
처리.
실행결과
com.dinfree.java.part1.MyException: MyException: doException
at com.dinfree.java.part1.ExceptionTest2.doExeption(ExceptionTest2.java:10)
at com.dinfree.java.part1.ExceptionTest2.main(ExceptionTest2.java:18)
02: 문자열 다루기
문자열 이란?
모든 프로그램언어에서 가장 기본이 되는 자료형은 숫자형 으로 원시 자료형(Premitive Type)이라고도 합니다. 그런데 실제 우리가 사용하는 프로그램에서는 숫자값들 보다는 문자, 문자열을 더 많이 사용하게 됩니다. 예를 들어 네이버 메인 화면의 정보들 역시 문자열로 되어 있고 카페나 블로그에 글을 쓸때에도 이름, 제목, 내용 등 모두 문자열을 사용하게 됩니다.
컴퓨터는 자체적으로 문자열을 처리할 수 없으며 결국 문자열이라는 것은 문자로 이루어진 배열의 형태로 이해할 수 있습니다. 그리고 문자라는 것은 이미 우리가 배운것 처럼 결국 숫자로 된 코드값으로 변환되어 처리되는 것이지요.
String msg = "HELLO";
문자열로 선언된 msg 변수의 값 “HELLO”는 다음과 같이 문자 타입의 배열 구조가 됩니다.
`H` | `E` | `L` | `L` | `O` |
그리고 각각의 문자는 아스키 코드로 변환되어 다음과 같이 숫자로 이루어진 구조가 됩니다.
72 | 69 | 76 | 76 | 79 |
객체지향 언어에서는 클래스 타입을 사용할 수 있으므로 문자열 역시 별도의 클래스 타입으로 만들어 사용할 수 있습니다. 자바의 경우 String
클래스가 문자열 타입이라고 할 수 있습니다.
문자열 생성 및 비교
자바에서는 문자열 처리가 매우 쉽지만 기본적인 구조와 특징들을 잘 이해하고 사용하지 않으면 생각하지 못한 문제가 발생하거나 성능상에 문제가 발생할 수 있습니다.
String
클래스는 불변(immutable)클래스 이므로 다음과 같은 특징이 있습니다.
- 생성된 String 인스턴스가 갖고 있는 문자열값은 읽어올 수만 있고, 변경할 수는 없다.
- ’+’ 연산자를 이용해서 문자열을 결합하는 경우 인스턴스내의 문자열이 바뀌는 것이 아니라 새로운 문자열이 담긴 String 인스턴스가 생성됨.
- 문자열간의 결합등의 작업은 문자열
+
보다는 StringBuffer클래스를 사용.
String s1 = "hello";
String s2 = "hello";
- “hello” 라는 값을 가지는 새로운 String 클래스의 인스턴스 생성후 s1은 해당 인스턴스 참조.
- s2는 이미 “hello” 값을 가지는 인스턴스가 있으므로 새로운 객체를 생성하지 않고 s1의 변수 주소 할당.
- 따라서
s1 == s2
비교는 true.
String s1 = new String("hello");
String s2 = "hello";
- s1은 새로운 인스턴스 생성 새로운 주소값 할당.
- 이때 “hello” 값을 가지는 인스턴스가 있다면 해당 인스턴스 주소를 값으로 가짐. 없다면 새롭게 인스턴스 생성후 참조.
- s2는 이미 “hello” 값을 가지는 인스턴스가 있으므로 새로운 객체를 생성하지 않고 s1의 인스턴스 참조. 단 s2변수의 주소는 새롭게 할당.
- 따라서
s1 == s2
비교는 false.
분명 s1 과 s2는 같은 값을 가지고 있는데 비교연산시 false 가 나오게 되므로 실제 프로그램을 개발할때 예상치 못한 문제가 발생할 수 있습니다. 이유는 비교연산시 내용이 아니가 변수의 주소값을 비교하기 때문에 그렇습니다.
따라서 문자열 비교시 String
클래스에서 제공하는 equals()
메서드를 사용하는 것이 바람직 합니다.
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)) // true
» 실습: 자바 문자열 생성과 비교
실습개요
문자열을 생성하고 비교하는 과정을 통해 자바 문자열 생성의 특징을 이해하고 equals() 사용법을 실습합니다.
소스코드
public class StringTest1 {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = "hello";
System.out.printf("%s,%s\n", s1.hashCode(), s2.hashCode());
String s3 = new String("hello");
String s4 = "hello";
System.out.printf("%s,%s\n", s3.hashCode(), s4.hashCode());
System.out.printf("s1 == s2: %s\n", s1 == s2);
System.out.printf("s1 == s3: %s\n", s1 == s3);
System.out.printf("s2 == s4: %s\n", s2 == s4);
System.out.printf("s1.equals(s2): %s\n",s1.equals(s2));
s2 = s2 + " world";
String s5 = "hello world";
System.out.printf("s2 == s5: %s\n", s2 == s5);
}
}
hashCode()
메서드는 문자열 내용에 기반한 유일한 코드값으로 s1~s4는 내용이 동일하므로 모두 같은값을 가짐.s1 == s2
,s3 == s4
는 변수의 주소가 서로 다르므로 false.s2 == s4
는 변수의 주소가 동일하므로 true.s1.equals(s2)
,s2.equals(s3)
등은 모두 내용이 동일하므로 true.s2 = s2 + " world";
s2는 “hello world” 인스턴스를 새로 생성하고 새로운 변수 주소를 가지고 생성된 인스턴스 주소를 값으로 가짐.s5
는 새로운 변수 주소를 가지며 앞에서 생성된 인스턴스 주소를 값으로 가짐.- 따라서
s2 == s5
는 false. equals() 로 비교하면 true 가 됨.
실행결과
99162322,99162322
99162322,99162322
s1 == s2: false
s1 == s3: false
s2 == s4: true
s1.equals(s2): true
s2 == s5: false
참고로 디버거를 이용해 각 변수의 id 값과 인스턴스 주소 등을 확인하면 다음과 같습니다.
- s2와 s4의 id 값이 같고 s1, s3는 id 값이 다른것을 알 수 있음.
- 변수의 값은 모두 동일한 id 를 참조하고 있음.
- 문자열의 실제 값들은 아스키 코드의 문자 배열임을 확인할 수 있음.
String 클래스 주요 메서드
문자열과 관련된 다양한 기능들은 String
클래스의 메서드를 통해 제공 됩니다. 전체 메서드 목록은 Java API Document JDK11 에서 확인할 수 있습니다.
여기서는 몇몇 중요한 메서드만 살펴보도록 합니다.
join(), StringJoiner
join()
메서드는 문자열에 구분자를 넣어 결합하거나 분리하는 기능을 제공합니다. 최근 프로그램에서 데이터 조작이 많아지고 특히 입사시험등에 많이 사용되는 코딩테스트등에 유용하게 사용되는 기능 입니다.
String cars = "hyundai,mercedes,bmw";
String[] arr = cars.split(","); // ["hyundai","mercedes","bmw"]
String str = String.join("-", arr);
System.out.println(str); // hyundai-mercedes-bmw
- split() 메서드는 StringTokenizer 클래스를 사용해 구현할수도 있음.
StringJoiner
클래스는 문자열 결합을 도와주는 클래스로 형식에 맞게 문자열을 결합하는데 이용할 수 있습니다.
StringJoiner sj = new StringJoiner(",", "[", "]");
String[] carArr = { "hyundai", "mercedes", "bmw" };
for(String s : carArr)
sj.add(s.toUpperCase());
System.out.println(sj.toString()); // [HYUNDAI,MERCEDES,BMW]
trim(), substring(), replace(), toCharArr()
trim() 은 문자열 양쪽의 공백을 제거해 주는 메서드 이고 substring()은 문자열 내용중 일부만 추출할때 사용합니다. replace() 는 특정 문자를 다른 문자로 대체 합니다.
String s1 = " Hello World ";
System.out.println(s1.trim()); // Hello World
String s2 = s1.substring(1,3)
System.out.println(s1.trim()); // He
System.out.println(s1.replace('l','k')); // Hekko Workd
Char[] carr = s2.toCharArr(); // ['H','e','l','l','o',' ','W','o','r','l','d',] 공백생략.
문자열 원소의 위치 및 크기 관련
- indexOf(): 특정 문자가 처음 나오는 위치를 리턴.
- lastIndexOf(): 특정 문자가 마지막으로 나오는 위치를 리턴.
- charAt(): 지정된 인덱스의 문자를 리턴.
- startsWith(): 특정 문자 혹은 문자열 패턴으로 시작하는지 확인.
- length(): 문자열의 길이를 리턴.
StringBuffer, StringBuilder 클래스 활용
앞에서 배운것 처럼 문자열 결합시 새로운 인스턴스가 계속해서 생겨나기 때문에 for 문을 돌면서 지속적으로 문자열을 결합하는 형태의 프로그램은 성능에 큰 영향을 미칩니다.
이경우 StringBuffer
와 StringBuilder
클래스는 기본적으로 동일한 클래스 이며 문자열을 결합하는데 유용한 클래스 입니다. 두 클래스의 차이는 멀티스레드에 안전하게 처리되었는지의 차이로 StrinfBuffer
의 경우 Thread safe 로 멀티스레드 처리에 안전하지만 성능이 저하될 수 있습니다. 따라서 멀티스레드 처리가 없는 프로그램이라면 StringBuilder 유리하다고 할 수 있습니다.
StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" World");
String str = sb.toString();
- 최종적으로 문자열이 필요한 경우에만 toString() 으로 변환.
- 서로다른 StringBuffer 인스턴스에 같은 값이 들어 있어도
==
,equals()
를 이용한 비교는 false. toString()
으로 변환한다음equals()
로 비교해야 함.
» 실습: 문자열 처리 관련 종합 실습
실습개요
String 클래스의 여러 메서드들과 문자열 처리와 관련된 유틸리티 클래스들을 사용하는 실습 예제 입니다.
먼저 문자열 데이터를 조작하는 메서드와 유틸리티 클래스 사용 예제 입니다.
소스코드
import java.util.Arrays;
import java.util.StringJoiner;
public class StringTest2 {
public static void main(String[] args) {
StringJoiner sj = new StringJoiner(",", "[", "]");
String[] carArr = { "hyundai", "mercedes", "bmw" };
for (String s : carArr)
sj.add(s.toUpperCase());
System.out.println(sj.toString());
String s1 = " Hello World ";
String s2 = s1.trim();
System.out.printf("#%s#\n", s1);
System.out.printf("#%s#\n", s2);
String s3 = s1.substring(1, 3);
System.out.println(s3);
System.out.println(s1.replace('l', 'k'));
char[] carr = s2.toCharArray();
System.out.println(Arrays.toString(carr));
}
}
실행결과
[HYUNDAI,MERCEDES,BMW]
# Hello World #
#Hello World#
He
Hekko Workd
[H, e, l, l, o, , W, o, r, l, d]
다음은 String 클래스의 여러 메서드와 StringBuffer 를 사용하는 예제 입니다.
소스코드
package com.dinfree.java.part1;
public class StringTest3 {
public static void main(String[] args) {
String s1 = "Hello World";
System.out.println(s1);
System.out.printf("indexOf('l'): %s\n", s1.indexOf('l'));
System.out.printf("lastIndexOf('l'): %s\n", s1.lastIndexOf('l'));
System.out.printf("charAt(6): %s\n", s1.charAt(6));
System.out.printf("startWith(\"He\"): %s\n", s1.startsWith("He"));
System.out.printf("length(): %s\n", s1.length());
StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" World");
String str = sb.toString();
System.out.println(str);
}
}
실행결과
Hello World
indexOf('l'): 2
lastIndexOf('l'): 9
charAt(6): W
startWith("He"): true
length(): 11
Hello World
참고 자료
- 오라클 자바 홈페이지: http://java.oracle.com
- Introduction to Java Programming-IBM : https://www.ibm.com/developerworks
- Java Tutorial for Complete Beginners: https://www.udemy.com