minhui study

서버에 데이터 요청하고 응답받기1( 소켓 사용하기, 웹으로 요청하기 ) 본문

안드로이드

서버에 데이터 요청하고 응답받기1( 소켓 사용하기, 웹으로 요청하기 )

minhui 2020. 7. 21. 10:39

네트워킹이란?

네트워킹은 인터넷에 연결되어 있는 원격지의 서버 또는 원격지의 단말과 통신해서 데이터를 주고받는 동작들을 포함한다. 즉, 여러분이 가지고 있는 단말의 데이터만사용하는 것이 아니라 멀리 떨어져 있는 서버나 다른 사람의 데이터를 조회할 수도 있다. 서버에 데이터를 저장할 때는 먼저 인터넷을 통해 데이터 통신이 가능한지를 알아본 후 데이터를 주고받는 과정도 진행하며 데이터를 주고받는 과정은 매우 복잡하다.

그럼에도 불구하고 네트워킹을 사용하는 이유는 인터넷에 연결되어 있는 여러 단말을 동시에 사용할 수 있어 다양한 데이터를 효율적으로 사용할 수 있기 때문이다.

 

원격지의 서버를 연결하는 가장 단순한 방식은 클라이언트와 서버가 일대일로 연결하는 2-tier C/S방식이다. 대부분 클라이언트가 서버에 연결되어 데이터를 요청하고 응답받는다. HTTP 프로토콜, FTP 프로토콜 그리고 POP3 프로토콜 등의 연결 방식을 모두 아래 그림과 같은 방법으로 서버로 간편하게 접속하여 처리한다.

https://www.zentut.com/jsp-tutorial/introduction-to-jsp/

 

3-tier 연결 방식을 사용하면 서버를 좀 더 유연하게 구성할 수 있다. 응용 서버와 데이터 서버로 서버를 구성하면 데이터베이스를 분리할 수 있어 중간에 비즈니스 로직을 처리하는 응용 서버가 좀 더 다양한 역할을 할 수 있다.

Do it! 안드로이드 프로그래밍 

 

 

 

단말 간의 통신이 일반화되며 클라이언트와 서버의 관계는 Peer-to-Peer 통심으로 불리는 P2P 모델로도 변형되어 사용되기도 한다. P2P 모델은 서버를 두지 않고 단말끼리 서버아 클라이언트 역할을 하며 정보 검색이나 파일 송수신으로 정보를 공유하는 데 많이 사용된다. 

Do it! 안드로이드 프로그래밍

 

 

 

 

소켓 사용하기

HTTP 프로토콜과 소켓

HTTP프로토콜은 소켓으로 웹서버에 연결한 후 요청을 전송하고 응답을 받은 다음 연결을 끊는다. 이런 특성을 '비연결성(Stateless'이라고 하는데 이런 특성 때문에 실시간으로 데이터를 처리하는 앱은 응답 속도를 높이기 위해 연결성이 있는 소켓 연결을 선호했지만 지금은 인터넷의 속도가 빨라져 HTTP 프로토콜을 사용하는 웹이 일반적이 되었고 속도가 느리지 않으면서도 국제 표준을 따른다는 장점을 가진 웹서버로 많은 서버가 만들어지게 되었다.

 

안드로이드는 소켓 연결 등을 시도하거나 응답을 받아 처리할 때 스레드를 사용해야 한다. 네트워킹 실습을 위해 먼저 클라이언트와 서버 소켓을 만들어 보자. 

레이아웃

일단 위와 같이 화면 레이아웃을 만들었다면 MainActivity.java 파일을 열고 코드를 입력한다. 텍스트뷰와 입력상자는 클래스 안에 변수를 선언하고 findViewByid로 찾아 변수에 할당한다. 첫 번째 "전송" 버튼을 눌렀을 때 새로 만들 send( ) 메서드를 호출하도록 하고 두 번째 "서버 시작" 버튼을 눌렀을 때는 startServer() 메서드를 호출하도록 한다. (이 두 개의 메서드는 모두 네트워킹 기능을 사용할 것이므로 스레드로 만들어야 한다. 즉, 버튼을 눌렀을 때 스레드 안에서 동작하게 만드는 게 중요하다.)

 

그리고 두 개의 텍스트뷰는 결과를 화면에 출력하기 위한 것으로 printClientLog() 메서드는 화면 상단에 있는 텍스트뷰에 글자를 출력하도록 하고 printServerLog() 메서드는 화면 하단에 있는 텍스트뷰에 글자를 출력하도록 한다. 새로 만들어진 스레드에서 이 메서드들을 호출할 것이므로 핸들러 객체를 이용하며 Runnable 객체의 run() 메서드 안에서 텍스트뷰를 접근하고 있다. 텍스트뷰의 append() 메서드로 전달될 파라미터는 printClientLog()와 printServerLog() 메서드로 전달되는 파라미터가 그대로 전달되어야 하므로 final로 정의했다. 

 

이제 클라이언트에서 데이터를 전송하는 send() 메서드를 살펴보자. 여기서에서 서버와 클라이언트가 6002번 포트를 사용하도록 하였다. 접속할 IP주소는 "localhost", 포트는 6002번을 사용하고 있다. new연산자로 만드는 소켓을 이 IP주소와 포트 번호를 파라미터로 전달받으며 새로 만들어진 소켓을 통해 데이터를 보내거나 받고 싶을 때는 getOutputStream()과 getInputStream() 메서드로 입출력 스트림 객체를 참조한다. 

*여기서는 문자열 객체 그대로 보내기 위해 ObjectOutputStream,InputSream 클래스를 사용하였다.

☆실제 앱을 만들 때도 ObjectOutputStream,InputSream을 사용할까?

 → 잘 사용하지 않는다. 만일 자바가 아닌 다른 언어로 만들어진 서버와 통신할 경우에는 데이터 송수신이 정상적으로 이루어지지 않을 수 있으므로 일반적으로는 DatainputStream과 DataOutputStream을 많이 사용한다.

 

 

그리고 클라이언트가 접속할 서버는 startServer() 메서드 안에 구성한다.

소켓 서버는 ServerSocket 클래스로 만든 후, 클라이언트로부터의 요청을 처리할 수 있는데 포트 번호는 클라이언트에서 접속할 6002번을 그대로 사용한다. while구문을 사용해서 클라이언트의 접속을 기다리다가 클라이언트의 접속 요청이 왔을 때 accept()메서드를 통해 소켓 객체가 반환되므로 클라이언트 소켓의 연결 정보를 확인할 수 있다. 여기서는 클라이언트에서 접속한 포트 번호를 확인한 후 보내온 문자열에 "from Server."라는 문자열을 붙여 클라이언트로 다시 보낸다.

 

 

< MainActivity.java >

package org.techtown.socket;

import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class MainActivity extends AppCompatActivity {
    EditText editText;

    TextView textView;
    TextView textView2;

    Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.editText);
        textView = findViewById(R.id.textView);
        textView2 = findViewById(R.id.textView2);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {  # 스레드 안에서 send()메서드 호출하기
                final String data = editText.getText().toString();

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        send(data);
                    }
                }).start();

            }
        });

        Button button2 = findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { # 스레드 안에서 startServer() 메서드 호출하기
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        startServer();
                    }
                }).start();

            }
        });
    }

    public void send(String data) {
        try {
            int portNumber = 6002;
            Socket sock = new Socket("localhost", portNumber); # 소켓 객체 만들기
            printClientLog("소켓 연결함.");

            ObjectOutputStream outstream = new ObjectOutputStream(sock.getOutputStream()); # 소켓 객체로 데이터 보내기
            outstream.writeObject(data);
            outstream.flush();
            printClientLog("데이터 전송함.");

            ObjectInputStream instream = new ObjectInputStream(sock.getInputStream());
            printClientLog("서버로부터 받음 : " + instream.readObject());
            sock.close();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    public void startServer() {
        try {
            int portNumber = 6002;

            ServerSocket server = new ServerSocket(portNumber); # 소켓 서버 객체 만들기
            printServerLog("서버 시작함 : " + portNumber);

            while(true) { # 클라이언트가 접속했을 때 만들어지는 소켓 객체 참조하기
                Socket sock = server.accept();
                InetAddress clientHost = sock.getLocalAddress();
                int clientPort = sock.getPort();
                printServerLog("클라이언트 연결됨 : " + clientHost + " : " + clientPort);

                ObjectInputStream instream = new ObjectInputStream(sock.getInputStream());
                Object obj = instream.readObject();
                printServerLog("데이터 받음 : " + obj);

                ObjectOutputStream outstream = new ObjectOutputStream(sock.getOutputStream());
                outstream.writeObject(obj + " from Server.");
                outstream.flush();
                printServerLog("데이터 보냄.");

                sock.close();
            }
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    public void printClientLog(final String data) { # 화면 상단에 결과 출력하는 함수
        Log.d("MainActivity", data);

        handler.post(new Runnable() { # 클라이언트 쪽 로그를 화면에 있는 텍스트 뷰에 출력하기 위해 핸들러 사용
            @Override
            public void run() {
                textView.append(data + "\n");
            }
        });

    }

    public void printServerLog(final String data) { # 화면 하단에 결과 출력하는 함수
        Log.d("MainActivity", data);

        handler.post(new Runnable() { # 서버 쪽 로그를 화면에 있는 텍스트뷰에 출력하기 위해 핸들러 사용하기
            @Override
            public void run() {
                textView2.append(data + "\n");
            }
        });
    }

}

 

 

이제 마기막으로 /app/manifests 폴더 안에 있는 AndroidManifest.xml 파일을 열고 INTERNET권한을 추가해준다.

 

앱을 실행한 루 [서버 시작] 버튼을 먼저 눌러 서버를 시작시키면 시작되었다는 로그가 화면 하단에 출력되고 화면 상단에 입력상자에 글자를 입력하고 [전송] 버튼을 누르면 그 내용이 서버로 전송되었다가 다시 클라이언트 쪽으로 전달된 것을 확인할 수 있다.

 

클라이언트에서 전송한 글자가 서버를 통해 전달된 결과

 

웹으로 요청하기

HTTP로 웹 서버에 접속하기

HTTP로 웹 서버에 접속하는 것도 소켓의 경우와 마찬가지로 표준 자바의 방식을 그대로 사용할 수 있다. 자바에서 HTTP 클라이언트를 만드는 가장 간단한 방법은 URL 객체를 만들고 이 객체의 openConnection() 메서드를 호출하여 HttpURLConnection 객체를 만드는 것이다. 

 

URL 객체에 들어 있는 문자열이 "http://"를 포함하면 HTTP 연결을 위한 객체를 만들게 되므로 openConnection()메서드가 리턴하는 URLConnection 객체를 HttpURLConnection으로 형변환하여 사용할 수 있다. HttpURLConnection 객체로 연결할 경우에는 GET이나 POST와 같은 요청 방식과 함께 요청을 위한 파라미터들을 설정할 수 있다.

요청방식을 지정하는 메서드는 setRequestMethod()로 GET이나 POST 문자열을 파라미터로 전달한다. setRequestProperty()메서드는 요청할 때 헤더에 들어가는 필드 값을 지정할 수 있도록 한다. 이번에는 GET 방식을 사용하여 웹페이지 주소를 입력하면 해당 페이지의 내용을 가져오는 앱을 만들어 보자

 

다음과 같이 입력 상자에 사이트 주소를 입력하고 요청하기 버튼을 누르면 그 사이트로부터 응답 데이터를 가져와 화면 하단의 텍스트뷰에 보여주도록 만들어 보자

화면 레이아웃

 

 

MainActivity.java 파일을 열고 버튼을 클릭했을 때 웹으로 요청하는 코드를 추가하자.

[1] : 버튼을 누르면 사용자가 입력한 사이트 주소를 이용해 request()메서드를 호출한다. request() 메서드 안에서는 인터넷을 사용할 것이므로 스레드 안에서 동작하도록 스레드 객체를 하나 생성하고 그 안에서 request() 메서드를 호출하도록 한다. 스레드에서 처리한 결과물을 화면에 표시할 때 사용하도록 핸들러 객체도 만들어 변수에 할당해 둔다.

 

[2] : request() 메서드에서는 응답 결과물을 모아 화면에 출력한다. 

request() 메서드 안에 정의된 웹페이지 요청 부분을 보면 가장 먼저 URL 객체를 만들고 있다. 파라미터로 전달된 URL 문자열을 이용해 만들어진 객체의 openConnection() 메서드를 호출하면 HttpURLConnection 객체가 반환된다.

이 객체에 GET방식으로 요청한다고 setRequestMethod()로 설정하고 getResponseCode()메서드를 호출하면 이 때 내부적으로 웹서버에 페이지를 요청하는 과정을 수행하게 된다. 그리고 스트림에서 한 줄씩 읽어 들이는 메서드인 readLine()은 BufferedReader 클래스에 정의되어 있으므로 HttpURLConnection 객체의 스트림을 이 클래스의 객체로 만든 후에 처리한다.

 

 

[3] :  화면에 출력할 때 사용하는 println()메서드는 핸들러를 사용하면서 화면에 들어있는 텍스트뷰의 append() 메서드를 호출하도록 한다. 

 

 

 

 

< MainActivity.java >

package org.techtown.http;

import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity; # ---- [1]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    EditText editText;
    TextView textView;

    Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.editText);
        textView = findViewById(R.id.textView);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String urlStr = editText.getText().toString();

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        request(urlStr); # 스레드 안에서 request() 메서드 호출하기
                    }
                }).start();
            }
        });
    }

    public void request(String urlStr) { # --- [2]
        StringBuilder output = new StringBuilder();
        try {
            URL url = new URL(urlStr);

            HttpURLConnection conn = (HttpURLConnection) url.openConnection(); #HTTPURLConnection 객체 만들기
            if (conn != null) {
                conn.setConnectTimeout(10000); # 연결 대기 시간 설정
                conn.setRequestMethod("GET");
                conn.setDoInput(true); # 객체의 입력이 가능하도록 만들어 준다. 
                # 응답 코드가 HTTP_OK인 경우 정상적인 응답이 온 경우이므로 응답으로 들어온 스트림을 문자열로 반환하여 반환한다.
                # 만약 요청한 주소의 페이지가 없는 경우에는 HTTP_NOT_FOUND 코드가 반환되며 이외에도 다양한 응답 코드가 정의되어 있다.

                int resCode = conn.getResponseCode();
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); # 입력 데이터를 받기 위한 Reader 객체 생성
                String line = null;
                while (true) {
                    line = reader.readLine();
                    if (line == null) {
                        break;
                    }

                    output.append(line + "\n");
                }
                reader.close();
                conn.disconnect();
            }
        } catch (Exception ex) {
            println("예외 발생함 : " + ex.toString());
        }

        println("응답 -> " + output.toString());
    }

    public void println(final String data) { # --- [3]
        handler.post(new Runnable() {
            @Override
            public void run() {
                textView.append(data + "\n");
            }
        });

    }

}

 

마지막으로 이 앱이 인터넷 권한을 사용하므로 manifest파일을 열고 다음 권한을 추가하고 <application>태그에 속성을 하나 더 추가한다.

 

입력한 사이트 주소 : www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=430156241533f1d058c603178cc3ca0e&targetDt=20120101

 

 

입력한 주소는 영화 정보를 받아올 수 있도록 오픈되어 있는 주소 중 하나로 이 주소 안에 들어있는 key값은 직접 이 사이트에 등록하여 발급받아야 하며 위의 실습의 key값은 단순히 테스트용이다. 응답 결과물의 내용도 매우  많은 것을 알 수 있는데 자세히 살펴보면 그 사이에 영화 정보가 포함되어 있는 것을 볼 수 있다. 

응답 결과물의 포맷은 JSON문자열이며 이 문자열을 어떻게 처리해야 하는지는 나중에 다시 살펴보도록 하자.

HttpURLConnection 클래스를 사용하여 웹페이지의 내용을 가져오는 요청 방식은 웹 서버에 접근하는 가장 기본적인 방식이라고 할 수 있다.

 

 

 



 

< 출처 및 참고 자료 >

Do it! 안드로이드 프로그래밍  ( 정재곤 박사 지음 /이지스 퍼블리싱 )

 

 

 

Comments