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()
2020년 8월 12일 수요일
matplotlib 한글 깨지지 않게..
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)
2020년 7월 21일 화요일
입출력 다중화 (파일 디스크립터, select)
2020년 6월 17일 수요일
ZeroMQ 기본 개념 - 2
2020년 6월 16일 화요일
ZeroMQ 기본 개념
2020년 5월 28일 목요일
[Java] 러시아 시간 출력 오류
이 경우엔 아래의 두가지 대안이 있다.
2020년 5월 24일 일요일
[Java] 빈도수를 간단하게, map의 merge()
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 사용법
문법
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 인스턴스를 만든 다음 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일 월요일
어떻게 일하며 성장할 것인가 - 전영민 지음.
이 책은 롯데 그룹에서 꽤 오래 재직하신 분께서 들려주는 회사 생활 가이드? 노하우? 뭐 그런 책이다. 꽤나 단정적이고 솔직하게 책을 쓰셨기 때문에 일정부분 반감이 생기긴 해도 도움이 되는 충고들이 많이 담겨있다. 아예 책이 마음에 안든다고 해도 건질 것은 있다. 적어도 윗 분들(?)이 요즘 아래 사람들을 어떻게 생각하고 있는지에 대해서 조금이나마 살펴볼 수 있는 그런 부분에서. 나쁘게 얘기하면 꼰대 마인드? 뭐 그런거.. 근데 나도 년차가 어느정도 쌓이다 보니 걍 수긍이 되버리는 대목들이 이젠 더 많다..
몇 대목 옮겨 적어보고 이 책은 놓아주기로.
'인사'에 대해.
인사는 확률게임인 것 같다. 도무지 속을 알 수 없는 사람을 다루는 일인데 완벽한 인사란 게 세상에 존재할 수 없다. .. 승진심사를 할 때 정해진 승진 티오에 따라 작업을 하다 보면 승진 서열부에 줄을 긋게 된다. 이름이 그 줄 위에 있으면 승진이고 그 밑은 탈락이다. 그러면 그 줄 바로 위에 있는 사람과 바로 밑에 있는 사람이 차이가 크게 나서 그렇게 운명이 갈린 걸까? 사실 두 사람이 뒤바뀌어도 누구 하나 느끼지 못할 정도의 차이일 뿐이다. 고등학교 때 반 석차 15등과 16등이 정말 차이가 있어서 그렇게 된 걸까? 그런 것과 유사한 것이다. ...
→ 여기서 저자의 팁은? 조직 내의 누가 보더라도 저 친구는 1등이다라는 느낌이 들 수 있도록 노력하라 이다. ㄷㄷ
명함에 집착하지 마라. 중요한 것은 명함에 적힌 것이 아니라 당신 자신의 역량이다.
'아이디어'에 대해.
창의성이라는 것은 타고나는 것이 아니다 더구나 만사를 삐딱하게 보는 시각에서 나오는 것도 아니다. ... 창의성이라는 것은 살아오면서 축적해온 지식과 경험이 집중적인 문제 해결의지와 맞부디쳐서 터지는 스파크일 뿐이다. ... 직장생활은 지겨운 밥벌이가 아니다. 일이 지겨운 이유는 당신이 맡은 일을 개선하거나 바꾸려 하지 않고 지난 달에 했던 그 방식으로 계속하니까 그런 것이다. 규정된 원칙에 따라 시키는 대로만 하니까 그게 지겨운 것이다 당신이 조금 더 고민해서 그 원칙을 바꾸려고 해보라 매일이 새로워질 것이다.
일반적인 사람들은 '경로 의존성'이라는 것에 지배를 당한다 자신들이 과거에 성공했던 방법이나 칭창받았던 행동을 강화하고 반복하려는 성향이 있다는 말이다.
'변화'에 대해.
변화는 작심만으로는 부족하다. 그런 나태와 게으름을 이겨내고 변화를 지속시키는 방법은 일상에서 변화의 플랫폼을 만드는 수밖에 없는 것 같다. 변화의 플랫폼은 행동의 방아쇠와 같은 것이다. 특정한 상황적인 계기에 도달하면 특정한 행동을 하겠다는 사전 계획이라는 뜻이다.
→ 예를 들면 이런 것. 3호선을 타다가 고속터미널 역이라는 안내가 나오면 무조건 내려서 9호선으로 갈아타게 되는 것. 월요일 아침에는 하늘이 무너져도 반드시 무엇을 하겠다는 결심이 목표를 지키는데 큰 힘이 되는 것이다.
'열정'에 대해.
열정을 불러올 수 있는 목표는 몇 가지 갖추어야 할 요건이 있다.
1. 정해진 시간
2. 목표의 구체성 - 시간이 되었을 때 목표의 달성 정도를 측정할 수 있어야 한다.
3. 목표의 난이도 - 최선을 다했을 때 달성이 가능할 정도로
4. 목표의 가슴 떨림
5. 목표에 도달하는 것에 대한 중간 목표 설정
보통의 인간이라면 작은 성공에 자신을 계속 노출시키는 것이 도움이 된다. 어디선가 읽은 내용에선 목표를 주변 사람에게 말로 알리는 것도 효과가 있다고 한다.
디플레이션 시대의 생존법(?)
2020년부터는 한국에서도 베이비붐 세대의 은퇴가 본격화된다 그들은 은퇴하면서 가지고 있는 부동산이나 투자자산을 처분해서 빚부터 갚으려 할 것이다. 그러면 그때부터 한국도 대규모 부채의 구조조정기에 들어갈 것으로 예상한다 그렇게 되면 추가로 부동산이나 주식 가격은 내려갈 수 있다.
해리 덴트는 우리가 앞으로 봉착하게 되는 디레버리징과 디플레이션 시대에 살아남을 수 잇는 방법을 두 가지 추천했다. 첫 번째는 절대 빚지지 말라는 것이다. 대부분의 자산 가치가 떨어지는 마당에 빚을 얻어서 무엇을 산다는 것은 바로 죽음으로 가는 길이다. ...
두 번째 생존방법은 지금 다니는 직장을 지키라는 것이다 이런 때일수록 가장 확실한 생존방법은 지금 자리를 지키는 것이다. ... 가능하면 절대 다른 곳을 쳐다보지 말고 지금 있는 곳에서 최선을 다해서 살아남아라.
2020년 4월 16일 목요일
[Python] 재시도 로직이 추가된 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 파일을 이용해 연결된다. *.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는 데코레이터 패턴을 적용한 좋은 예다. (* 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가해준다. 서브클래스를 만드는 것보다 더 합리적이다.)
여기서 데코레이터는 말 그대로 장식자로, 특정 객체를 감싼 뒤 중간에 위치해서 오고 가는 요청과 응답을 더 매끄럽게 처리해 준다. 당연히 데코레이터는 자신이 감싼 객체의 동작을 잘 알고 있어야 한다.
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
어쨌든 조치를 하기 전에 데이터 백업은 필수.
참고로 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을 작성하면 원하는 작업을 할 수 있는 것이다.
2020년 3월 4일 수요일
[Java] 프로토콜 버퍼 (Protocol Buffers) 기초
자세한 내용은 아래 링크를 참고하시면 됨.
https://developers.google.com/protocol-buffers/docs/javatutorial
프로토콜 버퍼는 직렬화 라이브러리로 프로그래밍 언어를 통해 만들어진 데이터를 bytes로 변환해 준다. 프로토콜 버퍼는 XML, JSON 방식보다 더 작고(smaller data) 빠르게 동작한다. 데이터를 텍스트가 아니라 bytes로 변환하기 때문이다. 알려진 내용에선 프로토콜 버퍼가 텍스트 포맷 송수신 대비 10배 적은 용량을 사용하면서도 100배 빠르게 동작한다고 한다.
튜토리얼에 따르면 프로토콜 버퍼를 사용하기 위해선 아래의 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와 궁합이 좋다.
디코딩 단계에선 ProtobufVarint32FrameDecoder와 ProtobufDecoder를,
인코딩 단계에선 ProtobufVarint32LengthFieldPrepender와 ProtobufEncoder를 사용하면 된다.
사용자의 입장에선 데이터 프레임의 길이만 앞에 붙여서 서로 주고 받으면 되는 것이다.
2020년 3월 3일 화요일
[Netty] Netty의 데이터 처리 순서
서버 입장에서 보면 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의 인증 기능은 인증(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
• 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 파라미터 정리.
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 #{}과 ${} 차이점.
${}는 문자열 대체 기능으로 (매개 변수에 사용할 수도 있지만) 주 용도는 SQL 문의 메타 데이터(테이블 이름 또는 컬럼 이름)가 동적일 때 일부 문자를 대체해주기 위해 쓰인다. MyBatis는 ${} 내 데이터를 변경하거나 이스케이프 처리하지 않는다.
${} 방식을 쓸 때엔 주의할점이 있다. 사용자로부터 입력 받은 값을 확인하지 않고 바로 구문에 전달해서는 안되는데 SQL 주입 공격으로부터 안전하지 않기 때문이다. 사용자 입력값을 ${}를 이용해 구문으로 전달해야만 하는 로직이 있다면(구체적으로 그럴 일이 있는진 모르겠다만.) 자체적으로 값에 이상이 없는지 검증 과정을 거쳐야 한다.
별도로 데이터베이스 수행 계획에 영향을 주기도 하는데 아래 링크에서 내용을 확인할 수 있다.
https://lng1982.tistory.com/246
테이블에 담긴 데이터의 성격에 따라 ${} 방식을 사용했을 때 성능 상 이점이 생길 수 있다는 내용이다.
그러나 이 정도까지의 튜닝이 필요한 환경에서 일하는 경우는 드물기 때문에 #{}로 사용이 가능한 경우엔 가급적 #{}를 쓰는게 나을 것이다.
공식 페이지: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html
2020년 2월 12일 수요일
규모에 따라 서비스 확장하기 (Application Scaling)
• 데이터베이스
• 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는 사용자가 원하는 어떤 형식으로든 이벤트와 데이터 형식을 만들어 쓸 수 있게끔 지원하고 있다. 일반적으론 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은 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 속성
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은 일반 스트림이기 때문에 사용하기 편한 형식으로 변환해 사용할 수 있다.
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(); } } }
• 버퍼: 간단한 작업 집합으로 인터페이스 된 연속된 메모리 블록을 나타낸다.
• 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."); } }