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

8. 네트워크 프로그래밍

이번 강좌에서는 네트워크 프로그래밍의 주요 개념과 Socket, URL, HttpURLConnection 등을 이용해 네트워크 프로그래밍 방법을 배웁니다.

이 강의를 통해 네트워크 프로그램이 무엇인지 이해하고 자바 에서 기본적인 입출력 프로그램을 구현할 수 있게 됩니다.



01: 네트워크 프로그래밍 개요

네트워크는 컴퓨터와 컴퓨터를 연결한 망형 조직으로, 기업 내 컴퓨터들을 연결한 랜LAN에서부터 전 세계를 하나로 연결한 인터넷까지 모두 네트워크라고 할 수 있다.

원래 네트워크의 기술적 유형에는 여러 가지가 있었지만, 지금은 TCP/IP 통신 프로토콜을 사용하는 컴퓨터 네트워크가 가장 일반적이다. 인터넷 역시 TCP/IP를 기반으로 한다.

[그림: 네트워크 연결 구조 예시]

여기서는 TCP/IP 기반(인터넷 포함)의 네트워크 프로그래밍을 기본으로 하며 학습을 위해서는 다음과 같은 기본적인 지식이 필요하다.

[그림: 네트워크 프로그래밍 구조]

Client, Server

네트워크 프로그램은 기본적으로 서버 프로그램과 클 라이언트 프로그램으로 이루어진다. 서버는 서비스를 제공하는 쪽을 말하고, 클라이언트는 서비스를 이용하는 쪽을 말한다. 카카오톡 등 채팅 프로그램을 예로 들면, 사용자의 스마트폰이나 컴퓨터에 설치된 프로그램은 클라이언트에 해당한다. 카카오톡 서버는 여러 사용자가 접속하여 서로 전달하는 메시지 를 중계하는 프로그램이다.

www도 인터넷 기반의 네트워크 서비스로 컴퓨터에서 사용하는 웹 브라우저(크롬, IE 등)는 클라이언트, 그리고 네이버, 다음, 구글 등은 서버가 된다.

Port

포트는 서버 컴퓨터에서 서비스 접속을 위해 만들어 둔 일종의 출입구로 볼 수있다. 숫자로 구분되며 서버 컴퓨터는 여러 네트워크 서비스를 제공할 수 있어, 접속하는 클라이언트는 자신이 원하는 서비스의 포트를 정확하게 알아야 해당 서비스에 연결할 수 있다. 대표적인 예로 웹서버는 80번포트를 사용 하며 모든 네트워크 서비스에는 고유의 포트가 있다.

Socket

소켓은 TCP/IP 네트워크에서 클라이언트와 서버를 연결하는 통로를 말한다. 스트림처럼 클라이언 트와 서버는 소켓으로 연결되고, 연결된 소켓을 이용하여 스트림을 만들어 입출력을 처리하는 개념 이다. 일반적으로 네트워크 프로그램을 만든다는 것은 TCP/IP 소켓 프로그래밍에 기반한다.

Protocol

프로토콜은 네트워크에서 컴퓨터와 컴퓨터가 데이터를 주고받기 위해 정해 둔 규약으로 보내는 클라이언트와 서버 사이의 일종의 대화 내용과 프로세스를 정해둔 것으로 이해할 수 있다. TCP/IP는 대표적인 네트워크 프로토콜이며 웹 브라우저와 웹 서버와의 통신에 사용되는 HTTP도 프로토콜이다. 카카오톡의 로그인, 메시지 전송, 로그아웃, 파일 전송 등 일련의 동작도 프로토콜로 정의되어 있다고 볼 수 있다.

HTTP

HTTP는 Hyper Text Transfer Protocol의 약어로, 웹의 통신 프로토콜을 말한다. HTTP는 TCP/IP에 기반을 둔 응용 프로토콜로 웹 브라우저와 웹 서버가 통신하는 데 필요한 메시지 규격이다.

Message

메시지는 네트워크 프로그램에서 클라이언트와 서버가 주고받는 일련의 데이터를 말한다. 메시지는 단순히 데이터가 아니라 커뮤니케이션을 위해 필요한 정보를 구조화해 둔 것으로 이해해야 한다.

메시지를 구조화 하고 프로그램에서 쉽게 파싱하기 위해서는 일련의 규칙을 가진 체계를 사용해야 하는데 현재 가장 널리 사용되는 메시지 규격은 JSON이다.

예를 들어 날씨 정보를 제공하는 서버(예를 들면 기상청)에서 API 서비스를 통해 JSON 규격의 메시지를 전달해 준다고 하면 모바일 앱에서 JSON 메시지를 파싱해서 원하는 형태로 가공한 후 화면에 보여 준다.


02: TCP/IP 소켓 프로그래밍

자바에서 제공하는 소켓은 크게 두 가지이다. 하나는 클라이언트에서 서버에 접속하는 데 필요한 Socket 클래스이고, 다른 하나는 프로그램이 서버로 동작하면서 클라이언트의 연결을 받는데 필요한 ServerSocket 클래스이다.

클라이언트가 서버에 접속하게 되면 서버는 별도의 Socket 클래스를 사용하여 클라이언트를 연결한다. 결국 클라이언트와 서버는 Socket을 통해 서로 연결 상태가 유지 된다.

연결된 소켓을 통해 입출력 스트림이 확보되고 나면 이후 프로그래밍 과정은 일반 I/O와 동일하며 바이트 혹은 문자 스트림을 사용해 입출력을 처리할 수 있다. 이 과정에서 메시지 규격이나 절차등은 프로그래머가 직접 정의해야 한다.

다음은 일반적인 자바 소켓 프로그래밍 과정이다.

[그림: 자바 소켓 프로그래밍 절차]
  1. 서버는 ServerSocket을 생성하고 클라이언트의 접속을 기다린다.
  2. 클라이언트는 Socket을 생성하고 서버에 접속한다.
  3. 서버는 클라이언트의 접속을 허용하고, 클라이언트와 연결된 Socket을 생성한다.
  4. 이때 서버는 클라이언트와 연결된 상태로 소켓사용이 끝날때 까지 다른 연결을 허용하지 않는다.
  5. 클라이언트와 서버는 각각 입출력 스트림을 생성한다.
  6. 입출력 스트림을 통해 클라이언트와 서버는 데이터를 주고받는다.
  7. 데이터 전송이 끝나면 소켓을 닫는다.

실습개요

간단한 서버와 클라이언트를 구현하고, 클라이언트에서 서버로 메시지를 전송하는 프로그램을 작성해 본다.

먼저 서버 프로그램이다.

public class BasicSocketServer {
    public static void main(String[] args) {
        int serverPort = 8080;

        try {
            ServerSocket serverSocket = new ServerSocket(serverPort);
            System.out.println("Server is listening on port " + serverPort);

            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("New client connected");

                // 입출력 스트림 생성
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                // 클라이언트로부터 메시지 수신
                String message = in.readLine();
                // 콘솔에 메시지 출력
                System.out.println(message);

                // 클라이언트에 메시지 전송
                out.println("이 메시지는 서버에서 클라이언트로 전송되는 확인 메시지 입니다!");

                // 소켓 및 스트림 close
                out.close();
                in.close();
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

다음은 클라이언트 프로그램 이다.

public class BasicSocketClient {
    public static void main(String[] args) {
        String serverIP = "127.0.0.1"; // localhost
        int serverPort = 8080;

        try {
            Socket socket = new Socket(serverIP, serverPort);

            // 입출력 스트림 생성
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 서버에 메시지 전송
            out.println("홍길동> 반갑습니다. 저는 홍길동입니다.");

            // 키보드로 입력받아 메시지를 전송하는 것으로 개선.
            //System.out.print("서버에 보낼 메시지를 입력하세요: ");
            //Scanner scan = new Scanner(System.in);
            //String message = scan.nextLine();
            //out.println("홍길동> "+message);

            // 서버로 부터 메시지 수신
            String response = in.readLine();
            System.out.println("Server response: " + response);

            // 소켓 및 스트림 close
            out.close();
            in.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

실행


03: HTTP 클라이언트 프로그래밍

최근에는 과거의 전통적인 소켓 연결 대신 URL 기반으로 웹 서버와 통신하는 방식의 프로그램이 스마트폰 등 모바일 인터넷 환경 이 발전하면서 급속도로 성장했다. 이런 프로그래밍 모델을 Restful 또는 REST(RE-presentational Status Transfer) 방식이라고 한다.

Rest API 혹은 Open API 등의 서비스는 인터넷 기반의 HTTP 프로토콜을 사용하고 있다. 이는 기본적으로 소켓을 사용할 수 있다는 것을 의미한다. 그러나 직접 소켓을 사용하게 되면 HTTP 프로톨 규격에 따른 절차와 헤더 등을 직접 구현해야 하므로 매우 번거롭다.

따라서 HTTP 클라이언트 프로그램은 별도의 라이브러리를 많이 사용한다. 기본 자바 API만 사용한다면 Http(s)URLConnection 클래스를 이용하면 된다. 이 부분은 특별한 제약이 있는 것은 아니므로 취향에 따라 라이브러리를 선택해 사용하면 된다.

» 실습1: HTTPsURLConnection 예제

실습개요

자바의 기본 HTTPsURLConnection 클래스로 웹 페이지를 읽어오는 프로그램을 작성해 본다.

site 변수에 원하는 웹 페이지 주소를 입력하고 실행하면 해당 웹 페이지의 내용을 콘솔에 출력한다. 예제에서 weather.naver.com은 네이버 날씨 페이지로 웹 페이지를 그대로 읽어오는 것이기 때문에 html 코드가 출력되는 것을 알 수 있다.

kiosk.free.beeceptor.com/menu 은 임시 서버에 만들어둔 rest api 서비스로, 메뉴 정보를 json 형태로 제공한다. 이를 읽어와 json 형태로 출력하는 것을 확인할 수 있다.

    public static void main(String[] args) {
        //final String site = "https://kiosk.free.beeceptor.com/menu";
        final String site = "https://weather.naver.com";

        try {
            URL url = new URL(site);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();

            conn.setRequestMethod("GET");

            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
» 실습2: HTTPClient 라이브러리 예제

HttpClient 라이브러리는 java.net 패키지에 포함되어 있으며 자바 기본 라이브러리만 사용해 Rest API 서비스를 호출하는 클라이언트 개발에 가장 적합하다.

HTTP 의 다양한 메소드, 헤더, 인증, 멀티파트(파일전송) 등을 지원하며, 비동기 처리도 가능하다. 또한, HTTP/2, WebSocket, HTTP/1.1, HTTP/1.0 등을 지원한다.

단순한 요청은 HTTPsURLConnection과 크게 다르지 않아 보이지만 실제 사용에 있어서는 훨씬 편리하다. 또한, 비동기 처리를 지원하므로 별도의 스레드를 사용하지 않아도 된다.

실습개요

rest api 서비스를 제공하는 서버로 부터 메뉴 정보를 읽어와 출력하는 프로그램을 작성해 본다.

public class HttpClientExam {
    public void start() {
        try {
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(new URI("https://kiosk.free.beeceptor.com/menu"))
                    //.uri(new URI("https://apingweb.com/api/rest/70825b554c07e276ad86bfb46c5082b63/menu")
                    .GET()
                    .build();

            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            //InputStream is = client.send(request, HttpResponse.BodyHandlers.ofInputStream()).body();

            System.out.println("Response Code: " + response.statusCode());
            // 서버가 응답을 ascii로만 하는 경우 콘솔상에는 유니코드값으로 출력될 수 있으나 JSON 파싱할때는 문제 없음.
            System.out.println("Response: " + response.body());

            printMenu(response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // JsonFileExam 에서 가져온 코드.
    private void printMenu(String data) throws ParseException {
        // JSON 파싱, json-simple library 사용 -> build.gradle에 추가
        JSONParser parser = new JSONParser();
        JSONObject obj = (JSONObject) parser.parse(data);
        System.out.println(obj.get("menu"));

        // 메뉴와 옵션을 출력
        for(JSONObject menu : (Iterable<JSONObject>) obj.get("menu")) {
            System.out.printf("[%s, %s]\n", menu.get("name"), menu.get("price"));
            for(JSONObject option : (Iterable<JSONObject>) menu.get("options")) {
                System.out.printf("\t >> %s, %s\n", option.get("name"), option.get("price"));
            }
        }
    }

    public static void main(String[] args) {
        new HttpClientExam().start();
    }
}

실행결과



참고 자료