반응형

자바(Java)에서 Exception을 잘 다루면 여러 상황에 대한 대응력과 유연성을 높일 수 있지만, 예외처리에 대한 일관된 기준이나 잘 정리된 설계가 없으면 어떻게 처리(handling)할 지 막막해지기도 한다. 그래도 코딩하는 과정에서, 내가 만든 특정한 클래스를 참조해서 쓰는 다른 (작성자가 나일 수도 있고, 다른 개발자일 수도 있는) 클래스에게 이 예외상황만큼은 반드시 명시적으로 인지하고 특별하게 처리하도록 하고 싶으면 메쏘드 제목에서 throws 구문으로 예외를 caller에게 전달해야 한다.


그런데 특정한 예외를 caller에게 전달하려고 throws로 예외를 명시했는데도 caller 쪽에서 그 예외를 catch하지 못하는 경우가 있는데, 알고 보니 throw 하는 예외가 상속 관계에 의해서 부모 예외 타입으로 다른 곳에서 먼저 catch되는 바람에 caller 쪽에서 처리하지 못했던 것이었다.



구체적인 상황은 다음과 같다:


UDP Datagram Socket을 이용한 메세지 송수신 역할을 담당하는 클래스가 있는데, 여기에 타임아웃(timeout) 기능을 추가해서 특정한 메세지 전송에 대해서 응답을 받기 위해서 UDP Socket을 열되, 일정 시간 동안만 기다리도록 만들 필요가 생겼다. 찾아보니 Socket 클래스의 setSoTimeout 메쏘드를 시간값 파라미터와 함께 호출하면 해당 시간이 지난 뒤에 자동으로 SocketTimeoutException을 발생시킨다는 사실을 확인했다. 


그래서 아래와 같이 sendMessageAndGetReply(String ip, int port, String msg, int timeout) 메쏘드를 정의하고 throws SocketTimeoutException을 추가로 적어 주었는데, 그럼에도 불구하고 caller 쪽에서는 SocketTimeoutException을 catch할 수가 없었다.



public static String sendMessageAndGetReply(String ip, int port, String msg, int timeout) throws SocketTimeoutException {

String reply = null;

byte[] sendData = new byte[1024];

byte[] receiveData = new byte[1024];

try {

DatagramSocket clientSocket = new DatagramSocket();

sendData = msg.getBytes();

DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName(ip), port);

clientSocket.send(sendPacket);

DatagramPacket recvPacket = new DatagramPacket(receiveData, receiveData.length);

clientSocket.setSoTimeout(timeout); // 추가로 타임아웃을 명시적으로 설정한 부분

clientSocket.receive(recvPacket);

reply = new String(recvPacket.getData());

clientSocket.close();

} catch (UnknownHostException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

return reply;

}



그런데 신기하게도 실행 화면에서는 SocketTimeoutException이 발생했다고 출력은 되는데, 정작 caller에서의 catch 구문은 실행되지 않았다.

코드의 다른 곳에서 해당 예외를 이미 처리했다는 의미인데, 알고 보니 sendMessageAndGetReply 메쏘드에서 catch (IOException e) 부분이 원인이었다. SocketTimeoutException은 java.io.InterruptedIOException을 extend한 자식 클래스이고, InterruptedIOException은 또한 IOException을 extend하고 있기 때문에, sendMessageAndGetReply가 해당 예외를 이미 처리해 버렸기 때문에, 제아무리 메쏘드 이름 옆에 throws로 명시해 두어도 throw가 되지 않았던 것이었다.


이것을 해결하기 위해서 결국 코드를 아래와 같이 고쳐야 했다. [2]


public static String sendMessageAndGetReply(String ip, int port, String msg, int timeout) throws SocketTimeoutException {

String reply = null;

byte[] sendData = new byte[1024];

byte[] receiveData = new byte[1024];

try {

DatagramSocket clientSocket = new DatagramSocket();

sendData = msg.getBytes();

DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName(ip), port);

clientSocket.send(sendPacket);

DatagramPacket recvPacket = new DatagramPacket(receiveData, receiveData.length);

clientSocket.setSoTimeout(timeout); // 추가로 타임아웃을 명시적으로 설정한 부분

clientSocket.receive(recvPacket);

reply = new String(recvPacket.getData());

clientSocket.close();

} catch (UnknownHostException e) {

e.printStackTrace();

} catch (IOException e) {

if(e instanceof SocketTimeoutException){

// 자식 클래스 예외를 특별하게 처리

throw new SocketTimeoutException();

}

e.printStackTrace();

}

return reply;

}


이렇게 했더니 콜 스택이 짧아지긴 했지만, 그래도 caller 쪽으로 Socket TimeoutException만큼은 확실하게 넘어가는 것을 확인할 수 있었다.


결론적으로, 당연한 얘기지만 예외를 처리할 때, throw하려는 예외가 혹시 해당 위치에 이미 존재하는 다른 catch 구문에 의해서 같이 처리되지는 않는지 상속 관계를 바탕으로 꼼꼼하게 확인할 필요가 있다.




<참고자료>

[1] http://stackoverflow.com/questions/27797451/i-cant-catch-sockettimeoutexception

[2] http://stackoverflow.com/questions/20532855/why-cant-i-handle-exception-e-with-try-catch-clause



반응형
블로그 이미지

Bryan_

,
반응형

자바에서 DatagramSocket (UDP 소켓)을 파라미터 없이 생성하거나, 포트 번호만 파라미터로 지정하면 특정 네트워크 인터페이스에 지정되는 것이 아니고, 인터페이스에 상관 없이 포트 번호만 맞으면 패킷을 주고 받는 식으로 작동한다.


실제로 인터넷에 검색해 보면 UDP 소켓을 특정한 네트워크 인터페이스에 지정하고자 하는 요구가 상당히 많은 것을 알 수 있다. 하지만 컴퓨터에 네트워크 인터페이스가 여러 개 있을 경우에 UDP 소켓에서 특정 네트워크 인터페이스를 지정하는 것이 Java Virtual Machine (JVM) 입장에서는 자연스럽지 않을 수 있다. "패킷이 컴퓨터 밖으로 나갈 때, 이를 내보낼 가장 적합한 네트워크 인터페이스를 선택한다"는 보편적인 논리에서 보면 이것은 원래 운영체제(OS)에 내장된 라우팅 프로토콜의 역할이기 때문에, User mode에 있는 JVM 위에서 돌아가는 자바 어플리케이션이 이 역할에 일부분 끼어드는 것처럼 보이는 것이다. 아무튼 자바 어플리케이션 입장에서 불가능하지는 않지만, 바람직한 구성은 아닐 수도 있다.


어쨌든 패킷을 받는 서버 입장에서 DatagramSocket을 생성할 때, 생성자(constructor) 파라미터에 특정 IP 주소와 포트 번호를 모두 파라미터로 넘김으로써 특정 네트워크 인터페이스에서만 패킷을 받도록 지정할 수는 있다.


byte[] receiveData = new byte[1024];

String ip = "192.168.3.4"; // 현재 컴퓨터의 특정 네트워크 인터페이스에 할당된 IP 주소

String port = 54321; // 원하는 포트 번호

DatagramSocket socket = new DatagramSocket(new InetSocketAddress(ip, port));

DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

socket.receive(receivePacket);


패킷을 보내는 클라이언트 입장에서는, 그냥 DatagramPacket을 보낼 때 destination IP 주소를 어떻게 지정하느냐에 따라서 특정 네트워크 인터페이스를 통한 전송 여부가 결정된다. 이것은 운영체제의 라우팅 테이블 설정을 따라간다.


byte[] sendData = new byte[1024];

// sendData에 대한 메세지 생성 작업 수행

String ip = "192.168.3.6";

int port = 54321;

DatagramSocket clientSocket = new DatagramSocket();

DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName(ip), port);

clientSocket.send(sendPacket);


예를 들어, 리눅스의 route 설정이 유선랜 eth0과 무선랜 wlan0 모두 활성화되어 있고, default (0.0.0.0)에 대한 게이트웨이가 유선랜(eth0) 게이트웨이로 설정되어 있다고 가정하자. 이 때, 로컬 네트워크 밖에 있는 인터넷 상의 어떤 기기에 UDP 패킷을 보내고자 한다면, 자바 어플리케이션은 eth0을 통해서 패킷을 보낼 것이다. 이런 경우에 반드시 무선랜 wlan0으로 패킷이 나가도록 하고 싶으면 리눅스에서 route 명령으로 wlan0을 통해서 인터넷으로 나가는 경로를 설정해 주어야 한다.


문제는 서버의 입장에서 DatagramSocket에 특정 IP 주소를 지정해 버리면, 브로드캐스트(broadcast) 메세지를 받지 못하는 문제가 생긴다. 상대편에서 정확하게 IP 주소를 지정해서 보내는 경우에만 패킷을 받을 수 있고, 테스트해 본 결과, 가령 192.168.3.255 와 같은 destination IP로 전송되는 패킷은 받지 못했다.


이 경우에는 서버의 입장에서는 특정한 IP 주소를 지정하지 말고 모든 인터페이스와 모든 주소에 대해서 받을 수 있도록 해 두고, 일단 패킷을 받고 나서 전송자의 IP 주소 대역을 보고 판단하는 루틴을 코드에 추가하는 것이 좋을 것 같다. (이것이 best solution인지는 알 수 없으나, 현재로써는 이것이 working solution이다.)


byte[] receiveData = new byte[1024];

String port = 54321; // 원하는 포트 번호

DatagramSocket socket = new DatagramSocket(port);

DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

socket.receive(receivePacket);




반응형
블로그 이미지

Bryan_

,
반응형

OS: Windows 8.1 (64-bit)

Java: 1.8.0_60

Eclipse: Mars (64-bit)

프로젝트가 사용하는 JRE System Library: JavaSE-1.7



이클립스에서 프로젝트를 Export 명령으로 Runnable JAR 파일을 만들고 다른 환경(예를 들면 리눅스가 돌아가는 보드PC 같은 곳)에서 실행하려고 했는데 다음과 같은 에러가 발생했다:




Exception in thread "main" java.lang.UnsupportedClassVersionError: [실행하려는 클래스 이름] : Unsupported major.minor version 52.0

        at java.lang.ClassLoader.defineClass1(Native Method)

        at java.lang.ClassLoader.defineClass(ClassLoader.java:788)

        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:14                                                                    2)

        at java.net.URLClassLoader.defineClass(URLClassLoader.java:447)

        at java.net.URLClassLoader.access$100(URLClassLoader.java:71)

        at java.net.URLClassLoader$1.run(URLClassLoader.java:361)

        at java.net.URLClassLoader$1.run(URLClassLoader.java:355)

        at java.security.AccessController.doPrivileged(Native Method)

        at java.net.URLClassLoader.findClass(URLClassLoader.java:354)

        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)

        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)

        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)



일단 .jar 파일을 돌리려는 대상 보드PC (Odroid)에 설치된 자바는 OpenJDK고 버전이 1.7.0_25다.
이클립스 프로젝트에서 JRE 라이브러리는 비록 1.7로 되어 있었지만, 이클립스가 실제로 사용하는 컴파일러는 1.8로 되어 있었기에 빌드한 버전이 맞지 않아서 생기는 문제였다.

해결하려면 Window > Preferences 에 가서,
Java > Compiler를 선택하고, 오른쪽에 보이는 화면에서 Compiler compliance level을 1.7로 설정한다.


그러면 현재 프로젝트들을 모두 새로 빌드할 것이다.
그 뒤에 다시 Runnable JAR 파일을 만들어서 실행하면 될 것이다.



반응형
블로그 이미지

Bryan_

,
반응형

Java 버전이 올라가면서 향상된 for 루프문(enhanced for loop) 문법이 나왔는데, 이것으로는 배열을 초기화할 수 없다는 사실을 발견했다.


예를 들어, 10개짜리 문자열 배열(String array)을 만들고 향상된 for 루프를 쓰게 되면 아래와 같이 코딩할 수는 있는데,

String[] strArray = new String[10];

for( String str : strArray ){

str = "test_string";

}


for( String str: strArray ){

System.out.println(str); // NullPointerException 발생

}


위와 같이 하면 배열의 각 문자열이 초기화되지 않고 여전히 null 상태로 존재하게 된다.

번거롭더라도 배열 초기화는 index number를 사용해서 아래와 같이 명시적으로 해 주는 것이 좋다.


for(int i=0; i<strArray.length; i++){

strArray[i] = "test_string";

}


이 문제는 String 뿐만 아니라 객체 배열(Object array), 기본 타입 배열(primitive type array; 예를 들어, int[], double[])에서도 마찬가지로 발생한다.

다만 기본 타입(primitive type) 배열의 경우에는 향상된 for 루프를 쓰더라도 NullPointerException은 발생하지 않지만, 값들이 0인 상태로 바뀌지 않은 채 존재할 것이다.



반응형
블로그 이미지

Bryan_

,