프로그래밍언어 자바 Part-1

5. 자바 중급 활용-1

이번 강좌에서는 자바를 좀 더 제대로 사용하기위해 try~catch, throws, throw 등을 이용한 예외처리 기법을 살펴보고 사용자 정의 예외클래스를 만들고 활용해 봅니다. 또한 프로그램 개발시 많이 필요한 문자열의 동작구조를 배우고 주요 API들을 사용해 문자열을 다루는 방법을 배우게 됩니다.

이 강의를 통해 자바언어의 중급활용을 통해 기초를 다지고 보다 복잡한 자바프로그램 구조를 이해하고 개발 할 수 있습니다.



01: 예외처리

예외처리는 자바 프로그램에서 실행중 발생할 수 있는 예외적인 상황을 대비한 코드를 미리 만들어두는 것으로 문제 발생시 좀 더 안전한 구조를 제공하며 문제 해결에 필요한 정보등을 제공할 수 있습니다

일반적으로 프로그램에서 문제가 발생하는 것을 에러(error)라고 하며 보통 에러는 컴파일 과정에 발생하는 컴파일 에러와 실행중 발생하는 런타임 에러로 구분 합니다.

try ~ catch 블럭

자바에서 기본적인 예외처리 방법은 try ~ catch 블럭을 사용하는 것입니다. 그러면 언제 try ~ catch 를 사용해야 할까요? 다행이도 개발자가 굳이 신경을 쓰지 않더라도 특정 클래스의 메서드를 사용할때 컴파일러에 의해 try ~ catch 작성이 요구 됩니다. 물론 이클립스와 같은 개발도구는 컴파일 이전에 미리 관련해서 처리해야 한다고 알려주게 되고 기본 코드도 자동으로 생성해 줍니다.

그러면 컴파일러는 어떤 원리에서 try ~ catch 블럭을 사용해야 한다고 강제 할 수 있을까요? 이것은 좀 더 뒤에서 알아보도록 합니다.

우선 예외 상황이란 어떤 경우가 있을지 생각해 봅니다.

이처럼 정상적으로 프로그램이 동작할 수도 있지만 예외적인 상황이 발생하면 정상동작이 어려운 경우를 예외 상황으로 이해하면 됩니다. 이러한 예외 상황 환경에서 동작하는 클래스를 만들때 해당 기능을 수행하는 메서드에 throws 구문을 추가해 특정 예외 상황 처리를 강제하게 됩니다.

예외 처리 클래스는 java.lang.Exception 클래스를 부모로 하는 대표적인 클래스들이 있으며 사용자 정의 예외클래스를 만드려면 Exception 클래스를 상속받아 구현하면 됩니다.

예외처리 유형

자바의 예외처리는 Checked ExceptionUnchecked Exception 이 있습니다.

Checked Exception

Unchecked Exception

» 실습: 예외처리 기본 예제

실습개요

파일로 부터 데이터를 읽어 출력하는 프로그램을 구현하면서 기본적인 예외처리 과정을 살펴 봅니다.

앞에서 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());
        }
    }
}

소스를 작성하면 이클립스에서 다음과 에러가 있다는 표시를 만나게 되고 빨간 점을 살짝 클릭하면 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 {

}

직접 예외 클래스를 만드는 경우 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;
    }
}

소스코드

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();
        }
    }
}

실행결과

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 s1 = "hello";
String s2 = "hello";
String s1 = new String("hello");
String s2 = "hello";

분명 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);
    }
}

실행결과

99162322,99162322
99162322,99162322
s1 == s2: false
s1 == s3: false
s2 == s4: true
s1.equals(s2): true
s2 == s5: false

참고로 디버거를 이용해 각 변수의 id 값과 인스턴스 주소 등을 확인하면 다음과 같습니다.

js_3-1
[그림: 디버거를 이용한 문자열 변수 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

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',] 공백생략.

문자열 원소의 위치 및 크기 관련

StringBuffer, StringBuilder 클래스 활용

앞에서 배운것 처럼 문자열 결합시 새로운 인스턴스가 계속해서 생겨나기 때문에 for 문을 돌면서 지속적으로 문자열을 결합하는 형태의 프로그램은 성능에 큰 영향을 미칩니다.

이경우 StringBufferStringBuilder 클래스는 기본적으로 동일한 클래스 이며 문자열을 결합하는데 유용한 클래스 입니다. 두 클래스의 차이는 멀티스레드에 안전하게 처리되었는지의 차이로 StrinfBuffer의 경우 Thread safe 로 멀티스레드 처리에 안전하지만 성능이 저하될 수 있습니다. 따라서 멀티스레드 처리가 없는 프로그램이라면 StringBuilder 유리하다고 할 수 있습니다.

StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" World");
String str = sb.toString();

» 실습: 문자열 처리 관련 종합 실습

실습개요

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

참고 자료