2020년 8월 12일 수요일

matplotlib 한글 깨지지 않게..

rc를 임포트한 다음에 한글이 지원되는 폰트의 이름을 지정해 주면 된다.
한글 윈도우 사용자라면 malgun.ttf는 있을거란 가정이 깔린다.

import matplotlib.pyplot as plt
from matplotlib import font_manager, rc

font_name = font_manager.FontProperties(fname="C:/Windows/Fonts/malgun.ttf").get_name()
rc('font', family=font_name)

x = list(range(1,11))
y = [a * 2 for a in range(0, 10)]

l = plt.plot(x, y, 'ro', label='빨강 닷')
plt.setp(l, markersize=4)
#plt.setp(l, markerfacecolor='C0')
plt.xlabel('X-축')
plt.ylabel('Y-축')
plt.title('차트 제목')
plt.legend(loc='best')
# plt.savefig('fig.pdf') # to pdf
plt.show()


https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.rc.html


log4jdbc-remix 사용법

https://code.google.com/archive/p/log4jdbc-remix/


프로젝트는 2013년 11월 중단된 상태고, 앞으론 log4jdbc-log4j2가 log4jdbc-remix(이건 2012년 4월에 시작된 프로젝트)를 대체한다고 한다. 


log4jdbc-remix의 주 기능은 쿼리문과 쿼리 결과를 보기 좋게 로그로 기록해 주는 것이다. 데이터베이스에 요청한 쿼리문이 의도와 맞게 작성되었는지 확인하는 용도로 활용할 때 좋다. (콘솔에 찍어보는 용도)


이건 Java 6 이상 버전에서 사용 가능하고, 제공되는 Proxy에서 기존에 정의해둔 DataSource를 참조하는 설정 방식을 따르기 때문에 사용하기 편리하다.


안정화 릴리즈 버전은 0.2.7이고, maven 기준 다음과 같이 끌어다 쓸 수 있다.

<dependency>

    <groupId>org.lazyluke</groupId>

    <artifactId>log4jdbc-remix</artifactId>

    <version>0.2.7</version>

</dependency>


log4j.xml엔 jdbc.sqlonly, jdbc.resultset 이름으로 로거 설정을 등록해주면 된다.

jdbc.sqlonly는 쿼리문을 기록하는 용도이고, jdbc.resultset은 쿼리 결과를 기록하는 용도이다.


프록시 빈 등록은 인터넷에 많이 돌아다니는 걸 그대로 쓰면 된다.

<bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource">

        <constructor-arg ref="dataSourceSpied" />

        <property name="logFormatter">

            <bean class="net.sf.log4jdbc.tools.Log4JdbcCustomFormatter">

                <property name="loggingType" value="MULTI_LINE" />

                <property name="sqlPrefix" value="SQL         :  "/>

            </bean>

        </property>

 </bean>




2020년 8월 10일 월요일

[펌] 비트겐슈타인이 남긴 명언

 

1889. 4. 26 빈~ 1951. 4. 29 잉글랜드 케임브리지셔 케임브리지. 오스트리아 태생 영국의 철학자. 1925~50년 영국 철학계에서 가장 영향력 있는 철학자 중 한 사람이었으며, 논리학 이론과 언어철학에 관한 독창적이며 중요한 철학적 사유체계를 제시했다.


1. 쓸모없는 문제에 관여하지 마라.

2. 말할 수 없는 것에 관해서는 침묵해야 한다.

3. 언어는 만물의 척도다.

4. 생각도 일종의 언어이다.

5. 말에는 음악이 깃들어 있다.

6. 내 언어의 한계는 내 세계의 한계를 의미한다.

7. 우리는 남에게 자신의 속마음을 감추고 싶어한다. 인간의 마음이란 아름답게만 간직되는 것은 아니기 때문이다.

8. 두려움이 아니라, 두려움의 극복이 칭찬받을만한 것이고, 인생을 보람차게 만든다.

9. 마음속 용기야말로 처음에는 겨자씨처럼 작아도 점점 성장해서 거목이 되는 것이다.

10. 어떤 돌이 전혀 움직이지 않고, 도저히 손을 쓸 방도가 없다면 먼저 주변의 돌부터 움직여라.

11. 문제를 해결하는 힘은 새로운 정보를 얻는데서 오는 것이 아니라, 이미 오래전부터 알고 있던 것을 체계적으로 정리하는데서 온다.

12. 철학자란 건강한 인식을 얻기 위해서 자기안에 박혀있는 다양한 사고의 오류를 고쳐야 하는 사람이다.

13. 반대되는 결론도 항상 함께 생각하라.

14. 오늘날 우리의 교육은 고뇌하고 인내하는 능력을 누르는 방향으로 흐르고 있다.

15. 생활이 자꾸만 변화하는 것은 인생에 있어서 가장 기본적인 일이다. 물론, 그것은 습관에 있어서도 마찬가지다.

16. 나는 왜 우리가 여기에 있는지 그 이유를 알지 못한다. 그러나, 나는 우리가 단지 즐기기 위해서 여기에 있는 것은 아니라는 그 사실만큼은 확신한다.

17. 자신과 아무런 상관이 없는 문제에 자신을 끌어들이지 않는 것은 철학자의 주요한 기술중 하나이다.

18. 자아성찰은 내 삶의 새로운 한 부분이어야 한다.

19. 의심은 믿음 이후에 온다.

20. 확실하다는 말로써 우리는 완전한 확신, 의심의 부재를 나타내며, 또한 그것으로 다른 사람들을 설득시키고자 한다. 하지만, 우리의 그 믿음은 주관적 확실성임을 알아야한다.

21. 인간의 몸은 인간의 정신을 표현하는 가장 훌륭한 그림이다.


22. 너무 많이 아는 사람이 거짓말을 하지 않기란 어렵다.

23. 인생이 견딜 수 없게 되었을때, 우리는 상황이 변화할 것을 기대한다. 그러나, 가장 중요하고 가장 효과적인 변화, 즉 자기자신의 태도를 바꿔야한다는 인식에는 거의 생각이 미치지 못한다.

24. 사물의 가장 중요한 측면은 그것이 너무나도 단순하고 친숙한 것이기 때문에, 우리의 눈길을 끌지 못한다. 따라서, 가장 기본적으로 탐구해야 하는 것은 그냥 스쳐가는 것 중에 있다.

25. 나는 구두점을 많이 써서 읽는 속도를 늦춰보려고 하는 편이다. 내가 쓴 글이 천천히 읽혀지기를 희망하기 때문이다. 나 자신이 읽는 것처럼 ...

26. 철학적 탐구는 인간생활에 보탬이 되는 쪽으로 국한되어야 한다.


2020년 7월 29일 수요일

포카 요케 (poka-yoke)

품질 관리의 측면에서 실수를 방지하도록 행동을 제한하거나 정확한 동작을 수행하게끔 하도록 강제하는 여러 가지 제한점을 만들어 실패를 방지하는 방법을 말하는 용어. 토요타의 시게오 신고에 의해 처음으로 고안됐으며, 실수를 피하다라는 뜻의 일본어에서 왔다.
... 인지 심리학 및 인간의 컴퓨터 상호작용 분야에서도 이를 이용하면 여러 가지 실수를 방지하는 디자인 원리의 하나로 받아들여져서 이용하고 있다.
[위키피디어]

생활에서의 포카 요케 예
- 자동차 기어가 "P"에 있지 않으면 시동이 걸리지 않도록 함.
- USB 장치를 꼽을 때 뒤집어서 꼽으면 작동하지 않음.
- 문이 닫히기 전에 전자 레인지가 작동하지 않음.

이 개념을 제안한 사람은 토요타 생산 시스템의 개척자 중 한 명인 시게오 신고로 '제로 결함'이라는 아이디어를 제안한 품질 전문가였다고 한다.

실수가 발생하지 않도록 끊임없이 노력한다는 철학이 포함된다. 최종 제품의 품질이 올라가려면 연결된 모든 프로세스에서 일정 수준 이상의 품질이 제공되어야 한다. 따라서 결함이 후속 프로세스로 이어지는 것을 방지해야 한다. (완제품 상태에서 결함이 발견되는 것이 가장 나쁜 케이스)

실수 방지 목표를 달성하는데 도움이 되는 특징은 다음과 같다.
- 단순함 : 솔루션이 복잡하지 않고 오류 발생 문제를 명확히 해결해줄 수 있어야 한다.
- 자동화 : 사람의 추가 개입이 없으면 좋다.
- 즉각적인 피드백 : 조건이 사양을 벗어난 경우 즉시 피드백을 해주면 좋다.

-
아예 실수를 할 수 없게끔 하면 사용자가 제품이나 프로세스에 대해 생각할 필요가 없을 것이다.
또는 프로세스를 벗어나는 경우 피드백을 줘서 프로세스 안에서만 행동이 있게끔 유도할 수 있다. 
(정해진 순서 또는 경우의 수 내에서만 행동을 제한하는 것.)

2020년 7월 21일 화요일

입출력 다중화 (파일 디스크립터, select)

입출력 다중화는 단일 프로세스에서 여러 개의 파일을 제어할 수 있도록 해준다. 입출력 다중화는 여러 개의 파일을 다루기 위해서, 파일 기술자(FD, File Descriptor)를 배열(그룹)로 관리한다. 데이터 변경을 감시할 파일 기술자를 배열에 포함시키고, 배열에 포함된 파일 기술자에 변경이 발생하면 파일 기술자에 대응하는 배열에 표시하는 방식이다. 사용자는 파일 기술자 배열의 값을 검사(FD_ISSET)하는 방식으로 여러 개의 파일을 처리할 수 있게 된다. (일반적으로 파일 기술자 테이블의 크기는 1024라 한다. 이는 프로세스가 열 수 있는 파일의 최대 개수, 파일 기술자 테이블의 크기로부터 영향을 받는다.)

• fd_set 구조체
typedef struct
{
    int fds_bits[32];
} fd_set;

변경의 확인은 이벤트 기반이 아니므로, 배열에 있는 모든 값을 전부 검사해야 하는데 1000개의 클라이언트가 연결되어 있다고 가정할 경우 1000개의 필드를 모두 검사해야 한다. 이 방식은 병렬 처리가 아니기 때문에 주의할 필요가 있는데 서버가 하나의 클라이언트를 처리하는 동안 다른 클라이언트는 대기해야 한다. 따라서 상대적으로 데이터 처리 과정이 짧은 서비스에 적합한 방식이라 볼 수 있다.

변경에 대한 확인은 select() 함수를 통하는데 select 함수는 데이터가 변경된 파일의 개수를 반환한다. 데이터가 변경된 파일의 목록을 반환하는 것이 아니기 때문에 사전에 연결된 파일의 번호를 가지고 있지 않으면 배열의 개수만큼 루프를 돌아야 할 수도 있다. (참고로 파일 디스크립터는 0부터 채번되고, 프로세스에서 사용하지 않는 가장 작은 값을 할당한다. 일반적으로 0번은 stdin, 1번은 stdout, 2번은 stderr.)

• FD_ZERO : fd_set 구조체를 초기화 한다. (모든 비트 값을 0으로 세팅)
fd_set rfds;
FD_ZERO(&rfds); // memset(&rfds, 0, sizeof(fd_set));

• FD_SET : 특정 파일 디스크립터를 fd_set 변수에 추가할 때 사용한다.
int fd_socket = socket(AF_INET, SOCK_STREAM, 0);
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(fd_socket, &rfds);

• FD_ISSET : fd_set 변수의 특정 파일 디스크립터를 가리키는 비트가 1인지 확인한다.

• select() : fd_set 변수에서 1로 세팅된 파일 디스크립터에서 이벤트가 발생하면 이를 감지한다.
fd_set rfds, _fds;

FD_ZERO(&rfds);

FD_SET(1, &rfds);
FD_SET(3, &rfds);

max_fd = 3;

while (1) {
    _fds = rfds; // select() 후 fd_set 변수의 내용이 변경되므로 매번 복사본을 전달.
  
    select(max_fd + 1, &_fds, NULL, NULL, NULL); // 1, 3 값을 갖는(FD_SET으로 설정된) 파일 디스크립터의 이벤트를 감시한다.

    // ...
}

* 위 내용에서 '파일'은 '소켓'을 포함한다. (과거 시스템에서 소켓, 장치를 모두 파일의 개념으로 관리했었던 것에서 유래.)

참고 사이트: https://www.joinc.co.kr/w/Site/system_programing/File/select

2020년 6월 17일 수요일

ZeroMQ 기본 개념 - 2

• ZeroMQ의 Socket API

ZeroMQ는 새로운 개념을 욱여넣지 않고, 친숙한 소켓 기반 API 이름을 따서 기능을 제공하고 있다. 사용자는 친숙한 이름의 API를 따르되 '분산 소프트웨어를 디자인하고 구현하고 있다.'라는 생각을 갖고 ZeroMQ를 대할 필요가 있다.

ZeroMQ에선 accept() API가 없으며 네트워크 연결은 백그라운드에서 진행되고, 일시적으로 연결이 끊어지더라도 ZeroMQ가 자동으로 다시 연결해준다. 하나 더 기억할 것은 ZeroMQ의 zme_send()의 실제 동작이다. 이 메서드는 메시지를 연결된 소켓으로 바로 전달하는 일을 하지 않고 실제론 메시지를 큐에 넣는 일을 한다. (사용자는 인지하지 않아도 되는) I/O 담당 쓰레드가 비동기적으로 큐에 쌓인 메시지를 실제 연결된 소켓으로 전달하는 일을 할 것이다.

대부분의 유스케이스는 TCP 방식으로 커버가 된다. ZeroMQ에서는 TCP를 'disconnected TCP' 전송 방식이라고도 표현을 하는데 이는 엔드 포인트가 연결이 되지 않아도 작동하기 때문에 이름지어진 표현이다. 클라이언트는 엔드포인트에 바인드된 서버가 없더라도 별다른 오류 없이 connect 작업을 할 수 있는 것은 ZeroMQ가 갖는 특징이다.

inproc이라는 방식의 연결도 제공하는데 이는 tcp, ipc 전송 방식보다 빠르게 작동한다. 다만 클라이언트 연결전에 반드시 서버가 실행되어 bind()를 하고 있어야 하는 제약 조건이 걸린다. (이후 버전에서 해결할 예정이라고..)

ZeroMQ에서 context를 만들면 하나의 I/O 쓰레드가 백그라운드에서 작업을 시작하게 된다. 일반적인 요구조건에선 하나의 I/O 쓰레드만 가지고 모든 작업을 처리할 수 있다고 한다. I/O 쓰레드의 개수를 늘리고 싶은 경우엔 zmq_ctx_set() 메서드의 ZMQ_IO_THREADS 옵션을 조정하면 된다.


• ZeroMQ가 내부적으로 해주는 일

ZeroMQ가 하는 일을 요약하면, 

1) 노드에 빠르고 효율적으로 메시지를 전송해주고,
2) 자동으로 다시 연결을 해주며
3) 메시지 큐잉을 해준다.
4) 메시지 큐는 프로세스가 메모리 부족 현상을 겪지 않게끔 적절한 크기에서 제한되며
5) 소켓 오류를 처리해준다.
6) 마지막으로 노드간 통신에서 데드락 같은 현상을 겪지 않게 보장해준다.


• ZeroMQ가 제공하는 소켓 페어

목록 중에선 거의 상위 3개 목록 중 하나를 쓰게될 것 같다. 아래의 패턴 외 다른 조합을 쓰는 경우엔  ZeroMQ가 올바른 동작을 보장해주지 않는다.

- PUB - SUB : 디커플링된 형태의 데이터 분산 패턴.
- REQ - REP
- PUSH - PULL : fan-out / fan-in의 병렬 작업 분산 및 수집 패턴에 적합.
- REQ - ROUTER
- DEALER - REP
- DEALER - ROUTER
- DEALER - DEALER
- ROUTER - ROUTER
- PAIR - PAIR

PUB - SUB과 REP - REQ 같은걸 조합해서 쓸 수도 있는데 예를 들어 모든 subscriber가 연결된 이후에 publish할 수 있도록 애플리케이션을 디자인 해야 하는 경우에 유용하다. 간단한 시나리오는
1) Publisher가 PUB 소켓을 열고 "Hello" 메시지를 발행한다.
2) Subscriber는 SUB 소켓을 열고 "Hello" 메시지에 응답한다. (REQ/REP 페어를 통해)
3) Publisher는 Subscriber의 응답을 확인한 뒤 실제 데이터 전송을 시작한다.


2020년 6월 16일 화요일

ZeroMQ 기본 개념

• ZeroMQ 등장의 배경

요즘 많은 애플리케이션은 LAN 또는 인터넷과 같은 일종의 네트워크를 통해 확장되는 구조를 갖는다. 이는 응용 프로그램 개발자로 하여금 일종의 메시징을 수행하게끔 한다.  일부 개발자는 메세지-큐 제품을 사용하지만 아직도 대부분은 TCP를 사용해 메시징을 수행한다. 이 프로토콜은 사용하기 어렵진 않지만 여러 케이스에 대응해 안정적인 방식으로 동작시키긴 어렵다. (메시지의 전달을 보장한다던가, 서로 다른 아키텍처 간의 메시지 전달이라던가 하는.) 게다가 재사용 가능한 메시징 시스템을 구축하는 것은 매우 어려운 일이다. 만들어진 제품을 사용하더라도 상대적으로 복잡하고 비싸며 제대로 쓰기까지 몇 주에서 몇 달이 걸릴 수도 있다.

최근의 대부분의 메시징 프로젝트는 중심에 broker를 두는 방식으로 설계되어 있다. Broker는 어드레싱, 라우팅, 큐잉등의 역할을 수행하게 되는데 이러한 구조를 따르면 네트워크의 복잡성을 꽤나 줄여주기 때문에 혜택이 크다. 하지만 broker 자체가 병목이 되거나 broker가 제대로 동작하지 않을 때 전체 네트워크에 위협이 되는 등의 문제가 생긴다. 이러한 구조를 따르는 프로젝트에선 두번째 세번째 네번째 broker를 추가로 띄우는 방식으로 이를 보완하지만 이는 다시 구성을 복잡하게 만들어 버린다.

ZeroMQ는 위 요구사항과 문제점을 해결하기 위해 만들어졌다. ZeroMQ는 쉽고 가벼운, 모든 프로그래밍 언어와 모든 OS에서 동작하는 메시징 기술이다. ZeroMQ는 다양한 기술적 이슈와 장애를 대응하면서 마치 작은 서버처럼 동작한다.


• ZeroMQ가 제공하는 패턴

ZeroMQ에선 어느 쪽이 연결되고 어느 쪽이 바인딩되는지는 중요하지 않다. 그리고 ZeroMQ는 전달하고 받는 데이터의 길이만을 알고 있다. 보내고 받는 데이터가 종단에서 어떻게 해석되는가에 대해선 관여하지 않는다. 그러한 부분의 책임은 사용자에게 있다. 복잡한 데이터 형식을 주고 받는다면 프로토콜 버퍼 같은 라이브러리를 사용할 수도 있을 것이고, 단순하게 문자열 데이터를 주고받을 수 있을 것이다. (C로 작업하는 경우 수신한 문자열에 null 바이트 처리를 해주어야 한다.)

ZeroMQ는 요청과 응답이 있는 REP-REQ 패턴, 발행-구독의 PUB-SUB 패턴 등을 제공한다. PUB-SUB 패턴의 경우 비동기로 동작하는데 SUB 소켓은 PUB 소켓보다 먼저 실행되더라도 첫 n개의 메시지를 놓칠 가능성이 있다. SUB 소켓과 PUB 소켓이 서로 연결되는데 걸리는 시간이 짧지 않기 때문이다. 이런 경우에 대해 SUB 소켓의 연결을 확인한 뒤 메시지를 발행할 수 있는 기법이 마련되어 있다. (단순히 sleep 한 뒤 동작하게 하는 방법도 있지만 권장되진 않는다.)

마지막으로 PUSH-PULL 방식을 제공한다. 단방향으로 흐르는 데이터 스트림을 다루는데 효과적이다. 단지 초기 데이터 또는 task를 내보내는 연결점에 데이터를 끌어오는 worker를 붙이거나 떼는 방식으로 네트워크를 확장하거나 축소할 수 있다. 하나의 연결점에 연결된 다수의 소켓은 고르게 처리된다. 이걸 fair-queuing이라고 부른다. fair-queuing 이지만 문제점이 없진 않은데 여러 개의 노드를 같이 동작시켜도 초기 연결 과정에서 가장 먼저 연결된 worker에 일시적으로 작업이 쏠리는 경우가 발생할 수 있다.

ZeroMQ 애플리케이션은 context를 만드는 것으로 작업이 시작된다. 하나의 프로세스는 반드시 하나의 context만을 만들어 사용해야 한다. context는 프로세스에서 사용하는 모든 소켓을 담는 컨테이너로 동작한다. 여러 개의 context를 만들어 사용할 수도 있지만 ZeroMQ는 이를 서로 다른 인스턴스(프로세스)로 인식하게 되므로 일반적인 사용 방식은 아니다. (리눅스의 fork()를 쓴다면 context를 생성하는 zmq_ctx_new()는 fork() 이후에 호출하는 것이 바람직하다.)

ZeroMQ에서 소켓은 여러 연결을 자동으로 관리하는 작은 백그라운드 통신 엔진의 출입구로 작동한다. 사용자는 내부의 연결을 확인하거나 제어할 수 없게끔 되어 있는데 이는 의도적인 설계이며 오히려 이러함 감춤이 ZeroMQ 확장성의 키 요소가 된다.

2020년 5월 28일 목요일

[Java] 러시아 시간 출력 오류

특정 국가, 환경에서 Java에서 가져온 시간과 윈도우에 적용된 시간 값이 다른 경우가 발생할 수 있다.

이는 대부분 오래된 JDK나 JRE를 사용해서 생긴 문제라고 보면 된다.

우리에겐 낯설은 써머타임(Daylight saving time, DST)에 대한 것인데,
써머타임 적용, 폐지에 대한 신규 정보가 JVM 시스템에 반영되어있지 않아서 발생하는 문제라고 한다.

해결은 JDK나 JRE를 최신 버전으로 올려주면 되는데 문제는 쉽게 업데이트를 결정할 수 없는 경우다. (안정성 또는 호환성 고려, 폐쇄망 환경 등으로 인한 업데이트의 어려움)

이 경우엔 아래의 두가지 대안이 있다.


1. 실행 옵션에 -XX:+UseGetTimeOfDay를 적용.

시간 관련 동작 수행 시 매번 gettimeofday() 시스템 콜을 이용해 시스템 시간을 가져온다. 성능 저하가 있다.



2. 타임존 정보를 갱신해주는 프로그램을 이용해 패치 (Time Zone Updater)

오라클에서 제공하고 있다. 

tzupdater.jar를 내려받은 후 아래 명령어를 실행해주면 시간이 정상적으로 보정된다.
java -jar tzupdater.jar -v -f -l http://data.iana.org/time-zones/releases/tzdata2017c.tar.gz


2020년 5월 24일 일요일

[Java] 빈도수를 간단하게, map의 merge()

Map을 이용해 빈도수를 계산할 때 어떤 언어에서는 간단히 myMap[key]++과 같이 해주면 되는데 Java에서는 그렇게 동작하지 않는다.

그래서 대개 AtomicInteger와 같이 레퍼런스 타입으로 처리하거나 ContainsKey 또는 get()의 null값 체크 후 빈도수를 기록하는 지저분한 코드가 작성되곤 한다.

Java 8에선 이러한 문제(?)가 다소 해결되었는데 아래와 같이 Map의 merge 함수를 이용하면 한 줄의 코드로 단순 카운팅 기능을 구현할 수 있다. 한 줄의 코드이지만 Atomic operation은 아님에 주의할 필요는 있다.

myMap.merge(key, 1, Integer::sum);

함수 이름이 좀 헷갈리게 지어진 것은 아쉽지만 한번 기억해두면 쓰기 편하다.

public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)

• key: 키

• value: 값. 초기 값으로 쓰이거나 이전 갑과 병합됨.

• remappingFunction: 새로운 값을 지정하는데 쓰이는 함수, 이전 값이 있는 경우 호출됨. BiFunction은 함수 인수가 2개이면서 결과 값이 있는 함수를 뜻한다.

2020년 5월 19일 화요일

[NSIS] StrCpy 사용법

IndexOf와 RIndexOf랑 StrCpy를 쓰면 결과물이 지저분해질 순 있어도 NSIS 내부에서 원하는 문자열 조작을 어떤식으로든 할 수 있다.

문법
StrCpy 저장변수 문자열 [최대길이] [시작_오프셋]
: 최대 길이와 시작 오프셋이 생략되거나 음수 값이 올 수 있는게 특징이다.

예시
StrCpy $0 "a string"
→ "a string" - 기본 복사

StrCpy $0 "a string" 3
→ "a s" - 최대 길이를 3으로 지정했으므로 처음의 3글자인 "a s"만 선택됨.

StrCpy $0 "a string" -1
→ "a strin" - 최대 길이를 음수로 하면 문자열의 뒤에서 부터 잘라내는 동작을 함.

StrCpy $0 "a string" "" 2
→ "string" - 복사 시작 위치를 2로 지정했으므로 "a "가 스킵됨.

StrCpy $0 "a string" "" -3
→ "ing" - 복사 시작 위치를 음수로 하면 뒤에서 지정된 길이만큼 문자열을 가져옴.

StrCpy $0 "a string" 3 -4
→ "rin" - 뒤에서 부터 4글자인 "ring"을 가져온 다음 앞의 3글자인 "rin"만 선택됨.

https://nsis.sourceforge.io/Reference/StrCpy

2020년 5월 18일 월요일

[C#] Task, Task

Task와 Task의 Generic인 Task<TResult>는 쓰레드풀에서 쓰레드를 가져와 비동기 작업을 실행해 준다. Generic은 값을 반환하는 작업인 경우에 쓰인다. Task를 실행한 다음엔 IsCanceled, IsCompleted, IsFaulted 등 속성을 이용해 Task의 상태를 확인할 수 있다.

Task를 실행하는 방법엔 여러가지가 있다. Task 인스턴스를 만든 다음 Start() 메서드를 호출하거나 TaskFactory.StartNew(Action<Object>, Object) 메서드를 호출해 한번에 처리하는 방식이 있다. 더 간단하게는 Task.Run()을 이용할 수도 있다. 다만 이 메서드는 .Net 4.5 이상에서 제공된다.

Task는 비동기적으로 실행되기 때문에 Task의 모든 작업이 끝나기 전에 애플리케이션이 종료되는 경우가 생길 수 있다. 이러한 경우엔 Wait() 메서드를 이용해 Task의 작업이 끝날때까지 호출 쓰레드가 기다리게 할 수 있다. 무한정 기다리기 싫은 경우엔 Wait() 메서드에 timeout을 지정해주면 된다.

Task는 대기하지 않고 백그라운드에서 돌리고 싶은 작업이 있을 때 유용하다. 실행 문맥과 독립적인 작업을 Task로 처리하면 사용자가 대기하지 않아도 되기 때문에 사용성 측면에서도 이점이 있다. 여기에 코드까지 깔끔해지는 것은 덤이다.


// Task의 생성과 실행을 따로.
var t1 = new Task(() => {
    // work
});

t1.Start();
// other work
t1.Wait();

// Task의 실행과 생성을 한 번에.
var t2 = Task.Factory.StartNew(() => {
    // work
});
// other work
t2.Wait();


2020년 5월 14일 목요일

GTA 5 (Grand Theft Auto V) PC 무료



















무려 GTA 5를 21일까지 에픽게임즈에서 무료로 다운로드 받을 수 있다.

출시된지 오래된 게임이라 높은 사양을 요구하지 않아서 대부분의 PC에서 부담없이 즐길 수 있으리라 생각된다.

최소 사양
권장 사양
OS
Windows 10 64비트, Windows 8.1 64비트, Windows 8 64비트, Windows 7 64비트 서비스 팩 1, Windows Vista 64비트 서비스 팩 2* (*Vista OS에서는 NVIDIA 비디오 카드를 권장합니다)
OS
Windows 10 64비트, Windows 8.1 64비트, Windows 8 64비트, Windows 7 64비트 서비스 팩 1
프로세서
인텔 코어 2 쿼드 CPU Q6600 @ 2.40GHz(4 CPU) / AMD Phenom 9850 쿼드코어 프로세서(4 CPU) @ 2.5GHz
프로세서
인텔 코어 i5 3470 @ 3.2GHZ(4 CPU) / AMD X8 FX-8350 @ 4GHZ(8 CPU)
메모리
4GB RAM
메모리
8GB RAM
비디오 카드
NVIDIA 9800 GT 1GB / AMD HD 4870 1GB (DX 10, 10.1, 11)
비디오 카드
NVIDIA GTX 660 2GB / AMD HD7870 2GB
사운드 카드
DirectX 10 100% 호환
사운드 카드
DirectX 10 100% 호환
HDD 공간
90 GB available space
HDD 공간
90 GB available space

누구에겐 부담을 가질만한 잔인한 샌드박스 게임이지만 공짜라는데 뭐 받지 않을 이유가 있나.
내려받기는 에픽 게임즈 런처 설치한 뒤 런처 통해서 내려받는 방법이 가장 수월하다.


2020년 4월 20일 월요일

어떻게 일하며 성장할 것인가 - 전영민 지음.

이 책은 2014년 선임 진급할 때 연구소장님께서 주셨던 책으로 지금도 가끔씩 들여다 보는 책이다. 지금 생각해보면 진부하면서도 인상적인 진급 선물이었다. 그 당시 잘나가는 인문학 서적도 아니고 자칫 반감을 살 수도 있는 자기개발서를 굳이 골라 후배들에게 선물해준 것이다. 몇마디 조언이나 커피 한잔 사주며 퉁(?)치셔도 될 일이었는데 말이다. 굳이.. 뭐, 덕분에 지금도 회사 생활 나름 잘 하고 있다. 2015년 이전의 회사 생활이 그립긴 하지만. 그 때가 힘들긴 했어도 회사 다니는 맛(?) 같은게 있었던 것 같다. 밤새고 샤워하는 와중에 해답이 보인다던가, 꿈속에서도 일한다던가..;;

이 책은 롯데 그룹에서 꽤 오래 재직하신 분께서 들려주는 회사 생활 가이드? 노하우? 뭐 그런 책이다. 꽤나 단정적이고 솔직하게 책을 쓰셨기 때문에 일정부분 반감이 생기긴 해도 도움이 되는 충고들이 많이 담겨있다. 아예 책이 마음에 안든다고 해도 건질 것은 있다. 적어도 윗 분들(?)이 요즘 아래 사람들을 어떻게 생각하고 있는지에 대해서 조금이나마 살펴볼 수 있는 그런 부분에서. 나쁘게 얘기하면 꼰대 마인드? 뭐 그런거.. 근데 나도 년차가 어느정도 쌓이다 보니 걍 수긍이 되버리는 대목들이 이젠 더 많다..

몇 대목 옮겨 적어보고 이 책은 놓아주기로.

'인사'에 대해.
인사는 확률게임인 것 같다. 도무지 속을 알 수 없는 사람을 다루는 일인데 완벽한 인사란 게 세상에 존재할 수 없다. .. 승진심사를 할 때 정해진 승진 티오에 따라 작업을 하다 보면 승진 서열부에 줄을 긋게 된다. 이름이 그 줄 위에 있으면 승진이고 그 밑은 탈락이다. 그러면 그 줄 바로 위에 있는 사람과 바로 밑에 있는 사람이 차이가 크게 나서 그렇게 운명이 갈린 걸까? 사실 두 사람이 뒤바뀌어도 누구 하나 느끼지 못할 정도의 차이일 뿐이다. 고등학교 때 반 석차 15등과 16등이 정말 차이가 있어서 그렇게 된 걸까? 그런 것과 유사한 것이다. ...
→ 여기서 저자의 팁은? 조직 내의 누가 보더라도 저 친구는 1등이다라는 느낌이 들 수 있도록 노력하라 이다. ㄷㄷ
명함에 집착하지 마라. 중요한 것은 명함에 적힌 것이 아니라 당신 자신의 역량이다.

'아이디어'에 대해.
창의성이라는 것은 타고나는 것이 아니다 더구나 만사를 삐딱하게 보는 시각에서 나오는 것도 아니다. ... 창의성이라는 것은 살아오면서 축적해온 지식과 경험이 집중적인 문제 해결의지와 맞부디쳐서 터지는 스파크일 뿐이다. ... 직장생활은 지겨운 밥벌이가 아니다. 일이 지겨운 이유는 당신이 맡은 일을 개선하거나 바꾸려 하지 않고 지난 달에 했던 그 방식으로 계속하니까 그런 것이다. 규정된 원칙에 따라 시키는 대로만 하니까 그게 지겨운 것이다 당신이 조금 더 고민해서 그 원칙을 바꾸려고 해보라 매일이 새로워질 것이다.
일반적인 사람들은 '경로 의존성'이라는 것에 지배를 당한다 자신들이 과거에 성공했던 방법이나 칭창받았던 행동을 강화하고 반복하려는 성향이 있다는 말이다.

'변화'에 대해.
변화는 작심만으로는 부족하다. 그런 나태와 게으름을 이겨내고 변화를 지속시키는 방법은 일상에서 변화의 플랫폼을 만드는 수밖에 없는 것 같다. 변화의 플랫폼은 행동의 방아쇠와 같은 것이다. 특정한 상황적인 계기에 도달하면 특정한 행동을 하겠다는 사전 계획이라는 뜻이다.
→ 예를 들면 이런 것. 3호선을 타다가 고속터미널 역이라는 안내가 나오면 무조건 내려서 9호선으로 갈아타게 되는 것. 월요일 아침에는 하늘이 무너져도 반드시 무엇을 하겠다는 결심이 목표를 지키는데 큰 힘이 되는 것이다.

'열정'에 대해.
열정을 불러올 수 있는 목표는 몇 가지 갖추어야 할 요건이 있다.
1. 정해진 시간
2. 목표의 구체성 - 시간이 되었을 때 목표의 달성 정도를 측정할 수 있어야 한다.
3. 목표의 난이도 - 최선을 다했을 때 달성이 가능할 정도로
4. 목표의 가슴 떨림
5. 목표에 도달하는 것에 대한 중간 목표 설정
보통의 인간이라면 작은 성공에 자신을 계속 노출시키는 것이 도움이 된다. 어디선가 읽은 내용에선 목표를 주변 사람에게 말로 알리는 것도 효과가 있다고 한다.

디플레이션 시대의 생존법(?)
2020년부터는 한국에서도 베이비붐 세대의 은퇴가 본격화된다 그들은 은퇴하면서 가지고 있는 부동산이나 투자자산을 처분해서 빚부터 갚으려 할 것이다. 그러면 그때부터 한국도 대규모 부채의 구조조정기에 들어갈 것으로 예상한다 그렇게 되면 추가로 부동산이나 주식 가격은 내려갈 수 있다.
해리 덴트는 우리가 앞으로 봉착하게 되는 디레버리징과 디플레이션 시대에 살아남을 수 잇는 방법을 두 가지 추천했다. 첫 번째는 절대 빚지지 말라는 것이다. 대부분의 자산 가치가 떨어지는 마당에 빚을 얻어서 무엇을 산다는 것은 바로 죽음으로 가는 길이다. ...
두 번째 생존방법은 지금 다니는 직장을 지키라는 것이다 이런 때일수록 가장 확실한 생존방법은 지금 자리를 지키는 것이다. ... 가능하면 절대 다른 곳을 쳐다보지 말고 지금 있는 곳에서 최선을 다해서 살아남아라.


2020년 4월 16일 목요일

[Python] 재시도 로직이 추가된 requests 모듈 사용 예제

파이썬엔 간단하게 HTTP 요청을 할 수 있게 해주는 requests 모듈이 있다. 렌더링을 위한 데이터 획득이 아니라면 굳이 브라우저를 이용할 필요가 없기 때문에 json 데이터 등을 가져오는게(API 호출) 목적이라면 requests 모듈을 이용하는 것이 보다 합리적이다. 여기서 한가지 주의할 점은 네트워크 환경이 불안정한 경우 일시적으로 요청 작업이 실패할 수 있다는 부분이다. 따라서 사용자는 요청 실패에 대한 로직 흐름까지 고려할 필요가 있다.

아주 단순하게 문제를 해결하자면 루프 구조 내에서 원하는 결과가 나올 때까지 응답을 계속하는 방법이 있을 수 있다.

# 방법 1
def get(url):
    try:
        return requests.get(url)
    except Exception:
        time.sleep(1)
        return get(url) # 재시도

# 방법 2
while True:
    response = get(url)
    if response.status_code != 500: # 500, server error
        break
    else:
        time.sleep(1)

방법 1의 문제점은 url 문자열이 잘못되어있을 경우 무한정 요청 작업을 수행한다는 것이고, 방법 2의 문제점은 다양한 네트워크 에러를 로직이 올바르게 처리해주지 못한다는 점이다.

제안되는 솔루션은 다음과 같다.

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

def requests_retry_session(
    retries=3,
    backoff_factor=0.3,
    status_forcelist=(500, 502, 504),
    session=None,
):
    session = session or requests.Session()
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    return session

사용은 아래와 같이 한다. session 객체 전달은 선택 사양으로, 사용자가 session 객체를 따로 인자로 넘겨주지 않으면 requests_retry_session() 함수 내부에서 session 객체를 새로 생성하는 것을 위 코드에서 볼 수 있다. 그리고 status_forcelist엔 요청 실패로 간주할 응답 코드를 넣어줄 수 있다.


# 단순 get
response = requests_retry_session().get(url)

# 단순 get, timeout이 있는 경우.
response = requests_retry_session().get(url, timeout=5)

# 헤더 정보가 필요한 경우
s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})

response = requests_retry_session(session=s).get(url)

Retry 모듈과 HTTPAdapter 모듈을 이용해 요청을 할 때 재시도 정보를 포함시키는 것이다. backoff 팩터는 retries 값과 연계해 요청과 요청 사이에 쉬어줄 시간을 정해주게 된다.

쉬어줄 시간에 대한 로직은 다음과 같다.
슬립 = backoff factor * 2^(재시도 횟수 - 1)

따라서 위 함수의 기본 값으로 지정된 3과 0.3을 그대로 쓰는 경우
• 1회 재시도 sleep : 0초 (0.3 * 0)
• 2회 재시도 sleep : 0.6초 (0.3 * 2)
• 3회 재시도 sleep : 1.2초 (0.3 * 4)
값을 갖게 된다.

따라서 3회 요청에 실패하는 경우 사용자는 최대 1.8초 + @(통신 비용)을 기다리게 되는 셈이다.
위 함수는 당연히 post() 호출 시에도 유효하다.

출처: https://www.peterbe.com/plog/best-practice-with-retries-with-requests

아래는 유사 한글 포스팅 참고.
https://knight76.tistory.com/entry/python-%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EC%98%88%EC%8B%9C-requests-HTTPAdapter-Retry

2020년 4월 8일 수요일

모두 제자리 - 도미니크 로로

'심플하게 산다'로 대 히트를 쳤던 도미니크 로로의 또 다른 책으로, 짧은 글들을 모아 엮어 낸 책이다. 다른 책 심플하게 산다나 다시 쓰는 내 인생의 리스트에 나온 글귀와 비슷한 부분이 많아서 읽는데 좀 지루했는데 한번 붙잡은 책은 끝까지 읽는 편이라 어쨌든 끝까지 읽긴 했다. 밀리의 서재를 통해 읽었는데 구매해서 읽었다면 조금 후회했을 만한 책.

글쓴이는 오랫동안 일본에 거주하며 선불교와 동양 철학에 큰 영향을 받았다고 한다. 그래서인지 책 곳곳에 일본 문학에서 가져온 글귀나 일본인의 생활 방식에 감탄하는 구문들이 있다. 웹 서비스를 통해 읽은 책이라 '소장'할 순 없으니 개인적으로 기억에 남는 문장을 옮겨 적어 보기로 한다.

대도시에서 한 평의 가격이 얼마인지 계산하면 요즘 시대에 공간을 갖는다는 것이 얼마나 큰 사치에 속하는지 알게 된다.
• 많이 갖지 않으면 공간도 벌 수 있다.
• 소유하고 저장하고 계속 갖고 있을지 아니면 버릴지 결정하는 것은 삶의 질을 높이는 일이기도 하다.
• 좁은 공간에 사는 사람은 깔끔하게 정리할 수밖에 없다. 공간이 귀하다는 것을 알기에 편하게 살기 위해 좁은 공간을 잘 활용한다.
• 쓸모 있을까 생각했을 때 머뭇거린다면 별로 필요하지 않은 물건이다. 필요한 물건이라면 생각조차 할 필요가 없기 때문이다.
목표를 이루려면 단계를 세분화해야 한다. 너무나 당연하다 보니 많은 사람들이 그러한 점을 잊거나 과대평가한다.
• 정리에서, 추억의 물건은 맨 마지막에 다뤄야 한다. 이러한 물건은 감정이 깃들어 있어서 분류, 정리하기가 가장 힘들다.
• 조금 깊이 생각해 보면 미학은 일치를 추구하려는 본능일 뿐이다.
• 평범한 것들이 유지하고 원칙을 따르며 이대로 좋다는 확신으로 가득할 때 일상의 평범한 옷을 입고 있어도 위대해진다.
• 너무 완벽하게 질서 정연하면 서정적인 면이 없다.
• 몸은 훌륭한 도구다. 경험을 통해 습관을 들이기 때문이다.
• 일본 속담에서는 작은 것을 하찮게 여기는 사람은 절대 큰일을 할 수 없다는 말이 있다.
• 정리를 하면 물질의 영향력에서 자유로워져서 건강하고 자연스럽게 살 수 있으며 가볍게 살 수 있다.

뭐 이런 책이다.
꼭 필요한 물건만 소유하면서 삶에 집중하자는 책. 확실히 주변에 불필요한 물건이 많으면 물건에 일상이 눌리는 느낌이 들곤 한다. 가족이 생기고 식구가 늘다보니 한번 읽을 책을 계속 갖고 있기에도 어딘가 부담이 가는 요즘이다.

밀리의 서재를 1년 결재해서 책을 읽고 있는데, 쨍한 컴퓨터 디스플레이나 모바일 디스플레이로 읽어야 해서 눈이 좀 아프긴 한데 꽤나 만족스럽다. 원하는 시간, 원하는 장소에서 내가 원하는 형태로 책을 읽을 수 있으니 말이다. 아쉬운 건 내가 꼭 읽고 싶은 책은 정작 서비스 목록에 없다는 점. 그리고 '종이 냄새'가 나지 않으니 책을 읽을 때의 만족감이 좀 덜하다.

아, 배우 박정민이 쓴 전설의 책(?) 쓸만한 인간도 여기서 읽을 수 있었다. 마포구청 도서관에선 예약이 밀려 있어서 도저히 읽을 수가 없었는데 밀리의 서재에 딱 있길래 반갑게 읽었다. 책이 막 좋진 않다. 연예인 등 유명인이 쓴 신변잡기 책 중에서 조금 나은 정도다. 그래도 뭐 나는 배우 박정민을 좋아하니까 이만하면 되었다.

2020년 3월 16일 월요일

[C++] 암시적 링크와 명시적 링크

동적 라이브러리 (DLL)을 컴파일 하면 *.lib 파일과 *.dll 파일이 생성된다. 여기서 *.lib 파일(가져오기 라이브러리)은 라이브러리의 전체 코드를 포함하고 있는 정적 라이브러리의 *.lib와 달리 DLL에서 제공하고자 하는 외부 함수 참조 정보가 담겨져 있다. 이렇게 생성된 DLL을 다른 실행 파일에서 연결하려면 다음의 두가지 방식 중 하나를 선택해야 한다.

  • 암시적 링크 : 정적 로드 방식. 함수가 정적으로 연결되고 실행 파일 내에 포함된 것과 동일한 방식으로 DLL에서 내보낸 함수를 호출할 수 있다. *.lib 파일을 이용해 연결된다. *.lib에 담겨진 정보를 토대로 런타임에 DLL의 함수 코드를 참조할 수 있게 된다.

  • 명시적 링크 : 런타임에 필요시 DLL을 로드한다. DLL에의 각 함수에 접근할 함수 포인터를 설정해 함수를 호출해야 한다. 다 사용하고 나면 DLL을 언로드 해주어야 한다. 각각 LoadLibrary(), GetProcAddress(), FreeLibrary()의 세가지 함수를 통해 구현된다.

대부분의 응용 프로그램에선 간편하고 쓰기 쉽기 때문에 암시적 링크를 사용하지만 명시적 링크 방식을 쓸 때의 장점도 있다. 명시적 링크 방식을 쓰면 프로그램 실행 중에 동적으로 DLL을 교체할 수 있다. (플러그인) 그리고 프로그램 실행 전에 필요한 모든 DLL을 메모리에 로딩하는 암시적 링크 방식에 비해서 빠른 실행 시간을 갖는다.

https://docs.microsoft.com/ko-kr/cpp/build/linking-an-executable-to-a-dll?view=vs-2019

[Java] BufferedReader

BufferedReader는 입력 스트림(파일, 소켓, ...) 에서 텍스트를 쉽게 읽을 수 있는 클래스이다. 텍스트 데이터를 효율적으로 읽을 수 있도록 문자를 버퍼링한다. (입력된 데이터가 바로 전달되지 않고 버퍼링 된 후 전달된다.)

BufferedReader는 데코레이터 패턴을 적용한 좋은 예다. (* 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가해준다. 서브클래스를 만드는 것보다 더 합리적이다.)
여기서 데코레이터는 말 그대로 장식자로, 특정 객체를 감싼 뒤 중간에 위치해서 오고 가는 요청과 응답을 더 매끄럽게 처리해 준다. 당연히 데코레이터는 자신이 감싼 객체의 동작을 잘 알고 있어야 한다.

reader = new BufferedReader(new FileReader("sample.txt"));
reader = new BufferedReader(new InputStreamReader(System.in));
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());

위 구문에서 보듯 Reader를 구현하는 인스턴스를 감싸, 유연하게 버퍼링 기능을 추가(확장) 해줄 수 있다. 저렇게 한번 감싸주면 readLine() 같은 메서드를 이용해 데이터를 쉽고 간편하게 읽어들일 수 있게 된다.

마지막으로 BufferedReader는 Scanner와 자주 비교된다. 비슷한 일을 해주는 클래스를 굳이 따로 구분한 까닭이 있을 것이다. 주요 차이점은 다음과 같다.

• BufferedReader는 thread-safe하지만 Scanner는 그렇지 않다.
• Scanner는 정규 표현식을 써서 구문 분석을 할 수 있다. (파싱 같은 작업을 하는데 편리하다.)
• Scanner는 IOException 을 숨기고, BufferedReader는 이를 드러낸다.
• BufferedReader의 기본 버퍼 크기가 더 크고, 데이터를 읽기만 하는 경우 Scanner 보다 빠르게 처리할 수 있다.

Scanner 사용법 : Text Parsing Made Easy
https://dzone.com/articles/java-scanner-text-parsing-made-easy

2020년 3월 10일 화요일

[Django] no such table: main.auth_user__old

해당 오류는 SQLite 3.26.0에서 ALTER TABLE RENAME 구문에 대한 사양 변경으로 발생한 오류라고 한다. 기존의 모델(models.py) 또는 어드민(admin.py)을 수정하는 과정에서 이 오류가 발생할 수 있다. 오류는 Django 2.1.5 이하 버전에서 발생하며 Django의 버전을 2.1.5 이상으로 올린뒤 다시 마이그레이션(makemigrations, migrate) 명령을 호출해주면 문제는 해결된다. 사용하는 버전에 따라 SQLite를 다시 설치해야 하는 경우도 있다고 하는데 내 경우엔 필요한 작업이 아니었기 때문에 확실한 정보는 아니다.

어쨌든 조치를 하기 전에 데이터 백업은 필수.

참고로 contrib.auth.views.login(), logout() 함수는 Django 2.1에서 삭제되었으므로 해당 함수를 사용하는 Django 버전을 쓴다면 LoginView.as_view(template_name='...') 방식으로 수정이 필요하다.

//

위와 같이 작업한 후 마이그레이션은 되었는데, 다시 사용자 데이터를 수정할 때 동일한 문제가 생긴다면 직접 db.sqlite3에 접속해 문제를 해결하는 방법이 있다. (기존 DB를 날리고 새 DB 파일을 생성하는 건 차마 못하겠다..)

커맨드 라인에서 Django 프로젝트 폴더로 이동해 sqlite3 db.sqlite3를 입력하면 Django가 바라보는 SQLite DB에 접근할 수 있다.

테이블 목록을 확인하는 명령어는 .tables 인데 입력해보면
사용자 데이터는 auth_user
그룹 데이터는 auth_group
사용자와 그룹에 대한 관계 데이터는 auth_user_groups
에 저장되어 있는 것을 확인할 수 있다.

수정하고 싶은 데이터의 ID를 알아내 스키마에 맞게 SQL을 작성하면 원하는 작업을 할 수 있는 것이다.

문제가 되는(?) auth_user_groups의 테이블 생성 구문을 살펴보면 아래와 같이 user_id 컬럼이 존재하지 않는 auth_user__old 테이블의 id를 참조하고 있는 것으로 확인된다.

CREATE TABLE "auth_user_groups" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"group_id" integer NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("group_id") REFERENCES "auth_group"("id"),
FOREIGN KEY("user_id") REFERENCES "auth_user__old"("id")
);

아마도. 다른 트러블 슈팅 문서대로 조치하였는데 고쳐지지 않는다면 이 부분을 해결하면 될 것이다. 방법은 아래와 같이 외래키가 올바르게 지정된 테이블을 생성한 뒤 교체해 주는 작업을 진행하면 된다.

PRAGMA foreign_keys = OFF;

CREATE TABLE auth_user_groups_temp (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "user_id" integer NOT NULL REFERENCES "auth_user" ("id"), 
    "group_id" integer NOT NULL REFERENCES "auth_group" ("id")
);

INSERT INTO auth_user_groups_temp SELECT * FROM auth_user_groups;

DROP TABLE auth_user_groups;

ALTER TABLE auth_user_groups_temp RENAME TO auth_user_groups;

PRAGMA foreign_keys = ON;


2020년 3월 4일 수요일

[Java] 프로토콜 버퍼 (Protocol Buffers) 기초

아래의 내용은 Java 언어를 이용해 프로토콜 버퍼를 사용하는 것에 대한 내용이다.

자세한 내용은 아래 링크를 참고하시면 됨.
https://developers.google.com/protocol-buffers/docs/javatutorial

프로토콜 버퍼는 직렬화 라이브러리로 프로그래밍 언어를 통해 만들어진 데이터를 bytes로 변환해 준다. 프로토콜 버퍼는 XML, JSON 방식보다 더 작고(smaller data) 빠르게 동작한다. 데이터를 텍스트가 아니라 bytes로 변환하기 때문이다. 알려진 내용에선 프로토콜 버퍼가 텍스트 포맷 송수신 대비 10배 적은 용량을 사용하면서도 100배 빠르게 동작한다고 한다.

게다가 사용자가 하기 싫어하는 지저분한 작업들을 프로토콜 버퍼가 대신 해준다. 직렬화/역직렬화는 물론이고 enum과 문자열 조작에 대한 것들이 이러한 작업이다. 또한 서로 다른 언어간 데이터를 주고 받을 때 발생하는 문제점들도 프로토콜 버퍼가 해결해준다. 사실 이게 가장 강력한 기능일 것이다.

튜토리얼에 따르면 프로토콜 버퍼를 사용하기 위해선 아래의 3가지를 알아야 한다고 되어 있다.

• .proto 파일을 정의하는 방법.
• 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법
• 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.

프로토콜 버퍼를 사용하는 이유는 다음과 같다.

• 직접 직렬화 라이브러리를 작성해야 하는 경우 또는 빌트-인 직렬화 기능보다 더 나은 솔루션이 필요한 경우
• 직렬화된 객체를 각기 다른 언어로 작성된 프로그램에서 읽어서 사용해야 하는 경우

* 물론 "단순한 객체를 단일 언어에서" 간단하게 객체를 읽고 쓰기 위함이라면 (임시로) 자체적인 로직을 작성하거나 XML/JSON 등의 텍스트 형식으로 이를 처리할 수 있을 것이다.


□ .proto 파일을 정의하는 방법.


프로토콜 버퍼의 프로토콜 포맷은 .proto 확장자를 이용한다.

C의 struct 정의와 유사한 문법으로 메시지 형식을 정의하는데 message 키워드를 이용해 각각의 데이터 스트럭쳐를 정의할 수 있다. 이렇게 만들어진 .proto 파일은 프로토콜 버퍼 컴파일러를 통해 타겟 언어의 소스 코드로 변환된다. 그리고 각자 작성하는 프로그래밍 언어에서 이 파일을 참조해 목적에 맞는 작업을 하면 된다.

message A { message B { ... } } 식의 nested 방식을 지원하며 bool, int32, float, double, string등의 익숙한 자료형을 쓸 수 있다. enum을 지원하며 repeated 키워드를 이용해 리스트 형식을 표현할 수 있다.

* Java 언어인 경우 package, java_package 옵션을 이용해 이름 충돌이 발생하지 않도록 패키지 이름을 정의해주어야 한다.

필드엔 반드시 값이 지정되어야 한다는 의미의 required와 값이 지정되지 않아도 된다는 의미의 optional이 위치하게 된다. 튜토리얼에 의하면 required는 가급적 사용하지 않는 것이 좋다고 한다. 메시지 구조의 유연함을 떨어뜨리는 요인으로 나중에 가서 하위 호환성을 지키기 어려워지기 때문이다.

마지막으로 한가지, 모든 필드엔 " = 1", " = 2" 식의 숫자 태그를 붙여주어야 한다. 필드를 직렬화할 때 정보를 컴파일러에게 알려주는 일을 하는데 하위 호환성을 제공하려면 한번 부여한 숫자는 절대로 변경해선 안된다. 태그가 변경되면 바이너리 포맷이 틀어지기 때문이다.


□ 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법


protoc.exe가 제공되는데 이를 가지고 타겟 프로그래밍 언어로 프로토콜 포맷에 대한 소스코드를 생성할 수 있다.
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/프로토콜포맷파일명


□ 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.


필드에 대한 getter(), setter()는 물론이고 빌더-패턴을 제공하기 때문에 객체에 값을 지정하고 읽는 것엔 큰 어려움이 없을 것이다.

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhones(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

객체를 읽고 쓰는덴 아래의 4가지 API가 제공된다.

byte[] 버전과,
  • byte[] toByteArray();
  • static Person parseFrom(byte[] data);

스트림 버전이 있다.
  • void writeTo(OutputStream output);
  • static Person parseFrom(InputStream input);

추가로, 통신으로 객체를 주고 받을 수도 있는데 Netty와 궁합이 좋다.
디코딩 단계에선 ProtobufVarint32FrameDecoderProtobufDecoder를,
인코딩 단계에선 ProtobufVarint32LengthFieldPrependerProtobufEncoder를 사용하면 된다.
사용자의 입장에선 데이터 프레임의 길이만 앞에 붙여서 서로 주고 받으면 되는 것이다.


2020년 3월 3일 화요일

[Netty] Netty의 데이터 처리 순서

Netty에서의 데이터 처리 순서를 이해하려면 Channel과 Pipeline에 대한 이해가 먼저 되어야 한다. Netty에서 Channel은 읽기, 쓰기, 연결 등의 I/O 작업이 가능한 통로이고, Channel에 연결된 Pipeline엔 인바운드, 아웃바운드, 인아웃바운드 핸들러가 쭉 연결되어 있다. 각 핸들러는 서블릿 필터와 유사한 역할을 수행한다.

서버 입장에서 보면 Channel은 연결된 클라이언트마다 생성되고, Channel당 하나의 Pipeline을 갖게 된다. Pipeline에 올라가는 핸들러는 등록된 순서에 따라 연결되는데 인바운드, 아웃바운드, 그리고 인아웃바운드 핸들러를 구분하지 않고 하나의 double-linked list에 추가된다. 개인적으론 인바운드와 아웃바운드에 대한 체인이 따로 구분되었다면 이해하고 사용하기에 더 편했을 것이란 아쉬움이 있다.

인바운드 핸들러는 읽기 이벤트를 처리하는데 ByteBuf를 데이터 스트럭쳐로 변환하는 일을 하므로 디코더라고 하고,
아웃바운드 핸들러는 쓰기 이벤트를 처리하는데 데이터 스트럭쳐를 ByteBuf로 변환하는 일을 하므로 엔코더라고 한다.
* 인아웃바운드 핸들러는 읽기와 쓰기 이벤트 모두에 관여할 것이다.

엔코더와 디코더는 처리 순서를 고려하여 Pipeline의 시작 부분에 위치시키는 것이 일반적이다. 그리고 비즈니스 핸들러는 가장 나중에 위치시켜야 한다. 알아두어야 할 것은 모든 핸들러는 하나의 파이프라인에 연결된다는 점이다.


@ChannelInitializer

ChannelPipeline p = ch.pipeline();
p.addLast(new MyEncoder());
p.addLast(new MyDecoder());
p.addLast(new MyBusiness()); // 비즈니스 핸들러는 가장 마지막에 위치.

내가 작성한 프로그램으로 데이터가 입력된다면 처리 순서는 다음과 같을 것이다. 입력 데이터에 대해 엔코더는 관여하지 않는다.
MyDecoder (ByteBuf를 데이터 스트럭쳐로 변환) -> MyBusiness


반대로 내가 작성한 프로그램이 데이터를 다른 곳으로 출력한다면 처리 순서는 다음과 같아진다. 위와 마찬가지로 출력 데이터에 대해 디코더는 관여하지 않는다.
MyBusiness -> MyEncoder (데이터 스트럭쳐를 ByteBuf로 변환)


2020년 3월 2일 월요일

[Django] Django의 인증 시스템 (authentication system)

Django의 인증 시스템은 일반적인 프로젝트의 인증과 관련된 요구사항을 쉽게 해결해준다. 기능 확장에 대해서도 충분히 열려있기 때문에 기본적으로 제공되는 기능 바탕 위에 사용자가 원하는 기능을 추가할 수도 있다.
(* Django의 인증 기능은 인증(authentication)과 권한(authorization) 부여를 모두 제공하고 있으므로 일반적으론 인증 시스템(authentication system)이라고 한다.)


코어 클래스, User

Django에서 인증은 User 객체를 이용해 이루어진다. User는 주요 속성으로 username, password, email, first_name, last_name을 갖는다. 이 중 username, password, email 값은 반드시 지정되어야 한다. (required field) User 클래스는 django.contrib.auth.models 패키지에 정의되어 있다. 아래 링크로 들어가면 대략적인 느낌 정도는 파악할 수 있을 것이다. (코드는 좀 어렵다.)
https://docs.djangoproject.com/en/1.8/_modules/django/contrib/auth/models/

User에 대한 조작은 User.objects를 통해 가지고 올 수 있는 UserManager 객체를 통해 할 수 있다. 그렇지만 Django admin을 통해 GUI로 조작하는 것이 훨씬 간편하다.. 인증에서 username이 ID로 쓰인다고 보면 되고 password는 저장 시점에 암호화되어 데이터베이스에 기록된다. 암호화 과정에서 SHA256 해시 알고리즘과 함께 PBKDF2 알고리즘을 쓰는데 이는 충분히 안전하다. 이마저도 사용자가 알고리즘을 변경할 수 있게끔 구성되어 있다.


기본 인증

인증은 django.contrib.auth의 authenticate 메서드를 통해 수행되는데 입력된 username과 password가 유효하면 User 객체가 반환된다. (유효하지 않으면 None을 반환한다.)


권한

Django에는 권한 시스템이 내장되어 있다. 특정 사용자나 그룹에 권한을 부여할 수 있는 기능을 제공한다. 제공되는건 Model 객체에 대한 view, add, change, delete 권한이고, 이는 AbstractUser 클래스가 상속받는 PermissionsMixin 클래스를 통해 관리된다.


웹 요청의 인증

Django는 세션과 미들웨어를 사용해 인증 시스템을 request 객체에 연결해주고 있다.
이는 매우 편리한데 모든 request 객체로부터 로그인된 User 객체를 얻을 수 있기 때문이다. (request.user)
만약 사용자가 로그인된 상태가 아니면 AnonymousUser가 반환된다.

사용자의 인증 여부는 아래의 간단한 코드를 통해 확인할 수 있다.

if request.user.is_authenticated:
    # do something for authenticated users.

view 함수가 호출되기 전에 이를 확인할 수도 있는데 @login_required 데코레이터가 이러한 일을 해준다. (CBV 방식이면 LoginRequireMixin이 같은 일을 한다.) 비슷한 기능으로 특정 조건 체크를 위한 @user_passes_test, 권한 체크를 위한 @permission_required가 제공된다. 그냥 필요한게 다 만들어져 있다고 보면 된다. Django가 뚱땡이 프레임워크라는걸 잊지 말자.

@login_required
def my_view(request):
    # 로그인된 상태여야만 진입할 수 있음.


사용자 로그인, 로그아웃

사용자 로그인은 login() 함수를 이용해서 할 수 있다. login()은 request 객체와 user 객체를 인수로 받고, login()은 Django의 세션 프레임워크를 사용해 세션에 사용자의 ID를 저장한다.
로그아웃은 django.contrib.auth의 logout 함수를 이용하여 수행된다.

기타 기본 login.html, logout.html을 확장하거나 하는 내용들이 아래 링크에 자세히 설명되어 있다. 이 부분은 내용이 너무 길어서 생략하기로 한다.

마지막으로 언급하고 싶은 부분은 Django의 인증 시스템은 Django를 쓰지 않는 사용자에게도 좋은 참고 자료가 된다는 것이다.

https://docs.djangoproject.com/en/3.0/topics/auth/default/

[Django] DateField - auto_now, auto_now_add

Django Model Field(django.db.models.fields)에서 시간을 표현하는 클래스로 DateField, TimeField, DateTimeField가 있다.

 • class DateField(DateTimeCheckMixin, Field): # 날짜
 • class TimeField(DateTimeCheckMixin, Field): # 시간
 • class DateTimeField(DateField): # 날짜와 시간

시간과 관련해서 객체를 데이터베이스에 저장할 때 자주 쓰이는 항목이 있는데 그건 바로 생성 시간과 수정 시간에 관한 것이다. 속성 중 auto_now와 auto_now_add가 이러한 기능을 제공한다.

 • auto_now: 최종 수정 시간. True로 셋 되어 있으면 update 작업이 일어날 때마다 시간이 갱신된다.
 • auto_now_add: 최초 생성 시간. True로 셋 되어 있으면 최초 insert 작업이 일어날 때만 시간이 설정된다.

하나의 필드는 둘 중 하나만 True 값을 갖을 수 있고, 둘 중 하나에 True가 할당되면 이후 사용자가 직접 수정할 수 없게 되어 있다. 최소한의 이력 관리라도 되어야 한다면 아래와 같은 모델을 떠올릴 수 있을 것이다.

class MyModel(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)

https://docs.djangoproject.com/en/2.2/_modules/django/db/models/fields/#DateTimeField

2020년 2월 23일 일요일

[ML] LightGBM 파라미터 정리.

LightGBM은 더 적은 자원을 쓰면서 빠르기까지 해서 xgboost의 대안 알고리즘으로 자주 거론된다. xgboost의 성능은 매우 뛰어나지만 파라미터 튜닝이 복잡하고 학습 시간이 느린 단점이 있다. LightGBM은 xgboost 대비 비슷한 성능을 내면서도 빠른 학습 속도를 제공한다.

ML 알고리즘의 경우 성능이 조금 떨어지더라도 속도가 빠르면 여러가지 장점이 있는데, 속도가 빠르면 FE에 더 많은 시간을 할애할 수 있기도 하고, grid search 등으로 빠르게 튜닝해서 오히려 더 나은 결과를 뽑아낼 수도 있기 때문이다.

LightGBM은 기존 알고리즘의 level-wise(균형 트리 방식)가 아닌 leaf-wise 방식으로 학습이 진행된다. 기존 방식에선 트리의 균형을 우선하는데 트리의 균형을 위해 추가되는 연산이 학습 속도를 떨어뜨리는 요인이 되었다. LightGBM은 트리의 규형보다는 리프 노드를 지속적으로 분할하면서 학습을 진행한다. 결과로 비대칭적인 깊은 트리가 생성되지만 level-wise 방식에서 리프 노드가 생성될 때보다 손실을 줄이는 장점이 있다고 한다.


자주 참조되는 파라미터는 아래와 같다. 지원되는 부스터 중 dart는 따로 공부해야 할 주제로 보인다. 여기서 속도 개선을 위한 CPU, GPU 관련 파라미터도 별도로 존재하는데 관련 파라미터는 아래의 링크로 대신한다.
https://testlightgbm.readthedocs.io/en/latest/Parameters.html

• objective: 목표 설정. 기본 값은 'regression'.
 - regression : 회귀.
   - regression_l2 (별칭: mean_squared_error, mse)
  - regression_l1 (별칭: mean_absolute_error, mae)
 - binary : 이진 분류.
 - multiclass : 멀티-클래스 분류. num_class로 분류 가짓수를 정해주어야 한다.

• boosting: 부스팅 방식을 설정. gbdt가 가장 많이 쓰인다.
 - gbdt : gradient boosting decision tree
 - dart : dropouts meet multiple additive regression trees
 - goss : gradient-based one-side sampling

• data (별칭: train, train_data) : 학습용 데이터

• valid (별칭: test, valid_data, test_data) : 검증용 데이터

• num_iterations: 부스팅 반복 횟수. 학습에 사용할 트리 개수를 뜻한다. 멀티-클래스 분류 문제인 경우 num_class * num_iterations 만큼의 트리가 사용된다.

• learning_rate: 학습 속도. 기본 값은 0.1. 값이 크면 건성건성 학습하고, 작으면 학습용 데이터에 너무 피팅되므로 실험을 통해 적정값으로 조절해주어야 한다.

• num_leaves: 하나의 트리에서 사용될 수 있는 최대 리프 노드 개수. 기본 값은 31.

• max_depth: 트리 깊이의 제한. 데이터가 적은 가운데 트리 깊이가 깊어지면 오버-핏 되는 경향이 있다. 기본 값은 제한 없음인데 -1로 표시한다. 기본 값은 -1.

• min_data_in_leaf: 리프 노드가 되기 위한 최소 샘플 데이터 수. 기본 값은 20. 이것 역시 오버-핏이 발생하지 않게끔 해주는 파라미터. 기본 값은 20.

• feature_fraction: 각 이터레이션에서 사용할 feature의 비율. LightGBM은 각 이터레이션마다 여기에 설정된 비율 만큼의 feature를 랜덤하게 선택해 학습을 진행한다. 오버-핏을 다루는 파라미터이지만 부가적으로 학습 속도를 높이는 효과도 있다. 0~1 사이의 값을 지정해야 하고, 기본 값은 1.

• bagging_fraction: feature_fraction이 column 방향으로의 random selection이라면 bagging_fraction은 row 방향으로의 random selection이다. LightGBM이 각 이터레이션마다 쓸 데이터의 비율을 뜻한다. 기본 값은 1.

• bagging_freq: bagging 수행의 빈도. 몇 번의 이터레이션 마다 bagging을 할 것인지 지정한다. 기본 값은 0으로 사용하지 않음을 의미.

• early_stopping_round: 학습 과정에서 측정된 스코어가 향상되지 않으면 중간에 학습을 중단하게 한다. 기본 값은 0.

• lamgda_l1, lambda_l2: l1 정규화, l2 정규화. 어느 정도의 값을 지정해야 하는지 난 아직 잘 모르겠다.

• min_gain_to_split: 트리 분기를 수행하기 위한 최소 기준. 무작정 분기할 순 없으니 의미 있는 feature의 의미 있는 값 기준으로 트리가 분기할 수 있도록 알고리즘에 알려주기 위함이다. 마찬가지로 적정 값을 어떻게 정해주어야 하는지 잘 모르겠음.

• categorical_feature: 카테고리 변수를 LightGBM에 알려주는 용도. 카테고리 변수를 기존엔 원핫인코딩 방식으로 지정해주곤 했는데 이렇게 했을 경우 속도도 느려지고 트리의 밸런스가 안좋게 만들어지는 경우가 생긴다. LightGBM에 카테고리 변수를 따로 지정해주면 LightGBM이 알아서 잘 처리해준다.

• metric: 평가식. regression인 경우 l2(mean_squared_error)가 기본값. 이진 분류에선 binary_logloss가 기본 값이다. 멀티-클래스 분류인 경우 multi_logloss, multi_error가 쓰인다. auc 평가식도 많이 쓰이는 값.

https://lightgbm.readthedocs.io/en/latest/Python-Intro.html


2020년 2월 20일 목요일

[Java] MyBatis #{}과 ${} 차이점.

#{}는 MyBatis가 제공하는 매개 변수 표기법으로 MyBatis에게 PreparedStatement 매개 변수를 작성하도록 알려준다. SQL Injection을 예방해주기 때문에 MyBatis에서 권장되는 방식이다.

${}는 문자열 대체 기능으로 (매개 변수에 사용할 수도 있지만) 주 용도는 SQL 문의 메타 데이터(테이블 이름 또는 컬럼 이름)가 동적일 때 일부 문자를 대체해주기 위해 쓰인다. MyBatis는 ${} 내 데이터를 변경하거나 이스케이프 처리하지 않는다.

${} 방식을 쓸 때엔 주의할점이 있다. 사용자로부터 입력 받은 값을 확인하지 않고 바로 구문에 전달해서는 안되는데 SQL 주입 공격으로부터 안전하지 않기 때문이다. 사용자 입력값을 ${}를 이용해 구문으로 전달해야만 하는 로직이 있다면(구체적으로 그럴 일이 있는진 모르겠다만.) 자체적으로 값에 이상이 없는지 검증 과정을 거쳐야 한다.

별도로 데이터베이스 수행 계획에 영향을 주기도 하는데 아래 링크에서 내용을 확인할 수 있다.
https://lng1982.tistory.com/246

테이블에 담긴 데이터의 성격에 따라 ${} 방식을 사용했을 때 성능 상 이점이 생길 수 있다는 내용이다.
그러나 이 정도까지의 튜닝이 필요한 환경에서 일하는 경우는 드물기 때문에 #{}로 사용이 가능한 경우엔 가급적 #{}를 쓰는게 나을 것이다.


공식 페이지: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

2020년 2월 12일 수요일

규모에 따라 서비스 확장하기 (Application Scaling)

웹과 모바일 앱 애플리케이션은 일반적으로 3개의 컴포넌트로 구성된다.

  • 데이터베이스
  • API
  • 클라이언트

데이터베이스는 데이터를 보관하고, API는 요청에 따른 데이터를 서빙한다. 그리고 클라이언트는 데이터를 렌더링해 사용자에게 표시한다. 클라이언트와 API를 구분하는 이유는, 클라이언트와 API를 완전히 별개의 개체로 생각하고 미리 구분해두면 애플리케이션 확장에 대한 추론이 훨씬 간단해지기 때문이다.


다음은 서비스를 시작한 뒤 서서히 사용자(트래픽)가 증가하게 될 때 애플리케이션을 어떻게 구성(대응)해야 하느냐에 대한 가이드다. 원문은 아래 링크에 있다. (아래의 내용은 원문에 대한 번역이 아니기 때문에 영어에 익숙하다면 원문의 내용을 참고하는 것이 좋다.)
https://alexpareto.com/scalability/systems/2020/02/03/scaling-100k.html


1~10 Users : 하나의 서버

아주 적은 사용자(1~10명)를 대상으로 할 땐 위의 세가지 컴포넌트를 하나의 서버에서 실행되게 하는 것이 좋을 것이다. 한 명의 엔지니어가 데이터베이스, API, 클라이언트를 모두 통제할 수 있기 때문이다.


10~100 Users : 데이터베이스 분리

사용자가 10~100명 수준이 된다면 가장 먼저 고려할 것은 데이터베이스를 물리적으로 분리하는 것이다. 클라우드 환경이라면 아마존 RDS 같은 것이 될 수 있는데, 비용이 조금 비싸지지만 멀티-리젼 중복, 읽기 전용 복제본 생성, 자동 백업 등 다양한 기능을 쉽게 추가할 수 있다.


100~1000 Users : 클라이언트 분리

사용자가 100명을 넘어서면서 트래픽이 안정적으로 나와주면 클라이언트 컴포넌트 까지도 분리를 고려해야 한다. 앞서 적힌 내용처럼 최초 구현 단계에서부터 클라이언트와 API 컴포넌트를 명확히 구분지어 놓으면 확장이 쉬워진다. (API 입장에선 클라이언트가 웹이든 데스크탑이든 그저 같은 API를 사용하는 클라이언트일 뿐이다.)


1000+ Users : Load balancer

하나의 API 인스턴스가 모든 트래픽을 감당하기 어려워지게 되므로 더 많은 컴퓨팅 파워가 필요하게 된다.

로드 밸런서가 투입된다. API 앞에 로드 밸런서를 배치하고 트래픽을 분산시키는 방식이다. 이를 통해 수평적 확장이 가능하게 된다. 동일한 코드를 실행하는 서버를 추가할수록 처리할 수 있는 요청량은 늘어나게 된다. 로드 밸런서는 트래픽이 가장 적은 인스턴스로 요청을 라우팅한다. (물론 처음 연결맺은 API 인스턴스와 계속 통신하게 할수도 있다.)


10,000+ Users : CDN

정적 컨텐츠를 API 서버로부터 구분한다. 이 작업은 초기에 해두는 것이 좋긴 하다. 그리고 CDN 도입을 고려해야 한다. CDN은 전 세계의 다른 데이터 센터에 정적인 데이터를 자동으로 캐시한다.


100,000+ Users: 데이터베이스에 대한 확장

데이터베이스에 대한 스케일링 작업은 매우 어려운 편이다. API 서버의 경우 무상태(stateless)로 구성할 수 있어 확장이 간편하지만 데이터베이스의 경우 '상태'를 갖기 때문이다.

데이터베이스의 부하를 낮추기 위한 가장 간편하고 효과적인 선택은 데이터 보관에 대한 캐시 레이어를 마련하는 것이다. Redis 또는 Memcached와 같은 키-밸류 기반의 메모리 저장 솔루션이 이를 지원한다. 특히 Redis의 경우 자체로 클러스터링을 지원하기 때문에 확장으로부터 안전하다. 여기서 더 할 수 있는 일은 읽기에 대한 복제본(Read replica)를 추가하거나 파티셔닝, 샤딩 등의 기법을 사용하는 것이 도움이 된다.


+ 모니터링 도구
New Relic 또는 Datadog과 같은 서비스를 이용해 시스템을 모니터링할 수 있다. 이를 통해 요청이 느리고 개선이 필요한 부분을 파악할 수 있을 것이다.

2020년 2월 11일 화요일

[WPF] ContentControl과 Template

들여다볼 때마다 헷갈리는 개념.
네이버 쪽 블로그 보니 이미 작년에 한번 정리했던 내용이다.
https://blog.naver.com/seokcrew/221584014717

ContentControl은 단일 내용으로 된 컨트롤을 나타낸다. Button의 부모인 ButtonBase나 ScrollViewer 같은 컨트롤은 이 클래스를 직접 상속받고 있다.

ContentControl은 외형을 결정짓는 속성으로 Template과 ContentTemplate을 가질 수 있다.
  • Template의 값 유형은 ControlTemplate이며 컨트롤의 모양을 정의한다.
  • ContentTemplate의 값 유형은 DataTemplate이며 데이터의 모양을 정의한다.

ContentPresenter는 ContrentControl의 ControlTemplate에 배치될 때 ContentControl의 Content를 자동으로 가져와서 표시해 준다. ContentControl 클래스에 의해 표시되는 Content가 런타임에 결합될 위치를 지정해준다.
  • ContentControl 상속 구조 : FrameworkElement → Control → ContentControl
  • ContentPresenter 상속 구조 : FrameworkElement → ContentPresenter

* ContentControl의 ContentTemplate을 정의하는 DataTemplate은 다시 ContentControl을 갖을 수 있고, ContentControl은 별도로 하나의 자식 요소를 가질 수 있는데 다시 각각의 ContentControl은 ControlTemplate을 갖을 수 있기 때문에 여러 단계로 중첩된 구조가 생기면 한 눈에 파악하기 어려워진다.


2020년 2월 9일 일요일

Socket.io 요약

socket.io는 브라우저와 서버 간 이벤트 기반으로 양방향 통신을 가능하게 하는 라이브러리다. 내부적인 heartbeat 메커니즘을 통해 자동 재접속과 연결 끊어짐 감지를 해주기 때문에 사용하는 입장에서 라이브러리를 믿고 간편하게 쓸 수 있다.

socket.io는 사용자가 원하는 어떤 형식으로든 이벤트와 데이터 형식을 만들어 쓸 수 있게끔 지원하고 있다. 일반적으론 JSON 포맷을 쓰게 되겠지만 binary 데이터 형식의 송수신도 지원한다.
브라우저와 서버 간 이벤트를 주고받는 방법은 단순하다. 기본적으로 아래 2개의 메서드로 구현된다.
  • emit: 이벤트를 발생시킨다.
  • on: 이벤트를 수신한다.

참조: https://socket.io/docs/


Room

room 기능도 제공한다. 임의의 채널을 정의해서 특정 소켓을 그루핑 시킬 수 있는데 간단하게 생각하면 채팅방과 같은 개념으로 이해할 수 있을 것이다. namespace로 구분되는 room을 한정해서 제한된 데이터 교환이 가능하다. 이는 응용 측면에서만 아니라 설계 측면에서 리소스를 줄여주는 역할도 하기 때문에 유용하다.
기본 namespace 이름은 '/' 이다. namespace를 지정하고 싶으면 io.of() 함수를 이용하면 된다. (클라이언트에선 io()의 파라미터로 namespace 값을 전달.)


Broadcasting

브로드캐스팅의 경우 io객체의 emit() 함수를 이용하면 되는데, 발송자를 제외한 브로드캐스팅이 필요한 경우엔 socket.broadcast.emit() 함수를 이용하여야 한다. namespace를 한정해 이벤트를 발생시킬 수도 있는데 io.to() 함수를 이용하면 된다.

참조: https://socket.io/docs/rooms-and-namespaces/



** 채팅 솔루션이 급하다면 아래 링크를. 코드를 보면 알겠지만 FE가 할 일이 더 많다. 그만큼 편하게 되어있다는 뜻.
https://github.com/socketio/socket.io/tree/master/examples/chat



서버 기본 코드. 아래 꼴에서 크게 달라지지 않는다.

// Express initializes 'app' to be a function handler that you can supply to an HTTP server.
var app = require('express')();
var http = require('http').createServer(app);

var io = require('socket.io')(http);

// define a route handler '/' that gets called when we hit our website home.
app.get('/', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});

// listen on the 'connection' event for incoming sockets.
io.on('connection', function (socket) {
    console.log('a user connected');
    socket.on('disconnect', function() {
        console.log('user disconnected');
    });

    socket.on('chat message', function (msg) {
        io.emit('chat message', msg);
    });
});

// http server listen on port 3000
http.listen(3000, function () {
    console.log('listening on *:3000');
});


위 코드와 대응되는 클라이언트 측 스크립트.

// client
$(function() {
    var socket = io();
    $('form').submit(function (e) {
        e.preventDefault();
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
    });

    socket.on('chat message', function (msg) {
        $('#messages').append($('<li>').text(msg));
    });
});


아래는 공식 페이지에서 제공하는 emit cheatsheet.

io.on('connect', onConnect);

function onConnect(socket){

  // sending to the client
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // sending to all clients except sender
  socket.broadcast.emit('broadcast', 'hello friends!');

  // sending to all clients in 'game' room except sender
  socket.to('game').emit('nice game', "let's play a game");

  // sending to all clients in 'game1' and/or in 'game2' room, except sender
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // sending to all clients in 'game' room, including sender
  io.in('game').emit('big-announcement', 'the game will start soon');

  // sending to all clients in namespace 'myNamespace', including sender
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // sending to a specific room in a specific namespace, including sender
  io.of('myNamespace').to('room').emit('event', 'message');

  // sending to individual socketid (private message)
  io.to(`${socketId}`).emit('hey', 'I just met you');

  // WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
  // named `socket.id` but the sender. Please use the classic `socket.emit()` instead.

  // sending with acknowledgement
  socket.emit('question', 'do you think so?', function (answer) {});

  // sending without compression
  socket.compress(false).emit('uncompressed', "that's rough");

  // sending a message that might be dropped if the client is not ready to receive messages
  socket.volatile.emit('maybe', 'do you really need it?');

  // specifying whether the data to send has binary data
  socket.binary(false).emit('what', 'I have no binaries!');

  // sending to all clients on this node (when using multiple nodes)
  io.local.emit('hi', 'my lovely babies');

  // sending to all connected clients
  io.emit('an event sent to all connected clients');

};


2020년 2월 6일 목요일

[Java] Callable, Future, FutureTask

Callable, Futre는 Java 1.5에 추가된 Concurrency API 사양이다.

Callable은 Runnable과 주로 비교된다.

Callable vs Runnable
Runnable의 run() 메서드는 반환 값이 없으나 Callable의 call()은 완료시 결과를 리턴하게 되어 있다. 기존 Runnable의 run()은 반환 값이 없기 때문에 run() 메서드의 실행 결과를 구하기 위해서는 공용 메모리나 파이프 같은 것들이 필요하였다. Callable은 이를 개선하기 위해 마련된 인터페이스이다.

Callable
Callable 객체는 ExecutorService.submit() (submit 메서드는 Future 타입으로 리턴되고, execute 메서드는 void 타입이다.)과 같은 메서드에 던져진다. 던져진 작업은 큐에 보관되었다가 가용한 쓰레드가 생길 때 call() 메서드가 실행될 것이다. 쓰레드 풀에 던져진 작업은 바로 실행되는 것이 아니기 때문에 리턴 값은 미래의 어느 시점에 구할 수 있다.

Future
이러한 구조를 만들기 위해 Java에선 Future라는 인터페이스를 만들어 제공하고 있다. Future는 당장은 아니지만 언젠간 결과를 갖게되는 객체 정도로 여기면 될 것이다. Future는 기본적으로 Main 쓰레드와 분리된 쓰레드의 결과를 추적할 수 있는 한 가지 방법이 되는 것이다. Future의 get() 메서드는 Callable 인터페이스의 call() 메서드 실행이 완료될 때까지 블로킹 된다.

FutureTask
Future는 Runnable과 함께 작동하도록 만들 수 있는데 FutureTask라는 concrete 클래스가 이러한 역할을 해준다. FutureTask는 Runnable과 Future를 모두 구현하고 있다. FutureTask는 작업을 수행하는 쓰레드와 결과를 받는 쓰레드가 분리된 구조로 동작하고, 중간에 작업 완료 여부를 체크하거나 작업을 취소시킬 수도 있다.

참고: https://www.geeksforgeeks.org/future-and-futuretask-in-java/

[Java] ExecutorService, CountDownLatch

적절한 쓰레드 풀의 사용은 오래 걸리는 작업의 시간을 단축 시킨다.

아래와 같은 코드가 있고, doWork()이 완료되기까지 비교적 오랜 시간이 소요된다면.

public void batchWork(Collection<Work> works) {
    for (Work work : works) {
        work.doWork();
    }
}

위의 코드는 아래와 같은 형식으로 개선이 가능하다.
execute() 내부 동작이 병렬로 실행되므로 work 개체는 쓰레드 안정성을 확보하고 있어야 한다.

public void batchWork(Collection<Work> works) {
    ExecutorService executor = Executor.newFixedThreadPool(4);
    CountDownLatch latch = new CountDownLatch(collection.size());

    for (Work work : works) {
        executor.execute(() -> {
            work.doWork();

            latch.countDown();
        })
    }

    latch.await();
}

CountDownLatch가 전체 작업의 시작과 끝의 타이밍 제어를 해주고 있다.

참고: https://www.baeldung.com/java-countdown-latch

HTML/CSS Margin 속성

마진은 태그 요소와 바깥 요소 간 간격(외부 여백, 패딩은 내부 여백.)을 지정한다. 1개부터 4개까지 값이 올 수 있다. 음수 값도 허용하는데, 음수 값을 적절히 이용하면 재미있는 배치를 표현할 수 있다.

margin을 지정하는 방법은 대략 아래와 같이 정리될 것이다. margin-top, margin-right 식으로 하나 하나 값을 따로 지정할 수도 있다.

1. 하나의 값
margin: 10px;
 top, right, bottom, left 모두 10px.

2. 두개의 값 : 상하-우좌
margin: 10px 5px;
 top, bottom: 10px
 right, left: 5px

3. 세개의 값 : 상-우좌-하
margin: 10px 5px 15px;
 top : 10px
 right, left : 5px
 bottom : 15px

4. 네개의 값 : 상-우-하-좌
margin: 10px 5px 15px 20px;
 top 10px
 right 5px
 left 15px
 bottom 20px

웹 환경에서만 개발하는 사람이라면 margin 값 지정에 별다른 어려움이 없겠지만 WPF 개발을 주로 하고 있어서 개인적으론 항상 헷갈린다.
(WPF에선 margin 적용이 left-top-righ-bottom 순이라 CSS의 top-right-bottom-left 순과 차이가 있다.)

2020년 2월 4일 화요일

HTML 주요 태그 정리.

• 문서를 구성하는 큰 골격에서의 주요 태그.

<doctype>
웹문서의 가장 위에 doctype 선언이 온다. doctype은 웹문서의 형식을 알려주기 위해 사용된다.
html5에선 doctype을 간단히 <!DOCTYPE html> 한가지로 정의한다.

<html>
doctype 다음에 html 태그가 오는데, 다른 모든 태그는 이 태그의 자손이 된다.

<head>
링크, 스크립트, 스타일시트의 정의 및 메타 태그가 들어간다.

<body>
실제 콘텐츠가 배치되는 곳, html 문서에서 본문에 해당한다.

<div>
<div> 자체론 아무 의미가 없지만 웹 페이지의 블럭을 지정하는 용도로 자주 쓰인다.

<script>
웹 문서에서 실행 가능한 스크립트를 넣거나 외부 스크립트를 참조할 때 쓴다. type 속성은 생략 가능하며 생략된 경우 JavaScript로 처리된다.



• 문서의 내용을 구성하는데 쓰이는 태그.

<article>
재배포, 재사용이 가능한 컨텐츠를 포함할 때 쓴다.
기사, 포럼 게시물, 사용자 코멘트 등이 대표적.

<br>, <hr>
<br>은 문서에서 줄바꿈이 필요할 때 사용된다.
<hr>은 문서 단락들 사이에 의미적인 분리를 위해 쓴다. 수평선으로 표시된다.

<canvas>
html5에 추가된 태그로 스크립트 언어를 이용해 그래픽을 표현할 수 있다.

<code>
프로그래밍 코드 조각을 표현할 때 사용된다.

<dl>, <dt>, <dd>
용어와 설명을 표시하는데 사용한다.

<embed>
플러그인 컨텐츠를 표시하는데 사용된다.

<figure>, <figcaption>
그림이나 도표의 제목 또는 범례를 표시하는데 사용된다.

<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
문서의 제목에 사용된다.

<img>
사진을 표시할 때 사용된다. alt 속성으로 이미지가 표시되지 않을 때 대체 정보를 사용자에게 전달해줄 수 있다.

<li>, <ul>, <ol>
목록을 표시하는데 사용한다. 정렬 순서가 있는 경우 <li>를 <ol>로 감싸고, 정렬 순서가 없는 경우 <ul>로 감싸준다.

<p>
문서의 단락 정의에 사용된다.

<pre>
html 파일에 입력된 문자 그대로 보여줄 때 사용한다.

<section>
일반 문서 및 애플리케이션 영역을 표시한다. <article> 태그와 다르게 웹상에서 재배포를 할 수 없다는 의미를 담고 있다.



• <head>와 관련된 태그들.

<meta>
메타 정보를 정의한다.

<link>
CSS와 같은 외부 자원에 대한 링크를 정의한다.

<style>
CSS를 정의하는데 사용된다.



• 폼과 관련된 태그들

<form>
클라이언트와 서버 간 정보를 교환하기 위해 사용된다. method 속성으로 http 전송 방식을 지정해줄 수 있다.

<fieldset>
폼 양식에서 레이블과 컨트롤을 그룹으로 묶어 주기 위해 사용된다.

<input>
폼 양식에서 사용자 입력을 받을 때 사용한다. type으로 text, password, checkbox, radio, file, submit 등의 형식을 부여할 수 있다. (기본 값은 텍스트 박스.)

<label>
폼 양식에서 input 등 항목에 대한 설명을 붙일 때 사용한다.

<select>
폼 양식에서 <option> 태그와 함께 옵션을 선택하는 드롭다운 목록을 제공할 때 사용된다.

<textarea>
폼 양식에서 여러 줄로 구성된 텍스트를 입력받는 용도로 쓰인다. cols, rows 속성으로 표시되는 크기를 지정할 수 있다.



• 표와 관련된 태그들

<table>
데이터를 2차원 표로 표시할 때 사용된다.

<thead>
표에서 머리글 행을 표시할 때 사용된다.

<tbody>
표에서 내용에 대한 행을 표시할 때 사용된다.

<tr>
표에서 표의 행을 표시할 때 사용한다. <th> 또는 <td> 태그를 포함한다.

<td>
표의 셀. colspan, rowspan 등으로 셀 병합을 할 수 있다.

<th>
표에서 헤더, 제목이 되는 셀은 <th>로 표현된다.

<title>
문서의 제목을 정의한다. <head> 요소 내에 위치한다.

<caption>
표에서 제목을 정의할 때 사용된다.

<col>
표에서 열을 정의한다.

<colgroup>
표에서 열의 그룹을 정의할 떄 쓴다. <col> 태그를 포함한다.



• 블록 레벨 태그와 인라인 레벨 태그의 구분

# 블록 레벨 태그
<address>, <article>, <aside>, <blockquote>, <canvas>,
<dd>, <div>, <dl>, <dt>, <fieldset>, <figcaption>, <figure>,
<footer>, <form>, <h1>-<h6>, <header>, <hr>, <li>, <main>,
<nav>, <noscript>, <ol>, <p>, <pre>, <section>, <table>,
<tfoot>, <ul>, <video>

# 인라인 레벨 태그
<a>, <abbr>, <acronym>, <b>, <bdo>, <big>, <br>, <button>,
<cite>, <code>, <dfn>, <em>, <i>, <img>, <input>, <kbd>, <label>,
<map>, <object>, <output>, <q>, <samp>, <script>, <select>,
<small>, <span>, <strong>, <sub>, <sup>, <textarea>, <time>,
<tt>, <var>

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.");
    }
}