본문 바로가기

국비 교육

2020.9.25 일자 수업 : JSON, 네트워크

 실습 - JSON 형식 

 

git/eomcs-java-project-2020/mini-pms-32

다양한 언어와 플랫폼에서 모두 호환되는 데이터 형식은 xml과 JSON 이 있다. 다만 xml는 메타데이터의 비중이 크기 때문에 메모리 낭비가 심하다. 따라서 JSON 형식에 따라 데이터 파일을 만들어보려고 한다.

 

JSON이란?

  • 속성-값 또는 키-값 으로 된 데이터 객체를 텍스트로 표현하는 개방형 표준 데이터 포맷이다. 예를 들어 다음과 같은 형식을 취한다.
    {속성:값, 속성:값, ...}
{"no":1,"name":"1","email":"1","password":"1","photo":"1","tel":"1"}
  • 텍스트 형식이기 때문에 프로그래밍 언어나 운영체제에 영향을 받지 않는다.
  • 바이너리 방식에 비해 데이터 커지는 문제가 있지만 모든 프로그래밍 언어에서 다룰 수 있다는 장점이 있다.
  • 인터넷 상에서 애플리케이션 간에 데이터를 주고 받을 때 주로 사용한다.
  • 특히 이기종 플랫폼(OS, 프로그래밍 언어 등) 간에 데이터를 교환할 때 유용하다.
  • JSON 공식 홈인 https://www.json.org 사이트에 자세한 내용이 있다.

JSON 라이브러리이란?

  • JSON 데이터 포맷을 다루는 라이브러리다.
  • JSON 홈페이지에 다양한 프로그래밍 언어에서 사용할 수 있는 라이브러리를 소개한다. (홈페이지 가장 밑에 소개되어있다)

GSON이란?

  • 구글에서 제공하는 JSON 자바 라이브러리다.
  • 자바 체를 JSON 형식의 텍스트 변환하는 기능을 제공한다.
  • JSON 형식의 텍스트를 자바 객체로 변환하는 기능을 제공한다.
  • 자세한 내용과 임포트 방법은 여기를 참고할 수 있다.
    search.maven.org/artifact/com.google.code.gson/gson/2.8.6/jar

 

훈련 목표

  • GSON 자바 라이브러리를 프로젝트에 임포트한다.
  • save/load 메서드에서 Gson 객체를 생성하여 도메인 객체들의 Collection <-> Json 포맷의 String 변환하는데 사용한다.
  • 각 파일을 .csv 파일이 아니라 .json 파일로 바꿔주고 수정한 메서드를 호출한다.

1단계 : build.gradle 파일에서 다음과 같이 한 줄을 추가한다.
implementation 'com.google.code.gson:gson:2.8.6'

dependencies {
    // This dependency is used by the application.
    implementation 'com.google.guava:guava:29.0-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.13'

    implementation 'com.google.code.gson:gson:2.8.6'
}

2단계 : 터미널에서 해당 프로젝트 파일로 가서 gradle eclipse를 실행한 후, 이클립스에서 프로젝트를 refresh 하면, JRE System. Library에 gson 라이브러리가 추가된 것을 확인할 수 있다.

 

3단계 : gson 라이브러리 임포트를 완료헀다면, 기존에 csv 포맷의 파일에 직접 FileWriter를 호출하여 String을 출력해주던 saveObjects를 다음과 같이 수정한다.

  • Gson 객체를 생성한다.
  • 도메인 객체가 들어있는 리스트를 파라미터로 주어, toJson을 호출하면, 리스트 안의 객체들을 모두 json 포맷의 문자열로 변환한다.
  • 변환된 문자열을 BufferedWriter를 통해 출력한다.
  • 버퍼에 남은 데이터까지 모두 출력되도록 flush를 호출한다.
  private static <T extends CsvObject> void saveObjects(Collection<T> list, File file) {
    BufferedWriter out = null;
    try {
      out = new BufferedWriter(new FileWriter(file));

      Gson gson = new Gson();
      String Json = gson.toJson(list);
      out.write(Json);

      out.flush();

      System.out.printf("총 %d 개의 객체를 '%s' 파일에 저장했습니다.\n", list.size(), file.getName());

    } catch (IOException e) {
      System.out.printf("객체를 '%s' 파일에 쓰기 중 오류 발생! - %s\n", file.getName(), e.getMessage());

    } finally {
      try {
        out.close();
      } catch (IOException e) {
      }
    }
  }

4단계 : 이렇게 메서드를 바꾸고 나면 App 클래스의 스태틱 필드로 지정되어있던 파일 객체들의 경로를 .csv 파일에서 .json으로 변경한다.

  static File boardFile = new File("./board.json"); // 게시글을 저장할 파일 정보

  static File memberFile = new File("./member.json"); // 회원을 저장할 파일 정보

  static File projectFile = new File("./project.json"); // 프로젝트를 저장할 파일 정보

  static File taskFile = new File("./task.json"); // 작업을 저장할 파일 정보

5단계 : loadObjects도 다음과 같이 수정한다.

  • StringBuilder 객체를 생성하여 json 파일에 저장된 모든 데이터를 입력받아 저장한다.
  • Gson 객체를 생성한다.
  • 데이터를 저장한 StringBuilder 객체를 String 으로 변환해준 것과 변환받고자하는 도메인 객체의 배열에 대한 클래스 정보를 파라미터로 주어, fromJson을 호출하면 각 객체들의 배열을 만들어 리턴한다.
  • 배열에 있는 도메인 객체들을 하나씩 꺼내어 객체를 담아야할 Collection에 담아준다.
  private static <T> void loadObjects(Collection<T> list, File file, Class<T[]> clazz) {
    BufferedReader in = null;

    try {
      in = new BufferedReader(new FileReader(file));
      
      StringBuilder strBuilder = new StringBuilder();
      int b = 0;
      while ((b = in.read()) != -1) {
        strBuilder.append((char) b);
      }
      
      Gson gson = new Gson();
      T[] arr = gson.fromJson(strBuilder.toString(), clazz);
      for (T obj : arr)
        list.add(obj);
      
      System.out.printf("'%s' 파일에서 총 %d 개의 객체를 로딩했습니다.\n", file.getName(), list.size());

    } catch (Exception e) {
      System.out.printf("'%s' 파일에 객체를 읽기 중 오류 발생! - %s\n", file.getName(), e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

loadObejcts를 다음과 같이 수정할 수도 있다.

  • Gson 객체를 생성한다.
  • 파일 입력 도구와 입력받고자하는 도메인 객체 배열의 클래스 정보를 파라미터로 주고, fromJson을 호출하면 도메인 객체들의 배열을 리턴한다.
  • 리턴받은 배열에서 객체를 하나씩 꺼내서 list에 추가한다.
  private static <T> void loadObjects(Collection<T> list, File file, Class<T[]> clazz) {
    BufferedReader in = null;

    try {
      in = new BufferedReader(new FileReader(file));
      
      Gson gson = new Gson();
      T[] arr = gson.fromJson(in, clazz);
      for (T obj : arr)
        list.add(obj);
      
      System.out.printf("'%s' 파일에서 총 %d 개의 객체를 로딩했습니다.\n", file.getName(), list.size());

    } catch (Exception e) {
      System.out.printf("'%s' 파일에 객체를 읽기 중 오류 발생! - %s\n", file.getName(), e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

다음과 같이 한줄로 줄이는 방법도 있다.

  • Gson 객체를 생성하여 위의 방법처럼 fromJson을 호출하여 배열을 리턴받는다.
  • Arrays 클래스의 asList를 호출하여 T[]배열을 List<T> 객체로 변환한다.
  • addAll을 통해 List<T>에 있는 모든 객체를 한번에 list에 추가한다.
  private static <T> void loadObjects(Collection<T> list, File file, Class<T[]> clazz) {
    BufferedReader in = null;

    try {
      in = new BufferedReader(new FileReader(file));
      
      list.addAll(Arrays.asList(new Gson().fromJson(in, clazz)));
      
      System.out.printf("'%s' 파일에서 총 %d 개의 객체를 로딩했습니다.\n", file.getName(), list.size());

    } catch (Exception e) {
      System.out.printf("'%s' 파일에 객체를 읽기 중 오류 발생! - %s\n", file.getName(), e.getMessage());
    } finally {
      try {
        in.close();
      } catch (Exception e) {
      }
    }
  }

6단계 : save / load 메서드를 위와 같이 수정해준 후 수정 사항에 알맞게 메서드를 호출해준다.

특히 loadObjects 의 세번째 파라미터로 도메인 객체의 클래스 정보를 담은 필드를 넘겨주어야 한다.

public class App {
  public static void main(String[] args) {
  
    loadObjects(boardList, boardFile, Board[].class);
    loadObjects(memberList, memberFile, Member[].class);
    loadObjects(projectList, projectFile, Project[].class);
    loadObjects(taskList, taskFile, Task[].class);
    .
    .
    .
    saveObjects(boardList, boardFile);
    saveObjects(memberList, memberFile);
    saveObjects(projectList, projectFile);
    saveObjects(taskList, taskFile);
  }
}

 네트워크 

 git/eomcs-java-basic/src/main com.eomcs.net.ex02.Client0110
 git/eomcs-java-basic/src/main com.eomcs.net.ex02.Server0110

클라이언트(client)와 서버(server)

  • 클라이언트 (client) : 연결을 요청하는 프로그램

  • 서버 (server) :  연결 요청을 받는 프로그램

  • 소켓 (Socket) : 네트워크를 경유하는 프로세스 간 통신의 접속점.
    소켓을 통해 클라이언트와 서버 프로그램 사이에 데이터를 송수신할 수 있다.
    예를 들어 택배를 보낸다고 했을 때, 상자에 물건을 넣고 인적사항과 주소를 적어야 하는 역할이 소켓의 역할이라 할수 있다.

  • IP 주소(Internet Protocol address) : 컴퓨터 네트워크에서 장치들이 서로를 인식하고 통신을 하기 위해서 사용하는 특수한 번호

  • 포트 번호 (Port Number) : Ip주소를 통해 데이터를 전송할 상대 컴퓨터까지 도달한 후, 데이터를 받을 프로세스를 구별하기 위한 식별자.

클라이언트와 서버가 소켓을 통해 연결되는 과정

  • 서버 소켓클라이언트 소켓을 만든다.(socket())

  • 서버 소켓로컬 IP를 가지고 포트를 열고(bind()) 클라이언트 연결을 기다린다.(listen())

  • 클라이언트 소켓IP 주소를 이용해 목적지 호스트를 찾아내고 포트를 이용해 통신 접속점을 찾아내서 연결을 만든다.(connect())

  • 서버 소켓이 연결을 수락하면(accept()), 포트를 이용해 데이터를 주고 받는다.

    (send(), recv())

출처 : https://qlyh8.tistory.com/86

클라이언트

  • 클라이언트가 서버에 연결 요청을 하기 위해서는 Socket 객체가 필요하다.
  • Socket 객체를 생성하는 생성자에는 로컬 컴퓨터의 IP주소 또는 도메인 이름이 필요하며, 또한 서버가 갖는 포트번호가 필요하다. 서버와의 연결이 이루어져야만 Socket 객체가 리턴된다.
  • 연결이 실패하면 에러를 띄운다.
  • 서버와의 연결을 끊으려면 close()를 호출해주면 된다. 작업이 끝난 후에는 서버측에서 해당 클라이언트와 연결하는 동안 사용한 자원을 다른 클라이언트에 빠르게 사용할 수 있도록 바로바로 close()를 호출해주는 것이 좋다.
import java.net.Socket;

public class Client0110 {

  public static void main(String[] args) throws Exception {
    Socket socket = new Socket("localhost", 8888);
    System.out.println("서버와 연결되었음!");

    socket.close();
    System.out.println("서버와 연결을 끊었음!");
  }
}

서버

  • 네트워크 연결을 기다리는 역할을 수행할 ServerSocket 객체를 준비한다.
  • ServerSocket 객체의 생성자를 호출할 때에는 파라미터로 이 프로그램의 포트번호를 결정하여 넣어주면 OS는 이 번호를 갖고 데이터를 받을 프로그램을 결정할 것이다.
  • 포트 번호를 지정할 때에는 다른 프로그램에서 사용하고 있는 포트 번호를 사용하면 안되는데, 자주 사용되는 포트 번호가 있으므로 그런 포트 번호는 피해서 지정해야 한다.
  • 실행에 시동을 걸 수 있도록 Scanner.nextLine()를 호출하여 실행 중에 사용자가 엔터를 치기 전까지 다음 코드를 실행하지 못하도록 한다.
  • close()를 호출하여 서버를 종료시킨다.

포트 번호(0~65535, 2바이트)

  • 0~1023(well-known port)
    특정 프로그램이 관습적으로 사용하는 포트 번호로 가능한 이 포트 번호를 피해야 한다.
    ex - 7(echo), 20(FTP 데이터 포트), 21(FTP 제어포트), 23(telnet), 25(SMTP), 53(DNS), 80(HTTP), 110(POP3), 143(IMAP) 등
  • 1024 ~ 49151 (registered port)
    일반적인 통신 프로그램을 작성할 때 이 범위 포트 번호를 사용한다. 다만 이 범위에서도 특정 프로그램이 널리 사용하는 번호가 있다.
    ex - 8080(proxy), 1521(Oracle), 3306(MySQL) 등의 번호를 피해야 한다.
  • 49152 ~ 65535 (dynamic port)
    이 범위는 클라이언트가 OS로부터 자동 발급 받는 포트 번호이다.
import java.net.ServerSocket;
import java.util.Scanner;

public class Server0110 {
  public static void main(String[] args) throws Exception {
    Scanner keyboard = new Scanner(System.in);

    System.out.println("서버 실행!");
    ServerSocket ss = new ServerSocket(8888);

    System.out.println("클라이언트 연결을 기다리는 중...");

    keyboard.nextLine();

    ss.close();
    System.out.println("서버 종료!");

    keyboard.close();
  }
}