아메리카노가 그렇게 맛있답니다 여러분

읽기에 앞서, 5화는 '4화 try/catch/finally, throws'와 연결되는 내용이므로 못 보신 분들은 보시는 것을 추천드립니다.


이번에는 강제로 예외를 발생시키는 예외발생에 대해 알아보겠습니다.

단어만 들으면 상당히 이상할 수 있습니다. 예외발생.. 예외를 일부러 만든다? 사실 쓰는 저도 당황스럽습니다. 왜냐하면 throw를 줘가면서 소스를 짜본 적이 없어서요.. 예외를 발생시키는 이유는 상위클래스에게 예외가 발생하였음을 알리기 위해 씁니다. 즉, 예외발생과 예외이양을 하여야 상위클래스가 무슨 예외가 발생하였는지 알 수 있습니다.

예외이양을 하는 방법은 throws를 사용하는 것입니다. 네, 맞습니다. 3화에서 본 try/catch, throws의 그 throws입니다. 3화에서는 throws는 예외를 넘기기만 할 뿐 try/catch처럼 사용자가 예외 발생에 대한 컨트롤을 할 수 없다고 했습니다. 사실 throws가 저런 기능밖에 없었다면 모든 프로그래머들이 try/catch밖에 사용하지 않았겠죠.


throws는 메소드를 호출한 쪽에게 예외를 던질 수 있습니다. "


물론 예외가 발생해야 던지겠죠? throws가 예외를 잘 던질 수 있도록 예외를 발생시켜주는 것이 throw입니다.

예외발생과 이양에 관해서는 예제를 보면서 하는 것이 편할 것 같습니다.


먼저 광부가 보석을 캘 때를 예외로 만들어봅시다. (간단하게 Exception을 상속받아 만들겠습니다.)

class OreException extends Exception{
}

실행할 main함수와 채석장에서 돌을 캐는 행동을 hitStone()이라는 메소드로 만들겠습니다.

public class test { public static void main(String[] args) { try { System.out.println("작업을 시작합니다."); hitStone(); System.out.println("광물을 발견하지 못했어요."); } catch(OreException e) { System.out.println("광물 발견!"); } } public static void hitStone() throws OreException { final int STONE = 1; final int DIAMOND = 2; int err = 0; int array[] = new int[10];   for(int i = 0; i < 9; i++) array[i] = STONE;   array[9] = DIAMOND; try{ for(int i = 0; i < 10; i++) { err = array[i]; if(array[i] == 1) System.out.println("두두두두"); else throw new OreException(); } } catch (OreException e) { System.out.printf("%d를 발견하였습니다.\n", err); throw e; } finally { System.out.println("채굴작업이 끝났습니다."); } } }

이 소스코드가 그림과 동일한 소스코드입니다. 실행시키면 아래 순서대로 결과가 출력됩니다.


예측한 것과 순서가 다르게 나오신 분들도 있을 것 같습니다. 소스코드가 실행된 순서에 대해서도 알아봅시다.




이렇게 설명을 같이 놓고 보니 이 소스코드는 예외가 발생한다면 main() 안에 있는 "광물을 발견하지 못했다"는 문구는 절대 출력되지 않겠네요. throw를 이용하여 예외를 발생시켜서 보석을 찾았는가에 대한 정보를 얻어낼 수 있었습니다. 예제에서는 비록 보석을 찾았는지에 대해서만 예외처리를 시켰지만 '곡괭이가 부러졌는가?'에 대해서도 같이 예외처리를 넣을 수 있습니다.

class OreException extends Exception{}
class NoShovelException extends Exception{}
 
public class test {
  public static void main(String[] args) {
    try {
    System.out.println("작업을 시작합니다.");
    hitStone();
    System.out.println("광물을 발견하지 못했어요.");
    }
    catch(OreException e) {
      System.out.println("광물 발견!");
    }
    catch(NoShovelException e) {
    }
    catch(Exception e) {
    }
  }
  public static void hitStone() throws Exception {
    final int STONE = 1;
    final int DIAMOND = 2;
    int shovel = 3, err = 0;
    int array[] = new int[10];
    for(int i = 0; i < 9; i++)
      array[i] = STONE;
    array[9] = DIAMOND;
    try{ 
      for(int i = 0; i < 10; i++) {
        err = array[i];
        if(shovel == 0)
          throw new NoShovelException();
        if(array[i] == 1)
          System.out.println("두두두두");
        else
          throw new OreException();
        shobel--;
      }
    }
    catch (OreException e) {
      System.out.printf("%d를 발견하였습니다.\n", err);
      throw e;
    }
    catch (NoShovelException e) {
      System.out.println("삽이 없습니다.");
      throw e;
    }
    finally {
      System.out.println("채굴작업이 끝났습니다.");
    }
  }
}



(삽으로 돌을 캔다는 것 자체가 이상하지만) 삽으로 돌을 3번 캐면 삽의 내구도가 0이 되어 삽이 없다는 말과 함께 작업을 끝내게 됩니다. 만약 main() 안에 있는 catch (NoShovelException e)안에 뭔가 써져있다면 해당 문구도 실행됩니다.  

throw는 라이브러리에서 많이 쓴다는 사실과 이런 것이 있다 정도로만 알고 있었는데 예문을 만들어보니 나름 throw도 재미있네요.

예외란?

프로그램이 처리하기에 문제가 생기는 것. 그러니까 프로그래머가 생각한대로 작동할 수 없는 상황을 예외라고 생각하시면 됩니다. 예외에 대한 예를 들어보자면, 파일을 읽도록 했는데 파일이 없거나 100번째 배열에 값을 저장하려 하는데 배열이 99번째가 마지막일 경우가 예외라고 할 수 있습니다.


예외를 들어보니 예외처리에 대한 감이 잡힙니다. 예외처리는 예외가 발생할 경우 프로그램이 할 일을 말하는 것입니다.

예외처리가 갖는 의미는 자바를 잘 쓸수록 커집니다. 사용자가 쓰기 쉬운 클래스이름으로 만들어서 접근이 용이합니다. 간단한 예로 C에서 파일의 끝을 while(feof(fp) != NULL)로 알아냈습니다. 하지만 자바에서는 EOFException이라는 예외처리 클래스를 사용하여 보다 직관적으로 소스코드를 읽어낼 수 있도록 도와줍니다. 


자바에서 예외처리는 다음과 같은 형식으로 사용합니다.

try {
  //실행시킬 코드
}
catch (Exception e) {
  //try에서 예외가 발생하면 실행된다.
}

'먼저 try에서 실행할 명령을 적어주고 실행하다가 예외가 발생한 부분은 catch에 있는 실행코드를 따른다.'


finally를 때때로 쓰기도 합니다. finally까지 쓴다면 소스코드가 조금 길어집니다.

try {
  //실행시킬 코드
} catch(Exception e) {
  //try에서 예외가 발생하면 실행된다.
}
finally {
  //반드시 실행될 코드
}

finally 안에 있는 것들은 반드시 실행된다는 것만 알면 됩니다. finally 사용은 5화에서 예외발생 때 같이 하겠습니다.

예외처리에 대해 충분히 알았다면, 예외의 종류는 무엇이 있는지 알아보도록 하겠습니다.

예외처리의 종류는 대략 아래 그림과 같습니다.


(위의 예외처리 목록 안에 있는 것들만 있는 것은 아닙니다. 너무 많기 때문에 알고 있는 것과 책을 참고한 것들만 적었습니다.)

그림을 보면 알 수 있듯이, 예외처리는 extends를 사용하여 만들어졌습니다. 즉, Exception을 사용하면 어떤 예외가 발생하는가에 상관없이 예외가 발생하면 catch문으로 넘깁니다. 자세한 예외처리를 하려면 Exception 아래의 서브클래스들을 쓰는 것이 좋을 것 같습니다.


try와 catch를 이용하여 예외처리를 할 수 있지만 throws를 사용하여 예외처리를 할 수도 있습니다.

public class Rfile {
  public static void main(String[] args) throws FileNotFoundException {
    File file = new File("res/tet.txt");
    Scanner input = new Scanner(file);
    System.out.println(input.nextLine());
    input.close();
  }
}

throws 대신에 try/catch를 사용하면 아래와 같습니다.

public class Rfile {
  public static void main(String[] args) {
 
    File file = new File("res/tet.txt");
    Scanner input;
    try {
      input = new Scanner(file);
      System.out.println(input.nextLine());
    } catch (FileNotFoundException e) {
    }
  }
}


throws를 사용한 것과 try/catch를 사용한 것의 차이는 무엇일까.

일단 throws는 클래스에 선언하였으므로 예외처리하는 범위가 try/catch보다 넓음을 알 수 있다. 또한 throws의 경우 해당 Exception이 알아서 예외처리를 진행합니다. (위의 경우 FileNotFoundException이 스스로 처리.)

하지만 try/catch를 사용하여 만들면 Exception이 발생할 경우 내가 어떻게 할 것인가에 대해 작성할 수 있습니다.


CheckedException / UncheckedException

예외처리를 하다보면 반드시 예외처리를 해야만 코드가 실행되는 경우가 있고 예외처리를 하지 않아도 상관없이 실행되는 코드가 있습니다. 예외처리를 반드시 해야 하는 경우를 CheckedException이라고 합니다. IOException이 대표적이며 컴파일러 단계에서 Exception이 발생하고 이 예외가 발생한 경우는 논리적인 오류가 아닙니다. 반면에 UncheckedException은 예외처리를 반드시 하지 않아도 실행됩니다. IndexOutOfBoundException이 대표적이며 프로그램이 실행될 때 Exception이 발생합니다. 이 예외가 발생한 경우는 대부분 사용자가 정수만 받을 수 있는데 문자를 넣었다던가 프로그래머가 없는 배열에 값을 넣도록 하는 등의 논리적인 문제입니다. (결론은 프로그래머의 짧은 생각 때문에 만들어지는 Exception.)


              

CheckedException

UncheckedException

필수성

O

X

체크 단계

컴파일러

프로그램 실행 시

예외 형태

명확

논리적

IOException

ArithmeticException

IndexOutOfBoundException…


저번 시간에는 클래스와 객체에 대하여 설명드렸습니다. 이번에는 클래스의 상속에 대해 설명드리려 합니다.



상속(extends)이란?


상속이란 A클래스가 B클래스에 정의된 필드와 메소드를 사용할 수 있도록 만드는 것을 말합니다.

이때 A는 부모클래스, B는 자식(extends)클래스가 됩니다. 이를 전문적인 말로 슈퍼 클래스(A)와 서브 클래스(B)라고 합니다.

자바에서 모든 클래스는 Object라고 하는 클래스를 상속받습니다. 이는 아무 것도 상속받지 않은 클래스도 포함됩니다.


상속을 이용하는 이유는 다음과 같습니다.


1. 소스코드를 반복해서 쓸 필요가 없습니다.

만약에 '생물'이라는 슈퍼클래스를 만들고 '인간', '늑대', '코끼리'라는 서브클래스를 만들었다면, 슈퍼클래스에 int iAlive;를 만들어서 생물클래스를 상속받는 모든 서브클래스들이 살았는지 죽었는지를 표시할 수 있도록 만들 수 있습니다.

슈퍼클래스를 만들어서 상속받도록 하지 않았다면 '인간', '늑대', '코끼리', ... 여튼 만들고 싶은 모든 동물 클래스에 살았는지 죽었는지를 확인할 코드를 만들어야 할 것입니다.

이는 비단 필드(맴버 변수)뿐만 아니라 메소드에도 적용됩니다.

2. 클래스 간 계층 분류 및 관리의 간편화

생물로 든 예를 보고 모든 소스코드를 하나씩 쓰고 고치는 것이 문제없으니 상속(extends)를 쓰지 않겠다는 사람이 있을 수 있습니다. 하지만 상속을 쓰는 이유 중 다른 큰 이유는 만들어놓은 소스를 추후 고칠 떄 간편하다는 점과 분류를 하기 쉽다는 점입니다. 만약에 금붕어와 짚신벌레, 코끼리, 독수리, 그리고 지네가 상속을 이용해서 만들어지지 않았다면 분류별로 묶기가 어려울 것입니다. (소스코드상에서는 서로 관계없는 클래스로 이해하므로.)

관리의 간편화의 경우에는 자신이 직접 소스코드를 짜면서 추가/삭제할 일이 생긴가면 금새 체감할 수 있을 것입니다.


여튼 크게 두 가지 이유로 상속을 사용하는 이유를 설명했습니다. 그렇다면 상속이라는 것은 몇 가지까지 받을 수 있을까요? 예를 들어 저는 집에서는 아들이지만, 가게에서는 손님이며, 해외에서는 외국인입니다.  같은 한 사람이지만 여러 가지로 불릴 수 있다는 것이죠. 이 경우 저는 세 개의 클래스(Son, Customer, Alien)를 상속받게 될까요?

아쉽지만 제가 상속받을 수 있는 클래스는 한 개가 최대입니다. 이는 마름모 문제(진짜로 이 이름은 아닙니다.)가 발생하기 때문인데, 그림으로 보면 다음과 같습니다.

(참고로 인터페이스는 다중상속이 가능합니다. 참고::인터페이스는 다중 상속이 되는 이유)


대충 무슨 문제가 생길지 보이나요? 우선 Son이 Human을 상속받고, Customer도 Human을 상속받습니다. 그리고 저를 의미하는 Me에 Son과 Customer를 상속시키죠. 이렇게 상속을 시키면 저는 Human을 두 번 상속받는 격이 됩니다. 왜냐하면 Son과 Customer가 둘 다 Human을 상속받았기 때문이죠. 그런 이유로 자바에서 상속은 한 클래스에게만 받을 수 있습니다. 만들고 싶다면 Son 또는 Customer 하나만 상속받아서 Me를 만들어야 하는 것입니다.

class Me extends Son { //컴마(,)쓰고 클래스를 더 상속받으려 하면 Error가 나옵니다.   } class Son extends Human { private String name; public String getName() { return name; } } class Human { private String name; }



업캐스팅과 다운캐스팅


앞에서 자바에서 모든 클래스는 Object를 상속받는다고 하였습니다. 즉, 위 소스코드를 사용하여 가계도를 그려본다면,







이렇게 표현할 수 있습니다. 일반적으로, Object클래스에 가까울수록 사용할 수 있는 필드와 메소드가 적어집니다.

(생물이 갖는 공통적인 성질의 개수와 늑대가 갖는 공통적인 성질의 개수 중 누가 더 많은가를 생각하면 쉽습니다. 최소한 늑대가 생물이 갖는 공통적인 성질은 기본적으로 갖고 있기 때문에 늑대가 더 많은 성질을 갖고 있습니다.) 

자바기본 클래스와 사용자가 만든 클래스들은 Object를 상속받는다는 점은 아래의 소스를 통해 확인할 수 있습니다.

public class Main {
	public static void main(String[] args) {
		Me a = (Me) new Object();
		a.getName();
		Integer b = (Integer) new Object();
		b.toString();
		}
}
 
class Me extends Son {
 
}
class Son extends Human {
	private String name;
	public String getName() {
		return name;
	}
}
class Human {
	private String name;	
}

new 앞에 무엇인가 쓰긴 했지만 결론적으로 우리는 Object 클래스를 사용하여 a와 b라는 객체를 만들었습니다. 그리고 a는 Son에서 정의한 getName()이라는 메소드를 사용할 수 있고 b는 자바 기본 클래스 중 하나인 Integer를 사용하여 int자료형을 String형으로 바꾸는 메소드를 실행해냈습니다.

위처럼 new Ojbect();를 써서 슈퍼클래스 쪽으로 바꾸는 일을 다운캐스팅(downcasting)이라고 합니다. 그리고 형변환처럼 써서 서브클래스 쪽으로 바꾸는 일을 업캐스팅(upcasting)이라고 합니다. 

추가적인 말씀을 드리자면, Human a = new Human();으로 만든 객체 a는 다운캐스팅을 통해 Son이 될 수는 있습니다. 하지만 a가 getName() 메소드를 쓸 수 있는 것은 아닙니다. 객체 자체는 Human으로 만들어졌기 때문입니다.


접근제어자와 상속


마지막으로, 상속을 말할 때 접근제어자를 빼먹을 수 없습니다.  접근제어자는 클래스 외부에서 해당 클래스에 접근하려 할 때 접근을 허용할 것인가 말 것인가를 정하는 지정자라고 할 수 있습니다.

접근제어자는 아래와 같이 네 가지가 있으며 이 중 default는 입력하지 않으면 자동으로 default로 인식합니다.


Keyword

클래스 내부

동일 패키지

다른 패키지

하위 클래스

일반 클래스

하위 클래스

일반 클래스

public

O

O

O

O

O

protected

O

O

O

O

 

default

O

O

O

 

 

private

O

 

 

 

 


public, protected, default, private 순으로 외부에서 값을 변경할 수 있는 범위가 좁아집니다. private로 만든 변수의 경우 get~~나 set~~를 통해서 가져오는 방법 외엔 다른 수가 없습니다. (이를 getter/setter라고 부릅니다.)



+상속받은 객체의 호출


상속받은 객체(이하 서브클래스)는 자바에서 호출될 때 다음의 순서를 거칩니다.


1. 서브클래스 호출

2. 서브클래스의 슈퍼클래스 호출

3. 서브클래스의 슈퍼클래스 실행

4. 서브클래스 실행


package constructorEx;
 
public class Super {
	Super() {
		System.out.println("Print A");
	}
	public static void main(String[] args) {
		Sub a = new Sub();
 
	}
}
class Sub extends Super {
	Sub() {
		System.out.println("Print B");
	}
}

위와 같은 예제가 있다고 할 때, Sub a = new Sub();를 써서 Sub형 객체 a를 호출할 경우

Print A

Print B

가 출력되는데 이를 통해 Sub의 생성자가 Super를 상속받으면 Super의 Print A도 출력하게 됨을 알 수 있습니다.

하지만 먼저 출력된다고 먼저 호출되었다는 것은 아닙니다. 왜냐하면 Sub a를 호출했는데 Sub가 Super를 상속받았음을 알고 Sub호출 후 Super를 호출하는 것이기 때문입니다.


대략 이런 모습으로 생각하시면 편합니다.