2020년 1월 8일 수요일

[Java] Java 소켓 통신 (NIO)


1. 요약
  1.1 Java는 blocking 소켓과 non-blocking 소켓을 제공한다.
  1.2 Blocking 소켓은 java.net 패키지에서 제공되고 쓰기 쉽다. 서버 입장에서 보면 멀티-쓰레딩 구조인데 동시 접속자가 적을 것으로 예상되는 소규모의 애플리케이션에선 이걸 써도 무방하다.
  1.3 Non-blocking 버전은 java 1.4 부터 지원되었고, java.nio 패키지에서 제공된다. NIO(New I/O?)라고 부른다. Blocking 소켓에 비해 적은 비용으로 많은 클라이언트의 요청을 처리할 수 있다.
  1.4 NIO의 ByteBuffer은 사용법이 좀 까다로워서 사용상 주의가 필요하다. Non-blocking 소켓을 써야만 하는 상황이라면 Netty framework이 더 나은 선택지일 수 있다. Netty는 조금 더 나은 성능에 더 쓰기 편한 'ByteBuf'를 제공한다.

2. Original Socket (Blocking)
  (클라이언트) 소켓 인스턴스 생성은 아래와 같이 간단하다. 블로킹 방식으로 동작하기 때문에 클라이언트 입장에서 사용하기가 쉽다.
  소켓 인스턴스가 서버에 연결되면 서버에 입력, 출력 스트림을 얻을 수 있다. 입력 스트림은 서버에서 데이터를 읽는 데 쓰이고, 출력 스트림은 서버에 데이터를 쓰는데 이용된다.
  InputStream와 OutputStream은 일반 스트림이기 때문에 사용하기 편한 형식으로 변환해 사용할 수 있다.

  서버 측도 어렵진 않다. port를 선택해 listen 하고, accept()를 호출하면 클라이언트 연결이 될 때까지 블로킹 된다. 클라이언트가 서버에 연결되면 accept() 메서드는 서버가 클라이언트와 통신할 수 있는 소켓을 반환한다. 이는 클라이언트에 사용된 것과 같은 소켓 클래스이므로 마찬가지 방식으로 데이터를 읽고 쓸 수 있다.

  서버는 클라이언트와 다르게 여러 클라이언트와 통신할 필요가 있으므로 accept()는 while 문 안에 놓고, 연결된 클라이언트가 있으면 이에 대한 핸들러는 별도의 쓰레드에서 동작시키는 구조로 로직을 작성하게 된다. 문제는 클라이언트가 많아질 때이다. 늘어나는 쓰레드는 서버에 부담을 준다. 물론 일반적인 규모의 프로그램에서는 굳이 non-blocking 소켓을 쓸 필요도 없고, 문제없이 동작할 것이다.

public class SocketServer extends Thread {
    private ServerSocket serverSocket;
    private int port;
    private volatile boolean running = false;

    public SocketServer(int port) {
        this.port = port;
    }

    public void startServer() {
        try {
            serverSocket = new ServerSocket(port);
            this.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void stopServer() {
        running = false;
        this.interrupt();
    }

    @Override
    public void run() {
        running = true;
        while (running) {
            try {
                System.out.println("Listening for a connection");
                Socket socket = serverSocket.accept();
                System.out.println("Client connected.");
                RequestHandler requestHandler = new RequestHandler(socket);
                requestHandler.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        int port = 12345;
        SocketServer server = new SocketServer(port);
        server.startServer();

        // Shutdown in 1 minute.
        try {
            Thread.sleep(60000);
        } catch(Exception e) {
            e.printStackTrace();
        }
        
        server.stopServer();
    }
}

class RequestHandler extends Thread {
    private Socket socket;
    
    RequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream());
            
            String line = in.readLine();
            while(line != null && line.length() > 0) {
                out.println("Received: " + line); // Echo message
                out.flush();
                line = in.readLine();
            }

            in.close();
            out.close();
            socket.close();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

2. New IO (NIO, non-blocking socket)
  non-blocking 버전으로 Java 1.4 이후로 도입되었다. java.nio 패키지는 다음과 같은 주요 기능을 제공한다.
 • 채널: NIO 버퍼에서 다른 NIO 버퍼로의 대량 전송을 지원하도록 디자인되었다.
 • 버퍼: 간단한 작업 집합으로 인터페이스 된 연속된 메모리 블록을 나타낸다.
 • non-blocking I/O: 파일, 소켓과 같은 일반적인 I/O 소스에 '채널'을 연결해주는 역할을 한다.

  Java에서 non-blocking 방식으로 통신하려면 우선 목적지(destination)에 대한 '채널'을 연 다음 '버퍼'를 이용해 서로 데이터를 주고 받아야 한다. 버퍼(java.nio.Buffer)엔 읽기/쓰기에 대한 위치(position), 버퍼의 고정 크기(capacity), 버퍼에 쓸 수 있는 데이터의 양(limit) 등의 속성이 있다. 주요 메서드론 clear(), flip(), rewind(), compact(), wrap() 등이 있는데 생각보다 까다로운 편. (Netty에선 쓰기 편하게 정리되어있다.)

  NIO에선 AsynchronousServerSocketChannel이 ServerSocket의 역할을 대신한다. 채널을 열기 위한 open() 메서드가 있고, 특정 포트와 연결하기 위한 bind() 메서드가 있다. 그리고 클라이언트의 연결을 허용하는 accept() 메서드로 구성된다.


public class NioSocketServer {
    public NioSocketServer() {
        try {
            final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(12345));

            listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
                @Override
                public void completed(AsynchronousSocketChannel ch, Void att) {
                    listener.accept(null, this); // Accept the next connection.

                    ch.write(ByteBuffer.wrap("Hello world\n".getBytes())); // Send hello message.

                    ByteBuffer byteBuffer = ByteBuffer.allocate(4096); // Create read buffer.
                    try {
                     boolean running = true;
                        int bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS); // Read, timeout 20sec.
                        
                        while (bytesRead != -1 && running) {
                            if (byteBuffer.position() > 2) {
                                byteBuffer.flip(); // Ready to read.

                                // Buffer data to string.
                                byte[] lineBytes = new byte[bytesRead];
                                byteBuffer.get(lineBytes, 0, bytesRead);
                                String line = new String(lineBytes);
                                System.out.println("ECHO: " + line);

                                ch.write(ByteBuffer.wrap(line.getBytes())); // ECHO

                                byteBuffer.clear();
                                bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS);
                            } else {
                                running = false;
                            }
                        }
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        ch.write(ByteBuffer.wrap("Good Bye\n".getBytes())); // Send timeout message.
                    }

                    try {
                        if (ch.isOpen()) {
                            ch.close();
                        }
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, Void att) {
                    //
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new NioSocketServer();
        System.out.println("Started.");
        
        try {
            Thread.sleep(60 * 1000);
        } catch(Exception e) {
            e.printStackTrace();
        }
        
        System.out.println("Aborted.");
    }
}


[Python] Python에서 언더스코어('_')가 갖는 의미.

Python을 쓰다보면 메서드나 변수의 앞과 뒤에 '_'나 '__'를 붙이는 경우를 자주 보게 된다. 값을 무시하는 용도로 쓰는 '_'에 대한 건 스킵하고, 이 글에선 이 두가지 경우에 대해서만 다루기로 한다..


1. Single Underscore
  '_' 를 변수나 메서드 앞에 붙이는 건 단지 Python에서 쓰이는 이름 짓기 규칙일 뿐이다. 이는 특정 변수나 메서드가 내부 용임을 다른 프로그래머에게 알려주는 힌트로서 존재한다. 다만 '*' 임포트를 사용할 경우 모듈에서 정의된 메서드를 private 처럼 동작하게 해주는데, '*' 임포트 방식은 알다시피 권장되는 방식이 아니다.

# my_module.py
def _internal_func():
    return 42

>>> from my_module import *
>>> _internal_func()
NameError: "name '_internal_func' is not defined"

>>> import my_module
>>> my_module._internal_func()
42

  '_' 가 변수의 끝에 붙는 경우는 변수의 이름 충돌을 피하기 위해 쓰이는 Python의 컨벤션이다.


2. Double Underscore, 던더
  '__'와 같이 언더스코어 두 개를 변수나 메서드 앞에 붙이는 경우가 있다. 이는 Python 인터프리터 동작을 바꾸어주므로 단순 컨벤션이 아니다. 이러한 이중 언더스코어 prefix는 Python 인터프리터가 속성 이름을 다시 생성하게 해준다.(맹글링이라 한다.) 속성 이름 앞에 클래스 이름을 붙여주는 것인데 서브 클래스와의 이름 충돌을 피하게 해주어 서브 클래스에서 변수가 대체되지 않도록 보호해준다.
(private을 위한 장치라 생각할 수도 있긴 하지만 부수적인 효과일 뿐이고 실제 목적은 그러한 것이 아니다.)

  마지막으로 '__'를 변수나 메서드 앞 뒤로 붙이는 경우가 있다. 이 경우엔 맹글링이 적용되지 않는다. 이는 특별한 문법적 기능이나 특별한 기능을 제공하기 위한 장치로 'magic' 또는 'special' 메서드라 불리운다. 대표적으로 '__init__' 이라든가 '__str__' 등이 있다.

2020년 1월 6일 월요일

[Java] 디렉터리 및 파일 삭제

아래의 코드는 사용자가 지정한 디렉터리 밑을 돌아다니면서 파일과 디렉터리를 삭제하는 코드다.
for 문 안에서 삭제하지 말아야 하는 패턴 같은걸 추가하면 더 안전하게 사용할 수 있다.

private void deleteFiles(String path) {
    File file = new File(path);
    if (file.exists() && file.isDirectory()) {
        File fileList[] = file.listFiles();
        if (fileList == null) {
            return;
        }
        
        for (int i = 0, length = fileList.length; i < length; i++) {
            if (fileList[i].isDirectory()) {
                deleteFiles(fileList[i].getAbsolutePath());
                
                fileList[i].delete(); // delete sub directory.
            } else if (fileList[i].isFile()) {
                fileList[i].delete();
            }
        }
        
        file.delete(); // delete main directory.
    }
}


2019년 12월 23일 월요일

[Python] Bar 차트 그리기.

matplotlib을 이용해 작성한 Bar 차트 그리기 코드.
데이터 목록에 대한 라벨과 값을 같이 표시해준다.

import numpy as np
import matplotlib.pyplot as plt

def draw_bar_chart(values, labels, subject, draw_mean=True, fig_size=(10, 6)):
    x = np.arange(len(values))
    y = values

    figure = plt.figure(figsize=fig_size)   
    barlist = plt.bar(x, y, 0.7, align='center')

    mean = np.mean(y)
    if draw_mean == True:
        plt.axhline(mean, color='coral', linestyle='dashed', linewidth=1)

    handles = []
    for index in np.arange(len(barlist)):
        if barlist[index].get_height() > mean:
            color = '#ec8482' # red like
        else:
            color = '#a4d8ee' # blue like
        barlist[index].set_color(color)
        handles.append(plt.Rectangle((0,0), 1, 1, color=color))

    plt.title(subject)
    plt.legend(handles, labels)
    plt.xticks(x, labels)

    for bar in barlist:
        height = bar.get_height()
        val = int(height)
        plt.text(bar.get_x() + bar.get_width() / 2.,
                 height + height*0.01,
                 val, ha='center', va='bottom')
    plt.show()

draw_bar_chart([1,2,4,6,7,7], ['v1','v2','v3','v4','v5','v6'], 'sample')

[펌] 데이터 분석으로 미래를 예측할 수 있을까

https://brunch.co.kr/@amangkim/35

아래는 개인적으로 추린 요약. 원문을 읽는게 당연히 좋다.

  "데이터를 다루는 사람은 많은 양의 데이터 수집을 통해 보다 정확한 미래를 예측할 수 있다고 믿는다. 하지만 데이터는 어찌됐던 과거의 산물이다. 데이터의 양이 많아진들 그 또한 과거의 것이기 때문에 미래를 직접 대변할 수는 없을 것이다. 많은 데이터 과학자들은 데이터를 이용해 미래를 예측하거나 영향력을 분석한다. 여기엔 큰 가정이 깔리는데 그 가정은 '과거의 사건들이 현재나 미래에도 재현된다.'라는 것이다. 모든 통계학의 예측 모델은 바로 이 재현성을 기반으로 한다. 그러나 미래는 재현되지 않는다. 쉽게 적으면 2017년 1월 1일은 1999년 1월 1일과 다른 것을 떠올리면 될 것이다. 비슷하게 보일 뿐 같은게 아니다. 통계를 통한 미래 예측은 과거를 기반으로 미래를 Simulation하는 것만이 가능할 뿐, 실질적인 미래를 예측할 수는 없을 것이다."

글쓴이는 예측과 패턴을 구분짓고 있다. 이 둘을 구분하는 기준은 시간의 영향력, 즉 재현성인데 시간의 영향력(시간에 따라 결과가 크게 달라진다면)이 크면 예측이 되고, 시간의 영향력이 없거나 작으면 패턴의 문제가 되는 것이다. 시간의 영향력이 작다는 것은 조건만 맞으면 같거나 아주 비슷한 결과가 재현 된다는 것을 뜻한다. 이건 충분히 가능하다는게 글쓴이의 주장이다. 즉, 데이터 분석의 목표로 잡아야 할 것은 미래의 예측이 아니라 과거 데이터로부터 패턴을 찾아내는 것이다. 한계점을 받아들이고 데이터를 기반으로 추측된 값이 갖는 의미와 속성을 파악하는 것은 틀림없이 유용한 도구가 된다.

무언가를 시도하기에 앞서 잊지 말아야 할 것은 데이터 과학의 기반이 되는 통계학, 확률론이 가지는 속성, 확률론의 모집합이 되는 수학이 갖는 속성을 되도록 바르게 파악하고 있어야 한다.

2019년 12월 22일 일요일

개발 분야와 관계없이 알아야 하는 기술 10가지

개발 분야와 관계없이 알아야 하는 기술 10가지 - 이기곤@수아랩

한빛 데브그라운드 주니어 2019년 발표 내용 정리.

1. 문자열 인코딩: 인코딩이란 컴퓨터가 문자를 이해할 수 있게 만든 규칙.
 - ASCII: ANSI에서 표준화한 7비트 부호체계. 영문 키보드로 입력할 수 있는 모든 기호들이 할당되어 있음. UTF-8과 호환, UTF-16과는 비호환.
 - EUC-KR: 완성형 한글 인코딩 방식 중 하나로 시작은 '가', 끝은 '힝'. 지원 글자 수는 2,350자로 현대에 쓰이는 한글 11,172자에 비해 부족. (Legacy, 결제 모듈 등에서 아직 쓰임.)
 - Unicode: 전 세계의 모든 문자를 다루도록 설계된 표준 문자 전산 처리 방식. Unicode 인코딩 방식엔 UTF-8, UTF-16, UTF-32가 있고, 일반적인 상황에선 UTF-8을 Unicode라 봐도 무방.
 - UTF-8: 사실상 표준으로 글자당 1~4 바이트를 쓰는 가변 길이 인코딩 방식. 1바이트 영역은 ASCII 코드와 하위 호환성을 갖음.
 - UTF-16: 2바이트 고정 인코딩 방식. 자바, 윈도우즈 API에서 쓰이는데 멀티바이트라고도 함. 호환성 목적으로 지금도 쓰임.
 - UTF-32: 4바이트 고정. 데이터 낭비가 심해서 사실상 쓰지 않지만 가변 길이를 고려하지 않아도 되는 장점 또한 존재해 프로그램 내부적으로 쓰이는 경우도 있음.

​2. Base64: 바이너리 데이터(이미지, 텍스트, 동영상)를 아스키 기반 문자열로 인코딩하는 방법. (Base64가 암호화는 아님.)
 - 문자 코드에 영향을 받지 않도록 공통 ASCII 영역의 문자만을 이용해 인코딩 함.
 - 64진법. 전자 메일이나 웹 등의 환경에서 바이너리 데이터를 전송하는데 쓰임. (RFC 1421, RFC 2045)
 - 바이너리 -> 텍스트 변환 간 일반적으로 33% 데이터 용량이 증가.

3. JSON: 구조화 텍스트 기반 데이터 규격. 정수, 실수, 텍스트를 규격화해서 실어 나르는 용도.
 - 장점: 범용적, 읽기 편함. 디버깅 용이. (JSON 자체가 JavaScript Object Notation인 만큼 JavaScript 언어와 궁합이 아주 좋다.)
 - 단점: 고용량, 처리 비용이 높음. (일반적인 사용 사례에선 무시해도 되는 수준이나 Latency가 중요한 게임, 금융 도메인에선 사용에 제약.)
 - JSON 규격 변경에서 오는 결함 가능성을 염두하고 사용해야 함.
 - Latency가 중요한 경우엔 Google Protocol Buffer 또는 Apache Thrift 사용이 권장됨.

​4. 다국어 처리: 프로그램 수정없이 여러 언어를 지원하는 기법.
 - 리소스 파일 방식, 언어 설정 방식, 프로그램 방식(하드코딩)으로 대응할 수 있음.
 - 일반적으론 Key:Value 기준으로 타깃 언어를 획득하는 방안.
 - Python엔 gettext 패키지, 안드로이드, C# 등의 언어는 프레임워크 레벨에서 지원됨. 표준은 i18N

​5. 날짜와 시간
 - 데이터를 주고 받는 상호간 타임존이 서로 다르면 문제가 발생함. 타임존 기준은 UTC, 한국은 UTC+9. 하나의 시스템은 하나의 타임존을 쓰도록 하는 것이 권장됨.
 - Apache Zookeeper 같은 분산 코디네이션 툴을 쓰면 특정 시스템의 타임존 변화를 감지할 수도 있음. 또는 주기적으로 상호간 타임존을 확인하고 맞추는 프로토콜을 반영하는 것도 고려할 수 있음.
 - 시간엔 단조 시간과 실제 시간(벽 시계 시간, wall time)이 있음.
 - 단조 시간? 운영체제나 CPU가 계산하는 시간으로 운영체제 시작 이후 종료까지 절대 바뀌지 않는 시간을 말함. 단조 시간은 컴퓨터마다 독립적.

​6. 정규표현식 (Regex): 주어진 문자열 속에서 특정 패턴을 찾고자 할 때 쓰는 기술.
 - 비밀번호 패턴에 숫자 1개, 영어 소문자 1개, 영어 대문자 1개 포함 조건 같은걸 적용한다면 정규 표현식으로 걸러낼 수 있음.
 - 정규표현식은 regex101.com과 같은 사이트에서 미리 검증해볼 수 있음.

​7. UUID: 범용 고유 식별자. 네트워크 상에서 서로 모르는 개체를 식별하기 위한 기술.
 - 4개의 하이픈과 32개의 16진수 문자로 구성. 하이픈을 빼면 저장하는데 16바이트가 필요함.
 - UUID v4는 중앙 관리 시스템에서 고유 ID를 할당받는 방식이 아니라 개발 주체가 스스로 식별자 이름을 짓는 방식임.
 - 중앙의 서버 없이 Object, 작업, 요청 등에 ID를 메기고 싶은 경우 UUID를 쓰면 된다.
 - 340,282,366,920,938,463,463,374,607,431,768,211,456개의 경우의 수를 갖기 때문에 사실상 중복은 없다고 인정됨.
 - UUID에도 충돌은 있을 수 있으므로 신뢰성이 중요한 시스템에선 UUID Pool(UUID 십만개, 백만개를 먼저 생성한 뒤 그 안에서만 쓰도록)을 고려해야 함.

​8. Random: 게임 규칙, 랜덤 박스, 암호화키, UUID 등을 만드는데 쓰임.
 - 쓰임에 따라 유사난수와 완전 난수를 구분해야 함. 우리가 쓰고 있는 대부분의 난수는 유사 난수임.
 - 유사난수는 난수를 흉내낸 난수로, SEED 값을 주면 SEED 값 기준으로 일련의 숫자를 만들어낸다. 완전한 난수에 비해 연산속도가 빠름.
 - 게임에서 유사난수 같은걸 쓰면 특정 시간대의 아이템 드랍률을 미리 알 수도 있으므로 문제가 된다.
 - 완전 난수는 암호학적으로 안전. 유사 난수에 비해 3~4배 느림. 노이즈를 이용해 난수를 만들어내므로 예측 불가. 인증키 생성, 게임 아이템 생성 등에 쓰인다.

​9. 해쉬함수: 임의의 값을 고정길이 값으로 변환하는 알고리즘.
 - 특정 해시 알고리즘은 입력값, 입력길이와 무관하게 항상 같은 길이의 문자열을 반환한다.
 - 해시 맵 같은데 쓰인다. 해시 충돌이 있긴 하지만 가능성은 매우 낮으며 접근 비용이 O(1)이므로 매력적.
 - 민감한 데이터의 변조를 막는데 쓸 수 있다. 암호를 해시화해서 저장하는데 보안 향상을 위해 Salt가 추가됨.
 - SHA-256 이상을 쓰는것이 안전하지만 MurmurHash의 경우 안전하진 않아도 성능이 매우 좋아 Redis같은 곳에서 쓰인다.

​10. HTTP(S): 웹의 기반 기술. 텍스트 기반의 통신.
 - HTTP의 주요 성질은 Stateless. 상대가 살아있는지 요청하기 전엔 알 수 없다.
 - HTTP는 기본적으로 1개의 요청당 1개의 소켓을 사용. 요청 전 연결하고 데이터를 주고받은 후 연결을 끊음.
 - Stateless 성질을 보완하기 위해 Cookie, Session 등을 쓴다. (또는 웹소켓, HTTP/2를 쓰면 연결 중단 없이 데이터 송수신 가능)
 - Header에 주요 정보가 담긴다. (User-Agent, Content-Type, Accept ...)
 - HTTPS = HTTP + TLS, HTTPS의 암호화 수준은 TLS 버전에 따른다. 서버의 경우 인증된 제3자가 발급한 인증서가 필요.
 - 분산 환경에선 스티키 세션(처음 연결된 서버와만 통신되게끔 하는 개념), CORS(서로 다른 도메인간 요청을 허용해 주는 것.)에 대해 알아둘 필요 있음.

2019년 12월 18일 수요일

Base 64 인코딩 (RFC 1421, RFC 2045)

https://tools.ietf.org/html/rfc2045#page-24

바이너리 데이터를 문자 코드에 영향 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 문자열로 바꾸는 인코딩 방식. 64진법으로, 이는 2의 제곱수들에 기반한 진법들 중에서 화면에 표시되는 ASCII 문자들을 써서 표현할 수 있는 가장 큰 진법이다. 다음 제곱수인 128진법에는 128개의 기호가 필요한데 화면에 표시되는 ASCII 문자들은 128개가 되지 않는다. 이러한 까닭으로 Base 64 인코딩은 전자 메일이나 웹 등의 환경에서 바이너리 데이터를 전송하는데 쓰인다.

일반적으로 Base 64 인코딩은 62개의 'A-Z, a-z, 0-9'를 쓰며 62번째엔 '+', 63번째엔 '/'를 쓴다. 그리고 패딩이나 문자열의 끝을 처리하기 위한 문자로 '='를 쓴다. 이렇게 했을 때 데이터 전송량이 약 33% 정도 늘어나는 단점이 있지만 딱히 대안이 없다.

ASCII로 인코딩해서 바로 전송하는게 더 낫지 않느냐는 의문이 생길 수 있는데 시스템 별로 ASCII를 처리하는 방식에 차이가 있고 ASCII는 여러 제어 문자를 포함하고 있기 때문에 이기종 간 데이터 교환으로부터 안전(적합)하지 않다.