diff --git a/2013/12/30/octopress-on-windows/index.html b/2013/12/30/octopress-on-windows/index.html new file mode 100644 index 00000000..443d9777 --- /dev/null +++ b/2013/12/30/octopress-on-windows/index.html @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +octopress on windows | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ octopress on windows +

+ + +
+ + + + +
+ + +

octopress도 대게는 ruby가 기본 설치된 mac에서 많이들 사용하는 듯 하다. 검색해보면 대부분 OS X를 기준으로 한 셋팅법이다. 윈도우에서 사용하는 것도 많이 어렵진 않지만 한글 인코딩 때문에 많이 헤맸음 ㅜㅠ

+

일단 기본적으로 아래 두 개의 글을 참고해 설치했는데,

+
    +
  1. http://stb.techelex.com/setup-octopress-on-windows7/
  2. +
  3. http://chulhankim.github.io/blog/2013/07/31/octopress-and-github.html
  4. +
+

ruby는 생소한 언어이기도 하고 링크가 사라지면 다시 헤맬수도 있으니 간략하게 다시 정리.

+ + +

Ruby 설치

일단 윈도우에는 Ruby가 없기 때문에 먼저 설치를 해야 한다.
다운로드 페이지에서 Ruby와 DevKit을 다운받는다.
내가 사용한 버전은 Ruby 2.0.0-p353 (x64)와 DevKit-mingw64-64-4.7.2-20130224-1432-sfx.exe

+

DevKit을 사용하기 전에 install 과정이 필요하다. 이 단계를 실행하기 전에 ruby의 bin 폴더가 path에 잡혀 있는 것이 좋다. 그러면 DevKit 초기화 과정에서 ruby의 경로를 알아서 감지하므로, config.yml을 수정할 필요가 없다.

+
1
2
3
cd C:/RubyDevKit
ruby dk.rb init # 이 때 config.yml이 생김. 이 전에 ruby bin을 path에 넣자.
ruby dk.rb install
+

python 설치

python은 없어도 상관없다. 하지만 syntax highlighting을 하려거든 python이 필요하다. 이것도 OS X는 기본 설치되어 있어서 크게 이슈가 없는듯. 나는 한참 써보다가 알았는데, 나중에 python을 설치하면 뭔가 더 해주어야 하는 것 같아 귀찮다. 그냥 처음부터 python을 설치해놓고 path에 python이 포함되도록 해두는게 좋겠다.

+

Octopress 받기

1
2
3
4
cd c:/github
git clone git://github.com/imathis/octopress.git octopress
cd octopress #replace octopress with username.github.com
ruby --version # Should report Ruby 1.9.3
+

ruby 패키지들 (dependencies) 설치:

+
1
2
3
cd c:/github/octopress       #replace octopress with username.github.com
gem install bundler
bundle install
+

octporess의 기본 테마 설치:

+
1
$ rake install
+

이부분에서 말을 안들을 수가 있는데, 뭔가 모듈의 버전이 맞지 않는 문제다.

+
1
2
3
4
5
6
D:\Blog\DevNote>rake install
rake aborted!
You have already activated rake 0.9.6, but your Gemfile requires rake 0.9.2.2. P
repending `bundle exec` to your command may solve this.
D:/Blog/DevNote/Rakefile:2:in `<top (required)>'
(See full trace by running task with --trace)
+

이 때 bundle update rake 해주면 해결. 다음 글을 참고했다.

+
1
2
3
4
5
6
7
D:\Blog\DevNote>bundle update rake
Fetching gem metadata from https://rubygems.org/.......
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Using rake (0.9.6)
...(중략)...
Your bundle is updated!
+

Octopress를 Github Pages용으로 설정

1
$ rake setup_github_pages
+

Github Pages는 계정 페이지와 프로젝트 페이지로 나뉜다.
각각의 경우에 따라 수동설정을 해주어야 하는데(이 부분은 두 번째 글에 잘 설명되어 있다.), 프로젝트 페이지의 경우가 조금 더 손댈 곳이 많다.

+
    +
  • 계정 페이지 설정인 경우
  • +
+

_config.yml에서 url, title, subtitle, author 정도만 수정해주면 된다.

+
    +
  • 프로젝트 페이지 설정의 경우
  • +
+

먼저 git remote 추가.

+
1
2
$ git remote add origin `https://github.com/username/projectname.git
$ git config branch.master.remote origin
+

_config.yml, config.rb, Rakefile 을 열어서 /github라고 된 부분을 repository 명으로 수정.

+

한글 인코딩 문제 해결

이제 부푼 꿈을 안고 첫 포스팅을 만들어보면 잘 동작한다.
하지만.. 한글을 사용하면 다시 인코딩 관련 에러를 만나게 된다.
여기서 엄청난 시간을 소모했는데, octopress 안에서 해결을 보려고 하니 힘들다. ruby는 한 번도 안써봐서 코드 보기도 힘들고 ㅡㅠ…
검색해보면 jekyll 코드 일부를 직접 수정하는 방법도 있는데,
그것보다 cmd창의 코드 페이지를 변경해주면 간단하게 해결된다.

+
1
chcp 65001 # 다시 되돌리려면 chcp 949
+

rake generate를 하거나 rake preview를 하기 전에, 코드페이지를 항상 변경해주고 실행한다. batch파일을 미리 만들어두니 편하다.

+

markdown 문법은 검색하면 어렵지 않게 찾을 수 있다.

+

블로그 내부 링크 만들기

기본으로 제공되는 기능이 없는듯? 플러그인 폴더에 아래 파일 하나 넣어주어야 한다.

+ +

여기 에서 참고했다. 아래 문법을 사용한다.

+
1
[link to this post]({% post_url 2012-01-05-hello-world %})
+

eof.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/07/14/move-to-octopress/index.html b/2014/07/14/move-to-octopress/index.html new file mode 100644 index 00000000..c277839e --- /dev/null +++ b/2014/07/14/move-to-octopress/index.html @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +move to octopress! | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ move to octopress! +

+ + +
+ + + + +
+

기존에 티스토리에서 운영 중이던 프로그래밍 관련 블로그(devnote.tistory.com)를 Octopress로 이사합니다. 사실 운영이라고 말하기도 뭣할 만큼 오랫동안 방치되어 있었는데, 다시금 분위기를 쇄신하고자 환경을 바꿔볼까 합니다.

+ + +

기존 블로그를 feedburner 주소로 구독중이었다면 새로운 블로그로 자동으로 넘어갑니다. 하지만 티스토리 기본 rss 주소를 사용중이었다면, 이참에 feed-burner로 갈아타 주세요 ‘ㅁ’)/

+

feed burder address : http://feeds.feedburner.com/florist_devnote

+

Octopress는 기존과는 다른 형태의 static engine이라서 호감이 갑니다. 맘에 드는 점을 몇가지만 꼽아보면

+
    +
  • vim으로 글을 적을 수 있다는 것
  • +
  • 본문 글이 로컬에 text(markdown)파일로 남는 다는 점
  • +
  • 블로그 주소에 github.io를 쓴다는 것
  • +
  • 기본적으로 큰 글씨를 사용하는 시원한 테마들.
  • +
+

… 등입니다. markdown으로 글을 적게 된다면 하루패드를 사용해야 겠다고 생각했었는데, vim으로 적는게 더 느낌이 좋네요 :) vim을 무척 잘 쓰는 편은 못되지만, octopress덕에 git이나 vim을 자주 접하게 되면 좀 더 익숙해 지는 계기가 될테니 그런 점도 마음에 듭니다.

+

그 외 나머지 추가 기능이나 설정 같은 건 아직 제대로 모르는 상태이지만, 하루 이틀 미루다보면 너무 늘어져 버릴 것 같아서 우선 이사 공표(?)부터 내지릅니다.

+

집에 애가 생기고 난 후 부터는 개인 시간이 많이 줄어들면서 블로그에도 소홀해지게 되었는데, 앞으로는 굳이 테크니컬한 내용의 글이 아니더라도 개발에 관련된 소소한 글들도 올릴 생각입니다. 이를테면 기계식 키보드에 대한 이야기라던가… 하는 것도요. (글쓰기 연습을 위해서라도 무엇이든 꾸준히 글을 좀 적어야 겠다는 개인적인 욕망(?) 때문입니다.)

+

앞으로 여러가지 글들 종종 올리겠습니다.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/07/16/the-benz-programmer/index.html b/2014/07/16/the-benz-programmer/index.html new file mode 100644 index 00000000..21265422 --- /dev/null +++ b/2014/07/16/the-benz-programmer/index.html @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +벤츠타는 프로그래머 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ 벤츠타는 프로그래머 +

+ + +
+ + + + +
+ + +

요 며칠간 이 책을 읽었습니다. 회사 도서관에 갔다가 제목이 끌려서 한 번 읽어봤어요.
누가 정한건지 모르겠지만 책 제목 참 멋지게 지었습니다. 주변에서 제가 이 책 읽는 것 보면 모두들 제목에 대해 관심을 보이더군요 ㅎ

+

진로를 고민중인 학생이나 일을 시작한지 얼마 되지 않는 신입 개발자들을 주 대상으로 삼은 책입니다. 다소 주관적이긴 하지만 선배 개발자 입장에서 들려주는 이런 저런 이야기들이 적혀 있어요.
저자는 자기관리를 잘 하는 분이신 듯 합니다. 구체적인 개인 목표를 세우고 달성을 위해 노력하는 점이라든지, 꾸준한 자기계발에 관심을 두는 점 같은 좋은 습관을 많이 만들어두신 것 같네요.

+ + +

저는 작업 도중에 빌드 걸어놓고 잠깐씩 기다리는 동안에 주로 읽었습니다.
빌드시간에 조금 난이도가 있는 기술서적을 읽을 때는, 내용을 좀 따라가려다 보면 빌드가 끝나서 흐름이 끊기고, 이게 계속 반복되다보니 책에 제대로 집중할 수가 없었습니다.
그래서 빌드시간에 책읽는 것은 거의 포기를 하고 있었는데, 이런 책은 부담없이 읽을 수 있어서 빌드 중에 읽어도 괜찮더군요.
그래서 앞으로는 빌드하는 중에 이런 가벼운 책들 읽으면 되겠구나 하는 생각을 해봤습니다.

+

저는 책을 읽다가 조금 엉뚱한 구절에 눈길이 확 쏠렸는데,

+

…결혼하고 아이들이 생긴 이후에는 집에서 어떤 일을 한다는 게 쉽지 않았다. 그래서 집중해서 집필하거나 공모전 참가 준비를 할 때는 주말마다 본가로 달려갔다. 본가에서는 식사 시간 이외에는 누구도 방해하는 사람이 없어서 원하는 일에 집중할 수 있었기 때문이다.

+
+ +

이 부분입니다. 저도 아이가 생긴 후에는 개인 시간을 내기가 쉽지 않아서 적잖이 고민을 하고 있는데, 주말마다 본가에 가서 혼자만의 시간을 가질 수 있었다는 저 이야기는 정말 부럽기 짝이 없네요 ㅜㅠ…

+ + +

저는 집에 아이가 생기고 한동안은 개인 시간은 아예 포기하고 지냈습니다. 주말마다 즐겁게 참여하던 스터디도 못 나가게 되었고, 집에서 컴퓨터 앞에 앉아 코딩을 하는 것은 거의 꿈도 꾸질 못했어요.

+

이제는 아이도 어느 정도 자랐고 하니 조금씩 개인 시간을 확보하고 다시 자기관리에 신경을 좀 써야겠다고 다짐했습니다. 이런 다짐을 한 것에는 최근에 이 책을 읽었던 것도 어느 정도 영향이 있었겠지요. 벤츠 타는 것도 난 바라지 않아요. 그냥 원하는 만큼 양껏 코딩하고 놀 수 있게만 됐으면 좋겠네요 ;ㅁ;)…

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/07/19/google-c-plus-plus-style-guide/index.html b/2014/07/19/google-c-plus-plus-style-guide/index.html new file mode 100644 index 00000000..7021a6e2 --- /dev/null +++ b/2014/07/19/google-c-plus-plus-style-guide/index.html @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +google c++ style guide | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ google c++ style guide +

+ + +
+ + + + +
+

지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.
본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요.

+

처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다.

+ + +

사실 컨벤션이 통일되면 좋다는 것은 아주 상식적인 말입니다만, 개개인이 선호하는 스타일이 다 다르기 때문에 통일을 하기가 쉽지 않다는 것이 문제입니다. 팀에서도 그동안 몇 차례 시도 했었지만 잘 안되었다가, 이번에서야 겨우 성공했어요.

+

이번에 컨벤션의 통일을 성공한 주된 요인 중의 하나는 구글 내부에서 사용하는 컨벤션을 정리해서 공개한 구글 C++ 스타일 가이드라고 볼 수 있습니다. 이 문서의 내용을 가져와 몇 가지 사항만 프로젝트에 맞게 조정하여 적용 하였지요. 구글 컨벤션의 코드들은 처음 볼 땐 좀 낮설었지만 적응하고 나니 이젠 괜찮군요.

+

팀에 도입하는 과정에서, 팀 내 능력자분들께서 원문을 한글로 깔끔하게 번역 & 정리해 주셨습니다.
구글에서 검색해보니 오래전에 번역되다가 말았던 문서들은 몇 개 보이는데 이번에 팀 내에서 번역한 문서는 아직 공유가 널리 안 된 것 같아서 다시 한 번 소개도 할 겸 포스팅 합니다. - 이 글의 목적입니다.

+

일단 간단한 샘플을 한 번 볼까요? (제가 구글 컨벤션을 100% 체득(?)한 상황은 아니지만, 대략적으로 분위기만 한 번 둘러보죠.)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
public:
Test();
virtual ~Test();

int some_value() const;
void set_some_value(const int val) {
some_value_ = val; // 간단한 예제이니 inline으로 짜봅니다.
}

private:
int some_value_;
};
+

구글 컨벤션의 아주 일부 규칙들이 몇 개 적용된 클래스 선언입니다.

+
    +
  • opening brace을 아랫줄로 내리지 않고 바로 붙여쓰는 것이나,
  • +
  • 들여쓰기는 스페이스 2칸.
  • +
  • 클래스의 멤버변수는 c스타일처럼 _로 연결된 소문자 단어를 사용하고 _를 끝에 붙인다는 점
  • +
  • getter는 멤버 변수의 이름과 같게,
  • +
  • setter는 set_변수명()의 규칙을 지닌다.
  • +
  • 클래스 접근 권한 지정자(public, private)는 스페이스 1칸 들여쓰기.
  • +
+

…정도가 보이네요. (변수명 선언 방식이 헝가리안 표기가 아니예요!)

+

구글 컨벤션을 따르는 코드의 예제는 breakpad, protobuf같은 구글의 오픈소스 프로젝트에서 볼 수 있습니다. 구글 코드 이외에도 몇몇 오픈소스들을 보면 구글 컨벤션의 영향을 받은듯한 코드들이 제법 보입니다. 얼마전에 잠시 가지고 놀았던 msgpack도 어느정도 구글 컨벤션의 영향을 받은 듯한 모양새를 가지고 있더군요.

+

구글 컨벤션은 위의 예제에서 보이는 단순한 들여쓰기, 줄바꿈 같은 형식 이야기 말고도 디자인 철학과 관련된 규약들이 많이 있어서, 평소 생각하지 못했던 여러가지 이슈들을 상기시켜줍니다. 문서 내용을 읽는 것만 해도 자신의 코딩 스타일에 대해 많은 점검(?)을 할 수 있어요.

+

개인적으로는 팀에서 정해진 룰 때문에 먼저 좀 겪어보게 되었는데 나쁘지 않더군요. 아직까지 마음에 안 드는 조항들도 몇 가지 있지만, 앞으로는 팀 코드가 아닌 개인 작업을 할 때에도 구글 컨벤션을 지켜 코딩해볼 생각입니다.

+

다시 한 번 링크 :

+

본격 컨벤션 적용을 위한 팁 :

vs2013의 서식 설정 옵션 활용

visual studio를 이용해 윈도우에서 개발하는 환경일 경우, IDE로 vs2013을 사용하면 많은 도움이 됩니다. 빌드는 예전 버전으로 하더라도 IDE만 vs2013을 사용할 수 있습니다. 2013에는 IDE의 자동 formatting 방식을 직접 설정할 수 있어요.

+ + +

게다가 vs2013 Update 2를 설치하면 설정 가능한 옵션이 좀 더 늘어납니다! Update 3는 아직 안나왔지만 나오면 설정이 더 늘어날지도!!

+

포맷팅을 자동으로 고쳐주는 AStyle 활용

AStyle이란 멋진 프로그램이 있어요. 포맷팅을 자동으로 고쳐주는 프로그램인데, 오픈소스로 되어있어 직접 수정 & 활용할 수 있습니다. 이걸 perforce 클라이언트인 p4v.exe pending changelist 창에서 일괄 적용하게 설정할 수도 있고, vs 플러그인으로 만들어서 코딩 중에도 실행해 볼 수 있어요. 포맷팅을 알아서 고쳐주니까 코딩중에는 들여쓰기가 어떻고 빈 칸이 어떤지 일일이 신경 쓸 필요 없으니 아주 편리합니다 -_-)b 강추예요.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/07/21/octopress-tips-on-windows/index.html b/2014/07/21/octopress-tips-on-windows/index.html new file mode 100644 index 00000000..b369ca3c --- /dev/null +++ b/2014/07/21/octopress-tips-on-windows/index.html @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Octopress Tips on windows | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ Octopress Tips on windows +

+ + +
+ + + + +
+

개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.

+ + +

윈도우 실행 (Windows + R) 창에서 블로그 패스로 바로 이동 하기

+ +

이거야 뭐… 환경변수에 블로그 경로를 넣어주면 된다. 이렇게 하면 실행 창에 %변수이름%만 입력하면 바로 탐색기를 열 수 있다.
환경 변수 설정을 해주는 PowerShell 스크립트를 만들어서 블로그 폴더의 루트에 놔두면 경로를 옮기거나 depot을 새로 받아도 편하게 셋팅할 수 있다.

+
ps_register_path.ps1code from github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 현재 스크립트의 실행 경로를 얻는다.
$blog_path = (Get-Item -Path ".\" -Verbose).FullName

# 경로 확인
"blog path : $blog_path"

# 실행 경로를 환경변수에 등록(유저 레벨)
[Environment]::SetEnvironmentVariable("blogpath", $blog_path, "User")

# output result
"Environment Variable update. {0} = {1}" -f "blogpath", $blog_path

# pause
Write-Host "Press any key to continue ..."
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+ + +

%blogpath% 이외에 자주 접근하는 경로는 바로가기를 만든다

+ +

octopress를 쓰면서 커맨드를 실행하는 주된 경로는 root path다. 이외에도 첨부파일 경로나 글 본문을 저장하는 ./source/_posts 등이 흔히 쓰이는데, 이런 경로에 대한 .lnk 파일을 만들어두면 훨씬 편하다.
위 스샷처럼 바로가기를 만들어두고 po정도 타이핑하고 엔터하면 ./source/_posts로 이동한다.

+

나는 탐색기를 주로 이용하고자 이렇게 했지만 cmd창에서 바로가기 하고 싶다면 symbolic link를 만들면 될거다.

+

웹페이지 바로가기도 만들어 두면 편하게 이동 가능. (웹 바로가기는 .url 확장자. 브라우저 주소창에서 슥 끌어다 놓으면 생김)

+

자주 쓰는 동작들은 스크립트로 자동화한다

+ +

Note : 이 항목이 이 포스팅의 핵심 입니다.

+

Octopress를 쓰면서 마음에 드는 점 중에 하나인데, 마음만 먹으면 조작 과정을 내맘대로 스크립팅할 수 있다는 점이다.
처음 octopress를 이용하려면 갖가지 명령어들을 일일이 숙지하고 사용하기가 불편한 것이 사실이지만,
batch파일과 PowerShell을 통해서 얼마든지 내 입맛대로 자동화 할 수 있다.
PowerShell을 한 번 다뤄보고 싶었지만 딱히 기회가 없었는데 이참에 다뤄보게 되어 재미있었다.
지금은 몇 개 안되긴 하지만 개인적으로 만들어 사용중인 스크립트들은 http://github.com/leafbird/devnote/ 에서 확인할 수 있다.

+

예제로 한 가지만 살펴보자.

+

자동화 예시 : 새글 작성을 간편하게

ocotpress에서 새 글을 적으려면 아래의 순서대로 실행해야 한다.

+
    +
  1. blog path로 이동.
  2. +
  3. cmd창 오픈
  4. +
  5. rake new_post['포스팅 제목'] 명령 실행
  6. +
  7. ./source/_posts로 이동
  8. +
  9. 자동으로 생성된 .markdown 파일을 찾아서 오픈
  10. +
  11. 글 작성 시작
  12. +
+

이 절차를 아래처럼 PowerShell로 스크립팅한다.

+
ps_rake_new_post.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 환경변수 BLOG_PATH에 설정된 블로그 root 경로로 이동
cd $env:blogpath

#input으로 새 글의 제목을 받는다.
$title = Read-Host 'Enter Title'

# 실행 : rake new_post['제목']
$argument = [string]::Format("new_post[{0}]", $title)
$out = rake.bat $argument

# 생성된 파일의 이름과 경로를 추출한다.
$out = $out.Replace("Creating new post: ", "")

# 생성된 파일을 gvim으로 오픈!
$new_file_path = [System.IO.Path]::Combine($PSScriptRoot, $out)
gvim.exe $new_file_path
+ +

커맨드 창에 PowerShell ./ps_rake_new_post.ps1 입력하는 것도 귀찮으니 이것도 batch파일로 만들자.

+
02_ps_rake_new_post.bat
1
2
@echo off
powershell ./ps_rake_new_post.ps1
+ +

이제 이 batch를 실행해서 새 글 제목을 입력하면 에디터까지 자동으로 열린다.

+

git conflict : 여러 머신에서 하나의 블로그에 번갈아 포스팅 하는 경우

git을 사용할 때 불편한 점 중의 하나가 머지(merge)다. 여러 머신을 사용할 경우엔 다른 곳에서 수정했던 사항을 미리 pull 받고 난 후 작업해야 하는데, 이걸 혹시나 깜박 잊고 새 글을 써서 generate했다면 conflict 대 참사가 일어난다.

+

blog root경로는 보통의 git repository를 사용하는 것과 유사하기 때문에 큰 문제가 없는데 _deploy폴더가 문제다. 이 폴더는 블로그 엔진이 generate한 블로그 리소스를 배포하기 위해 사용하는데, 실제로는 gh-pages 브랜치의 clone이기 때문이다. 그래서 서로 다른 여러 개의 depot clone을 가지고 블로깅을 할 땐 blog root와 함께 _deploy도 함께 git pull 해주어야 문제가 없다.

+

하지만 _deploy폴더는 굳이 동기화까지 받을 필요는 없다. 어차피 블로그 엔진이 배포하는 과정에서 새로 만들기 때문이다.
어떻게 활용하든 상관없지만 만약 _deploy폴더가 충돌이나서 html파일을 한땀 한땀 머지해야 하는 상황이 되었다면 주저없이 삭제해 버리고 새로 만들자.

+
1
2
3
4
5
6
7
8
cd %blogpath%
rmdir /s /q _deploy
mkdir _deploy
cd _deploy
git init
git remote add origin https://....
git pull
git check --track origin/gh-pages
+
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/08/19/yoda-notation/index.html b/2014/08/19/yoda-notation/index.html new file mode 100644 index 00000000..ac16d676 --- /dev/null +++ b/2014/08/19/yoda-notation/index.html @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Yoda Notation | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ Yoda Notation +

+ + +
+ + + + +
+ + +

지난번에 google c++ style guide 에 대해서 한참 수다를 떨었는데,
요즘에도 비슷한 주제의 책을 읽고 있습니다. 임백준씨가 번역하신 ‘읽기 좋은 코드가 좋은 코드다’ 인데요,
이것도 가볍게 읽을 수 있는 내용이어서 빌드 시간 중간에 띄엄띄엄 읽고 있어요.

+

이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 Yoda Conditions 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다.

+
May the force be with you.
1
2
3
4
int val = 20;
if(20 == val) { // <- yoda notation here.
...
}
+ + + +

조건문을 val == 20으로 적는 것이 일반적인 언어 어순과 같아서 읽기가 좋지만
프로그래머의 실수로 val = 20과 같이 잘못된 코드가 만들어지고 컴파일 에러 없이 그대로 실행되는 것을 막기 위해서
일부러 변수와 상수의 위치를 서로 바꾸는 거죠.

+

요다는 영화 스타워즈에서 영문권 사람들도 이해하기 어려울 정도로 꼬인 문법의 말을 사용합니다. 이를 빗대어 위와 같은 조건문 표기 방식을 Yoda Notation이라고 부르는군요. 재미있는 네이밍입니다 :)

+

책에서는 요즘의 컴파일러들이 조건절 내에서의 할당에 대해 경고를 내주기 때문에, 요다 표기법은 ‘점차 불필요한 과거의 일이 되어가고 있다’ 고 말합니다.
저도 쉽게 읽히지 않는 이상한 순서 때문에 요다 표기법을 안 좋아하는데,
책에서도 저랑 같은 생각을 이야기 하고 있어서 반갑네요. 이 뿐만 아니라 이 책은 전반적으로 소스코드의 스타일에 대해 많은 부분 공감가는 방식들을 다수 소개하고 있습니다.

+

예전에 함께 작업했던 어떤 프로그래머분이, 제가 올린 코드를 리뷰하고 나서 제가 추가한 코드의 조건절을 모두 요다 표기법으로 바꾸었던 적이 있습니다. 그거 참… 별 거 아닌데 기분이 나쁘더군요. 그 뒤로 요다 표기법이 싫어졌는지도 모르겠습니다. 하지만 어쨌든 이젠 옛날 이야기가 되어가고 있는겁니다. Visual Studio 2012 기준으로 /W4(경고 수준 4) 설정에 /WX(경고를 오류로 처리) 설정을 더하면 C4706 경고의 발생으로 인해 컴파일 시점에서 코딩 실수를 미리 잡아낼 수 있습니다.

+

에, 그러니까 내가 하고 싶었던 말은, 이제 이런 거 필요 없다 이겁니다 :)

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/09/12/claenup-cpp-project-1st/index.html b/2014/09/12/claenup-cpp-project-1st/index.html new file mode 100644 index 00000000..beb6816d --- /dev/null +++ b/2014/09/12/claenup-cpp-project-1st/index.html @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上 +

+ + +
+ + + + +
+

지워도 되는 헤더 인클루드를 색출하고 싶다

매우 느리게 찔끔찔끔 진행하는 토이 프로젝트가 있는데, 오늘 처음으로 무언가 그럴싸한 아웃풋이 나오게 되어 스냅샷을 하는 느낌으로 간단히 포스팅.

+

cpp 프로젝트 규모가 점점 커지게 되면 빌드 시간 때문에 많은 고통을 겪는다. 이때문에 increadi build 같은 분산 빌드 솔루션도 쓰는거고 unity build 같은 꼼수도 사용하게 되는거다.

+

하지만 저런 솔루션들을 사용하기 이전에, 코드를 정리하는 것이 먼저 선행될 필요가 있다. cpp는 특성상 작업하다보면 소스파일에 불필요한 헤더파일의 #include가 남게되고, 이것들이 불필요한 dependency를 만들어내면서 늘어지는 빌드 시간을 무시할 수 없기 때문이다.

+

그런데 문제는 그렇게 생긴 불필요 인클루드 구문이 무엇인지를 골라내기가 힘들다는 점이다. 프로젝트 규모가 커질수록 더욱 힘들다. c#같은 경우 불필요 using 구문을 아예 visual studio IDE가 자체적으로 정리해주기까지 하지만, cpp는 색출조차 힘들다 보니 이런 기능을 제공하는 3rd party tool도 없어 보인다. Whole Tomato의 Spaghetti 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다.

+

그래서 한 번 직접 만들어보기로 했다.

+ + +

프로젝트 내의 cpp 파일을 개별 컴파일 하기

일단은 만들려는 툴에서, 입력으로 받은 vc 프로젝트에 포함된 cpp 파일을 개별로 컴파일 할 수 있어야 한다.
그렇게 되면 cpp 파일마다 돌면서 코드 안에 있는 #include를 직접 하나씩 제거해보면서 컴파일이 성공하는지를 확인할거다. 그러면 불필요할 것이라 예상되는 #include의 후보를 만들 수 있다.

+

무식한 방법이다. cpu를 많이 먹을거고 시간도 오래 걸릴거다. 하지만 저렇게라도 알 수 있다면 새벽에 실행해서 리포트 뽑아놓도록 CI에 물려놓으면 그만이다.

+

무식하기도 하지만 또한 불완전한 방법이기도 하다. 위의 동작으로 불필요 #include 후보 리스트를 만들었다고 해도,
헤더파일 끼리의 상호 참조관계, 내부 포함 관계등이 여러 복잡한 상황을 연출하기 때문에
후보로 지목된 헤더가 실은 필요한 녀석일 수도 있다.

+

하지만 일단은 후보 리스트 색출까지 먼저 진행해 보기로 한다.
사실 정말 확실한 불필요 #include가 색출 가능하다면 tool이 아예 코드를 코치는 것까지 자동으로 처리해 줄 수도 있을 것 같지만… 일단 나중에 생각하기로.

+

프로젝트에 포함된 cpp 파일의 리스트를 구하는 것은 일도 아니다. vcxproj파일은 xml 형태로 되어 있으므로, /Project/ItemGroup/ClCompile 경로의 xml element를 얻어와 파일 경로를 읽어내면 끝이다.

+

그다음은 이 파일을 각각 컴파일 할 수 있어야 하는데… 이것은 생각보다 만만치가 않다. cl.exe를 실행해서 컴파일 하면 되지만, cl.exe의 커맨드라인 옵션으로 들어가야 하는 인자가 엄청나게 많고, 이 옵션을 vcxproj 파일에서 일일이 파싱하고 다시 조합하기란 상당히 귀찮고 짜증나는 작업이다.

+

이 귀찮은 작업을 MSBuild에 맡겨버릴 수 있다. MSBuild에 /t:BuildCompile 옵션과 /p:SelectedFiles=xxx을 쓰면 vcxproj를 알아서 파싱해서 cl.exe의 커맨드라인 인자를 직접 만들어준다.

+

이렇게 해서 일단 프로젝트 파일에 있는 cpp를 개별 컴파일 하는 것까지 성공.

+ + +

여기까지 하고 나니 cpp 파일당 컴파일 시간까지 덤으로 얻게 됨.
앗싸.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/09/17/cleanup-cpp-project-2nd/index.html b/2014/09/17/cleanup-cpp-project-2nd/index.html new file mode 100644 index 00000000..2844494a --- /dev/null +++ b/2014/09/17/cleanup-cpp-project-2nd/index.html @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C++ 코드 정리 자동화 - 2. 불필요한 #include 찾기 下 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C++ 코드 정리 자동화 - 2. 불필요한 #include 찾기 下 +

+ + +
+ + + + +
+

이전 포스트 ‘C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上‘ 에서 이어진다.

+

지워도 되는 인클루드를 찾아냈다

개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.

+

이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.

+
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
27
28
작업 대상으로 1개의 프로젝트가 입력 되었습니다.
-------------------------------------------------
Service : 프로젝트 정리.
Service : PCH 생성.
컴파일 : stdafx.cpp ... 성공. 걸린 시간 : 1.04초
Client.cpp의 인클루드를 검사합니다.
- process #1 Client.cpp (1/2) ... X
- process #1 Client.cpp (2/2) ... X
ClientAcceptor.cpp의 인클루드를 검사합니다.
- process #1 ClientAcceptor.cpp (1/2) ... 컴파일 가능!
- process #1 ClientAcceptor.cpp (2/2) ... X
ClientConnection.cpp의 인클루드를 검사합니다.
- process #1 ClientConnection.cpp (1/3) ... X
- process #1 ClientConnection.cpp (2/3) ... X
- process #1 ClientConnection.cpp (3/3) ... X
Start.cpp의 인클루드를 검사합니다.
- process #1 Start.cpp (1/4) ... X
- process #1 Start.cpp (2/4) ... X
- process #1 Start.cpp (3/4) ... X
- process #1 Start.cpp (4/4) ... X
ThreadEntry.cpp의 인클루드를 검사합니다.
- process #1 ThreadEntry.cpp (1/1) ... X
-------------------------------------------------
Project : Service 모두 1개의 인클루드가 불필요한 것으로 의심됩니다.
D:\Dev\uni\World\Service\ClientAcceptor.cpp
- 2 line : #include "World/Service/Client.h"

총 소요 시간 : 13.289 sec
+ + +

이 정도 만들어서 회사에서 만들고 있는 프로젝트에 조금 돌려 보았는데, 덕분에 꽤나 많은 불필요 인클루드를 색출해 내었다. 회사 프로젝트는 덩치가 제법 크고, 아직 서비스 중이지 않은 코드여서 용감무쌍한 리팩토링이 자주 일어나기 때문에 관리가 잘 안되는 파일이 제법 있더라. 아무튼 덕을 톡톡히 보았다.

+

튜닝 : 솔루션 단위로 검사할 수 있게 만들자

프로젝트 파일 단위로 어느 정도 돌아가니까, 솔루션 파일 단위로도 돌릴수 있게 확장했다. sln 파일을 파싱해서 프로젝트 리스트만 얻어오면 끝나는 일이다.

+

하지만 sln 파일은 vcxproj 파일처럼 쉽게 파싱할 수는 없다. 이녀석은 xml 포맷이 아니라, 자체적인 포맷을 가지고 있다. 사실 sln 파일을 파싱해 본 게 이번이 처음이 아닌데, 예전에는 lua를 써서 직접 노가다 파싱을 했더니 별로 재미도 없고 잘 돌아가지도 않고 코딩하는 재미도 별로 없더라.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // 솔루션 파일은 이렇게 생겼다. 왜죠...

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
MinimumVisualStudioVersion = 10.0.40219.1
... 중략 ...
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F95C61E3-AF95-4CA9-8837-A203762B2B29}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gtest", "External\gtest\gtest.vcxproj", "{C7A81BFC-6E28-4859-A8B5-2FEA80E012B2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{042F2157-2118-44AA-8BB9-8B5DD01FA3A9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "unittest", "Test\unittest.vcxproj", "{24A57754-D332-4575-AEBF-2AFCBC0A7E4B}"
EndProject
... 후략 ...
+

C#으로 sln 파일을 파싱해주는 적당히 괜찮은 코드가 인터넷 어딘가에 돌아다닌다. 이곳에 있는 놈을 가져다 붙였다. build configuration 같은 걸 얻어올 순 없지만 프로젝트 리스트 얻는 데에는 충분하다.

+

튜닝 : 느리다. 멀티 스레드로 돌리자

한때는 툴을 만들때 lua도 써보고 python도 써봤지만 요즘은 C#만 쓰게된다. 디버깅 하기도 편하고, 특히 멀티스레딩으로 돌리기가 너무 편하다. TPL, Concurrent Collection조금 갖다 끄적거리면 금방 병렬처리된다.

+

특히나 이런 식으로 병렬성이 좋은 툴은 훨씬 빠르게 돌릴 수 있게 된다. 커맨드 라인 인자로 --multi-thread를 주면 주요 작업을 Parallel.ForEach로 돌리도록 처리했다. 다만 멀티스레드로 돌리면 파일로 남기는 로그가 엉망이 되기 때문에… 단일 스레드로도 돌 수 있도록 남겨둠.

+

이번엔 병렬처리할 때 thread-safe한 container가 필요했는데, System.Collections.Concurrent에 가면 queue, stack, dictionary등 종류별로 잔뜩 들어있으니 적당한 놈으로 바로 갖다 쓰면 된다. 편하다 C#. 네이티브 코더는 그냥 웁니다 ㅠㅠ…

+

지금 내가 가진 개인 코드 중에는 덩치큰 cpp 프로젝트가 없어서, 조그만 솔루션 하나 시험삼아 돌려봤다.

+ + +

87초 걸리던 것이 24초로 빨리짐. 대충 4배 가량 빨라졌다. 내일 회사에서 대빵 큰 프로젝트에 한 번 돌려봐야지. 생각하니 기대된다.

+

More Improvement : 불필요한 전방선언(forward declaration) 색출.

툴을 좀 더 확장할 수 있을거 같다. 클래스와 구조체 전방선언을 써놓고 지우지 않아서 찌꺼기가 된 부분을 이것으로 찾아낼 수 있을 것 같다. 이건 파일을 일일이 컴파일 하지 않아도 되니까 훨씬 빠르게 가능할 듯.

+

전방선언 확인 작업도 따지고 보면 단순 string 처리니까… 시간될 때 카페에 가서 찬찬히 코딩하다보면 금방 짤 수 있겠지. cpp 파일을 write하는 작업도 없어서 read만 하면 되기 때문에 아마 병렬성도 훨씬 더 좋을 것이다.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2014/09/30/cleanup-cpp-project-3rd/index.html b/2014/09/30/cleanup-cpp-project-3rd/index.html new file mode 100644 index 00000000..8dd907d0 --- /dev/null +++ b/2014/09/30/cleanup-cpp-project-3rd/index.html @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C++ 코드 정리 자동화 - 3. pch 사이즈 확인, #include 순서정리 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C++ 코드 정리 자동화 - 3. pch 사이즈 확인, #include 순서정리 +

+ + +
+ + + + +
+

pch 파일 사이즈

팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.

+ + +

MSBuild로 단일 cpp 파일을 컴파일하면 이런 메시지가 나오는데,

+
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
27
28
29
30
31
32
33
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\CL.exe 
/c
/ID:\Dev\uni\External\
/ID:\Dev\uni\Test\
/ID:\Dev\uni\
/Zi
/nologo
/W4
/WX
/sdl
/Od
/D WIN32
/D _DEBUG
/D _CONSOLE
/D _LIB
/D _UNICODE
/D UNICODE
/Gm
/EHsc
/RTC1
/MDd
/GS
/fp:precise
/Zc:wchar_t
/Zc:forScope
/Yc"stdafx.h"
/Fp"x64\Debug\unittest.pch"
/Fo"x64\Debug\\"
/Fd"x64\Debug\vc120.pdb"
/Gd
/TP
/errorReport:queue
stdafx.cpp
+

여기 cl.exe로 들어가는 인자 중에 /Fp"x64\Debug\unittest.pch" 요 부분에 pch 경로가 있음. 그러니까 결국 툴에서 pch사이즈를 구하려면

+
    +
  1. 프로젝트 리빌드하고
  2. +
  3. pch 생성 헤더를 cl.exe로 컴파일하면서 /Fp 스위치를 읽어 경로 파악.
  4. +
  5. 위에서 새로 생성된 pch파일의 사이즈를 확인.
  6. +
+

… 해주면 된다.

+

#include 순서 자동 정렬

구글의 C++ 스타일 가이드 문서 중에 include 의 이름과 순서 항목에 보면 헤더 인클루드에 몇가지 카테고리와 순서를 정해 두었는데,

+

주된 목적이 dir2/foo2.h에 있는 것들을 구현하거나 테스트하기 위한 dir/foo.cc나 dir/foo_test.cc에서 include를 아래처럼 순서에 따라 배열하라.

+
    +
  1. dir2/foo2.h (아래 설명 참조).
  2. +
  3. C 시스템 파일
  4. +
  5. C++ 시스템 파일
  6. +
  7. 다른 라이브러리의 .h 파일
  8. +
  9. 현재 프로젝트의 .h 파일
  10. +
+
+ +

팀에서 정한 컨벤션도 이 규칙을 그대로 따라야 해서.. 매번 코딩할 때마다 인클루드 순서에 신경쓰기 싫어서 자동화 처리를 작성. 더불어 경로 없이 파일명만 적은 경우나 상대경로를 사용한 인클루드도 지정된 path를 모두 적어주도록 컨버팅하는 처리도 만듦. 만드는 과정이야 대단한 건 없다. sln, vcxproj파일 파싱하는 것은 만들어 두었으니, 그냥 스트링 처리만 좀 더 해주면 금방 만들어진다. 툴로 sorting하고나면 아래처럼 만들어줌.

+
TestCode.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"
#include "TestAsset/ProjRoot/TestCode.h"

// system headers
#include <vector>

// other project's headers
#include "TestAsset/OuterProject.h"
#include "TestAsset/OuterProjectX.h"

// inner project's headers
#include "TestAsset/ProjRoot/InterProject.h"
#include "TestAsset/ProjRoot/InterProjectA.h"
#include "TestAsset/ProjRoot/InterProjectB.h"
#include "TestAsset/ProjRoot/InterProjectC.h"

void main() {
return;
}
+

epilog

대충 이정도 돌아가는 툴을 만들어서 개인 pc에 셋팅해둔 jenkins에 물려놓고 사용중. 원래는 필요없는 include찾아주는 기능만 만들려다가 include sorting 기능은 그냥 한 번 추가나 해볼까 싶어 넣은건데, 아주 편하다. 코딩할 땐 순서 상관 없이 상대경로로 대충 넣어놓고 툴을 돌리면 컨벤션에 맞게 예쁘게 수정해준다.

+

불필요 인클루드를 찾는 동작은 회사 코드 기준으로 컨텐츠 코드 전체 검색시 50분 정도 걸리는 듯. 이건 매일 새벽에 jenkins가 한 번씩 돌려놓게 해놓고, 매일 아침에 출근해서 확인한다.

+

pch사이즈는 baseline 구축을 생각하고 만들어 본건데.. (박일, 사례로 살펴보는 디버깅 참고) baseline을 만들려면 지표들을 좀 더 모아야 하고, db도 붙여야 하니 이건 제대로 만들려면 시간이 필요할 것 같다(..라고 쓰고 ‘더이상 업데이트 되지 않는다’ 라고 읽는다.)

+

그리고 C#.

C#은 재미있다. 이번에 툴 만들때도 한참 빠져들어서 재미있게 만들었다. Attribute를 달아서 xml 파일을 자동으로 로딩하는 처리를 만들어 보았는데, cpp에서 하기 힘든 깔끔한 이런 가능성들이 마음에 든다. 규모 큰 프로젝트는 안해봐서 모르겠지만 개인적으로 가지고 놀기에는 제일 맘에 듬. 디버깅 하기 좋고 코드 짜기도 좋고.

+

Visual Stuio Online

코드 관리를 visual studio online에서 해봤다. 비공개 코드는 주로 개인 Nas나 bitbucket에 올려놓는데, VS IDE에서 링크가 있길래 한 번 눌러봤다가 한 번 써봄.
bitbucket보다 좀 더 많은 기능이 있다. 빌드나 단위테스트를 돌려볼 수 있고(하지만 유료), backlog, splint관리용 보드가 좀 더 디테일하다. 개인 코딩 말고 팀을 꾸려서 작업을 한다면 한 번 제대로 사용해 보는 것을 고려해 볼 순 있겠으나… 왠지 그냥 마음이 안간다. 나같으면 그냥 github 유료 결제해서 쓸 거 같애 ‘ㅅ’)

+

이제 이건 고마하고 다음 toy project로 넘어가야지.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/00.png" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/00.png" new file mode 100644 index 00000000..b2650066 Binary files /dev/null and "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/00.png" differ diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/01.jpg" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/01.jpg" new file mode 100644 index 00000000..b55abfa3 Binary files /dev/null and "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/01.jpg" differ diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/02.jpeg" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/02.jpeg" new file mode 100644 index 00000000..b5457294 Binary files /dev/null and "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/02.jpeg" differ diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" new file mode 100644 index 00000000..54a92f88 --- /dev/null +++ "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +테크니컬 리더십: 시작하기 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ 테크니컬 리더십: 시작하기 +

+ + +
+ + + + +
+

예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.

+ + + + +

테크니컬 리더십: 시작하기

내가 소프트웨어 엔지니어가 되기 전에는 이 직업에서 가장 중요한 점은 코딩이라고 생각했다. 그것은 잘못된 생각이었고, 소프트웨어 공학의 가장 중요한(그리고 가장 어려운)점은 다른 사람들과 원만하게 잘 협력하는 것이다.

+

나는 “관리자는 되지 않을거야!”라고 스스로에게 말해왔고, “그렇게 하면, 내 모든 에너지를 개발에만 집중시킬 수 있을거야!” 라고 생각했다. 내 이후의 경력도 기술 지향적인 실무자 위주로만 관리해 간다면 이 어려운 대인관계를 어느 정도 무시할 수 있을 거라고 생각했다.

+
+

빨리 가려거든 혼자 가고, 멀리 가려거든 함께 가라.

+
+

내가 업무에서 대인관계를 소홀히 여기던 때 의아하게 생각했던 점은 “왜 사람들은 나의 의견을 들어주지 않지?” 하는 점이었다. 이는 슬랙(Slack)의 플랫폼 팀에서 처음 작업을 시작했을 때 특히 그러했다. 나는 슬랙의 API가 토큰을 사용하고 있는 점을 변경하여 보안을 강화하고, 제품 개발팀 전체에 걸쳐 일관된 개발 과정을 유지하도록 개선하고 싶었다. 그러나 몇 달 동안, 나의 제안이 많은 이들의 시간을 보다 가치있게 활용할 수 있는 방법이라고 PM이나 팀원들을 설득하는 것은 불가능했다.

+

이후로도 몇 차례 나의 의견은 받아들여지지 않고, 같은 팀 수석 엔지니어들의 의견이 채택되는 것을 지켜보면서 내게 무언가 빠진 요소가 있다는 것을 알게 되었는데, 그것은 바로 ‘리더십’이었다. 나는 매일같이 키보드에만 코를 박고 있으면 안되는 것이었다. 내가 성장하기를 원한다면, 다른 사람들이 나와 동등한 수준으로 기여할 수 있도록 도움을 주어야 했던 것이다. 나는 리더십을 통해 나의 영향력을 키워야 할 필요가 있었다.

+

이 글을 통해 필자 스스로가 리더십에 대해 배운 점과, 개발자 리더십의 절차(Path)에 대해 이야기해 보고자 한다.

+

자기 자신을 리딩하기

슬랙의 엔지니어로 지내면서, 나는 관리(management)와 리더십(leadership)이 어떻게 다른지 이해하게 되었다.

+

관리자(manager)는 자신의 보고서에 대한 책임이 있다. 관리자들은 코칭과 구조화를 통해 좋은 팀을 구축하는 것에 중점을 둔다. 또한 팀의 성장을 위해 성과를 관리한다.

+

관리자(manager)는 종종 리더(leader)를 겸임하지만, 리딩은 사실 다른 누구라도 할 수 있는 별개의 것이다. 리딩은 권위에 의존하는 무언가가 아니라, 다른 사람에게 미치는 영향력에 대한 것이다. 리딩은 비전에 대해 소통하고, 비전을 실현하기 위해 다른 이들에게 힘을 실어주는 것이다.

+

당신은 다른 이들을 리드하기 전에, 먼저 당신 자신을 리드할 수 있어야 한다. 자신을 리딩하는 것은 타인을 리드하거나, 조직을 리드하기 전에 반드시 먼저 선행되어야 한다. 자신을 리딩한다는 개념은 다양한 분야와 기업에서 정리한 여러 리더십의 정의들에서 찾아볼 수 있다.

+

자기 자신을 리딩하는 것은 그 사람의 우수한 역량과 밀접한 관련이 있다. 모범적인 자세를 통해 드러나는 리더십은 타인에게 자극을 주는 가장 강력한 방법이기 때문이다. 자신을 리딩한다는 말은 늘 최선을 다해서 개인의 업무를 수행하고, 스스로가 만들어내는 결과물의 품질에 대해 책임을 지는 것을 의미한다.

+

자기 자신에게 성공적인 리더십을 발휘하기 위한 다섯하기 요소는 방향 맞추기, 전문가 되기, 공유하기, 일관되게 실행하기, 효과적인 의사소통하기 이다.

+

방향 맞추기(Finding Alignment)

+ +

직장에서 우수함을 나타내려면 먼저, 팀을 이해해야 하고, 회사를 이해해야 한다.

+

‘원칙’이란 어떤 행동이 바람직한지, 혹은 바람직하지 않은지를 안내하는 회사의 규범을 말한다. 대개는 이런 원칙들이 명확하게 규정되지 않은 경우도 많은데, 이런 숨은 원칙을 잘 찾아내는 것 역시 개인의 몫이다. 이 원칙들은 당신의 나침반과도 같다. 원칙들은 당신이 회사의 목표와 가치에 맞는 결정을 내리는데 큰 도움을 줄 것이다.

+

슬랙에서의 예를 들어보면, 우리는 슬랙의 사용자들에게 매우 뛰어난 사용 경험을 제공하고 있다는 믿음이 있다. 어떤 고객이 슬랙의 핵심 기능 중의 하나가 망가졌다는 제보를 한다면, 나에게는 그 즉시 내가 하고 있던 일을 모두 멈추고 현상을 확인해 즉시 문제를 해결하는 것이 가장 중요하다. 하지만 다른 회사에서는 내가 하던 일을 내팽개치는 것이 완전히 잘못된 판단이 될 수도 있는 것이다.

+

대부분의 결정은 여러가지 가치를 두고 다각도로 고민하면서 내려져야 한다. 오늘은 그동안 쌓아둔 기술 부채를 해결하는데 시간을 쓸 것인가? 아니면 좀 더 미루고 내일의 작업을 위한 기반작업을 할 것인가? 버그를 잡는 것, 툴을 만드는 것, 새로운 기능을 개발하는 것이 더 중요하진 않은가? 직장에서 할애할 수 있는 총 시간과 에너지의 양은 제한되어 있다. 회사가 중요하게 생각하는 것과 개인이 노력을 기울이는 방향을 동일하게 맞출 때 당신의 기여도는 가장 최대의 효율을 발휘할 것이다.

+

방향성 맞추기는 단지 회사가 당신에게 바라는 일을 수행하는 것만을 뜻하지 않는다. 여러분들은 리더로서 여러가지 문제를 직면하고, 이를 해결하기 위한 (숨어있는) 솔루션을 제시할 숱한 기회들을 마주하게 될 것이다. 하지만 그 때마다 다른 동료들에게 이것이 왜 문제이며, 왜 이를 해결하기 위해 에너지를 써야 하는가를 납득시키기 위해서는 먼저 회사가 무엇을 중요하게 생각하는지를 이해하고 다른사람에게 잘 설명할 수 있어야 한다.

+

전문가 되기(Become an Expert)

전문가가 되는 것은 개인 스킬을 연마하는 것에 관한 이야기다. 잠재력을 가진 상태라는 것이 하나의 좋은 자질일 순 있겠지만, 그걸로는 충분하지 않다. 리더는 실제로 뛰어난 전문가(export)여야 한다. 콜로라도 대학의 앤더스 에릭슨 교수에 따르면, 전문가가 되기 위해서는 평균 10년 이상 높은 수준의 의식적인 노력을 10,000시간 이상 기울여야 한다고 말한다.

+

사람들은 종종 내가 오페라를 불렀던 경험이 소프트웨어 공학 경력에 도움이 되는지를 묻곤한다. 맞다! 음악을 통해서 나는 스스로의 마음가짐을 발전시킬 수 있었다. 아리아를 연습할 때면 가장 자신 없는 파트를 제일 자신있는 파트만큼의 자연 스러운 소리가 나올 때까지 몇시간이고 반복해서 연습했다. 소프트웨어 공학도 이것과 똑같다. 우리는 자신이 취약한 부분을 개발하는데 더욱 많은 시간을 투자해야 한다.

+

숙련을 쌓는 방법에 지름길이란 없다. 다만 꾸준하고 의식적인 노력으로 개발시키는 것 뿐이다. 내 자신에게(그리고 당신 주변의 사람들에게) 질문을 던져보자: 내가 가장 크게 성장할 수 있는 분야는 무엇인가? 전문가가 되기 위해서 나는 어떤 스킬을 개발해야 하는가?

+

당신이 개발하기 원하는 많은 스킬들이 있을 수 있지만, 노력을 기울이기 전에 먼저 다음의 질문을 던져보기를 권장한다: 그 스킬은 회사가 추구하는 방향에 부합하는가? 그 스킬은 나의 개인적인 목표에도 부합하는가?

+

‘아직 아무것도 이룬 것이 없다’는 생각만 하고 있을 게 아니라 매일 꾸준히 지식과 스킬을 체득하고자 노력하는 과정이 필요하다. 누구나 태어날 때부터 전문가였던 사람은 없다.

+

공유하기(Share)

자기 자신을 리딩하는 과정이 지나면, 다른 사람을 리드할 기회가 주어지고, 당신의 동료들이 최고의 성과를 내도록 역할을 부여하게 된다. 이를 성공적으로 수행하기 위해서는 먼저 지식을 공유해야 한다.

+

스킬을 습득하기 위해 많은 개인 시간을 소비한 후라면 선뜻 지식을 공유하는 것이 쉽지 않을 수도 있다. 특별한 전문성을 혼자만 “소유”하고 싶은 것은 본능적인 생각이다. 전문 지식은 체득 과정의 노력이 보이지 않을 땐 마치 마술처럼 느껴질 수도 있다. 당신은 혼자만의 마법을 비밀 상자에 숨겨놓고 외딴 곳에 보관하고 있다가 필요할 때만 꺼내서 사용하고 싶어할 수 있다. 다른 사람들은 그걸 어떻게 하는지 모르기 때문에, 당신만의 전문성은 여전히 마법을 유지하게 될 것이다.

+

하지만 바로 이 부분이 핵심이다. 당신의 노하우를 혼자만 알고 있으면 동료들은 당신에게 의존하게 되고, 결국 동료들의 성장을 방해하는 셈이 된다. 당신 스스로도 새로운 일을 배우는 것을 불안하다고 여기게 되어, 자신의 성장마저 방해하는 셈이 된다. 당신은 동료들이 팀에 기여하는 것을 막고 있으며, 팀을 아주 적극적으로 망치고 있는 셈이다.

+

나도 내가 가진 정보를 혼자만 유지하곤 했는데, 일부러 숨기고자 해서 그랬다기 보다는 이것이 유익한 정보인지 깨닫지 못했던 경우였다. 예를 들어, 나의 프로젝트에서는 업무의 진행을 방해하는 일반적인 문제점들에 대해 탐구하고 정리해왔다: 킥오프, 최종 마일스톤, 회귀 없는 릴리즈 같은 것들(역주: 예시의 내용들이 무엇을 말하는지는 잘 모르겠습니다). 나는 주위 동료들도 함께 성공했으면 하는 마음에 내가 유지하던 정보들 중 다른 팀들과 공유할 수 있는 기술들을 분류하기 시작했다. 사실 내 프로젝트만 잘 돌아가면 상관 없는 일이었지만.. 그것은 추가 확장이 없는 x1배의 영향력이다. 허나 이런 정보들은 모든 팀들에게 적용 가능한 것들이었고, 이것은 xN 배의 영향력을 발휘하게 된다.

+

지식을 숨기는 대신 공유하라. 멘토링이나 페어 프로그래밍 같은 1:1 방식도 좋고, 프레젠테이션이나 문서화 같은 1:N 방식도 좋다. 당신이 배운 사실을 다른 사람들에게도 가르쳐라. 그럼 다른 사람들은 다시 그 다음 사람들을 가르칠 것이다. 당신은 다시 배우고자 하는 그 다음 스킬로 자유롭게 이동할 수 있다. 지식이란 마르지 않는 샘이다. 아무리 배워도 항상 더 많이 남아있다.

+ + + +

일관되게 실행하기(Execute Consistently)

일전에 나의 관리자와 나눴던 대화가 기억난다. 나는 관리자에게 최근의 프로젝트에서 내가 매우 뛰어난 성과를 기록했다고 말하고, 내가 언제쯤 승진할 수 있느냐고 질문했다. 그는 현명하게 대답했다: “당신은 이번과 같은 좋은 성과를 일관되고 꾸준하게 달성할 수 있음을 증명해야 합니다.”

+

일관성. 그것은 일시적인 운과 리더십의 차이를 말해준다.

+

당신이 어느 한가지 일을 딱 한 번 잘해냈다는 것은 별로 중요하지 않다. 정말 중요한 것은 당신이 그 일을 다시, 또 다시, 그리고 또 다시 잘 해낼 수 있는가 하는 것이다.

+

일관성 있는 실행력을 갖기 위해서는, 다양한 규모와 유형의 여러가지 프로젝트를 해봐야 할 것이다. 작은 규모, 큰 규모, 복합적인 기능, 사용자 친화적 UX, 백엔드 솔루션 등등. 이러한 경험들에서 당신은 다양한 도전 과제를 마주하고 해결 방안들을 개발하게 된다. 당신의 약점이 무엇인지를 드러내주고 당신이 스킬을 연마하도록 도울것이다.

+

당신의 관리자에게, 당신이 익히려고 하는 기술들을 미리 공유하라. 앞으로 맡게 될 프로젝트를 주시하고 그 중에 자신이 흥미가 가는 부분이 무엇이며 왜 그렇게 생각하는지를 관리자에게 미리 알려라. 당신이 지금 프로젝트를 진행중이라면, 작업하는 동안 나는 어떤 스킬을 선정해 발전시켜갈 것인가에 대해 생각하라. 이것은 직장에서의 시간을 최대의 효율로 활용하는데 큰 도움을 줄 것이다.

+

때로는 당신이 크게 열정을 느끼지 못하지만 팀의 임무에는 중요한(mission-critical)일에 배정이 될 때도 있다. 당신은 이 또한 잘 해낼 수 있음을 증명해야 한다.

+

일관되게 실행하는 것은 개인의 브랜드를 개발시키고 동료들에게 신뢰를 쌓을 수 있는 방법이다. 신뢰감을 형성하고 키우는 데에는 많은 시간과 경험이 필요하다. 하룻밤 만에 만들어지지 않는다. 한 번 신뢰를 얻었다 하더라도 지속적인 노력이 뒤따라야만 이를 오래도록 유지할 수 있다.

+

효과적인 의사소통하기(Communicate Effectively)

“왜 사람들이 내 말을 들어주지 않는거야?” 하고 궁금해 한 적이 있는가?

+

나는 신입일 때 여러 차례 위와 같은 질문을 하곤 했다. 그러던 어느날 문득 내가 성장의 준비가 되었을 즈음에, 사장님이 중요한 단서를 주었다: 나는 동료들에게 부정적인 성향으로 인식되고 있었다는 점이다. 처음엔 그 피드백을 듣고 기분이 상했다. 하지만 이것이 나의 경력에서 중요한 전환점이 되었다. 그 후로 나는 ‘목소리’ 코치와 함께 일하게 되었고, 효과적인 커뮤니케이션의 중요한 비밀을 깨닫게 되었다. 그것은 경청(listening)이다.

+

경청이란 단순히 정보를 받아들이는 것이 아니다. 경청은 정보와 함께 그것의 맥락을 모두 합쳐 하나의 덩어리로 합성하는 것이다. 경청은 상대방의 의견이 어디에서 왔는지를 이해하고, 더 깊은 이해를 얻기 위해 명확한 질문을 던지는 것을 말한다. 이 합성의 듣기는 효과적인 커뮤니케이션의 가장 기본임과 동시에, 당신이 말하고자 하는 아이디어에도 엄청난 힘을 실어준다 - 믿거나 말거나.

+ + +
+

레벨이 올라감에 따라 관리자 트랙과 엔지니어 트랙에게는 모두 동일한 의사소통 기술이 요구됩니다. 각 트랙의 진정한 능력자들이 서로 다른 트랙의 능력자를 존재할 수 있게 만듭니다. - Sarah Mei

+
+

효과적인 의사소통의 또 다른 측면은 적절한 맥락으로 반복하는 것이다. 사람들이 왜 내 말에 귀기울이지 않는지 몰랐을 때의 나는 했던 말을 다시 반복해야 할 때면 화를 내면서 말했다.

+

나중에서야 효과적인 의사소통의 고수들을 관찰하기 시작했다. 그들은 다방면으로 정보를 노출한다. 적절한 시간 간격을 두고 반복적으로 정보를 전달하고, 듣는 사람이 누군가에 따라 그에 맞는 다양한 세부 정보들을 제공한다.

+

정보를 듣고 종합하고, 효과적으로 공유하는 방법을 익히는 것은 직급에서 오는 권위에 의존하지 않고 사람들에게 영향을 미치는 기본적 기술이다. 모두가 하나의 비전을 바라하도록 사람들을 모으기 위해서는 이러한 영향력이 필요하다.

+

. . .

+

소프트웨어 엔지니어로 일을 시작할 때, 왜 나의 아이디어가 회사에서-그리고 업계에서-잘 받아들여지지 않는 것인가를 궁금해했다. 그러던 중 컴퓨터만 골똘히 들여다보던 시선을 잠시 벗어나, 주변의 훌륭한 동료들을 만나보게 되면서 깨달았다. 내가 생각하는 방향성을 다른 사람들이 함께 공감하고, 실현하기 위해 같이 노력하도록 동기부여할 수 있다면 훨씬 더 큰 영향력을 미칠 수 있다는 것을.

+

리더십에 관해서는 배워야 할 것이 많고, 필자 개인적으로는 더 많은 것들을 배워야 한다. 리더급 개발자가 되고자 한다면, 먼저 자기 자신을 리딩하는 것부터 시작하기를 권한다. 이 외에 당신이 찾아낸 리더십에 대해 내게도 알려주길 바란다!

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/00.jpg" "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/00.jpg" new file mode 100644 index 00000000..73d9fcfc Binary files /dev/null and "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/00.jpg" differ diff --git "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" new file mode 100644 index 00000000..4b5e4da7 --- /dev/null +++ "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C# 고성능 서버 - __FILE__, __LINE__ 대체제 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C# 고성능 서버 - __FILE__, __LINE__ 대체제 +

+ + +
+ + + + +
+

C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__ 등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.

+

성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.

+ + +

콜스택을 얻어와서 가장 마지막 함수를 찍는 방법

현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.

+
1
2
3
4
5
6
7
8
9
StackTrace st = new StackTrace(new StackFrame(true)); 

Console.WriteLine(" Stack trace for current level: {0}", st.ToString());

StackFrame sf = st.GetFrame(0);
Console.WriteLine(" File: {0}", sf.GetFileName());
Console.WriteLine(" Method: {0}", sf.GetMethod().Name);
Console.WriteLine(" Line Number: {0}", sf.GetFileLineNumber());
Console.WriteLine(" Column Number: {0}", sf.GetFileColumnNumber());
+

C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.

+

콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음

.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.

+

콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.

지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.

+

Log4Net, NLog 모두 아주 좋은 로그 모듈인 것은 분명하다. Log4Net은 apache 소프트웨어 재단의 모듈인 만큼 아주 많은 곳에서 쓰이고 있을것이다. 두 모듈 모두 설정 문서만 읽어봐도 정말 기능이 많다. 로그파일을 사이즈나 시간에 맞춰 새 파일로 나눠주는 것은 물론이고, 메일로 로그를 전송할 수도 있고, 로그 레벨 설정도 자유롭고, 파일 생성 정책도 디테일하게 조절할 수 있고… 아무튼 아주 많다.

+

내가 이 두 모듈을 떠나서 직접 만들어 사용하는 가장 큰 이유는 성능 때문이다. 나에게는 굳이 내가 사용도 하지 않을 것 같은 다수의 편의기능들보다도 딱 내가 필요한 동작만 가지고 있더라도 가볍고 빠른 로그 모듈이 필요했다. Log4Net은 오래되서 잘 기억이 나지 않지만 NLog같은 경우 모듈 자체에서 스레드도 제법 많이 만들어서 운용하는걸 디버깅하다 본 기억이 있는데, 이런 내부 구조도 고성능 엔진을 만든다는 측면에서 부담스러웠다. (고성능을 위한 File IO 전략은 이 글의 주제에서 벗어나니까 다음 기회에 별도의 포스트로 다뤄보겠다.)

+

범용적인 로그 모듈들은 성능 또한 일반적이다. 크게 좋지도 않고 아주 나쁘지도 않는 수준을 보여준다. NLog를 사용할 때 설정에서 파일 이름과 라인 위치를 출력하는 동작을 끈 채로 사용해도 성능에는 별반 차이가 없었는데, 아마도 파일로 출력만 하지 않을 뿐 내부에서는 동일하게 StacFrame 을 얻어오는 동작이 실행되고 있을거라고 추측했다. 혹은 StackFrame 때문이 아닌, 다른 많은 부수 기능들 때문일 수도 있을 텐데, 아무튼 나의 기대치에는 맞지 않았다.

+

System.Runtime.CompilerServices

.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void DoProcessing()
{
TraceMessage("Something happened.");
}

public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
+

함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에 채워진다.

+

과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.

+

CompilerServices 장점 : 가볍고 빠르다.

위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__ 은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.

+

CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.

1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
WriteLog("invalid value:{0}", value); // 불가능합니다.
}

public void WriteLog(string format,
params object[] list,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+

함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.

+

하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라

+
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
// WriteLog("invalid value:{0}", value); // C++스러워 보이지만, 촌스러운 방식이예요.
WriteLog($"invalid value:{value}"); // 가능합니다. 권장됩니다. Effective C# 읽어보세요.
}

public void WriteLog(string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+

C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.

+

C++은 코드영역을 사용하지만, C#은 힙을 사용한다.

좀 더 성능에 집착해보자(?).

+

윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__ 은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.

+ + +

(이미지 출처 : wikipedia)

+

이미지에서 text로 표현된 부분이 코드영역이다. 이 공간은 고정적인 읽기 전용의 공간이다. C++의 __FILE__ 매크로를 다르게 표현하면 결국 이 코드영역의 특정 위치를 가르키는 char*로 변환될 뿐이다. 추가적인 객체 할당은 없다.

+

하지만 C#은 코드영역을 사용하지 않는다. [CallerFilepath] string filePath함수 호출이 일어날 때마다 heap 영역에 스트링 객체를 할당한다. 디버그를 위해 상세하게 로그를 달면 달 수록 heap에는 동일한 텍스트가 반복적으로 할당되어 메모리에 압력을 가하게 된다.

+

C#에서는 C++처럼 코드영역을 참조하는 문자열을 만드는 방법이 없다. 모든 참조형식의 객체는 heap이 아닌 공간을 사용할 수 없기 때문으로 추측이 된다. value type을 object 형식으로 가리키면 굳이 비싼 비용을 들이면서까지 heap에 추가할당을 만드는 boxing을 하는 이유와 같을 것이다.

+

반복적으로 사용하는 똑같은 문자열인데도, 매번 함수가 불릴 때마다 이걸 heap에 재할당을 할까? 하고 나도 처음엔 그렇게 생각했다. C++을 하면서 생긴 사고의 관성일 것이다. C#의 string은 참조 타입이고, immutable해서 한 번 할당하면 변경도 불가한 성격을 갖고 있기 때문에 충분히 착각할 만한 상황이기도 하다 - 라고 자기 합리화를 해본다. 하지만 windbg를 이용해 heap을 디버깅 하던 중 무수히 많은 파일 경로 텍스트가 중복으로 잔뜩 들어있는걸 보고 나서야 아닌 것을 깨달았다.

+

Interned String

완전하게 내용이 같은 string을 pooling하여 heap에 한 번만 할당하고 돌려쓰는 방법이 없는 것은 아니다. 이렇게 언어 자체적으로 문자열을 풀링하는 처리를 Java와 C#에서는 모두 Interning이라고 부른다.

+ +

사용법은 간단하다. 풀링하고 싶은 문자열을 사용할 때 string.Intern() 메소드를 한 번 더 감싸주면 된다. 현재 회사에서 실제 사용중인 모듈의 인터페이스 부분만 보면 아래처럼 되어있다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.IO;
using System.Runtime.CompilerServices;

public static class Log
{
public static void Debug(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
{
// ... 중략...
provider.Debug($"{message} ({BuildTag(file, line)})");
}

private static string BuildTag(string file, int line)
{
return string.Intern($"{Path.GetFileName(file)}:{line.ToString()}");
}
}
+

전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.

+
1
2
3
4
5
2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32)
2020-12-21 12:08:02.145 [Info] [Send] [20017] kREGISTER_GAME_SERVER_REQ actionId:3 (SerializableExt.cs:92)
2020-12-21 12:08:02.205 [Info] db connection Initialized. type:Auth server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.221 [Info] db connection Initialized. type:Contents server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.238 [Info] db connection Initialized. type:Game server:localhost count:16 (DbPool.cs:40)
+

interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.

+

C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.

+

마치면서

로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.

+
    +
  • 함수 호출자 정보를 얻고 싶을 땐 StackFrame 사용 보다 CompileServices 하위 어트리뷰트를 쓰는게 낫다.
  • +
  • C#은 모든 문자열을 항상 heap에 할당한다. 심지어 literal text같은 상수 문자열을 사용한다 하더라도 메모리 코드영역의 직접 참조가 불가능하다.
  • +
  • 로그를 찍을 때마다 heap에 불필요한 객체 할당이 발생하는 것을 줄이고 싶다면 문자열을 Interning하면 된다.
  • +
+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/00.jpg" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/00.jpg" new file mode 100644 index 00000000..94f1f635 Binary files /dev/null and "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/00.jpg" differ diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/01.png" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/01.png" new file mode 100644 index 00000000..beded0cc Binary files /dev/null and "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/01.png" differ diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/02.png" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/02.png" new file mode 100644 index 00000000..a852e77d Binary files /dev/null and "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/02.png" differ diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/03.png" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/03.png" new file mode 100644 index 00000000..29772189 Binary files /dev/null and "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/03.png" differ diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" new file mode 100644 index 00000000..598ca338 --- /dev/null +++ "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C# 고성능 서버 - System.IO.Pipeline 도입 후기 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C# 고성능 서버 - System.IO.Pipeline 도입 후기 +

+ + +
+ + + + +
+ + +

2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.

+

윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.

+

하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.

+ + +

System.IO.Pipeline 소개.

pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.

+

RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer) 외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.

+

Pipeline이라는 이름을 굉장히 잘 지었다. 이름처럼 메모리 버퍼를 끝없이 연결된 긴 파이프라인처럼 쓸 수 있게 해주는 라이브러리 이기 때문이다. 단위길이 만큼의 버퍼를 계속 이어붙여서 무한하게 이어진 가상의 버퍼를 만드는데, 이걸 너네가 만들면 시간도 오래 걸리고 버그도 넘나 많을테니 우리가 미리 만들었어. 그냥 가져다 쓰렴. 하고 내놓은 것이 Pipeline이다.

+ + +

(이미지 출처 : devblogs.microsoft.com)

+

이미지의 초록색 부분은 class Pipe 의 내부 구조를 도식화한다. 일정한 크기의 작은 버퍼들이 링크드 리스트로 연결 되어있다. 내부 구조는 안에 숨겨져있고 외부로는 ReadOnlySequence 타입을 이용해 버퍼간 이음매가 드러나지 않는 seamless한 인터페이스만을 제공한다. 이것이 Pipeline의 핵심이다.

+

이 외의 디테일한 부분은 Pipeline을 이해하기 쉽게 잘 설명한 MS 블로그의 포스팅이 있어 이것으로 대신한다.

+

장점 : 불필요한 메모리 복사를 없앤다.

고성능 소켓 IO 구현에 관심이 있는 C++ 프로그래머라면 google protobuf의 ZeroCopyStream 을 이미 접해봤을지 모른다. 그렇다면 Pipeline의 중요한 장점을 쉽게 이해할 것이다. Pipeline의 버퍼 운용 아이디어는 프로토콜 버퍼의 ZeroCopyStream과 유사하기 때문이다. 소켓으로 데이터를 주고 받는 과정에서 발생하는 불필요한 버퍼간 메모리 복사를 최소한으로 줄여주어 성능향상을 꾀한다는 점에서 두 라이브러리가 추구하는 방향은 동일하다.

+

프로그래밍에 미숙한 개발자가 만든 서버일수록 버퍼간 복사 발생이 빈번하게 발생한다. 커널모드 아래에서 일어나는 소켓버퍼와 NIC 버퍼간의 복사까지는 일단 관두더라도, 최소한 유저모드 위에서의 불필요한 버퍼 복사는 없어야 한다.

+

전송할 데이터 타입을 버퍼로 직렬화 하면서 한 번 복사하고, 이걸 소켓에다가 send 요청을 하자니 OVERLAPPED에 연결된 버퍼에다가 넣어줘야 해서 추가로 또 복사하고… send 완료 통지 받고 나면 transferred bytes 뒤에 줄서있을 미전송 데이터들을 다시 앞으로 당겨주느라 또 한번 복사가 발생하기 쉽다. recv 받은 뒤에도 메시지 단위 하나 분량 만큼만 읽어 fetching하고 나면 뒤에 남은 데이터들을 버퍼 맨 앞으로 당겨와야겠으니… 여기서 또 한 번 추가복사 하게 될것이다.

+

서버가 감당할 통신량이 많아질수록 불필요한 복사들이 누적되어 쓸데없이 cpu power를 낭비하게 될텐데, Pipeline의 도입은 이런 부분을 쉽게 해결해 준다. msdn 블로그에서는 Pipeline을 사용하면 복잡한 버퍼 운용 구현을 대신 해결해주니까 프로그래머가 비즈니스 로직의 구현에 좀 더 집중할 수 있게 도와준다고 약을 팔고 설명하고 있다.

+

장점 : 네트워크 버퍼의 고정길이 제약을 없애준다.

가장 단순하게 소켓 레이어를 구현하면 송/수신용 고정 사이즈 byte[] 버퍼를 각각 하나씩 붙여서 만들게 될 것이다. 대략 구현중인 게임이 어느 정도 사이즈의 패킷을 주고 받는지를 귀납적으로 파악해서 (주로 게임 서버는 작은 사이즈 패킷을 많이 받고, 큰 사이즈 패킷을 많이 보낸다. 로그인할때, 캐릭터 선택할 때 보내는 패킷이 통상 제일 크다) 버퍼의 크기를 눈치껏 결정해서 상수로 고정한다. 버퍼를 거거익선으로 크게크게 잡으면 좋겠지만 대량의 동접을 처리해야 할때 메모리 사용량이 높아져서 부담이 된다. 그러니 적당히 오가는 패킷 사이즈를 봐서 터지지만 않을 정도의 고정길이 버퍼를 걸어두는 식으로 만들게 된다.

+

이렇게 만들면 불안하다. 컨텐츠를 점점 추가하다가 언젠가 한 두번은 네트워크 버퍼 overflow가 발생해 버퍼 크기를 늘려잡고 다시 빌드해야 하기 일쑤다. 아니면 버퍼를 넘치게 만든 문제 패킷의 구조를 변경하거나 두 개의 패킷으로 쪼개는 등 다이어트를 시켜서 해결할 수도 있겠다. 어느쪽이든 고성능 서버의 네트워크 레이어 구현으로는 적당하지 않은 솔루션이다. 메모리를 더 써서 해결하거나, 개발에 제약(패킷의 최대 크기)을 두어 해결하거나. 모두 석연치 않다.

+

Pipeline과 ZeroCopyStream 의 무한버퍼 컨셉은 이러한 고정길이 버퍼의 단점을 해결해준다. 처음엔 작은 크기의 버퍼만 가지고 있다가, 공간이 모자라면 추가로 더 할당받아 링크드 리스트 뒤에 붙이기만 하면 된다. 각각의 peer(= single socket)가 실제 사용하는 메모리 공간은 주고받는 데이터의 크기에 따라서 늘어나거나 줄어드는 유연성이 생긴다. 메모리를 효율적으로 사용하면서도 단일 메시지의 사이즈 제약도 없어진다.

+

단점 : 너무 많은 Task를 생성한다.

위의 두가지 장점만으로 Pipeline의 도입을 시도해볼 가치는 충분했다. 그래서 우리는 게임서버의 수신 버퍼를 Pipeline으로 대체하고, MS Azure 에서 F8s 급 인스턴스 수십대를 동원해 10만 동접 스트레스 테스트를 진행해 보았다.

+

결과는 기대와 완전히 달랐는데.. Pipeline 도입 전보다 영 더 못한 성능을 보여줬다. 이건 뭐… cpu 사용량이 높고 낮아지는 것이 문제가 아니라, 동접이 일정수치 이상 오르면 서버가 아무 일도 처리하지 않고 멈춰버렸다. 반응없는 프로세스에서 덤프를 떠서 디버거로 살펴보면… 대기상태인 스레드가 잔뜩 생겨있고, 일해야 할 스레드가 부족해서 추가 스레드를 계속해서 만들어내고 있는 것처럼 보였다.

+
1
2
3
4
5
6
7
8
9
// msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다.
async Task ProcessLinesAsync(Socket socket)
{
var pipe = new Pipe();
Task writing = FillPipeAsync(socket, pipe.Writer);
Task reading = ReadPipeAsync(pipe.Reader);

return Task.WhenAll(reading, writing);
}
+ + +

원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe 인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task 객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.

+

task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.

+

스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.

+

의문 : Kestrel은 Pipeline 때문에 엄청 빨라졌는데?

+ +

(이미지 출처 : stackoverflow.com)

+

ASP.NET Core는 Pipeline으로 구현한 kestrel 웹서버에서 실행할 때 기존의 iis 기반보다 훨씬 더 향상된 퍼포먼스를 보여준다. Pipeline의 버퍼 운용 효율성으로 인한 이득을 제대로 누리고 있는 것이다. kestrel의 뛰어난 성능 결과를 보여주는 여러 벤치마크 결과들 덕분에 나도 기대를 가득 안고 서둘러 Pipeline을 도입하고 테스트 해보았으나.. 결과는 좋지 않았다.

+

그럼 우리 게임서버에 도입한 테스트 결과는 왜 이리 처참한 것인가? ms 형들이 잘못 만들었을 리는 없으니 내가 가져다 붙이는 과정에 문제가 있었던 것인가?

+

차이가 생기는 원인은 Kestrel은 http 통신을 하는 웹서버이고, 우리의 게임서버는 연결을 유지하고 있는 TCP 서버이기 때문이다. Kestrel은 통신량의 거의 전부가 socket이 열린 채로 길게 대기할 필요가 없기 때문에, task을 소켓의 2배수나 4배수만큼 오래도록 유지하고 있을 이유 자체가 없다. 그래서 단점으로 지적한 waiting task가 kestrel에서는 발생하지 않는다. 상술했던 단점을 다시 표현해 보자면 Pipeline의 사용시 기본적으로 task 대기가 발생하는 것을 성능 하락의 원인으로 볼 수 있지만, 이 task들의 수명 혹은 대기시간이 상당히 길다는 점과 함께 만나면 성능을 더욱 악화시키는 원인이 된다. Kestrel의 단명하는(?) 소켓들과 task들은 Pipeline와 함께 사용되면서 충분히 좋은 성능을 가져다 줄 것이다. 수많은 벤치마킹 결과들이 증명하듯이.

+

대안 : 불필요한 복사가 없는 가변버퍼를 직접 만들자.

우리는 게임서버에서 Pipeline을 다시 드러냈다. http와 유사하게 single pair request/response 통신 후 소켓을 닫아도 되는 경우가 아니면 Pipeline으로 성능상의 혜택을 보기는 힘들다고 판단했기 때문이다. 그래도 불필요한 메모리복사는 만들고 싶지 않으니 메모리 버퍼 운용하는 부분만 직접 구현해 사용하기로 했다.

+ + +

클래스 이름이 Pipeline과 protobuf를 모두 가져다 섞어놓은 느낌이 들겠지만 착각일 뿐이다. 두 api를 모두 사용해본 경험의 영향을 받긴 했지만… *Stream.cs 클래스들은 실제로 System.IO.Stream을 상속받아서 이름이 좀 비슷해졌다. 이 Stream 구현들이 단위버퍼들간의 연결을 seamless하게 쓸 수 있게해주는 역할을 한다. 주요 구현을 담고 있으나 사용계층에 노출될 필요는 없기 때문에 Detail 아래로 숨겨두었다. 사용자는 부모타입인 Stream 추상 클래스만 보게 된다.

+

인터페이스로 ReadOnlySequence<T>를 사용하지 않은 이유는 이 구현을 Unity3D로 만든 클라이언트에서도 똑같이 사용하기 위해서였다. 현시점 유니티의 mono framework가 지원하는 C# 문법 버전이 낮아서 ReadOnlySequence<T>를 지원하지 않기 때문이다. 그런데 Stream 을 이용해도 어렵지 않게 seamless 를 구현할 수 있었고, 실제 사용하기에도 스트림 형태가 훨씬 익숙하고 편해서 결과적으로는 더 만족스러운 선택이었다. ReadOnlySequence<T> 가 뭔지 모르는 프로그래머도 Stream은 알고 있을 것이다.

+

실제 사용 계층으로 노출하는 클래스는 아래의 세 클래스 만으로 정리했다.

+
    +
  • MemoryPipe : 소켓 수신버퍼 처리 전용. System.IO.Pipeline과 유사하다.
  • +
  • SendBuffer : 소켓 송신버퍼 처리 전용.
  • +
  • ZeroCopyBuffer : 네트워크 버퍼가 아닌 범용적인 용도의 인터페이스.
  • +
+

패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.

+

이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.

+
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Cs.ServerEngine.Network.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;

public int SegmentCount => this.segments.Count;

public int CalcTotalSize()
{
int result = 0;
foreach (var data in this.segments)
{
result += data.DataSize;
}

return result;
}

public BinaryWriter GetWriter() => new BinaryWriter(new ZeroCopyOutputStream(this));
public BinaryReader GetReader() => new BinaryReader(new ZeroCopyInputStream(this));

internal void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if (this.last == null || this.last.IsFull)
{
this.last = LohSegment.Create(LohPool.SegmentSize.Size4k);
this.segments.Enqueue(this.last);
}

int copied = this.last.AddData(buffer, offset, count);

offset += copied;
count -= copied;
}
}

internal LohSegment[] Move()
{
var result = this.segments.ToArray();
this.segments.Clear();
this.last = null;

return result;
}

internal LohSegment Peek()
{
return this.segments.Peek();
}

internal void PopHeadSegment()
{
var segment = this.segments.Dequeue();
segment.ToRecycleBin();

if (this.segments.Count == 0)
{
this.last = null;
}
}
}
}

+

본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment> 가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.

+

조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.

+

이렇게 하니 ZeroCopyBuffer는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline의 유사품이 되었다. 제공되는 메서드 중에는 async method 가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.

+

마치면서

System.IO.Pipeline은 ASP.NET Core의 성능을 크게 끌어올린 네트워크 버퍼 운용 라이브러리다. 이를 적용하면 네트워크 버퍼구현의 여러가지 문제점들과 boilerplate한 구현들을 손쉽게 해결할 수 있으나, 최소 2 tasks/peer를 소켓의 수명만큼 열어두어야 하기 때문에 소켓을 긴 시간 유지하는 타입의 TCP서버라면 도입 전에 신중한 성능 테스트를 거쳐야 한다.

+

사이즈가 무한인 가상의 버퍼라는 컨셉만을 가져와 직접 만들어 사용중인 ZeroCopyBuffer 모듈의 인터페이스도 간단하게 소개해 보았다. Unity3D 클라이언트 네트워크 모듈에도 함께 사용하기 위해 ReadOnlySequence<T> 대신 System.IO.Stream으로 추상화한 인터페이스를 제공했는데, 이렇게 하니 요구사항을 충분히 만족하면서도 사용 계층에게는 더 익숙한 형태의 인터페이스를 제공할 수 있어서 만족스러웠다.

+

본 포스팅에는 단위버퍼로 이용한 구현체인 LohSegment에 대한 소개가 없었다. 글 분량 조절에 실패하여 일부로 언급하지 않았는데, 다음에 가비지 컬렉터를 주제로 포스팅하면서 추가로 다뤄볼 예정이다.

+

참고:

+ + +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/00.png" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/00.png" new file mode 100644 index 00000000..03af4979 Binary files /dev/null and "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/00.png" differ diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/01.png" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/01.png" new file mode 100644 index 00000000..08c5d784 Binary files /dev/null and "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/01.png" differ diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/02.png" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/02.png" new file mode 100644 index 00000000..9affef49 Binary files /dev/null and "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/02.png" differ diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" new file mode 100644 index 00000000..a2bb2da9 --- /dev/null +++ "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C# 고성능 서버 - Thread Local Storage | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C# 고성능 서버 - Thread Local Storage +

+ + +
+ + + + +
+

프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread) 키워드를 이용해서 tls 변수를 선언할 수 있다.

+

C#에도 ThreadLocal<T> 라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.

+

이를 해결하는 방법과 주의해야 할 사항을 정리해본다.

+ + +

알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.

+

수정한 내용 요약 : 새로 깨어난 스레드인데도 AsyncLocal<T>에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도 AsyncLocal<T>의 값을 복사하고 있었기 때문이었습니다.

+
+ +

async / await 을 절대 가볍게 접근하면 안된다

주제와 약간 벗어날 수 있지만 서두에 미리 한 번 짚고 넘어갈 부분이 있다. 절대로 async / await를 이용한 비동기 프로그래밍을 만만하게 보아서는 안된다는 것이다.

+

나도 그랬지만 누구든지 제일 처음 비동기 메서드를 접했을 땐 이해하기 쉽고 간단한 기능이라는 첫인상을 가지게 될 것이다. 개인적으로는 비동기 메서드를 적용하고 난 후의 코드가 동기 프로그래밍과 너무 비슷해져 버리는 점이 착각을 유발하는 큰 원인이라고 생각한다 (MS: 얘는 뭐 좋게 해줘도 불만이 많네..)

+

이전에 DB 쿼리나 네트워크 통신같은 IO 작업에서 비동기로 받는 결과값을 처리하기 위해서는 하나의 동일한 주제(single concern)를 위한 로직임에도 불구하고 비동기 요청 이전과 이후의 코드가 분절되어야 했다. 이를테면 비동기 요청 전의 코드와 응답 후의 코드를 서로 다른 메서드로 나누어서 짜야 했다는 뜻이다. 코드의 가독성에 대해 고민을 좀 해봤던 개발자라면 람다를 써서 어떻게든 읽기 좋고 관리하기 좋도록 애써 보았을 수도 있으나, 가독성에서 정도의 차이가 있을 뿐 명백하게 존재하는 코드상의 분절을 피할 수 없었다.

+

비동기 메서드의 등장으로 이런 상황은 옛날 이야기가 되었다. 안간힘을 써보아도 완전하게 붙이기 힘들었던 분절된 코드들은 이제 하나의 async 함수 안에서 seamless하게 구현할 수 있게 되었다. 작성한 코드를 읽을 때에도 (신경써서 읽지 않는다면) 어디가 동기 처리이고, 어디가 비동기 처리인지도 잘 모르고 넘어갈만큼 술술 읽어내려가게 되었다. 좋게 해석하자면 어플리케이션 개발자가 좀 더 로직에만 집중 할 수 있는 환경이 되었다.

+

이것은 호수에 떠있는 백조와 같다. 일단 겉으로 보기에는 아주 우아하게 비동기 코드를 표현했으나, 조금만 안을 들여다보면 비동기 요청을 기준으로 발생하는 여전한 로직의 분절, 그에 따른 실행 시점 시간차 및 실행 환경상의 차이 등은 당연게도 여전히 존재하고 있기 때문이다. 이로 인한 이슈들은 동시성(concurrency)이 있는 멀티스레드 환경에서 더 잘 드러난다. MS는 실제로 프로그래머들이 하부의 복잡한 메커니즘을 잘 모르더라도 쉽고 편하게 비동기 로직을 다룰 수 있는 유토피아를 꿈꾸었을지 모르겠다. 하지만 싱글 스레드로 간단한 툴 한두개 짜는거면 몰라도… C#이란 언어로 고성능 서버를 만들겠다고 한다면, 이에 대한 충분한 이해가 없이는 런타임에서 예상못한 오작동을 피할 수 없을 것이다.

+

이후 글에서 언급할 내용도 비동기 함수의 실행 시점차와 관련되어 있으므로, 비동기 메서드에 대한 어느 정도의 이해가 필요하다.

+

ThreadLocal

우선 잠깐 언급했던 ThreadLocal<T> 클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던 [ThreadStatic] 어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서 ThreadLocal<T> 가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인 ThreadLocal<T> 를 사용하면 된다.

+

모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}

// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.

// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());

internal static Random Instance => Random.Value;
}
}
}

+

이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는 Random 객체가 어느순간 2번 스레드의 Random 객체로 바뀐다 해도 동작에 큰 영향이 없다.

+

AsyncLocal

문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.

+ + +

스레드별로 서로 다른 상태값을 사용해야 하는 예를 구승모 교수님의 Dispatcher 구현에서 찾아볼 수 있다. (ThreadLocal.h) Dispatcher는 고성능 멀티스레드 로직 수행을 위한 Actor 패턴 구현체다. 스레드에 lock을 걸지 않으면서도 서로 다른 스레드간 간섭 없이 순차실행을 가능하게 하기 위해, 스레드는 현재 자신의 수행상태 일부를 tls에 기록해 두어야 한다.

+

친절한 ms 형들이 이런 경우를 위해 AsyncLocal 클래스도 미리 만들어 두었다. 생긴것도 서로 비슷해서 ThreadLocal<T> 를 사용했던 변수에 대신 AsyncLocal<T> 로 바꿔주면 위에서 말한 문제를 해결할 수 있다. 0번 스레드가 먼저 코드를 수행하다가 await 구문을 만나서 대기하고, 대기가 풀려날 때 2번 스레드로 변경이 되었더라도 AsyncLocal<T> 가 2번 스레드의 tls 값을 알아서 “영희” -> “철수”로 바꿔주는 것이다.

+ + + + +

문제점 : 의도치 않게 값의 복사 발생

이러면 문제는 해결된 것 같지만, 또 다른 문제가 있다. 여기가 이 글의 핵심이다 집중해주기 바란다. AsyncLocal<T>ThreadPool이 다른 새 스레드를 추가로 깨우게 하는 특정 api들 중에 하나를 호출하는 경우, 기본적으로 호출자 스레드의 변수값을 새로운 스레드에게 복사해주는 기본 동작을 갖고 있다. 현재 스레드에서만 고유하게 유지하려고 기록해 둔 tls의 변수들이 요주의 api중 하나를 호출하는 순간 새로운 다른 스레드로 복사되는 것이다. 현재 우리 프로젝트 구현의 범위 기준에서, AsyncLocal의 값을 복사시키는 메서드들은 아래와 같다.

+
    +
  1. Fire-and-forgot 으로 동작할 백그라운드 작업이 필요해서 직접 ThreadPool에 요청하는 메서드들

    +
      +
    • Task.Run()
    • +
    • ThreadPool.QueueUserWorkItem()
    • +
    +
  2. +
  3. 비동기 소켓의 IO 완료통지를 포함해, 네트워크 이벤트 콜백을 유발하는 메서드들

    +
      +
    • Socket.ConnectAsync() - ConnectEx() in win32
    • +
    • Socket.DisconnectAsync() - DisconnectEx() in win32
    • +
    • Socket.AcceptAsync() - AcceptEx() in win32
    • +
    • Socket.ReceiveAsync() - WSARecv() in win32
    • +
    • Socket.SendAsync() - WSASend() in win32
    • +
    +
  4. +
+

1번 백그라운드 작업 요청 메서드들은 스레드풀을 대상으로 하는 동작이니까 어느 정도 이해가 된다고 하지만, 2번 네트워크 콜백들은 tls를 복사한다는 점이 선뜻 연결이 잘 되지 않는다. managed 메서드의 이름이 낮설어 보일까 싶어 win32에 해당하는 함수명도 같이 적었는데, 그냥 OVERLAPPED 구조체를 이용해 IOCP에 통지를 요청하는 네트워크 api들 전체를 말한다.

+

0번 스레드가 게임 로직을 열심히 수행하다가 클라이언트로 동기화 패킷을 보낼 상황이 되었다. 그래서 패킷을 만들어 소켓에 SendAsync()를 한 번 걸어놓고, 다시 또 다른 로직을 열심히 수행한다. 근데 0번 스레드가 걸었던 send 요청이 완료되어 새롭게 2번 스레드가 OnSendCompleted 메서드를 실행하려고 깨어났는데, 이 때 0번 스레드가 AsyncLocal<T>에 저장해두었던 tls 값들을 2번 스레드가 고대로 복사받아서 수행을 시작하는 것이다.

+

AsyncLocal<T>는 자신의 존재 목적과 취지에 충실하고자, 서로 다른 스레드들간에 조금이라도 관련이 있을라 치면 아주 얄짤없이 값을 복사해대는 것 같다. 하지만 win32에서 iocp에 비동기 작업의 완료 통지를 요청하고, 전혀 관련없는 다른 스레드로부터 이를 받아 처리해오던 고전적 처리방식에 익숙해서 그런지 이런 과도한 친절이 부담스럽다. 너 때문에 Dispatcher 동작이 다 깨지잖아. 조치가 필요하다.

+

원치 않는 AsyncLocal 복사는 꺼준다.

다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는 SuppressFlow() 호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}

public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;

// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}
+

작업 요청 후에는 RestoreFlow 를 불러 복구해주면 되는데, SuppressFlow 메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.

+

네트워크 구현부에도 수정이 필요하다. SocketAsyncEventArgs 객체를 사용해 비동기 요청을 수행하는 모든 곳에도 RestoreFlow 를 불러준다. (SocketAsyncEventArgs는 win32의 OVERLAPPED 구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);

using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}
+

이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도 AsyncLocal<T>을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.

+

정리

    +
  • C#의 비동기 메서드는 코드상으로는 매끈하게 이어져 있는듯 보이지만 실은 비동기 요청 지점을 전후로 분리 실행되며, 실행 스레드가 서로 다를 수도 있다.
  • +
  • 이로 인해 ThreadLocal<T> 로는 비대칭적(asymmetric)인 tls 데이터를 다루기가 어렵기 때문에 AsyncLocal<T>라는 클래스가 별도로 존재한다.
  • +
  • AsyncLocal<T>는 스레드풀에서 새로운 다른 스레드를 깨어나게 할 때도 값을 복사시킨다. 이는 ExecutionContext.SuppressFlow() 로 제어가 가능하다.
  • +
+

현재 사용중인 게임서버의 스레드 모델도 승모님의 JobDispatcher와 유사한 Actor 기반 구조를 채택해서 락 없이 구현하고 있다. 지금 서버 구현 기준에서 값이 복사되는 tls 변수가 문제를 일으키는 케이스는 액터를 구현하기 위한 로직 한 군데 뿐이다. 일반적으로 게임 서버를 구현할 때 스레드별로 비대칭적인(asymmetric) tls 변수를 유지해야 하는 경우가 흔치는 않을 것이다. 액터 패턴을 구현한다고 해서 tls 변수가 반드시 필수적인 것도 아니다. 이전 프로젝트에서 tls를 사용하지 않는 액터 구현도 사용해본 적이 있기 때문이다.

+

하지만 고성능 서버를 목표로 스레드 효율성을 튜닝한다면 반드시 사용을 염두에 두게 되는 도구가 TLS이므로, 본 글에서 언급한 내용을 숙지하고 있으면 성능 튜닝에서 많은 삽질을 세이브 하게 될것이다.

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/00.jpg" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/00.jpg" new file mode 100644 index 00000000..dd7c78fa Binary files /dev/null and "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/00.jpg" differ diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/01.png" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/01.png" new file mode 100644 index 00000000..644318e3 Binary files /dev/null and "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/01.png" differ diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/02.gif" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/02.gif" new file mode 100644 index 00000000..88b5242f Binary files /dev/null and "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/02.gif" differ diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" new file mode 100644 index 00000000..2f5cd397 --- /dev/null +++ "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +C# 고성능 서버 - 메모리 단편화 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ C# 고성능 서버 - 메모리 단편화 +

+ + +
+ + + + +
+ + +

이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.

+

이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.

+ + +

기본 용어 및 개념 정리

SOH / LOH / POH

가장 먼저 관리 힙(managed heap)의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라 SOH, LOH, POH로 나뉜다.

+
    +
  • SOH는 Small Object Heap으로, 85kb보다 작은 사이즈의 메모리를 할당한다. 경우에 따라 차이는 있겠지만 대다수의 객체들이 주로 할당/해제 되는 공간이다.
  • +
  • LOH는 Large Object Heap으로, 85kb보다 큰 사이즈의 메모리를 할당한다.
  • +
  • POH는 Pinned Object Heap으로, pinning할 메모리를 위해 .Net 5부터 새롭게 추가된 공간이다.
  • +
+

POH는 사실 다짜고자 단편화의 해법에 가까운 존재이긴하나.. 분류상 미리 언급되었다. 이후에 다시 추가적으로 설명한다.

+

Compression

SOH의 메모리는 객체가 얼마나 오래 살았느냐에 따라 0세대부터 2세대까지 세대를 구분한다. GC가 한 번 실행될 때 사용이 끝난 메모리는 해제되고, 아직 사용중인 메모리는 다음 세대로 승격한다. 이 때 살아남은 메모리들은 압축(Compression)의 과정을 거친다. 압축이란 메모리 단편화를 줄이기 위해, 살아남은 메모리들을 사이사이 공백이 없도록 한 공간으로 몰아서 재배치하는 동작을 말한다. 실제로 관리 힙 내부에서 객체들은 세대별로 모아두어야 하기 때문에, 메모리 해제 및 승격을 거친 후에는 세대별 구획에 맞춰 메모리를 재정렬하는 과정이 반드시 필요하다.

+ + +

오.. 이거 처음에 너무 신기했다. 네이티브 언어로 만들어진 코드에서는 불가능한 동작이다. C++로 짠 코드라면 프로그래머가 직접 작성한 비즈니스 로직 상에서 이미 무수히 많은 포인터들이 가상 메모리의 주소값 자체를 가르키고 있기 때문이다. C#의 참조타입 변수들도 C++ 포인터와 유사하다고 볼 순 있지만 직접적으로 메모리 주소가 노출되어 있지는 않기 때문에 가능한 일이다. 객체의 메모리상 주소가 바뀌더라도 모든 참조들을 새로운 주소값으로 알아서 갱신해 주어서, 매니지드 레벨의 코드상에서는 마치 아무 일도 없었다는 듯이 시치미를 떼는 신박한 동작이다.

+

Pinned Memory

하지만 메모리 압축이 이미 할당된 모든 객체들의 위치를 제멋대로 바꿀 수 있는 것은 아니며, 모든 법칙에 항상 예외는 존재한다. 매니지드 레벨은 결국 네이티브 레벨 위에서 돈다. 네이티브 영역과의 상호참조가 필요한 매니지드 메모리는 함부로 값을 옮겨다닐 수가 없다. 위에서 언급한 C++로 만든 코드였다면 불가능하다고 말한 이유와 크게 다르지 않은 상황이다.
네이티브 영역에서 매니지드 영역의 메모리를 참조할 일이 있을 때는 메모리를 이동이 발생하지 않는 안전한 공간에 복사(copying)하거나, 이동할 수 없도록 고정(pinning)해둬야 한다. 매니지드 메모리가 다른 주소로 이동하지 않도록 고정하는 것을 Memory Pinning, 이렇게 고정된 메모리를 Pinned Memory라고 부른다.

+ + +

데이터 마샬링(매니지트/네이티브 상호통신)의 입장에서 보면 pinning은 불필요한 복사를 줄여주는 효율적인 동작이다. 하지만 가비지 컬렉터 입장에서 보자면 엄청난 방해꾼임이 분명하다. pinned memory 는 gc의 압축 동작을 방해하기 때문이다

+
+

고정(Pinning)은 데이터를 현재 메모리 위치상에 임시로 잠그기 때문에, CLR의 가비지 수집기에 의한 재배치를 막아줍니다.
Pinning temporarily locks the data in its current memory location, thus keeping it from being relocated by the common language runtime’s garbage collector.
(https://docs.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning)

+
+
+

고정(Pinning)은 메모리의 단편화를 유발하고, 일반적으로 객체 압축 과정을 복잡하게 만들기 때문에 자체적인 비용 부담을 가집니다.
Pinning has its own costs, because it introduces fragmentation (and in general complicates object compaction a lot).
(https://tooslowexception.com/pinned-object-heap-in-net-5/)

+
+

단편화 발생의 원인

성능좀 끌어올려보겠다고 다짐한 C# 게임서버의 메모리 단편화는 어디서 발생하는가.

+

핵심부터 말하자면 소켓의 send / receive에 걸어주는 바이트 배열 버퍼가 pinning되기 때문에, 가비지 컬렉터의 압축과정을 많이 방해하게 되면서 메모리 단편화를 유발한다. 이 부분이 메모리 단편화의 가장 주된 요인이다. 그런데다가 높은 TPS를 처리해내는 고성능 게임서버를 만들려고 한다면.. 소켓 IO의 수가 많아짐에 따라 네트워크 버퍼의 개수와 사용 빈도도 당연히 높아질 수밖에 없다. 때문에 대량의 네트워크 통신을 견딜 수 있도록 만드려면 네트워크 버퍼를 어떻게 운용할 것인지가 중요하다.

+

DB와 통신하기 위한 DBMS 클라이언트도 많은 수의 pinned handle을 만들어낸다. 현재 우리 프로젝트는 System.Data.SqlClient 네임스페이스 하위의 클래스들을 이용해 Azure SQL과 통신하고 있는데, 생각해보면 db client도 DBMS에 연결되어 쿼리와 데이터를 던지고 받는 통신모듈이니 당연한 이야기다.

+

코드상에서 임의의 객체를 약참조 하기 위해 사용하는 System.WeakReference도 pinning handle을 사용하고 있어, 단편화 유발의 원인이 된다. 이건 참 아이러니한 일이다. 참조하는 대상이 쉽게 메모리 해제될 수 있도록 약참조하는 기능을 하지만, WeakReference 자신은 고정된 메모리를 만들면서 메모리 단편화를 가속시킨다. 처음 서버 기반을 만들 땐 WeakReference가 GC를 방해한다는 사실을 모르고 엄청시리 쓰고 있었는데, 비교적 근래에 실 서비스에서 메모리 문제들을 겪으면서 디버깅 하던 중 메모리가 고정되고 있음을 알게됐다. 현재는 약참조 사용이 꼭 필요한 일부를 제외하고는 모두 제거하였고, 가능하면 WeakReference 의 사용을 자제하고 있다.

+

메모리 상의 고정된 핸들에 대한 정보는 windbg로 힙을 뒤져보면 알 수 있다. sos.dll 로딩된 상태에서 !gchandles 명령 쳐보면 현재 어떤 객체가 pinning되어있고, 몇개나 존재하는지 확인할 수 있다.

+ + + + +

단편화 해결 솔루션

상술한 원인들 중 가장 명백한 원인제공자는 네트워크 버퍼다. 빈번히 쓰이는 네트워크 버퍼를 잘 운용하는 것이 단편화 해결의 핵심이다.

+

네트워크 버퍼용 byte[] 객체를 ArrayPool<T> 을 이용해 풀링하는 것은 그다지 개선의 효과가 없었다. ArrayPool<T>클래스는 효율적으로 객체의 할당과 해제 빈도를 완화하고 관리해주지만, 어쨌거나 SOH 공간에서 할당을 받기 때문에, 이글에서 말하고 있는 pinning 이나 단편화 현상 해결 등과는 크게 상관이 없다.

+

메모리 압축은 SOH에서만 발생한다. 따라서 pinned memory가 GC성능 저하 및 메모리 단편화를 일으키는 것도 SOH에만 해당하는 이야기다. 그러니 네트워크 버퍼는 그냥 SOH에 잡지 않는 것이 좋겠다.

+

솔루션 1. 네트워크 버퍼를 POH에 할당하기

MS 형들도 역시 성능상에서 이런 문제가 있음을 분명히 알고 있다. .NET 5부터는 고정된 메모리로 사용할 객체를 할당하는 별도의 힙 공간인 POH가 새로 생겼다. 현재 회사에서 만든 게임 서버는 프레임워크 버전이 낮아서 아직 사용해 보지는 못했다. (우리 프로젝트는 .NET Framework 4.7.2로 개발을 시작해서 현재 .NET Core 3.1을 사용중이다). 이 글에서 POH에 대한 기본적인 설명을 확인할 수 있다. 아직 서비스하기 전이거나, 사용중인 프레임워크가 .NET 5 이상이라면 POH의 도입을 검토해 볼 만 하다.
링크된 글에서 설명하는 것처럼 POH는 그 존재 목적상, blittable 형식만을 할당할 수 있도록 제한되어있다. 네이티브 코드와 통신하기 위한 데이터를 할당하는 전용의 공간이므로, 기술적인 한계가 아닌 설계상의 의도로 제한을 걸어두었다.

+

솔루션 2. 네트워크 버퍼를 LOH에 할당하기

LOH의 객체들은 메모리 압축으로 인한 재배치를 진행하지 않으며, 세대가 구분되어있지도 않다. 2세대 GC가 수행될 때만 LOH상의 메모리 해제가 진행되므로, 모두 2세대 객체라고 부르기도 한다. 세대 구분이 없으니 메모리 공간상에서 꼭 재배치(Compression) 해주어야 할 필요도 없다.
LOH의 객체는 기본설정상 가상 메모리 주소공간에 한 번 할당되면 위치가 이동되지 않는다. 그러니 빈번하게 할당과 해제를 반복하는 메모리를 LOH에 많이 만들면 금방 조각나버릴 공간이다. 이런 경우라면 LOH에서도 압축을 하도록 설정을 조정할 수는 있지만.. 이렇게 사용하는 것은 그다지 취지(?)에 맞지 않는 기분이 든다. LOH에는 오래도록 유지하거나, 아예 해제할 계획이 없는 덩치큰 메모리들을 위치시키는 것이 용도상 더 적절하다.
우리는 게임 런칭 전 10만 동접을 시뮬레이션하는 부하테스트를 진행했다. 당시 메모리 단편화 이슈로 한참을 고생하던 중, 이 글의 해결 사례를 보고나서 네트워크 버퍼 할당을 LOH로 옮겨 보기로 했다.

+

네트워크 버퍼를 LOH로 옮긴 이후 메모리 단편화 문제는 말끔해 해결되었다. 한 번에 100Mb 단위의 커다란 메모리 청크를 LOH에 잡아두고, 이를 다시 ArraySegment<byte>로 잘게 나누어 풀링하면서 사용하는 방식이다. C++에서 고전적으로 메모리 풀링을 구현할 때 접근하는 방식과 유사하다.

+

C#에서는 버퍼의 조각을 byte[]로 표현할 수 없다. C++에서 byte[]는 개념상 가르키는 대상이 고정인 포인터 (byte * const)와 유사하다(물론 문법상 차이는 있다). 그러므로 커다란 바이트 배열도 포인터, 여러개의 작은 배열들도 포인터로 가르키는 셈이니까 모두 byte[]로 표현되는게 아무 문제가 없다. 하지만 C#에서는 byte[]도 하나의 독립된 매니지드 객체이므로 C++과는 차이가 있다. 큰 배열의 단위조각을 표현할 때 ArraySegment<byte>를 사용해야 하는 이유다.

+

조금은 다른 이야기지만 처음 ArrayPool<T> 가 BCL에 들어왔을때 아주 당연하게 착각한것이, 이놈으로 byte[]를 풀링하면 내부적으로 큰 청크를 한 번만 할당해서 이걸 조각내서 쓸것으로 생각했다. 메모리 관리라 하면 으레 이 방식이 익숙해서였다. 하지만 조금만 생각해보면, C#에서는 불가능한 이야기다. 덩치큰 byte[]를 여러개의 작은 byte[]로 표현할 수가 없다. ArrayPool<T> 코드를 보면 할당 자체는 SOH상에서 단일객체 단위로 발생하나, 그 외 나머지 기법들을 이용해 최적화를 진행함을 알 수 있다. 코드를 보면 2세대 GC가 불릴 때 콜백을 얻어와 현재 메모리 압력을 진단하고, 선택적으로 메모리를 해제하는 등의 테크닉을 볼 수 있다. 이런건 나중에 메모리 로우레벨을 제어해야 할 경우 참고하여 응용하면 좋을듯 하다.

+ +

이전 포스팅 C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는 ZeroCopyBuffer의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼 LohSegment 클래스가 바로 LOH에 할당한 커다란 청크의 일부분에 해당한다.

+
1
2
3
4
5
6
7
8
namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...
+

LohSegment를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서 LOH에 할당될 것이고.. 청크를 다시 잘 쪼개서 ConcurrentQueue<>에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;

private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}
+

정리

C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.

+

SOH에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을 LOH에 할당하면 이런 문제를 해결할 수 있다.

+

참고자료

+ +
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_00.png" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_00.png" new file mode 100644 index 00000000..8a83d16b Binary files /dev/null and "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_00.png" differ diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_01.png" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_01.png" new file mode 100644 index 00000000..7c00de77 Binary files /dev/null and "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/memory_01.png" differ diff --git a/archives/2013/12/index.html b/archives/2013/12/index.html new file mode 100644 index 00000000..df229407 --- /dev/null +++ b/archives/2013/12/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2013 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2013/index.html b/archives/2013/index.html new file mode 100644 index 00000000..b9c2634d --- /dev/null +++ b/archives/2013/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2013 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2014/07/index.html b/archives/2014/07/index.html new file mode 100644 index 00000000..82047c3a --- /dev/null +++ b/archives/2014/07/index.html @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2014 +
+ + + + + + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2014/08/index.html b/archives/2014/08/index.html new file mode 100644 index 00000000..f3eb4402 --- /dev/null +++ b/archives/2014/08/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2014 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2014/09/index.html b/archives/2014/09/index.html new file mode 100644 index 00000000..46426032 --- /dev/null +++ b/archives/2014/09/index.html @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2014 +
+ + + + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2014/index.html b/archives/2014/index.html new file mode 100644 index 00000000..2b87394d --- /dev/null +++ b/archives/2014/index.html @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2014 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/11/index.html b/archives/2018/11/index.html new file mode 100644 index 00000000..1afeffcc --- /dev/null +++ b/archives/2018/11/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2018 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/index.html b/archives/2018/index.html new file mode 100644 index 00000000..524b00c0 --- /dev/null +++ b/archives/2018/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2018 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/12/index.html b/archives/2020/12/index.html new file mode 100644 index 00000000..5e240bcb --- /dev/null +++ b/archives/2020/12/index.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 00000000..d2918d0a --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/01/index.html b/archives/2021/01/index.html new file mode 100644 index 00000000..8f1d5d3b --- /dev/null +++ b/archives/2021/01/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2021 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/08/index.html b/archives/2021/08/index.html new file mode 100644 index 00000000..edb01a7e --- /dev/null +++ b/archives/2021/08/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2021 +
+ + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/index.html b/archives/2021/index.html new file mode 100644 index 00000000..91580aef --- /dev/null +++ b/archives/2021/index.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 00000000..9525f08d --- /dev/null +++ b/archives/index.html @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + +
+ 2018 +
+ + +
+ 2014 +
+ + + + + + + + + + + + +
+
+ + + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 00000000..fdbc7656 --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +아카이브 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+ 음..! 총 14개의 포스트 포스트를 마저 작성하세요 +
+ + +
+ 2014 +
+ + + + + + +
+ 2013 +
+ + + + +
+
+ + + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/main.css b/css/main.css new file mode 100644 index 00000000..e6583409 --- /dev/null +++ b/css/main.css @@ -0,0 +1,2532 @@ +:root { + --body-bg-color: #fff; + --content-bg-color: #f5f5f5; + --card-bg-color: #f5f5f5; + --text-color: #555; + --blockquote-color: #666; + --link-color: #555; + --link-hover-color: #222; + --brand-color: #222; + --brand-hover-color: #222; + --table-row-odd-bg-color: #f9f9f9; + --table-row-hover-bg-color: #f5f5f5; + --menu-item-bg-color: #ddd; + --theme-color: #222; + --btn-default-bg: transparent; + --btn-default-color: var(--link-color); + --btn-default-border-color: var(--link-color); + --btn-default-hover-bg: transparent; + --btn-default-hover-color: var(--link-hover-color); + --btn-default-hover-border-color: var(--link-hover-color); + --highlight-background: #f0f0f0; + --highlight-foreground: #444; + --highlight-gutter-background: #dedede; + --highlight-gutter-foreground: #555; + color-scheme: light; +} +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} +body { + margin: 0; +} +main { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} +a { + background: transparent; +} +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +img { + border-style: none; +} +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} +button, +input { +/* 1 */ + overflow: visible; +} +button, +select { +/* 1 */ + text-transform: none; +} +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} +progress { + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type='checkbox'], +[type='radio'] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +[type='search'] { + outline-offset: -2px; /* 2 */ + -webkit-appearance: textfield; /* 1 */ +} +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + font: inherit; /* 2 */ + -webkit-appearance: button; /* 1 */ +} +details { + display: block; +} +summary { + display: list-item; +} +template { + display: none; +} +[hidden] { + display: none; +} +::selection { + background: #262a30; + color: #eee; +} +html, +body { + height: 100%; +} +body { + background: var(--body-bg-color); + box-sizing: border-box; + color: var(--text-color); + font-family: Nanum Gothic, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 1em; + line-height: 2; + min-height: 100%; + position: relative; + transition: padding 0.2s ease-in-out; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: Noto Sans KR, Nanum Gothic, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-weight: bold; + line-height: 1.5; + margin: 30px 0 15px; +} +h1 { + font-size: 1.5em; +} +h2 { + font-size: 1.375em; +} +h3 { + font-size: 1.25em; +} +h4 { + font-size: 1.125em; +} +h5 { + font-size: 1em; +} +h6 { + font-size: 0.875em; +} +p { + margin: 0 0 20px; +} +a { + border-bottom: 1px solid #ccc; + color: var(--link-color); + cursor: pointer; + outline: 0; + text-decoration: none; + overflow-wrap: break-word; +} +a:hover { + border-bottom-color: var(--link-hover-color); + color: var(--link-hover-color); +} +iframe, +img, +video, +embed { + display: block; + margin-left: auto; + margin-right: auto; + max-width: 100%; +} +hr { + background-image: repeating-linear-gradient(-45deg, #ddd, #ddd 4px, transparent 4px, transparent 8px); + border: 0; + height: 3px; + margin: 40px 0; +} +blockquote { + border-left: 4px solid #ddd; + color: var(--blockquote-color); + margin: 0; + padding: 0 15px; +} +blockquote cite::before { + content: '-'; + padding: 0 5px; +} +dt { + font-weight: bold; +} +dd { + margin: 0; + padding: 0; +} +.table-container { + overflow: auto; +} +table { + border-collapse: collapse; + border-spacing: 0; + font-size: 0.875em; + margin: 0 0 20px; + width: 100%; +} +tbody tr:nth-of-type(odd) { + background: var(--table-row-odd-bg-color); +} +tbody tr:hover { + background: var(--table-row-hover-bg-color); +} +caption, +th, +td { + padding: 8px; +} +th, +td { + border: 1px solid #ddd; + border-bottom: 3px solid #ddd; +} +th { + font-weight: 700; + padding-bottom: 10px; +} +td { + border-bottom-width: 1px; +} +.btn { + background: var(--btn-default-bg); + border: 2px solid var(--btn-default-border-color); + border-radius: 0; + color: var(--btn-default-color); + display: inline-block; + font-size: 0.875em; + line-height: 2; + padding: 0 20px; + transition: background-color 0.2s ease-in-out; +} +.btn:hover { + background: var(--btn-default-hover-bg); + border-color: var(--btn-default-hover-border-color); + color: var(--btn-default-hover-color); +} +.btn + .btn { + margin: 0 0 8px 8px; +} +.btn .fa-fw { + text-align: left; + width: 1.285714285714286em; +} +.toggle { + line-height: 0; +} +.toggle .toggle-line { + background: #fff; + display: block; + height: 2px; + left: 0; + position: relative; + top: 0; + transition: all 0.4s; + width: 100%; +} +.toggle .toggle-line:first-child { + margin-top: 1px; +} +.toggle .toggle-line:not(:first-child) { + margin-top: 4px; +} +.toggle.toggle-arrow :first-child { + left: 50%; + top: 2px; + transform: rotate(45deg); + width: 50%; +} +.toggle.toggle-arrow :last-child { + left: 50%; + top: -2px; + transform: rotate(-45deg); + width: 50%; +} +.toggle.toggle-close :nth-child(2) { + opacity: 0; +} +.toggle.toggle-close :first-child { + top: 6px; + transform: rotate(45deg); +} +.toggle.toggle-close :last-child { + top: -6px; + transform: rotate(-45deg); +} +/* + +Original highlight.js style (c) Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #F0F0F0; +} + + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + + +/* User color: hue: 0 */ + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +code, +kbd, +figure.highlight, +pre { + background: var(--highlight-background); + color: var(--highlight-foreground); +} +figure.highlight, +pre { + line-height: 1.6; + margin: 0 auto 20px; +} +figure.highlight figcaption, +pre .caption, +pre figcaption { + background: var(--highlight-gutter-background); + color: var(--highlight-foreground); + display: flow-root; + font-size: 0.875em; + line-height: 1.2; + padding: 0.5em; +} +figure.highlight figcaption a, +pre .caption a, +pre figcaption a { + color: var(--highlight-foreground); + float: right; +} +figure.highlight figcaption a:hover, +pre .caption a:hover, +pre figcaption a:hover { + border-bottom-color: var(--highlight-foreground); +} +pre, +code { + font-family: consolas, Menlo, monospace, 'PingFang SC', 'Microsoft YaHei'; +} +code { + border-radius: 3px; + font-size: 0.875em; + padding: 2px 4px; + overflow-wrap: break-word; +} +kbd { + border: 2px solid #ccc; + border-radius: 0.2em; + box-shadow: 0.1em 0.1em 0.2em rgba(0,0,0,0.1); + font-family: inherit; + padding: 0.1em 0.3em; + white-space: nowrap; +} +figure.highlight { + overflow: auto; + position: relative; +} +figure.highlight pre { + border: 0; + margin: 0; + padding: 10px 0; +} +figure.highlight table { + border: 0; + margin: 0; + width: auto; +} +figure.highlight td { + border: 0; + padding: 0; +} +figure.highlight .gutter { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} +figure.highlight .gutter pre { + background: var(--highlight-gutter-background); + color: var(--highlight-gutter-foreground); + padding-left: 10px; + padding-right: 10px; + text-align: right; +} +figure.highlight .code pre { + padding-left: 10px; + width: 100%; +} +figure.highlight .marked { + background: rgba(0,0,0,0.3); +} +pre .caption, +pre figcaption { + margin-bottom: 10px; +} +.gist table { + width: auto; +} +.gist table td { + border: 0; +} +pre { + overflow: auto; + padding: 10px; + position: relative; +} +pre code { + background: none; + padding: 0; + text-shadow: none; +} +.blockquote-center { + border-left: 0; + margin: 40px 0; + padding: 0; + position: relative; + text-align: center; +} +.blockquote-center::before, +.blockquote-center::after { + left: 0; + line-height: 1; + opacity: 0.6; + position: absolute; + width: 100%; +} +.blockquote-center::before { + border-top: 1px solid #ccc; + text-align: left; + top: -20px; + content: '\f10d'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; +} +.blockquote-center::after { + border-bottom: 1px solid #ccc; + bottom: -20px; + text-align: right; + content: '\f10e'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; +} +.blockquote-center p, +.blockquote-center div { + text-align: center; +} +.group-picture { + margin-bottom: 20px; +} +.group-picture .group-picture-row { + display: flex; + gap: 3px; + margin-bottom: 3px; +} +.group-picture .group-picture-column { + flex: 1; +} +.group-picture .group-picture-column img { + height: 100%; + margin: 0; + object-fit: cover; + width: 100%; +} +.post-body .label { + color: #555; + padding: 0 2px; +} +.post-body .label.default { + background: #f0f0f0; +} +.post-body .label.primary { + background: #efe6f7; +} +.post-body .label.info { + background: #e5f2f8; +} +.post-body .label.success { + background: #e7f4e9; +} +.post-body .label.warning { + background: #fcf6e1; +} +.post-body .label.danger { + background: #fae8eb; +} +.post-body .link-grid { + display: grid; + grid-gap: 1.5rem; + gap: 1.5rem; + grid-template-columns: 1fr 1fr; + margin-bottom: 20px; + padding: 1rem; +} +@media (max-width: 767px) { + .post-body .link-grid { + grid-template-columns: 1fr; + } +} +.post-body .link-grid .link-grid-container { + border: solid #ddd; + box-shadow: 1rem 1rem 0.5rem rgba(0,0,0,0.5); + min-height: 5rem; + min-width: 0; + padding: 0.5rem; + position: relative; + transition: background 0.3s; +} +.post-body .link-grid .link-grid-container:hover { + animation: next-shake 0.5s; + background: var(--card-bg-color); +} +.post-body .link-grid .link-grid-container:active { + box-shadow: 0.5rem 0.5rem 0.25rem rgba(0,0,0,0.5); + transform: translate(0.2rem, 0.2rem); +} +.post-body .link-grid .link-grid-container .link-grid-image { + border: 1px solid #ddd; + border-radius: 50%; + box-sizing: border-box; + height: 5rem; + padding: 3px; + position: absolute; + width: 5rem; +} +.post-body .link-grid .link-grid-container p { + margin: 0 1rem 0 6rem; +} +.post-body .link-grid .link-grid-container p:first-of-type { + font-size: 1.2em; +} +.post-body .link-grid .link-grid-container p:last-of-type { + font-size: 0.8em; + line-height: 1.3rem; + opacity: 0.7; +} +.post-body .link-grid .link-grid-container a { + border: 0; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; +} +@keyframes next-shake { + 0% { + transform: translate(1pt, 1pt) rotate(0deg); + } + 10% { + transform: translate(-1pt, -2pt) rotate(-1deg); + } + 20% { + transform: translate(-3pt, 0pt) rotate(1deg); + } + 30% { + transform: translate(3pt, 2pt) rotate(0deg); + } + 40% { + transform: translate(1pt, -1pt) rotate(1deg); + } + 50% { + transform: translate(-1pt, 2pt) rotate(-1deg); + } + 60% { + transform: translate(-3pt, 1pt) rotate(0deg); + } + 70% { + transform: translate(3pt, 1pt) rotate(-1deg); + } + 80% { + transform: translate(-1pt, -1pt) rotate(1deg); + } + 90% { + transform: translate(1pt, 2pt) rotate(0deg); + } + 100% { + transform: translate(1pt, -2pt) rotate(-1deg); + } +} +.post-body .note { + border-radius: 3px; + margin-bottom: 20px; + padding: 1em; + position: relative; + border: 1px solid #eee; + border-left-width: 5px; +} +.post-body .note summary { + cursor: pointer; + outline: 0; +} +.post-body .note summary p { + display: inline; +} +.post-body .note h2, +.post-body .note h3, +.post-body .note h4, +.post-body .note h5, +.post-body .note h6 { + border-bottom: initial; + margin: 0; + padding-top: 0; +} +.post-body .note :first-child { + margin-top: 0; +} +.post-body .note :last-child { + margin-bottom: 0; +} +.post-body .note.default { + border-left-color: #777; +} +.post-body .note.default h2, +.post-body .note.default h3, +.post-body .note.default h4, +.post-body .note.default h5, +.post-body .note.default h6 { + color: #777; +} +.post-body .note.primary { + border-left-color: #6f42c1; +} +.post-body .note.primary h2, +.post-body .note.primary h3, +.post-body .note.primary h4, +.post-body .note.primary h5, +.post-body .note.primary h6 { + color: #6f42c1; +} +.post-body .note.info { + border-left-color: #428bca; +} +.post-body .note.info h2, +.post-body .note.info h3, +.post-body .note.info h4, +.post-body .note.info h5, +.post-body .note.info h6 { + color: #428bca; +} +.post-body .note.success { + border-left-color: #5cb85c; +} +.post-body .note.success h2, +.post-body .note.success h3, +.post-body .note.success h4, +.post-body .note.success h5, +.post-body .note.success h6 { + color: #5cb85c; +} +.post-body .note.warning { + border-left-color: #f0ad4e; +} +.post-body .note.warning h2, +.post-body .note.warning h3, +.post-body .note.warning h4, +.post-body .note.warning h5, +.post-body .note.warning h6 { + color: #f0ad4e; +} +.post-body .note.danger { + border-left-color: #d9534f; +} +.post-body .note.danger h2, +.post-body .note.danger h3, +.post-body .note.danger h4, +.post-body .note.danger h5, +.post-body .note.danger h6 { + color: #d9534f; +} +.post-body .tabs { + margin-bottom: 20px; +} +.post-body .tabs, +.tabs-comment { + padding-top: 10px; +} +.post-body .tabs ul.nav-tabs, +.tabs-comment ul.nav-tabs { + background: var(--body-bg-color); + display: flex; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 0; + padding: 0; + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 5; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs, + .tabs-comment ul.nav-tabs { + display: block; + margin-bottom: 5px; + } +} +.post-body .tabs ul.nav-tabs li.tab, +.tabs-comment ul.nav-tabs li.tab { + border-bottom: 1px solid #ddd; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-radius: 0 0 0 0; + border-top: 3px solid transparent; + flex-grow: 1; + list-style-type: none; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab, + .tabs-comment ul.nav-tabs li.tab { + border-bottom: 1px solid transparent; + border-left: 3px solid transparent; + border-right: 1px solid transparent; + border-top: 1px solid transparent; + } +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab, + .tabs-comment ul.nav-tabs li.tab { + border-radius: 0; + } +} +.post-body .tabs ul.nav-tabs li.tab a, +.tabs-comment ul.nav-tabs li.tab a { + border-bottom: initial; + display: block; + line-height: 1.8; + padding: 0.25em 0.75em; + text-align: center; + transition: all 0.2s ease-out; +} +.post-body .tabs ul.nav-tabs li.tab a i, +.tabs-comment ul.nav-tabs li.tab a i { + width: 1.285714285714286em; +} +.post-body .tabs ul.nav-tabs li.tab.active, +.tabs-comment ul.nav-tabs li.tab.active { + border-bottom-color: transparent; + border-left-color: #ddd; + border-right-color: #ddd; + border-top-color: #fc6423; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab.active, + .tabs-comment ul.nav-tabs li.tab.active { + border-bottom-color: #ddd; + border-left-color: #fc6423; + border-right-color: #ddd; + border-top-color: #ddd; + } +} +.post-body .tabs ul.nav-tabs li.tab.active a, +.tabs-comment ul.nav-tabs li.tab.active a { + cursor: default; +} +.post-body .tabs .tab-content, +.tabs-comment .tab-content { + border: 1px solid #ddd; + border-radius: 0 0 0 0; + border-top-color: transparent; +} +@media (max-width: 413px) { + .post-body .tabs .tab-content, + .tabs-comment .tab-content { + border-radius: 0; + border-top-color: #ddd; + } +} +.post-body .tabs .tab-content .tab-pane, +.tabs-comment .tab-content .tab-pane { + padding: 20px 20px 0; +} +.post-body .tabs .tab-content .tab-pane:not(.active), +.tabs-comment .tab-content .tab-pane:not(.active) { + display: none; +} +.pagination .prev, +.pagination .next, +.pagination .page-number, +.pagination .space { + display: inline-block; + margin: -1px 10px 0; + padding: 0 10px; +} +@media (max-width: 767px) { + .pagination .prev, + .pagination .next, + .pagination .page-number, + .pagination .space { + margin: 0 5px; + } +} +.pagination .page-number.current { + background: #ccc; + border-color: #ccc; + color: var(--content-bg-color); +} +.pagination { + border-top: 1px solid #eee; + margin: 120px 0 0; + text-align: left; +} +.pagination .prev, +.pagination .next, +.pagination .page-number { + border-bottom: 0; + border-top: 1px solid #eee; + transition: border-color 0.2s ease-in-out; +} +.pagination .prev:hover, +.pagination .next:hover, +.pagination .page-number:hover { + border-top-color: var(--link-hover-color); +} +@media (max-width: 767px) { + .pagination { + border-top: 0; + } + .pagination .prev, + .pagination .next, + .pagination .page-number { + border-bottom: 1px solid #eee; + border-top: 0; + } + .pagination .prev:hover, + .pagination .next:hover, + .pagination .page-number:hover { + border-bottom-color: var(--link-hover-color); + } +} +.pagination .space { + margin: 0; + padding: 0; +} +.comments { + margin-top: 60px; + overflow: hidden; +} +.comment-button-group { + display: flex; + display: flex; + flex-wrap: wrap; + justify-content: center; + justify-content: center; + margin: 1em 0; +} +.comment-button-group .comment-button { + margin: 0.1em 0.2em; +} +.comment-button-group .comment-button.active { + background: var(--btn-default-hover-bg); + border-color: var(--btn-default-hover-border-color); + color: var(--btn-default-hover-color); +} +.comment-position { + display: none; +} +.comment-position.active { + display: block; +} +.tabs-comment { + margin-top: 4em; + padding-top: 0; +} +.tabs-comment .comments { + margin-top: 0; + padding-top: 0; +} +.headband { + background: var(--theme-color); + height: 3px; +} +@media (max-width: 991px) { + .headband { + display: none; + } +} +.site-brand-container { + display: flex; + flex-shrink: 0; + padding: 0 10px; +} +.use-motion .column, +.use-motion .site-brand-container .toggle { + opacity: 0; +} +.site-meta { + flex-grow: 1; + text-align: center; +} +@media (max-width: 767px) { + .site-meta { + text-align: center; + } +} +.custom-logo-image { + margin-top: 20px; +} +@media (max-width: 991px) { + .custom-logo-image { + display: none; + } +} +.brand { + border-bottom: 0; + color: var(--brand-color); + display: inline-block; + padding: 2px 1px; +} +.brand:hover { + color: var(--brand-hover-color); +} +.site-title { + font-family: Lato, Nanum Gothic, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 1.375em; + font-weight: normal; + line-height: 1.5; + margin: 0; +} +.site-subtitle { + color: #999; + font-size: 0.8125em; + margin: 10px 0; +} +.use-motion .site-title, +.use-motion .site-subtitle, +.use-motion .custom-logo-image { + opacity: 0; + position: relative; + top: -10px; +} +.site-nav-toggle, +.site-nav-right { + display: none; +} +@media (max-width: 767px) { + .site-nav-toggle, + .site-nav-right { + display: flex; + flex-direction: column; + justify-content: center; + } +} +.site-nav-toggle .toggle, +.site-nav-right .toggle { + color: var(--text-color); + padding: 10px; + width: 22px; +} +.site-nav-toggle .toggle .toggle-line, +.site-nav-right .toggle .toggle-line { + background: var(--text-color); + border-radius: 1px; +} +@media (max-width: 767px) { + .site-nav { + --scroll-height: 0; + height: 0; + overflow: hidden; + transition: height 0.2s ease-in-out; + } + body:not(.site-nav-on) .site-nav .animated { + animation: none; + } + body.site-nav-on .site-nav { + height: var(--scroll-height); + } +} +.menu { + margin: 0; + padding: 1em 0; + text-align: center; +} +.menu-item { + display: inline-block; + list-style: none; + margin: 0 10px; +} +@media (max-width: 767px) { + .menu-item { + display: block; + margin-top: 10px; + } + .menu-item.menu-item-search { + display: none; + } +} +.menu-item a { + border-bottom: 0; + display: block; + font-size: 0.8125em; + transition: border-color 0.2s ease-in-out; +} +.menu-item a:hover, +.menu-item a.menu-item-active { + background: var(--menu-item-bg-color); +} +.menu-item .fa, +.menu-item .fab, +.menu-item .far, +.menu-item .fas { + margin-right: 8px; +} +.menu-item .badge { + display: inline-block; + font-weight: bold; + line-height: 1; + margin-left: 0.35em; + margin-top: 0.35em; + text-align: center; + white-space: nowrap; +} +@media (max-width: 767px) { + .menu-item .badge { + float: right; + margin-left: 0; + } +} +.use-motion .menu-item { + visibility: hidden; +} +.sidebar-inner { + color: #999; + padding: 18px 10px; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; +} +.cc-license .cc-opacity { + border-bottom: 0; + opacity: 0.7; +} +.cc-license .cc-opacity:hover { + opacity: 0.9; +} +.cc-license img { + display: inline-block; +} +.site-author-image { + border: 2px solid #333; + max-width: 96px; + padding: 2px; +} +.site-author-name { + color: #f5f5f5; + font-weight: normal; + margin: 5px 0 0; +} +.site-description { + color: #999; + font-size: 1em; + margin-top: 5px; +} +.links-of-author a { + font-size: 0.8125em; +} +.links-of-author .fa, +.links-of-author .fab, +.links-of-author .far, +.links-of-author .fas { + margin-right: 2px; +} +.sidebar .sidebar-button:not(:first-child) { + margin-top: 15px; +} +.sidebar .sidebar-button button { + background: transparent; + color: #fc6423; + cursor: pointer; + line-height: 2; + padding: 0 15px; + border: 1px solid #fc6423; + border-radius: 4px; +} +.sidebar .sidebar-button button:hover { + background: #fc6423; + color: #fff; +} +.sidebar .sidebar-button button .fa, +.sidebar .sidebar-button button .fab, +.sidebar .sidebar-button button .far, +.sidebar .sidebar-button button .fas { + margin-right: 5px; +} +.links-of-blogroll { + font-size: 0.8125em; +} +.links-of-blogroll-title { + font-size: 0.875em; + font-weight: 600; +} +.links-of-blogroll-list { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar-nav { + display: none; + margin: 0; + padding-bottom: 20px; + padding-left: 0; +} +.sidebar-nav-active .sidebar-nav { + display: block; +} +.sidebar-nav li { + border-bottom: 1px solid transparent; + color: #666; + cursor: pointer; + display: inline-block; + font-size: 0.875em; +} +.sidebar-nav li.sidebar-nav-overview { + margin-left: 10px; +} +.sidebar-nav li:hover { + color: #f5f5f5; +} +.sidebar-toc-active .sidebar-nav-toc, +.sidebar-overview-active .sidebar-nav-overview { + border-bottom-color: #87daff; + color: #87daff; +} +.sidebar-toc-active .sidebar-nav-toc:hover, +.sidebar-overview-active .sidebar-nav-overview:hover { + color: #87daff; +} +.sidebar-panel-container { + flex: 1; + overflow-x: hidden; + overflow-y: auto; +} +.sidebar-panel { + display: none; +} +.sidebar-overview-active .site-overview-wrap { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; +} +.sidebar-toc-active .post-toc-wrap { + display: block; +} +.sidebar-toggle { + bottom: 61px; + height: 16px; + padding: 5px; + width: 16px; + background: #222; + cursor: pointer; + opacity: 0.8; + position: fixed; + z-index: 30; + left: 30px; +} +@media (max-width: 991px) { + .sidebar-toggle { + left: 20px; + } +} +.sidebar-toggle:hover { + opacity: 1; +} +@media (max-width: 991px) { + .sidebar-toggle { + opacity: 1; + } +} +.sidebar-toggle:hover .toggle-line { + background: #87daff; +} +@media (any-hover: hover) { + body:not(.sidebar-active) .sidebar-toggle:hover :first-child { + left: 50%; + top: 2px; + transform: rotate(45deg); + width: 50%; + } + body:not(.sidebar-active) .sidebar-toggle:hover :last-child { + left: 50%; + top: -2px; + transform: rotate(-45deg); + width: 50%; + } +} +.sidebar-active .sidebar-toggle :nth-child(2) { + opacity: 0; +} +.sidebar-active .sidebar-toggle :first-child { + top: 6px; + transform: rotate(45deg); +} +.sidebar-active .sidebar-toggle :last-child { + top: -6px; + transform: rotate(-45deg); +} +.post-toc { + font-size: 0.875em; +} +.post-toc ol { + list-style: none; + margin: 0; + padding: 0 2px 5px 10px; + text-align: left; +} +.post-toc ol > ol { + padding-left: 0; +} +.post-toc ol a { + transition: all 0.2s ease-in-out; +} +.post-toc .nav-item { + line-height: 1.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.post-toc .nav .nav-child { + display: none; +} +.post-toc .nav .active > .nav-child { + display: block; +} +.post-toc .nav .active-current > .nav-child { + display: block; +} +.post-toc .nav .active-current > .nav-child > .nav-item { + display: block; +} +.post-toc .nav .active > a { + border-bottom-color: #87daff; + color: #87daff; +} +.post-toc .nav .active-current > a { + color: #87daff; +} +.post-toc .nav .active-current > a:hover { + color: #87daff; +} +.site-state { + display: flex; + flex-wrap: wrap; + justify-content: center; + line-height: 1.4; +} +.site-state-item { + padding: 0 15px; +} +.site-state-item a { + border-bottom: 0; + display: block; +} +.site-state-item-count { + display: block; + font-size: 1.25em; + font-weight: 600; +} +.site-state-item-name { + color: inherit; + font-size: 0.875em; +} +.footer { + color: #999; + font-size: 0.875em; + padding: 20px 0; +} +.footer.footer-fixed { + bottom: 0; + left: 0; + position: absolute; + right: 0; +} +.footer-inner { + box-sizing: border-box; + text-align: left; + display: flex; + flex-direction: column; + justify-content: center; + margin: 0 auto; + width: 700px; +} +@media (max-width: 767px) { + .footer-inner { + width: auto; + } +} +@media (min-width: 1200px) { + .footer-inner { + width: 800px; + } +} +@media (min-width: 1600px) { + .footer-inner { + width: 900px; + } +} +.use-motion .footer { + opacity: 0; +} +.languages { + display: inline-block; + font-size: 1.125em; + position: relative; +} +.languages .lang-select-label span { + margin: 0 0.5em; +} +.languages .lang-select { + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; +} +.with-love { + color: #f00; + display: inline-block; + margin: 0 5px; +} +@keyframes icon-animate { + 0%, 100% { + transform: scale(1); + } + 10%, 30% { + transform: scale(0.9); + } + 20%, 40%, 60%, 80% { + transform: scale(1.1); + } + 50%, 70% { + transform: scale(1.1); + } +} +.back-to-top { + font-size: 12px; + align-items: center; + bottom: -100px; + color: #fff; + display: flex; + height: 26px; + transition: bottom 0.2s ease-in-out; + background: #222; + cursor: pointer; + opacity: 0.8; + position: fixed; + z-index: 30; + left: 30px; +} +.back-to-top span { + margin-right: 8px; + display: none; +} +.back-to-top .fa { + text-align: center; + width: 26px; +} +@media (max-width: 991px) { + .back-to-top { + left: 20px; + } +} +.back-to-top:hover { + opacity: 1; +} +@media (max-width: 991px) { + .back-to-top { + opacity: 1; + } +} +.back-to-top:hover { + color: #87daff; +} +.back-to-top.back-to-top-on { + bottom: 30px; +} +.reading-progress-bar { + --progress: 0; + background: #37c6c0; + height: 3px; + position: fixed; + z-index: 50; + width: var(--progress); + left: 0; + top: 0; +} +.rtl.post-body p, +.rtl.post-body a, +.rtl.post-body h1, +.rtl.post-body h2, +.rtl.post-body h3, +.rtl.post-body h4, +.rtl.post-body h5, +.rtl.post-body h6, +.rtl.post-body li, +.rtl.post-body ul, +.rtl.post-body ol { + direction: rtl; + font-family: UKIJ Ekran; +} +.rtl.post-title { + font-family: UKIJ Ekran; +} +.post-button { + margin-top: 40px; + text-align: left; +} +.use-motion .post-block, +.use-motion .pagination, +.use-motion .comments { + visibility: hidden; +} +.use-motion .post-header { + visibility: hidden; +} +.use-motion .post-body { + visibility: hidden; +} +.use-motion .collection-header { + visibility: hidden; +} +.posts-collapse .post-content { + margin-bottom: 35px; + margin-left: 35px; + position: relative; +} +@media (max-width: 767px) { + .posts-collapse .post-content { + margin-left: 0; + margin-right: 0; + } +} +.posts-collapse .post-content .collection-title { + font-size: 1.125em; + position: relative; +} +.posts-collapse .post-content .collection-title::before { + background: #999; + border: 1px solid #fff; + margin-left: -6px; + margin-top: -4px; + position: absolute; + top: 50%; + border-radius: 50%; + content: ' '; + height: 10px; + width: 10px; +} +.posts-collapse .post-content .collection-year { + font-size: 1.5em; + font-weight: bold; + margin: 60px 0; + position: relative; +} +.posts-collapse .post-content .collection-year::before { + background: #bbb; + margin-left: -4px; + margin-top: -4px; + position: absolute; + top: 50%; + border-radius: 50%; + content: ' '; + height: 8px; + width: 8px; +} +.posts-collapse .post-content .collection-header { + display: block; + margin-left: 20px; +} +.posts-collapse .post-content .collection-header small { + color: #bbb; + margin-left: 5px; +} +.posts-collapse .post-content .post-header { + border-bottom: 1px dashed #ccc; + margin: 30px 2px 0; + padding-left: 15px; + position: relative; + transition: border 0.2s ease-in-out; +} +.posts-collapse .post-content .post-header::before { + background: #bbb; + border: 1px solid #fff; + left: -6px; + position: absolute; + top: 0.75em; + transition: background 0.2s ease-in-out; + border-radius: 50%; + content: ' '; + height: 6px; + width: 6px; +} +.posts-collapse .post-content .post-header:hover { + border-bottom-color: #666; +} +.posts-collapse .post-content .post-header:hover::before { + background: #222; +} +.posts-collapse .post-content .post-meta-container { + display: inline; + font-size: 0.75em; + margin-right: 10px; +} +.posts-collapse .post-content .post-title { + display: inline; +} +.posts-collapse .post-content .post-title a { + border-bottom: 0; + color: var(--link-color); +} +.posts-collapse .post-content .post-title .fa-external-link-alt { + font-size: 0.875em; + margin-left: 5px; +} +.posts-collapse .post-content::before { + background: #f5f5f5; + content: ' '; + height: 100%; + margin-left: -2px; + position: absolute; + top: 1.25em; + width: 4px; +} +.post-body { + font-family: Nanum Gothic, 'PingFang SC', 'Microsoft YaHei', sans-serif; + overflow-wrap: break-word; +} +@media (min-width: 1200px) { + .post-body { + font-size: 1.125em; + } +} +@media (min-width: 992px) { + .post-body { + text-align: justify; + } +} +@media (max-width: 991px) { + .post-body { + text-align: justify; + } +} +.post-body h1 .header-anchor, +.post-body h2 .header-anchor, +.post-body h3 .header-anchor, +.post-body h4 .header-anchor, +.post-body h5 .header-anchor, +.post-body h6 .header-anchor, +.post-body h1 .headerlink, +.post-body h2 .headerlink, +.post-body h3 .headerlink, +.post-body h4 .headerlink, +.post-body h5 .headerlink, +.post-body h6 .headerlink { + border-bottom-style: none; + color: inherit; + float: right; + font-size: 0.875em; + margin-left: 10px; + opacity: 0; +} +.post-body h1 .header-anchor::before, +.post-body h2 .header-anchor::before, +.post-body h3 .header-anchor::before, +.post-body h4 .header-anchor::before, +.post-body h5 .header-anchor::before, +.post-body h6 .header-anchor::before, +.post-body h1 .headerlink::before, +.post-body h2 .headerlink::before, +.post-body h3 .headerlink::before, +.post-body h4 .headerlink::before, +.post-body h5 .headerlink::before, +.post-body h6 .headerlink::before { + content: '\f0c1'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; +} +.post-body h1:hover .header-anchor, +.post-body h2:hover .header-anchor, +.post-body h3:hover .header-anchor, +.post-body h4:hover .header-anchor, +.post-body h5:hover .header-anchor, +.post-body h6:hover .header-anchor, +.post-body h1:hover .headerlink, +.post-body h2:hover .headerlink, +.post-body h3:hover .headerlink, +.post-body h4:hover .headerlink, +.post-body h5:hover .headerlink, +.post-body h6:hover .headerlink { + opacity: 0.5; +} +.post-body h1:hover .header-anchor:hover, +.post-body h2:hover .header-anchor:hover, +.post-body h3:hover .header-anchor:hover, +.post-body h4:hover .header-anchor:hover, +.post-body h5:hover .header-anchor:hover, +.post-body h6:hover .header-anchor:hover, +.post-body h1:hover .headerlink:hover, +.post-body h2:hover .headerlink:hover, +.post-body h3:hover .headerlink:hover, +.post-body h4:hover .headerlink:hover, +.post-body h5:hover .headerlink:hover, +.post-body h6:hover .headerlink:hover { + opacity: 1; +} +.post-body .exturl .fa { + font-size: 0.875em; + margin-left: 4px; +} +.post-body .image-caption, +.post-body img + figcaption, +.post-body .fancybox + figcaption { + color: #999; + font-size: 0.875em; + font-weight: bold; + line-height: 1; + margin: -15px auto 15px; + text-align: center; +} +.post-body iframe, +.post-body img, +.post-body video, +.post-body embed { + margin-bottom: 20px; +} +.post-body .video-container { + height: 0; + margin-bottom: 20px; + overflow: hidden; + padding-top: 75%; + position: relative; + width: 100%; +} +.post-body .video-container iframe, +.post-body .video-container object, +.post-body .video-container embed { + height: 100%; + left: 0; + margin: 0; + position: absolute; + top: 0; + width: 100%; +} +.post-gallery { + display: flex; + min-height: 200px; +} +.post-gallery .post-gallery-image { + flex: 1; +} +.post-gallery .post-gallery-image:not(:first-child) { + clip-path: polygon(40px 0, 100% 0, 100% 100%, 0 100%); + margin-left: -20px; +} +.post-gallery .post-gallery-image:not(:last-child) { + margin-right: -20px; +} +.post-gallery .post-gallery-image img { + height: 100%; + object-fit: cover; + opacity: 1; + width: 100%; +} +.posts-expand .post-gallery { + margin-bottom: 60px; +} +.posts-collapse .post-gallery { + margin: 15px 0; +} +.posts-expand .post-header { + font-size: 1.125em; + margin-bottom: 60px; + text-align: center; +} +.posts-expand .post-title { + font-size: 1.5em; + font-weight: normal; + margin: initial; + overflow-wrap: break-word; +} +.posts-expand .post-title-link { + border-bottom: 0; + color: var(--link-color); + display: inline-block; + position: relative; +} +.posts-expand .post-title-link::before { + background: var(--link-color); + bottom: 0; + content: ''; + height: 2px; + left: 0; + position: absolute; + transform: scaleX(0); + transition: transform 0.2s ease-in-out; + width: 100%; +} +.posts-expand .post-title-link:hover::before { + transform: scaleX(1); +} +.posts-expand .post-title-link .fa-external-link-alt { + font-size: 0.875em; + margin-left: 5px; +} +.post-sticky-flag { + display: inline-block; + margin-right: 8px; + transform: rotate(30deg); +} +.posts-expand .post-meta-container { + color: #999; + font-family: Nanum Gothic, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 0.75em; + margin-top: 3px; +} +.posts-expand .post-meta-container .post-description { + font-size: 0.875em; + margin-top: 2px; +} +.posts-expand .post-meta-container time { + border-bottom: 1px dashed #999; +} +.post-meta { + display: flex; + flex-wrap: wrap; + justify-content: center; +} +:not(.post-meta-break) + .post-meta-item::before { + content: '|'; + margin: 0 0.5em; +} +.post-meta-item-icon { + margin-right: 3px; +} +@media (max-width: 991px) { + .post-meta-item-text { + display: none; + } +} +.post-meta-break { + flex-basis: 100%; + height: 0; +} +.post-nav { + border-top: 1px solid #eee; + display: flex; + gap: 30px; + justify-content: space-between; + margin-top: 1em; + padding: 10px 5px 0; +} +.post-nav-item { + flex: 1; +} +.post-nav-item a { + border-bottom: 0; + display: block; + font-size: 0.875em; + line-height: 1.6; +} +.post-nav-item a:active { + top: 2px; +} +.post-nav-item .fa { + font-size: 0.75em; +} +.post-nav-item:first-child .fa { + margin-right: 5px; +} +.post-nav-item:last-child { + text-align: right; +} +.post-nav-item:last-child .fa { + margin-left: 5px; +} +.post-footer { + display: flex; + flex-direction: column; + justify-content: center; +} +.post-eof { + background: #ccc; + height: 1px; + margin: 80px auto 60px; + width: 8%; +} +.post-block:last-of-type .post-eof { + display: none; +} +.post-tags { + margin-top: 40px; + text-align: left; +} +.post-tags a { + display: inline-block; + font-size: 0.8125em; +} +.post-tags a:not(:last-child) { + margin-right: 10px; +} +.social-like { + border-top: 1px solid #eee; + font-size: 0.875em; + margin-top: 1em; + padding-top: 1em; + text-align: center; +} +.reward-container { + margin: 1em 0 0; + padding: 1em 0; + text-align: center; +} +.reward-container button { + background: transparent; + color: #87daff; + cursor: pointer; + line-height: 2; + padding: 0 15px; + border: 2px solid #87daff; + border-radius: 2px; + outline: 0; + transition: all 0.2s ease-in-out; + vertical-align: text-top; +} +.reward-container button:hover { + background: #87daff; + color: #fff; +} +.post-reward { + display: none; + padding-top: 20px; +} +.post-reward.active { + display: block; +} +.post-reward div { + display: inline-block; +} +.post-reward div span { + display: block; +} +.post-reward img { + display: inline-block; + margin: 0.8em 2em 0; + max-width: 100%; + width: 180px; +} +@keyframes next-roll { + from { + transform: rotateZ(30deg); + } + to { + transform: rotateZ(-30deg); + } +} +.category-all-page .category-all-title { + text-align: center; +} +.category-all-page .category-all { + margin-top: 20px; +} +.category-all-page .category-list { + list-style: none; + margin: 0; + padding: 0; +} +.category-all-page .category-list-item { + margin: 5px 10px; +} +.category-all-page .category-list-count { + color: #bbb; +} +.category-all-page .category-list-count::before { + content: ' ('; +} +.category-all-page .category-list-count::after { + content: ') '; +} +.category-all-page .category-list-child { + padding-left: 10px; +} +.event-list hr { + background: #222; + margin: 20px 0 45px; +} +.event-list hr::after { + background: #222; + color: #fff; + content: 'NOW'; + display: inline-block; + font-weight: bold; + padding: 0 5px; +} +.event-list .event { + --event-background: #222; + --event-foreground: #bbb; + --event-title: #fff; + background: var(--event-background); + padding: 15px; +} +.event-list .event .event-summary { + border-bottom: 0; + color: var(--event-title); + margin: 0; + padding: 0 0 0 35px; + position: relative; +} +.event-list .event .event-summary::before { + animation: dot-flash 1s alternate infinite ease-in-out; + background: var(--event-title); + left: 0; + margin-top: -6px; + position: absolute; + top: 50%; + border-radius: 50%; + content: ' '; + height: 12px; + width: 12px; +} +.event-list .event:nth-of-type(odd) .event-summary::before { + animation-delay: 0.5s; +} +.event-list .event:not(:last-child) { + margin-bottom: 20px; +} +.event-list .event .event-relative-time { + color: var(--event-foreground); + display: inline-block; + font-size: 12px; + font-weight: normal; + padding-left: 12px; +} +.event-list .event .event-details { + color: var(--event-foreground); + display: block; + line-height: 18px; + padding: 6px 0 6px 35px; +} +.event-list .event .event-details::before { + color: var(--event-foreground); + display: inline-block; + margin-right: 9px; + width: 14px; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; +} +.event-list .event .event-details.event-location::before { + content: '\f041'; +} +.event-list .event .event-details.event-duration::before { + content: '\f017'; +} +.event-list .event .event-details.event-description::before { + content: '\f024'; +} +.event-list .event-past { + --event-background: #f5f5f5; + --event-foreground: #999; + --event-title: #222; +} +@keyframes dot-flash { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} +ul.breadcrumb { + font-size: 0.75em; + list-style: none; + margin: 1em 0; + padding: 0 2em; + text-align: center; +} +ul.breadcrumb li { + display: inline; +} +ul.breadcrumb li:not(:first-child)::before { + content: '/\00a0'; + font-weight: normal; + padding: 0.5em; +} +ul.breadcrumb li:last-child { + font-weight: bold; +} +.tag-cloud { + text-align: center; +} +.tag-cloud a { + display: inline-block; + margin: 10px; +} +.tag-cloud-0 { + border-bottom-color: #aaa; + color: #aaa; +} +.tag-cloud-1 { + border-bottom-color: #9a9a9a; + color: #9a9a9a; +} +.tag-cloud-2 { + border-bottom-color: #8b8b8b; + color: #8b8b8b; +} +.tag-cloud-3 { + border-bottom-color: #7c7c7c; + color: #7c7c7c; +} +.tag-cloud-4 { + border-bottom-color: #6c6c6c; + color: #6c6c6c; +} +.tag-cloud-5 { + border-bottom-color: #5d5d5d; + color: #5d5d5d; +} +.tag-cloud-6 { + border-bottom-color: #4e4e4e; + color: #4e4e4e; +} +.tag-cloud-7 { + border-bottom-color: #3e3e3e; + color: #3e3e3e; +} +.tag-cloud-8 { + border-bottom-color: #2f2f2f; + color: #2f2f2f; +} +.tag-cloud-9 { + border-bottom-color: #202020; + color: #202020; +} +.tag-cloud-10 { + border-bottom-color: #111; + color: #111; +} +.gt-header a, +.gt-comments a, +.gt-popup a { + border-bottom: 0; +} +.gt-container .gt-popup .gt-action.is--active::before { + top: 0.7em; +} +.use-motion .animated { + animation-fill-mode: none; + visibility: inherit; +} +.use-motion .sidebar .animated { + animation-fill-mode: both; +} +hr { + height: 2px; + margin: 20px 0; +} +.btn { + padding: 0 10px; +} +.headband { + display: none; +} +@media (max-width: 767px) { + .pagination { + margin: 80px 0 0; + text-align: center; + } +} +.footer { + background: var(--content-bg-color); + color: var(--text-color); + padding: 10px 0; +} +@media (max-width: 767px) { + .footer-inner { + text-align: center; + } +} +.column { + background: var(--content-bg-color); +} +.header { + align-items: center; + display: flex; + padding: 20px 0; +} +@media (max-width: 767px) { + .header { + display: block; + padding: 10px 0; + } +} +.site-meta { + line-height: normal; +} +@media (max-width: 767px) { + .site-meta .brand { + display: block; + } +} +.site-meta .site-title { + font-weight: bolder; +} +.logo-line { + background: var(--brand-color); + display: block; + height: 2px; + margin: 0 auto; + width: 75%; +} +@media (max-width: 767px) { + .logo-line { + display: none; + } +} +.use-motion .logo-line:first-of-type { + transform: scaleX(0); + transform-origin: left; +} +.use-motion .logo-line:last-of-type { + transform: scaleX(0); + transform-origin: right; +} +.site-subtitle { + display: none; +} +.site-nav { + flex-grow: 1; +} +@media (max-width: 767px) { + .site-nav { + padding: 0 10px 0; + } +} +@media (max-width: 767px) { + .main-menu { + padding-top: 10px; + } +} +.menu { + padding: 0; +} +.menu .menu-item { + margin: 0; +} +@media (max-width: 767px) { + .menu .menu-item { + margin-top: 5px; + } +} +.menu .menu-item a { + border-radius: 2px; + padding: 0 10px; + transition-property: background; +} +@media (max-width: 767px) { + .menu .menu-item a { + text-align: left; + } +} +.menu .menu-item .badge { + background: #fff; + border-radius: 10px; + color: #555; + padding: 1px 4px; + text-shadow: 1px 1px 0 rgba(0,0,0,0.1); +} +.posts-expand.index .post-header { + text-align: left; +} +@media (max-width: 767px) { + .posts-expand.index .post-header { + text-align: center; + } +} +.posts-expand.index .post-meta-container { + margin-top: 5px; +} +.posts-expand.index .post-meta { + justify-content: flex-start; +} +@media (max-width: 767px) { + .posts-expand.index .post-meta { + justify-content: center; + } +} +.posts-expand .post-eof { + display: none; +} +.posts-expand .post-block:not(:first-of-type) { + margin-top: 120px; +} +.posts-expand .post-header { + margin-bottom: 20px; +} +.posts-expand .post-tags a { + background: var(--content-bg-color); + border-bottom: 0; + padding: 1px 5px; +} +.posts-expand .post-tags a:hover { + background: var(--menu-item-bg-color); +} +.posts-expand .post-nav { + margin-top: 40px; +} +.post-button { + margin-top: 20px; +} +.post-button .btn { + background: none; + border: 0; + border-bottom: 2px solid var(--btn-default-border-color); + padding: 0; + transition-property: border; +} +.post-button .btn:hover { + border-bottom-color: var(--btn-default-hover-border-color); +} +.header { + margin: 0 auto; + width: 700px; +} +@media (max-width: 767px) { + .header { + width: auto; + } +} +@media (min-width: 1200px) { + .header { + width: 800px; + } +} +@media (min-width: 1600px) { + .header { + width: 900px; + } +} +.main-inner { + margin: 0 auto; + width: 700px; + padding-bottom: 80px; +} +@media (max-width: 767px) { + .main-inner { + width: auto; + } +} +@media (min-width: 1200px) { + .main-inner { + width: 800px; + } +} +@media (min-width: 1600px) { + .main-inner { + width: 900px; + } +} +@media (max-width: 767px) { + .main-inner { + padding-left: 20px; + padding-right: 20px; + } +} +.post-block:first-of-type { + padding-top: 80px; +} +@media (max-width: 767px) { + .post-block:first-of-type { + padding-top: 60px; + } +} +@media (min-width: 1200px) { + .sidebar-active { + padding-left: 320px; + } +} +.sidebar { + left: -320px; +} +.sidebar-active .sidebar { + left: 0; +} +.sidebar { + background: #222; + bottom: 0; + box-shadow: inset 0 2px 6px #000; + max-height: 100vh; + overflow-y: auto; + position: fixed; + top: 0; + transition: all 0.2s ease-out; + width: 320px; + z-index: 20; +} +.sidebar a { + border-bottom-color: #555; + color: #999; +} +.sidebar a:hover { + border-bottom-color: #eee; + color: #eee; +} +.links-of-author:not(:first-child) { + margin-top: 15px; +} +.links-of-author a { + border-bottom-color: #555; + display: inline-block; + margin-bottom: 10px; + margin-right: 10px; + vertical-align: middle; +} +.links-of-author a::before { + background: #ffde2a; + display: inline-block; + margin-right: 3px; + transform: translateY(-2px); + border-radius: 50%; + content: ' '; + height: 4px; + width: 4px; +} +.links-of-blogroll-item { + padding: 2px 10px; +} +.links-of-blogroll-item a { + box-sizing: border-box; + display: inline-block; + max-width: 280px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.popular-posts .popular-posts-item .popular-posts-link:hover { + background: none; +} +.sidebar-dimmer { + background: #000; + height: 100%; + left: 0; + opacity: 0; + position: fixed; + top: 0; + transition: visibility 0.4s, opacity 0.4s; + visibility: hidden; + width: 100%; + z-index: 10; +} +.sidebar-active .sidebar-dimmer { + opacity: 0.7; + visibility: visible; +} +@media (min-width: 1200px) { + .sidebar-dimmer { + display: none; + } +} +.sub-menu { + margin: 10px 0; +} +.sub-menu .menu-item { + display: inline-block; +} diff --git a/css/noscript.css b/css/noscript.css new file mode 100644 index 00000000..6418c57d --- /dev/null +++ b/css/noscript.css @@ -0,0 +1,48 @@ +body { + margin-top: 2rem; +} +.use-motion .menu-item, +.use-motion .sidebar, +.use-motion .sidebar-inner, +.use-motion .post-block, +.use-motion .pagination, +.use-motion .comments, +.use-motion .post-header, +.use-motion .post-body, +.use-motion .collection-header { + visibility: visible; +} +.use-motion .column, +.use-motion .site-brand-container .toggle, +.use-motion .footer { + opacity: initial; +} +.use-motion .site-title, +.use-motion .site-subtitle, +.use-motion .custom-logo-image { + opacity: initial; + top: initial; +} +.use-motion .logo-line { + transform: scaleX(1); +} +.search-pop-overlay, +.sidebar-nav { + display: none; +} +.sidebar-panel { + display: block; +} +.noscript-warning { + background-color: #f55; + color: #fff; + font-family: sans-serif; + font-size: 1rem; + font-weight: bold; + left: 0; + position: fixed; + text-align: center; + top: 0; + width: 100%; + z-index: 50; +} diff --git a/images/140708_00.png b/images/140708_00.png new file mode 100644 index 00000000..6d48dfc0 Binary files /dev/null and b/images/140708_00.png differ diff --git a/images/140716_00.jpg b/images/140716_00.jpg new file mode 100644 index 00000000..33b5b9e2 Binary files /dev/null and b/images/140716_00.jpg differ diff --git a/images/140719_00.png b/images/140719_00.png new file mode 100644 index 00000000..f9bcd541 Binary files /dev/null and b/images/140719_00.png differ diff --git a/images/140721_00.png b/images/140721_00.png new file mode 100644 index 00000000..7debe81e Binary files /dev/null and b/images/140721_00.png differ diff --git a/images/140721_01.PNG b/images/140721_01.PNG new file mode 100644 index 00000000..8910cd20 Binary files /dev/null and b/images/140721_01.PNG differ diff --git a/images/140721_02.PNG b/images/140721_02.PNG new file mode 100644 index 00000000..1a5c9a20 Binary files /dev/null and b/images/140721_02.PNG differ diff --git a/images/140912_00.png b/images/140912_00.png new file mode 100644 index 00000000..27e1a6b5 Binary files /dev/null and b/images/140912_00.png differ diff --git a/images/140917_00.PNG b/images/140917_00.PNG new file mode 100644 index 00000000..9062ba0c Binary files /dev/null and b/images/140917_00.PNG differ diff --git a/images/141014_00.png b/images/141014_00.png new file mode 100644 index 00000000..0b5119bc Binary files /dev/null and b/images/141014_00.png differ diff --git a/images/141014_01.png b/images/141014_01.png new file mode 100644 index 00000000..b94cddbb Binary files /dev/null and b/images/141014_01.png differ diff --git a/images/141015_00.png b/images/141015_00.png new file mode 100644 index 00000000..1895ae88 Binary files /dev/null and b/images/141015_00.png differ diff --git a/images/apple-touch-icon-next.png b/images/apple-touch-icon-next.png new file mode 100644 index 00000000..86a0d1d3 Binary files /dev/null and b/images/apple-touch-icon-next.png differ diff --git a/images/avatar.gif b/images/avatar.gif new file mode 100644 index 00000000..3b5d744b Binary files /dev/null and b/images/avatar.gif differ diff --git a/images/favicon-16x16-next.png b/images/favicon-16x16-next.png new file mode 100644 index 00000000..de8c5d3a Binary files /dev/null and b/images/favicon-16x16-next.png differ diff --git a/images/favicon-32x32-next.png b/images/favicon-32x32-next.png new file mode 100644 index 00000000..e02f5f4d Binary files /dev/null and b/images/favicon-32x32-next.png differ diff --git a/images/logo-algolia-nebula-blue-full.svg b/images/logo-algolia-nebula-blue-full.svg new file mode 100644 index 00000000..886c422e --- /dev/null +++ b/images/logo-algolia-nebula-blue-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 00000000..992c1a58 --- /dev/null +++ b/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/octopress.jpeg b/images/octopress.jpeg new file mode 100644 index 00000000..15663990 Binary files /dev/null and b/images/octopress.jpeg differ diff --git a/images/yoda1.jpg b/images/yoda1.jpg new file mode 100644 index 00000000..15a0e683 Binary files /dev/null and b/images/yoda1.jpg differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..3f5537f1 --- /dev/null +++ b/index.html @@ -0,0 +1,1066 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+ + +

이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.

+

이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread) 키워드를 이용해서 tls 변수를 선언할 수 있다.

+

C#에도 ThreadLocal<T> 라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.

+

이를 해결하는 방법과 주의해야 할 사항을 정리해본다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+ + +

2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.

+

윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.

+

하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__ 등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.

+

성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.

+ + +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

pch 파일 사이즈

팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

이전 포스트 ‘C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上‘ 에서 이어진다.

+

지워도 되는 인클루드를 찾아냈다

개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.

+

이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.

+
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
27
28
작업 대상으로 1개의 프로젝트가 입력 되었습니다.
-------------------------------------------------
Service : 프로젝트 정리.
Service : PCH 생성.
컴파일 : stdafx.cpp ... 성공. 걸린 시간 : 1.04초
Client.cpp의 인클루드를 검사합니다.
- process #1 Client.cpp (1/2) ... X
- process #1 Client.cpp (2/2) ... X
ClientAcceptor.cpp의 인클루드를 검사합니다.
- process #1 ClientAcceptor.cpp (1/2) ... 컴파일 가능!
- process #1 ClientAcceptor.cpp (2/2) ... X
ClientConnection.cpp의 인클루드를 검사합니다.
- process #1 ClientConnection.cpp (1/3) ... X
- process #1 ClientConnection.cpp (2/3) ... X
- process #1 ClientConnection.cpp (3/3) ... X
Start.cpp의 인클루드를 검사합니다.
- process #1 Start.cpp (1/4) ... X
- process #1 Start.cpp (2/4) ... X
- process #1 Start.cpp (3/4) ... X
- process #1 Start.cpp (4/4) ... X
ThreadEntry.cpp의 인클루드를 검사합니다.
- process #1 ThreadEntry.cpp (1/1) ... X
-------------------------------------------------
Project : Service 모두 1개의 인클루드가 불필요한 것으로 의심됩니다.
D:\Dev\uni\World\Service\ClientAcceptor.cpp
- 2 line : #include "World/Service/Client.h"

총 소요 시간 : 13.289 sec
+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

지워도 되는 헤더 인클루드를 색출하고 싶다

매우 느리게 찔끔찔끔 진행하는 토이 프로젝트가 있는데, 오늘 처음으로 무언가 그럴싸한 아웃풋이 나오게 되어 스냅샷을 하는 느낌으로 간단히 포스팅.

+

cpp 프로젝트 규모가 점점 커지게 되면 빌드 시간 때문에 많은 고통을 겪는다. 이때문에 increadi build 같은 분산 빌드 솔루션도 쓰는거고 unity build 같은 꼼수도 사용하게 되는거다.

+

하지만 저런 솔루션들을 사용하기 이전에, 코드를 정리하는 것이 먼저 선행될 필요가 있다. cpp는 특성상 작업하다보면 소스파일에 불필요한 헤더파일의 #include가 남게되고, 이것들이 불필요한 dependency를 만들어내면서 늘어지는 빌드 시간을 무시할 수 없기 때문이다.

+

그런데 문제는 그렇게 생긴 불필요 인클루드 구문이 무엇인지를 골라내기가 힘들다는 점이다. 프로젝트 규모가 커질수록 더욱 힘들다. c#같은 경우 불필요 using 구문을 아예 visual studio IDE가 자체적으로 정리해주기까지 하지만, cpp는 색출조차 힘들다 보니 이런 기능을 제공하는 3rd party tool도 없어 보인다. Whole Tomato의 Spaghetti 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다.

+

그래서 한 번 직접 만들어보기로 했다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+ + +

지난번에 google c++ style guide 에 대해서 한참 수다를 떨었는데,
요즘에도 비슷한 주제의 책을 읽고 있습니다. 임백준씨가 번역하신 ‘읽기 좋은 코드가 좋은 코드다’ 인데요,
이것도 가볍게 읽을 수 있는 내용이어서 빌드 시간 중간에 띄엄띄엄 읽고 있어요.

+

이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 Yoda Conditions 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다.

+
May the force be with you.
1
2
3
4
int val = 20;
if(20 == val) { // <- yoda notation here.
...
}
+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/bookmark.js b/js/bookmark.js new file mode 100644 index 00000000..8e3ae6ad --- /dev/null +++ b/js/bookmark.js @@ -0,0 +1,56 @@ +/* global CONFIG */ + +document.addEventListener('DOMContentLoaded', () => { + 'use strict'; + + const doSaveScroll = () => { + localStorage.setItem('bookmark' + location.pathname, window.scrollY); + }; + + const scrollToMark = () => { + let top = localStorage.getItem('bookmark' + location.pathname); + top = parseInt(top, 10); + // If the page opens with a specific hash, just jump out + if (!isNaN(top) && location.hash === '') { + // Auto scroll to the position + window.anime({ + targets : document.scrollingElement, + duration : 200, + easing : 'linear', + scrollTop: top + }); + } + }; + // Register everything + const init = function(trigger) { + // Create a link element + const link = document.querySelector('.book-mark-link'); + // Scroll event + window.addEventListener('scroll', () => link.classList.toggle('book-mark-link-fixed', window.scrollY === 0), { passive: true }); + // Register beforeunload event when the trigger is auto + if (trigger === 'auto') { + // Register beforeunload event + window.addEventListener('beforeunload', doSaveScroll); + document.addEventListener('pjax:send', doSaveScroll); + } + // Save the position by clicking the icon + link.addEventListener('click', () => { + doSaveScroll(); + window.anime({ + targets : link, + duration: 200, + easing : 'linear', + top : -30, + complete: () => { + setTimeout(() => { + link.style.top = ''; + }, 400); + } + }); + }); + scrollToMark(); + document.addEventListener('pjax:success', scrollToMark); + }; + + init(CONFIG.bookmark.save); +}); diff --git a/js/comments-buttons.js b/js/comments-buttons.js new file mode 100644 index 00000000..505c21b7 --- /dev/null +++ b/js/comments-buttons.js @@ -0,0 +1,25 @@ +/* global CONFIG */ + +(function() { + const commentButton = document.querySelectorAll('.comment-button'); + commentButton.forEach(element => { + const commentClass = element.classList[2]; + element.addEventListener('click', () => { + commentButton.forEach(active => active.classList.toggle('active', active === element)); + document.querySelectorAll('.comment-position').forEach(active => active.classList.toggle('active', active.classList.contains(commentClass))); + if (CONFIG.comments.storage) { + localStorage.setItem('comments_active', commentClass); + } + }); + }); + let { activeClass } = CONFIG.comments; + if (CONFIG.comments.storage) { + activeClass = localStorage.getItem('comments_active') || activeClass; + } + if (activeClass) { + const activeButton = document.querySelector(`.comment-button.${activeClass}`); + if (activeButton) { + activeButton.click(); + } + } +})(); diff --git a/js/comments.js b/js/comments.js new file mode 100644 index 00000000..4045e8c0 --- /dev/null +++ b/js/comments.js @@ -0,0 +1,21 @@ +/* global CONFIG */ + +window.addEventListener('tabs:register', () => { + let { activeClass } = CONFIG.comments; + if (CONFIG.comments.storage) { + activeClass = localStorage.getItem('comments_active') || activeClass; + } + if (activeClass) { + const activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`); + if (activeTab) { + activeTab.click(); + } + } +}); +if (CONFIG.comments.storage) { + window.addEventListener('tabs:click', event => { + if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return; + const commentClass = event.target.classList[1]; + localStorage.setItem('comments_active', commentClass); + }); +} diff --git a/js/config.js b/js/config.js new file mode 100644 index 00000000..caa0075b --- /dev/null +++ b/js/config.js @@ -0,0 +1,66 @@ +if (!window.NexT) window.NexT = {}; + +(function() { + const className = 'next-config'; + + const staticConfig = {}; + let variableConfig = {}; + + const parse = text => JSON.parse(text || '{}'); + + const update = name => { + const targetEle = document.querySelector(`.${className}[data-name="${name}"]`); + if (!targetEle) return; + const parsedConfig = parse(targetEle.text); + if (name === 'main') { + Object.assign(staticConfig, parsedConfig); + } else { + variableConfig[name] = parsedConfig; + } + }; + + update('main'); + + window.CONFIG = new Proxy({}, { + get(overrideConfig, name) { + let existing; + if (name in staticConfig) { + existing = staticConfig[name]; + } else { + if (!(name in variableConfig)) update(name); + existing = variableConfig[name]; + } + + // For unset override and mixable existing + if (!(name in overrideConfig) && typeof existing === 'object') { + // Get ready to mix. + overrideConfig[name] = {}; + } + + if (name in overrideConfig) { + const override = overrideConfig[name]; + + // When mixable + if (typeof override === 'object' && typeof existing === 'object') { + // Mix, proxy changes to the override. + return new Proxy({ ...existing, ...override }, { + set(target, prop, value) { + target[prop] = value; + override[prop] = value; + return true; + } + }); + } + + return override; + } + + // Only when not mixable and override hasn't been set. + return existing; + } + }); + + document.addEventListener('pjax:success', () => { + variableConfig = {}; + }); +})(); diff --git a/js/motion.js b/js/motion.js new file mode 100644 index 00000000..aad22db1 --- /dev/null +++ b/js/motion.js @@ -0,0 +1,140 @@ +/* global NexT, CONFIG */ + +NexT.motion = {}; + +NexT.motion.integrator = { + queue: [], + init : function() { + this.queue = []; + return this; + }, + add: function(fn) { + const sequence = fn(); + if (CONFIG.motion.async) this.queue.push(sequence); + else this.queue = this.queue.concat(sequence); + return this; + }, + bootstrap: function() { + if (!CONFIG.motion.async) this.queue = [this.queue]; + this.queue.forEach(sequence => { + const timeline = window.anime.timeline({ + duration: 200, + easing : 'linear' + }); + sequence.forEach(item => { + if (item.deltaT) timeline.add(item, item.deltaT); + else timeline.add(item); + }); + }); + } +}; + +NexT.motion.middleWares = { + header: function() { + const sequence = []; + + function getMistLineSettings(targets) { + sequence.push({ + targets, + scaleX : [0, 1], + duration: 500, + deltaT : '-=200' + }); + } + + function pushToSequence(targets, sequenceQueue = false) { + sequence.push({ + targets, + opacity: 1, + top : 0, + deltaT : sequenceQueue ? '-=200' : '-=0' + }); + } + + pushToSequence('.column'); + CONFIG.scheme === 'Mist' && getMistLineSettings('.logo-line'); + CONFIG.scheme === 'Muse' && pushToSequence('.custom-logo-image'); + pushToSequence('.site-title'); + pushToSequence('.site-brand-container .toggle', true); + pushToSequence('.site-subtitle'); + (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') && pushToSequence('.custom-logo-image'); + + const menuItemTransition = CONFIG.motion.transition.menu_item; + if (menuItemTransition) { + document.querySelectorAll('.menu-item').forEach(targets => { + sequence.push({ + targets, + complete: () => targets.classList.add('animated', menuItemTransition), + deltaT : '-=200' + }); + }); + } + + return sequence; + }, + + subMenu: function() { + const subMenuItem = document.querySelectorAll('.sub-menu .menu-item'); + if (subMenuItem.length > 0) { + subMenuItem.forEach(element => { + element.classList.add('animated'); + }); + } + return []; + }, + + postList: function() { + const sequence = []; + const { post_block, post_header, post_body, coll_header } = CONFIG.motion.transition; + + function animate(animation, elements) { + if (!animation) return; + elements.forEach(targets => { + sequence.push({ + targets, + complete: () => targets.classList.add('animated', animation), + deltaT : '-=100' + }); + }); + } + + document.querySelectorAll('.post-block').forEach(targets => { + sequence.push({ + targets, + complete: () => targets.classList.add('animated', post_block), + deltaT : '-=100' + }); + animate(coll_header, targets.querySelectorAll('.collection-header')); + animate(post_header, targets.querySelectorAll('.post-header')); + animate(post_body, targets.querySelectorAll('.post-body')); + }); + + animate(post_block, document.querySelectorAll('.pagination, .comments')); + + return sequence; + }, + + sidebar: function() { + const sequence = []; + const sidebar = document.querySelectorAll('.sidebar-inner'); + const sidebarTransition = CONFIG.motion.transition.sidebar; + // Only for Pisces | Gemini. + if (sidebarTransition && (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini')) { + sidebar.forEach(targets => { + sequence.push({ + targets, + complete: () => targets.classList.add('animated', sidebarTransition), + deltaT : '-=100' + }); + }); + } + return sequence; + }, + + footer: function() { + return [{ + targets: document.querySelector('.footer'), + opacity: 1 + }]; + } +}; diff --git a/js/next-boot.js b/js/next-boot.js new file mode 100644 index 00000000..1225fd2d --- /dev/null +++ b/js/next-boot.js @@ -0,0 +1,75 @@ +/* global NexT, CONFIG */ + +NexT.boot = {}; + +NexT.boot.registerEvents = function() { + + NexT.utils.registerScrollPercent(); + NexT.utils.registerCanIUseTag(); + + // Mobile top menu bar. + document.querySelector('.site-nav-toggle .toggle').addEventListener('click', event => { + event.currentTarget.classList.toggle('toggle-close'); + const siteNav = document.querySelector('.site-nav'); + if (!siteNav) return; + siteNav.style.setProperty('--scroll-height', siteNav.scrollHeight + 'px'); + document.body.classList.toggle('site-nav-on'); + }); + + document.querySelectorAll('.sidebar-nav li').forEach((element, index) => { + element.addEventListener('click', () => { + NexT.utils.activateSidebarPanel(index); + }); + }); + + window.addEventListener('hashchange', () => { + const tHash = location.hash; + if (tHash !== '' && !tHash.match(/%\S{2}/)) { + const target = document.querySelector(`.tabs ul.nav-tabs li a[href="${tHash}"]`); + target && target.click(); + } + }); +}; + +NexT.boot.refresh = function() { + + /** + * Register JS handlers by condition option. + * Need to add config option in Front-End at 'scripts/helpers/next-config.js' file. + */ + CONFIG.prism && window.Prism.highlightAll(); + CONFIG.mediumzoom && window.mediumZoom('.post-body :not(a) > img, .post-body > img', { + background: 'var(--content-bg-color)' + }); + CONFIG.lazyload && window.lozad('.post-body img').observe(); + CONFIG.pangu && window.pangu.spacingPage(); + + CONFIG.exturl && NexT.utils.registerExtURL(); + NexT.utils.wrapTableWithBox(); + NexT.utils.registerCopyCode(); + NexT.utils.registerTabsTag(); + NexT.utils.registerActiveMenuItem(); + NexT.utils.registerLangSelect(); + NexT.utils.registerSidebarTOC(); + NexT.utils.registerPostReward(); + NexT.utils.registerVideoIframe(); +}; + +NexT.boot.motion = function() { + // Define Motion Sequence & Bootstrap Motion. + if (CONFIG.motion.enable) { + NexT.motion.integrator + .add(NexT.motion.middleWares.header) + .add(NexT.motion.middleWares.postList) + .add(NexT.motion.middleWares.sidebar) + .add(NexT.motion.middleWares.footer) + .bootstrap(); + } + NexT.utils.updateSidebarPosition(); +}; + +document.addEventListener('DOMContentLoaded', () => { + NexT.boot.registerEvents(); + NexT.boot.refresh(); + NexT.boot.motion(); +}); diff --git a/js/pjax.js b/js/pjax.js new file mode 100644 index 00000000..ec24341d --- /dev/null +++ b/js/pjax.js @@ -0,0 +1,36 @@ +/* global NexT, CONFIG, Pjax */ + +const pjax = new Pjax({ + selectors: [ + 'head title', + 'script[type="application/json"]', + '.main-inner', + '.post-toc-wrap', + '.languages', + '.pjax' + ], + analytics: false, + cacheBust: false, + scrollTo : !CONFIG.bookmark.enable +}); + +document.addEventListener('pjax:success', () => { + pjax.executeScripts(document.querySelectorAll('script[data-pjax]')); + NexT.boot.refresh(); + // Define Motion Sequence & Bootstrap Motion. + if (CONFIG.motion.enable) { + NexT.motion.integrator + .init() + .add(NexT.motion.middleWares.subMenu) + .add(NexT.motion.middleWares.postList) + // Add sidebar-post-related transition. + .add(NexT.motion.middleWares.sidebar) + .bootstrap(); + } + if (CONFIG.sidebar.display !== 'remove') { + const hasTOC = document.querySelector('.post-toc'); + document.querySelector('.sidebar-inner').classList.toggle('sidebar-nav-active', hasTOC); + NexT.utils.activateSidebarPanel(hasTOC ? 0 : 1); + NexT.utils.updateSidebarPosition(); + } +}); diff --git a/js/schedule.js b/js/schedule.js new file mode 100644 index 00000000..8f0c26cc --- /dev/null +++ b/js/schedule.js @@ -0,0 +1,138 @@ +/* global CONFIG */ + +// https://developers.google.com/calendar/api/v3/reference/events/list +(function() { + // Initialization + const calendar = { + orderBy : 'startTime', + showLocation: false, + offsetMax : 72, + offsetMin : 4, + showDeleted : false, + singleEvents: true, + maxResults : 250 + }; + + // Read config form theme config file + Object.assign(calendar, CONFIG.calendar); + + const now = new Date(); + const timeMax = new Date(); + const timeMin = new Date(); + + timeMax.setHours(now.getHours() + calendar.offsetMax); + timeMin.setHours(now.getHours() - calendar.offsetMin); + + // Build URL + const params = { + key : calendar.api_key, + orderBy : calendar.orderBy, + timeMax : timeMax.toISOString(), + timeMin : timeMin.toISOString(), + showDeleted : calendar.showDeleted, + singleEvents: calendar.singleEvents, + maxResults : calendar.maxResults + }; + + const request_url = new URL(`https://www.googleapis.com/calendar/v3/calendars/${calendar.calendar_id}/events`); + Object.entries(params).forEach(param => request_url.searchParams.append(...param)); + + function getRelativeTime(current, previous) { + const msPerMinute = 60 * 1000; + const msPerHour = msPerMinute * 60; + const msPerDay = msPerHour * 24; + const msPerMonth = msPerDay * 30; + const msPerYear = msPerDay * 365; + + let elapsed = current - previous; + const tense = elapsed > 0 ? ' ago' : ' later'; + + elapsed = Math.abs(elapsed); + + if (elapsed < msPerHour) { + return Math.round(elapsed / msPerMinute) + ' minutes' + tense; + } else if (elapsed < msPerDay) { + return Math.round(elapsed / msPerHour) + ' hours' + tense; + } else if (elapsed < msPerMonth) { + return 'about ' + Math.round(elapsed / msPerDay) + ' days' + tense; + } else if (elapsed < msPerYear) { + return 'about ' + Math.round(elapsed / msPerMonth) + ' months' + tense; + } + + return 'about ' + Math.round(elapsed / msPerYear) + ' years' + tense; + } + + function buildEventDOM(tense, event, start, end) { + const durationFormat = { + weekday: 'short', + hour : '2-digit', + minute : '2-digit' + }; + const relativeTime = tense === 'now' ? 'NOW' : getRelativeTime(now, start); + const duration = start.toLocaleTimeString([], durationFormat) + ' - ' + end.toLocaleTimeString([], durationFormat); + + let location = ''; + if (calendar.showLocation && event.location) { + location = `${event.location}`; + } + let description = ''; + if (event.description) { + description = `${event.description}`; + } + + const eventContent = `
+

+ ${event.summary} + ${relativeTime} +

+ ${location} + ${duration} + ${description} +
`; + return eventContent; + } + + function fetchData() { + const eventList = document.querySelector('.event-list'); + if (!eventList) return; + + fetch(request_url.href).then(response => { + return response.json(); + }).then(data => { + if (data.items.length === 0) { + eventList.innerHTML = '
'; + return; + } + // Clean the event list + eventList.innerHTML = ''; + let prevEnd = 0; // used to decide where to insert an
+ const utc = new Date().getTimezoneOffset() * 60000; + + data.items.forEach(event => { + // Parse data + const start = new Date(event.start.dateTime || (new Date(event.start.date).getTime() + utc)); + const end = new Date(event.end.dateTime || (new Date(event.end.date).getTime() + utc)); + + let tense = 'now'; + if (end < now) { + tense = 'past'; + } else if (start > now) { + tense = 'future'; + } + + if (tense === 'future' && prevEnd < now) { + eventList.insertAdjacentHTML('beforeend', '
'); + } + + eventList.insertAdjacentHTML('beforeend', buildEventDOM(tense, event, start, end)); + prevEnd = end; + }); + }); + } + + fetchData(); + const fetchDataTimer = setInterval(fetchData, 60000); + document.addEventListener('pjax:send', () => { + clearInterval(fetchDataTimer); + }); +})(); diff --git a/js/schemes/muse.js b/js/schemes/muse.js new file mode 100644 index 00000000..e5fc8bdf --- /dev/null +++ b/js/schemes/muse.js @@ -0,0 +1,60 @@ +/* global CONFIG */ + +document.addEventListener('DOMContentLoaded', () => { + + const isRight = CONFIG.sidebar.position === 'right'; + + const sidebarToggleMotion = { + mouse: {}, + init : function() { + window.addEventListener('mousedown', this.mousedownHandler.bind(this)); + window.addEventListener('mouseup', this.mouseupHandler.bind(this)); + document.querySelector('.sidebar-dimmer').addEventListener('click', this.clickHandler.bind(this)); + document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this)); + window.addEventListener('sidebar:show', this.showSidebar); + window.addEventListener('sidebar:hide', this.hideSidebar); + }, + mousedownHandler: function(event) { + this.mouse.X = event.pageX; + this.mouse.Y = event.pageY; + }, + mouseupHandler: function(event) { + const deltaX = event.pageX - this.mouse.X; + const deltaY = event.pageY - this.mouse.Y; + const clickingBlankPart = Math.hypot(deltaX, deltaY) < 20 && event.target.matches('.main'); + // Fancybox has z-index property, but medium-zoom does not, so the sidebar will overlay the zoomed image. + if (clickingBlankPart || event.target.matches('img.medium-zoom-image')) { + this.hideSidebar(); + } + }, + clickHandler: function() { + document.body.classList.contains('sidebar-active') ? this.hideSidebar() : this.showSidebar(); + }, + showSidebar: function() { + document.body.classList.add('sidebar-active'); + const animateAction = isRight ? 'fadeInRight' : 'fadeInLeft'; + document.querySelectorAll('.sidebar .animated').forEach((element, index) => { + element.style.animationDelay = (100 * index) + 'ms'; + element.classList.remove(animateAction); + setTimeout(() => { + // Trigger a DOM reflow + element.classList.add(animateAction); + }); + }); + }, + hideSidebar: function() { + document.body.classList.remove('sidebar-active'); + } + }; + if (CONFIG.sidebar.display !== 'remove') sidebarToggleMotion.init(); + + function updateFooterPosition() { + const footer = document.querySelector('.footer'); + const containerHeight = document.querySelector('.column').offsetHeight + document.querySelector('.main').offsetHeight + footer.offsetHeight; + footer.classList.toggle('footer-fixed', containerHeight <= window.innerHeight); + } + + updateFooterPosition(); + window.addEventListener('resize', updateFooterPosition); + window.addEventListener('scroll', updateFooterPosition, { passive: true }); +}); diff --git a/js/third-party/analytics/baidu-analytics.js b/js/third-party/analytics/baidu-analytics.js new file mode 100644 index 00000000..c10e7d01 --- /dev/null +++ b/js/third-party/analytics/baidu-analytics.js @@ -0,0 +1,7 @@ +/* global _hmt */ + +if (!window._hmt) window._hmt = []; + +document.addEventListener('pjax:success', () => { + _hmt.push(['_trackPageview', location.pathname]); +}); diff --git a/js/third-party/analytics/google-analytics.js b/js/third-party/analytics/google-analytics.js new file mode 100644 index 00000000..2cd128f7 --- /dev/null +++ b/js/third-party/analytics/google-analytics.js @@ -0,0 +1,35 @@ +/* global CONFIG, dataLayer, gtag */ + +if (!CONFIG.google_analytics.only_pageview) { + if (CONFIG.hostname === location.hostname) { + window.dataLayer = window.dataLayer || []; + window.gtag = function() { + dataLayer.push(arguments); + }; + gtag('js', new Date()); + gtag('config', CONFIG.google_analytics.tracking_id); + + document.addEventListener('pjax:success', () => { + gtag('event', 'page_view', { + page_location: location.href, + page_path : location.pathname, + page_title : document.title + }); + }); + } +} else { + const sendPageView = () => { + if (CONFIG.hostname !== location.hostname) return; + const uid = localStorage.getItem('uid') || (Math.random() + '.' + Math.random()); + localStorage.setItem('uid', uid); + navigator.sendBeacon('https://www.google-analytics.com/collect', new URLSearchParams({ + v : 1, + tid: CONFIG.google_analytics.tracking_id, + cid: uid, + t : 'pageview', + dp : encodeURIComponent(location.pathname) + })); + }; + document.addEventListener('pjax:complete', sendPageView); + sendPageView(); +} diff --git a/js/third-party/analytics/growingio.js b/js/third-party/analytics/growingio.js new file mode 100644 index 00000000..0460833b --- /dev/null +++ b/js/third-party/analytics/growingio.js @@ -0,0 +1,10 @@ +/* global CONFIG, gio */ + +if (!window.gio) { + window.gio = function() { + (window.gio.q = window.gio.q || []).push(arguments); + }; +} + +gio('init', `${CONFIG.growingio_analytics}`, {}); +gio('send'); diff --git a/js/third-party/analytics/matomo.js b/js/third-party/analytics/matomo.js new file mode 100644 index 00000000..290a3e09 --- /dev/null +++ b/js/third-party/analytics/matomo.js @@ -0,0 +1,19 @@ +/* global CONFIG */ + +if (CONFIG.matomo.enable) { + window._paq = window._paq || []; + const _paq = window._paq; + + /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + const u = CONFIG.matomo.server_url; + _paq.push(['setTrackerUrl', u + 'matomo.php']); + _paq.push(['setSiteId', CONFIG.matomo.site_id]); + const d = document; + const g = d.createElement('script'); + const s = d.getElementsByTagName('script')[0]; + g.async = true; + g.src = u + 'matomo.js'; + s.parentNode.insertBefore(g, s); +} diff --git a/js/third-party/chat/chatra.js b/js/third-party/chat/chatra.js new file mode 100644 index 00000000..e495b8e1 --- /dev/null +++ b/js/third-party/chat/chatra.js @@ -0,0 +1,19 @@ +/* global CONFIG, Chatra */ + +(function() { + if (CONFIG.chatra.embed) { + window.ChatraSetup = { + mode : 'frame', + injectTo: CONFIG.chatra.embed + }; + } + + window.ChatraID = CONFIG.chatra.id; + + const chatButton = document.querySelector('.sidebar-button button'); + if (chatButton) { + chatButton.addEventListener('click', () => { + Chatra('openChat', true); + }); + } +})(); diff --git a/js/third-party/chat/gitter.js b/js/third-party/chat/gitter.js new file mode 100644 index 00000000..2b26d050 --- /dev/null +++ b/js/third-party/chat/gitter.js @@ -0,0 +1,5 @@ +/* global CONFIG */ + +((window.gitter = {}).chat = {}).options = { + room: CONFIG.gitter.room +}; diff --git a/js/third-party/chat/tidio.js b/js/third-party/chat/tidio.js new file mode 100644 index 00000000..bffb918e --- /dev/null +++ b/js/third-party/chat/tidio.js @@ -0,0 +1,10 @@ +/* global tidioChatApi */ + +(function() { + const chatButton = document.querySelector('.sidebar-button button'); + if (chatButton) { + chatButton.addEventListener('click', () => { + tidioChatApi.open(); + }); + } +})(); diff --git a/js/third-party/comments/changyan.js b/js/third-party/comments/changyan.js new file mode 100644 index 00000000..18a1be4f --- /dev/null +++ b/js/third-party/comments/changyan.js @@ -0,0 +1,39 @@ +/* global NexT, CONFIG */ + +document.addEventListener('page:loaded', () => { + const { appid, appkey } = CONFIG.changyan; + const mainJs = 'https://cy-cdn.kuaizhan.com/upload/changyan.js'; + const countJs = `https://cy-cdn.kuaizhan.com/upload/plugins/plugins.list.count.js?clientId=${appid}`; + + // Get the number of comments + setTimeout(() => { + return NexT.utils.getScript(countJs, { + attributes: { + async: true, + id : 'cy_cmt_num' + } + }); + }, 0); + + // When scroll to comment section + if (CONFIG.page.comments && !CONFIG.page.isHome) { + NexT.utils.loadComments('#SOHUCS') + .then(() => { + return NexT.utils.getScript(mainJs, { + attributes: { + async: true + } + }); + }) + .then(() => { + window.changyan.api.config({ + appid, + conf: appkey + }); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error('Failed to load Changyan', error); + }); + } +}); diff --git a/js/third-party/comments/disqus.js b/js/third-party/comments/disqus.js new file mode 100644 index 00000000..381c26fc --- /dev/null +++ b/js/third-party/comments/disqus.js @@ -0,0 +1,41 @@ +/* global NexT, CONFIG, DISQUS */ + +document.addEventListener('page:loaded', () => { + + if (CONFIG.disqus.count) { + const loadCount = () => { + NexT.utils.getScript(`https://${CONFIG.disqus.shortname}.disqus.com/count.js`, { + attributes: { id: 'dsq-count-scr' } + }); + }; + + // defer loading until the whole page loading is completed + window.addEventListener('load', loadCount, false); + } + + if (CONFIG.page.comments) { + // `disqus_config` should be a global variable + // See https://help.disqus.com/en/articles/1717084-javascript-configuration-variables + window.disqus_config = function() { + this.page.url = CONFIG.page.permalink; + this.page.identifier = CONFIG.page.path; + this.page.title = CONFIG.page.title; + if (CONFIG.disqus.i18n.disqus !== 'disqus') { + this.language = CONFIG.disqus.i18n.disqus; + } + }; + NexT.utils.loadComments('#disqus_thread').then(() => { + if (window.DISQUS) { + DISQUS.reset({ + reload: true, + config: window.disqus_config + }); + } else { + NexT.utils.getScript(`https://${CONFIG.disqus.shortname}.disqus.com/embed.js`, { + attributes: { dataset: { timestamp: '' + +new Date() } } + }); + } + }); + } + +}); diff --git a/js/third-party/comments/disqusjs.js b/js/third-party/comments/disqusjs.js new file mode 100644 index 00000000..d8401eee --- /dev/null +++ b/js/third-party/comments/disqusjs.js @@ -0,0 +1,23 @@ +/* global NexT, CONFIG, DisqusJS */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.page.comments) return; + + NexT.utils.loadComments('#disqus_thread') + .then(() => NexT.utils.getScript(CONFIG.disqusjs.js, { condition: window.DisqusJS })) + .then(() => { + window.dsqjs = new DisqusJS({ + api : CONFIG.disqusjs.api || 'https://disqus.com/api/', + apikey : CONFIG.disqusjs.apikey, + shortname : CONFIG.disqusjs.shortname, + url : CONFIG.page.permalink, + identifier: CONFIG.page.path, + title : CONFIG.page.title + }); + window.dsqjs.render(document.querySelector('.disqusjs-container')); + }); +}); + +document.addEventListener('pjax:send', () => { + if (window.dsqjs) window.dsqjs.destroy(); +}); diff --git a/js/third-party/comments/gitalk.js b/js/third-party/comments/gitalk.js new file mode 100644 index 00000000..08d07f4c --- /dev/null +++ b/js/third-party/comments/gitalk.js @@ -0,0 +1,24 @@ +/* global NexT, CONFIG, Gitalk */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.page.comments) return; + + NexT.utils.loadComments('.gitalk-container') + .then(() => NexT.utils.getScript(CONFIG.gitalk.js, { + condition: window.Gitalk + })) + .then(() => { + const gitalk = new Gitalk({ + clientID : CONFIG.gitalk.client_id, + clientSecret : CONFIG.gitalk.client_secret, + repo : CONFIG.gitalk.repo, + owner : CONFIG.gitalk.github_id, + admin : [CONFIG.gitalk.admin_user], + id : CONFIG.gitalk.path_md5, + proxy : CONFIG.gitalk.proxy, + language : CONFIG.gitalk.language || window.navigator.language, + distractionFreeMode: CONFIG.gitalk.distraction_free_mode + }); + gitalk.render(document.querySelector('.gitalk-container')); + }); +}); diff --git a/js/third-party/comments/isso.js b/js/third-party/comments/isso.js new file mode 100644 index 00000000..2c706013 --- /dev/null +++ b/js/third-party/comments/isso.js @@ -0,0 +1,15 @@ +/* global NexT, CONFIG */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.page.comments) return; + + NexT.utils.loadComments('#isso-thread') + .then(() => NexT.utils.getScript(`${CONFIG.isso}js/embed.min.js`, { + attributes: { + dataset: { + isso: `${CONFIG.isso}` + } + }, + parentNode: document.querySelector('#isso-thread') + })); +}); diff --git a/js/third-party/comments/livere.js b/js/third-party/comments/livere.js new file mode 100644 index 00000000..c4bcd2e1 --- /dev/null +++ b/js/third-party/comments/livere.js @@ -0,0 +1,19 @@ +/* global NexT, CONFIG, LivereTower */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.page.comments) return; + + NexT.utils.loadComments('#lv-container').then(() => { + window.livereOptions = { + refer: CONFIG.page.path.replace(/index\.html$/, '') + }; + + if (typeof LivereTower === 'function') return; + + NexT.utils.getScript('https://cdn-city.livere.com/js/embed.dist.js', { + attributes: { + async: true + } + }); + }); +}); diff --git a/js/third-party/comments/utterances.js b/js/third-party/comments/utterances.js new file mode 100644 index 00000000..332ee057 --- /dev/null +++ b/js/third-party/comments/utterances.js @@ -0,0 +1,17 @@ +/* global NexT, CONFIG */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.page.comments) return; + + NexT.utils.loadComments('.utterances-container') + .then(() => NexT.utils.getScript('https://utteranc.es/client.js', { + attributes: { + async : true, + crossOrigin : 'anonymous', + 'repo' : CONFIG.utterances.repo, + 'issue-term': CONFIG.utterances.issue_term, + 'theme' : CONFIG.utterances.theme + }, + parentNode: document.querySelector('.utterances-container') + })); +}); diff --git a/js/third-party/fancybox.js b/js/third-party/fancybox.js new file mode 100644 index 00000000..bb436abb --- /dev/null +++ b/js/third-party/fancybox.js @@ -0,0 +1,38 @@ +document.addEventListener('page:loaded', () => { + + /** + * Wrap images with fancybox. + */ + document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => { + const $image = $(element); + const imageLink = $image.attr('data-src') || $image.attr('src'); + const $imageWrapLink = $image.wrap(``).parent('a'); + if ($image.is('.post-gallery img')) { + $imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery'); + } else if ($image.is('.group-picture img')) { + $imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group'); + } else { + $imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default'); + } + + const imageTitle = $image.attr('title') || $image.attr('alt'); + if (imageTitle) { + // Do not append image-caption if pandoc has already created a figcaption + if (!$imageWrapLink.next('figcaption').length) { + $imageWrapLink.append(`

${imageTitle}

`); + } + // Make sure img title tag will show correctly in fancybox + $imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle); + } + }); + + $.fancybox.defaults.hash = false; + $('.fancybox').fancybox({ + loop : true, + helpers: { + overlay: { + locked: false + } + } + }); +}); diff --git a/js/third-party/math/katex.js b/js/third-party/math/katex.js new file mode 100644 index 00000000..ad745b18 --- /dev/null +++ b/js/third-party/math/katex.js @@ -0,0 +1,7 @@ +/* global NexT, CONFIG */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.enableMath) return; + + NexT.utils.getScript(CONFIG.katex.copy_tex_js).catch(() => {}); +}); diff --git a/js/third-party/math/mathjax.js b/js/third-party/math/mathjax.js new file mode 100644 index 00000000..fe4d4488 --- /dev/null +++ b/js/third-party/math/mathjax.js @@ -0,0 +1,36 @@ +/* global NexT, CONFIG, MathJax */ + +document.addEventListener('page:loaded', () => { + if (!CONFIG.enableMath) return; + + if (typeof MathJax === 'undefined') { + window.MathJax = { + tex: { + inlineMath: { '[+]': [['$', '$']] }, + tags : CONFIG.mathjax.tags + }, + options: { + renderActions: { + insertedScript: [200, () => { + document.querySelectorAll('mjx-container').forEach(node => { + const target = node.parentNode; + if (target.nodeName.toLowerCase() === 'li') { + target.parentNode.classList.add('has-jax'); + } + }); + }, '', false] + } + } + }; + NexT.utils.getScript(CONFIG.mathjax.js, { + attributes: { + defer: true + } + }); + } else { + MathJax.startup.document.state(0); + MathJax.typesetClear(); + MathJax.texReset(); + MathJax.typesetPromise(); + } +}); diff --git a/js/third-party/pace.js b/js/third-party/pace.js new file mode 100644 index 00000000..c22d59f0 --- /dev/null +++ b/js/third-party/pace.js @@ -0,0 +1,7 @@ +/* global Pace */ + +Pace.options.restartOnPushState = false; + +document.addEventListener('pjax:send', () => { + Pace.restart(); +}); diff --git a/js/third-party/quicklink.js b/js/third-party/quicklink.js new file mode 100644 index 00000000..2543ad1e --- /dev/null +++ b/js/third-party/quicklink.js @@ -0,0 +1,37 @@ +/* global CONFIG, quicklink */ + +(function() { + if (typeof CONFIG.quicklink.ignores === 'string') { + const ignoresStr = `[${CONFIG.quicklink.ignores}]`; + CONFIG.quicklink.ignores = JSON.parse(ignoresStr); + } + + let resetFn = null; + + const onRefresh = () => { + if (resetFn) resetFn(); + if (!CONFIG.quicklink.enable) return; + + let ignoresArr = CONFIG.quicklink.ignores || []; + if (!Array.isArray(ignoresArr)) { + ignoresArr = [ignoresArr]; + } + + resetFn = quicklink.listen({ + timeout : CONFIG.quicklink.timeout, + priority: CONFIG.quicklink.priority, + ignores : [ + uri => uri.includes('#'), + uri => uri === CONFIG.quicklink.url, + ...ignoresArr + ] + }); + }; + + if (CONFIG.quicklink.delay) { + window.addEventListener('load', onRefresh); + document.addEventListener('pjax:success', onRefresh); + } else { + document.addEventListener('page:loaded', onRefresh); + } +})(); diff --git a/js/third-party/search/algolia-search.js b/js/third-party/search/algolia-search.js new file mode 100644 index 00000000..12a554c8 --- /dev/null +++ b/js/third-party/search/algolia-search.js @@ -0,0 +1,130 @@ +/* global instantsearch, algoliasearch, CONFIG, pjax */ + +document.addEventListener('DOMContentLoaded', () => { + const { indexName, appID, apiKey, hits } = CONFIG.algolia; + + const search = instantsearch({ + indexName, + searchClient : algoliasearch(appID, apiKey), + searchFunction: helper => { + if (document.querySelector('.search-input').value) { + helper.search(); + } + } + }); + + if (typeof pjax === 'object') { + search.on('render', () => { + pjax.refresh(document.querySelector('.algolia-hits')); + }); + } + + // Registering Widgets + search.addWidgets([ + instantsearch.widgets.configure({ + hitsPerPage: hits.per_page || 10 + }), + + instantsearch.widgets.searchBox({ + container : '.search-input-container', + placeholder : CONFIG.i18n.placeholder, + // Hide default icons of algolia search + showReset : false, + showSubmit : false, + showLoadingIndicator: false, + cssClasses : { + input: 'search-input' + } + }), + + instantsearch.widgets.stats({ + container: '.algolia-stats', + templates: { + text: data => { + const stats = CONFIG.i18n.hits_time + .replace('${hits}', data.nbHits) + .replace('${time}', data.processingTimeMS); + return `${stats} + Algolia`; + } + }, + cssClasses: { + text: 'search-stats' + } + }), + + instantsearch.widgets.hits({ + container : '.algolia-hits', + escapeHTML: false, + templates : { + item: data => { + const { title, excerpt, excerptStrip, contentStripTruncate } = data._highlightResult; + let result = `${title.value}`; + const content = excerpt || excerptStrip || contentStripTruncate; + if (content && content.value) { + const div = document.createElement('div'); + div.innerHTML = content.value; + result += `

${div.textContent.substring(0, 100)}...

`; + } + return result; + }, + empty: data => { + return `
+ ${CONFIG.i18n.empty.replace('${query}', data.query)} +
`; + } + }, + cssClasses: { + list: 'search-result-list' + } + }), + + instantsearch.widgets.pagination({ + container: '.algolia-pagination', + scrollTo : false, + showFirst: false, + showLast : false, + templates: { + first : '', + last : '', + previous: '', + next : '' + }, + cssClasses: { + list : ['pagination', 'algolia-pagination'], + item : 'pagination-item', + link : 'page-number', + selectedItem: 'current', + disabledItem: 'disabled-item' + } + }) + ]); + + search.start(); + + // Handle and trigger popup window + document.querySelectorAll('.popup-trigger').forEach(element => { + element.addEventListener('click', () => { + document.body.classList.add('search-active'); + setTimeout(() => document.querySelector('.search-input').focus(), 500); + }); + }); + + // Monitor main search box + const onPopupClose = () => { + document.body.classList.remove('search-active'); + }; + + document.querySelector('.search-pop-overlay').addEventListener('click', event => { + if (event.target === document.querySelector('.search-pop-overlay')) { + onPopupClose(); + } + }); + document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); + document.addEventListener('pjax:success', onPopupClose); + window.addEventListener('keyup', event => { + if (event.key === 'Escape') { + onPopupClose(); + } + }); +}); diff --git a/js/third-party/search/local-search.js b/js/third-party/search/local-search.js new file mode 100644 index 00000000..92a264dc --- /dev/null +++ b/js/third-party/search/local-search.js @@ -0,0 +1,99 @@ +/* global CONFIG, pjax, LocalSearch */ + +document.addEventListener('DOMContentLoaded', () => { + if (!CONFIG.path) { + // Search DB path + console.warn('`hexo-generator-searchdb` plugin is not installed!'); + return; + } + const localSearch = new LocalSearch({ + path : CONFIG.path, + top_n_per_article: CONFIG.localsearch.top_n_per_article, + unescape : CONFIG.localsearch.unescape + }); + + const input = document.querySelector('.search-input'); + + const inputEventFunction = () => { + if (!localSearch.isfetched) return; + const searchText = input.value.trim().toLowerCase(); + const keywords = searchText.split(/[-\s]+/); + const container = document.querySelector('.search-result-container'); + let resultItems = []; + if (searchText.length > 0) { + // Perform local searching + resultItems = localSearch.getResultItems(keywords); + } + if (keywords.length === 1 && keywords[0] === '') { + container.classList.add('no-result'); + container.innerHTML = '
'; + } else if (resultItems.length === 0) { + container.classList.add('no-result'); + container.innerHTML = '
'; + } else { + resultItems.sort((left, right) => { + if (left.includedCount !== right.includedCount) { + return right.includedCount - left.includedCount; + } else if (left.hitCount !== right.hitCount) { + return right.hitCount - left.hitCount; + } + return right.id - left.id; + }); + const stats = CONFIG.i18n.hits.replace('${hits}', resultItems.length); + + container.classList.remove('no-result'); + container.innerHTML = `
${stats}
+
+ `; + if (typeof pjax === 'object') pjax.refresh(container); + } + }; + + localSearch.highlightSearchWords(document.querySelector('.post-body')); + if (CONFIG.localsearch.preload) { + localSearch.fetchData(); + } + + if (CONFIG.localsearch.trigger === 'auto') { + input.addEventListener('input', inputEventFunction); + } else { + document.querySelector('.search-icon').addEventListener('click', inputEventFunction); + input.addEventListener('keypress', event => { + if (event.key === 'Enter') { + inputEventFunction(); + } + }); + } + window.addEventListener('search:loaded', inputEventFunction); + + // Handle and trigger popup window + document.querySelectorAll('.popup-trigger').forEach(element => { + element.addEventListener('click', () => { + document.body.classList.add('search-active'); + // Wait for search-popup animation to complete + setTimeout(() => input.focus(), 500); + if (!localSearch.isfetched) localSearch.fetchData(); + }); + }); + + // Monitor main search box + const onPopupClose = () => { + document.body.classList.remove('search-active'); + }; + + document.querySelector('.search-pop-overlay').addEventListener('click', event => { + if (event.target === document.querySelector('.search-pop-overlay')) { + onPopupClose(); + } + }); + document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); + document.addEventListener('pjax:success', () => { + localSearch.highlightSearchWords(document.querySelector('.post-body')); + onPopupClose(); + }); + window.addEventListener('keyup', event => { + if (event.key === 'Escape') { + onPopupClose(); + } + }); +}); diff --git a/js/third-party/statistics/firestore.js b/js/third-party/statistics/firestore.js new file mode 100644 index 00000000..3ea7ba67 --- /dev/null +++ b/js/third-party/statistics/firestore.js @@ -0,0 +1,60 @@ +/* global CONFIG, firebase */ + +firebase.initializeApp({ + apiKey : CONFIG.firestore.apiKey, + projectId: CONFIG.firestore.projectId +}); + +(function() { + const getCount = (doc, increaseCount) => { + // IncreaseCount will be false when not in article page + return doc.get().then(d => { + // Has no data, initialize count + let count = d.exists ? d.data().count : 0; + // If first view this article + if (increaseCount) { + // Increase count + count++; + doc.set({ + count + }); + } + return count; + }); + }; + + const db = firebase.firestore(); + const articles = db.collection(CONFIG.firestore.collection); + + document.addEventListener('page:loaded', () => { + + if (CONFIG.page.isPost) { + // Fix issue #118 + // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent + const title = document.querySelector('.post-title').textContent.trim(); + const doc = articles.doc(title); + let increaseCount = CONFIG.hostname === location.hostname; + if (localStorage.getItem(title)) { + increaseCount = false; + } else { + // Mark as visited + localStorage.setItem(title, true); + } + getCount(doc, increaseCount).then(count => { + document.querySelector('.firestore-visitors-count').innerText = count; + }); + } else if (CONFIG.page.isHome) { + const promises = [...document.querySelectorAll('.post-title')].map(element => { + const title = element.textContent.trim(); + const doc = articles.doc(title); + return getCount(doc); + }); + Promise.all(promises).then(counts => { + const metas = document.querySelectorAll('.firestore-visitors-count'); + counts.forEach((val, idx) => { + metas[idx].innerText = val; + }); + }); + } + }); +})(); diff --git a/js/third-party/statistics/lean-analytics.js b/js/third-party/statistics/lean-analytics.js new file mode 100644 index 00000000..8397112b --- /dev/null +++ b/js/third-party/statistics/lean-analytics.js @@ -0,0 +1,107 @@ +/* global CONFIG */ +/* eslint-disable no-console */ + +(function() { + const leancloudSelector = url => { + url = encodeURI(url); + return document.getElementById(url).querySelector('.leancloud-visitors-count'); + }; + + const addCount = Counter => { + const visitors = document.querySelector('.leancloud_visitors'); + const url = decodeURI(visitors.id); + const title = visitors.dataset.flagTitle; + + Counter('get', `/classes/Counter?where=${encodeURIComponent(JSON.stringify({ url }))}`) + .then(response => response.json()) + .then(({ results }) => { + if (results.length > 0) { + const counter = results[0]; + leancloudSelector(url).innerText = counter.time + 1; + Counter('put', '/classes/Counter/' + counter.objectId, { + time: { + '__op' : 'Increment', + 'amount': 1 + } + }) + .catch(error => { + console.error('Failed to save visitor count', error); + }); + } else if (CONFIG.leancloud_visitors.security) { + leancloudSelector(url).innerText = 'Counter not initialized! More info at console err msg.'; + console.error('ATTENTION! LeanCloud counter has security bug, see how to solve it here: https://github.com/theme-next/hexo-leancloud-counter-security. \n However, you can still use LeanCloud without security, by setting `security` option to `false`.'); + } else { + Counter('post', '/classes/Counter', { title, url, time: 1 }) + .then(response => response.json()) + .then(() => { + leancloudSelector(url).innerText = 1; + }) + .catch(error => { + console.error('Failed to create', error); + }); + } + }) + .catch(error => { + console.error('LeanCloud Counter Error', error); + }); + }; + + const showTime = Counter => { + const visitors = document.querySelectorAll('.leancloud_visitors'); + const entries = [...visitors].map(element => { + return decodeURI(element.id); + }); + + Counter('get', `/classes/Counter?where=${encodeURIComponent(JSON.stringify({ url: { '$in': entries } }))}`) + .then(response => response.json()) + .then(({ results }) => { + for (const url of entries) { + const target = results.find(item => item.url === url); + leancloudSelector(url).innerText = target ? target.time : 0; + } + }) + .catch(error => { + console.error('LeanCloud Counter Error', error); + }); + }; + + const { app_id, app_key, server_url } = CONFIG.leancloud_visitors; + const fetchData = api_server => { + const Counter = (method, url, data) => { + return fetch(`${api_server}/1.1${url}`, { + method, + headers: { + 'X-LC-Id' : app_id, + 'X-LC-Key' : app_key, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + }; + if (CONFIG.page.isPost) { + if (CONFIG.hostname !== location.hostname) return; + addCount(Counter); + } else if (document.querySelectorAll('.post-title-link').length >= 1) { + showTime(Counter); + } + }; + + let api_server; + if (server_url) { + api_server = server_url; + } else if (app_id.slice(-9) === '-MdYXbMMI') { + api_server = `https://${app_id.slice(0, 8).toLowerCase()}.api.lncldglobal.com`; + } + + document.addEventListener('page:loaded', () => { + if (api_server) { + fetchData(api_server); + } else { + fetch(`https://app-router.leancloud.cn/2/route?appId=${app_id}`) + .then(response => response.json()) + .then(({ api_server }) => { + fetchData(`https://${api_server}`); + }); + } + }); +})(); diff --git a/js/third-party/tags/mermaid.js b/js/third-party/tags/mermaid.js new file mode 100644 index 00000000..9623dc51 --- /dev/null +++ b/js/third-party/tags/mermaid.js @@ -0,0 +1,32 @@ +/* global NexT, CONFIG, mermaid */ + +document.addEventListener('page:loaded', () => { + const mermaidElements = document.querySelectorAll('.mermaid'); + if (mermaidElements.length) { + NexT.utils.getScript(CONFIG.mermaid.js, { + condition: window.mermaid + }).then(() => { + mermaidElements.forEach(element => { + const newElement = document.createElement('div'); + newElement.innerHTML = element.innerHTML; + newElement.className = element.className; + const parent = element.parentNode; + // Fix issue #347 + // Support mermaid inside backtick code block + if (parent.matches('pre')) { + parent.parentNode.replaceChild(newElement, parent); + } else { + parent.replaceChild(newElement, element); + } + }); + mermaid.initialize({ + theme : CONFIG.darkmode && window.matchMedia('(prefers-color-scheme: dark)').matches ? CONFIG.mermaid.theme.dark : CONFIG.mermaid.theme.light, + logLevel : 4, + flowchart: { curve: 'linear' }, + gantt : { axisFormat: '%m/%d/%Y' }, + sequence : { actorMargin: 50 } + }); + mermaid.init(); + }); + } +}); diff --git a/js/third-party/tags/pdf.js b/js/third-party/tags/pdf.js new file mode 100644 index 00000000..7e828911 --- /dev/null +++ b/js/third-party/tags/pdf.js @@ -0,0 +1,23 @@ +/* global NexT, CONFIG, PDFObject */ + +document.addEventListener('page:loaded', () => { + if (document.querySelectorAll('.pdf-container').length) { + NexT.utils.getScript(CONFIG.pdf.object_url, { + condition: window.PDFObject + }).then(() => { + document.querySelectorAll('.pdf-container').forEach(element => { + PDFObject.embed(element.dataset.target, element, { + pdfOpenParams: { + navpanes : 0, + toolbar : 0, + statusbar: 0, + pagemode : 'thumbs', + view : 'FitH' + }, + PDFJS_URL: CONFIG.pdf.url, + height : element.dataset.height + }); + }); + }); + } +}); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 00000000..949cd97f --- /dev/null +++ b/js/utils.js @@ -0,0 +1,431 @@ +/* global NexT, CONFIG */ + +HTMLElement.prototype.wrap = function(wrapper) { + this.parentNode.insertBefore(wrapper, this); + this.parentNode.removeChild(this); + wrapper.appendChild(this); +}; + +(function() { + const onPageLoaded = () => document.dispatchEvent( + new Event('page:loaded', { + bubbles: true + }) + ); + + if (document.readyState === 'loading') { + document.addEventListener('readystatechange', onPageLoaded, { once: true }); + } else { + onPageLoaded(); + } + document.addEventListener('pjax:success', onPageLoaded); +})(); + +NexT.utils = { + + registerExtURL: function() { + document.querySelectorAll('span.exturl').forEach(element => { + const link = document.createElement('a'); + // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings + link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + link.rel = 'noopener external nofollow noreferrer'; + link.target = '_blank'; + link.className = element.className; + link.title = element.title; + link.innerHTML = element.innerHTML; + element.parentNode.replaceChild(link, element); + }); + }, + + /** + * One-click copy code support. + */ + registerCopyCode: function() { + let figure = document.querySelectorAll('figure.highlight'); + if (figure.length === 0) figure = document.querySelectorAll('pre:not(.mermaid)'); + figure.forEach(element => { + element.querySelectorAll('.code .line span').forEach(span => { + span.classList.forEach(name => { + span.classList.replace(name, `hljs-${name}`); + }); + }); + if (!CONFIG.copycode.enable) return; + let target = element; + if (CONFIG.copycode.style !== 'mac') target = element.querySelector('.table-container') || element; + target.insertAdjacentHTML('beforeend', '
'); + const button = element.querySelector('.copy-btn'); + button.addEventListener('click', () => { + const lines = element.querySelector('.code') || element.querySelector('code'); + const code = lines.innerText; + if (navigator.clipboard) { + // https://caniuse.com/mdn-api_clipboard_writetext + navigator.clipboard.writeText(code).then(() => { + button.querySelector('i').className = 'fa fa-check-circle fa-fw'; + }, () => { + button.querySelector('i').className = 'fa fa-times-circle fa-fw'; + }); + } else { + const ta = document.createElement('textarea'); + ta.style.top = window.scrollY + 'px'; // Prevent page scrolling + ta.style.position = 'absolute'; + ta.style.opacity = '0'; + ta.readOnly = true; + ta.value = code; + document.body.append(ta); + ta.select(); + ta.setSelectionRange(0, code.length); + ta.readOnly = false; + const result = document.execCommand('copy'); + button.querySelector('i').className = result ? 'fa fa-check-circle fa-fw' : 'fa fa-times-circle fa-fw'; + ta.blur(); // For iOS + button.blur(); + document.body.removeChild(ta); + } + }); + element.addEventListener('mouseleave', () => { + setTimeout(() => { + button.querySelector('i').className = 'fa fa-copy fa-fw'; + }, 300); + }); + }); + }, + + wrapTableWithBox: function() { + document.querySelectorAll('table').forEach(element => { + const box = document.createElement('div'); + box.className = 'table-container'; + element.wrap(box); + }); + }, + + registerVideoIframe: function() { + document.querySelectorAll('iframe').forEach(element => { + const supported = [ + 'www.youtube.com', + 'player.vimeo.com', + 'player.youku.com', + 'player.bilibili.com', + 'www.tudou.com' + ].some(host => element.src.includes(host)); + if (supported && !element.parentNode.matches('.video-container')) { + const box = document.createElement('div'); + box.className = 'video-container'; + element.wrap(box); + const width = Number(element.width); + const height = Number(element.height); + if (width && height) { + box.style.paddingTop = (height / width * 100) + '%'; + } + } + }); + }, + + registerScrollPercent: function() { + const backToTop = document.querySelector('.back-to-top'); + const readingProgressBar = document.querySelector('.reading-progress-bar'); + // For init back to top in sidebar if page was scrolled after page refresh. + window.addEventListener('scroll', () => { + if (backToTop || readingProgressBar) { + const contentHeight = document.body.scrollHeight - window.innerHeight; + const scrollPercent = contentHeight > 0 ? Math.min(100 * window.scrollY / contentHeight, 100) : 0; + if (backToTop) { + backToTop.classList.toggle('back-to-top-on', Math.round(scrollPercent) >= 5); + backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; + } + if (readingProgressBar) { + readingProgressBar.style.setProperty('--progress', scrollPercent.toFixed(2) + '%'); + } + } + if (!Array.isArray(NexT.utils.sections)) return; + let index = NexT.utils.sections.findIndex(element => { + return element && element.getBoundingClientRect().top > 10; + }); + if (index === -1) { + index = NexT.utils.sections.length - 1; + } else if (index > 0) { + index--; + } + this.activateNavByIndex(index); + }, { passive: true }); + + backToTop && backToTop.addEventListener('click', () => { + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: 0 + }); + }); + }, + + /** + * Tabs tag listener (without twitter bootstrap). + */ + registerTabsTag: function() { + // Binding `nav-tabs` & `tab-content` by real time permalink changing. + document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { + element.addEventListener('click', event => { + event.preventDefault(); + // Prevent selected tab to select again. + if (element.classList.contains('active')) return; + const nav = element.parentNode; + // Get the height of `tab-pane` which is activated before, and set it as the height of `tab-content` with extra margin / paddings. + const tabContent = nav.nextElementSibling; + tabContent.style.overflow = 'hidden'; + tabContent.style.transition = 'height 1s'; + // Comment system selection tab does not contain .active class. + const activeTab = tabContent.querySelector('.active') || tabContent.firstElementChild; + // Hight might be `auto`. + const prevHeight = parseInt(window.getComputedStyle(activeTab).height.replace('px', ''), 10) || 0; + const paddingTop = parseInt(window.getComputedStyle(activeTab).paddingTop.replace('px', ''), 10); + const marginBottom = parseInt(window.getComputedStyle(activeTab.firstElementChild).marginBottom.replace('px', ''), 10); + tabContent.style.height = prevHeight + paddingTop + marginBottom + 'px'; + // Add & Remove active class on `nav-tabs` & `tab-content`. + [...nav.children].forEach(target => { + target.classList.toggle('active', target === element); + }); + // https://stackoverflow.com/questions/20306204/using-queryselector-with-ids-that-are-numbers + const tActive = document.getElementById(element.querySelector('a').getAttribute('href').replace('#', '')); + [...tActive.parentNode.children].forEach(target => { + target.classList.toggle('active', target === tActive); + }); + // Trigger event + tActive.dispatchEvent(new Event('tabs:click', { + bubbles: true + })); + // Get the height of `tab-pane` which is activated now. + const hasScrollBar = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight); + const currHeight = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height.replace('px', ''), 10); + // Reset the height of `tab-content` and see the animation. + tabContent.style.height = currHeight + paddingTop + marginBottom + 'px'; + // Change the height of `tab-content` may cause scrollbar show / disappear, which may result in the change of the `tab-pane`'s height + setTimeout(() => { + if ((document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight)) !== hasScrollBar) { + tabContent.style.transition = 'height 0.3s linear'; + // After the animation, we need reset the height of `tab-content` again. + const currHeightAfterScrollBarChange = parseInt(window.getComputedStyle(tabContent.querySelector('.active')).height.replace('px', ''), 10); + tabContent.style.height = currHeightAfterScrollBarChange + paddingTop + marginBottom + 'px'; + } + // Remove all the inline styles, and let the height be adaptive again. + setTimeout(() => { + tabContent.style.transition = ''; + tabContent.style.height = ''; + }, 250); + }, 1000); + if (!CONFIG.stickytabs) return; + const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10; + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: offset + }); + }); + }); + + window.dispatchEvent(new Event('tabs:register')); + }, + + registerCanIUseTag: function() { + // Get responsive height passed from iframe. + window.addEventListener('message', ({ data }) => { + if (typeof data === 'string' && data.includes('ciu_embed')) { + const featureID = data.split(':')[1]; + const height = data.split(':')[2]; + document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; + } + }, false); + }, + + registerActiveMenuItem: function() { + document.querySelectorAll('.menu-item a[href]').forEach(target => { + const isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); + const isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); + target.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); + }); + }, + + registerLangSelect: function() { + const selects = document.querySelectorAll('.lang-select'); + selects.forEach(sel => { + sel.value = CONFIG.page.lang; + sel.addEventListener('change', () => { + const target = sel.options[sel.selectedIndex]; + document.querySelectorAll('.lang-select-label span').forEach(span => { + span.innerText = target.text; + }); + // Disable Pjax to force refresh translation of menu item + window.location.href = target.dataset.href; + }); + }); + }, + + registerSidebarTOC: function() { + this.sections = [...document.querySelectorAll('.post-toc li a.nav-link')].map(element => { + const target = document.getElementById(decodeURI(element.getAttribute('href')).replace('#', '')); + // TOC item animation navigate. + element.addEventListener('click', event => { + event.preventDefault(); + const offset = target.getBoundingClientRect().top + window.scrollY; + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: offset, + complete : () => { + history.pushState(null, document.title, element.href); + } + }); + }); + return target; + }); + }, + + registerPostReward: function() { + const button = document.querySelector('.reward-container button'); + if (!button) return; + button.addEventListener('click', () => { + document.querySelector('.post-reward').classList.toggle('active'); + }); + }, + + activateNavByIndex: function(index) { + const target = document.querySelectorAll('.post-toc li a.nav-link')[index]; + if (!target || target.classList.contains('active-current')) return; + + document.querySelectorAll('.post-toc .active').forEach(element => { + element.classList.remove('active', 'active-current'); + }); + target.classList.add('active', 'active-current'); + let parent = target.parentNode; + while (!parent.matches('.post-toc')) { + if (parent.matches('li')) parent.classList.add('active'); + parent = parent.parentNode; + } + // Scrolling to center active TOC element if TOC content is taller then viewport. + const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); + if (!document.querySelector('.sidebar-toc-active')) return; + window.anime({ + targets : tocElement, + duration : 200, + easing : 'linear', + scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top + }); + }, + + updateSidebarPosition: function() { + if (window.innerWidth < 1200 || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; + // Expand sidebar on post detail page by default, when post has a toc. + const hasTOC = document.querySelector('.post-toc'); + let display = CONFIG.page.sidebar; + if (typeof display !== 'boolean') { + // There's no definition sidebar in the page front-matter. + display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); + } + if (display) { + window.dispatchEvent(new Event('sidebar:show')); + } + }, + + activateSidebarPanel: function(index) { + const duration = 200; + const sidebar = document.querySelector('.sidebar-inner'); + const panel = document.querySelector('.sidebar-panel-container'); + const activeClassName = ['sidebar-toc-active', 'sidebar-overview-active']; + + if (sidebar.classList.contains(activeClassName[index])) return; + + window.anime({ + duration, + targets : panel, + easing : 'linear', + opacity : 0, + translateY: [0, -20], + complete : () => { + // Prevent adding TOC to Overview if Overview was selected when close & open sidebar. + sidebar.classList.replace(activeClassName[1 - index], activeClassName[index]); + window.anime({ + duration, + targets : panel, + easing : 'linear', + opacity : [0, 1], + translateY: [-20, 0] + }); + } + }); + }, + + getScript: function(src, options = {}, legacyCondition) { + if (typeof options === 'function') { + return this.getScript(src, { + condition: legacyCondition + }).then(options); + } + const { + condition = false, + attributes: { + id = '', + async = false, + defer = false, + crossOrigin = '', + dataset = {}, + ...otherAttributes + } = {}, + parentNode = null + } = options; + return new Promise((resolve, reject) => { + if (condition) { + resolve(); + } else { + const script = document.createElement('script'); + + if (id) script.id = id; + if (crossOrigin) script.crossOrigin = crossOrigin; + script.async = async; + script.defer = defer; + Object.assign(script.dataset, dataset); + Object.entries(otherAttributes).forEach(([name, value]) => { + script.setAttribute(name, String(value)); + }); + + script.onload = resolve; + script.onerror = reject; + + if (typeof src === 'object') { + const { url, integrity } = src; + script.src = url; + if (integrity) { + script.integrity = integrity; + script.crossOrigin = 'anonymous'; + } + } else { + script.src = src; + } + (parentNode || document.head).appendChild(script); + } + }); + }, + + loadComments: function(selector, legacyCallback) { + if (legacyCallback) { + return this.loadComments(selector).then(legacyCallback); + } + return new Promise(resolve => { + const element = document.querySelector(selector); + if (!CONFIG.comments.lazyload || !element) { + resolve(); + return; + } + const intersectionObserver = new IntersectionObserver((entries, observer) => { + const entry = entries[0]; + if (!entry.isIntersecting) return; + + resolve(); + observer.disconnect(); + }); + intersectionObserver.observe(element); + }); + } +}; diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 00000000..6eeb571d --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.
본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요.

+

처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+ + +

요 며칠간 이 책을 읽었습니다. 회사 도서관에 갔다가 제목이 끌려서 한 번 읽어봤어요.
누가 정한건지 모르겠지만 책 제목 참 멋지게 지었습니다. 주변에서 제가 이 책 읽는 것 보면 모두들 제목에 대해 관심을 보이더군요 ㅎ

+

진로를 고민중인 학생이나 일을 시작한지 얼마 되지 않는 신입 개발자들을 주 대상으로 삼은 책입니다. 다소 주관적이긴 하지만 선배 개발자 입장에서 들려주는 이런 저런 이야기들이 적혀 있어요.
저자는 자기관리를 잘 하는 분이신 듯 합니다. 구체적인 개인 목표를 세우고 달성을 위해 노력하는 점이라든지, 꾸준한 자기계발에 관심을 두는 점 같은 좋은 습관을 많이 만들어두신 것 같네요.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

기존에 티스토리에서 운영 중이던 프로그래밍 관련 블로그(devnote.tistory.com)를 Octopress로 이사합니다. 사실 운영이라고 말하기도 뭣할 만큼 오랫동안 방치되어 있었는데, 다시금 분위기를 쇄신하고자 환경을 바꿔볼까 합니다.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+ + +

octopress도 대게는 ruby가 기본 설치된 mac에서 많이들 사용하는 듯 하다. 검색해보면 대부분 OS X를 기준으로 한 셋팅법이다. 윈도우에서 사용하는 것도 많이 어렵진 않지만 한글 인코딩 때문에 많이 헤맸음 ㅜㅠ

+

일단 기본적으로 아래 두 개의 글을 참고해 설치했는데,

+
    +
  1. http://stb.techelex.com/setup-octopress-on-windows7/
  2. +
  3. http://chulhankim.github.io/blog/2013/07/31/octopress-and-github.html
  4. +
+

ruby는 생소한 언어이기도 하고 링크가 사라지면 다시 헤맬수도 있으니 간략하게 다시 정리.

+ +
+ + 더 읽어보기 » + +
+ + + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/placeholder b/placeholder deleted file mode 100644 index e69de29b..00000000 diff --git a/post-sitemap.xml b/post-sitemap.xml new file mode 100644 index 00000000..b0a38302 --- /dev/null +++ b/post-sitemap.xml @@ -0,0 +1,124 @@ + + + + + http://leafbird.github.io/devnote/ + daily + 1 + + + + + http://leafbird.github.io/devnote/2021/08/08/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/ + 2023-01-07T02:18:49.836Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2021/01/01/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-Thread-Local-Storage/ + 2023-01-07T02:18:49.832Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2020/12/26/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-FILE-LINE-%EB%8C%80%EC%B2%B4%EC%A0%9C/ + 2023-01-07T02:18:49.830Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2020/12/27/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-System-IO-Pipeline-%EB%8F%84%EC%9E%85-%ED%9B%84%EA%B8%B0/ + 2023-01-07T02:18:49.830Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/09/30/cleanup-cpp-project-3rd/ + 2023-01-07T02:18:49.829Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2018/11/12/%ED%85%8C%ED%81%AC%EB%8B%88%EC%BB%AC-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/ + 2023-01-07T02:18:49.829Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2013/12/30/octopress-on-windows/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/07/14/move-to-octopress/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/07/19/google-c-plus-plus-style-guide/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/07/16/the-benz-programmer/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/07/21/octopress-tips-on-windows/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/08/19/yoda-notation/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/09/17/cleanup-cpp-project-2nd/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + http://leafbird.github.io/devnote/2014/09/12/claenup-cpp-project-1st/ + 2023-01-07T02:18:49.828Z + weekly + 0.6 + + + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..14b94357 --- /dev/null +++ b/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://leafbird.github.io/devnote/sitemap.xml diff --git a/rss2.xml b/rss2.xml new file mode 100644 index 00000000..9bf0cdf9 --- /dev/null +++ b/rss2.xml @@ -0,0 +1,392 @@ + + + + leafbird/devnote + http://leafbird.github.io/devnote/ + + + + + Sat, 07 Jan 2023 02:18:49 GMT + http://hexo.io/ + + + C# 고성능 서버 - 메모리 단편화 + http://leafbird.github.io/devnote/2021/08/08/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/ + http://leafbird.github.io/devnote/2021/08/08/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/ + Sun, 08 Aug 2021 12:44:20 GMT + + <img src="/devnote/2021/08/08/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/00.jpg" class=""> + +<p>이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.</p> +<p>이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다. </p> + + + +

이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.

이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.

기본 용어 및 개념 정리

SOH / LOH / POH

가장 먼저 관리 힙(managed heap)의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라 SOH, LOH, POH로 나뉜다.

  • SOH는 Small Object Heap으로, 85kb보다 작은 사이즈의 메모리를 할당한다. 경우에 따라 차이는 있겠지만 대다수의 객체들이 주로 할당/해제 되는 공간이다.
  • LOH는 Large Object Heap으로, 85kb보다 큰 사이즈의 메모리를 할당한다.
  • POH는 Pinned Object Heap으로, pinning할 메모리를 위해 .Net 5부터 새롭게 추가된 공간이다.

POH는 사실 다짜고자 단편화의 해법에 가까운 존재이긴하나.. 분류상 미리 언급되었다. 이후에 다시 추가적으로 설명한다.

Compression

SOH의 메모리는 객체가 얼마나 오래 살았느냐에 따라 0세대부터 2세대까지 세대를 구분한다. GC가 한 번 실행될 때 사용이 끝난 메모리는 해제되고, 아직 사용중인 메모리는 다음 세대로 승격한다. 이 때 살아남은 메모리들은 압축(Compression)의 과정을 거친다. 압축이란 메모리 단편화를 줄이기 위해, 살아남은 메모리들을 사이사이 공백이 없도록 한 공간으로 몰아서 재배치하는 동작을 말한다. 실제로 관리 힙 내부에서 객체들은 세대별로 모아두어야 하기 때문에, 메모리 해제 및 승격을 거친 후에는 세대별 구획에 맞춰 메모리를 재정렬하는 과정이 반드시 필요하다.

오.. 이거 처음에 너무 신기했다. 네이티브 언어로 만들어진 코드에서는 불가능한 동작이다. C++로 짠 코드라면 프로그래머가 직접 작성한 비즈니스 로직 상에서 이미 무수히 많은 포인터들이 가상 메모리의 주소값 자체를 가르키고 있기 때문이다. C#의 참조타입 변수들도 C++ 포인터와 유사하다고 볼 순 있지만 직접적으로 메모리 주소가 노출되어 있지는 않기 때문에 가능한 일이다. 객체의 메모리상 주소가 바뀌더라도 모든 참조들을 새로운 주소값으로 알아서 갱신해 주어서, 매니지드 레벨의 코드상에서는 마치 아무 일도 없었다는 듯이 시치미를 떼는 신박한 동작이다.

Pinned Memory

하지만 메모리 압축이 이미 할당된 모든 객체들의 위치를 제멋대로 바꿀 수 있는 것은 아니며, 모든 법칙에 항상 예외는 존재한다. 매니지드 레벨은 결국 네이티브 레벨 위에서 돈다. 네이티브 영역과의 상호참조가 필요한 매니지드 메모리는 함부로 값을 옮겨다닐 수가 없다. 위에서 언급한 C++로 만든 코드였다면 불가능하다고 말한 이유와 크게 다르지 않은 상황이다.
네이티브 영역에서 매니지드 영역의 메모리를 참조할 일이 있을 때는 메모리를 이동이 발생하지 않는 안전한 공간에 복사(copying)하거나, 이동할 수 없도록 고정(pinning)해둬야 한다. 매니지드 메모리가 다른 주소로 이동하지 않도록 고정하는 것을 Memory Pinning, 이렇게 고정된 메모리를 Pinned Memory라고 부른다.

데이터 마샬링(매니지트/네이티브 상호통신)의 입장에서 보면 pinning은 불필요한 복사를 줄여주는 효율적인 동작이다. 하지만 가비지 컬렉터 입장에서 보자면 엄청난 방해꾼임이 분명하다. pinned memory 는 gc의 압축 동작을 방해하기 때문이다

고정(Pinning)은 데이터를 현재 메모리 위치상에 임시로 잠그기 때문에, CLR의 가비지 수집기에 의한 재배치를 막아줍니다.
Pinning temporarily locks the data in its current memory location, thus keeping it from being relocated by the common language runtime’s garbage collector.
(https://docs.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning)

고정(Pinning)은 메모리의 단편화를 유발하고, 일반적으로 객체 압축 과정을 복잡하게 만들기 때문에 자체적인 비용 부담을 가집니다.
Pinning has its own costs, because it introduces fragmentation (and in general complicates object compaction a lot).
(https://tooslowexception.com/pinned-object-heap-in-net-5/)

단편화 발생의 원인

성능좀 끌어올려보겠다고 다짐한 C# 게임서버의 메모리 단편화는 어디서 발생하는가.

핵심부터 말하자면 소켓의 send / receive에 걸어주는 바이트 배열 버퍼가 pinning되기 때문에, 가비지 컬렉터의 압축과정을 많이 방해하게 되면서 메모리 단편화를 유발한다. 이 부분이 메모리 단편화의 가장 주된 요인이다. 그런데다가 높은 TPS를 처리해내는 고성능 게임서버를 만들려고 한다면.. 소켓 IO의 수가 많아짐에 따라 네트워크 버퍼의 개수와 사용 빈도도 당연히 높아질 수밖에 없다. 때문에 대량의 네트워크 통신을 견딜 수 있도록 만드려면 네트워크 버퍼를 어떻게 운용할 것인지가 중요하다.

DB와 통신하기 위한 DBMS 클라이언트도 많은 수의 pinned handle을 만들어낸다. 현재 우리 프로젝트는 System.Data.SqlClient 네임스페이스 하위의 클래스들을 이용해 Azure SQL과 통신하고 있는데, 생각해보면 db client도 DBMS에 연결되어 쿼리와 데이터를 던지고 받는 통신모듈이니 당연한 이야기다.

코드상에서 임의의 객체를 약참조 하기 위해 사용하는 System.WeakReference도 pinning handle을 사용하고 있어, 단편화 유발의 원인이 된다. 이건 참 아이러니한 일이다. 참조하는 대상이 쉽게 메모리 해제될 수 있도록 약참조하는 기능을 하지만, WeakReference 자신은 고정된 메모리를 만들면서 메모리 단편화를 가속시킨다. 처음 서버 기반을 만들 땐 WeakReference가 GC를 방해한다는 사실을 모르고 엄청시리 쓰고 있었는데, 비교적 근래에 실 서비스에서 메모리 문제들을 겪으면서 디버깅 하던 중 메모리가 고정되고 있음을 알게됐다. 현재는 약참조 사용이 꼭 필요한 일부를 제외하고는 모두 제거하였고, 가능하면 WeakReference 의 사용을 자제하고 있다.

메모리 상의 고정된 핸들에 대한 정보는 windbg로 힙을 뒤져보면 알 수 있다. sos.dll 로딩된 상태에서 !gchandles 명령 쳐보면 현재 어떤 객체가 pinning되어있고, 몇개나 존재하는지 확인할 수 있다.

단편화 해결 솔루션

상술한 원인들 중 가장 명백한 원인제공자는 네트워크 버퍼다. 빈번히 쓰이는 네트워크 버퍼를 잘 운용하는 것이 단편화 해결의 핵심이다.

네트워크 버퍼용 byte[] 객체를 ArrayPool<T> 을 이용해 풀링하는 것은 그다지 개선의 효과가 없었다. ArrayPool<T>클래스는 효율적으로 객체의 할당과 해제 빈도를 완화하고 관리해주지만, 어쨌거나 SOH 공간에서 할당을 받기 때문에, 이글에서 말하고 있는 pinning 이나 단편화 현상 해결 등과는 크게 상관이 없다.

메모리 압축은 SOH에서만 발생한다. 따라서 pinned memory가 GC성능 저하 및 메모리 단편화를 일으키는 것도 SOH에만 해당하는 이야기다. 그러니 네트워크 버퍼는 그냥 SOH에 잡지 않는 것이 좋겠다.

솔루션 1. 네트워크 버퍼를 POH에 할당하기

MS 형들도 역시 성능상에서 이런 문제가 있음을 분명히 알고 있다. .NET 5부터는 고정된 메모리로 사용할 객체를 할당하는 별도의 힙 공간인 POH가 새로 생겼다. 현재 회사에서 만든 게임 서버는 프레임워크 버전이 낮아서 아직 사용해 보지는 못했다. (우리 프로젝트는 .NET Framework 4.7.2로 개발을 시작해서 현재 .NET Core 3.1을 사용중이다). 이 글에서 POH에 대한 기본적인 설명을 확인할 수 있다. 아직 서비스하기 전이거나, 사용중인 프레임워크가 .NET 5 이상이라면 POH의 도입을 검토해 볼 만 하다.
링크된 글에서 설명하는 것처럼 POH는 그 존재 목적상, blittable 형식만을 할당할 수 있도록 제한되어있다. 네이티브 코드와 통신하기 위한 데이터를 할당하는 전용의 공간이므로, 기술적인 한계가 아닌 설계상의 의도로 제한을 걸어두었다.

솔루션 2. 네트워크 버퍼를 LOH에 할당하기

LOH의 객체들은 메모리 압축으로 인한 재배치를 진행하지 않으며, 세대가 구분되어있지도 않다. 2세대 GC가 수행될 때만 LOH상의 메모리 해제가 진행되므로, 모두 2세대 객체라고 부르기도 한다. 세대 구분이 없으니 메모리 공간상에서 꼭 재배치(Compression) 해주어야 할 필요도 없다.
LOH의 객체는 기본설정상 가상 메모리 주소공간에 한 번 할당되면 위치가 이동되지 않는다. 그러니 빈번하게 할당과 해제를 반복하는 메모리를 LOH에 많이 만들면 금방 조각나버릴 공간이다. 이런 경우라면 LOH에서도 압축을 하도록 설정을 조정할 수는 있지만.. 이렇게 사용하는 것은 그다지 취지(?)에 맞지 않는 기분이 든다. LOH에는 오래도록 유지하거나, 아예 해제할 계획이 없는 덩치큰 메모리들을 위치시키는 것이 용도상 더 적절하다.
우리는 게임 런칭 전 10만 동접을 시뮬레이션하는 부하테스트를 진행했다. 당시 메모리 단편화 이슈로 한참을 고생하던 중, 이 글의 해결 사례를 보고나서 네트워크 버퍼 할당을 LOH로 옮겨 보기로 했다.

네트워크 버퍼를 LOH로 옮긴 이후 메모리 단편화 문제는 말끔해 해결되었다. 한 번에 100Mb 단위의 커다란 메모리 청크를 LOH에 잡아두고, 이를 다시 ArraySegment<byte>로 잘게 나누어 풀링하면서 사용하는 방식이다. C++에서 고전적으로 메모리 풀링을 구현할 때 접근하는 방식과 유사하다.

C#에서는 버퍼의 조각을 byte[]로 표현할 수 없다. C++에서 byte[]는 개념상 가르키는 대상이 고정인 포인터 (byte * const)와 유사하다(물론 문법상 차이는 있다). 그러므로 커다란 바이트 배열도 포인터, 여러개의 작은 배열들도 포인터로 가르키는 셈이니까 모두 byte[]로 표현되는게 아무 문제가 없다. 하지만 C#에서는 byte[]도 하나의 독립된 매니지드 객체이므로 C++과는 차이가 있다. 큰 배열의 단위조각을 표현할 때 ArraySegment<byte>를 사용해야 하는 이유다.

조금은 다른 이야기지만 처음 ArrayPool<T> 가 BCL에 들어왔을때 아주 당연하게 착각한것이, 이놈으로 byte[]를 풀링하면 내부적으로 큰 청크를 한 번만 할당해서 이걸 조각내서 쓸것으로 생각했다. 메모리 관리라 하면 으레 이 방식이 익숙해서였다. 하지만 조금만 생각해보면, C#에서는 불가능한 이야기다. 덩치큰 byte[]를 여러개의 작은 byte[]로 표현할 수가 없다. ArrayPool<T> 코드를 보면 할당 자체는 SOH상에서 단일객체 단위로 발생하나, 그 외 나머지 기법들을 이용해 최적화를 진행함을 알 수 있다. 코드를 보면 2세대 GC가 불릴 때 콜백을 얻어와 현재 메모리 압력을 진단하고, 선택적으로 메모리를 해제하는 등의 테크닉을 볼 수 있다. 이런건 나중에 메모리 로우레벨을 제어해야 할 경우 참고하여 응용하면 좋을듯 하다.

이전 포스팅 C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는 ZeroCopyBuffer의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼 LohSegment 클래스가 바로 LOH에 할당한 커다란 청크의 일부분에 해당한다.

1
2
3
4
5
6
7
8
namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...

LohSegment를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서 LOH에 할당될 것이고.. 청크를 다시 잘 쪼개서 ConcurrentQueue<>에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;

private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}

정리

C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.

SOH에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을 LOH에 할당하면 이런 문제를 해결할 수 있다.

참고자료

]]>
+ + + + c# + + 고성능 + + 게임서버 + + ArrayPool + + Memory + + Fragmentation + + + http://leafbird.github.io/devnote/2021/08/08/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/#disqus_thread + +
+ + + C# 고성능 서버 - Thread Local Storage + http://leafbird.github.io/devnote/2021/01/01/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-Thread-Local-Storage/ + http://leafbird.github.io/devnote/2021/01/01/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-Thread-Local-Storage/ + Fri, 01 Jan 2021 07:00:49 GMT + + <p>프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 <a href="https://en.wikipedia.org/wiki/Thread-local_storage">Thread Local Storage</a> (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 <code>__declspec(thread)</code> 키워드를 이용해서 tls 변수를 선언할 수 있다. </p> +<p>C#에도 <code>ThreadLocal&lt;T&gt;</code> 라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다. </p> +<p>이를 해결하는 방법과 주의해야 할 사항을 정리해본다. </p> + + + + 프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread) 키워드를 이용해서 tls 변수를 선언할 수 있다.

C#에도 ThreadLocal<T> 라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.

이를 해결하는 방법과 주의해야 할 사항을 정리해본다.

알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.

수정한 내용 요약 : 새로 깨어난 스레드인데도 AsyncLocal<T>에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도 AsyncLocal<T>의 값을 복사하고 있었기 때문이었습니다.

async / await 을 절대 가볍게 접근하면 안된다

주제와 약간 벗어날 수 있지만 서두에 미리 한 번 짚고 넘어갈 부분이 있다. 절대로 async / await를 이용한 비동기 프로그래밍을 만만하게 보아서는 안된다는 것이다.

나도 그랬지만 누구든지 제일 처음 비동기 메서드를 접했을 땐 이해하기 쉽고 간단한 기능이라는 첫인상을 가지게 될 것이다. 개인적으로는 비동기 메서드를 적용하고 난 후의 코드가 동기 프로그래밍과 너무 비슷해져 버리는 점이 착각을 유발하는 큰 원인이라고 생각한다 (MS: 얘는 뭐 좋게 해줘도 불만이 많네..)

이전에 DB 쿼리나 네트워크 통신같은 IO 작업에서 비동기로 받는 결과값을 처리하기 위해서는 하나의 동일한 주제(single concern)를 위한 로직임에도 불구하고 비동기 요청 이전과 이후의 코드가 분절되어야 했다. 이를테면 비동기 요청 전의 코드와 응답 후의 코드를 서로 다른 메서드로 나누어서 짜야 했다는 뜻이다. 코드의 가독성에 대해 고민을 좀 해봤던 개발자라면 람다를 써서 어떻게든 읽기 좋고 관리하기 좋도록 애써 보았을 수도 있으나, 가독성에서 정도의 차이가 있을 뿐 명백하게 존재하는 코드상의 분절을 피할 수 없었다.

비동기 메서드의 등장으로 이런 상황은 옛날 이야기가 되었다. 안간힘을 써보아도 완전하게 붙이기 힘들었던 분절된 코드들은 이제 하나의 async 함수 안에서 seamless하게 구현할 수 있게 되었다. 작성한 코드를 읽을 때에도 (신경써서 읽지 않는다면) 어디가 동기 처리이고, 어디가 비동기 처리인지도 잘 모르고 넘어갈만큼 술술 읽어내려가게 되었다. 좋게 해석하자면 어플리케이션 개발자가 좀 더 로직에만 집중 할 수 있는 환경이 되었다.

이것은 호수에 떠있는 백조와 같다. 일단 겉으로 보기에는 아주 우아하게 비동기 코드를 표현했으나, 조금만 안을 들여다보면 비동기 요청을 기준으로 발생하는 여전한 로직의 분절, 그에 따른 실행 시점 시간차 및 실행 환경상의 차이 등은 당연게도 여전히 존재하고 있기 때문이다. 이로 인한 이슈들은 동시성(concurrency)이 있는 멀티스레드 환경에서 더 잘 드러난다. MS는 실제로 프로그래머들이 하부의 복잡한 메커니즘을 잘 모르더라도 쉽고 편하게 비동기 로직을 다룰 수 있는 유토피아를 꿈꾸었을지 모르겠다. 하지만 싱글 스레드로 간단한 툴 한두개 짜는거면 몰라도… C#이란 언어로 고성능 서버를 만들겠다고 한다면, 이에 대한 충분한 이해가 없이는 런타임에서 예상못한 오작동을 피할 수 없을 것이다.

이후 글에서 언급할 내용도 비동기 함수의 실행 시점차와 관련되어 있으므로, 비동기 메서드에 대한 어느 정도의 이해가 필요하다.

ThreadLocal

우선 잠깐 언급했던 ThreadLocal<T> 클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던 [ThreadStatic] 어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서 ThreadLocal<T> 가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인 ThreadLocal<T> 를 사용하면 된다.

모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}

// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.

// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());

internal static Random Instance => Random.Value;
}
}
}

이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는 Random 객체가 어느순간 2번 스레드의 Random 객체로 바뀐다 해도 동작에 큰 영향이 없다.

AsyncLocal

문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.

스레드별로 서로 다른 상태값을 사용해야 하는 예를 구승모 교수님의 Dispatcher 구현에서 찾아볼 수 있다. (ThreadLocal.h) Dispatcher는 고성능 멀티스레드 로직 수행을 위한 Actor 패턴 구현체다. 스레드에 lock을 걸지 않으면서도 서로 다른 스레드간 간섭 없이 순차실행을 가능하게 하기 위해, 스레드는 현재 자신의 수행상태 일부를 tls에 기록해 두어야 한다.

친절한 ms 형들이 이런 경우를 위해 AsyncLocal 클래스도 미리 만들어 두었다. 생긴것도 서로 비슷해서 ThreadLocal<T> 를 사용했던 변수에 대신 AsyncLocal<T> 로 바꿔주면 위에서 말한 문제를 해결할 수 있다. 0번 스레드가 먼저 코드를 수행하다가 await 구문을 만나서 대기하고, 대기가 풀려날 때 2번 스레드로 변경이 되었더라도 AsyncLocal<T> 가 2번 스레드의 tls 값을 알아서 “영희” -> “철수”로 바꿔주는 것이다.

문제점 : 의도치 않게 값의 복사 발생

이러면 문제는 해결된 것 같지만, 또 다른 문제가 있다. 여기가 이 글의 핵심이다 집중해주기 바란다. AsyncLocal<T>ThreadPool이 다른 새 스레드를 추가로 깨우게 하는 특정 api들 중에 하나를 호출하는 경우, 기본적으로 호출자 스레드의 변수값을 새로운 스레드에게 복사해주는 기본 동작을 갖고 있다. 현재 스레드에서만 고유하게 유지하려고 기록해 둔 tls의 변수들이 요주의 api중 하나를 호출하는 순간 새로운 다른 스레드로 복사되는 것이다. 현재 우리 프로젝트 구현의 범위 기준에서, AsyncLocal의 값을 복사시키는 메서드들은 아래와 같다.

  1. Fire-and-forgot 으로 동작할 백그라운드 작업이 필요해서 직접 ThreadPool에 요청하는 메서드들

    • Task.Run()
    • ThreadPool.QueueUserWorkItem()
  2. 비동기 소켓의 IO 완료통지를 포함해, 네트워크 이벤트 콜백을 유발하는 메서드들

    • Socket.ConnectAsync() - ConnectEx() in win32
    • Socket.DisconnectAsync() - DisconnectEx() in win32
    • Socket.AcceptAsync() - AcceptEx() in win32
    • Socket.ReceiveAsync() - WSARecv() in win32
    • Socket.SendAsync() - WSASend() in win32

1번 백그라운드 작업 요청 메서드들은 스레드풀을 대상으로 하는 동작이니까 어느 정도 이해가 된다고 하지만, 2번 네트워크 콜백들은 tls를 복사한다는 점이 선뜻 연결이 잘 되지 않는다. managed 메서드의 이름이 낮설어 보일까 싶어 win32에 해당하는 함수명도 같이 적었는데, 그냥 OVERLAPPED 구조체를 이용해 IOCP에 통지를 요청하는 네트워크 api들 전체를 말한다.

0번 스레드가 게임 로직을 열심히 수행하다가 클라이언트로 동기화 패킷을 보낼 상황이 되었다. 그래서 패킷을 만들어 소켓에 SendAsync()를 한 번 걸어놓고, 다시 또 다른 로직을 열심히 수행한다. 근데 0번 스레드가 걸었던 send 요청이 완료되어 새롭게 2번 스레드가 OnSendCompleted 메서드를 실행하려고 깨어났는데, 이 때 0번 스레드가 AsyncLocal<T>에 저장해두었던 tls 값들을 2번 스레드가 고대로 복사받아서 수행을 시작하는 것이다.

AsyncLocal<T>는 자신의 존재 목적과 취지에 충실하고자, 서로 다른 스레드들간에 조금이라도 관련이 있을라 치면 아주 얄짤없이 값을 복사해대는 것 같다. 하지만 win32에서 iocp에 비동기 작업의 완료 통지를 요청하고, 전혀 관련없는 다른 스레드로부터 이를 받아 처리해오던 고전적 처리방식에 익숙해서 그런지 이런 과도한 친절이 부담스럽다. 너 때문에 Dispatcher 동작이 다 깨지잖아. 조치가 필요하다.

원치 않는 AsyncLocal 복사는 꺼준다.

다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는 SuppressFlow() 호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}

public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;

// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}

작업 요청 후에는 RestoreFlow 를 불러 복구해주면 되는데, SuppressFlow 메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.

네트워크 구현부에도 수정이 필요하다. SocketAsyncEventArgs 객체를 사용해 비동기 요청을 수행하는 모든 곳에도 RestoreFlow 를 불러준다. (SocketAsyncEventArgs는 win32의 OVERLAPPED 구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);

using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}

이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도 AsyncLocal<T>을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.

정리

  • C#의 비동기 메서드는 코드상으로는 매끈하게 이어져 있는듯 보이지만 실은 비동기 요청 지점을 전후로 분리 실행되며, 실행 스레드가 서로 다를 수도 있다.
  • 이로 인해 ThreadLocal<T> 로는 비대칭적(asymmetric)인 tls 데이터를 다루기가 어렵기 때문에 AsyncLocal<T>라는 클래스가 별도로 존재한다.
  • AsyncLocal<T>는 스레드풀에서 새로운 다른 스레드를 깨어나게 할 때도 값을 복사시킨다. 이는 ExecutionContext.SuppressFlow() 로 제어가 가능하다.

현재 사용중인 게임서버의 스레드 모델도 승모님의 JobDispatcher와 유사한 Actor 기반 구조를 채택해서 락 없이 구현하고 있다. 지금 서버 구현 기준에서 값이 복사되는 tls 변수가 문제를 일으키는 케이스는 액터를 구현하기 위한 로직 한 군데 뿐이다. 일반적으로 게임 서버를 구현할 때 스레드별로 비대칭적인(asymmetric) tls 변수를 유지해야 하는 경우가 흔치는 않을 것이다. 액터 패턴을 구현한다고 해서 tls 변수가 반드시 필수적인 것도 아니다. 이전 프로젝트에서 tls를 사용하지 않는 액터 구현도 사용해본 적이 있기 때문이다.

하지만 고성능 서버를 목표로 스레드 효율성을 튜닝한다면 반드시 사용을 염두에 두게 되는 도구가 TLS이므로, 본 글에서 언급한 내용을 숙지하고 있으면 성능 튜닝에서 많은 삽질을 세이브 하게 될것이다.

]]>
+ + + + c# + + 고성능 + + 게임서버 + + Thread + + AsyncLocal + + TLS + + + http://leafbird.github.io/devnote/2021/01/01/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-Thread-Local-Storage/#disqus_thread + +
+ + + C# 고성능 서버 - System.IO.Pipeline 도입 후기 + http://leafbird.github.io/devnote/2020/12/27/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-System-IO-Pipeline-%EB%8F%84%EC%9E%85-%ED%9B%84%EA%B8%B0/ + http://leafbird.github.io/devnote/2020/12/27/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-System-IO-Pipeline-%EB%8F%84%EC%9E%85-%ED%9B%84%EA%B8%B0/ + Sun, 27 Dec 2020 08:34:58 GMT + + <img src="/devnote/2020/12/27/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-System-IO-Pipeline-%EB%8F%84%EC%9E%85-%ED%9B%84%EA%B8%B0/00.jpg" class=""> + +<p>2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다. </p> +<p>윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 <a href="https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/hh997032(v=ws.11)">Registerd IO</a> 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다. </p> +<p>하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. <a href="https://docs.microsoft.com/ko-kr/dotnet/standard/io/pipelines">Pipeline</a> 이다.</p> + + + +

2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.

윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.

하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.

System.IO.Pipeline 소개.

pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.

RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer) 외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.

Pipeline이라는 이름을 굉장히 잘 지었다. 이름처럼 메모리 버퍼를 끝없이 연결된 긴 파이프라인처럼 쓸 수 있게 해주는 라이브러리 이기 때문이다. 단위길이 만큼의 버퍼를 계속 이어붙여서 무한하게 이어진 가상의 버퍼를 만드는데, 이걸 너네가 만들면 시간도 오래 걸리고 버그도 넘나 많을테니 우리가 미리 만들었어. 그냥 가져다 쓰렴. 하고 내놓은 것이 Pipeline이다.

(이미지 출처 : devblogs.microsoft.com)

이미지의 초록색 부분은 class Pipe 의 내부 구조를 도식화한다. 일정한 크기의 작은 버퍼들이 링크드 리스트로 연결 되어있다. 내부 구조는 안에 숨겨져있고 외부로는 ReadOnlySequence 타입을 이용해 버퍼간 이음매가 드러나지 않는 seamless한 인터페이스만을 제공한다. 이것이 Pipeline의 핵심이다.

이 외의 디테일한 부분은 Pipeline을 이해하기 쉽게 잘 설명한 MS 블로그의 포스팅이 있어 이것으로 대신한다.

장점 : 불필요한 메모리 복사를 없앤다.

고성능 소켓 IO 구현에 관심이 있는 C++ 프로그래머라면 google protobuf의 ZeroCopyStream 을 이미 접해봤을지 모른다. 그렇다면 Pipeline의 중요한 장점을 쉽게 이해할 것이다. Pipeline의 버퍼 운용 아이디어는 프로토콜 버퍼의 ZeroCopyStream과 유사하기 때문이다. 소켓으로 데이터를 주고 받는 과정에서 발생하는 불필요한 버퍼간 메모리 복사를 최소한으로 줄여주어 성능향상을 꾀한다는 점에서 두 라이브러리가 추구하는 방향은 동일하다.

프로그래밍에 미숙한 개발자가 만든 서버일수록 버퍼간 복사 발생이 빈번하게 발생한다. 커널모드 아래에서 일어나는 소켓버퍼와 NIC 버퍼간의 복사까지는 일단 관두더라도, 최소한 유저모드 위에서의 불필요한 버퍼 복사는 없어야 한다.

전송할 데이터 타입을 버퍼로 직렬화 하면서 한 번 복사하고, 이걸 소켓에다가 send 요청을 하자니 OVERLAPPED에 연결된 버퍼에다가 넣어줘야 해서 추가로 또 복사하고… send 완료 통지 받고 나면 transferred bytes 뒤에 줄서있을 미전송 데이터들을 다시 앞으로 당겨주느라 또 한번 복사가 발생하기 쉽다. recv 받은 뒤에도 메시지 단위 하나 분량 만큼만 읽어 fetching하고 나면 뒤에 남은 데이터들을 버퍼 맨 앞으로 당겨와야겠으니… 여기서 또 한 번 추가복사 하게 될것이다.

서버가 감당할 통신량이 많아질수록 불필요한 복사들이 누적되어 쓸데없이 cpu power를 낭비하게 될텐데, Pipeline의 도입은 이런 부분을 쉽게 해결해 준다. msdn 블로그에서는 Pipeline을 사용하면 복잡한 버퍼 운용 구현을 대신 해결해주니까 프로그래머가 비즈니스 로직의 구현에 좀 더 집중할 수 있게 도와준다고 약을 팔고 설명하고 있다.

장점 : 네트워크 버퍼의 고정길이 제약을 없애준다.

가장 단순하게 소켓 레이어를 구현하면 송/수신용 고정 사이즈 byte[] 버퍼를 각각 하나씩 붙여서 만들게 될 것이다. 대략 구현중인 게임이 어느 정도 사이즈의 패킷을 주고 받는지를 귀납적으로 파악해서 (주로 게임 서버는 작은 사이즈 패킷을 많이 받고, 큰 사이즈 패킷을 많이 보낸다. 로그인할때, 캐릭터 선택할 때 보내는 패킷이 통상 제일 크다) 버퍼의 크기를 눈치껏 결정해서 상수로 고정한다. 버퍼를 거거익선으로 크게크게 잡으면 좋겠지만 대량의 동접을 처리해야 할때 메모리 사용량이 높아져서 부담이 된다. 그러니 적당히 오가는 패킷 사이즈를 봐서 터지지만 않을 정도의 고정길이 버퍼를 걸어두는 식으로 만들게 된다.

이렇게 만들면 불안하다. 컨텐츠를 점점 추가하다가 언젠가 한 두번은 네트워크 버퍼 overflow가 발생해 버퍼 크기를 늘려잡고 다시 빌드해야 하기 일쑤다. 아니면 버퍼를 넘치게 만든 문제 패킷의 구조를 변경하거나 두 개의 패킷으로 쪼개는 등 다이어트를 시켜서 해결할 수도 있겠다. 어느쪽이든 고성능 서버의 네트워크 레이어 구현으로는 적당하지 않은 솔루션이다. 메모리를 더 써서 해결하거나, 개발에 제약(패킷의 최대 크기)을 두어 해결하거나. 모두 석연치 않다.

Pipeline과 ZeroCopyStream 의 무한버퍼 컨셉은 이러한 고정길이 버퍼의 단점을 해결해준다. 처음엔 작은 크기의 버퍼만 가지고 있다가, 공간이 모자라면 추가로 더 할당받아 링크드 리스트 뒤에 붙이기만 하면 된다. 각각의 peer(= single socket)가 실제 사용하는 메모리 공간은 주고받는 데이터의 크기에 따라서 늘어나거나 줄어드는 유연성이 생긴다. 메모리를 효율적으로 사용하면서도 단일 메시지의 사이즈 제약도 없어진다.

단점 : 너무 많은 Task를 생성한다.

위의 두가지 장점만으로 Pipeline의 도입을 시도해볼 가치는 충분했다. 그래서 우리는 게임서버의 수신 버퍼를 Pipeline으로 대체하고, MS Azure 에서 F8s 급 인스턴스 수십대를 동원해 10만 동접 스트레스 테스트를 진행해 보았다.

결과는 기대와 완전히 달랐는데.. Pipeline 도입 전보다 영 더 못한 성능을 보여줬다. 이건 뭐… cpu 사용량이 높고 낮아지는 것이 문제가 아니라, 동접이 일정수치 이상 오르면 서버가 아무 일도 처리하지 않고 멈춰버렸다. 반응없는 프로세스에서 덤프를 떠서 디버거로 살펴보면… 대기상태인 스레드가 잔뜩 생겨있고, 일해야 할 스레드가 부족해서 추가 스레드를 계속해서 만들어내고 있는 것처럼 보였다.

1
2
3
4
5
6
7
8
9
// msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다.
async Task ProcessLinesAsync(Socket socket)
{
var pipe = new Pipe();
Task writing = FillPipeAsync(socket, pipe.Writer);
Task reading = ReadPipeAsync(pipe.Reader);

return Task.WhenAll(reading, writing);
}

원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe 인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task 객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.

task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.

스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.

의문 : Kestrel은 Pipeline 때문에 엄청 빨라졌는데?

(이미지 출처 : stackoverflow.com)

ASP.NET Core는 Pipeline으로 구현한 kestrel 웹서버에서 실행할 때 기존의 iis 기반보다 훨씬 더 향상된 퍼포먼스를 보여준다. Pipeline의 버퍼 운용 효율성으로 인한 이득을 제대로 누리고 있는 것이다. kestrel의 뛰어난 성능 결과를 보여주는 여러 벤치마크 결과들 덕분에 나도 기대를 가득 안고 서둘러 Pipeline을 도입하고 테스트 해보았으나.. 결과는 좋지 않았다.

그럼 우리 게임서버에 도입한 테스트 결과는 왜 이리 처참한 것인가? ms 형들이 잘못 만들었을 리는 없으니 내가 가져다 붙이는 과정에 문제가 있었던 것인가?

차이가 생기는 원인은 Kestrel은 http 통신을 하는 웹서버이고, 우리의 게임서버는 연결을 유지하고 있는 TCP 서버이기 때문이다. Kestrel은 통신량의 거의 전부가 socket이 열린 채로 길게 대기할 필요가 없기 때문에, task을 소켓의 2배수나 4배수만큼 오래도록 유지하고 있을 이유 자체가 없다. 그래서 단점으로 지적한 waiting task가 kestrel에서는 발생하지 않는다. 상술했던 단점을 다시 표현해 보자면 Pipeline의 사용시 기본적으로 task 대기가 발생하는 것을 성능 하락의 원인으로 볼 수 있지만, 이 task들의 수명 혹은 대기시간이 상당히 길다는 점과 함께 만나면 성능을 더욱 악화시키는 원인이 된다. Kestrel의 단명하는(?) 소켓들과 task들은 Pipeline와 함께 사용되면서 충분히 좋은 성능을 가져다 줄 것이다. 수많은 벤치마킹 결과들이 증명하듯이.

대안 : 불필요한 복사가 없는 가변버퍼를 직접 만들자.

우리는 게임서버에서 Pipeline을 다시 드러냈다. http와 유사하게 single pair request/response 통신 후 소켓을 닫아도 되는 경우가 아니면 Pipeline으로 성능상의 혜택을 보기는 힘들다고 판단했기 때문이다. 그래도 불필요한 메모리복사는 만들고 싶지 않으니 메모리 버퍼 운용하는 부분만 직접 구현해 사용하기로 했다.

클래스 이름이 Pipeline과 protobuf를 모두 가져다 섞어놓은 느낌이 들겠지만 착각일 뿐이다. 두 api를 모두 사용해본 경험의 영향을 받긴 했지만… *Stream.cs 클래스들은 실제로 System.IO.Stream을 상속받아서 이름이 좀 비슷해졌다. 이 Stream 구현들이 단위버퍼들간의 연결을 seamless하게 쓸 수 있게해주는 역할을 한다. 주요 구현을 담고 있으나 사용계층에 노출될 필요는 없기 때문에 Detail 아래로 숨겨두었다. 사용자는 부모타입인 Stream 추상 클래스만 보게 된다.

인터페이스로 ReadOnlySequence<T>를 사용하지 않은 이유는 이 구현을 Unity3D로 만든 클라이언트에서도 똑같이 사용하기 위해서였다. 현시점 유니티의 mono framework가 지원하는 C# 문법 버전이 낮아서 ReadOnlySequence<T>를 지원하지 않기 때문이다. 그런데 Stream 을 이용해도 어렵지 않게 seamless 를 구현할 수 있었고, 실제 사용하기에도 스트림 형태가 훨씬 익숙하고 편해서 결과적으로는 더 만족스러운 선택이었다. ReadOnlySequence<T> 가 뭔지 모르는 프로그래머도 Stream은 알고 있을 것이다.

실제 사용 계층으로 노출하는 클래스는 아래의 세 클래스 만으로 정리했다.

  • MemoryPipe : 소켓 수신버퍼 처리 전용. System.IO.Pipeline과 유사하다.
  • SendBuffer : 소켓 송신버퍼 처리 전용.
  • ZeroCopyBuffer : 네트워크 버퍼가 아닌 범용적인 용도의 인터페이스.

패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.

이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Cs.ServerEngine.Network.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;

public int SegmentCount => this.segments.Count;

public int CalcTotalSize()
{
int result = 0;
foreach (var data in this.segments)
{
result += data.DataSize;
}

return result;
}

public BinaryWriter GetWriter() => new BinaryWriter(new ZeroCopyOutputStream(this));
public BinaryReader GetReader() => new BinaryReader(new ZeroCopyInputStream(this));

internal void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if (this.last == null || this.last.IsFull)
{
this.last = LohSegment.Create(LohPool.SegmentSize.Size4k);
this.segments.Enqueue(this.last);
}

int copied = this.last.AddData(buffer, offset, count);

offset += copied;
count -= copied;
}
}

internal LohSegment[] Move()
{
var result = this.segments.ToArray();
this.segments.Clear();
this.last = null;

return result;
}

internal LohSegment Peek()
{
return this.segments.Peek();
}

internal void PopHeadSegment()
{
var segment = this.segments.Dequeue();
segment.ToRecycleBin();

if (this.segments.Count == 0)
{
this.last = null;
}
}
}
}

본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment> 가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.

조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.

이렇게 하니 ZeroCopyBuffer는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline의 유사품이 되었다. 제공되는 메서드 중에는 async method 가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.

마치면서

System.IO.Pipeline은 ASP.NET Core의 성능을 크게 끌어올린 네트워크 버퍼 운용 라이브러리다. 이를 적용하면 네트워크 버퍼구현의 여러가지 문제점들과 boilerplate한 구현들을 손쉽게 해결할 수 있으나, 최소 2 tasks/peer를 소켓의 수명만큼 열어두어야 하기 때문에 소켓을 긴 시간 유지하는 타입의 TCP서버라면 도입 전에 신중한 성능 테스트를 거쳐야 한다.

사이즈가 무한인 가상의 버퍼라는 컨셉만을 가져와 직접 만들어 사용중인 ZeroCopyBuffer 모듈의 인터페이스도 간단하게 소개해 보았다. Unity3D 클라이언트 네트워크 모듈에도 함께 사용하기 위해 ReadOnlySequence<T> 대신 System.IO.Stream으로 추상화한 인터페이스를 제공했는데, 이렇게 하니 요구사항을 충분히 만족하면서도 사용 계층에게는 더 익숙한 형태의 인터페이스를 제공할 수 있어서 만족스러웠다.

본 포스팅에는 단위버퍼로 이용한 구현체인 LohSegment에 대한 소개가 없었다. 글 분량 조절에 실패하여 일부로 언급하지 않았는데, 다음에 가비지 컬렉터를 주제로 포스팅하면서 추가로 다뤄볼 예정이다.

참고:

]]>
+ + + + c# + + 고성능 + + 게임서버 + + Network + + Socket + + Pipeline + + + http://leafbird.github.io/devnote/2020/12/27/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-System-IO-Pipeline-%EB%8F%84%EC%9E%85-%ED%9B%84%EA%B8%B0/#disqus_thread + +
+ + + C# 고성능 서버 - __FILE__, __LINE__ 대체제 + http://leafbird.github.io/devnote/2020/12/26/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-FILE-LINE-%EB%8C%80%EC%B2%B4%EC%A0%9C/ + http://leafbird.github.io/devnote/2020/12/26/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-FILE-LINE-%EB%8C%80%EC%B2%B4%EC%A0%9C/ + Sat, 26 Dec 2020 02:11:05 GMT + + <p>C++에서 가장 기본적으로 사용했던 <code>__FILE__, __LINE__, __FUNCTION__</code> 등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.<br>UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.</p> +<p>성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다. </p> + + + + C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__ 등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.

성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.

콜스택을 얻어와서 가장 마지막 함수를 찍는 방법

현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.

1
2
3
4
5
6
7
8
9
StackTrace st = new StackTrace(new StackFrame(true)); 

Console.WriteLine(" Stack trace for current level: {0}", st.ToString());

StackFrame sf = st.GetFrame(0);
Console.WriteLine(" File: {0}", sf.GetFileName());
Console.WriteLine(" Method: {0}", sf.GetMethod().Name);
Console.WriteLine(" Line Number: {0}", sf.GetFileLineNumber());
Console.WriteLine(" Column Number: {0}", sf.GetFileColumnNumber());

C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.

콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음

.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.

콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.

지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.

Log4Net, NLog 모두 아주 좋은 로그 모듈인 것은 분명하다. Log4Net은 apache 소프트웨어 재단의 모듈인 만큼 아주 많은 곳에서 쓰이고 있을것이다. 두 모듈 모두 설정 문서만 읽어봐도 정말 기능이 많다. 로그파일을 사이즈나 시간에 맞춰 새 파일로 나눠주는 것은 물론이고, 메일로 로그를 전송할 수도 있고, 로그 레벨 설정도 자유롭고, 파일 생성 정책도 디테일하게 조절할 수 있고… 아무튼 아주 많다.

내가 이 두 모듈을 떠나서 직접 만들어 사용하는 가장 큰 이유는 성능 때문이다. 나에게는 굳이 내가 사용도 하지 않을 것 같은 다수의 편의기능들보다도 딱 내가 필요한 동작만 가지고 있더라도 가볍고 빠른 로그 모듈이 필요했다. Log4Net은 오래되서 잘 기억이 나지 않지만 NLog같은 경우 모듈 자체에서 스레드도 제법 많이 만들어서 운용하는걸 디버깅하다 본 기억이 있는데, 이런 내부 구조도 고성능 엔진을 만든다는 측면에서 부담스러웠다. (고성능을 위한 File IO 전략은 이 글의 주제에서 벗어나니까 다음 기회에 별도의 포스트로 다뤄보겠다.)

범용적인 로그 모듈들은 성능 또한 일반적이다. 크게 좋지도 않고 아주 나쁘지도 않는 수준을 보여준다. NLog를 사용할 때 설정에서 파일 이름과 라인 위치를 출력하는 동작을 끈 채로 사용해도 성능에는 별반 차이가 없었는데, 아마도 파일로 출력만 하지 않을 뿐 내부에서는 동일하게 StacFrame 을 얻어오는 동작이 실행되고 있을거라고 추측했다. 혹은 StackFrame 때문이 아닌, 다른 많은 부수 기능들 때문일 수도 있을 텐데, 아무튼 나의 기대치에는 맞지 않았다.

System.Runtime.CompilerServices

.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void DoProcessing()
{
TraceMessage("Something happened.");
}

public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}

함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에 채워진다.

과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.

CompilerServices 장점 : 가볍고 빠르다.

위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__ 은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.

CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.

1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
WriteLog("invalid value:{0}", value); // 불가능합니다.
}

public void WriteLog(string format,
params object[] list,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}

함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.

하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라

1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
// WriteLog("invalid value:{0}", value); // C++스러워 보이지만, 촌스러운 방식이예요.
WriteLog($"invalid value:{value}"); // 가능합니다. 권장됩니다. Effective C# 읽어보세요.
}

public void WriteLog(string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}

C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.

C++은 코드영역을 사용하지만, C#은 힙을 사용한다.

좀 더 성능에 집착해보자(?).

윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__ 은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.

(이미지 출처 : wikipedia)

이미지에서 text로 표현된 부분이 코드영역이다. 이 공간은 고정적인 읽기 전용의 공간이다. C++의 __FILE__ 매크로를 다르게 표현하면 결국 이 코드영역의 특정 위치를 가르키는 char*로 변환될 뿐이다. 추가적인 객체 할당은 없다.

하지만 C#은 코드영역을 사용하지 않는다. [CallerFilepath] string filePath함수 호출이 일어날 때마다 heap 영역에 스트링 객체를 할당한다. 디버그를 위해 상세하게 로그를 달면 달 수록 heap에는 동일한 텍스트가 반복적으로 할당되어 메모리에 압력을 가하게 된다.

C#에서는 C++처럼 코드영역을 참조하는 문자열을 만드는 방법이 없다. 모든 참조형식의 객체는 heap이 아닌 공간을 사용할 수 없기 때문으로 추측이 된다. value type을 object 형식으로 가리키면 굳이 비싼 비용을 들이면서까지 heap에 추가할당을 만드는 boxing을 하는 이유와 같을 것이다.

반복적으로 사용하는 똑같은 문자열인데도, 매번 함수가 불릴 때마다 이걸 heap에 재할당을 할까? 하고 나도 처음엔 그렇게 생각했다. C++을 하면서 생긴 사고의 관성일 것이다. C#의 string은 참조 타입이고, immutable해서 한 번 할당하면 변경도 불가한 성격을 갖고 있기 때문에 충분히 착각할 만한 상황이기도 하다 - 라고 자기 합리화를 해본다. 하지만 windbg를 이용해 heap을 디버깅 하던 중 무수히 많은 파일 경로 텍스트가 중복으로 잔뜩 들어있는걸 보고 나서야 아닌 것을 깨달았다.

Interned String

완전하게 내용이 같은 string을 pooling하여 heap에 한 번만 할당하고 돌려쓰는 방법이 없는 것은 아니다. 이렇게 언어 자체적으로 문자열을 풀링하는 처리를 Java와 C#에서는 모두 Interning이라고 부른다.

사용법은 간단하다. 풀링하고 싶은 문자열을 사용할 때 string.Intern() 메소드를 한 번 더 감싸주면 된다. 현재 회사에서 실제 사용중인 모듈의 인터페이스 부분만 보면 아래처럼 되어있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.IO;
using System.Runtime.CompilerServices;

public static class Log
{
public static void Debug(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
{
// ... 중략...
provider.Debug($"{message} ({BuildTag(file, line)})");
}

private static string BuildTag(string file, int line)
{
return string.Intern($"{Path.GetFileName(file)}:{line.ToString()}");
}
}

전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.

1
2
3
4
5
2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32)
2020-12-21 12:08:02.145 [Info] [Send] [20017] kREGISTER_GAME_SERVER_REQ actionId:3 (SerializableExt.cs:92)
2020-12-21 12:08:02.205 [Info] db connection Initialized. type:Auth server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.221 [Info] db connection Initialized. type:Contents server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.238 [Info] db connection Initialized. type:Game server:localhost count:16 (DbPool.cs:40)

interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.

C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.

마치면서

로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.

  • 함수 호출자 정보를 얻고 싶을 땐 StackFrame 사용 보다 CompileServices 하위 어트리뷰트를 쓰는게 낫다.
  • C#은 모든 문자열을 항상 heap에 할당한다. 심지어 literal text같은 상수 문자열을 사용한다 하더라도 메모리 코드영역의 직접 참조가 불가능하다.
  • 로그를 찍을 때마다 heap에 불필요한 객체 할당이 발생하는 것을 줄이고 싶다면 문자열을 Interning하면 된다.
]]>
+ + + + c# + + 고성능 + + 게임서버 + + 메모리 + + string interning + + + http://leafbird.github.io/devnote/2020/12/26/C-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%84%9C%EB%B2%84-FILE-LINE-%EB%8C%80%EC%B2%B4%EC%A0%9C/#disqus_thread + +
+ + + 테크니컬 리더십: 시작하기 + http://leafbird.github.io/devnote/2018/11/12/%ED%85%8C%ED%81%AC%EB%8B%88%EC%BB%AC-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/ + http://leafbird.github.io/devnote/2018/11/12/%ED%85%8C%ED%81%AC%EB%8B%88%EC%BB%AC-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/ + Mon, 12 Nov 2018 14:05:29 GMT + + <p>예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.<br>원문은 슬랙 개발 블로그의 <a href="https://slack.engineering/technical-leadership-getting-started-e5161b1bf85c">Technical Leadership: Getting Started</a>라는 글입니다.<br>번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요. </p> +<img src="/devnote/2018/11/12/%ED%85%8C%ED%81%AC%EB%8B%88%EC%BB%AC-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/00.png" class="" title="개발 실무자의 리더십은 스스로를 리딩하는 것에서 시작한다"> + + + + 예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.

테크니컬 리더십: 시작하기

내가 소프트웨어 엔지니어가 되기 전에는 이 직업에서 가장 중요한 점은 코딩이라고 생각했다. 그것은 잘못된 생각이었고, 소프트웨어 공학의 가장 중요한(그리고 가장 어려운)점은 다른 사람들과 원만하게 잘 협력하는 것이다.

나는 “관리자는 되지 않을거야!”라고 스스로에게 말해왔고, “그렇게 하면, 내 모든 에너지를 개발에만 집중시킬 수 있을거야!” 라고 생각했다. 내 이후의 경력도 기술 지향적인 실무자 위주로만 관리해 간다면 이 어려운 대인관계를 어느 정도 무시할 수 있을 거라고 생각했다.

빨리 가려거든 혼자 가고, 멀리 가려거든 함께 가라.

내가 업무에서 대인관계를 소홀히 여기던 때 의아하게 생각했던 점은 “왜 사람들은 나의 의견을 들어주지 않지?” 하는 점이었다. 이는 슬랙(Slack)의 플랫폼 팀에서 처음 작업을 시작했을 때 특히 그러했다. 나는 슬랙의 API가 토큰을 사용하고 있는 점을 변경하여 보안을 강화하고, 제품 개발팀 전체에 걸쳐 일관된 개발 과정을 유지하도록 개선하고 싶었다. 그러나 몇 달 동안, 나의 제안이 많은 이들의 시간을 보다 가치있게 활용할 수 있는 방법이라고 PM이나 팀원들을 설득하는 것은 불가능했다.

이후로도 몇 차례 나의 의견은 받아들여지지 않고, 같은 팀 수석 엔지니어들의 의견이 채택되는 것을 지켜보면서 내게 무언가 빠진 요소가 있다는 것을 알게 되었는데, 그것은 바로 ‘리더십’이었다. 나는 매일같이 키보드에만 코를 박고 있으면 안되는 것이었다. 내가 성장하기를 원한다면, 다른 사람들이 나와 동등한 수준으로 기여할 수 있도록 도움을 주어야 했던 것이다. 나는 리더십을 통해 나의 영향력을 키워야 할 필요가 있었다.

이 글을 통해 필자 스스로가 리더십에 대해 배운 점과, 개발자 리더십의 절차(Path)에 대해 이야기해 보고자 한다.

자기 자신을 리딩하기

슬랙의 엔지니어로 지내면서, 나는 관리(management)와 리더십(leadership)이 어떻게 다른지 이해하게 되었다.

관리자(manager)는 자신의 보고서에 대한 책임이 있다. 관리자들은 코칭과 구조화를 통해 좋은 팀을 구축하는 것에 중점을 둔다. 또한 팀의 성장을 위해 성과를 관리한다.

관리자(manager)는 종종 리더(leader)를 겸임하지만, 리딩은 사실 다른 누구라도 할 수 있는 별개의 것이다. 리딩은 권위에 의존하는 무언가가 아니라, 다른 사람에게 미치는 영향력에 대한 것이다. 리딩은 비전에 대해 소통하고, 비전을 실현하기 위해 다른 이들에게 힘을 실어주는 것이다.

당신은 다른 이들을 리드하기 전에, 먼저 당신 자신을 리드할 수 있어야 한다. 자신을 리딩하는 것은 타인을 리드하거나, 조직을 리드하기 전에 반드시 먼저 선행되어야 한다. 자신을 리딩한다는 개념은 다양한 분야와 기업에서 정리한 여러 리더십의 정의들에서 찾아볼 수 있다.

자기 자신을 리딩하는 것은 그 사람의 우수한 역량과 밀접한 관련이 있다. 모범적인 자세를 통해 드러나는 리더십은 타인에게 자극을 주는 가장 강력한 방법이기 때문이다. 자신을 리딩한다는 말은 늘 최선을 다해서 개인의 업무를 수행하고, 스스로가 만들어내는 결과물의 품질에 대해 책임을 지는 것을 의미한다.

자기 자신에게 성공적인 리더십을 발휘하기 위한 다섯하기 요소는 방향 맞추기, 전문가 되기, 공유하기, 일관되게 실행하기, 효과적인 의사소통하기 이다.

방향 맞추기(Finding Alignment)

직장에서 우수함을 나타내려면 먼저, 팀을 이해해야 하고, 회사를 이해해야 한다.

‘원칙’이란 어떤 행동이 바람직한지, 혹은 바람직하지 않은지를 안내하는 회사의 규범을 말한다. 대개는 이런 원칙들이 명확하게 규정되지 않은 경우도 많은데, 이런 숨은 원칙을 잘 찾아내는 것 역시 개인의 몫이다. 이 원칙들은 당신의 나침반과도 같다. 원칙들은 당신이 회사의 목표와 가치에 맞는 결정을 내리는데 큰 도움을 줄 것이다.

슬랙에서의 예를 들어보면, 우리는 슬랙의 사용자들에게 매우 뛰어난 사용 경험을 제공하고 있다는 믿음이 있다. 어떤 고객이 슬랙의 핵심 기능 중의 하나가 망가졌다는 제보를 한다면, 나에게는 그 즉시 내가 하고 있던 일을 모두 멈추고 현상을 확인해 즉시 문제를 해결하는 것이 가장 중요하다. 하지만 다른 회사에서는 내가 하던 일을 내팽개치는 것이 완전히 잘못된 판단이 될 수도 있는 것이다.

대부분의 결정은 여러가지 가치를 두고 다각도로 고민하면서 내려져야 한다. 오늘은 그동안 쌓아둔 기술 부채를 해결하는데 시간을 쓸 것인가? 아니면 좀 더 미루고 내일의 작업을 위한 기반작업을 할 것인가? 버그를 잡는 것, 툴을 만드는 것, 새로운 기능을 개발하는 것이 더 중요하진 않은가? 직장에서 할애할 수 있는 총 시간과 에너지의 양은 제한되어 있다. 회사가 중요하게 생각하는 것과 개인이 노력을 기울이는 방향을 동일하게 맞출 때 당신의 기여도는 가장 최대의 효율을 발휘할 것이다.

방향성 맞추기는 단지 회사가 당신에게 바라는 일을 수행하는 것만을 뜻하지 않는다. 여러분들은 리더로서 여러가지 문제를 직면하고, 이를 해결하기 위한 (숨어있는) 솔루션을 제시할 숱한 기회들을 마주하게 될 것이다. 하지만 그 때마다 다른 동료들에게 이것이 왜 문제이며, 왜 이를 해결하기 위해 에너지를 써야 하는가를 납득시키기 위해서는 먼저 회사가 무엇을 중요하게 생각하는지를 이해하고 다른사람에게 잘 설명할 수 있어야 한다.

전문가 되기(Become an Expert)

전문가가 되는 것은 개인 스킬을 연마하는 것에 관한 이야기다. 잠재력을 가진 상태라는 것이 하나의 좋은 자질일 순 있겠지만, 그걸로는 충분하지 않다. 리더는 실제로 뛰어난 전문가(export)여야 한다. 콜로라도 대학의 앤더스 에릭슨 교수에 따르면, 전문가가 되기 위해서는 평균 10년 이상 높은 수준의 의식적인 노력을 10,000시간 이상 기울여야 한다고 말한다.

사람들은 종종 내가 오페라를 불렀던 경험이 소프트웨어 공학 경력에 도움이 되는지를 묻곤한다. 맞다! 음악을 통해서 나는 스스로의 마음가짐을 발전시킬 수 있었다. 아리아를 연습할 때면 가장 자신 없는 파트를 제일 자신있는 파트만큼의 자연 스러운 소리가 나올 때까지 몇시간이고 반복해서 연습했다. 소프트웨어 공학도 이것과 똑같다. 우리는 자신이 취약한 부분을 개발하는데 더욱 많은 시간을 투자해야 한다.

숙련을 쌓는 방법에 지름길이란 없다. 다만 꾸준하고 의식적인 노력으로 개발시키는 것 뿐이다. 내 자신에게(그리고 당신 주변의 사람들에게) 질문을 던져보자: 내가 가장 크게 성장할 수 있는 분야는 무엇인가? 전문가가 되기 위해서 나는 어떤 스킬을 개발해야 하는가?

당신이 개발하기 원하는 많은 스킬들이 있을 수 있지만, 노력을 기울이기 전에 먼저 다음의 질문을 던져보기를 권장한다: 그 스킬은 회사가 추구하는 방향에 부합하는가? 그 스킬은 나의 개인적인 목표에도 부합하는가?

‘아직 아무것도 이룬 것이 없다’는 생각만 하고 있을 게 아니라 매일 꾸준히 지식과 스킬을 체득하고자 노력하는 과정이 필요하다. 누구나 태어날 때부터 전문가였던 사람은 없다.

공유하기(Share)

자기 자신을 리딩하는 과정이 지나면, 다른 사람을 리드할 기회가 주어지고, 당신의 동료들이 최고의 성과를 내도록 역할을 부여하게 된다. 이를 성공적으로 수행하기 위해서는 먼저 지식을 공유해야 한다.

스킬을 습득하기 위해 많은 개인 시간을 소비한 후라면 선뜻 지식을 공유하는 것이 쉽지 않을 수도 있다. 특별한 전문성을 혼자만 “소유”하고 싶은 것은 본능적인 생각이다. 전문 지식은 체득 과정의 노력이 보이지 않을 땐 마치 마술처럼 느껴질 수도 있다. 당신은 혼자만의 마법을 비밀 상자에 숨겨놓고 외딴 곳에 보관하고 있다가 필요할 때만 꺼내서 사용하고 싶어할 수 있다. 다른 사람들은 그걸 어떻게 하는지 모르기 때문에, 당신만의 전문성은 여전히 마법을 유지하게 될 것이다.

하지만 바로 이 부분이 핵심이다. 당신의 노하우를 혼자만 알고 있으면 동료들은 당신에게 의존하게 되고, 결국 동료들의 성장을 방해하는 셈이 된다. 당신 스스로도 새로운 일을 배우는 것을 불안하다고 여기게 되어, 자신의 성장마저 방해하는 셈이 된다. 당신은 동료들이 팀에 기여하는 것을 막고 있으며, 팀을 아주 적극적으로 망치고 있는 셈이다.

나도 내가 가진 정보를 혼자만 유지하곤 했는데, 일부러 숨기고자 해서 그랬다기 보다는 이것이 유익한 정보인지 깨닫지 못했던 경우였다. 예를 들어, 나의 프로젝트에서는 업무의 진행을 방해하는 일반적인 문제점들에 대해 탐구하고 정리해왔다: 킥오프, 최종 마일스톤, 회귀 없는 릴리즈 같은 것들(역주: 예시의 내용들이 무엇을 말하는지는 잘 모르겠습니다). 나는 주위 동료들도 함께 성공했으면 하는 마음에 내가 유지하던 정보들 중 다른 팀들과 공유할 수 있는 기술들을 분류하기 시작했다. 사실 내 프로젝트만 잘 돌아가면 상관 없는 일이었지만.. 그것은 추가 확장이 없는 x1배의 영향력이다. 허나 이런 정보들은 모든 팀들에게 적용 가능한 것들이었고, 이것은 xN 배의 영향력을 발휘하게 된다.

지식을 숨기는 대신 공유하라. 멘토링이나 페어 프로그래밍 같은 1:1 방식도 좋고, 프레젠테이션이나 문서화 같은 1:N 방식도 좋다. 당신이 배운 사실을 다른 사람들에게도 가르쳐라. 그럼 다른 사람들은 다시 그 다음 사람들을 가르칠 것이다. 당신은 다시 배우고자 하는 그 다음 스킬로 자유롭게 이동할 수 있다. 지식이란 마르지 않는 샘이다. 아무리 배워도 항상 더 많이 남아있다.

일관되게 실행하기(Execute Consistently)

일전에 나의 관리자와 나눴던 대화가 기억난다. 나는 관리자에게 최근의 프로젝트에서 내가 매우 뛰어난 성과를 기록했다고 말하고, 내가 언제쯤 승진할 수 있느냐고 질문했다. 그는 현명하게 대답했다: “당신은 이번과 같은 좋은 성과를 일관되고 꾸준하게 달성할 수 있음을 증명해야 합니다.”

일관성. 그것은 일시적인 운과 리더십의 차이를 말해준다.

당신이 어느 한가지 일을 딱 한 번 잘해냈다는 것은 별로 중요하지 않다. 정말 중요한 것은 당신이 그 일을 다시, 또 다시, 그리고 또 다시 잘 해낼 수 있는가 하는 것이다.

일관성 있는 실행력을 갖기 위해서는, 다양한 규모와 유형의 여러가지 프로젝트를 해봐야 할 것이다. 작은 규모, 큰 규모, 복합적인 기능, 사용자 친화적 UX, 백엔드 솔루션 등등. 이러한 경험들에서 당신은 다양한 도전 과제를 마주하고 해결 방안들을 개발하게 된다. 당신의 약점이 무엇인지를 드러내주고 당신이 스킬을 연마하도록 도울것이다.

당신의 관리자에게, 당신이 익히려고 하는 기술들을 미리 공유하라. 앞으로 맡게 될 프로젝트를 주시하고 그 중에 자신이 흥미가 가는 부분이 무엇이며 왜 그렇게 생각하는지를 관리자에게 미리 알려라. 당신이 지금 프로젝트를 진행중이라면, 작업하는 동안 나는 어떤 스킬을 선정해 발전시켜갈 것인가에 대해 생각하라. 이것은 직장에서의 시간을 최대의 효율로 활용하는데 큰 도움을 줄 것이다.

때로는 당신이 크게 열정을 느끼지 못하지만 팀의 임무에는 중요한(mission-critical)일에 배정이 될 때도 있다. 당신은 이 또한 잘 해낼 수 있음을 증명해야 한다.

일관되게 실행하는 것은 개인의 브랜드를 개발시키고 동료들에게 신뢰를 쌓을 수 있는 방법이다. 신뢰감을 형성하고 키우는 데에는 많은 시간과 경험이 필요하다. 하룻밤 만에 만들어지지 않는다. 한 번 신뢰를 얻었다 하더라도 지속적인 노력이 뒤따라야만 이를 오래도록 유지할 수 있다.

효과적인 의사소통하기(Communicate Effectively)

“왜 사람들이 내 말을 들어주지 않는거야?” 하고 궁금해 한 적이 있는가?

나는 신입일 때 여러 차례 위와 같은 질문을 하곤 했다. 그러던 어느날 문득 내가 성장의 준비가 되었을 즈음에, 사장님이 중요한 단서를 주었다: 나는 동료들에게 부정적인 성향으로 인식되고 있었다는 점이다. 처음엔 그 피드백을 듣고 기분이 상했다. 하지만 이것이 나의 경력에서 중요한 전환점이 되었다. 그 후로 나는 ‘목소리’ 코치와 함께 일하게 되었고, 효과적인 커뮤니케이션의 중요한 비밀을 깨닫게 되었다. 그것은 경청(listening)이다.

경청이란 단순히 정보를 받아들이는 것이 아니다. 경청은 정보와 함께 그것의 맥락을 모두 합쳐 하나의 덩어리로 합성하는 것이다. 경청은 상대방의 의견이 어디에서 왔는지를 이해하고, 더 깊은 이해를 얻기 위해 명확한 질문을 던지는 것을 말한다. 이 합성의 듣기는 효과적인 커뮤니케이션의 가장 기본임과 동시에, 당신이 말하고자 하는 아이디어에도 엄청난 힘을 실어준다 - 믿거나 말거나.

레벨이 올라감에 따라 관리자 트랙과 엔지니어 트랙에게는 모두 동일한 의사소통 기술이 요구됩니다. 각 트랙의 진정한 능력자들이 서로 다른 트랙의 능력자를 존재할 수 있게 만듭니다. - Sarah Mei

효과적인 의사소통의 또 다른 측면은 적절한 맥락으로 반복하는 것이다. 사람들이 왜 내 말에 귀기울이지 않는지 몰랐을 때의 나는 했던 말을 다시 반복해야 할 때면 화를 내면서 말했다.

나중에서야 효과적인 의사소통의 고수들을 관찰하기 시작했다. 그들은 다방면으로 정보를 노출한다. 적절한 시간 간격을 두고 반복적으로 정보를 전달하고, 듣는 사람이 누군가에 따라 그에 맞는 다양한 세부 정보들을 제공한다.

정보를 듣고 종합하고, 효과적으로 공유하는 방법을 익히는 것은 직급에서 오는 권위에 의존하지 않고 사람들에게 영향을 미치는 기본적 기술이다. 모두가 하나의 비전을 바라하도록 사람들을 모으기 위해서는 이러한 영향력이 필요하다.

. . .

소프트웨어 엔지니어로 일을 시작할 때, 왜 나의 아이디어가 회사에서-그리고 업계에서-잘 받아들여지지 않는 것인가를 궁금해했다. 그러던 중 컴퓨터만 골똘히 들여다보던 시선을 잠시 벗어나, 주변의 훌륭한 동료들을 만나보게 되면서 깨달았다. 내가 생각하는 방향성을 다른 사람들이 함께 공감하고, 실현하기 위해 같이 노력하도록 동기부여할 수 있다면 훨씬 더 큰 영향력을 미칠 수 있다는 것을.

리더십에 관해서는 배워야 할 것이 많고, 필자 개인적으로는 더 많은 것들을 배워야 한다. 리더급 개발자가 되고자 한다면, 먼저 자기 자신을 리딩하는 것부터 시작하기를 권한다. 이 외에 당신이 찾아낸 리더십에 대해 내게도 알려주길 바란다!

]]>
+ + + + 리더십 + + + http://leafbird.github.io/devnote/2018/11/12/%ED%85%8C%ED%81%AC%EB%8B%88%EC%BB%AC-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/#disqus_thread + +
+ + + C++ 코드 정리 자동화 - 3. pch 사이즈 확인, #include 순서정리 + http://leafbird.github.io/devnote/2014/09/30/cleanup-cpp-project-3rd/ + http://leafbird.github.io/devnote/2014/09/30/cleanup-cpp-project-3rd/ + Tue, 30 Sep 2014 06:17:15 GMT + + <h2 id="pch-파일-사이즈"><a href="#pch-파일-사이즈" class="headerlink" title="pch 파일 사이즈"></a>pch 파일 사이즈</h2><p>팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.</p> + + + + pch 파일 사이즈

팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.

MSBuild로 단일 cpp 파일을 컴파일하면 이런 메시지가 나오는데,

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
27
28
29
30
31
32
33
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\CL.exe 
/c
/ID:\Dev\uni\External\
/ID:\Dev\uni\Test\
/ID:\Dev\uni\
/Zi
/nologo
/W4
/WX
/sdl
/Od
/D WIN32
/D _DEBUG
/D _CONSOLE
/D _LIB
/D _UNICODE
/D UNICODE
/Gm
/EHsc
/RTC1
/MDd
/GS
/fp:precise
/Zc:wchar_t
/Zc:forScope
/Yc"stdafx.h"
/Fp"x64\Debug\unittest.pch"
/Fo"x64\Debug\\"
/Fd"x64\Debug\vc120.pdb"
/Gd
/TP
/errorReport:queue
stdafx.cpp

여기 cl.exe로 들어가는 인자 중에 /Fp"x64\Debug\unittest.pch" 요 부분에 pch 경로가 있음. 그러니까 결국 툴에서 pch사이즈를 구하려면

  1. 프로젝트 리빌드하고
  2. pch 생성 헤더를 cl.exe로 컴파일하면서 /Fp 스위치를 읽어 경로 파악.
  3. 위에서 새로 생성된 pch파일의 사이즈를 확인.

… 해주면 된다.

#include 순서 자동 정렬

구글의 C++ 스타일 가이드 문서 중에 include 의 이름과 순서 항목에 보면 헤더 인클루드에 몇가지 카테고리와 순서를 정해 두었는데,

주된 목적이 dir2/foo2.h에 있는 것들을 구현하거나 테스트하기 위한 dir/foo.cc나 dir/foo_test.cc에서 include를 아래처럼 순서에 따라 배열하라.

  1. dir2/foo2.h (아래 설명 참조).
  2. C 시스템 파일
  3. C++ 시스템 파일
  4. 다른 라이브러리의 .h 파일
  5. 현재 프로젝트의 .h 파일

팀에서 정한 컨벤션도 이 규칙을 그대로 따라야 해서.. 매번 코딩할 때마다 인클루드 순서에 신경쓰기 싫어서 자동화 처리를 작성. 더불어 경로 없이 파일명만 적은 경우나 상대경로를 사용한 인클루드도 지정된 path를 모두 적어주도록 컨버팅하는 처리도 만듦. 만드는 과정이야 대단한 건 없다. sln, vcxproj파일 파싱하는 것은 만들어 두었으니, 그냥 스트링 처리만 좀 더 해주면 금방 만들어진다. 툴로 sorting하고나면 아래처럼 만들어줌.

TestCode.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"
#include "TestAsset/ProjRoot/TestCode.h"

// system headers
#include <vector>

// other project's headers
#include "TestAsset/OuterProject.h"
#include "TestAsset/OuterProjectX.h"

// inner project's headers
#include "TestAsset/ProjRoot/InterProject.h"
#include "TestAsset/ProjRoot/InterProjectA.h"
#include "TestAsset/ProjRoot/InterProjectB.h"
#include "TestAsset/ProjRoot/InterProjectC.h"

void main() {
return;
}

epilog

대충 이정도 돌아가는 툴을 만들어서 개인 pc에 셋팅해둔 jenkins에 물려놓고 사용중. 원래는 필요없는 include찾아주는 기능만 만들려다가 include sorting 기능은 그냥 한 번 추가나 해볼까 싶어 넣은건데, 아주 편하다. 코딩할 땐 순서 상관 없이 상대경로로 대충 넣어놓고 툴을 돌리면 컨벤션에 맞게 예쁘게 수정해준다.

불필요 인클루드를 찾는 동작은 회사 코드 기준으로 컨텐츠 코드 전체 검색시 50분 정도 걸리는 듯. 이건 매일 새벽에 jenkins가 한 번씩 돌려놓게 해놓고, 매일 아침에 출근해서 확인한다.

pch사이즈는 baseline 구축을 생각하고 만들어 본건데.. (박일, 사례로 살펴보는 디버깅 참고) baseline을 만들려면 지표들을 좀 더 모아야 하고, db도 붙여야 하니 이건 제대로 만들려면 시간이 필요할 것 같다(..라고 쓰고 ‘더이상 업데이트 되지 않는다’ 라고 읽는다.)

그리고 C#.

C#은 재미있다. 이번에 툴 만들때도 한참 빠져들어서 재미있게 만들었다. Attribute를 달아서 xml 파일을 자동으로 로딩하는 처리를 만들어 보았는데, cpp에서 하기 힘든 깔끔한 이런 가능성들이 마음에 든다. 규모 큰 프로젝트는 안해봐서 모르겠지만 개인적으로 가지고 놀기에는 제일 맘에 듬. 디버깅 하기 좋고 코드 짜기도 좋고.

Visual Stuio Online

코드 관리를 visual studio online에서 해봤다. 비공개 코드는 주로 개인 Nas나 bitbucket에 올려놓는데, VS IDE에서 링크가 있길래 한 번 눌러봤다가 한 번 써봄.
bitbucket보다 좀 더 많은 기능이 있다. 빌드나 단위테스트를 돌려볼 수 있고(하지만 유료), backlog, splint관리용 보드가 좀 더 디테일하다. 개인 코딩 말고 팀을 꾸려서 작업을 한다면 한 번 제대로 사용해 보는 것을 고려해 볼 순 있겠으나… 왠지 그냥 마음이 안간다. 나같으면 그냥 github 유료 결제해서 쓸 거 같애 ‘ㅅ’)

이제 이건 고마하고 다음 toy project로 넘어가야지.

]]>
+ + + + c++ + + + http://leafbird.github.io/devnote/2014/09/30/cleanup-cpp-project-3rd/#disqus_thread + +
+ + + C++ 코드 정리 자동화 - 2. 불필요한 #include 찾기 下 + http://leafbird.github.io/devnote/2014/09/17/cleanup-cpp-project-2nd/ + http://leafbird.github.io/devnote/2014/09/17/cleanup-cpp-project-2nd/ + Wed, 17 Sep 2014 11:30:24 GMT + + <p>이전 포스트 ‘<a href="/devnote/2014/09/12/claenup-cpp-project-1st/" title="C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上">C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上</a>‘ 에서 이어진다.</p> +<h2 id="지워도-되는-인클루드를-찾아냈다"><a href="#지워도-되는-인클루드를-찾아냈다" class="headerlink" title="지워도 되는 인클루드를 찾아냈다"></a>지워도 되는 인클루드를 찾아냈다</h2><p>개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.</p> +<p>이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.</p> +<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">작업 대상으로 1개의 프로젝트가 입력 되었습니다.</span><br><span class="line">-------------------------------------------------</span><br><span class="line">Service : 프로젝트 정리.</span><br><span class="line">Service : PCH 생성.</span><br><span class="line">컴파일 : stdafx.cpp ... 성공. 걸린 시간 : 1.04초</span><br><span class="line">Client.cpp의 인클루드를 검사합니다.</span><br><span class="line"> - process #1 Client.cpp (1&#x2F;2) ... X</span><br><span class="line"> - process #1 Client.cpp (2&#x2F;2) ... X</span><br><span class="line">ClientAcceptor.cpp의 인클루드를 검사합니다.</span><br><span class="line"> - process #1 ClientAcceptor.cpp (1&#x2F;2) ... 컴파일 가능!</span><br><span class="line"> - process #1 ClientAcceptor.cpp (2&#x2F;2) ... X</span><br><span class="line">ClientConnection.cpp의 인클루드를 검사합니다.</span><br><span class="line"> - process #1 ClientConnection.cpp (1&#x2F;3) ... X</span><br><span class="line"> - process #1 ClientConnection.cpp (2&#x2F;3) ... X</span><br><span class="line"> - process #1 ClientConnection.cpp (3&#x2F;3) ... X</span><br><span class="line">Start.cpp의 인클루드를 검사합니다.</span><br><span class="line"> - process #1 Start.cpp (1&#x2F;4) ... X</span><br><span class="line"> - process #1 Start.cpp (2&#x2F;4) ... X</span><br><span class="line"> - process #1 Start.cpp (3&#x2F;4) ... X</span><br><span class="line"> - process #1 Start.cpp (4&#x2F;4) ... X</span><br><span class="line">ThreadEntry.cpp의 인클루드를 검사합니다.</span><br><span class="line"> - process #1 ThreadEntry.cpp (1&#x2F;1) ... X</span><br><span class="line">-------------------------------------------------</span><br><span class="line">Project : Service 모두 1개의 인클루드가 불필요한 것으로 의심됩니다.</span><br><span class="line">D:\Dev\uni\World\Service\ClientAcceptor.cpp</span><br><span class="line"> - 2 line : #include &quot;World&#x2F;Service&#x2F;Client.h&quot;</span><br><span class="line"></span><br><span class="line">총 소요 시간 : 13.289 sec</span><br></pre></td></tr></table></figure> + + + + 이전 포스트 ‘C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上‘ 에서 이어진다.

지워도 되는 인클루드를 찾아냈다

개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.

이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.

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
27
28
작업 대상으로 1개의 프로젝트가 입력 되었습니다.
-------------------------------------------------
Service : 프로젝트 정리.
Service : PCH 생성.
컴파일 : stdafx.cpp ... 성공. 걸린 시간 : 1.04초
Client.cpp의 인클루드를 검사합니다.
- process #1 Client.cpp (1/2) ... X
- process #1 Client.cpp (2/2) ... X
ClientAcceptor.cpp의 인클루드를 검사합니다.
- process #1 ClientAcceptor.cpp (1/2) ... 컴파일 가능!
- process #1 ClientAcceptor.cpp (2/2) ... X
ClientConnection.cpp의 인클루드를 검사합니다.
- process #1 ClientConnection.cpp (1/3) ... X
- process #1 ClientConnection.cpp (2/3) ... X
- process #1 ClientConnection.cpp (3/3) ... X
Start.cpp의 인클루드를 검사합니다.
- process #1 Start.cpp (1/4) ... X
- process #1 Start.cpp (2/4) ... X
- process #1 Start.cpp (3/4) ... X
- process #1 Start.cpp (4/4) ... X
ThreadEntry.cpp의 인클루드를 검사합니다.
- process #1 ThreadEntry.cpp (1/1) ... X
-------------------------------------------------
Project : Service 모두 1개의 인클루드가 불필요한 것으로 의심됩니다.
D:\Dev\uni\World\Service\ClientAcceptor.cpp
- 2 line : #include "World/Service/Client.h"

총 소요 시간 : 13.289 sec

이 정도 만들어서 회사에서 만들고 있는 프로젝트에 조금 돌려 보았는데, 덕분에 꽤나 많은 불필요 인클루드를 색출해 내었다. 회사 프로젝트는 덩치가 제법 크고, 아직 서비스 중이지 않은 코드여서 용감무쌍한 리팩토링이 자주 일어나기 때문에 관리가 잘 안되는 파일이 제법 있더라. 아무튼 덕을 톡톡히 보았다.

튜닝 : 솔루션 단위로 검사할 수 있게 만들자

프로젝트 파일 단위로 어느 정도 돌아가니까, 솔루션 파일 단위로도 돌릴수 있게 확장했다. sln 파일을 파싱해서 프로젝트 리스트만 얻어오면 끝나는 일이다.

하지만 sln 파일은 vcxproj 파일처럼 쉽게 파싱할 수는 없다. 이녀석은 xml 포맷이 아니라, 자체적인 포맷을 가지고 있다. 사실 sln 파일을 파싱해 본 게 이번이 처음이 아닌데, 예전에는 lua를 써서 직접 노가다 파싱을 했더니 별로 재미도 없고 잘 돌아가지도 않고 코딩하는 재미도 별로 없더라.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // 솔루션 파일은 이렇게 생겼다. 왜죠...

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
MinimumVisualStudioVersion = 10.0.40219.1
... 중략 ...
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F95C61E3-AF95-4CA9-8837-A203762B2B29}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gtest", "External\gtest\gtest.vcxproj", "{C7A81BFC-6E28-4859-A8B5-2FEA80E012B2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{042F2157-2118-44AA-8BB9-8B5DD01FA3A9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "unittest", "Test\unittest.vcxproj", "{24A57754-D332-4575-AEBF-2AFCBC0A7E4B}"
EndProject
... 후략 ...

C#으로 sln 파일을 파싱해주는 적당히 괜찮은 코드가 인터넷 어딘가에 돌아다닌다. 이곳에 있는 놈을 가져다 붙였다. build configuration 같은 걸 얻어올 순 없지만 프로젝트 리스트 얻는 데에는 충분하다.

튜닝 : 느리다. 멀티 스레드로 돌리자

한때는 툴을 만들때 lua도 써보고 python도 써봤지만 요즘은 C#만 쓰게된다. 디버깅 하기도 편하고, 특히 멀티스레딩으로 돌리기가 너무 편하다. TPL, Concurrent Collection조금 갖다 끄적거리면 금방 병렬처리된다.

특히나 이런 식으로 병렬성이 좋은 툴은 훨씬 빠르게 돌릴 수 있게 된다. 커맨드 라인 인자로 --multi-thread를 주면 주요 작업을 Parallel.ForEach로 돌리도록 처리했다. 다만 멀티스레드로 돌리면 파일로 남기는 로그가 엉망이 되기 때문에… 단일 스레드로도 돌 수 있도록 남겨둠.

이번엔 병렬처리할 때 thread-safe한 container가 필요했는데, System.Collections.Concurrent에 가면 queue, stack, dictionary등 종류별로 잔뜩 들어있으니 적당한 놈으로 바로 갖다 쓰면 된다. 편하다 C#. 네이티브 코더는 그냥 웁니다 ㅠㅠ…

지금 내가 가진 개인 코드 중에는 덩치큰 cpp 프로젝트가 없어서, 조그만 솔루션 하나 시험삼아 돌려봤다.

87초 걸리던 것이 24초로 빨리짐. 대충 4배 가량 빨라졌다. 내일 회사에서 대빵 큰 프로젝트에 한 번 돌려봐야지. 생각하니 기대된다.

More Improvement : 불필요한 전방선언(forward declaration) 색출.

툴을 좀 더 확장할 수 있을거 같다. 클래스와 구조체 전방선언을 써놓고 지우지 않아서 찌꺼기가 된 부분을 이것으로 찾아낼 수 있을 것 같다. 이건 파일을 일일이 컴파일 하지 않아도 되니까 훨씬 빠르게 가능할 듯.

전방선언 확인 작업도 따지고 보면 단순 string 처리니까… 시간될 때 카페에 가서 찬찬히 코딩하다보면 금방 짤 수 있겠지. cpp 파일을 write하는 작업도 없어서 read만 하면 되기 때문에 아마 병렬성도 훨씬 더 좋을 것이다.

]]>
+ + + + c++ + + + http://leafbird.github.io/devnote/2014/09/17/cleanup-cpp-project-2nd/#disqus_thread + +
+ + + C++ 코드 정리 자동화 - 1. 불필요한 #include 찾기 上 + http://leafbird.github.io/devnote/2014/09/12/claenup-cpp-project-1st/ + http://leafbird.github.io/devnote/2014/09/12/claenup-cpp-project-1st/ + Fri, 12 Sep 2014 11:26:14 GMT + + <h2 id="지워도-되는-헤더-인클루드를-색출하고-싶다"><a href="#지워도-되는-헤더-인클루드를-색출하고-싶다" class="headerlink" title="지워도 되는 헤더 인클루드를 색출하고 싶다"></a>지워도 되는 헤더 인클루드를 색출하고 싶다</h2><p>매우 느리게 찔끔찔끔 진행하는 토이 프로젝트가 있는데, 오늘 처음으로 무언가 그럴싸한 아웃풋이 나오게 되어 스냅샷을 하는 느낌으로 간단히 포스팅.</p> +<p>cpp 프로젝트 규모가 점점 커지게 되면 빌드 시간 때문에 많은 고통을 겪는다. 이때문에 <a href="https://www.incredibuild.com/">increadi build</a> 같은 분산 빌드 솔루션도 쓰는거고 <a href="http://www.slideshare.net/devcatpublications/ndc2010-unity-build">unity build</a> 같은 꼼수도 사용하게 되는거다. </p> +<p>하지만 저런 솔루션들을 사용하기 이전에, 코드를 정리하는 것이 먼저 선행될 필요가 있다. cpp는 특성상 작업하다보면 소스파일에 불필요한 헤더파일의 #include가 남게되고, 이것들이 불필요한 dependency를 만들어내면서 늘어지는 빌드 시간을 무시할 수 없기 때문이다. </p> +<p>그런데 문제는 그렇게 생긴 불필요 인클루드 구문이 무엇인지를 골라내기가 힘들다는 점이다. 프로젝트 규모가 커질수록 더욱 힘들다. c#같은 경우 불필요 using 구문을 아예 visual studio IDE가 자체적으로 정리해주기까지 하지만, cpp는 색출조차 힘들다 보니 이런 기능을 제공하는 3rd party tool도 없어 보인다. <a href="http://www.wholetomato.com/downloads/spaghettiDownload.asp">Whole Tomato의 Spaghetti</a> 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다. </p> +<p>그래서 한 번 직접 만들어보기로 했다. </p> + + + + 지워도 되는 헤더 인클루드를 색출하고 싶다

매우 느리게 찔끔찔끔 진행하는 토이 프로젝트가 있는데, 오늘 처음으로 무언가 그럴싸한 아웃풋이 나오게 되어 스냅샷을 하는 느낌으로 간단히 포스팅.

cpp 프로젝트 규모가 점점 커지게 되면 빌드 시간 때문에 많은 고통을 겪는다. 이때문에 increadi build 같은 분산 빌드 솔루션도 쓰는거고 unity build 같은 꼼수도 사용하게 되는거다.

하지만 저런 솔루션들을 사용하기 이전에, 코드를 정리하는 것이 먼저 선행될 필요가 있다. cpp는 특성상 작업하다보면 소스파일에 불필요한 헤더파일의 #include가 남게되고, 이것들이 불필요한 dependency를 만들어내면서 늘어지는 빌드 시간을 무시할 수 없기 때문이다.

그런데 문제는 그렇게 생긴 불필요 인클루드 구문이 무엇인지를 골라내기가 힘들다는 점이다. 프로젝트 규모가 커질수록 더욱 힘들다. c#같은 경우 불필요 using 구문을 아예 visual studio IDE가 자체적으로 정리해주기까지 하지만, cpp는 색출조차 힘들다 보니 이런 기능을 제공하는 3rd party tool도 없어 보인다. Whole Tomato의 Spaghetti 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다.

그래서 한 번 직접 만들어보기로 했다.

프로젝트 내의 cpp 파일을 개별 컴파일 하기

일단은 만들려는 툴에서, 입력으로 받은 vc 프로젝트에 포함된 cpp 파일을 개별로 컴파일 할 수 있어야 한다.
그렇게 되면 cpp 파일마다 돌면서 코드 안에 있는 #include를 직접 하나씩 제거해보면서 컴파일이 성공하는지를 확인할거다. 그러면 불필요할 것이라 예상되는 #include의 후보를 만들 수 있다.

무식한 방법이다. cpu를 많이 먹을거고 시간도 오래 걸릴거다. 하지만 저렇게라도 알 수 있다면 새벽에 실행해서 리포트 뽑아놓도록 CI에 물려놓으면 그만이다.

무식하기도 하지만 또한 불완전한 방법이기도 하다. 위의 동작으로 불필요 #include 후보 리스트를 만들었다고 해도,
헤더파일 끼리의 상호 참조관계, 내부 포함 관계등이 여러 복잡한 상황을 연출하기 때문에
후보로 지목된 헤더가 실은 필요한 녀석일 수도 있다.

하지만 일단은 후보 리스트 색출까지 먼저 진행해 보기로 한다.
사실 정말 확실한 불필요 #include가 색출 가능하다면 tool이 아예 코드를 코치는 것까지 자동으로 처리해 줄 수도 있을 것 같지만… 일단 나중에 생각하기로.

프로젝트에 포함된 cpp 파일의 리스트를 구하는 것은 일도 아니다. vcxproj파일은 xml 형태로 되어 있으므로, /Project/ItemGroup/ClCompile 경로의 xml element를 얻어와 파일 경로를 읽어내면 끝이다.

그다음은 이 파일을 각각 컴파일 할 수 있어야 하는데… 이것은 생각보다 만만치가 않다. cl.exe를 실행해서 컴파일 하면 되지만, cl.exe의 커맨드라인 옵션으로 들어가야 하는 인자가 엄청나게 많고, 이 옵션을 vcxproj 파일에서 일일이 파싱하고 다시 조합하기란 상당히 귀찮고 짜증나는 작업이다.

이 귀찮은 작업을 MSBuild에 맡겨버릴 수 있다. MSBuild에 /t:BuildCompile 옵션과 /p:SelectedFiles=xxx을 쓰면 vcxproj를 알아서 파싱해서 cl.exe의 커맨드라인 인자를 직접 만들어준다.

이렇게 해서 일단 프로젝트 파일에 있는 cpp를 개별 컴파일 하는 것까지 성공.

여기까지 하고 나니 cpp 파일당 컴파일 시간까지 덤으로 얻게 됨.
앗싸.

]]>
+ + + + c++ + + + http://leafbird.github.io/devnote/2014/09/12/claenup-cpp-project-1st/#disqus_thread + +
+ + + Yoda Notation + http://leafbird.github.io/devnote/2014/08/19/yoda-notation/ + http://leafbird.github.io/devnote/2014/08/19/yoda-notation/ + Tue, 19 Aug 2014 08:08:44 GMT + + <img src="/devnote/images/yoda1.jpg" class="center"> + +<p>지난번에 <a href="/devnote/2014/07/19/google-c-plus-plus-style-guide/" title="google c++ style guide">google c++ style guide</a> 에 대해서 한참 수다를 떨었는데,<br>요즘에도 비슷한 주제의 책을 읽고 있습니다. 임백준씨가 번역하신 <a href="http://www.yes24.com/24/goods/6692314?scode=032&OzSrank=1">‘읽기 좋은 코드가 좋은 코드다’</a> 인데요,<br> 이것도 가볍게 읽을 수 있는 내용이어서 빌드 시간 중간에 띄엄띄엄 읽고 있어요. </p> +<p>이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 <a href="http://en.wikipedia.org/wiki/Yoda_conditions">Yoda Conditions</a> 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다. </p> +<figure class="highlight cpp"><figcaption><span>May the force be with you.</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> val = <span class="number">20</span>;</span><br><span class="line"><span class="keyword">if</span>(<span class="number">20</span> == val) &#123; <span class="comment">// &lt;- yoda notation here.</span></span><br><span class="line"> ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure> + + + +

지난번에 google c++ style guide 에 대해서 한참 수다를 떨었는데,
요즘에도 비슷한 주제의 책을 읽고 있습니다. 임백준씨가 번역하신 ‘읽기 좋은 코드가 좋은 코드다’ 인데요,
이것도 가볍게 읽을 수 있는 내용이어서 빌드 시간 중간에 띄엄띄엄 읽고 있어요.

이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 Yoda Conditions 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다.

May the force be with you.
1
2
3
4
int val = 20;
if(20 == val) { // <- yoda notation here.
...
}

조건문을 val == 20으로 적는 것이 일반적인 언어 어순과 같아서 읽기가 좋지만
프로그래머의 실수로 val = 20과 같이 잘못된 코드가 만들어지고 컴파일 에러 없이 그대로 실행되는 것을 막기 위해서
일부러 변수와 상수의 위치를 서로 바꾸는 거죠.

요다는 영화 스타워즈에서 영문권 사람들도 이해하기 어려울 정도로 꼬인 문법의 말을 사용합니다. 이를 빗대어 위와 같은 조건문 표기 방식을 Yoda Notation이라고 부르는군요. 재미있는 네이밍입니다 :)

책에서는 요즘의 컴파일러들이 조건절 내에서의 할당에 대해 경고를 내주기 때문에, 요다 표기법은 ‘점차 불필요한 과거의 일이 되어가고 있다’ 고 말합니다.
저도 쉽게 읽히지 않는 이상한 순서 때문에 요다 표기법을 안 좋아하는데,
책에서도 저랑 같은 생각을 이야기 하고 있어서 반갑네요. 이 뿐만 아니라 이 책은 전반적으로 소스코드의 스타일에 대해 많은 부분 공감가는 방식들을 다수 소개하고 있습니다.

예전에 함께 작업했던 어떤 프로그래머분이, 제가 올린 코드를 리뷰하고 나서 제가 추가한 코드의 조건절을 모두 요다 표기법으로 바꾸었던 적이 있습니다. 그거 참… 별 거 아닌데 기분이 나쁘더군요. 그 뒤로 요다 표기법이 싫어졌는지도 모르겠습니다. 하지만 어쨌든 이젠 옛날 이야기가 되어가고 있는겁니다. Visual Studio 2012 기준으로 /W4(경고 수준 4) 설정에 /WX(경고를 오류로 처리) 설정을 더하면 C4706 경고의 발생으로 인해 컴파일 시점에서 코딩 실수를 미리 잡아낼 수 있습니다.

에, 그러니까 내가 하고 싶었던 말은, 이제 이런 거 필요 없다 이겁니다 :)

]]>
+ + + + coding convention + + c++ + + + http://leafbird.github.io/devnote/2014/08/19/yoda-notation/#disqus_thread + +
+ + + Octopress Tips on windows + http://leafbird.github.io/devnote/2014/07/21/octopress-tips-on-windows/ + http://leafbird.github.io/devnote/2014/07/21/octopress-tips-on-windows/ + Mon, 21 Jul 2014 07:44:48 GMT + + <p>개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.<br>앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요. </p> + + + + 개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.

윈도우 실행 (Windows + R) 창에서 블로그 패스로 바로 이동 하기

이거야 뭐… 환경변수에 블로그 경로를 넣어주면 된다. 이렇게 하면 실행 창에 %변수이름%만 입력하면 바로 탐색기를 열 수 있다.
환경 변수 설정을 해주는 PowerShell 스크립트를 만들어서 블로그 폴더의 루트에 놔두면 경로를 옮기거나 depot을 새로 받아도 편하게 셋팅할 수 있다.

ps_register_path.ps1code from github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 현재 스크립트의 실행 경로를 얻는다.
$blog_path = (Get-Item -Path ".\" -Verbose).FullName

# 경로 확인
"blog path : $blog_path"

# 실행 경로를 환경변수에 등록(유저 레벨)
[Environment]::SetEnvironmentVariable("blogpath", $blog_path, "User")

# output result
"Environment Variable update. {0} = {1}" -f "blogpath", $blog_path

# pause
Write-Host "Press any key to continue ..."
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

%blogpath% 이외에 자주 접근하는 경로는 바로가기를 만든다

octopress를 쓰면서 커맨드를 실행하는 주된 경로는 root path다. 이외에도 첨부파일 경로나 글 본문을 저장하는 ./source/_posts 등이 흔히 쓰이는데, 이런 경로에 대한 .lnk 파일을 만들어두면 훨씬 편하다.
위 스샷처럼 바로가기를 만들어두고 po정도 타이핑하고 엔터하면 ./source/_posts로 이동한다.

나는 탐색기를 주로 이용하고자 이렇게 했지만 cmd창에서 바로가기 하고 싶다면 symbolic link를 만들면 될거다.

웹페이지 바로가기도 만들어 두면 편하게 이동 가능. (웹 바로가기는 .url 확장자. 브라우저 주소창에서 슥 끌어다 놓으면 생김)

자주 쓰는 동작들은 스크립트로 자동화한다

Note : 이 항목이 이 포스팅의 핵심 입니다.

Octopress를 쓰면서 마음에 드는 점 중에 하나인데, 마음만 먹으면 조작 과정을 내맘대로 스크립팅할 수 있다는 점이다.
처음 octopress를 이용하려면 갖가지 명령어들을 일일이 숙지하고 사용하기가 불편한 것이 사실이지만,
batch파일과 PowerShell을 통해서 얼마든지 내 입맛대로 자동화 할 수 있다.
PowerShell을 한 번 다뤄보고 싶었지만 딱히 기회가 없었는데 이참에 다뤄보게 되어 재미있었다.
지금은 몇 개 안되긴 하지만 개인적으로 만들어 사용중인 스크립트들은 http://github.com/leafbird/devnote/ 에서 확인할 수 있다.

예제로 한 가지만 살펴보자.

자동화 예시 : 새글 작성을 간편하게

ocotpress에서 새 글을 적으려면 아래의 순서대로 실행해야 한다.

  1. blog path로 이동.
  2. cmd창 오픈
  3. rake new_post['포스팅 제목'] 명령 실행
  4. ./source/_posts로 이동
  5. 자동으로 생성된 .markdown 파일을 찾아서 오픈
  6. 글 작성 시작

이 절차를 아래처럼 PowerShell로 스크립팅한다.

ps_rake_new_post.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 환경변수 BLOG_PATH에 설정된 블로그 root 경로로 이동
cd $env:blogpath

#input으로 새 글의 제목을 받는다.
$title = Read-Host 'Enter Title'

# 실행 : rake new_post['제목']
$argument = [string]::Format("new_post[{0}]", $title)
$out = rake.bat $argument

# 생성된 파일의 이름과 경로를 추출한다.
$out = $out.Replace("Creating new post: ", "")

# 생성된 파일을 gvim으로 오픈!
$new_file_path = [System.IO.Path]::Combine($PSScriptRoot, $out)
gvim.exe $new_file_path

커맨드 창에 PowerShell ./ps_rake_new_post.ps1 입력하는 것도 귀찮으니 이것도 batch파일로 만들자.

02_ps_rake_new_post.bat
1
2
@echo off
powershell ./ps_rake_new_post.ps1

이제 이 batch를 실행해서 새 글 제목을 입력하면 에디터까지 자동으로 열린다.

git conflict : 여러 머신에서 하나의 블로그에 번갈아 포스팅 하는 경우

git을 사용할 때 불편한 점 중의 하나가 머지(merge)다. 여러 머신을 사용할 경우엔 다른 곳에서 수정했던 사항을 미리 pull 받고 난 후 작업해야 하는데, 이걸 혹시나 깜박 잊고 새 글을 써서 generate했다면 conflict 대 참사가 일어난다.

blog root경로는 보통의 git repository를 사용하는 것과 유사하기 때문에 큰 문제가 없는데 _deploy폴더가 문제다. 이 폴더는 블로그 엔진이 generate한 블로그 리소스를 배포하기 위해 사용하는데, 실제로는 gh-pages 브랜치의 clone이기 때문이다. 그래서 서로 다른 여러 개의 depot clone을 가지고 블로깅을 할 땐 blog root와 함께 _deploy도 함께 git pull 해주어야 문제가 없다.

하지만 _deploy폴더는 굳이 동기화까지 받을 필요는 없다. 어차피 블로그 엔진이 배포하는 과정에서 새로 만들기 때문이다.
어떻게 활용하든 상관없지만 만약 _deploy폴더가 충돌이나서 html파일을 한땀 한땀 머지해야 하는 상황이 되었다면 주저없이 삭제해 버리고 새로 만들자.

1
2
3
4
5
6
7
8
cd %blogpath%
rmdir /s /q _deploy
mkdir _deploy
cd _deploy
git init
git remote add origin https://....
git pull
git check --track origin/gh-pages
]]>
+ + + + octopress + + windows + + + http://leafbird.github.io/devnote/2014/07/21/octopress-tips-on-windows/#disqus_thread + +
+ + + google c++ style guide + http://leafbird.github.io/devnote/2014/07/19/google-c-plus-plus-style-guide/ + http://leafbird.github.io/devnote/2014/07/19/google-c-plus-plus-style-guide/ + Sat, 19 Jul 2014 02:12:37 GMT + + <p>지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.<br>본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요. </p> +<p>처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다. </p> + + + + 지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.
본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요.

처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다.

사실 컨벤션이 통일되면 좋다는 것은 아주 상식적인 말입니다만, 개개인이 선호하는 스타일이 다 다르기 때문에 통일을 하기가 쉽지 않다는 것이 문제입니다. 팀에서도 그동안 몇 차례 시도 했었지만 잘 안되었다가, 이번에서야 겨우 성공했어요.

이번에 컨벤션의 통일을 성공한 주된 요인 중의 하나는 구글 내부에서 사용하는 컨벤션을 정리해서 공개한 구글 C++ 스타일 가이드라고 볼 수 있습니다. 이 문서의 내용을 가져와 몇 가지 사항만 프로젝트에 맞게 조정하여 적용 하였지요. 구글 컨벤션의 코드들은 처음 볼 땐 좀 낮설었지만 적응하고 나니 이젠 괜찮군요.

팀에 도입하는 과정에서, 팀 내 능력자분들께서 원문을 한글로 깔끔하게 번역 & 정리해 주셨습니다.
구글에서 검색해보니 오래전에 번역되다가 말았던 문서들은 몇 개 보이는데 이번에 팀 내에서 번역한 문서는 아직 공유가 널리 안 된 것 같아서 다시 한 번 소개도 할 겸 포스팅 합니다. - 이 글의 목적입니다.

일단 간단한 샘플을 한 번 볼까요? (제가 구글 컨벤션을 100% 체득(?)한 상황은 아니지만, 대략적으로 분위기만 한 번 둘러보죠.)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
public:
Test();
virtual ~Test();

int some_value() const;
void set_some_value(const int val) {
some_value_ = val; // 간단한 예제이니 inline으로 짜봅니다.
}

private:
int some_value_;
};

구글 컨벤션의 아주 일부 규칙들이 몇 개 적용된 클래스 선언입니다.

  • opening brace을 아랫줄로 내리지 않고 바로 붙여쓰는 것이나,
  • 들여쓰기는 스페이스 2칸.
  • 클래스의 멤버변수는 c스타일처럼 _로 연결된 소문자 단어를 사용하고 _를 끝에 붙인다는 점
  • getter는 멤버 변수의 이름과 같게,
  • setter는 set_변수명()의 규칙을 지닌다.
  • 클래스 접근 권한 지정자(public, private)는 스페이스 1칸 들여쓰기.

…정도가 보이네요. (변수명 선언 방식이 헝가리안 표기가 아니예요!)

구글 컨벤션을 따르는 코드의 예제는 breakpad, protobuf같은 구글의 오픈소스 프로젝트에서 볼 수 있습니다. 구글 코드 이외에도 몇몇 오픈소스들을 보면 구글 컨벤션의 영향을 받은듯한 코드들이 제법 보입니다. 얼마전에 잠시 가지고 놀았던 msgpack도 어느정도 구글 컨벤션의 영향을 받은 듯한 모양새를 가지고 있더군요.

구글 컨벤션은 위의 예제에서 보이는 단순한 들여쓰기, 줄바꿈 같은 형식 이야기 말고도 디자인 철학과 관련된 규약들이 많이 있어서, 평소 생각하지 못했던 여러가지 이슈들을 상기시켜줍니다. 문서 내용을 읽는 것만 해도 자신의 코딩 스타일에 대해 많은 점검(?)을 할 수 있어요.

개인적으로는 팀에서 정해진 룰 때문에 먼저 좀 겪어보게 되었는데 나쁘지 않더군요. 아직까지 마음에 안 드는 조항들도 몇 가지 있지만, 앞으로는 팀 코드가 아닌 개인 작업을 할 때에도 구글 컨벤션을 지켜 코딩해볼 생각입니다.

다시 한 번 링크 :

본격 컨벤션 적용을 위한 팁 :

vs2013의 서식 설정 옵션 활용

visual studio를 이용해 윈도우에서 개발하는 환경일 경우, IDE로 vs2013을 사용하면 많은 도움이 됩니다. 빌드는 예전 버전으로 하더라도 IDE만 vs2013을 사용할 수 있습니다. 2013에는 IDE의 자동 formatting 방식을 직접 설정할 수 있어요.

게다가 vs2013 Update 2를 설치하면 설정 가능한 옵션이 좀 더 늘어납니다! Update 3는 아직 안나왔지만 나오면 설정이 더 늘어날지도!!

포맷팅을 자동으로 고쳐주는 AStyle 활용

AStyle이란 멋진 프로그램이 있어요. 포맷팅을 자동으로 고쳐주는 프로그램인데, 오픈소스로 되어있어 직접 수정 & 활용할 수 있습니다. 이걸 perforce 클라이언트인 p4v.exe pending changelist 창에서 일괄 적용하게 설정할 수도 있고, vs 플러그인으로 만들어서 코딩 중에도 실행해 볼 수 있어요. 포맷팅을 알아서 고쳐주니까 코딩중에는 들여쓰기가 어떻고 빈 칸이 어떤지 일일이 신경 쓸 필요 없으니 아주 편리합니다 -_-)b 강추예요.

]]>
+ + + + coding convention + + c++ + + + http://leafbird.github.io/devnote/2014/07/19/google-c-plus-plus-style-guide/#disqus_thread + +
+ + + 벤츠타는 프로그래머 + http://leafbird.github.io/devnote/2014/07/16/the-benz-programmer/ + http://leafbird.github.io/devnote/2014/07/16/the-benz-programmer/ + Wed, 16 Jul 2014 11:20:38 GMT + + <img src="/devnote/images/140708_00.png" class="center" width="300"> + +<p>요 며칠간 이 책을 읽었습니다. 회사 도서관에 갔다가 제목이 끌려서 한 번 읽어봤어요.<br>누가 정한건지 모르겠지만 책 제목 참 멋지게 지었습니다. 주변에서 제가 이 책 읽는 것 보면 모두들 제목에 대해 관심을 보이더군요 ㅎ</p> +<p>진로를 고민중인 학생이나 일을 시작한지 얼마 되지 않는 신입 개발자들을 주 대상으로 삼은 책입니다. 다소 주관적이긴 하지만 선배 개발자 입장에서 들려주는 이런 저런 이야기들이 적혀 있어요.<br>저자는 자기관리를 잘 하는 분이신 듯 합니다. 구체적인 개인 목표를 세우고 달성을 위해 노력하는 점이라든지, 꾸준한 자기계발에 관심을 두는 점 같은 좋은 습관을 많이 만들어두신 것 같네요. </p> + + + +

요 며칠간 이 책을 읽었습니다. 회사 도서관에 갔다가 제목이 끌려서 한 번 읽어봤어요.
누가 정한건지 모르겠지만 책 제목 참 멋지게 지었습니다. 주변에서 제가 이 책 읽는 것 보면 모두들 제목에 대해 관심을 보이더군요 ㅎ

진로를 고민중인 학생이나 일을 시작한지 얼마 되지 않는 신입 개발자들을 주 대상으로 삼은 책입니다. 다소 주관적이긴 하지만 선배 개발자 입장에서 들려주는 이런 저런 이야기들이 적혀 있어요.
저자는 자기관리를 잘 하는 분이신 듯 합니다. 구체적인 개인 목표를 세우고 달성을 위해 노력하는 점이라든지, 꾸준한 자기계발에 관심을 두는 점 같은 좋은 습관을 많이 만들어두신 것 같네요.

저는 작업 도중에 빌드 걸어놓고 잠깐씩 기다리는 동안에 주로 읽었습니다.
빌드시간에 조금 난이도가 있는 기술서적을 읽을 때는, 내용을 좀 따라가려다 보면 빌드가 끝나서 흐름이 끊기고, 이게 계속 반복되다보니 책에 제대로 집중할 수가 없었습니다.
그래서 빌드시간에 책읽는 것은 거의 포기를 하고 있었는데, 이런 책은 부담없이 읽을 수 있어서 빌드 중에 읽어도 괜찮더군요.
그래서 앞으로는 빌드하는 중에 이런 가벼운 책들 읽으면 되겠구나 하는 생각을 해봤습니다.

저는 책을 읽다가 조금 엉뚱한 구절에 눈길이 확 쏠렸는데,

…결혼하고 아이들이 생긴 이후에는 집에서 어떤 일을 한다는 게 쉽지 않았다. 그래서 집중해서 집필하거나 공모전 참가 준비를 할 때는 주말마다 본가로 달려갔다. 본가에서는 식사 시간 이외에는 누구도 방해하는 사람이 없어서 원하는 일에 집중할 수 있었기 때문이다.

이 부분입니다. 저도 아이가 생긴 후에는 개인 시간을 내기가 쉽지 않아서 적잖이 고민을 하고 있는데, 주말마다 본가에 가서 혼자만의 시간을 가질 수 있었다는 저 이야기는 정말 부럽기 짝이 없네요 ㅜㅠ…

저는 집에 아이가 생기고 한동안은 개인 시간은 아예 포기하고 지냈습니다. 주말마다 즐겁게 참여하던 스터디도 못 나가게 되었고, 집에서 컴퓨터 앞에 앉아 코딩을 하는 것은 거의 꿈도 꾸질 못했어요.

이제는 아이도 어느 정도 자랐고 하니 조금씩 개인 시간을 확보하고 다시 자기관리에 신경을 좀 써야겠다고 다짐했습니다. 이런 다짐을 한 것에는 최근에 이 책을 읽었던 것도 어느 정도 영향이 있었겠지요. 벤츠 타는 것도 난 바라지 않아요. 그냥 원하는 만큼 양껏 코딩하고 놀 수 있게만 됐으면 좋겠네요 ;ㅁ;)…

]]>
+ + + + book + + + http://leafbird.github.io/devnote/2014/07/16/the-benz-programmer/#disqus_thread + +
+ + + move to octopress! + http://leafbird.github.io/devnote/2014/07/14/move-to-octopress/ + http://leafbird.github.io/devnote/2014/07/14/move-to-octopress/ + Sun, 13 Jul 2014 15:24:24 GMT + + <p>기존에 티스토리에서 운영 중이던 <a href="http://devnote.tistory.com/">프로그래밍 관련 블로그(devnote.tistory.com)</a>를 Octopress로 이사합니다. 사실 운영이라고 말하기도 뭣할 만큼 오랫동안 방치되어 있었는데, 다시금 분위기를 쇄신하고자 환경을 바꿔볼까 합니다. </p> + + + + 기존에 티스토리에서 운영 중이던 프로그래밍 관련 블로그(devnote.tistory.com)를 Octopress로 이사합니다. 사실 운영이라고 말하기도 뭣할 만큼 오랫동안 방치되어 있었는데, 다시금 분위기를 쇄신하고자 환경을 바꿔볼까 합니다.

기존 블로그를 feedburner 주소로 구독중이었다면 새로운 블로그로 자동으로 넘어갑니다. 하지만 티스토리 기본 rss 주소를 사용중이었다면, 이참에 feed-burner로 갈아타 주세요 ‘ㅁ’)/

feed burder address : http://feeds.feedburner.com/florist_devnote

Octopress는 기존과는 다른 형태의 static engine이라서 호감이 갑니다. 맘에 드는 점을 몇가지만 꼽아보면

  • vim으로 글을 적을 수 있다는 것
  • 본문 글이 로컬에 text(markdown)파일로 남는 다는 점
  • 블로그 주소에 github.io를 쓴다는 것
  • 기본적으로 큰 글씨를 사용하는 시원한 테마들.

… 등입니다. markdown으로 글을 적게 된다면 하루패드를 사용해야 겠다고 생각했었는데, vim으로 적는게 더 느낌이 좋네요 :) vim을 무척 잘 쓰는 편은 못되지만, octopress덕에 git이나 vim을 자주 접하게 되면 좀 더 익숙해 지는 계기가 될테니 그런 점도 마음에 듭니다.

그 외 나머지 추가 기능이나 설정 같은 건 아직 제대로 모르는 상태이지만, 하루 이틀 미루다보면 너무 늘어져 버릴 것 같아서 우선 이사 공표(?)부터 내지릅니다.

집에 애가 생기고 난 후 부터는 개인 시간이 많이 줄어들면서 블로그에도 소홀해지게 되었는데, 앞으로는 굳이 테크니컬한 내용의 글이 아니더라도 개발에 관련된 소소한 글들도 올릴 생각입니다. 이를테면 기계식 키보드에 대한 이야기라던가… 하는 것도요. (글쓰기 연습을 위해서라도 무엇이든 꾸준히 글을 좀 적어야 겠다는 개인적인 욕망(?) 때문입니다.)

앞으로 여러가지 글들 종종 올리겠습니다.

]]>
+ + + + octopress + + + http://leafbird.github.io/devnote/2014/07/14/move-to-octopress/#disqus_thread + +
+ + + octopress on windows + http://leafbird.github.io/devnote/2013/12/30/octopress-on-windows/ + http://leafbird.github.io/devnote/2013/12/30/octopress-on-windows/ + Mon, 30 Dec 2013 14:06:15 GMT + + <img src="/devnote/images/octopress.jpeg" class="center"> + +<p>octopress도 대게는 ruby가 기본 설치된 mac에서 많이들 사용하는 듯 하다. 검색해보면 대부분 OS X를 기준으로 한 셋팅법이다. 윈도우에서 사용하는 것도 많이 어렵진 않지만 <strong>한글 인코딩 때문에 많이 헤맸음 ㅜㅠ</strong>…</p> +<p>일단 기본적으로 아래 두 개의 글을 참고해 설치했는데,</p> +<ol> +<li><a href="http://stb.techelex.com/setup-octopress-on-windows7/">http://stb.techelex.com/setup-octopress-on-windows7/</a></li> +<li><a href="http://chulhankim.github.io/blog/2013/07/31/octopress-and-github.html">http://chulhankim.github.io/blog/2013/07/31/octopress-and-github.html</a></li> +</ol> +<p>ruby는 생소한 언어이기도 하고 링크가 사라지면 다시 헤맬수도 있으니 간략하게 다시 정리.</p> + + + +

octopress도 대게는 ruby가 기본 설치된 mac에서 많이들 사용하는 듯 하다. 검색해보면 대부분 OS X를 기준으로 한 셋팅법이다. 윈도우에서 사용하는 것도 많이 어렵진 않지만 한글 인코딩 때문에 많이 헤맸음 ㅜㅠ

일단 기본적으로 아래 두 개의 글을 참고해 설치했는데,

  1. http://stb.techelex.com/setup-octopress-on-windows7/
  2. http://chulhankim.github.io/blog/2013/07/31/octopress-and-github.html

ruby는 생소한 언어이기도 하고 링크가 사라지면 다시 헤맬수도 있으니 간략하게 다시 정리.

Ruby 설치

일단 윈도우에는 Ruby가 없기 때문에 먼저 설치를 해야 한다.
다운로드 페이지에서 Ruby와 DevKit을 다운받는다.
내가 사용한 버전은 Ruby 2.0.0-p353 (x64)와 DevKit-mingw64-64-4.7.2-20130224-1432-sfx.exe

DevKit을 사용하기 전에 install 과정이 필요하다. 이 단계를 실행하기 전에 ruby의 bin 폴더가 path에 잡혀 있는 것이 좋다. 그러면 DevKit 초기화 과정에서 ruby의 경로를 알아서 감지하므로, config.yml을 수정할 필요가 없다.

1
2
3
cd C:/RubyDevKit
ruby dk.rb init # 이 때 config.yml이 생김. 이 전에 ruby bin을 path에 넣자.
ruby dk.rb install

python 설치

python은 없어도 상관없다. 하지만 syntax highlighting을 하려거든 python이 필요하다. 이것도 OS X는 기본 설치되어 있어서 크게 이슈가 없는듯. 나는 한참 써보다가 알았는데, 나중에 python을 설치하면 뭔가 더 해주어야 하는 것 같아 귀찮다. 그냥 처음부터 python을 설치해놓고 path에 python이 포함되도록 해두는게 좋겠다.

Octopress 받기

1
2
3
4
cd c:/github
git clone git://github.com/imathis/octopress.git octopress
cd octopress #replace octopress with username.github.com
ruby --version # Should report Ruby 1.9.3

ruby 패키지들 (dependencies) 설치:

1
2
3
cd c:/github/octopress       #replace octopress with username.github.com
gem install bundler
bundle install

octporess의 기본 테마 설치:

1
$ rake install

이부분에서 말을 안들을 수가 있는데, 뭔가 모듈의 버전이 맞지 않는 문제다.

1
2
3
4
5
6
D:\Blog\DevNote>rake install
rake aborted!
You have already activated rake 0.9.6, but your Gemfile requires rake 0.9.2.2. P
repending `bundle exec` to your command may solve this.
D:/Blog/DevNote/Rakefile:2:in `<top (required)>'
(See full trace by running task with --trace)

이 때 bundle update rake 해주면 해결. 다음 글을 참고했다.

1
2
3
4
5
6
7
D:\Blog\DevNote>bundle update rake
Fetching gem metadata from https://rubygems.org/.......
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Using rake (0.9.6)
...(중략)...
Your bundle is updated!

Octopress를 Github Pages용으로 설정

1
$ rake setup_github_pages

Github Pages는 계정 페이지와 프로젝트 페이지로 나뉜다.
각각의 경우에 따라 수동설정을 해주어야 하는데(이 부분은 두 번째 글에 잘 설명되어 있다.), 프로젝트 페이지의 경우가 조금 더 손댈 곳이 많다.

  • 계정 페이지 설정인 경우

_config.yml에서 url, title, subtitle, author 정도만 수정해주면 된다.

  • 프로젝트 페이지 설정의 경우

먼저 git remote 추가.

1
2
$ git remote add origin `https://github.com/username/projectname.git
$ git config branch.master.remote origin

_config.yml, config.rb, Rakefile 을 열어서 /github라고 된 부분을 repository 명으로 수정.

한글 인코딩 문제 해결

이제 부푼 꿈을 안고 첫 포스팅을 만들어보면 잘 동작한다.
하지만.. 한글을 사용하면 다시 인코딩 관련 에러를 만나게 된다.
여기서 엄청난 시간을 소모했는데, octopress 안에서 해결을 보려고 하니 힘들다. ruby는 한 번도 안써봐서 코드 보기도 힘들고 ㅡㅠ…
검색해보면 jekyll 코드 일부를 직접 수정하는 방법도 있는데,
그것보다 cmd창의 코드 페이지를 변경해주면 간단하게 해결된다.

1
chcp 65001 # 다시 되돌리려면 chcp 949

rake generate를 하거나 rake preview를 하기 전에, 코드페이지를 항상 변경해주고 실행한다. batch파일을 미리 만들어두니 편하다.

markdown 문법은 검색하면 어렵지 않게 찾을 수 있다.

블로그 내부 링크 만들기

기본으로 제공되는 기능이 없는듯? 플러그인 폴더에 아래 파일 하나 넣어주어야 한다.

여기 에서 참고했다. 아래 문법을 사용한다.

1
[link to this post]({% post_url 2012-01-05-hello-world %})

eof.

]]>
+ + + + octopress + + windows + + encoding + + + http://leafbird.github.io/devnote/2013/12/30/octopress-on-windows/#disqus_thread + +
+ +
+
diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..2c5453b6 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,11 @@ + + + + + + http://leafbird.github.io/devnote/post-sitemap.xml + 2023-01-07T02:18:49.836Z + + + + diff --git a/sitemap.xsl b/sitemap.xsl new file mode 100644 index 00000000..a2c4214e --- /dev/null +++ b/sitemap.xsl @@ -0,0 +1,191 @@ + + + + + + + XML Sitemap + + + + +
+

XML Sitemap

+

+ Generated by + Hexo Google-friendly sitemap generator, this is an XML + Sitemap, meant for consumption by search engines. +
+ You can find more information about XML sitemaps on + sitemaps.org. +

+ +

+ This XML Sitemap Index file contains + sitemaps. +

+ + + + + + + + + + + + + + + + + + +
SitemapLast Modified
+ + + + + +
+
+ +

+ This XML Sitemap contains + URLs. +

+

+ ↑ Sitemap Index +

+ + + + + + + + + + + + + + + + + + + + + + + +
URLPrioImagesCh. Freq.Last Mod.
+ + + + + + + + + + + + + + +
+
+
+ + + + + +
+
diff --git a/tags/ArrayPool/index.html b/tags/ArrayPool/index.html new file mode 100644 index 00000000..363c893b --- /dev/null +++ b/tags/ArrayPool/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: ArrayPool | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

ArrayPool + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/AsyncLocal/index.html b/tags/AsyncLocal/index.html new file mode 100644 index 00000000..721e36a4 --- /dev/null +++ b/tags/AsyncLocal/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: AsyncLocal | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

AsyncLocal + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Fragmentation/index.html b/tags/Fragmentation/index.html new file mode 100644 index 00000000..c331f9cf --- /dev/null +++ b/tags/Fragmentation/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Fragmentation | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Fragmentation + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Memory/index.html b/tags/Memory/index.html new file mode 100644 index 00000000..c3d7037a --- /dev/null +++ b/tags/Memory/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Memory | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Memory + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Network/index.html b/tags/Network/index.html new file mode 100644 index 00000000..0dc61b25 --- /dev/null +++ b/tags/Network/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Network | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Network + 태그 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Pipeline/index.html b/tags/Pipeline/index.html new file mode 100644 index 00000000..5db5e47b --- /dev/null +++ b/tags/Pipeline/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Pipeline | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Pipeline + 태그 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Socket/index.html b/tags/Socket/index.html new file mode 100644 index 00000000..03301359 --- /dev/null +++ b/tags/Socket/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Socket | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Socket + 태그 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/TLS/index.html b/tags/TLS/index.html new file mode 100644 index 00000000..68712c19 --- /dev/null +++ b/tags/TLS/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: TLS | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

TLS + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Thread/index.html b/tags/Thread/index.html new file mode 100644 index 00000000..e02e0f23 --- /dev/null +++ b/tags/Thread/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: Thread | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

Thread + 태그 +

+
+ + +
+ 2021 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/book/index.html b/tags/book/index.html new file mode 100644 index 00000000..511a908e --- /dev/null +++ b/tags/book/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: book | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

book + 태그 +

+
+ + +
+ 2014 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/c/index.html b/tags/c/index.html new file mode 100644 index 00000000..94928374 --- /dev/null +++ b/tags/c/index.html @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: c++ | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

c++ + 태그 +

+
+ + +
+ 2014 +
+ + + + + + + + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/coding-convention/index.html b/tags/coding-convention/index.html new file mode 100644 index 00000000..da019ab3 --- /dev/null +++ b/tags/coding-convention/index.html @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: coding convention | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

coding convention + 태그 +

+
+ + +
+ 2014 +
+ + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/encoding/index.html b/tags/encoding/index.html new file mode 100644 index 00000000..9f1635b5 --- /dev/null +++ b/tags/encoding/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: encoding | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

encoding + 태그 +

+
+ + +
+ 2013 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/octopress/index.html b/tags/octopress/index.html new file mode 100644 index 00000000..c99b0a83 --- /dev/null +++ b/tags/octopress/index.html @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: octopress | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

octopress + 태그 +

+
+ + +
+ 2014 +
+ + + + +
+ 2013 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/string-interning/index.html b/tags/string-interning/index.html new file mode 100644 index 00000000..611be145 --- /dev/null +++ b/tags/string-interning/index.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: string interning | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

string interning + 태그 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/windows/index.html b/tags/windows/index.html new file mode 100644 index 00000000..edf13c7b --- /dev/null +++ b/tags/windows/index.html @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: windows | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

windows + 태그 +

+
+ + +
+ 2014 +
+ + +
+ 2013 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\352\262\214\354\236\204\354\204\234\353\262\204/index.html" "b/tags/\352\262\214\354\236\204\354\204\234\353\262\204/index.html" new file mode 100644 index 00000000..1bde89a5 --- /dev/null +++ "b/tags/\352\262\214\354\236\204\354\204\234\353\262\204/index.html" @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: 게임서버 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

게임서버 + 태그 +

+
+ + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\352\263\240\354\204\261\353\212\245/index.html" "b/tags/\352\263\240\354\204\261\353\212\245/index.html" new file mode 100644 index 00000000..204f8fbc --- /dev/null +++ "b/tags/\352\263\240\354\204\261\353\212\245/index.html" @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: 고성능 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

고성능 + 태그 +

+
+ + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\353\246\254\353\215\224\354\213\255/index.html" "b/tags/\353\246\254\353\215\224\354\213\255/index.html" new file mode 100644 index 00000000..0331dbcc --- /dev/null +++ "b/tags/\353\246\254\353\215\224\354\213\255/index.html" @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: 리더십 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

리더십 + 태그 +

+
+ + +
+ 2018 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\353\251\224\353\252\250\353\246\254/index.html" "b/tags/\353\251\224\353\252\250\353\246\254/index.html" new file mode 100644 index 00000000..cef5f542 --- /dev/null +++ "b/tags/\353\251\224\353\252\250\353\246\254/index.html" @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +태그: 메모리 | leafbird/devnote + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+
+
+

메모리 + 태그 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + +
+
+ + + + + + +
+ + 0% +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +