diff --git a/about.html b/about.html new file mode 100644 index 0000000..891e1d5 --- /dev/null +++ b/about.html @@ -0,0 +1,106 @@ + + + + + 박성범 Simon Park + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + diff --git a/ads.txt b/ads.txt new file mode 100644 index 0000000..6027b6d --- /dev/null +++ b/ads.txt @@ -0,0 +1 @@ +google.com, pub-5029429318173951, DIRECT, f08c47fec0942fa0 diff --git a/article/0.html b/article/0.html new file mode 100644 index 0000000..c9ef5d7 --- /dev/null +++ b/article/0.html @@ -0,0 +1,308 @@ + + + + + + 📋 프론트엔드 개발자를 위한 토막상식 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ 📋 프론트엔드 개발자를 위한 토막상식 +

+ + + + +
+
Table of Contents +

+
+

프로젝트하면서 알게 된 것들과 코딩테스트를 통해 배운 것들, 자바스크립트&제이쿼리: 인터랙티브 프론트엔드 웹 개발 교과서(Jon Duckett)와 JavaScript: The Good Parts(Douglas Crockford)를 통해 공부한 것들, 그리고 Front-end Job Interview Questions에 나와있는 질문들에 대한 대답을 간략하게 정리했다.

+

일반적인 것들

+

헝가리안 표기법

+

변수 이름에 데이터 타입을 접두어로 붙이는 표기법이다. 숫자라면 nVar, 배열이라면 aVar 이런 식으로 붙인다. 2015년 네이버에서 인턴십을 할 때 코딩컨벤션이 헝가리언 표기법 + 캐멀케이스여서 그때 처음 접하고 사용하기 시작했는데, 요즘에는 헝가리언 표기법을 지양한다고 한다. 가독성과 유지보수 효율성이 떨어진다고. (참고)

+

블록 안에서 변수 선언

+

Java의 경우 한 블록 안에서만 쓰이는 변수를 코드 위쪽에 선언하지 않고 블록 안에 변수를 선언하면 코드를 파악하기 더욱 쉬워지고, 스코프를 제한할 수 있어 안전하다. 자바스크립트의 var타입은 블록 스코프가 아닌 함수 스코프를 적용하기 때문에 어디에 선언하든 아무런 차이가 없다. 하지만 let이나 const를 사용한다면 얘기가 달라진다. 이 둘은 블록 스코프이다. (참고)

+

점진적 향상법(Progressive enhancement)과 우아한 성능저하법(Graceful degradation)

+

사용자가 항상 최신 기술을 사용할 수 있는 환경에서 서비스를 사용하지는 않는다. 특히 웹사이트를 만들 때는 최신 브라우저와 구식 브라우저를 모두 신경써야 한다. 점진적 향상법과 우아한 성능저하법은 최신과 구식에 대응하기 위한 방법을 말한다. (참고)

+

점진적 향상법은 기본적으로 구식 기술 환경에서 동작할 수 있는 기능을 구현하고, 최신 기술을 사용할 수 있는 환경에서는 더 나은 사용자 경험을 제공할 수 있는 최신 기술을 제공하는 방법이다. 즉, 구식 환경에서도 충분히 서비스를 사용할 수 있고, 최신 환경이라면 더 나은 기능들을 사용할 수 있도록 만드는 것이다. 구식 브라우저를 사용하는 사용자에게 100만큼의 기능을 제공하고, 최신 브라우저를 사용하는 사용자에게는 130정도의 기능을 제공하도록 웹사이트를 만든다고 보면 된다.

+

우아한 성능저하법은 점진적 향상법과 반대이다. 이는 최신 기술을 기반으로 기능을 구현한 뒤, 구형 기술에 기반한 환경에서도 유사하게 동작하도록 만드는 방법이다. 최신 브라우저에서는 100만큼의 기능을 제공하고, 구식 브라우저에서는 50정도의 기능만을 제공하게 웹사이트를 만드는 것이다. img 태그에 alt 속성을 지정함으로써 이미지를 보여주지 못하는 환경에서 이미지를 텍스트로 대체하는 것이 대표적인 예시다.

+

FOUC(Flash Of Unstyled Content)

+

FOUC는 외부의 CSS 코드를 불러오기 전 스타일이 적용되지 않은 페이지가 잠시 나타나는 현상이다. 특히 IE에서 자주 발생하는 현상으로, 사옹자 경험을 저하하는 요인이 된다. FOUC가 발생하는 이유는 브라우저가 마크업에 참조된 파일들을 모아 DOM을 생성할 때 가장 빠르게 분석할 수 있는 부분(HTML)을 먼저 화면에 표시한 뒤, 화면에 출력된 마크업 순서에 따라 스타일(CSS)을 적용하고 스크립트(Javascript)를 실행하기 때문이다.

+

FOUC를 해결하기 위해서는 head 태그 안에 CSS를 링크하고, @import의 사용을 자제해야 한다. 또한 자바스크립트를 head 태그 안에서 로드하는 것도 방법될 수 있다. (하지만 별로 추천하지 않는다.) 어떤 방법으로도 해결되지 않는다면 FOUC가 발생하는 구역을 숨겼다가 브라우저가 준비됐을 때 다시 보여주는 방법이 있다. (참고)

+

ARIA(Accessible Rich Internet Applications)와 스크린리더

+

ARIA는 접근가능한 인터넷 어플리케이션을 의미한다. 이는 웹 콘텐츠를 개발할 때 장애인을 위한 접근성 향상 방법을 정의한다. ARIA를 사용해 웹사이트여러 곳의 접근성을 향상할 수 있다. ARIA는 <html> 태그에 role 속성을 지정하는 방식으로 사용할 수 있으며, 대부분의 브라우저와 스크린리더가 ARIA를 지원하고 있다. 스크린리더는 웹사이트의 구성 요소들을 읽어주는 프로그램으로, 시각장애를 가진 사용자가 컴퓨터 화면에 무엇이 있는지 인지할 수 있게 돕는다. (참고)

+

CSS 애니메이션과 Javascript 애니메이션의 차이

+

자바스크립트는 메인 쓰레드가 무거운 작업을 하고 있을 때 애니메이션 처리의 우선순위를 미뤄두는 반면 CSS는 독립적인 쓰레드가 애니메이션을 처리해준다. 때문에 CSS 애니메이션은 최적화가 쉽다. 하지만 항상 CSS 애니메이션이 우수한 것은 아니다. UI 요소가 작은 경우 CSS를 사용하고, 애니메이션을 세밀하게 제어해야 하는 경우 자바스크립트를 사용하는 것이 좋다. (참고1) (참고2)

+

동일출처정책(Same-origin policy)과 CORS(Cross-origin resource sharing)

+

동일출처정책은 한 출처의 문서가 다른 출처의 문서와 상호작용하지 못하도록하는 정책이다. 두 문서의 프로토콜, 포트, 호스트가 같으면 동일출처라고 한다. CORS는 어떤 웹페이지에 있는 자바스크립트가 해당 도메인이 아닌 다른 도메인에 XMLHttpRequests 요청을 허용하는 기법을 말한다. 브라우저는 서버와 통신할 때마다 서로에 대한 정보를 HTTP 헤더를 이용해 전달한다. CORS는 HTTP 헤더에 정보를 추가해 브라우저와 서버가 서로 통신해야 한다는 사실을 알게 한다. CORS를 통하지 않을 경우, Cross-domain 요청은 동일출처정책에 의해 브라우저가 금지한다. CORS는 다른 출처에서 온 요청을 허용할 지 결정하기 위해 브라우저와 서버가 상호교류하는 방법을 정의한 것이다. CORS는 W3C 명세에 포함되어 있지만 IE의 경우에는 비표준 XDomainRequest 객체를 사용하여 CORS 요청을 처리한다. (참고1) (참고2)

+

코드 의존성과 DRY원칙

+

어떤 스크립트는 다른 추가적인 스크립트를 필요로 하기도 한다. 이때 다른 스크립트에 의존하는 스크립트를 작성할 때 해당 스크립트에 의존성이 있다고 표현한다. jQuery를 활용하는 스크립트는 의존성이 있다고 할 수 있다. 이런 경우 주석을 통해 의존성을 명시하는 것이 좋다.

+

DRY원칙은 Don’t Repeat Yourself의 줄임말로, 소프트웨어 개발 원칙을 말한다. 한국어로는 중복배제라고 한다. 같은 작업을 수행하는 코드를 두 번 작성했다는 이를 코드 중복이라고 하는데, DRY원칙은 이러한 코드 중복을 지양하자는 원칙이다. 코드 중복의 반대 개념으로는 코드 재사용(Code reuse)이 있다. 코드 재사용은 같은 코드가 스크립트의 다른 곳에서 한 번 이상 사용되는 것을 말한다. 함수를 활용하는 것은 코드 재사용의 좋은 예로, 재사용 가능한 함수를 헬퍼 함수(Helper function)라고 한다. 코드 재사용을 권장하기 위해 개발자들은 작은 스크립트를 작성하는데, 이때문에 코드 재사용은 코드 사이에 더 많은 의존성을 만든다.

+

HTML

+

DOCTYPE

+

DOCTYPE은 문서형을 말하며, 해당 문서가 어떤 버전의 어떤 마크업 언어로 구성되어있는지를 의미한다. DOCTYPE을 선언하는 것을 DTD(Dodumenttation Type Declaration)라고 한다. DTD는 각 마크업의 각 버전에서 사용가능한 태그나 속성 등을 정의하기 때문에 문서의 최상단에 위치해야 한다. HTML5 이전 버전은 SGML(Standard Generalized Markup Language)에 기반했기 때문에 DTD 참조가 필수적이었다. HTML5는 DTD 참조가 필요하지 않으며, 하위 호환을 위해 <!DOCTYPE html>만으로 선언한다. (참고)

+

표준모드(Standard mode)와 호환모드(Quirks mode)

+

표준모드와 호환모드는 브라우저가 가진 두 가지 렌더링 모드다. 브라우저는 DTD에 따라 렌더링할 모드를 선택하는데, 이 과정을 Doctype sniffing또는 Doctype switching이라고 한다. 브라우저가 출력하고자 하는 문서가 최신이라면 W3C나 IETF의 표준을 엄격히 준수하는 표준모드로 렌더링을 한다. 반면 문서가 오래된 버전이라면 호환모드로 렌더링한다. 호환모드는 이전 세대의 브라우저에 맞는 비표준 규칙을 문서에 적용해 오래된 웹페이지들이 최신 브라우저에서 깨져보이지 않게 한다. 브라우저는 문서가 최신인지 아닌지를 DTD로 판단한다. 만약 DTD가 존재하지 않거나 일부가 누락된 경우 호환모드로 문서를 해석한다. 또한 IE의 경우 DTD 앞에 주석이나 다른 문자가 들어갔을 때도 문서를 호환모드로 해석한다. (참고)

+

XML과 XHTML

+

XML과 XHTML모두 웹 문서 규격이다. XML은 W3C에서 여러 특수 목적의 마크업 언어를 만드는 용도에서 권장되는 다목적 마크업 언어다. XML은 문서 상의 데이터 이름과 값 등을 구분하기 위해 만들어졌는데, XML은 SGML(참고로 SGML은 인터넷이 등장하기 이전에 만들어졌다.)을 기반으로한 HTML의 한계를 극복하여 XHTML을 이끌었다. XML을 기반으로 한 HTML을 만든 셈이다. XHTML은 XML의 문법을 따르며, HTML 문법과 매우 유사하지만 더 엄격하다.

+

XHTML이 더 표준인 것처럼 보이지만 사실 그렇게 권장하지 않는 사람들도 있다. 일단 HTML의 호환성이 더 높기 때문이다. XHTML은 1.1버전부터 비표준이나 비권장 태그를 호환하지 못하게 되면서 지나치게 엄격하다는는 비판을 받았다. IE가 XHTML을 해석하지 못하는 것 역시 XHTML의 호환성을 떨어뜨리는 요인이 됐다. 또한 XHTML과 HTML의 요소와 속성에는 차이가 거의 없다. 단지 HTML에서는 <br>로 써도 되지만 XHTML에서는 반드시 <br/>로 써야한다는 정도의 문법이 다를 뿐이다. 이런 점에서 굳이 XHTML을 써야할 기술적 이유는 없다는 것이다. 한편 HTML5가 발표되면서 XHTML은 거의 사용되지 않고 있다. (참고)

+

data-* 속성

+

HTML5에서 새로 추가된 data- 속성은 커스텀 데이터 속성으로, 개발자가 임의로 이름을 붙일 수 있는 속성이다. data- 속성은 <html> 태그 상에서 별다른 작용을 하지 않는다. 자바스크립트가 DOM의 데이터에 접근하거나 서버에서 받아온 데이터를 활용해야 할 때 사용된다. (참고)

+

Cookie, sessionStorage, localStorage

+

쿠키(Cookie), 세션 저장소(sessionStorage), 로컬 저장소(localStorage)는 브라우저에 데이터를 저장하기 위한 공간들이다. HTML5 이전에는 쿠키를 주로 사용했다. 하지만 쿠키는 많은 양의 데이터를 저장할 수 없고, 동일한 도메인에 페이지를 요청할 때마다 서버로 함께 전송되며, 변조가 쉬워 보안이 취약해진다. 그래서 HTML5부터는 저장소 객체(Storage object)를 정의하고 있다. 저장소 객체는 세션 저장소와 로컬 저장소 두 가지를 제공한다. 로컬 저장소에 데이터를 자장하면 창이나 탭을 닫아서 세션이 종료돼도 데이터가 보존되고, 열려 있는 모든 창이나 탭이 데이터를 공유하게 된다. 세션 저장소는 반대다. 일반적으로 브라우저는 저장소에 5mb 정도의 공간을 할당하며, 데이터는 키-값 쌍(KVP; Key-Value Pair)을 이용하는 저장소 객체의 속성으로 저장된다. 또한 브라우저는 데이터를 보호하기 위해 동일출처정책에 의거, 서로 다른 페이지는 같은 도메인에 저장된 데이터에만 접근이 가능하도록 제한하고 있다. (참고1) (참고2) (참고3)

+

script 태그의 async와 defer

+

<script> 태그에 async 속성과 defer 속성이 추가된 것은 HTML5부터다. 브라우저가 웹 문서에서 외부 스크립트를 불러오는 <script> 태그를 만나면 해당 스크립트를 내려받아 해석하고 실행할 때까지 HTML 코드 파싱 작업을 뒤로 미룬다. 그래서 무거운 스크립트 문서를 해석할 때는 페이지 전체가 느려지는 현상이 발생한다. 그런데 <script> 태그에 asyncdefer 속성을 지정하면 마크업 코드 작업을 중단하지 않고 스크립트를 동시에 내려받게 된다. defer는 마크업 파싱을 마친 다음 스크립트를 실행하며, async는 스크립트를 내려받는 즉시 스크립트를 실행한다. (참고)

+

Progressive rendering

+

Progressive rendering은 콘텐츠를 빠르게 화면에 렌더링하는 기법이다. 브로드밴드 인터넷이 등장하기 전에 매우 중요한 기술이었고, 모바일 플랫폼을 고려한다면 여전히 무시할 수 없는 부분이다. 가시영역의 이미지만 로딩해주는 jQuery 플러그인 Lazy loading이 좋은 예다. (참고)

+

CSS

+

Reset CSS

+

모든 브라우저에서 통일된 화면을 볼 수 있도록 CSS의 기본값을 초기화하는 것을 말한다. Eric Meyer’s ResetNormalize.css가 주로 쓰인다.

+

BFC(Block Formatting Context)와 IFC(Inline Formatting Context)

+

모든 HTML 요소는 사각형 박스 형태를 취하고 있다. 박스는 Box-model이라는 모델을 가지고 있다. CSS요소에는 display가 존재하는데, 이는 블록(Block)과 인라인(Inline) 두 가지 값을 가질 수 있다. block은 블록 레벨 요소(Block-level elements)를 의미하며, 블록 레벨 요소는 BFC에 속하는 박스이다. 블록 레벨 요소 박스는 수직으로 계속 쌓인다. inline은 인라인 레벨 요소(Inline-level elements)를 의미한다. 인라인 레벨 요소는 인라인 레벨 박스를 생성하며, 이는 IFC에 속한다. 인라인 레벨 요소 박스는 수평으로 계속 쌓인다. 또한 인라인 레벨 박스에 borderpadding이 눈에 보이더라도 사실은 line-height에 의해 높이가 조절된다. inline-block은 특이한데, 이 요소는 인라인 요소처럼 수평으로 쌓이지만 블록 레벨 요소의 박스처럼 높이를 계산한다. 즉 line-height에 의존하지 않는다. (참고)

+

클리어링(Clearing) 기술

+

클리어링은 float 속성이 주변 요소의 배치에 영향을 미치지 않도록하는 것이다. float 속성을 가진 요소는 자신의 위치를 주변 콘텐츠로부터 상대적으로 배치하기 때문에 다른 콘텐츠가 그 주위로 흐르게 된다. 클리어링를 통해 이를 방지할 수 있는데, 여기에는 4가지 방법이 있다.

+

첫 번째는 floatfloat으로 대응하는 것이다. 자식 요소 뿐만 아니라 부모 요소에게도 float 속성을 지정하면 부모 요소가 자식 요소의 높이를 반영한다. 단, 이렇게 하면 부모 요소의 너비가 자식 요소를 담을 정도로 줄어든다.

+

두 번째는 floatoverflow 속성으로 대응하는 것이다. 자식 요소의 높이를 부모에게 반영하는 방법으로, 부모 요소의 overflow 속성에 auto 또는 hidden 값을 부여한다. 하지만 auto 값의 경우 자식 요소가 부모 요소보다 클 때 스크롤바가 생기며, hidden 값의 경우 넘치는 부분이 잘려버린다.

+

세 번째로 float을 빈 엘리먼트로 클리어링할 수도 있다. float이 지정된 요소 뒤에 빈 요소를 추가하고 빈 요소의 clear 속성에 both 값을 부여하는 것인데, 의미없는 요소를 사용하게 되기 때문에 권장하지 않는다.

+

마지막으로 float을 가상 선택자 :after로 클리어링하는 방법이 있다. 가상 선택자는 가상 클래스(pseudo-class)와 가상 요소(pseudo-element)로 나뉜다. 가상 클래스는 특정 요소에 아무런 class를 부여하지 않았지만 마치 class를 변경한 것과 같은 역동적인 효과를 낼 수 있는 것들이다. :hover, :active, :focus 등이 여기에 속한다. 가상 요소는 존재하지 않는 요소를 가상으로 생성하는 선택자다. :first-line, :before, :after가 여기에 속한다. 가상 요소는 HTML 문서에 존재하지 않는 콘텐츠를 출력하기도 한다. 이렇게 가상 요소를 생성한 다음 display: block; clear: both; 처리를 하면 깔끔하게 클리어링을 할 수 있다. 가장 권장하는 방법이다.

+

Image Replacement

+

요소를 이미지로 교체하는 기법이다. 기본적으로 다음과 같이 하면 된다:

+
.elements {
+  background-image: url("image.png");
+}
+
+

그리고 A History of CSS Image Replacement에서 더 많은 예시를 볼 수 있다.

+

IE box model과 W3C box model의 차이

+

브라우저에 따라 박스 모델이 달라 요소에 지정된 너비와 높이가 같아도 서로 다르게 렌더링된다. 박스 모델은 기본적으로 margin, border, padding, content로 구성되어 있다.

+

+

W3C의 표준 박스 모델은 콘텐츠의 너비, 높이만을 widthheight로 계산하는 반면, IE의 박스 모델은 콘텐츠와 padding, border를 포함한 너비와 높이를 width, height로 계산한다. 이를 위한 해결책은 (1)DTD를 통해 브라우저가 쿽스 모드로 동작하지 않도록 하거나 (2)wrapper 요소를 사용해 wrapper 요소에 width, height 값을 할당하고 내부 엘리먼트에 padding, border 값을 할당하거나 (3)Conditional comment를 추가하거나 (3)css hack을 사용하는 방법이 있다. (참고)

+

그리드 시스템 (Grid system)

+

반응형 웹페이지를 만들기 위해 자주 쓰이는 기술이다. 페이지 위에 격자를 그리고 그 위에 요소를 그 위에 배치하는 방법으로, 기술적인 이유만이 아니라 디자인적인 이유로도 사용한다. 그리드 레이아웃은 CSS를 이용해 만들 수 있다. 쉽게 그리드 시스템을 사용하기 위해서 부트스트랩을 사용하기도 한다.

+

CSS 전처리기

+

CSS 문서가 방대해짐에 따라 작업 효율을 높이기 위해 등장한 기술이다. 전처리기를 사용하면 CSS 상의 반복적인 부분을 스크립트나 변수로 처리할 수 있고, 다양한 연산이 가능해진다. LessSass가 대표적인 CSS 전처리기이다.

+
@bgColor: #DFDFDF;
+body {
+  background-color: @bgColor;
+}
+
+

위는 Less의 예시다. 컴파일을 하면 bodybackground-color 값이 #DFDFDF로 치환된 CSS 코드를 얻을 수 있다.

+

반응형 디자인(Responsive design)과 적응형 디자인(Adaptive design)

+

반응형 디자인은 디스플레이의 너비에 따라 레이아웃을 변형시키고, 적응형 디자인은 고정적 레이아웃을 가진다. 반응형 웹이 미디어쿼리를 사용해 스타일 분기를 나누는 방법이라면 적응형 웹은 디바이스를 체크해 그 디바이스에 최적화된 마크업을 호출하는 방법이다. (참고)

+

Javascript

+

추상적 같음 비교(Abstract equality comparison)와 엄격한 같음 비교(Strict equality comparison)

+

추상적 같음 비교(==)는 두 변수를 같은 데이터 타입으로 변환한 다음 값을 비교하는 반면, 엄격한 같음 비교(===)는 두 변수의 값과 데이터 타입을 함께 비교한다. 따라서 값과 타입이 완전히 일치해야 true를 반환한다. 엄격한 같음 비교를 사용한 비교 결과는 예측이 쉽고 타입 강제가 일어나지 않기 때문에 추상적 같음 비교를 사용하는 것보다 낫다. (참고)

+

고급 예외처리(Advanced exception handling)

+

try...catchthrow로 고급 예외처리를 할 수 있다. 참고로 프로그램 실행 중에 발생하는 오류는 예외(Exception)이라고 하며, 코드의 문법적 오류는 에러(Error)라고 한다. try...catch는 구동 중 코드상에서 발생할 수 있는 오류들을 잡아준다.

+
try {
+  // try 블록은 예외가 발생할 수도 있는 부분
+} catch(e) {
+  // catch 블록은 try 블록에서 예외가 발생했을 때 호출되는 부분
+  // 객체 e는 name과 message 속성을 가짐
+} finally {
+  // 선택적으로 finally 블록을 추가할 수 있음
+  // 예외 발생 여부를 떠나 무조건 실행되어야 할 부분
+}
+
+

throw문은 강제로 예외를 발생시킬 때 사용한다. throw의 문법은 다음과 같다:

+
throw expression;
+
+

표현식의 값은 숫자, 문자 등 어떤 것이든 상관없다. throw가 발생하면 가장 가까운 catch 블록으로 이동한다. 다음과 같이 사용하면 된다:

+
try {
+  if(a > 100) {
+    throw "Value too high";
+  }
+} catch(e) {
+  alert(e);
+}
+
+

(참고1) (참고2)

+

Event delegation

+

이벤트 리스너를 등록하는 것은 DOM 입장에서는 부담스러운 일이며, 리소스를 상당히 잡아먹는 작업이다. 그래서 요소 하나하나에 이벤트를 등록하는 대신 위임을 하는 것이 바람직하다. 자식 요소들을 감싼 부모 요소에 이벤트를 등록해 이벤트를 위임하면 각각의 자식 요소들에 이벤트를 등록하는 효과를 낼 수 있다. 이벤트를 위임하면 리소스를 절약할 수 있을 뿐만 아니라 DOM 트리에 새로운 요소를 추가했을 때 코드를 추가할 필요가 없어지고, 코드의 양을 줄일 수도 있다. 다음은 ul 요소의 자식 요소인 li 요소를 클릭하면 해당 li 요소가 사라지는 코드이다.

+
<ul id="todoList">
+  <li>TODO: A</li>
+  <li>TODO: B</li>
+  <li>TODO: C</li>
+</ul>
+
+

아주 평범한 리스트이다.

+
function getTarget(e) {
+  if(!e) { // event 객체가 존재하지 않으면
+    e = window.event; // IE의 event 객체를 사용
+  }
+
+  return e.target || e.srcElement; // 이벤트가 발생한 요소를 가져옴
+}
+
+function itemDone(e) {
+  var elTarget, elParent;
+
+  elTarget = getTarget(e); // 이벤트가 발생한 요소 가져옴 (li)
+  elParent = target.parentNode; // 해당 요소의 부모 요소를 가져옴 (ul)
+  elParent.removeChild(elTarget); // 이벤트가 발생한 요소를 제거함 (li)
+}
+
+(function(){
+  var el = document.getElementById('todoList');
+
+  if(el.addEventListener) { // 이벤트 리스너가 지원되면
+    el.addEventListener('click', function(e) { // 클릭 이벤트에 리스너를 지정
+      itemDone(e);
+    }, false); // 이벤트 버블링을 사용
+  } else { // 이벤트 리스너가 지원되지 않으면
+    el.attachEvent('onclick', function(e) { // IE의 onclick 이벤트를 사용
+      itemDone(e);
+    }
+  });
+})();
+
+

IE까지 고려한 코드다. 하나의 이벤트 리스너로 요소 3개를 제어하고 있다다. jQuery는 보다 편하게 이벤트를 바인딩할 수 있도록 .delegate() 메소드를 제공하고 있다.

+

Prototype 기반 상속

+

자바스크립트는 클래스라는 개념이 없기 때문에 프로토타입(Prototype)이 클래스를 대신해 객체지향 프로그래밍의 핵심을 맡는다. 다만 ES6부터 클래스 문법이 추가됐기 때문에 프로토타입에 직접 접근할 필요가 없어졌다. 자바스크립트의 OOP는 진정한 OOP가 아닌가?를 참고해보자. 그래도 프토로타입은 여전히 자바스크립트의 기반이기 때문에 알아두는 것이 좋다. 자바스크립트에서는 이 프로토타입을 통해 다른 객체지향 언어에서 쓰이는 클래스를 구현할 수 있다.

+
function Player() {
+  this.hp = 100;
+  this.mp = 50;
+}
+
+var kim = new Player();
+var park = new Player();
+
+console.log(kim.hp); // 100
+console.log(kim.mp); // 50
+
+console.log(park.hp); // 100
+console.log(park.mp); // 50
+
+

kimparkhpmp를 각각 100, 50씩 가지고 있다. 만약 객체를 100개 만들면 200개의 변수가 메모리에 할당된다. 프로토타입을 활용하면 메모리를 아낄 수 있다.

+
function Player() {}
+Player.prototype.hp = 100;
+Player.prototype.mp = 50;
+
+var kim = new Player();
+var park = new Player();
+
+

kimpark은 프로토타입에 연결된 Player 객체의 값을 가져다 쓸 수 있다. hpmpkimpark이 공유하는 것이다. 이러한 매커니즘으로 프로토타입을 이용해 상속을 구현할 수 있다. (참고1) (참고2)

+

null, undefined, undeclared의 차이

+

이건 종종 헷갈리는 경우가 있다. null은 어떠한 값도 가리키지 않는다는 의미의 원시값이다. 변수를 선언하고 null을 할당한 경우 해당 변수가 어떠한 값도 가지지 않았다는 의미가 된다. undefined는 변수가 선언됐지만 값이 할당되지 않았다는 것을 의미하는 값이다. 즉, null은 개발자로부터 할당되는 값이고, undefined는 아예 할당을 하지 않은 상태다. undeclared는 변수 자체가 선언되지 않았다는 의미다. 콘솔에서는 undefined와 똑같이 표기되기 때문에 변수가 초기화되지 않았다는 것인지, 아예 선언되지 않았다는 것인지 확인할 필요가 있다.

+

클로저(Closure)

+

자바스크립트에서는 함수 안에 또 다른 함수를 선언할 수 있는데, 함수 안의 함수인 내부함수(inner function)는 외부함수(outer function)의 지역변수에 접근할 수 있다. 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수는 외부함수에 접근할 수 있다. 이러한 메커니즘을 클로저라고 한다. MDN에 클로저를 활용한 재밌는 예제가 있다:

+
function makeAdder(x) {
+  return function(y) {
+    return x + y;
+  };
+}
+
+var add5 = makeAdder(5);
+var add10 = makeAdder(10);
+
+console.log(add5(2));  // 7
+console.log(add10(2)); // 12
+
+

여기서 makeAdder(x)x를 인자로 받아서 새로운 함수를 반환한다. 그리고 반환되는 내부 함수는 y를 인자로 받아 x + y를 반환한다. (참고1) (참고2)

+

자바스크립트 모듈 패턴(Module pattern)

+

모듈 패턴(Module pattern)은 코드 설계 방법을 말한다. 여기서는 객체를 public과 private으로 나누는 캡슐화가 핵심이다. 자바스크립트는 public이나 private와 같은 접근 제한자를 제공하지 않지만, 클로저를 이용해 구현할 수 있다. private method는 코드 접근을 제한할 수 있을뿐만 아니라 추가적인 자바스크립트가 다른 스크립트와 이름이 충돌하는 것을 막을 수 있다. 매우 기본적인 방법은 모든 코드를 익명함수 안에 집어넣어 private 스코프로 만드는 것이다. 하지만 이렇게 하면 코드를 재사용하기 불편해지기 때문에 별도의 네임스페이스를 적용해야 한다. (참고1)

+

네이티브 객체(Native object), 호스트 객체(Host object), Built-in 객체

+

객체는 크게 3가지로 구분된다. 네이티브 객체는 ECMAScript 명세에 정의된 객체로, 자바스크립트의 모든 엔진에 구현된 표준객체이다. BOM(Browser Object Model)과 DOM 등은 모두 네이티브 객체며, 자바스크립트 엔진을 구동하는 측에서 빌드되는 객체이다. 호스트 객체는 개발자가 정의한 객체이다. 마지막으로 Built-in 객체는 자바스크립트 엔진을 구성하는 기본 객체들을 포함한다. Number, String, Array, Date 등의 내장객체들이 있다. (참고)

+

기능 검출(Feature detection)과 기능 추론(Feature inference)

+

기능 검출이란 스크립트가 호출하는 기능을 사용자의 브라우저가 지원하는지 체크하는 것을 말한다. 다음은 브라우저가 GPS를 지원하는지 확인하는 코드다.

+
if(navigator.geolocation) {...}
+
+

기능 추론도 기능 검출처럼 브라우저가 특정 기능을 지원하는지 체크하는 것이다. 하지만 'A기능을 지원하면 B기능도 지원할 것이다.'라는 추론이 바탕이 된다. 별로 좋지 않은 방법이다. (참고)

+

호이스팅(Hoisting)

+

호이스팅은 인터프리터가 스크립트를 해석할 때, 변수의 정의가 스코프에 따라 선언과 할당으로 분리되어 선언을 항상 컨텍스트의 최상위로 끌어올리는 것을 의미한다. 즉, 변수를 어디에서 선언하든 인터프리터는 최상단에서 선언한 것으로 해석한다. 이는 변수 뿐 아니라 함수에도 적용된다. 따라서 상단에서 함수를 호출하고, 하단에서 함수를 정의해도 기능적인 문제는 없다. (참고)

+

이벤트 흐름(Event flow)

+

HTML 요소가 다른 요소의 내부에 중첩되어 있을 때 자식 요소를 클릭하면 부모 요소를 클릭한 셈이 된다. 이처럼 이벤트는 흐름을 가지고 있으며, 이것을 이벤트 흐름이라고 부른다. 이벤트 흐름에는 두 가지 방식이 있다. 먼저 이벤트 버블링(Event bubbling)은 이벤트가 직접적으로 발생한 노드로부터 시작해 바깥 노드로 이벤트가 퍼져 나가는 방식을 말한다. 대부분의 브라우저가 기본적으로 이 방식을 지원한다. 반대로 이벤트 캡쳐링(Event capturing)은 바깥 노드부터 시작해서 안쪽으로 퍼지는 방식이다. IE8 혹은 그 이전 버전에서는 지원되지 않는다.

+

document load event와 DOMContentLoaded event

+

DOM을 제어하는 스크립트는 마크업의 모든 요소에 대한 처리가 끝난 뒤에 로드되어야 한다. 그래서 보통 <body> 태그 최하단에서 스크립트를 불러오도록한다. 또 다른 방법은 이벤트를 이용하는 것이다. document load event는 페이지의 모든 리소스가 로드된 이후에 실행된다. 때문에 구동이 지연되어 사용자 경험을 저하할 수 있다. 반면 DOMContentLoaded event는 스크립트 로드를 마치고 실행이 가능한 시점에 바로 실행된다. (참고)

+

조건부 삼항 연산자(Conditional ternary operator)

+

보통 그냥 '삼항 연산자’이라고 부른다. if문을 축약해서 쓸 수 있는 유용한 연산자이지만, 과하게 사용하면 코드의 가독성을 떨어뜨릴 수 있다.

+
var a = 1, b = 2;
+
+console.log(a < b ? "True" : "False"); // "True"
+console.log(a > b ? "True" : "False"); // "False"
+
+a < b ? (
+  console.log("True");
+  alert("True");
+) : (
+  console.log("False");
+  alert("False");
+); // "True"
+
+

조건이 true면 전자의 값을 반환하고, false면 후자의 값을 반환한다.

+

use strict

+

ES6에서 새로 추가된 기능으로, 스크립트에 "use strict;" 구문을 추가하면 strict mode에서 실행하게 된다. strict mode는 코딩 실수를 찾아 예외를 발생시키고, 전역 객체에 접근하는 것과 같은 위험한 액션을 막는다. 스크립트 전체에 적용할 수도 있고 특정 함수에만 적용할 수도 있다. (참고)

+

Call stack과 Task queue

+

자바스크립트 엔진은 요청이 들어올 때마다 요청을 순차적으로 call stack에 담아 하나씩 처리한다. call stack은 하나만 존재하기 때문에 요청도 하나씩만 처리할 수 밖에 없다. task queue는 처리해야 하는 task를 임시로 저장해두는 큐다. call stack이 비워지면 task queue에 있던 task들이 순서대로 call stack에 push된다. 가령 setTimeout() 함수로 10초의 딜레이를 둔다면, 그동안 setTimeout()이 처리할 task는 task queue에 쌓이고 다른 부분의 스크립트들이 실행된다. 따라서 딜레이를 0으로 줘도 setTimeout()의 task는 다른 것들보다 나중에 처리된다. (참고)

+ +
+
+ +
+ +
+
+

ES6와 함께 JavaScript로 OOP하기

+

자바스크립트의 OOP는 진정한 OOP가 아닌가?

+
+
+
+ + +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/1.html b/article/1.html new file mode 100644 index 0000000..8c4f2ce --- /dev/null +++ b/article/1.html @@ -0,0 +1,364 @@ + + + + + + ES6와 함께 JavaScript로 OOP하기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ ES6와 함께 JavaScript로 OOP하기 +

+ +

+ 자바스크립트의 OOP는 진정한 OOP가 아닌가? +

+ + + + +
+
Table of Contents +

+
+

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 절차지향 프로그래밍(Procedural Programming)과 대비되는 프로그래밍 방법론이다.

+

절차지향 프로그래밍 방식 언어로는 대표적으로 C언어가 있다. 일반적으로 C코드는 특정 기능을 수행하는 함수들로 구성되어 있다. C 프로그래머는 프로그램의 각 기능을 구현하고, 이 기능들이 어떤 절차로 수행되는가를 중심으로 개발한다. 객체지향 프로그래밍 방식 언어는 Java가 대표적이다. Java 프로그래머는 프로그램에 필요한 각 객체들의 속성과 동작을 구현하고, 이 객체들이 어떻게 상호작용하는가를 중심으로 개발한다. 절차지향 프로그래밍은 명령을 순차적으로 수행하고, 객체지향은 그렇지 않다는 의미는 아니다. C든 Java든 기본적으로 명령은 순서대로 수행된다.

+

C와 Java를 예시로 들었는데, 절차지향과 객체지향은 패러다임이지 언어의 속성은 아니다. Java로도 절차지향 프로그래밍을 할 수 있고, C언어로 객체지향 프로그래밍을 할 수 있다. 다만 C언어는 문법 자체로 객체지향을 지원하지 않기 때문에 매우 비효율적이다.[1] 반면 Java는 언어가 차체적으로 객체지향 프로그래밍을 위한 다양한 문법을 제공하고 있으며, 굳이 자바로 절차지향 프로그래밍을 할 이유는 없다.

+

OOP의 가장 큰 장점은 유지보수가 쉽다는 것이다. 특히 다른 사람과 함께 개발해야 할 때 발생하는 혼란을 줄여준다. 다른 사람들과 절차지향 프로그래밍 방식으로 프로젝트를 해봤다면 알겠지만, 내가 작업하는 코드에 다른 사람이 구현한 함수를 가져다 써야하는 경우가 있다. 반대로 내가 구현한 함수를 다른 사람이 가져다 쓰는 경우도 있다. 여기서 문제가 발생한다. 내가 작성한 함수를 다른 부분에도 사용하기 위해 내용을 고치면 내 함수를 쓰던 다른 사람의 코드에 문제가 생길 수 있다. 따라서 다른 사람이 작성한 코드를 매번 해석해야 하며, 내 함수가 어디서 어떻게 사용되는지 항상 신경쓰고 있어야 한다.

+

객체지향 프로그래밍 방식으로 개발을 한다면 이런 문제를 해결할 수 있다. 각 기능을 독립적인 모듈로 관리할 수 있으며, 다른 사람이 내 코드의 내용을 직접 수정하지 않고 데이터에 접근하게 만들 수 있다. 따라서 코드 재사용성을 높이고 의존성을 관리하기 쉬워진다. 대신 코드 설계를 잘해야 한다. 객체 사이의 관계를 생각하지 않고 무작정 코드를 작성하기 시작하면 모든 것이 꼬여버릴 수 있다.

+

+

처음 객체지향 프로그래밍 방식으로 개발을 하면 굉장히 번거롭다고 느껴진다. 특히 바로 결과물이 나오지 않고 설계도를 그려야 한다는 점이 답답하다. 말을 만들기 위해 말 공장을 먼저 만들어야 한다. 공장의 장점은 한 번 만들어두면 이후에 반복적으로 사용할 수 있고, 말에 추가적인 기능을 붙일 때도 공장에 장비를 하나 더 들여놓기만 하면 된다는 것이다. (실제로 공장에서 제품을 찍어내는 듯한 디자인 패턴인 factory pattern이 있다.) 객체지향 방식은 현실 세계를 표현하기 적합하고, 또 직관적이기도 하다. 코드와 서비스의 미래를 생각한다면 객체지향 프로그래밍이 필요하다.

+

자바스크립트로도 객체지향 프로그래밍을 할 수 있을까? 한때 자바스크립트로 객체지향 프로그래밍을 한다고 하면 "자바스크립트의 객체지향은 진정한 객체지향이 아니다"라고 하는 사람들이 있었다.

+

하지만 자바스크립트로도 객체지향 프로그래밍을 할 수 있다. 자바스크립트는 프로토타입을 기반으로 OOP의 대표적 특성인 캡슐화, 추상화, 다형성, 상속 등을 구현할 수 있다. 다음은 자바스크립트로 고양이 클래스를 만든 코드다:

+
function Cat(name, age) {
+    this.name = name;
+    this.age = age;
+};
+
+Cat.prototype.makeNoise = function() {
+    console.log('Meow!');
+}
+
+var cake = new Cat('Cake', 3);
+
+cake.makeNoise(); // 'Meow!'
+
+

다른 객체지향 언어를 사용해 본 사람이라면 일반적인 객체지향 언어와 굉장히 다르다는 것을 알 수 있다. 객체지향하면 떠오르는 클래스없이 함수가 쓰였고, 심지어 메소드는 그 함수 밖의 프로토타입에서 정의되었다. 자바스크립트의 프로토타입 기반 객체지향 프로그래밍[2]에 생소한 사람에게는 '이건 뭔가 잘못됐어’하는 생각이 들 수 있다.

+

ES6에는 클래스 기반 객체지향 프로그래밍 문법이 추가되면서 자바나 C++같은 다른 객체지향 언어들과 비슷한 방식으로, 보다 간결하게 객체지향 프로그래밍을 할 수 있게 되었다. (프로토타입 기반 OOP는 MDN의 Object-oriented JavaScript for beginners를 참고하자.)

+

Class

+
// Animal.js
+class Animal {
+
+}
+
+export default Animal;
+
+

클래스는 객체의 설계도다. 클래스의 내용을 바탕으로 인스턴스를 찍어낸다. 자바스크립트에서 클래스 선언은 아주 간단하다. 클래스 선언은 호이스팅되지만, let이나 const 처럼 그 값이 초기화되지는 않기 때문에 선언 전에 클래스를 사용하면 ReferenceError 예외를 던진다. 맨 마지막 라인 export default Animal은 Animal.js 파일에서 Animal 클래스를 외부로 export하기 위한 코드다. 클래스 외에도 함수나 변수 등을 export할 때도 이를 사용할 수 있다.

+

클래스 문법이 추가됐지만, 엄밀히 말하자면 진짜 클래스가 아니라 함수다. 즉, 자바스크립트에 새로운 객체지향 모델이 도입된 것이 아니고, 문법적으로만 클래스를 지원하게 된 것이다. 호이스팅된 함수는 선언 전에 사용할 수 있지만, 호이스팅된 클래스는 선언 전에 사용할 수 없다는 차이만 빼면 위 코드는 function Animal() { }과 같다.

+
// index.js
+import Animal from './Animal';
+
+let anim = new Animal();
+
+

다른 파일에서 Animal 클래스에 접근하려면 우선 Animal 클래스를 import해야 한다. (앞서 export default Animal 라인을 작성한 이유다.) anim 변수를 만들고 new 키워드를 통해 Animal을 생성할 수 있다. 여기서 anim은 Animal 클래스를 가리키는 레퍼런스 변수(Reference variable)이며, 인스턴스(Instance)라고 부른다. 그리고 이것이 바로 클래스라는 설계도를 이용해 인스턴스라는 개체를 생성하는 과정이다.

+

Constructor

+
// Animal.js
+class Animal {
+    constructor(name) {
+
+    }
+}
+
+export default Animal;
+
+

클래스는 하나의 constructor를 가질 수 있다. constructor는 new Animal(); 명령을 통해 실행되어 인스턴스를 초기화하는 역할을 한다. 또한 constructor에는 name처럼 매개변수를 둘 수도 있다. 만약 constructor를 명시하지 않는다면 비어있는 default constructor가 만들어진다. 굳이 빈 constructor를 만들 필요는 없다.

+

Instance variable

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+}
+
+export default Animal;
+
+

클래스의 멤버 프로퍼티는 constructor 안에 선언한다. 다른 언어에서는 이를 인스턴스 변수(Instance variable)라고 부르지만, 앞서 언급했듯 클래스는 사실 함수고, 자바스크립트에서 함수는 객체이기 때문에 this.name은 변수가 아닌 프로퍼티(Property)다.

+
// index.js
+import Animal from './Animal';
+
+let anim = new Animal('Jake');
+
+

인스턴스를 생성할 때 매개변수를 넘겨줄 수 있다. anim 인스턴스의 프로퍼티 name의 값은 'Jake’다.

+

Method

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+
+    getName() {
+        return this.name;
+    }
+}
+
+export default Animal;
+
+

메소드는 함수와 비슷하며, 메소드는 객체의 동작을 정의한다. getName() 메소드는 Animal 클래스의 프로퍼티인 this.name을 반환한다.

+
// index.js
+import Animal from './Animal';
+
+let anim = new Animal('Jake');
+
+console.log(anim.getName()); // 'Jake'
+
+

호출 역시 직관적이다.

+

Static Method

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    static sleep() {
+        console.log('Zzz');
+    }
+}
+
+export default Animal;
+
+

메소드 앞에 static 키워드를 붙여주면 따로 인스턴스를 생성하지 않고 메소드를 호출할 수 있다.

+
// index.js
+import Animal from './Animal';
+
+let anim = new Animal('Jake');
+
+Animal.sleep(); // 'Zzz'
+anim.sleep(); // Uncaught TypeError: anim.sleep is not a function
+
+

인스턴스를 통해 static 메소드를 호출하면 TypeError가 발생한다.

+

Information Hiding

+

자바스크립트에는 은닉된 프로퍼티라는 개념이 없다. 자바에는 private, protected, public과 같은 접근제어자가 있어서 외부에서 인스턴스 멤버에 접근하는 것을 통제할 수 있지만, 자바스크립트는 클래스의 모든 프로퍼티가 public이다.

+

종종 프로퍼티 이름 앞에 언더스코어를 붙이는 방식(this._name)으로 private한 변수임을 표현하는 경우도 있는데, 실제로 프로퍼티가 private하게 동작하는 것은 아니기 때문에 오해를 불러일으킨다는 의견이 있다. Airbnb JavaScript 스타일 가이드를 참고.

+

프로퍼티 대신 변수를 사용하면 정보를 은닉하는 효과를 낼 수 있다.

+
// Animal.js
+class Animal {
+    constructor(name) {
+        let name = name;
+
+        this.getName = () => {
+            return name;
+        };
+
+        this.setName = (newName) => {
+            name = newName;
+        }
+    }
+}
+
+export default Animal;
+
+

변수는 해당 블록 안에만 존재하기 때문에 해당 블록을 벗어나서 접근하면 undefined가 된다. (단, 블록 스코프를 갖는 let, const와 달리 var는 함수 스코프를 갖는다.) 따라서 constructor 안에 변수를 선언하면 외부에서 name에 직접 접근할 수 없다. 더불어 name을 가져오는 프로퍼티와 name을 설정하는 프로퍼티를 두면 외부에서 getNamesetName을 통해 name에 간접적으로 접근할 수 있다.

+

Inheritance & Polymorphism

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+
+    getName() {
+        return this.name;
+    }
+}
+
+export default Animal;
+
+

상속은 OOP 개념 중 하나다. 상속은 말그대로 해당 클래스의 모든 내용을 다른 클래스에 그대로 복사한다는 의미다. 즉, Animal 클래스의 프로퍼티 this.name과 메소드 getName()을 다른 클래스에 그대로 상속할 수 있다.

+
// Dog.js
+import Animal from './Animal';
+
+class Dog extends Animal {
+    constructor(name) {
+        super(name);
+    }
+}
+
+export default Dog;
+
+

extends 키워드를 사용해 Dog 클래스가 Animal 클래스를 상속했다. 이제 Animal 클래스는 Dog 클래스의 superclass가 되었고, Dog 클래스는 Animal 클래스의 subclass가 되었다. Dog 클래스는 Animal 클래스가 가지고 있는 this.namegetName()을 똑같이 갖는다.

+

subclass의 constructor에는 super()를 넣어 superclass의 constructor를 호출할 수도 있다. subclass에서 super()를 사용하지 않아도 되는 경우 에러가 발생하지는 않지만, 그래도 super()를 명시하길 권장한다.

+

클래스를 상속할 때는 IS-A 관계나 HAS-A 관계를 만족하는지 확인해야 한다. 가령 "사과는 과일이다(Apple is a fruit)"는 IS-A 관계를 만족하므로 Fruit 클래스가 Apple 클래스의 superclass가 될 수 있다. 한편 "차에는 바퀴가 있다(Car has a wheel)"는 HAS-A 관계를 만족하므로 Car 클래스가 Wheel 클래스의 superclass가 될 수 있다.

+
// index.js
+import Dog from './Dog';
+
+let jake = new Dog('Jake');
+
+console.log(jake.getName()); // 'Jake'
+
+

이런 식으로 사용한다. Dog 인스턴스 jake가 Animal 클래스의 getName()을 호출한다.

+

Overriding

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    makeNoise() {
+        console.log('It makes a noise');
+    }
+}
+
+export default Animal;
+
+

오버라이딩(Overriding)은 subclass가 superclass의 메소드를 덮어쓰는 것을 말한다. 먼저 Animal 클래스에 makeNoise() 메소드를 추가했다.

+
// Dog.js
+import Animal from './Animal';
+
+class Dog extends Animal {
+    constructor(name) {
+        super(name);
+    }
+
+    // Override
+    makeNoise() {
+        console.log('Bark!');
+    }
+}
+
+export default Dog;
+
+

Dog 클래스에 같은 이름의 메소드 makeNoise()를 정의했다.

+
// index.js
+import Dog from './Dog';
+
+let jake = new Dog('Jake');
+
+console.log(jake.getName()); // 'Jake'
+jake.makeNoise(); // 'Bark!'
+
+

Animal 클래스의 makeNoise()가 Dog 클래스의 makeNoise()로 오버라이드된 것을 볼 수 있다.

+

Overloading

+

오버로딩(Overloading)은 같은 이름, 다른 매개변수를 가진 메소드가 여러 개 존재하는 것을 말한다. 매개변수가 다르면 다른 메소드임을 알 수 있기 때문에 가능한 기능인데, 자바스크립트에서는 기본적으로 불가능하다. (대신 매개변수의 존재 여부에 따라 분기를 나누는 방식으로 구현할 수는 있다.) 한 클래스 안에 같은 이름을 가진 메소드가 여러 개 존재할 수 없으며, constructor도 반드시 하나만 있어야 한다.

+

Abstract

+

Animal 클래스가 분명 존재하지만, 단순히 '동물’을 만든다는 것은 조금 이상한 일이다. 동물은 추상적인 개념이기 때문에 Animal 객체를 생성하는 일이 있어서는 안 된다. 이럴 때 추상화(Abstraction)를 통해 new Animal(...);과 같은 명령을 미연에 방지할 수 있다. Java의 경우 public abstract class Animal {...}과 같은 방식으로 추상 클래스를 만들 수 있다. 아쉽지만 자바스크립트에서는 추상 클래스나 메소드를 만들 수 없다. 다만 추상 메소드를 직접 구현하는 방법은 있다.

+
// Animal.js
+class Animal {
+    constructor(name) {
+        this.name = name;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    // Abstract
+    makeNoise() {
+        throw new Error('makeNoise() must be implement.');
+    }
+}
+
+export default Animal;
+
+

makeNoise()를 추상 메소드로 만들어 subclass에서 구현되지 않은 makeNoise()를 호출하면 에러를 발생시키도록 했다. 이 경우 추상 메소드는 반드시 subclass에서 오버라이드되어야 한다.

+

추상 클래스를 만드는 것을 조금 더 번거롭다. 직접 Abstract 클래스를 만들어 상속시키는 방식인데, 스택오버플로우의 Does ECMAScript 6 have a convention for abstract classes?를 참고해보자.

+

Interface

+

인터페이스(Interface)는 추상 메소드들의 집합이다. 클래스와는 다르며, 인스턴스 변수를 가질 수 없다. 자바의 경우 인터페이스는 public interface Pet {...}과 같이 만들고, 다른 클래스에서 public class Dog extends Animal implements Pet과 같은 방식으로 구현(Implement)한다. 이 코드에서 Dog 클래스는 Animal 클래스를 상속받고, Pet 인터페이스를 구현한다. 즉, Animal 클래스의 메소드, 인스턴스 변수와 Pet 인터페이스의 추상 메소드들을 가진다.

+

인터페이스만 보면 이를 구현하는 클래스가 어떤 동작을 하는지 직관적으로 볼 수 있고, 자바에서는 각 타입별로 새로운 메소드를 오버로딩할 필요가 없어진다. (자바에서의 인터페이스는 점프 투 자바를 참고.) 매우 편리한 기능이지만, 자바스크립트는 타입이 없는 덕 타이핑(Duck typing) 언어이기 때문에 인터페이스와 같은 문법이 없다. 한편 타입스크립트에는 자바와 유사한 방식으로 인터페이스를 사용할 수 있다.

+
+
+
    +
  1. Adam Rosenfield, “Object-orientation in C”, Stack Overflow, 2009. ↩︎

    +
  2. +
  3. 임성묵, “자바스크립트는 왜 프롵토타입을 선택했을까”, 2021. ↩︎

    +
  4. +
+
+ +
+
+ +
+ +
+
+

♻️ 자바는 어떻게 Garbage Collection을 할까?

+

오브젝트의 일생

+
+
+
+ + +
+ +
+
+

📋 프론트엔드 개발자를 위한 토막상식

+

+
+
+
+ +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/10.html b/article/10.html new file mode 100644 index 0000000..accdc0a --- /dev/null +++ b/article/10.html @@ -0,0 +1,137 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.6 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ 🦕 공룡책으로 정리하는 운영체제 Ch.6 +

+ +

+ Synchronization +

+ + + + +
+
Table of Contents +

+
+

프로세스는 동시에 실행될 수 있으며, 여러 개의 프로세스가 협력할 때는 프로세스 사이에 데이터가 동기화되지 않는 문제가 발생할 수 있다.

+

Background

+

만약 두 프로세스가 동시에 어떤 변수의 값을 바꾼다면 프로그래머의 의도와는 다른 결과가 나올 것이다. 이처럼 프로세스가 어떤 순서로 데이터에 접근하느냐에 따라 결과 값이 달라질 수 있는 상황을 경쟁 상태(race condition)라고 한다.

+

The Critical-Section Problem

+

코드상에서 경쟁 조건이 발생할 수 있는 특정 부분을 critical section이라고 부른다. critical section problem를 해결하기 위해서는 몇가지 조건을 충족해야 한다.

+
    +
  • Mutual exclution (상호 배제): 이미 한 프로세스가 critical section에서 작업중일 때 다른 프로세스는 critical section에 진입해서는 안 된다.
  • +
  • Progress (진행): critical section에서 작업중인 프로세스가 없다면 다른 프로세스가 critical section에 진입할 수 있어야 한다.
  • +
  • Bounded waiting (한정 대기): critical section에 진입하려는 프로세스가 무한하게 대기해서는 안 된다.
  • +
+

Non-preemptive kernels로 구현하면 임계 영역 문제가 발생하지 않는다. 하지만 비선점 스케줄링은 반응성이 떨어지기 때문에 슈퍼 컴퓨터가 아니고선 잘 사용하지 않는다.

+

Peterson’s Solution

+

Peterson’s solution으로 임계 영역 문제를 해결할 수 있다. 임계 영역에서 프로세스가 작업중인지 저장하는 변수 flagcritical section에 진입하고자하는 프로세스를 가리키는 변수 turn 을 만들어 어떤 프로세스가 임계 영역에 진입하면 flaglock하고, 나오면 unlock하는 방식으로 임계 영역 문제를 해결한다.

+
do {
+  flag[i] = true;
+  turn = j;
+  while (flag[j] && turn == j);
+  // Critical section
+  flag[i] = false;
+  // Remainder section
+} while(true);
+
+

Mutex Locks

+

mutex locks은 여러 스레드가 공통 리소스에 접근하는 것을 제어하는 기법으로, lock이 하나만 존재할 수 있는 locking 매커니즘을 따른다. (참고로 'mutex’는 'MUTual EXclusion’을 줄인 말이다.) 이미 하나의 스레드가 critical section에서 작업중인 lock 상태에서 다른 스레드들은 critical section에 진입할 수 없도록 한다.

+

Semaphores

+

세마포어(Semaphore)는 여러 개의 프로세스나 스레드가 critical section에 진입할 수 있는 locking 매커니즘이다. 세마포어는 카운터를 이용해 동시에 리소스에 접근할 수 있는 프로세스를 제한한다. 물론 한 프로세스가 값을 변경할 때 다른 프로세스가 동시에 값을 변경하지는 못한다. 세마포어는 P와 V라는 명령으로 접근할 수 있다. (P, V는 try와 increment를 뜻하는 네덜란드어 Proberen과 Verhogen의 머릿글자다.)

+

Semaphore Usage

+

세마포어의 카운터가 한 개인 경우 바이너리 세마포어(Binary semaphore), 두 개 이상인 경우 카운팅 세마포어(Counting semaphore)라고 한다. 바이너리 세마포어는 사실상 mutex와 같다.

+

Deadlocks and Starvation

+

두 프로세스가 서로 종료될 때까지 대기하는 프로그램을 실행한다고 생각해보자. 프로세스 A는 B가 종료될 때까지, 프로세스 B는 A가 종료될 때까지 작업을 하지 않기 때문에 프로그램은 어떤 동작도 하지 못할 것이다. 이처럼 두 프로세스가 리소스를 점유하고 놓아주지 않거나, 어떠한 프로세스도 리소스를 점유하지 못하는 상태가 되어 프로그램이 멈추는 현상을 데드락(Deadlock)이라고 한다. 운영체제도 결국 소프트웨어이기 때문에 데드락에 빠질 수 있다.

+

Classic Problems of Synchronization

+

데드락에 관한 유명한 비유가 있다. 철학자 5명이 식탁 가운데 음식을 두고 철학자들은 사색과 식사를 반복한다. 포크는 총 5개, 단 음식을 먹으려면 2개의 포크를 사용해야 한다. 즉, 동시에 음식을 먹을 수 있는 사람은 두 명뿐이다. 운이 좋으면 5명의 철학자들이 돌아가면서 사색과 식사를 이어갈 수 있다. 하지만 모두가 포크를 하나씩 들고 식사를 하려하면 누구도 식사를 할 수 없는 상태, 다시말해 데드락에 빠져 버린다. 이것이 바로 철학자들의 만찬 문제(Dining-Philosophers Problem)이다.

+ +
+
+ +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.7

+

Deadlocks

+
+
+
+ + +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.5

+

Process Scheduling

+
+
+
+ +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/11.html b/article/11.html new file mode 100644 index 0000000..755ebc1 --- /dev/null +++ b/article/11.html @@ -0,0 +1,157 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.7 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ 🦕 공룡책으로 정리하는 운영체제 Ch.7 +

+ +

+ Deadlocks +

+ + + + +
+
Table of Contents +

+
+

Operating System Concepts Ch.7 Deadlocks

+

Ch.6 Synchronization에서 잠시 언급했듯이 데드락은 프로세스가 리소스를 점유하고 놓아주지 않거나, 어떠한 프로세스도 리소스를 점유하지 못하는 상태가 되어 프로그램이 멈추는 현상을 말한다.

+

System Model

+

프로세스는 다음과 같은 흐름으로 리소스를 이용한다.

+
    +
  1. Request: 리소스를 요청한다. 만약 다른 프로세스가 리소스를 사용중이라서 리소스를 받을 수 없다면 대기한다.
  2. +
  3. Use: 프로세스는 리소스 위에서 수행된다.
  4. +
  5. Release: 프로세스가 리소스를 놓아준다.
  6. +
+

Deadlock Characterization

+

데드락은 다음 4가지 상황을 만족해야 발생한다.

+
    +
  • Mutual exclusion: 여러 프로세스 중 하나만 critical section에 진입할 수 있을 때.
  • +
  • Hold and wait: 프로세스 하나가 리소스를 잡고 있고, 다른 것은 대기중일 때.
  • +
  • No preemption: OS가 작동중인 프로세스를 임의로 중단시킬 수 없을 때.
  • +
  • Circular wait: 프로세스가 순환적으로 서로를 기다릴 때.
  • +
+

만약 위 조건 중 하나만 만족되지 않아도 데드락은 발생하지 않는다.

+

Resource Allocation Graph

+

프로세스 간의 관계를 그래프로 도식화해 보면 데드락이 발생할지 예상할 수 있다. 만약 그래프에 순환 고리가 있다면 데드락 위험이 있다는 의미가 된다. 순환 고리가 있다고 무조건 데드락이 발생하는 것은 아니지만, 순환 고리가 없으면 절대로 데드락이 발생하지 않는다.

+

Methods for Handling Deadlocks

+

데드락을 제어하는 데는 크게 두가지 방법이 있는데, 하나는 데드락을 방지하는 것이고, 또 다른 하나는 데드락을 피하는 것이다.

+

Deadlock Prevention

+

데드락을 방지한다는 것은 데드락 발생 조건 중 하나를 만족시키지 않음으로써 데드락이 발생하지 않도록 하는 것이다.

+
    +
  • Mutual Exclusion: critical section problem을 해결하기 위해서는 이 조건을 만족해야 하므로, 공유되는 자원이 있다면 이 조건을 만족시킬 수 밖에 없다.
  • +
  • Hold and wait: 한 프로세스가 실행되기 전 모든 자원을 할당시키고, 이후에는 다른 프로세스가 자원을 요구하도록 한다. starvation 문제가 생길 수 있다.
  • +
  • No preemption: 리소스를 점유하고 있는 프로세스가 다른 리소스를 요청했을 때 즉시 리소스를 사용할 수 없다면 점유하고 있던 리소스를 release한다.
  • +
  • Circular wait: 리소스의 타입에 따라 프로세스마다 일대일 함수로 순서를 지정해준다.
  • +
+

데드락을 방지하는 것은 장치 효율과 시스템 성능을 떨어트리는 문제가 있다.

+

Deadlock Avoidance

+

데드락을 피하는 것은 데드락이 발생할 것 같을 때는 아예 리소스를 할당하지 않는 것이다. 여기서는 시스템이 unsafe 상태가 되지 않도록 해야 하며, 만약 unsafe 상태가 되면 최대한 빨리 safe 상태로 복구한다. 데드락 가능성은 포인터로 자원 할당 그래프(Resource allocation graph)를 구현해 판단한다. 만약 리소스 타입이 여러 개라면 banker’s algorithm을 사용한다.

+

Banker’s Algorithm

+

banker’s algorithm은 Dijkstra가 고안한 데드락 회피 알고리즘이다. 이는 프로세스가 리소스를 요청할 때마다 수행되며, 만약 리소스를 할당했을 때 데드락이 발생하는지 시뮬레이션한다.

+

Recovery from Deadlock

+

만약 시스템이 데드락을 방지하거나 회피하지 못했고, 데드락이 발생했다면 데드락으로부터 복구되어야 한다. 이때는 어떤 프로세스를 종료시킬지 정하는 것이 중요해진다. 여기에는 몇가지 판단 기준이 있다:

+
    +
  1. 프로세스의 중요도
  2. +
  3. 프로세스가 얼마나 오래 실행됐는가
  4. +
  5. 얼마나 많은 리소스를 사용했는가
  6. +
  7. 프로세스가 작업을 마치기 위해 얼마나 많은 리소스가 필요한가
  8. +
  9. 프로세스가 종료되기 위해 얼마나 많은 리소스가 필요한가
  10. +
  11. 프로세스가 batch인가 interactive한가
  12. +
+

Resource Preemption

+

데드락을 해결하기 위해 리소스 선점(Preemption) 방식을 사용할 때는 다음과 같은 이슈가 있다.

+
    +
  1. Selecting a victim: 어떤 프로세스를 종료시킬 지 결정한다.
  2. +
  3. Rollback: 데드락이 발생하기 전 상태로 되돌린다.
  4. +
  5. Starvation: 계속 같은 프로세스가 victim이 될 수 있다. 이 경우 기아(Starvation) 문제가 발생한다.
  6. +
+ +
+
+ +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.8

+

Memory-Management Strategies

+
+
+
+ + +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.6

+

Synchronization

+
+
+
+ +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/12.html b/article/12.html new file mode 100644 index 0000000..933fe92 --- /dev/null +++ b/article/12.html @@ -0,0 +1,174 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.8 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ 🦕 공룡책으로 정리하는 운영체제 Ch.8 +

+ +

+ Memory-Management Strategies +

+ + + + +
+
Table of Contents +

+
+

메모리에 로드된 프로세스를 효율적으로 관리하는 방법을 다루는 챕터로, 복잡한 매커니즘과 계산이 나오기 시작해 조금 어려워지는 단계다.

+

Background

+

Ch.1 Overview에서 언급했듯 메모리는 현대 컴퓨터 시스템의 핵심이다. 프로세스는 독립적인 메모리 공간을 차지하며, 시스템은 프로세스가 자신의 영역 외에는 접근할 수 없도록 막아야 한다.

+

Basic Hardware

+

CPU는 레지스터를 참조하며 메모리 공간을 보호하며, 레지스터 정보는 PCB에 담겨있다. 이때 레지스터는 base와 limit으로 나뉜다. base는 프로세스가 메모리에서 사용할 수 있는 가장 작은 physical address를 의미하며, limit은 사용할 수 있는 주소의 범위를 의미한다. 즉, 프로세스가 사용할 수 있는 가장 큰 주소는 base와 limit의 합이다. 따라서 어떤 프로세스의 base가 300040이고, limit이 120900이라면 프로세스가 접근할 수 있는 메모리 주소의 범위는 300040부터, 300040에 120900를 더한 420940까지가 된다.

+

Address Binding

+

일반적으로 프로그램은 디스크에 binary executable 파일로 저장되어 있다. 프로그램을 실행하기 위해서는 메모리에 로드해 프로세스로 만들어야 한다. 이때 디스크에서 메인 메모리로 로드되기를 대기하는 곳이 input queue다. 운영체제는 input queue에서 프로세스를 선택해 메모리에 로드한다.

+

명령과 데이터를 메모리 주소로 binding하는 시점에 binding이 구분된다.

+
    +
  • Compile time: 만약 compile time에 프로세스가 메모리의 어느 위치에 들어갈지 미리 알고 있다면 absolute codel를 생성할 수 있다. 위치가 변경된다면 코드를 다시 컴파일해야 한다. MS-DOS .COM 형식 프로그램이 예시다.
  • +
  • Load time: 프로세스가 메모리의 어느 위치에 들어갈지 미리 알 수 없다면 컴파일러는 relocatable code를 만들어야 한다. 이 경우 최종 바인딩은 로드의 소요 시간만큼 지연될 수 있다.
  • +
  • Execution time: 프로세스가 실행 중 메모리의 한 세그먼트에서 다른 세그먼트로 이동할 수 있다면 바인딩은 runtime까지 지연되어야 한다.
  • +
+

Logical Versus Physical Address Space

+

CPU가 생성하는 logical address이고, 메모리에 의해 취급되는 주소는 physical address이다. compile-time과 load-time에서 주소를 binding할 때는 logical address와 physical address가 같게 생성되는 반면 execution-time에서는 다르게 생성된다. 이 경우 logical address를 virtual address라고 한다. virtual address를 physical address로 대응시키는 것은 하드웨어 디바이스인 MMU(Memory-Management Unit)가 한다.

+

Swapping

+

메모리는 크기가 크지 않기 때문에 프로세스를 임시로 디스크에 보냈다가 다시 메모리에 로드해야 하는 상황이 생긴다. 이때 디스크로 내보내는 것을 swap out, 메모리로 들여보내는 것을 swap in이라고 하며, 우선 순위에 따라 어떤 프로세스를 swap in/out할지 결정한다. swap하는데 걸리는 시간의 대부분은 디스크 전송 시간이다.

+

Contiguous Memory Allocation

+

보통 메모리는 두 개의 영역으로 나눠 관리되는데, low memory에는 커널을, high memory에는 사용자 프로세스를 담는다. 이때 contiguous memory allocation 시스템에서는 각 프로세스들이 연속적인 메모리 공간을 차지하게 된다. 프로세스가 자신의 범위를 넘지 못하도록 하는 것은 base register와 limit register의 역할이다.

+

Memory Allocation

+

프로세스를 메모리에 로드할 때는 먼저 메모리 상에 프로세스를 넣을 수 있는 공간을 찾는다. 메모리을 분할하는 각 단위는 block이고, 이 중 사용 가능한 block을 hole이라고 한다. 이때 할당하는데 여러 방법이 있다.

+
    +
  • First fit: 첫 번째 hole을 할당
  • +
  • Best fit: hole 중에서 가장 작은 곳을 할당
  • +
  • Worst fit: 가장 큰 곳을 할당
  • +
+

하지만 Best fit도 그닥 효율이 좋지 않아 이런 식으로는 쓸 수 없다.

+

Fragmentation

+

fragmentation은 메모리 공간을 사용하지 못하게 되는 것을 말한다. (garbage collection에도 같은 문제가 생긴다.) 여러 프로세스에 메모리를 할당하는 과정을 거치면 메모리의 모습은 대략 아래 그림과 비슷할 것이다.

+
+---+----------+----+------+---------+
+|   | empty    |    |      | empty   |
++---+----------+----+------+---------+
+
+

각 block의 크기를 순서대로 30k, 60k, 20k, 40k, 60k라고 해보자. hole은 60k 두 곳뿐이다. 그런데 만약 70k 프로세스가 들어와야 한다면? 실제 메모리 공간은 120k가 비어있지만 어디에도 70k가 들어갈 수는 없다. 이것을 external fragmentation이라고 한다.

+

internal fragmentation은 실제 프로세스 공간보다 큰 메모리를 할당하게 되는 경우를 말한다. 일반적으로 메모리가 시스템 효율을 위해 고정 크기의 정수 배로 할당되기 때문에 생기는 현상이다.

+

이런 문제는 할당된 block을 한쪽으로 몰아 큰 block을 생성하는 compaction으로 해결할 수 있다.

+
+---+----+------+--------------------+
+|   |    |      | empty              |
++---+----+------+--------------------+
+
+

이렇게 하면 70k 프로세스도 들어갈 수 있다. 하지만 프로세스 할당은 정말 자주 일어나는 일이기 때문에 compaction처럼 오버헤드가 큰 작업을 매번 할 수는 없다. 과거에는 이 방법을 썼지만 이젠 다른 방법을 쓴다.

+

Segmentation

+

segmentation은 하나의 프로세스를 여러 개로 나누는 것을 말한다. segment는 main, function, method, object 등의 논리적 단위로, 인간의 관점으로 프로세스를 나눈 것이다. 각 segment의 base와 limit은 segment table에 저장된다.

+

Paging

+

paging은 segmentation과 마찬가지로 프로세스를 여러 조각으로 나누는 것이다. 그런데 단순히 크기를 기준으로 나누기 때문에 비슷한 요소라도 메모리 공간에 연속적으로 할당되지 않는다. 컴퓨터 입장에서는 해석하기 쉽지만 사람이 직접 관리하기는 어렵다.

+

paging에서는 physical memory의 각 block을 frame이라고 하고, logical memory의 각 block을 page라고 부른다. frame을 작게 나눌수록 fragment가 적게 생기며, 실제로 external fragmentation은 거의 생기지 않는다. logical address를 physical address로 변환하는 page table이 필요하다.

+

Basic Method

+

CPU에 의해 만들어진 주소는 page number(p)와 page offset(d) 두 부분으로 나뉜다. page number는 page table의 index로써 page table에 접근할 때 사용된다. page offset은 physical address를 얻을 때 쓰이며, page table의 base address에 page offset을 더하면 physical address를 구할 수 있다.

+

Hardware Support

+

page table은 메모리에 저장되어 있다. PTBR(Page-Table Base Register)가 page table을 가리키고, PTLR(Page-Table Length Register)가 page table의 크기를 가지고 있다. 따라서 매번 데이터에 접근할 때마다 한 번은 데이터에, 한 번은 page table에 접근해야 한다. 물론 이는 비효율적인 일이기 때문에 캐시같은 것을 사용해 해결했다.

+

TLB(Translation Look-aside Buffer)는 참조했던 페이지를 담아주는 캐시 역할을 한다. TLB는 key-value pair로 데이터를 관리하는 acssociative memory이며, CPU는 page table보다 TLB을 우선적으로 참조한다.

+

page number가 TLB에서 발견되는 비율을 hit ratio라고 하며, effective memory-access time을 구하는데 쓸 수 있다.

+
Effecftive memory-access=Hit ratio×Memory access time+(1Hit ratio)×(2×Memory access time) +\text{Effecftive memory-access} = \text{Hit ratio} \times \text{Memory access time} + (1 - \text{Hit ratio}) \times (2 \times \text{Memory access time}) +

만약 hit ratio가 80%이고, 평균 메모리 접근 시간이 100 나노초라면 다음과 같이 계산한다.

+
Effective memery-access time=0.8×100+0.2×200=120 +\text{Effective memery-access time} = 0.8 \times 100 + 0.2 \times 200 = 120 +

Protection

+

메모리 할당이 contiguous한 경우 limit만 비교해도 메모리를 보호할 수 있었다. 하지만 paging은 contiguous하지 않기 때문에 다른 방법을 쓴다. page table의 각 항목에는 valid-invalid bit가 붙어있어 그 값이 valid라면 해당 페이지에 접근이 가능하고, invalid라면 해당 페이지가 logical address space에 속하지 않아 접근할 수 없다는 것을 의미한다.

+

Shared Pages

+

paging의 또 다른 장점은 코드를 쉽게 공유할 수 있다는 것이다. 만약 코드가 reentrant code(또는 pure code)라면 공유가 가능하다. reentrant code는 runtime 동안 절대로 변하지 않는 코드이며, 따라서 여러 프로세스들이 동시에 같은 코드를 수행할 수 있다. 이런 식으로 공통 page를 공유하면 12개 로드해야 할 것을 6개만 로드해도 된다.

+

Structure of the Page Table

+

paging을 직접 적용하면 page table의 크기가 커진다. 페이지 테이블을 효율적으로 구성하는 몇 가지 방법이 있다.

+

Hierachial Paging

+

hierachical paging은 logical address space를 여러 단계의 page table로 분할하는 기법이다. two-level paging scheme이 예시인데, page table과 메모리 사이에 page table을 하나 더 둠으로써 모든 페이지를 로드해야 하는 부담을 줄일 수 있다.

+

Hashed Page Tables

+

말그대로 hash table을 이용해 page table을 관리하는 기법. address space가 32비트보다 커지면 hierachial paging이 비효율적이기 때문에 주로 이 방법을 쓴다. virtual page number를 hashing해 page table을 참조하는 데 사용한다. hashed page table에서는 linked list를 따라가며 page number를 비교하고, 일치하면 그에 대응하는 page frame number를 얻는다. hash table은 검색에 O(1)O(1) 시간이 걸려 매우 빠르지만 구현이 어렵다.

+

Inverted Page Tables

+

지금까지 page table은 각 page마다 하나의 항목을 가졌다. inverted page table은 메모리의 frame마다 한 항목씩 할당하는데, 이렇게 하면 physical frame에 대응하는 항목만 저장하면 되기 때문에 메모리를 훨씬 적게 사용하게 된다. 다만 탐색 시간이 오래 걸리기 때문에 대부분의 메모리는 inverted page table과 hased page table을 결합하는 방식으로 구현되어있다.

+ +
+
+ +
+ +
+
+

🏕️ 오픈소스 입문을 위한 아주 구체적인 가이드

+

+
+
+
+ + +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.7

+

Deadlocks

+
+
+
+ +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/13.html b/article/13.html new file mode 100644 index 0000000..4db240d --- /dev/null +++ b/article/13.html @@ -0,0 +1,161 @@ + + + + + + 🏕️ 오픈소스 입문을 위한 아주 구체적인 가이드 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ 🏕️ 오픈소스 입문을 위한 아주 구체적인 가이드 +

+ + + + +
+
Table of Contents +

+
+

작년 겨울부터 오픈소스에 관심이 생겨 이곳저곳에 이슈도 올리고 풀 리퀘스트도 보내고 있다. 오픈소스 기여의 가장 큰 장점은 남의 코드를 많이 읽을 수 있다는 점과 기술 트렌드를 계속 확인할 수 있다는 점이다. 그리고 영작 실력도 미세하게 (…) 향상된 것 같다. 처음 오픈소스 활동을 시작할 때 네이버 오픈소스 가이드가 큰 도움이 됐다. 오픈소스에 대한 감은 잡히지만 생각보다 구체적인 내용을 다루진 않는다.

+

Git이 설치되어 있지 않다면 설치하고, GitHub에 가입되어 있지 않다면 가입하도록 하자. 만약 익숙치 않다면 누구나 쉽게 이해할 수 있는 Git 입문을 얕게라도 읽어보는 것을 추천한다. 깃허브말고 GitLab이나 BitBucket도 있는데, 역시 깃허브의 규모가 가장 크다.

+

간단하게 오픈소스 컨트리뷰션과 관련된 용어를 정리하면 이렇다:

+
    +
  • Repository: 코드나 문서를 비롯한 리소스를 저장하는 곳을 말하며, 프로젝트 단위로 만든다. 원격 저장소(Remote repository)는 깃허브같은 호스팅 서비스 서버에 올라가 있는 저장소를 말하고, 로컬 저장소(Local repository)는 개인 컴퓨터에 있는 저장소를 말한다. 그냥 리포(repo)라고 줄여쓰기도 한다.
  • +
  • Fork: 다른 사람의 원격 저장소를 그대로 복사해 내 계정의 원격 저장소로 만드는 것을 의미한다.
  • +
  • Pull Request: 내 저장소의 변경 내용을 다른 사람의 저장소에 반영하도록 요청하는 것. 풀 리퀘스트를 보내면 해당 저장소의 메인테이너(프로젝트를 관리하는 사람)이 내 작업을 반영할지 말지 결정한다. 풀 리퀘, PR이라고 줄여말한다.
  • +
  • Issue: 프로젝트의 버그 리포트, 기능 제안, 질문 등을 말하며, 깃허브 저장소에서 Issues 탭에 들어가면 다양한 토론을 볼 수 있다.
  • +
+

🔭 프로젝트 고르기

+

처음 오픈소스 생태계에 들어서면 일단 혼란스럽다. 첫 번째 난관은 '어떤 프로젝트에 컨트리뷰션할 것인가?'인데, 가장 좋은 것은 자신이 사용하고 있는 프로젝트에 컨트리뷰션하는 것이다. 딱히 사용중인 오픈소스가 없고, 일단 컨트리뷰션을 해보고 싶다면 깃허브의 Explore 탭이나 CodeTriage를 둘러보면 자신이 다룰 수 있는 언어와 환경에 맞는 프로젝트를 찾을 수 있다. 재밌는 프로젝트들이 많다.

+

꼭 중대한 버그를 고치거나 기능을 개선, 추가하려 하지 않아도 된다. 실제로 전체 컨트리뷰션 비율 중 코드를 수정하는 것보다 문서의 오타를 고치거나 번역하는 컨트리뷰션의 비율이 더 높다. 뿐만 아니라 디자인 작업이나 의견 제시도 컨트리뷰션이니까 코드 수정에 압박받을 필요는 없다.

+

뭔가 컨트리뷰션할만한 프로젝트를 찾았다면 먼저 이슈 탭에 들어가 내가 하려는 작업을 이미 누군가하고 있지 않은지 확인해본다. 검색 결과가 따로 없고, 하려는 작업이 프로젝트의 방향이나 구조에 큰 영향을 끼치지 않는다면 바로 수정 작업에 들어가도 된다. 그것이 아니라면 직접 이슈를 올려서 의견을 받아보는 것이 좋다. 프로젝트마다 컨트리뷰션 가이드를 마련해두고 있으니 확인하길 권한다.

+

📌 저장소 포크하기

+

+

컨트리뷰션을 하려면 프로젝트의 저장소를 포크(Fork)해서 내 깃허브 계정에 동일한 저장소를 만들어야 한다. 만약 프로젝트의 원본 저장소를 아무나 수정할 수 있다면 헬게이트가 열릴 것이 뻔하기 때문에…

+

나는 TUI Editor라는 프로젝트에 컨트리뷰션을 하려 한다. 맨 오른쪽 ‘Fork’ 버튼을 누르면 자동으로 저장소가 복사된다. 만약 파일이 많고 용량이 크다면 포크하는 데 시간이 조금 걸릴 수 있다. 위 메뉴에 대해 좀 사족을 달자면, 'Watch’는 저장소에서 일어나는 활동에 대한 알림을 받을 것인지 설정하는 것이고, 'Star’는 저장소를 북마크하는 것이다. 스타는 단순히 북마크 기능을 하지만, 프로젝트를 응원한다는 의미가 될 수도 있으며, 프로젝트의 완성도와 인기의 척도(!)이기도 하다.

+

💾 저장소 클론하기

+

저장소를 클론(Clone)한다는 것은 원격 저장소를 그대로 복제해 로컬 저장소로 가져오겠다는 것이다. 즉, 깃허브에 있는 저장소를 다운받아서 내 컴퓨터에 저장한다는 의미다.

+

+

먼저 ‘Clone or download’ 버튼을 눌러서 나오는 주소를 복사한다. 그리고 컴퓨터에서 터미널을 열어서 clone 명령을 입력해주면 된다.

+
$ git clone https://github.com/nhnent/tui.editor.git
+
+

그러면 원격 저장소의 모든 파일이 담긴 폴더가 만들어진다.

+

🚧 작업하기

+

이제 내용을 수정한다. 코드를 고쳐도 좋고, 문서를 수정해도 된다. 작업 공간은 로컬 저장소니까 마음대로 하자!

+

📤 추가/커밋/푸시하기

+

변경 내용의 반영에는 3단계를 거치게 된다. 먼저 1단계 추가(Add)는 변경한 내용을 스테이징 영역(Staging area)에 올리는 것을 말하며, 2단계 커밋(Commit)은 스테이징 영역에 있는 내용을 최종 확정하겠다는 의미다.

+
$ git add *
+$ git commit -m "Update README.md"
+
+

커밋할 때는 변경 내용에 관한 메시지를 달 수 있다. 메시지는 이후 버전 관리를 편리하게 해주며, 다른 사람들에게 내가 어떤 내용을 변경했는지 요약해서 보여줄 수 있다. 다시말해 잘 써야 된다는 것이다. 좋은 git 커밋 메시지를 작성하기 위한 7가지 약속을 읽어보길 권한다. 커밋까지는 아직 원격저장소에 변경 내용이 반영되지 않은 상태다. 마지막 3단계로 푸시(push)를 해야 한다.

+
$ git push origin master
+
+

이제 내 원격 저장소에 변경 사항이 반영됐다. 하지만 원본 저장소에는 아직 내용이 반영되지 않았다. 여기까지는 아직 컨트리뷰션을 한 것이 아니다.

+

📮 풀 리퀘스트 보내기

+

풀 리퀘스트(Pull request)는 원본 저장소에 내 변경 내용을 반영해 달라고 요청하는 것이다. 풀 리퀘스트를 보내면 프로젝트를 관리하는 메인테이너(Maintainer)들이 작업 내용을 검토하고, 프로젝트에 반영할지 안 할지 결정한다.

+

+

‘Pull requests’ 탭에서 ‘New pull request’ 버튼을 누른다.

+

+

바로 나타나는 것은 브랜치 사이의 변경 내용에 관한 풀 리퀘를 보내는 것이다. 나는 저장소를 포크해서 작업했으니까 위에 있는 ‘compare across forks’ 링크를 눌러 저장소 사이의 변경 내용에 관한 풀 리퀘를 보내도록 해준다. 우측의 'head fork’에서 내가 포크해서 작업한 리포를 선택하면 된다.

+

그렇게 하면 글을 쓰는 에디터가 나온다. 제목은 변경 내용에 대한 요약을 쓰거나 그냥 커밋 메시지를 그대로 쓰기도 한다. 바로 이해할 수 있는 작업이었다면 따로 설명을 쓰지 않아도 되고, 변경 내용이 복잡하다면 설명을 쓴다. 만약 내가 작업한 내용에 관한 기존 이슈가 등록되어 있다면 #1713처럼 # 뒤에 이슈 번호를 적어 참조를 달아준다.

+

이 단계에서 가장 큰 문제는 왠지모를 불안함이었다. 풀 리퀘스트를 보내면 메인테이너들에게 알림이 가고, 내 컨트리뷰션은 완전히 공개된다. 일단 내 코드를 누군가에게 보여주는 것이 가장 떨린다. 그리고 내 컨트리뷰션에 부정적인 리뷰가 올 것 같아서 불안하기도 하다. 그래도 일단 보내는 것이다. (특별한 사정이 있는 프로젝트가 아니라면) 컨트리뷰션 자체를 싫어하는 사람은 없다.

+

😴 기다리기

+

풀 리퀘스트를 날리고나면 메인테이너가 내용을 확인하고 병합(Merge)해주길 기다린다. 며칠이 지나도 답이 없을 수 있다. 그렇다고 메인테이너를 재촉하지는 말자. 오픈소스 컨트리뷰터들은 보람을 먹고 사는 사람들이고, 각자의 본업이 따로 있는 경우가 대부분이다. 그냥 기다리는 것이다. 만약 내 요청이 묻힌 것 같다면 @ParkSB처럼 댓글로 태그해 정중하게 리뷰를 요청해볼 수 있다.

+

+

위 사진은 Hyper라는 프로젝트에 풀 리퀘스트를 보내고 반영된 것이다. #2579로 2579번 이슈에 참조를 달았고, 병합 후 메인테이너가 풀 리퀘를 닫았다.

+

슬프지만 내 작업이 거절될 수도 있다. 너무 상처받지 말고 다른 할 일을 찾아보자 (…) 왜 반영할 수 없는지 친절하게 설명해주는 메인테이너가 있는 반면, 말도 없이 풀 리퀘를 닫아버리는 메인테이너도 있다. 네이버 오픈소스 세미나에 가서 들었던 Outsider님의 강연에 따르면 이슈나 풀 리퀘에 달린 라벨을 보고 대략 메인테이너들의 성향을 대략 짐작할 수 있다고 한다.

+

+

이렇게 라벨이 많이 달려 있는 프로젝트의 메인테이너들은 친절하다고…

+

📥 저장소를 최신으로 유지하기

+

만약 같은 프로젝트에 앞으로 계속 컨트리뷰션을 하고 싶다면 해당 원격 저장소의 업데이트된 내용을 지속적으로 받아볼 수 있어야 한다. 물론 쉬운 방법이 있다. TUI Editor에 컨트리뷰션하는 경우 터미널을 열고 아래와 같은 명령어를 입력한다.

+
$ git remote add upstream https://github.com/nhnent/tui.editor.git
+
+

원본 저장소를 업스트림(Upstream)이라고 하며, 위 명령을 실행하면 upstream이라는 이름으로 원격 저장소가 추가된다. 업데이트된 원본 저장소의 내용을 가져오려면 아래 명령을 입력한다.

+
$ git fetch upstream
+$ git checkout master
+$ git merge upstream/master
+
+

fetch로 upstream 저장소의 내용을 가져와서 merge 명령으로 upstream 저장소의 master 브랜치 내용을 내 로컬 저장소에 병합한다. 이렇게 한 번씩 fetchmerge를 해주면 로컬 저장소를 최신으로 유지할 수 있다.

+ +
+
+ +
+ +
+
+

ES6 화살표 함수의 this에 관하여

+

+
+
+
+ + +
+ +
+
+

🦕 공룡책으로 정리하는 운영체제 Ch.8

+

Memory-Management Strategies

+
+
+
+ +
+
+ +
+
+ Articles +
+
+
+ + + + diff --git a/article/14.html b/article/14.html new file mode 100644 index 0000000..9e45c3a --- /dev/null +++ b/article/14.html @@ -0,0 +1,140 @@ + + + + + + ES6 화살표 함수의 this에 관하여 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Articles +
+

+ ES6 화살표 함수의 this에 관하여 +

+ + + + +
+
Table of Contents +

    +
    +

    동아리 웹 세미나 중 jQuery를 이용해서 요소를 클릭하면 요소의 내용이 바뀌는 예제를 시연했다.

    +
    $('#el').on('click', function() {
    +  $(this).html('clicked!');
    +});
    +
    +

    물론 잘 된다. 그런데 개발환경을 설명하는 부분에서 npm과 babel까지 설치하고 ES6로 같은 예제를 돌렸는데 왠지 작동하지 않았다.

    +
    import $ from 'jquery';
    +$('#el').on('click', () => {
    +  $(this).html('clicked!');
    +});
    +
    +

    뭔가 webpack에서 설정을 잘못해서 서버에 반영이 되지 않는 상황인줄 알고 일단 넘어갔다. 그런데 지금 생각해보니 결정적인 부분에서 실수한 것이었다. 화살표 함수에서 this는 스스로를 가리키지 않는다. 대신 외부 컨텍스트의 this값이 적용된다.

    +

    화살표 함수가 없던 구버전 ECMAScript에서는 외부 컨텍스트의 this에 접근하려면 별도의 변수를 만들어야했다. (MDN 화살표 함수 문서를 인용했다.)

    +
    function Person() {
    +  var that = this;  
    +  that.age = 0;
    +
    +  setInterval(function growUp() {
    +    that.age++;
    +  }, 1000);
    +}
    +
    +

    외부 컨텍스트 Personage에 접근하기 위해 that이라는 변수를 사용했다. 하지만 화살표 함수를 쓴다면?

    +
    function Person() {
    +  this.age = 0;
    +
    +  setInterval(() => {
    +    this.age++;
    +  }, 1000);
    +}
    +
    +

    thisPerson 객체를 참조하기 때문에 바로 age에 접근할 수 있다. that 같은 변수는 필요하지 않다.

    +

    그렇다면 클릭 이벤트는 어떻게 처리해야 할까?

    +
    import $ from 'jquery';
    +$('#el').on('click', (e) => {
    +  $(e.currentTarget).html('clicked!');
    +});
    +
    +

    jQuery에서는 currentTarget이라는 속성을 제공한다. 클릭 이벤트가 발생했을 때 이벤트 객체 ecurrentTarget에 접근하면 클릭된 요소를 조작할 수 있다.

    + +
    +
    + +
    + +
    +
    +

    🌞 개떡같은 코드와 함께한 하루 리뉴얼 이야기

    +

    거대한 레거시를 수습한 경험

    +
    +
    +
    + + +
    + +
    +
    +

    🏕️ 오픈소스 입문을 위한 아주 구체적인 가이드

    +

    +
    +
    +
    + +
    +
    + +
    +
    + Articles +
    +
    +
    + + + + diff --git a/article/15.html b/article/15.html new file mode 100644 index 0000000..0dea3a7 --- /dev/null +++ b/article/15.html @@ -0,0 +1,170 @@ + + + + + + 🌞 개떡같은 코드와 함께한 하루 리뉴얼 이야기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + Articles +
    +

    + 🌞 개떡같은 코드와 함께한 하루 리뉴얼 이야기 +

    + +

    + 거대한 레거시를 수습한 경험 +

    + + + + +
    +
    Table of Contents +

    +
    +

    2개월이나 지났지만, 더 시간이 지나면 잊어버릴 것 같아서 하루 리뉴얼에 대한 이야기를 해보려 한다. 워낙 충격적이고 힘든 경험이어서 아직도 어땠는지 기억이 난다.

    +

    +

    결과적으로 더러운(…) 경험이었다. 예상치 못한 오류, 쏟아져 나오는 버그, 개떡같은 코드, 완전 뒤죽박죽이었다. 이하 내용은 약 10개월간 내가 무슨 삽질을 했는지에 관한 회고다.

    +

    🔥 발단

    +

    '하루’는 내가 중학교 3학년 때(2013년) 친구와 졸업작품으로 만든 SNS다. 게시글은 하루만 유지되며, 페이스북의 좋아요 같은 '오호라’를 누르면 글의 유지 시간을 1시간 더 늘려줄 수 있다. 운좋게 언론사에서 인턴을 하던 학교 선배가 기사를 써줬고, 여기저기에 알려졌다.

    +

    그런데 특허 문제에 학업에 이런저런 이유로 더 이상 유지하기가 힘들어져서 제대로 신경쓰지 못했다. 반면 글은 나름 꾸준히 올라오고 있었고, 나는 거의 사골 수준으로 내 커리어에 하루를 우려먹었다. (대입 자소서에도 들어갔고, 우아한형제들 인턴 지원서에도 들어갔고, 공모전 이력에도 들어갔다.) 모르는 사람들이 하루 기사를 봤다며 연락을 해오기도 했고, 서비스에 대한 피드백이 들어오기도 했다. 이런 상황이 계속되다 보니 졸속으로 만들어놓은 사이트를 좀 고쳐놔야겠다는 생각을 하고 있었다.

    +

    그러다가 대학까지 입학하고, 나는 18학점 밖에 되지 않는 텅텅 빈 시간표 덕분에 시간이 남아 도는 상태가 되었다. 그리고 2017년 5월, 나는 무료로 풀린 스타크래프트 브루드워에 매진하고 있었다. (이후 187패 8승을 찍고 그만뒀다.) 그때 스타크래프트 리마스터가 출시됐는데, 아마 거기에 고무되었는지 하루도 리뉴얼할 때가 됐다는 생각이 들었다.

    +

    +

    일단 페이지에 글을 올리고 잤다. 빡센 커리큘럼에 고통을 호소하던 친구를 뒤로하고 나 혼자 개발해야 한다는 생각에 조금 막막해졌다.

    +

    그때는 새벽 3시였고, 다음날 10시반 수업이 있었지만 간단하게 감이라도 잡아보려 했다. 테스트 서버를 만들고 간단하게 세팅한 뒤 SSL 인증서를 발급받아 실서비스 서버의 파일들을 그대로 옮겨와봤다. 무료 도메인인 haroootest.tk도 적용시켰다. 생각보다 쉽게 할 수 있을 것 같았다.

    +

    하지만 순탄치 않았다. 6월부터 10월까지 작업하고, 이후 미뤄지다가 2학기를 마치고 2018년이 되어서야 다시 리뉴얼 작업에 들어갔다. 처음에는 리뉴얼을 '여름 업데이트’라고 불렀지만 결과적으로는 '18업데이트’가 되었다. (글에 "올해 안에는 끝낼 수 있겠죠?"라고 써놔서 더 찔린다.)

    +

    🌜 서버 건드리는 날은 밤새는 날

    +

    가장 먼저 해야될 작업은 서버 이전이었다. 기존 서버는 서버에 대해 1도 모르던 시절 세팅해 놓은 것이었기 때문에 찝찝한 마음을 지울 수 없었다. 옮기는 겸 서버 세팅도 제대로 하고 PHP 버전도 올릴 생각이었다. 먼저 네이버 클라우드 플랫폼(AWS와 같은 클라우드 컴퓨팅 서비스)에는 두 컨테이너가 있었다. 하나는 SSL 인증서를 발급받은 테스트 서버였고, 다른 하나는 실서비스 서버였다. 테스트 서버를 초기화하고 실서비스 서버로 쓰는 것이 내 목표였다.

    +

    서버를 지우는 것은 어렵지 않다. 그냥 ‘서버 반납’ 버튼만 누르면 된다. 문제는 생성이다. 새로 서버를 생성하면 DNS도 새로 잡아줘야 하고, 세팅도 다시 해줘야 하는데 서버만 건들면 항상 예상치 못한 일이 생겨 밤을 새곤했다. 예전에는 우분투 버전 올리겠다고 뻘짓하다가 새벽 1시부터 시작해 오전 8시까지 잠을 못잔 적도 있다. 한가한 날을 잡고 마음을 다잡은 뒤 새로운 서버를 생성했다.

    +

    +

    Putty로 서버에 접속하고 한번에 끝내는 Ubuntu 웹서버 세팅을 참고하며 서버를 세팅했다. 그렇게 삽질이 시작됐다. 당시 나는 CLI에 그리 익숙하지 않았고, 블로그에 나온대로 한다고 똑같이 되는 것도 아니었다. 그래도 처음 서버 생성할 때 별짓을 다 해봤기 때문에 웬만한 문제는 직접 해결할 수 있었다. 문제는 SSL이었다.

    +

    🔐 SSL 인증서로 안전한 서버 만들기

    +

    기존 서버는 http 프로토콜을 사용했기 때문에 항상 https로 바꿔야한다는 마음의 짐이 있었다. 이건 선택이 아니라 의무였다. 특히 크롬에서 http 사이트에 접속하면 '안전하지 않은 사이트’라는 경고가 떠서 바꾸지 않고는 안 되는 상황이었다. 하지만 생각보다 쉬운 일이 아니었다. 일단 SSL이 뭔지 잘 몰랐다! 먼저 공부부터 했다. 물론 그것이 뭔지 안다는 말이 그것을 사용할 수 있는 말은 아니다. 다만 커맨드를 치면서 내가 뭘하고 있는지 정도는 알 수 있게 되었다.

    +

    Let’s Encrypt는 무료로 SSL 인증서를 발급해준다. (예전에는 StartCom SSL로 테스트했는데, 무슨 이슈가 터져서 여전히 안전하지 않은 사이트라는 경고가 떴다.) 일단 발급받고 커맨드를 쳐서 여차저차 적용을 성공했다. 그런데 https://harooo.com/은 접속이 잘 됐지만 http://harooo.com/에 접속했을 때는 https://harooo.com/으로 리다이렉트가 되지 않았다.

    +

    왜일까? 이 문제로 이틀 정도를 흘려 보냈는데, 답은 생각보다 간단했다. 아파치 서버 설정을 작성할 때 80포트(http) 설정과 443포트(https) 설정을 하나의 파일 안에서 다룬 것이 실수였다. 아무리봐도 이상한 것 같아서 80포트와 443포트를 분리해 다시 설정을 작성했다.

    +

    /etc/apache2/sites-enabled/default-ssl.conf

    +
    <IfModule mod_ssl.c>
    +    <VirtualHost _default_:443>
    +        ServerAdmin webmaster@localhost
    +
    +        DocumentRoot /var/www
    +
    +        ErrorLog ${APACHE_LOG_DIR}/error.log
    +        CustomLog ${APACHE_LOG_DIR}/access.log combined
    +
    +        SSLEngine on
    +
    +        SSLCertificateFile   /etc/ssl/certs/ssl-cert-snakeoil.pem
    +        SSLCertificateKeyFile/etc/ssl/private/ssl-cert-snakeoil.key
    +
    +        <FilesMatch "\\.(cgi|shtml|phtml|php)$">
    +            SSLOptions +StdEnvVars
    +        </FilesMatch>
    +        <Directory /usr/lib/cgi-bin>
    +            SSLOptions +StdEnvVars
    +        </Directory>
    +    </VirtualHost>
    +</IfModule>
    +
    +## vim: syntax=apache ts=4 sw=4 sts=4 sr noet
    +
    +

    이렇게 하고 설정을 활성화했더니 잘 작동했다. 주소창 앞에 나오는 녹색 자물쇠가 너무 사랑스러워 보였다. 고생하며 서버 세팅과 SSL 적용을 마치니 너무 뿌듯했다. 그리고 무엇보다 vi가 정말 좋았다.

    +

    🐜 버그 폭발!

    +

    예상했던 일이지만 우분투와 PHP, MySQL을 최신으로 업데이트하니 버그가 쏟아져 나왔다. 거의 모든 페이지가 엉망이 되었다. 장난 아니었다. 정말 손댈 엄두가 나지 않았다.

    +

    +

    …그래서 2017년 10월 이후로 거의 손대지 못했다. 코드는 충격적일 정도로 개판이었다. mysql_query(...) 같은 구문을 모두 mysqli_query(...)로 바꿔야 했고, fetch_array(...)도 새로운 방식을 써야 했다. 거기다가 html과 javascript, php가 마구 섞여 있어서 정말 아수라장이 따로 없었다. 이해할 수 없는 코드가 가득했고, 이상한 시점에 쿼리를 날리기도 했다. 애초에 잘못된 로직도 있어서 그 부분도 새로 구현해야 했다.

    +

    사실 구현과 수정보다는 과거 코드를 해석하는 데 더 오랜 시간을 쏟았다. 코드가 너무 복잡해서 그냥 웹에 띄워진 결과물을 보고 아예 새로 구현하는 게 빠를 것 같았다. 과거의 나를 욕했고, 주석의 중요성을 절실히 깨달았다.

    +

    🎨 미세한 UI 개선

    +

    사실 하루를 처음 만들 때 나는 html과 css를 잘 몰랐다. 정보 수업에서 짧게 배운 내용과 W3Schools, 그리고 모던 웹 디자인을 위한 HTML5+CSS3 입문으로 공부하면서 만들었다.

    +

    그러다보니 디자인과 실제 웹에서의 결과물은 많이 달랐다. 리뉴얼에서 UI 변화는 빠질 수 없을 것 같아서 미세하게나마 개선 작업을 했다.

    +

    +

    +

    그나마 index 페이지는 차이가 큰 부분이지 다른 페이지들은 사실 큰 차이가 없다. 그런데 CSS 코드가 워낙 개판이라 이 부분도 완전히 새로 짜야했다. 과거의 나는 왜 그렇게 absolute를 좋아했던걸까.

    +

    ✨ 미세한 퀄리티 개선

    +

    잘 눈에 띄지 않지만 비밀번호 찾기, 이용약관, 개인정보취급방침, 설정 페이지 등 자잘한 페이지들의 디자인과 내용을 개선했다. 졸업작품 발표 때 작은 차이가 큰 차이를 만든다 해놓고 이런 부분에 잘 신경을 안 써놨더라.

    +

    🤯 다시는 개떡같은 코드를 쓰지 않겠습니다

    +

    엄청나게 힘들었지만 아무튼 끝내긴 했다.

    +

    +

    다행히 10개월 간의 삽질을 통해 나름 느낀 점이 있다. 첫 번째는 html, css, javascript, php의 완벽한 분리에 목숨을 걸어야겠다는 것. 두 번째는 쓸모있는 주석을 달아야겠다는 것. 세 번째는 한 번 더 생각하고 코드를 짜야한다는 것. 그리고 마지막으로 현재의 편안함이 나중엔 지옥으로 다가올 수 있다는 것.

    +

    많은 수업에서(특히 객체지향프로그래밍) 교수님들은 "당장은 필요없을지 몰라도 나중에 코드를 관리하기 굉장히 편해집니다."라고 말하지만, 잘 실감이 나지 않을 때가 있다. 역시 경험으로 배워야 한다…

    + +
    +
    + +
    + +
    +
    +

    Race condition 발생시키고 Mutex lock으로 해결하기

    +

    +
    +
    +
    + + +
    + +
    +
    +

    ES6 화살표 함수의 this에 관하여

    +

    +
    +
    +
    + +
    +
    + +
    +
    + Articles +
    +
    +
    + + + + diff --git a/article/16.html b/article/16.html new file mode 100644 index 0000000..59a14f7 --- /dev/null +++ b/article/16.html @@ -0,0 +1,218 @@ + + + + + + Race condition 발생시키고 Mutex lock으로 해결하기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + Articles +
    +

    + Race condition 발생시키고 Mutex lock으로 해결하기 +

    + + + + +
    +
    Table of Contents +

      +
      +

      race condition은 두 개 이상의 프로세스나 스레드가 하나의 데이터를 공유할 때 데이터가 동기화되지 않는 상황을 말한다. (공룡책으로 정리하는 운영체제 Ch.6에 정리했다.) 그리고 코드에서 이러한 문제가 발생할 수 있는 부분을 critical section이라고 하며, 이 문제를 해결하기 위해 한 번에 하나의 스레드만 critical section에 진입할 수 있도록 제어하는 기법을 mutex lock이라고 한다.

      +

      CentOS에서 3개의 스레드를 운영하는 프로그램을 짜고, race condition을 발생시킨 뒤 mutex lock을 통해 해결해보려 한다. 먼저 3개의 스레드를 만들어보자.

      +
      #include <stdio.h>
      +#include <pthread.h>
      +#include <time.h>
      +
      +void* performThread(void* data);
      +
      +int count = 0;
      +
      +int main(int argc, char* argv[]) {
      +  pthread_t threads[3];
      +  char threadNames[3][128] = {
      +    {"thread1"},
      +    {"thread2"},
      +    {"thread3"}
      +  };
      +
      +  pthread_create(&threads[0], NULL, performThread, (void*)threadNames[0]);
      +  pthread_create(&threads[1], NULL, performThread, (void*)threadNames[1]);
      +  pthread_create(&threads[2], NULL, performThread, (void*)threadNames[2]);
      +
      +  printf("%d\n", count);
      +
      +  return 0;
      +}
      +
      +void* performThread(void* data) {
      +  count++;
      +}
      +
      +

      3개의 스레드가 차례대로 count를 증가시켰기 때문에 예상대로 결과는 3이 나온다. 이걸 조금 바꿔서 count를 10만번 증가시키고, 모든 내용을 로그에 남기도록 하려한다. performThread만 수정해주면 된다.

      +
      void* performThread(void* data) {
      +  time_t currentTime;
      +  struct tm* timeInfo;
      +  char currentTimeString[128];
      +
      +  char* threadName = (char*)data;
      +  FILE* file;
      +  int i = 0;
      +
      +  file = fopen("event.log", "a");
      +
      +  for (i = 0; i < 100000; i++) {
      +    time(&currentTime);
      +    timeInfo = localtime(&currentTime);
      +    strftime(currentTimeString, 128, "%Y-%m-%d %H:%M:%S", timeInfo);
      +
      +    fprintf(file, "%s\t%s\t%d\n", currentTimeString, threadName, count);
      +
      +    count++;
      +  }
      +
      +  fclose(file);
      +}
      +
      +

      반복문을 10만번 돌면서 시간, 스레드 이름, count 순서로 로그를 한 줄씩 남기도록 수정하고 실행한다.

      +

      +

      엄청난 빈도로 race condition이 발생했으며, 최종 결과가 정확히 300000으로 떨어지지 않았다. 이에 앞서 VM에서 테스트하면서 race condition이 일어나지 않아 삽질을 많이 했는데, VM을 싱글 코어로 설정해서 그런 것이었다.

      +

      +

      위와 같이 싱글 코어 환경에서는 한 번에 하나의 스레드만 실행되기 때문에 race condition이 발생하지 않는다.

      +

      다시 멀티 코어 환경으로 돌아와서, race condition을 mutex lock으로 해결해본다.

      +
      #include <stdio.h>
      +#include <pthread.h>
      +#include <time.h>
      +
      +void* performThread(void* data);
      +
      +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      +int counter = 0;
      +FILE* file;
      +
      +int main(int argc, char* argv[]) {
      +  pthread_t threads[3];
      +  char threadNames[3][128] = {
      +    {"thread1"},
      +    {"thread2"},
      +    {"thread3"}
      +  };
      +  int i = 0;
      +
      +  file = fopen("event.log", "a");
      +
      +  for (i = 0; i < 100000; i++) {
      +    pthread_create(&threads[0], NULL, performThread, (void*)threadNames[0]);
      +    pthread_create(&threads[1], NULL, performThread, (void*)threadNames[1]);
      +    pthread_create(&threads[2], NULL, performThread, (void*)threadNames[2]);
      +  }
      +
      +  printf("Done: %d\n", counter);
      +  fclose(file);
      +
      +  return 0;
      +}
      +
      +void* performThread(void* data) {
      +  pthread_mutex_lock(&mutex);
      +
      +  time_t currentTime;
      +  struct tm* timeInfo;
      +  char currentTimeString[128];
      +  char* threadName = (char*)data;
      +
      +  time(&currentTime);
      +  timeInfo = localtime(&currentTime);
      +  strftime(currentTimeString, 128, "%Y-%m-%d %H:%M:%S", timeInfo);
      +
      +  fprintf(file, "%s\t%s\t%d\n", currentTimeString, threadName, counter);
      +
      +  counter++;
      +
      +  pthread_mutex_unlock(&mutex);
      +}
      +
      +

      먼저 mutex 변수를 만들었다. 그리고 critical section은 performThread 함수에서 로그를 남기고 counter를 증가시키는 부분이므로, 스레드가 performThread에 진입할 때 lock해 다른 스레드가 진입할 수 없도록 만든 후 나올 때는 unlock해주도록 했다.

      +

      +

      최종 값은 299999였다. counter가 0에서 출발하니까 최종 값이 299999이어야 각 스레드가 100000번씩 수행되었다는 의미가 된다. 잘 동작했다.

      + +
      +
      + +
      + +
      +
      +

      Java Design Pattern: Singleton

      +

      +
      +
      +
      + + +
      + +
      +
      +

      🌞 개떡같은 코드와 함께한 하루 리뉴얼 이야기

      +

      거대한 레거시를 수습한 경험

      +
      +
      +
      + +
      +
      + +
      +
      + Articles +
      +
      +
      + + + + diff --git a/article/17.html b/article/17.html new file mode 100644 index 0000000..e31d2ee --- /dev/null +++ b/article/17.html @@ -0,0 +1,133 @@ + + + + + + Java Design Pattern: Singleton + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +
      + Articles +
      +

      + Java Design Pattern: Singleton +

      + + + + +
      +
      Table of Contents +

        +
        +

        Singleton 패턴은 하나만 존재해야 하는 오브젝트를 만들 때 유용한 디자인 패턴이다. 간단히 구현해보자.

        +
        public class Singleton {
        +  private static Singleton instance;
        +
        +  private Singleton() {}
        +
        +  public static Singleton getInstance() {
        +    if (instance == null) {
        +      instance = new Singleton();
        +    }
        +
        +    return instance;
        +  }
        +}
        +
        +

        constructor를 private으로 정의하고, instance에 접근하기 위해서는 반드시 getInstance 메소드를 거치도록했다. 이제 한 번 인스턴스가 생성된 이후에는 또 다른 인스턴스가 생성될 일이 없다.

        +

        하지만 이렇게만 하면 멀티스레딩 환경에서 동기화가 제대로 되지 않을 경우 인스턴스가 두 개 만들어질 수 있다.

        +
        public class Singleton {
        +  private static Singleton instance;
        +
        +  private Singleton() {}
        +
        +  public static synchronized Singleton getInstance() {
        +    if (instance == null) {
        +      instance = new Singleton();
        +    }
        +
        +    return instance;
        +  }
        +}
        +
        +

        getInstance를 synchronized로 해주면 한 번에 하나의 스레드만 메소드에 접근할 수 있기 때문에 동기화 문제가 해결된다.

        + +
        +
        + +
        + +
        +
        +

        📊 파이썬으로 정리하는 Quick-Sort

        +

        +
        +
        +
        + + +
        + +
        +
        +

        Race condition 발생시키고 Mutex lock으로 해결하기

        +

        +
        +
        +
        + +
        +
        + +
        +
        + Articles +
        +
        +
        + + + + diff --git a/article/18.html b/article/18.html new file mode 100644 index 0000000..5de61f8 --- /dev/null +++ b/article/18.html @@ -0,0 +1,255 @@ + + + + + + 📊 파이썬으로 정리하는 Quick-Sort + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        +
        +
        + Articles +
        +

        + 📊 파이썬으로 정리하는 Quick-Sort +

        + + + + +
        +
        Table of Contents +

          +
          +

          quick sort는 말 그대로 빠른 정렬을 의미한다. 큰 문제를 쪼개 작은 문제로 만들어서 정복하는 divide-and-conquer 패러다임에 바탕을 두고 있어 퍼포먼스가 좋다.

          +

          +https://idea-instructions.com/quick-sort/

          +

          기본적인 매커니즘은 그리 어렵지 않다.

          +
            +
          1. 리스트에서 값 하나를 골라 pivot으로 정한다.
          2. +
          3. pivot보다 작은 것은 pivot의 왼쪽으로 보낸다.
          4. +
          5. pivot보다 큰 것은 pivot의 오른쪽으로 보낸다.
          6. +
          7. pivot의 왼쪽에 있는 값들로 새로운 리스트를 만들어 또 pivot을 정하고, pivot보다 작은 것은 왼쪽, 큰 것은 오른쪽으로 보낸다. 그리고 다시 왼쪽, 오른쪽 값들로 새로운 리스트를 만들어 이동하는 과정을 반복한다.
          8. +
          9. pivot의 오른쪽에 있는 값들로 새로운 리스트를 만들어 또 pivot을 정하고 pivot보다 작은 것은 왼쪽, 큰 것은 오른쪽으로 보낸다. 그리고 다시 왼쪽, 오른쪽 값들로 새로운 리스트를 만들어 이동하는 과정을 반복한다.
          10. +
          +

          위 과정을 재귀적으로 반복하면 리스트의 값들이 정렬된다. 단계적으로 살펴보면 이렇다.

          +
          # input
          +[85, 24, 63, 45, 17, 31, 96, 50]
          +
          +# 50을 pivot으로 정한다.
          +[85, 24, 63, 45, 17, 31, 96, "50"]
          +
          +# pivot(50)보다 작은 것은 pivot의 왼쪽으로, 큰 것은 오른쪽으로 보낸다.
          +[24, 45, 17, 31, "50", 85, 63, 96]
          +
          +# pivot(50)의 왼쪽에 있는 값들로 새로운 리스트를 만들고, 31을 pivot으로 정한다.
          +[_, _, _, _, "50", 85, 63, 96]
          +[24, 45, 17, "31"]
          +
          +# pivot(31)보다 작은 것은 pivot(31)의 왼쪽으로, 큰 것은 오른쪽으로 보낸다.
          +[_, _, _, _, "50", 85, 63, 96]
          +[24, 17, "31", 45]
          +
          +# pivot(31)의 왼쪽에 있는 값들로 새로운 리스트를 만들고, 17을 pivot으로 정한다.
          +[_, _, _, _, "50", 85, 63, 96]
          +[_, _, "31", 45]
          +[24, "17"]
          +
          +# pivot(17)보다 작은 것은 pivot(17)의 왼쪽으로, 큰 것은 오른쪽으로 보낸다.
          +[_, _, _, _, "50", 85, 63, 96]
          +[_, _, "31", 45]
          +["17", 24]
          +
          +# pivot(17)의 왼쪽에 있는 값들로 새로운 리스트를 만든다. (왼쪽에 값이 없으므로 패스)
          +[_, _, _, _, "50", 85, 63, 96]
          +[_, _, "31", 45]
          +["17", 24]
          +[]
          +
          +# pivot(17)의 오른쪽에 있는 값들로 새로운 리스트를 만든다. (값이 하나이므로 패스)
          +[_, _, _, _, "50", 85, 63, 96]
          +[_, _, "31", 45]
          +["17", _]
          +[] [24]
          +
          +# 리스트를 pivot(17) 오른쪽에 넣는다. (호출한 값이 return되는 과정)
          +[_, _, _, _, "50", 85, 63, 96]
          +[_, _, "31", 45]
          +["17", 24]
          +[] [_]
          +
          +# 리스트를 pivot(31) 왼쪽에 넣는다. (호출한 값이 return되는 과정)
          +[_, _, _, _, "50", 85, 63, 96]
          +[17, 24, "31", 45]
          +[_, _]
          +[] [_]
          +
          +# pivot(31)의 오른쪽에 있는 값들로 새로운 리스트를 만든다. (값이 하나이므로 패스)
          +[_, _, _, _, "50", 85, 63, 96]
          +[17, 24, "31", 45]
          +[_, _] [45]
          +[] [_]
          +
          +# 리스트를 pivot(31) 오른쪽에 넣는다. (호출한 값이 return되는 과정)
          +[_, _, _, _, "50", 85, 63, 96]
          +[17, 24, "31", 45]
          +[_, _] [_]
          +[] [_]
          +
          +# 리스트를 pivot(50) 왼쪽에 넣는다. (호출한 값이 return되는 과정)
          +[17, 24, 31, 45, "50", 85, 63, 96]
          +[_, _, _, _]
          +[_, _] [_]
          +[] [_]
          +
          +# 위 과정을 pivot(50)의 오른쪽 리스트에 대해 반복한다.
          +[17, 24, 31, 45, "50", 63, 85, 96]
          +[_, _, _, _]   [_, _, _]
          +[_, _] [_] [_, _] []
          +[] [_] [_] []
          +
          +# output
          +[17, 24, 31, 45, 50, 63, 85, 96]
          +
          +

          여기서는 리스트의 마지막 값을 항상 pivot으로 정했지만, random하게 pivot을 정할 수도 있고, 중앙값을 pivot으로 정할 수도 있다. 중앙값을 pivot으로 정하는 정렬 과정을 시각화하면 이 영상처럼 보인다. (quick sort를 이해하는 데 별로 도움은 안 되지만 재밌다.)

          +

          그리고 좀 더 최적화된 방법으로 inplace quick sort를 쓸 수 있다. inplace 방식은 pivot을 정하고 왼쪽, 오른쪽으로 값들을 이동시키는 과정을 개선한 것이다.

          +
            +
          1. 먼저 왼쪽에서 출발하는 인덱스 l과 오른쪽에서 출발하는 인덱스 r을 만든다.
          2. +
          3. l은 pivot보다 큰 값을 만나면 정지하고, r은 pivot보다 작은 값을 만나면 정지한다.
          4. +
          5. 두 인덱스가 모두 정지하면 값을 교체한다.
          6. +
          7. 이 과정을 lr이 같아질 때까지 반복한다.
          8. +
          +

          이런 식으로 구현하면 더 적은 메모리를 사용할 수 있다.

          +
          # pivot = 50
          +# l = 85, r = 96, (l > pivot)
          +["85", 24, 63, 45, 17, 31, "96", "50"]
          +
          +# l = 85, r = 31, (l > pivot) (r < pivot)
          +["85", 24, 63, 45, 17, "31", 96, "50"]
          +
          +# l과 r을 교체
          +["31", 24, 63, 45, 17, "85", 96, "50"]
          +
          +# l = 24, r = 17, (r < pivot)
          +[31, "24", 63, 45, "17", 85, 96, "50"]
          +
          +# l = 63, r = 17 (l > pivot) (r < pivot)
          +[31, 24, "63", 45, "17", 85, 96, "50"]
          +
          +# l과 r을 교체
          +[31, 24, "17", 45, "63", 85, 96, "50"]
          +
          +# l = 45, r = 45 (l == r)
          +[31, 24, 17, "45", 63, 85, 96, "50"]
          +
          +# l(45)과 r(45)이 pivot보다 작거나 같으므로 l += 1
          +# l = 63, r = 45
          +[31, 24, 17, "45", "63", 85, 96, "50"]
          +
          +# l과 pivot을 교체
          +[31, 24, 17, 45, "50", 85, 96, 63]
          +
          +

          파이썬으로 구현하면 다음과 같다.

          +
          def quick_sort(S, a, b):
          +  if a >= b:
          +    return
          +
          +  pivot = S[b]
          +  left = a
          +  right = b - 1
          +
          +  while left <= right:
          +    while left <= right and S[left] < pivot:
          +      left += 1
          +
          +    while left <= right and pivot < S[right]:
          +      right -= 1
          +
          +    if left <= right:
          +      S[left], S[right] = S[right], S[left]
          +      left, right = left + 1, right - 1
          +
          +  S[left], S[b] = S[b], S[left]
          +
          +  quick_sort(S, a, left - 1)
          +  quick_sort(S, left + 1, b)
          +
          +

          이미 정렬되어 있는 리스트를 quick sort로 정렬하는 경우 O(n2)O(n^2) 시간이 걸린다. 즉, worst case에서 O(n2)O(n^2) 시간이 걸린다는 말이다. 재밌는 것은 average case에서도 O(nlogn)O(n \log n) 시간이 걸리는데, 이는 quick sort라는 이름이 붙을 정도로 빠른 것은 아니다. 그럼에도 quick sort인 이유는 알고리즘의 캐시 hit ratio가 높기 때문이다. (컴퓨터 아키텍처에 관한 부분이다.) 그래서 이론적으로는 평균 O(nlogn)O(n \log n)이지만 실제로는 더 빠르게 정렬된다.

          + +
          +
          + +
          + +
          +
          +

          🐧 윈도우에서 우분투 돌리기

          +

          개발을 위한 WSL 세팅

          +
          +
          +
          + + +
          + +
          +
          +

          Java Design Pattern: Singleton

          +

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/19.html b/article/19.html new file mode 100644 index 0000000..772fb54 --- /dev/null +++ b/article/19.html @@ -0,0 +1,162 @@ + + + + + + 🐧 윈도우에서 우분투 돌리기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 🐧 윈도우에서 우분투 돌리기 +

          + +

          + 개발을 위한 WSL 세팅 +

          + + + + +
          +
          Table of Contents +

          +
          +

          윈도우로 개발을 하는 입장에서 터미널을 다루기란 조금 까다롭다. 리눅스나 OSX 환경에 맞춰진 프로젝트에 참여하면 명령이 다르게 동작해 삽질하고, OSX의 패키지 매니저인 Homebrew같은 것도 없어서 또 삽질을 하곤 한다.

          +

          때문에 윈도우에서 우분투를 가상머신으로 돌리거나 멀티부팅을 하는 등 다양한 방법들을 시도했는데, 역시 번거롭고 불편했다. 그러던 중 WSL을 알게됐다.

          +

          WSL은 Windows Subsystem for Linux의 약자로, 윈도우 서브시스템에 리눅스를 탑재하는 것이다. 마이크로소프트에서 공식적으로 지원하는 것이기 때문에 어느정도 안정성을 보장할 수 있다. 그냥 윈도우 스토어에서 우분투를 설치하면 되기 때문에 방법도 간단하다. Windows Subsystem for Linux Installation Guide for Windows 10을 참고.

          +

          설치한 우분투를 실행하여 UNIX 아이디와 패스워드를 설정하고 나면 아래와 같은 화면이 나타난다. 이제 윈도우에서 우분투 bash를 사용할 수 있다!

          +

          작업 디렉토리 링크하기

          +

          ~ 경로는 home 디렉토리를 의미하며, 처음 계정을 생성하면 기본적으로 /home/{ID}가 home 디렉토리로 설정된다. 어디서든 cd 명령어를 통해 이곳으로 이동할 수 있다. 하지만 주로 작업하는 디렉토리는 이곳이 아니므로, 설정이 필요하다.

          +

          home 디렉토리 자체는 나중에 또 다르게 사용할 것 같아서 home 디렉토리의 경로를 변경하기 보다는 이곳에 링크 파일을 두기로 결정했다. 내가 주로 작업 파일을 두는 경로는 c/Bitnami/wampstack/apache2/htdocs이다. wsl에서는 c드라이브가 \mnt 디렉토리의 하위 폴더로 존재하므로, 우분투에서 절대 경로는 /mnt/c/Bitnami/wampstack/apache2/htdocs가 된다. 만약 home 디렉토리 경로를 변경한다면 다음 명령을 사용하면 된다.

          +
          # usermod -d {PATH} {ID}
          +$ usermod -d /mnt/c/Bitnami/wampstack/apache2/htdocs parksb
          +
          +

          링크 파일은 윈도우의 바로가기 같은 것이다. 링크는 symbolic link와 hard link로 나뉘는데, 전자는 윈도우의 바로가기와 완전히 동일하다. 만약 원본 파일이 삭제된다면 symbolic link도 무효화된다. 후자는 동일한 내용의 다른 파일을 만드는 것이다. 원본 파일이나 hard link 둘 중 하나가 삭제돼도 다른 하나는 남아있다. 만약 원본 파일의 내용이 변경된다면 hard link의 파일 내용도 변경된다.

          +

          내가 원하는 것은 바로가기이므로, symbolic link 파일을 만들어보겠다.

          +
          # ln -s {OPTION} {ORIGIN} {TARGET}
          +$ ln -s /mnt/c/Bitnami/wampstack/apache2/htdocs /home/parksb/htdocs
          +
          +

          이렇게 하면 /home/parksb/mnt/c/Bitnami/wampstack/apache2/htdocs 디렉토리를 링크한 htdocs 파일이 만들어진다. 따라서 cd ~/htdocs를 하면 /mnt/c/Bitnami/wampstack/apache2/htdocs로 이동하는 것과 같아진다. -s 옵션은 symbolic link 파일을 만들겠다는 의미다. 만약 hard link 파일을 만든다면 아무런 옵션을 주지 않아도 된다.

          +

          Git branch 보여주기

          +

          git bash를 사용하면 bash에 git branch가 나타난다. 하지만 wsl bash에서는 git branch가 나타나지 않으므로 따로 설정을 해줘야 한다. 먼저 vim으로 .bashrc 파일을 열어서 bash 설정을 변경해주자. 나는 vim으로 수정했는데, vim이 익숙하지 않다면 vim 사용법을 참고해보자. 절대로 리눅스 파일을 윈도우 툴로 수정해서는 안 된다! 위험한 결과를 초래할 수 있으니 cli 에디터를 사용하지 못하겠다면 차라리 우분투를 gui로 사용하는 것을 권한다.

          +
          $ vim ~/.bashrc
          +
          +

          맨 아래에 다음과 같은 구문을 추가해준다. (Display git branch in bash prompt를 참고해 기존 형식에 맞게 조금 수정했다.)

          +
          # display git branch in bash prompt
          +git_branch() {
          +  git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \\(.*\\)/(\\1)/'
          +}
          +
          +export PS1='\\[\\033[0;32m\\]\\[\\033[0m\\033[0;32m\\]\\u@\\h:\\[\\033[0;36m\\]\\w\\[\\033[0;32m\\]$(git_branch)\\[\\033[0;32m\\]\\[\\033[0m\\033[0;32m\\] \\$\\[\\033[0m\\033[0;32m\\]\\[\\033[0m\\]'
          +
          +

          저장하고, 수정한 것을 적용시켜준다.

          +
          $ source ~/.bashrc
          +
          +

          git init이 안 된 곳에는 branch가 나오지 않고, init이 된 곳에는 나타난다.

          +

          npm 사용하기

          +

          가장 먼저 터진 이슈는 vscode의 통합 터미널에서 npm이 안 먹히는 것이었다.

          +
          $ npm -v
          +: not foundram Files/nodejs/npm: 3: /mnt/c/Program Files/nodejs/npm:
          +: not foundram Files/nodejs/npm: 5: /mnt/c/Program Files/nodejs/npm:
          +/mnt/c/Program Files/nodejs/npm: 6: /mnt/c/Program Files/nodejs/npm: Syntax error: word unexpected (expecting "in")
          +
          +

          검색해보니 Issue running npm command라는 깃허브의 WSL 저장소 이슈가 가장 먼저 나왔다. WSL은 서브시스템이기 때문에 윈도우에 node를 설치했더라도 리눅스쪽에 node를 다시 설치해줘야 한다.

          +
          $ curl -sL <https://deb.nodesource.com/setup_8.x> | sudo -E bash -
          +$ sudo apt-get install -y nodejs
          +
          +

          그리고 .profile 파일에서 환경변수를 설정해야 한다.

          +
          $ vim ~/.profile
          +
          +

          .profile 파일의 PATH 부분을 다음과 같이 수정한다.

          +
          PATH="$HOME/bin:$HOME/.local/bin:/usr/bin:$PATH"
          +
          +

          저장하고 나와서 설정을 바로 적용해준다.

          +
          $ source ~/.profile
          +
          +

          npm의 위치를 확인해보면 우분투 경로가 나온다.

          +
          $ which npm
          +/usr/bin/npm
          +
          +

          이제 윈도우에서도 리눅스와 같은 환경에서 작업할 수 있다!

          + +
          +
          + +
          + +
          +
          +

          윈도우즈에서 React Native 개발 환경 세팅하기

          +

          개발 환경 세팅만 사흘

          +
          +
          +
          + + +
          + +
          +
          +

          📊 파이썬으로 정리하는 Quick-Sort

          +

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/2.html b/article/2.html new file mode 100644 index 0000000..fbc26d1 --- /dev/null +++ b/article/2.html @@ -0,0 +1,273 @@ + + + + + + ♻️ 자바는 어떻게 Garbage Collection을 할까? + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + ♻️ 자바는 어떻게 Garbage Collection을 할까? +

          + +

          + 오브젝트의 일생 +

          + + + + +
          +
          Table of Contents +

          +
          +

          프로그램이 실행되는 내내 프로그램에서 사용하는 변수, 함수를 비롯한 각종 데이터들은 메모리에 할당되고, 해제되기를 반복한다. 이때 ‘언제, 어떤 데이터를 해제할 것인지’ 정하는 것이 중요한 문제다. C로 프로그래밍할 때는 개발자가 직접 메모리의 할당, 해제 시점을 정해준다.

          +
          int *ptr = (int*)malloc(sizeof(int) * 3); // 메모리 할당
          +
          +for (int i = 0; i < 3; i++) {
          +  ptr[i] = i;
          +}
          +
          +for (int i = 0; i < 3; i++) {
          +  printf("%d", ptr[i]); // 012
          +}
          +
          +free(ptr); // 메모리 해제
          +
          +

          만약 free()를 통해 메모리를 해제하지 않고 계속 동적할당을 수행한다면 사용할 수 있는 메모리가 꽉 차버리고 만다. 그런데 자바 프로그래밍을 할 때는 이러한 메모리 해제 작업을 직접 하지 않는다. 아무리 많은 객체를 만들고 지워도 메모리를 해제하는 코드는 어디에도 들어가지 않는다. 메모리 관리에 신경쓰지 않고 개발을 해도 괜찮은걸까? JVM이 Garbage Collection(GC)을 지원하기 때문에 괜찮다.

          +

          Memory Structure

          +

          GC는 더 이상 메모리를 차지하고 있지 않아도 되는 데이터들을 메모리에서 정리하는 작업이다. GC를 다루기 전에 먼저 메모리 구조를 살펴봐야 한다. 메모리는 저장하는 데이터의 종류에 따라 크게 네가지 영역으로 나뉜다.

          +
          +---------------+ High address
          +|     Stack     |
          ++-------+-------+
          +|       |       |
          +|       v       |
          +|               |
          +|       ^       |
          +|       |       |
          ++-------+-------+
          +|     Heap      |
          ++---------------+
          +| Static (Data) |
          ++---------------+
          +|  Text (Code)  |
          ++---------------+ Low address
          +
          +

          스택(Stack)에는 지역변수나 메서드가 저장되며, 힙(Heap) 영역에는 런타임에 동적 할당되는 데이터가 저장된다. 스태틱(Static)과 텍스트(Text) 영역은 프로그램에 상주하는 정적 변수, 바이트 코드를 저장한다. 런타임에 빈번하게 접근이 일어나는 부분은 스택과 힙이다.

          +
          public static void main(String[] args) {
          +  do();
          +}
          +
          +public void do() {
          +  Dog jake = new Dog();
          +}
          +
          +
          | jake    |  +--------+
          +| do()    |  | Dog1   | jake -> Dog1
          +| main()  |  |        |
          ++---------+  +--------+
          +   Stack        Heap
          +
          +

          메서드와 지역 변수들은 스택에 실행 순서대로 쌓인다. 가장 먼저 main()이 실행되어 스택에 들어간다. 이어서 do()가 호출되어 main() 위에 들어간다. 힙에는 런타임에 크기가 변하는 오브젝트들이 쌓인다. (Object 클래스를 상속받는 모든 데이터가 힙에 저장된다.) 위 예시에서 do() 메서드의 지역 변수 jake가 힙의 오브젝트 Dog1를 가리키게 된다. do()의 실행이 끝나면 아래와 같이 된다.

          +
          |         |  +--------+
          +|         |  | Dog1   |  ? -> Dog1
          +| main()  |  |        |
          ++---------+  +--------+
          +   Stack        Heap
          +
          +

          메모리의 스택 영역에서 do()가 나가며 Dog1 오브젝트를 가리키는 지역 변수 jake가 사라져버렸다. 더 이상 Dog1 오브젝트에 접근할 수 있는 방법이 없다. 이렇게 스택의 지역 변수로 레퍼런스가 이어지지 않는 오브젝트는 garbage가 된다. 이렇게 레퍼런스가 소멸해서 사용할 수 없어진 오브젝트를 메모리에서 제거하는 것이 바로 garbage collector가 하는 일이다.

          +

          Garbage Collection Algorithms

          +

          그럼 본격적으로 GC가 작동하는 방법에 대해 알아보자. 가장 먼저 드는 생각은 단순히 메서드 실행이 끝날 때마다 GC를 수행하면 될 것 같다. 하지만 그렇게 하려면 collector가 계속 메모리를 모니터링해야 하고, 수시로 GC가 일어날 수 있기 때문에 프로그램의 성능을 크게 떨어뜨리는 문제가 있다. 그래서 몇가지 효율적인 방법이 고안되었다.

          +

          Reference Counting

          +

          레퍼런스 카운팅은 주기적으로 GC를 수행할 때 오브젝트에 몇 개의 레퍼런스가 연결되어 있는지 체크하는 방법이다.

          +
          public static void main(String[] args) {
          +  do();
          +}
          +
          +public void do() {
          +  Dog jake = new Dog();
          +}
          +
          +
          | jake    |  +---------+
          +| do()    |  | Dog1[1] |  jake -> Dog1
          +| main()  |  |         |
          ++---------+  +---------+
          +   Stack         Heap
          +
          +

          do()의 지역 변수 jake 하나가 Dog1 오브젝트를 가리키고 있으므로 Dog1의 카운터는 1이다. 이처럼 레퍼런스가 이어지면 카운터를 하나 늘리고, 레퍼런스가 끊기면 카운터를 하나 줄여서 GC를 수행할 때 카운터가 0인 것들만 지워주면 된다. 매우 직관적이고 구현이 쉬운 방법이다. 하지만 문제가 있다. 아래와 같이 오브젝트가 서로를 가리키는 상황이 올 수 있다.

          +
          public static void main(String[] args) {
          +  do();
          +}
          +
          +public void do() {
          +  Dog jake = new Dog();
          +  Cat cake = new Cat();
          +
          +  jake.setFollowingCat(cake);
          +  cake.setFollowingDog(jake);
          +}
          +
          +public class Dog {
          +  private Cat followingCat;
          +  public void setFollowingCat(Cat c) {
          +    this.followingCat = c;
          +  }
          +}
          +
          +public class Cat {
          +  private Dog followingDog;
          +  public void setFollowingDog(Dog d) {
          +    this.followingDog = d;
          +  }
          +}
          +
          +
          | setFollowingDog() |
          +| setFollowingCat() |
          +| cake              |
          +| jake              |  +------------+  jake -> Dog1
          +| do()              |  | Dog1[2]    |  cake -> Cat1
          +| main()            |  |    Cat1[2] |  Dog1 -> cake -> Cat1
          ++-------------------+  +------------+  Cat1 -> jake -> Dog1
          +        Stack               Heap
          +
          +

          이렇게 Cat1Dog1을 참조하는 동시에 Dog1Cat1을 참조해 레퍼런스가 사이클링되면 do()의 수행이 끝나도 Cat1이 여전히 Dog1을 가리키고 있기 때문에 카운터는 0이 되지 않는다. 이 문제를 해결하기 위한 방법이 있다.

          +

          Tracing

          +

          이름 그대로 오브젝트의 레퍼런스를 추적하는 방법이다. 처음에는 스택의 지역 변수에서 시작해 해당 변수가 가리키고 있는 오브젝트를 추적한다. 만약 오브젝트에 레퍼런스가 연결되어 있다면 해당 오브젝트의 marked 값을 true로 설정한다.

          +
          | setFollowingDog() |
          +| setFollowingCat() |
          +| cake              |
          +| jake              |  +----------------+  jake -> Dog1
          +| do()              |  | Dog1[true]     |  cake -> Cat1
          +| main()            |  |     Cat1[true] |  Dog1 -> cake -> Cat1
          ++-------------------+  +----------------+  Cat1 -> jake -> Dog1
          +        Stack                 Heap
          +
          +

          만약 지역 변수로부터의 레퍼런스가 끊기면 연결된 오브젝트들의 marked 값을 false로 설정한다. GC를 수행할 때는 marked 값이 false인 오브젝트들만 메모리에서 해제하면 된다.

          +
          |         |  +----------------+  ? -> Dog1
          +|         |  | Dog1[false]    |  ? -> Cat1
          +| main()  |  |    Cat1[false] |  Dog1 -> cake(Cat1)
          ++---------+  +----------------+  Cat1 -> jake(Dog1)
          +   Stack            Heap
          +
          +

          괜찮아 보인다. 그런데 여기서 메모리를 해제할 때 한가지 신경써야 할 이슈가 있다.

          +
          +---+----------+----+------+---------+
          +|   | empty    |    |      | empty   |
          ++---+----------+----+------+---------+
          +
          +

          메모리를 해제하고 나면 중간에 빈 공간이 생기게 된다. 빈 공간이 늘어나면 새로 메모리가 할당 될 때 틈을 비집고 들어가야 하는 상황이 발생하며, 이렇게 되면 전체 메모리 공간은 충분한데 들어갈 틈이 없어서 메모리를 할당하지 못하는 문제가 생긴다. 효율적이지 않다.

          +
          +---+----+------+--------------------+
          +|   |    |      | empty              |
          ++---+----+------+--------------------+
          +
          +

          따라서 이미 할당된 메모리 공간을 한쪽으로 모아주는 compacting 작업을 해줘야 한다. 이것도 GC과정에서 수행된다. tracing은 'mark-and-sweep’이라고도 부르며, mark-and-sweep과 compacting이 순서대로 수행되기 때문에 MSC(Mark, Sweep, Compact)라고 줄여부른다.

          +

          Generational GC

          +

          JVM은 GC를 더 효율적으로 수행하기 위해 힙 메모리 구조를 보다 세밀하게 분류한다. GC는 기본적으로 오버헤드가 매우 큰 작업이다. GC가 시작될 때마다 JVM이 stop-the-world를 발생시켜 프로그램의 스레드를 모두 멈추고 앞서 설명한 MSC를 수행한다. 따라서 GC의 주기가 잦고, 규모가 클수록 오버헤트가 커진다.

          +

          JVM은 GC의 오버헤드를 줄이기 위해 generational GC를 사용한다. 이 방식은 '대부분의 오브젝트가 생성 이후 금방 garbage가 된다’는 통계적 관찰에서 출발했다. 보통 오브젝트가 만들어진지 얼마되지 않아 레퍼런스가 사라진다. 이 관찰을 활용해 GC의 효율을 높이기 위해 JVM은 heap의 영역을 세대별로 쪼개 관리한다.

          +
                           |--------- Old ---------|
          ++------+----+----+-----------+-----------+
          +| Eden | S0 | S1 |  Tenured  | Permanent |
          ++------+----+----+-----------+-----------+
          +|----- Young ----|
          +
          +

          eden, S0, S1은 생성된지 얼마되지 않은 오브젝트들이 쌓이는 공간이기 때문에 young generation이라고 부르며, 반대로 tenured와 permanent는 old generation이라고 부른다. 순서대로 살펴보자.

          +
            +
          • Eden: 에덴동산할 때 그 에덴이다. 오브젝트가 처음 생성됐을 때 eden에 들어간다.
          • +
          • Survivor 0, Survivor 1: 생성된 이후 시간이 조금 흘렀을 때 garbage가 되지 않은 오브젝트들이 eden에서 이곳으로 옮겨진다. 에덴에서 살아남은 오브젝트들이 들어간다고 해서 survivor space라고 부른다.
          • +
          • Tenured: 오래 살아있을 확률이 높은 오브젝트들이 들어간다. 여기서는 거의 GC가 수행되지 않는다.
          • +
          • Permanent: 프로그램이 끝날 때까지 살아있을 오브젝트들이 들어간다.
          • +
          +

          이렇게 힙을 나눠 각 영역에 대해서만 GC를 수행하면 성능을 더 높일 수 있다. 구체적인 수행 절차는 다음과 같다.

          +
            +
          1. 오브젝트가 계속 생성되어 Eden이 어느정도 차면 Eden에서 GC를 수행한다. (MSC 과정이 진행된다.) 여기서 garbage가 아닌 오브젝트들을 S0로 옮기고, Eden을 비운다.
          2. +
          3. Eden에서 몇 번 GC가 이뤄지면 S0에 살아남은 오브젝트가 쌓인다. 이렇게 S0가 가득차면 Eden과 S0에서 GC를 수행해 garbage가 아닌 오브젝트들을 S1으로 복사하고 eden과 S0를 비운다.
          4. +
          5. 같은 방식으로 S1이 가득차면 Eden과 S1에서 GC를 수행해 garbage가 아닌 오브젝트들을 S0로 복사하고 Eden과 S1을 비운다. 이렇게 S0와 S1을 왔다갔다 하면서 주기적으로 GC가 수행된다.
          6. +
          7. 위 과정을 반복하다보면 유난히 오래 살아남는 오브젝트들이 나올 수 있다. 만약 오브젝트의 age counter가 일정 이상이라면 tenured로 보내 S0와 S1이 가득차지 않도록 만들어준다.
          8. +
          +

          오브젝트가 살아남아 다음 세대로 넘어가는 것을 promotion이라고 표현한다. 또한 young generation에서 일어나는 GC를 minor GC라고 부르고, old generation에서 일어나는 GC를 major GC라고 부른다. minor GC는 자주, 빠르게 수행된다. 반대로 major GC는 가끔, 느리게 수행된다.

          +

          자동으로 수행되는 GC를 믿지 않고 System.gc() 메소드를 호출해 개발자가 GC를 강제할 수도 있는데, 성능을 크게 떨어뜨리는 매우 비효율적인 방법이기 때문에 절대 사용해서는 안 된다. (심지어 위험할 수도 있다.) 꼭 명시적으로 메모리를 해제하고 싶다면 차라리 오브젝트에 null을 할당해 레퍼런스를 끊는 것이 안전하다.

          +

          References

          + + +
          +
          + +
          + +
          +
          +

          프로세스간 통신을 활용해 프로그래밍하기

          +

          학적 관리 프로그램 만들기

          +
          +
          +
          + + +
          + +
          +
          +

          ES6와 함께 JavaScript로 OOP하기

          +

          자바스크립트의 OOP는 진정한 OOP가 아닌가?

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/20.html b/article/20.html new file mode 100644 index 0000000..ed7f878 --- /dev/null +++ b/article/20.html @@ -0,0 +1,202 @@ + + + + + + 윈도우즈에서 React Native 개발 환경 세팅하기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 윈도우즈에서 React Native 개발 환경 세팅하기 +

          + +

          + 개발 환경 세팅만 사흘 +

          + + + + +
          +
          Table of Contents +

          +
          +

          이번에 참여하게 된 프로젝트에서 리액트 네이티브를 이용하게 됐다. 리액트 네이티브는 리액트 아키텍처를 모바일에 적용한 것으로, ES6 문법과 리액트를 이용해 모바일 어플리케이션을 개발할 수 있도록 해주는 프레임워크다. 기존의 웹 프로그래밍 문법을 (거의)그대로 사용할 수 있다는 점과 안드로이드/iOS 버전을 따로 개발할 필요가 없다는 점이 매력적이다.

          +
          +

          시작에 앞서, 윈도우 이용자라면 WSL(Windows Subsystem for Linux)을 설치하는 것이 좋다. WSL은 윈도우에 서브시스템으로 리눅스를 탑재하는 것으로, 윈도우에서 리눅스를 다룰 수 있게 된다. WSL 세팅은 윈도우에 우분투 돌리기를 참고.

          +
          +

          CRNA

          +

          리액트 네이티브 개발 환경을 완전히 밑바닥에서 시작하는 것은 꽤 번거로운 일이다. 은혜롭게도 Create React Native App이라는 좋은 도구가 있다. 이를 사용하면 기본적인 개발 환경을 완벽히 만들어준다.

          +
          # install it once globally
          +$ npm install -g create-react-native-app
          +
          +# create a new application and run
          +$ create-react-native-app my-app
          +
          +

          이렇게하면 my-app이라는 이름의 폴더가 만들어지고 그 안에 각종 파일들이 생긴 것을 볼 수 있다. 이제 실행시켜보자.

          +
          $ cd my-app
          +$ npm start
          +
          +

          잘 작동한다면 터미널에 QR코드가 띄워진다. 어플리케이션은 Expo를 이용해 빌드할 것이고, Genymotion을 이용해 가상 머신에서 구동시킬 것이다.

          +

          Expo

          +

          Expo는 리액트 네이티브 어플리케이션의 빌드를 돕는 툴이다. 네이티브 API에 접근하는 것도 쉽게 만들어주고, 안드로이드와 iOS 버전을 알아서 빌드해준다. 무엇보다 코드를 수정하면 바로 hot reloading 시켜주는 것이 가장 편하다.

          +

          GUI 툴인 Expo XDE를 사용한다면, 우선 Expo를 실행한 뒤 'Open existing project…'버튼을 클릭한다. 그리고 앞서 CRNA로 만든 my-app 폴더를 찾아 선택해주면 my-app을 알아서 빌드해준다.

          +

          CLI 툴을 이용하려면 먼저 npm install -g expo-cli로 expo-cli를 설치해준다. 그리고 my-app 폴더에서 expo start를 실행하면 my-app을 빌드해준다.

          +

          Genymotion

          +

          Genymotion은 안드로이드 가상 머신을 구동하기 위한 도구다. 다운로드하고, 가상 머신을 하나 만들면 준비가 끝난다.

          +

          실행

          +

          실행 단계는 다음과 같다.

          +
            +
          1. my-app 폴더에서 npm start를 실행한다.
          2. +
          3. Expo에서 my-app 폴더를 선택해 빌드한다.
          4. +
          5. (가상 머신의 경우) Genymotion에서 가상 머신을 실행하고 Expo 어플리케이션에서 my-app을 선택한다.
          6. +
          7. (실단말의 경우) 휴대폰에 Expo를 설치하고, 터미널에 띄워진 QR코드를 스캔한다.
          8. +
          +

          만약 expo-cli를 사용한다면 더 간단하다.

          +
            +
          1. my-app 폴더에서 expo start를 실행한다.
          2. +
          3. (가상 머신의 경우) Genymotion에서 가상 머신을 실행하고 Expo 어플리케이션에서 my-app을 선택한다.
          4. +
          5. (실단말의 경우) 휴대폰에 Expo를 설치하고, 터미널에 띄워진 QR코드를 스캔한다.
          6. +
          +

          자, 이렇게 잘되면 좋겠지만 안타깝게도 많은 에러가 발생할 것이다.

          +

          Unable to start server

          +

          npm start를 하고 만날 수 있는 에러다.

          +
          $ npm start
          +9:34:11 AM: Unable to start server
          +See https://git.io/v5vcn for more information, either install watchman or run the following snippet:
          +sudo sysctl -w fs.inotify.max_user_instances=1024
          +sudo sysctl -w fs.inotify.max_user_watches=12288
          +...
          +npm ERR! code ELIFECYCLE
          +npm ERR! whyerrors@0.1.0 start: `react-native-scripts start`
          +npm ERR! Exit status 1
          +npm ERR!
          +npm ERR! Failed at the whyerrors@0.1.0 start script 'react-native-scripts start'.
          +npm ERR! Make sure you have the latest version of node.js and npm installed.
          +npm ERR! If you do, this is most likely a problem with the whyerrors package,
          +npm ERR! not with npm itself.
          +npm ERR! Tell the author that this fails on your system:
          +npm ERR!     react-native-scripts start
          +npm ERR! You can get information on how to open an issue for this project with:
          +npm ERR!     npm bugs whyerrors
          +npm ERR! Or if that isn't available, you can get their info via:
          +npm ERR!     npm owner ls whyerrors
          +npm ERR! There is likely additional logging output above.
          +...
          +
          +

          커널 변수 max_user_instancesmax_user_watches의 값이 작아서 발생하는 오류다. 터미널에 찍힌대로 입력해주면 된다. sysctl은 커널 변수를 제어하는 명령어이며, -w 옵션은 이어서 나오는 값을 writing하겠다는 의미다.

          +
          $ sudo sysctl -w fs.inotify.max_user_instances=1024
          +$ sudo sysctl -w fs.inotify.max_user_watches=12288
          +
          +

          만약 값을 영구적으로 바꾸고 싶다면 다음과 같이 하면 된다:

          +
          $ echo fs.inotify.max_user_instances=1024 | sudo tee -a /etc/sysctl.conf
          +$ echo fs.inotify.max_user_watches=12288 | sudo tee -a /etc/sysctl.conf
          +$ sudo sysctl -p
          +
          +

          Starting packager… 무한 로딩

          +

          마찬가지로 npm start를 하면 만날 수 있다.

          +
          $ npm start
          +...
          +Starting packager...
          +
          +

          잘 되는 것 같지만 이 상태가 무한히 지속된다. 찾아보니 리눅스에만 생기는 문제인 것 같다길래 괜히 wsl을 썼나 싶었다. 일단 Expo 포럼에 올라온 질문 Packager not loading on Linux에 제시된 해결책을 써봤다.

          +
          $ rm -rf node_modules
          +$ npm install
          +
          +

          아예 node_modules를 지우고 다시 설치하니 잘 돌아간다. 나중에 알았는데, node_modules 폴더가 아니라 package_lock.json 파일을 지우고 다시해도 해결할 수 있었다.

          +

          터미널에 QR코드가 나오지 않는 문제

          +

          npm start를 하면 실제 단말기에서 실행시킬 수 있는 QR 코드가 터미널에 찍혀 나와야 한다. 그런데 QR 코드가 있어야 할 자리가 텅텅 비어있었다. vscode에 등록된 이슈 React native, QR Code not showing on terminal에 따르면 vscode의 문제였다. powershell을 열어 npm start를 실행했다. 그랬더니 QR 코드가 제대로 나타났다.

          +

          다음날 아침 vscode를 업데이트하니 vscode의 통합터미널에서도 QR 코드가 잘 나왔다. expo에서 따로 QR 코드를 얻을 수 있다는 것은 나중에야 알았다.

          +

          ADB server didn’t ACK, failed to start daemon

          +

          Genymotion에 S7 가상 머신을 만들고 Expo에서 open android를 클릭했더니 다시 에러가 발생했다.

          +
          * daemon not running. starting it now on port 9000 *
          +ADB server didn't ACK
          +* failed to start daemon *
          +error: cannot connect to daemon
          +
          +

          이쯤되니 리액트 네이티브하지 말라는 신의 계신인가 싶었다. 스택오버플로우에 올라온 질문 Eclipse error "ADB server didn’t ACK, filed to start daemon"의 답변을 보고 adb kill-server도 해보고 taskkill -f adb.exe도 해봤지만 소용이 없었다. expo 설정에서 SDK의 경로를 수동으로 입력하기도 했지만 그것도 소용 없었다.

          +

          그리고 두 번째 답변에 따라 sdk 버전을 업데이트하고 윈도우 환경변수까지 다시 설정했더니 해결됐다.

          +

          결론

          +

          맥을 사야겠다.

          + +
          +
          + +
          + +
          +
          +

          📡 WSL에서 SSH 서버 열기

          +

          학교에서 아이패드로 코딩하기

          +
          +
          +
          + + +
          + +
          +
          +

          🐧 윈도우에서 우분투 돌리기

          +

          개발을 위한 WSL 세팅

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/21.html b/article/21.html new file mode 100644 index 0000000..6b54c0a --- /dev/null +++ b/article/21.html @@ -0,0 +1,154 @@ + + + + + + 📡 WSL에서 SSH 서버 열기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 📡 WSL에서 SSH 서버 열기 +

          + +

          + 학교에서 아이패드로 코딩하기 +

          + + + + +
          +
          Table of Contents +

          +
          +

          SSH(Secure Shell)는 안전하게 원격 접속을 하기 위해 사용하는 프로토콜이다. 윈도우 데스크탑에서 SSH 서버를 열면 아이패드에서 원격으로 데스크탑 쉘에 접속을 할 수 있다. WSL(Windows Subsystem for Linux)은 윈도우의 서브시스템에 리눅스를 탑재하는 기술이다. 아직 부드럽게 작동하지 않는 부분들이 조금 있지만, 마이크로소프트에서 WSL에 신경을 많이 쓰고 있기 때문에 충분히 쓸만하다.

          +

          내 목적은 학교에서 아이패드로 집에 있는 윈도우 10 데스크탑에 원격 접속해 코딩을 하는 것이었다. 우선 데스크탑에 WSL 우분투 16.04를 설치했고, 아이패드에는 터미널 앱 Termius를 설치했다.

          +

          openssh-server 재설치

          +

          SSH 서버를 열기위해서는 openssh-server라는 패키지가 필요하다. WSL 우분투 16.04 기준으로 SSH 서버를 실행하는 명령 service ssh start를 실행해보면 몇가지 에러가 나타난다. 기본 설치되어있는 openssh-server의 문제이므로, 재설치해준다.

          +
          $ sudo apt remove openssh-server
          +$ sudo apt install openssh-server
          +
          +

          sshd_config 파일 수정

          +

          sshd_config는 SSH 설정 파일이다. SSH 서버를 열기 전에 이 파일을 약간 수정해줘야 한다. root 권한이 필요하므로, 자신의 계정에 root 권한이 없다면 root 계정으로 전환하고 /etc/ssh/sshd_config 파일을 연다.

          +
          $ sudo su - root
          +$ vi /etc/ssh/sshd_config
          +
          +

          여기서 바꿔야 할 부분은 두 곳이다. (우부투 18.04 버전이라면 Port 값만 수정해도 된다.)

          +
            +
          • Port의 값을 변경한다. SSH 기본 포트는 22인데, 이는 윈도우에서 이미 사용 중인 포트이기 때문에 WSL에서는 다른 값으로 바꿔줘야 한다. 적당히 2222로 했다.
          • +
          • PasswordAuthentication의 값을 yes로 바꿔준다. 패스워드 인증을 사용하도록 설정하는 것이다.
          • +
          +

          저장 후 SSH 서버를 재시작해준다.

          +
          $ sudo service ssh --full-restart
          +
          +

          포트포워딩

          +

          서버 컴퓨터가 공유기에 연결되어 있는 상황이므로 외부에서 접속하려면 포트포워딩을 해줘야한다. 공인 아이피는 공유기에 할당되는데, 공유기에 연결된 컴퓨터나 휴대폰 등의 디바이스에는 192.168.0.x 형태의 사설 아이피가 할당된다. 따라서 외부망에서 공인 아이피를 입력해도 이 서버 컴퓨터로는 접속을 할 수가 없다.

          +

          ipTIME을 사용 중이라면 아래 과정을 따르면 된다. (제조사마다 설정 인터페이스가 조금씩 다르다.)

          +
            +
          1. 브라우저에 공유기의 공인 아이피 또는 192.168.0.1을 입력한다.
          2. +
          3. 로그인 후 '고급 설정 > NAT/라우터 관리 > 포트포워드 설정’에 들어간다.
          4. +
          5. 내부 아이피 주소는 '현재 접속된 PC의 IP 주소로 설정’으로 설정한다.
          6. +
          7. 프로토콜을 TCP로 설정한다.
          8. +
          9. 외부 포트는 sshd_config 파일에서 설정한 Port 값으로 설정한다. (2222 ~ 2222)
          10. +
          11. 내부 포트도 똑같이 설정한다. (2222 ~ 2222)
          12. +
          13. 규칙 이름은 마음대로 정한다.
          14. +
          15. 좌측 상단의 '저장’을 클릭한다.
          16. +
          +

          공유기 포트포워딩은 검색해도 많이 나오고, 생활코딩 강의도 잘 되어있다.

          +

          방화벽 설정

          +

          SSH 포트가 방화벽으로 막혀있을 경우 접속이 불가능하다.

          +
            +
          1. '제어판 > 시스템 및 보안 > Windows Defender 방화벽’에 들어가 '고급 설정’을 클릭한다.
          2. +
          3. 인바운드 규칙을 클릭하고 '새 규칙’을 클릭한다.
          4. +
          5. 규칙 종류는 '포트’를 선택한다.
          6. +
          7. 프로토콜은 TCP, '특정 원격 포트’는 앞서 설정한 Port 값으로 설정한다. (2222) 이름은 마음대로 정한다.
          8. +
          9. 아웃바운드 규칙도 마찬가지로 해준다.
          10. +
          +

          SSH 서버 접속

          +

          WSL bash를 종료하면 SSH 서버도 닫혀버린다. 다시 열고 싶을 때는 아래 명령을 실행하면 된다.

          +
          $ sudo service ssh start
          +
          +

          PuttyTermius 같은 프로그램을 이용해 접속 테스트를 해보자. 서버와 같은 컴퓨터에서 접속하는 경우 hostname은 서버의 공인 아이피로, port는 앞서 설정한 2222로 두면 된다.

          +

          이어서 외부망의 넷북에서 테스트했고, 이후 아이패드에서도 테스트했는데 잘 됐다! 이제 학교에 노트북들고 가지 않고 프로그래밍을 할 수 있게 되었다.

          + +
          +
          + +
          + +
          +
          +

          🌐 Top-Down으로 접근하는 네트워크

          +

          Computer Networks and the Internet

          +
          +
          +
          + + +
          + +
          +
          +

          윈도우즈에서 React Native 개발 환경 세팅하기

          +

          개발 환경 세팅만 사흘

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/23.html b/article/23.html new file mode 100644 index 0000000..21d3dad --- /dev/null +++ b/article/23.html @@ -0,0 +1,285 @@ + + + + + + 🌐 Top-Down으로 접근하는 네트워크 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 🌐 Top-Down으로 접근하는 네트워크 +

          + +

          + Computer Networks and the Internet +

          + + + + +
          +
          Table of Contents +

          +
          +

          James F. Kurose, Keith W. Ross의 Computer Networking: A Top-Down Approach는 잘 모르는 책이었는데 의외로 많은 학교에서 교재로 쓰이는 것 같다. 컴퓨터 네트워크 수업을 들으며 Computer Networking: A Top-Down Approach 7th Edition의 첫 챕터를 정리해보기로 했다.

          +

          첫 챕터는 컴퓨터 네트워크의 전반을 둘러보는 챕터다. 언제나 그렇듯 overview 챕터가 책에서 가장 중요하다. 원래 책 전체를 정리하려 했으나 더 이상 책을 번역하는 작업하는 정도의 정리는 무리라고 판단, 개괄적인 내용을 다루는 Ch.1 Computer Networks and the Internet만 정리하기로 했다.

          +

          What is the Internet?

          +

          인터넷이 뭘까? 여기서는 두 가지 이야기를 해볼 수 있다. 첫 번째는 인터넷을 구성하는 볼트와 너트, 하드웨어와 소프트웨어에 관한 것이고, 두 번째는 네트워킹 인프라에 관한 것이다.

          +

          A Nuts-and-Bolts Description

          +

          인터넷은 수많은 컴퓨팅 디바이스들이 연결된 컴퓨터 네트워크다. 몇 년전에는 PC, 리눅스 워크스테이션과 같은 전통적인 컴퓨터들이 인터넷을 구성했다면, 지금은 스마트폰, 웨어러블 기기, 태블릿, 자동차 등 거의 모든 것이 인터넷에 연결되고 있다. 따라서 컴퓨터 네트워크라는 용어가 조금은 구시대적인 말일 수도 있다.

          +

          인터넷에서 모든 기기들은 호스트(Host)와 엔드 시스템(End system)으로 나뉜다. 엔드 시스템은 커뮤니케이션 링크(Communication link)와 패킷 스위치(Packet switch)의 네트워크에 함께 연결되어 있다. 커뮤니케이션 링크는 구리선, 광케이블과 같은 여러 종류의 물리적 매체로 만들어져 있다. 서로 다른 링크는 각자 다른 전송률(Transmission rate)를 갖는다.

          +

          한 엔드 시스템에서 다른 엔드 시스템으로 데이터를 전송할 때, 데이터를 전송하는 엔드 시스템은 데이터를 나누고, 각 세그먼트에 헤더를 추가한다. 최종적으로 만들어지는 정보의 묶음을 패킷(Packet)이라고 부르며, 네트워크를 통해 목적지의 엔드 시스템으로 보내진다.

          +

          과거에는 회선 교환(Circuit switching) 방식을 사용했다. 과거 전화 방식이 이랬다. A가 B에게 전화를 하려면 먼저 교환수에게 B 회선에 연결해달라고 말해야 한다. 그리고 교환수가 교환기의 A 회선을 B에 연결하면 비로소 전화를 할 수 있었다. 현재 가장 많이 사용되는 통신 방식은 패킷 교환(Packet switching) 방식으로, 교환수 대신 라우터(Router)가 패킷의 경로를 설정하는 역할을 한다. 라우터는 패킷 스위치의 일종이며, 패킷 스위치는 패킷의 경로를 설정하는 네트워크 장비를 말한다.

          +

          엔드 시스템은 ISP(Internet Service Providers)를 통해 인터넷에 접속한다. ISP는 KT, SKT, LG U+와 같은 통신사를 말하며, 이들은 보유하고 있는 인터넷 회선을 개인이나 기업에게 임대해준다. IP를 할당해주는 것도 ISP다.

          +

          Services Description

          +

          지메일, 넷플릭스, 멜론, 페이스북과 같은 어플리케이션들을 분산 어플리케이션(Distributed application)라고 부른다. 분산 어플리케이션은 네트워크 상의 여러 엔드 시스템에서 서비스되는 어플리케이션으로, 서로 데이터를 교환하며 동작한다. 분산 어플리케이션은 단일 시스템에서 동작하는 전통적인 어플리케이션과 대비되는 용어로, 현재 사람들이 주로 사용하는 프로그램은 거의 모두 분산 어플리케이션이라고 봐도 무방하다.

          +

          엔드 시스템에서 작동하는 어플리케이션은 C, Java, 또는 Python 등 서로 다른 기반 위에 작성된다. 서로 다른 엔드 시스템 위에서 동작하는 분산 어플리케이션은 서로에게 데이터를 전송할 수 있어야 하는데, 여기서 중요한 이슈가 생긴다. 한 엔드 시스템에서 동작하는 어플리케이션이 어떻게 다른 엔드 시스템의 어플리케이션에게 데이터를 전송할 수 있는 걸까?

          +

          인터넷에 연결된 엔드 시스템은 소켓 인터페이스(Socket interface)를 제공한다. 소켓 인터페이스는 프로그램이 어떻게 다른 엔드 시스템에게 데이터를 전달할 수 있는 지 명시한 것으로, 현실에서 두 사람이 우편을 주고 받을 때 우체국의 역할을 한다고 생각하면 쉽다.

          +

          A Human Analogy & Nework Protocols

          +

          네트워크는 사람이 대화하는 것과 유사하다. 밥이 앨리스에게 "Hi"라고 말하면 앨리스도 밥에게 "Hi"라고 말한다. 마찬가지로 컴퓨터도 통신 연결을 요청하면 요청에 대한 응답을 보낸다.

          +

          성공적인 소통을 위해서는 대화의 주제, 대화의 수단, 대화의 시간이 일치해야 한다. 만약 한 쪽은 전화로 대화하려 하고, 한 쪽은 메일로 대화하려 한다면 소통이 불가능할 것이다. 마찬가지로 네트워크에서는 다음과 같은 요소들이 필요하다.

          +
            +
          • 문법(Syntax): 데이터의 형식, 인코딩/디코딩 정보.
          • +
          • 시맨틱(Semantic): 발신과 수신에 대해 정해진 행동, 에러 처리.
          • +
          • 타이밍(Timing): 메시지의 순서, 속도. (큰 파일을 보낼 경우 이를 여러 조각으로 잘라서 보내야 하는데, 이때 수신자가 어떤 순서로 조각을 합쳐야 하는지 알 수 있어야 한다.)
          • +
          +

          위 세가지 요소의 집합을 통신 규약(Communication protocol)이라고 한다.

          +

          The Network Edge

          +

          엔드 시스템은 웹 브라우저나 웹 서버와 같은 어플리케이션을 동작시키기 때문에 호스트라고도 한다. (책에서는 엔드 시스템과 호스트를 같은 의미로 사용한다.) 호스트는 클라이언트(Client)와 서버(Server)로 나뉜다. 쉽게 생각하면, 클라이언트는 데스크탑이나 노트북, 스마트폰이 될 수 있다. 한편 웹 페이지나 스트림 비디오 등을 저장하고 여러 곳에 전송할 수 있는 장비는 서버가 될 수 있다. 우리 사용하는 서버는 거대한 데이터 센터에 위치하고 있다.

          +

          Access Networks

          +

          2014년 기준, 선진국 가정의 78퍼센트 이상은 집에서 인터넷에 접속이 가능하다. 한국, 네덜란드, 핀란드, 스웨덴은 80퍼센트 이상 가능하다. 오늘날 통신에서는 광대역, IP 기반, 유무선 통합, 지능형이 트렌드로 자리 잡았다.

          +

          몇 년 전까지 IPTV와 VoIP, WoIP이 대세였고, 지금은 인터넷에 연결되는 기기의 종류가 늘어나며 XoIP(Anything over IP)라고 부르게 되었다. 이제 QoS나 QoE를 통해 X2X(Any to Any) 네트워크를 구축하고 있는 단계에 있으며, 미래에는 SDN이나 CCN과 같은 새로운 네트워크가 나타날 것이다.

          +

          Physical Media

          +

          통신에는 물리적 매체도 중요하다. 동축 케이블과 광 케이블과 같이 유선 연결을 위한 매체와 더불어 지상파 채널, 위성 채널과 같은 무선 연결을 위한 매체가 쓰인다.

          +

          The Network Core

          +

          네트워크 코어는 패킷 스위치 네트워크와 엔드 시스템들의 연결을 말한다.

          +

          Packet Switching

          +

          네트워크 어플리케이션에서 엔드 시스템들은 서로 메시지를 교환한다. 메시지는 텍스트, 이미지, 영상 등 무엇이든 담을 수 있다. 메시지를 목적지의 엔드 시스템에 보내면 발신 엔드 시스템은 메시지를 여러 조각으로 작게 나눈다. (이것이 패킷이다.) 발신 엔드 시스템과 수신 엔드 시스템 사이의 각 패킷은 커뮤니케이션 링크와 패킷 스위치(라우터와 링크 레이어 스위치)를 통해 전송된다. 만약 패킷 스위치가 L bits 패킷을 R bits/sec으로 전송하면 패킷이 전송되는 데 걸리는 시간은 L/R 초가 된다.

          +

          Store-and-Forward Transmission

          +

          대부분의 패킷 스위치는 저장 후 전달 전송(Store-and-Forward Transmission) 방식을 사용한다. store-and-forward 전송 방식에서 패킷 스위치는 패킷의 첫 비트를 링크에 전송하기 전에 패킷의 모든 비트를 받아야 한다.

          +
                                      Router
          ++--------+ 3 2 1          /--------\            +-------------+
          +| Source +-#-#-#---------+-->#      +-----------+ Destination |
          ++--------+                \--------/            +-------------+
          +                             Front of packet 1
          +                             stored in router,
          +                             awating remaining
          +                             bits before forwarding
          +
          +

          하나의 라우터에 연결된 두 개의 엔드 시스템을 상상해보자. 만약 한 엔드 시스템에서 패킷을 3개 전송하면, 라우터에 패킷이 순서대로 도착하게 된다.

          +

          Queuing Delays and Packet Loss

          +

          각 패킷 스위치네는 여러 개의 링크가 연결되어 있다. 각각의 링크에 대해 패킷 스위치는 출력 버퍼(Output buffer or Output queue)를 가지고 있다. 출력 버퍼는 링크에 보낼 패킷을 담고 있으며, 패킷 교환에 중요한 역할을 한다.

          +

          만약 패킷이 도착하면 링크에 전송해야 하지만, 링크가 혼잡하다면 패킷은 출력 버퍼에서 대기한다. 이때 대기할 때 지연되는 시간을 큐 지연(Queuing delay)이라고 부른다. 만약 패킷이 도착했을 때 이미 버퍼가 가득차 있다면 패킷 손실(Packet loss)이 일어나며, 도착한 패킷이나 큐 안에 있는 패킷이 손실된다.

          +

          Delay, Loss, and Throughput in Packet-Switched Networks

          +

          엔드 시스템끼리 통신할 때는 패킷 전송이 지연되거나 일부 패킷이 손실될 수 있으며, 컴퓨터 네트워크는 다양한 문제를 직면할 수 있다.

          +

          Overview of Delay in Packet-Switched Networks

          +

          호스트(the source)에서 출발한 패킷은 라우터들을 거쳐 또 다른 호스트(the destination)에서 끝난다. 패킷이 전송될 때 각 노드(호스트나 라우터)에서 일어나는 지연이 발생할 수 있는데, 여기에는 몇 가지 종류가 있다.

          +

          Types of Delay

          +
          +------+
          +| Node |-#---+      Router A
          ++------+     |     /--------\            /--------\
          +             +--->|      ### #-->-------+ Router B |
          ++------+     |     \--------/            \--------/
          +| Node |-#---+    |-----|---|-|---------|
          ++------+             |    |  |     |
          +                     |    |  |     Propagation
          +                     |    |  |
          +                     |    |  Transmission
          +                     |    |
          +                     |    Queueing Transmission (waiting for transmission)
          +                     |
          +                     Nodal processing
          +
          +
            +
          • 처리 지연(Processing Delay): 패킷의 헤더(Header)를 확인하고 패킷이 어디로 가야하는 지 결정할 때 지연되는 것을 말한다. 패킷의 에러를 체크할 때도 프로세싱 지연이 일어날 수 있다. 고속 라우터에서는 프로세싱 지연은 몇 마이크로초 이하로 걸린다. 이 과정 이후 라우터는 패킷을 링크된 라우터의 큐로 보낸다.
          • +
          • 큐 지연(Queuing Delay): 패킷이 큐에서 전송되기를 기다릴 때 발생하는 지연을 말한다. 지연의 길이는 몇 개의 패킷이 이미 큐에 있는지, 링크로 전송될 때까지 얼마나 대기해야 하는지에 따른다. 만약 큐가 비어있고, 어떠한 패킷도 전송되고 있지 않다면 패킷의 지연은 0이다. 반면, 트래픽이 혼잡하고 많은 패킷이 전송되길 기다리고 있다면 지연은 길어질 것이다. 큐 지연은 마이크로초에서 밀리초 정도가 될 수 있다.
          • +
          • 전송 지연(Transmission Delay): 패킷 스위치에서 패킷은 FCFS(First-Come-First-Served)에 따라 전송되는데, 이때 패킷의 모든 비트가 도착해야 패킷이 전송될 수 있다. 패킷의 길이는 L bits이고, 라우터 A와 라우터 B 사이 링크의 전송률은 R bits/sec이다. 예를 들어, 10 Mbps 이더넷 링크의 전송률은 R=10 Mbps이며, 100 Mbps 이더넷 링크의 전송률은 R-100 Mbps이다. 그리고 이때 전송 지연은 L/R이다.
          • +
          • 전파 지연(Propagation Delay): 링크로 보내진 비트는 라우터 B로 전송되어야 하는데, 이때 걸리는 시간이 전파 지연이다. 비트는 링크의 전파 속도에 따라 전파되며, 전파 속도는 링크의 물리적 매체(구리선, 광섬유 등)에 따라 다르다. 전파 속도의 범위는 보통 2 * 108 meters/sec에서 3 * 108 meters/sec사이이며, 이는 빛의 속도보다 조금 느린 정도다. 전파 지연은 두 라우터의 거리 d를 전파 속도 s로 나눈 것으로, d/s가 된다. WAN(Wide-Area Networks)에서 전파 지연은 밀리세컨드 수준이다.
          • +
          +

          Protocol Layers and Their Service Models

          +

          인터넷은 굉장히 복잡한 시스템이다. 네트워크 아키텍처를 더욱 효율적으로 조직할 수 있는 방법이 있다.

          +

          Layered Architecture

          +

          레이어 아키텍처는 기능별로 서로 독립적인 계층(Layer)을 나누는 것을 말한다. (운영체제의 그것과 동일하다.) 두 철학자가 대화하는 상황을 생각해보자. 한 쪽은 한국 철학자이고, 한쪽은 중국 철학자이다. 이들의 대화 주제는 철학으로 동일하다. 하지만 모국어가 다르기 때문에 통역가를 거쳐야 한다. 통역가는 각자 한국어를 영어로, 영어를 한국어로, 중국어를 영어로, 중국어를 영어로 통역할 수 있다. 또한 두 철학자가 멀리 떨어져 있기 때문에 전화를 통해 대화해야 한다.

          +

          이때 레이어는 (1)철학자 레이어 (2)통역가 레이어 (3)통신사 레이어로 분리할 수 있다. 철학자 레이어는 통역이 어떻게 되는지 신경쓸 필요가 없다. 통역가 레이어에서도 철학자들이 어떤 주제로 대화하는지 신경쓸 필요가 없다. 통신사 레이어는 어떤 언어인지 신경쓸 필요가 없으며, 사람 목소리든 바람소리든 전달만 하면 된다. 즉, 각 레이어가 독립적으로 동작하며, 자신의 작업에만 집중할 수 있게 된다. 또한 어떤 문제가 생겼을 때 어디서 문제가 생겼는지 파악하기도 쉬워진다. 그리고 여기서 레이어와 레이어를 연결해주는 것을 인터페이스(Interface)라고 한다.

          +

          두 시스템이 통신하기 위해 레이어 모델이 지켜야 하는 규칙은 다음과 같다:

          +
            +
          • 레이어의 개수가 동일해야 한다.
          • +
          • 서로 통신하는 두 레이어의 프로토콜이 같아야 한다.
          • +
          • 상위 레이어(Upper layer)와 하위 레이어(Lower layer)의 인터페이스가 같아야 한다.
          • +
          +

          Protocol Layering

          +

          네트워크의 레이어 모델에는 대표적으로 OSI(Open Systems Interconnection) 모델과 TCP/IP 프로토콜 슈트가 있다.

          +
          +----------------------+  +--------------------+
          +|                      |  | Application Layer  |
          +|                      |  +--------------------+
          +| Application Layter   |  | Presentation Layer |
          +|                      |  +--------------------+
          +|                      |  | Session Layer      |
          ++----------------------+  +--------------------+
          +| Transport Layer      |  | Transport Layer    |
          ++----------------------+  +--------------------+
          +| Internet Layer       |  | Network Layer      |
          ++----------------------+  +--------------------+
          +|                      |  | Data Link Layer    |
          +| Network Access Layer |  +--------------------+
          +|                      |  | Physical Layer     |
          ++----------------------+  +--------------------+
          +         TCP/IP                    OSI
          +
          +

          OSI 모델은 국제 표준화 기구(ISO)에서 만든 공식적 표준 모델이다. 하지만 계층이 7개나 되기 때문에 지금은 잘 쓰이지 않게 되었다. 인터넷 프로토콜 슈트 중 압도적으로 많이 쓰이는 것은 TCP/IP 프로토콜로, 사실상 표준이라고 볼 수 있다. TCP/IP의 계층은 4개이며, 미국 방위고등연구계획국(DARPA)에서 만들었다.

          +

          Application Layer

          +

          어플리케이션 레이어는 HTTP, SMTP, FTP 등 다양한 프로토콜을 포함하고 있으며, 여러 엔드시스템에 분산되어 다른 엔드 시스템에 패킷을 교환하는 역할을 하는다. 어플리케이션 레이어의 PDU(Protocol Data Unit)는 메시지(Message) 또는 단순히 데이터(Data)라고 한다.

          +

          Transport Layer

          +

          어플리케이션 사이에서 어플리케이션 레이어 메시지를 전송하는 역할을 한다. 인터넷에는 TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)라는 두 개의 전송 프로토콜이 있다. TCP는 연결 지향(Connection oriented)이며, 데이터가 잘 전달 됐는지 매번 확인하기 때문에 속도가 느리지만 데이터 손실이 없다. 반면 UDP는 비연결 지향(Connectionless oriented)이며, 데이터 손실이 있지만 속도가 빠르다. 트랜스포트 레이어의 PDU는 세그먼트(Segment)라고 하며, 세그먼트에 포함되는 트랜스포트 레이어 헤더에는 포트 번호 필드가 있다.

          +

          Network Layer

          +

          네트워크 레이어의 PDU는 데이터그램(Datagram) 또는 패킷(Packet)이며, 네트워크 레이어는 데이터그램을 호스트에서 다른 곳으로 보내는 역할을 한다. 발신 호스트의 트랜스포트 레이어 프로토콜(TCP 또는 UDP)에서 세그먼트와 목적지 주소를 네트워크 레이어에게 보내면 네트워크 레이어는 세그먼트를 목적지 호스트의 트랜스포트 레이어에게 보낸다. TCP/IP 모델에서 네트워크 레이어의 프로토콜은 IP(Internet Protocol)만 쓰이며, 이 덕분에 통신을 용이하게 할 수 있다. 데이터그램에 포함되는 헤더에는 프로토콜 필드가 있다.

          + +

          네트워크 레이어가 라우팅한 데이터그램은 발신지와 수신지 사이의 라우터들을 거친다. 패킷이 한 노드(호스트 또는 라우터)에서 다음 노드로 움직일 때, 네트워크 레이어는 링크 레이어 위에서 동작하게 된다. 이때 각 노드의 네트워크 레이어가 데이터그램을 링크 레이어로 내려보내는데, 링크 레이어는 이 데이터그램을 다음 노드의 링크 레이어에게 보낸다. 그리고 다음 노드의 링크 레이어는 수신한 데이터그램을 네트워크 레이어에게 올려보낸다. 링크 레이어의 PDU는 프레임(Frame)이며, 프레임의 헤더에는 타입 필드가 있다.

          +

          Physical Layer

          +

          피지컬 레이어의 역할은 프레임의 각 비트를 다음 노드로 보내는 것이다. 피지컬 레이어의 프로토콜은 링크의 종류에 따라 달라지는데, 가령 이더넷의 경우 구리연선이나 동축 케이블, 단일 모드 광섬유 등 다양한 프로토콜을 가진다. 피지컬 레이어의 PDU는 비트(Bit)다.

          +

          Encapsulation

          +
                                <Source>      |
          +                      +-------------+-+
          +          Message [M] | Application | |
          +                      +-------------+-+
          +      Segment [Ht][M] | Transport   | |
          +                      +-------------+-+
          + Datagram [Hn][Ht][M] | Network     | |
          +                      +-------------+-+
          +Frame [Hl][Hn][Ht][M] | Link        | |
          +                      +-------------+-+
          +                      | Physical    | |                   +----------+
          +                      +-------------+-+                   |          |
          +                                    |                   +-+----------+-+
          +                                    |   [Hl][Hn][Ht][M] | | Link     | | [Hl][Hn][Ht][M]
          +                                    |                   |-+----------+-+
          +                                    |                   | | Physical | |
          +                                    |                   +-+----------+-+ <Link-layer switch>
          +                                    |                     |          |
          +                                    +---------------------+          |
          +                <Destination> ^                                      |
          +                +-------------+-+                                    |
          +            [M] | Application | |                                    |
          +                +-------------+-+                     +----------+   |
          +        [Ht][M] | Transport   | |                     |          |   |
          +                +-------------+-+                   +-+----------+-+ |
          +    [Hn][Ht][M] | Network     | |       [Hn][Ht][M] | | Network  | | |     [Hn][Ht][M]
          +                +-------------+-+                   |-+----------+-+ |
          +[Hl][Hn][Ht][M] | Link        | |   [Hl][Hn][Ht][M] | | Link     | | | [Hl][Hn][Ht][M]
          +                +-------------+-+                   |-+----------+-+ |
          +                | Physical    | |                   | | Physical | | |
          +                +-------------+-+                   +-+----------+-+ | <Router>
          +                              |                       |          |   |
          +                              +-----------------------+          +---+
          +
          +

          위 그림은 데이터가 전송될 때 어떤 경로를 거치는지 보여준다. 그림에 있는 링크 레이어 스위치와 라우터는 모두 패킷 스위치다. 엔드 시스템과 비슷하게 라우터와 링크 레이어 스위치도 레이어 모델을 취하고 있다. 하지만 모든 레이어가 구현되어 있는 것은 아닌데, 링크 레이어 스위치의 경우 링크 레이어와 피지컬 레이어만있고, 라우터는 네트워크 레이어와 링크 레이어, 피지컬 레이어만 가지고 있다.

          +

          중요한 것은 캡슐화(Encapsulation)이다. 그 과정을 자세히 보자:

          +
            +
          1. 발신 호스트에서 에플리케이션 레이어 메시지는 트랜스포트 레이어로 보내진다. 트랜스포트 레이어는 메시지를 받고, 여기에 트랜스포트 레이어 헤더 정보를 덧붙인다. 어플리케이션 레이어 메시지와 트랜스포트 레이어 헤더는 트랜스포트 레이어 세그먼트를 구성한다.
          2. +
          3. 트랜스포트 레이어는 세그먼트를 네트워크 레이어로 보내고, 네트워크 레이어는 여기에 네트워크 레이어 헤더 정보를 붙여 (여기에는 발신지와 수신지의 주소가 들어있다.) 네트워크 레이어 데이터그램을 만든다. 그리고 이를 링크 레이어로 보낸다.
          4. +
          5. 링크 레이어는 네트워크 레이어 데이터그램에 링크 레이어 헤더를 붙여 링크 레이어 프레임을 만든다.
          6. +
          +

          각 레이어에서 패킷은 두가지 필드를 가지는데, 하나는 헤더 필드(Header field)이고, 하나를 페이로드 필드(Payload field)이다. 페이로드는 상위 레이어에서 내려온 패킷을 말한다.

          +

          Networks Under Attack

          +

          오늘날 인터넷은 기업, 대학, 정부기관 등 거의 모든 곳에서 사용하고 있기 때문에 보안이 매우 중요하다. 공격은 다음과 같이 분류할 수 있다:

          +
            +
          • 멀웨어(Malware) 설치
          • +
          • 서버 또는 네트워크 인프라(DoS; Denial-Of-Service Attack) 공격
          • +
          • 패킷 감청(Sniffing)
          • +
          • 신뢰할 수 있는 사람 사칭
          • +
          +

          History of Computer Networking and the Internet

          +

          위 내용들만 알아도 가족들이나 친구들한테 아는 척을 좀 할 수 있다. 하지만 칵테일 파티에서 인싸가 되고 싶다면 인터넷의 역사를 알아야 한다. (진짜 책에 이렇게 나온다.)

          +

          The Development of Packet Switching: 1961-1972

          +

          전쟁 공포가 고조되던 냉전시대에는 전화 네트워크를 기반으로한 회선 교환 방식이 쓰였다. 하지만 회선 교환 방식은 전화국만 폭격되면 통신이 마비되는 치명적인 문제가 있었다.

          +

          이를 해결하기 위해 MIT 대학원생 Leonard Kleinrock이 처음으로 패킷 교환 기술을 발표했고, 이후 1964년 랜드 연구소의 Paul Baran이 군사 네트워크에 패킷 교환 기술을 사용하는 방법을 고안했다. 또한 영국의 국립물리연구소의 Donald Davies와 Roger Scantlebury도 패킷 교환을 개발해냈다.

          +

          1969년 미국 국방부 산하의 고등 연구국(ARPA)은 UCLA와 스탠포드 연구소(SRI)를 연결해 최초의 패킷 교환 방식 네트워크를 구축했다. 이 통신망을 ARPAnet이라고 불렀고, 이후 1972년에는 15개 노드가 더 연결되었다.

          +

          Proprietary Networks and Internetworking: 1972-1980

          +

          초기 ARPAnet은 단일의 폐쇄형 네트워크였다. 1970년대 초중반, 하와이 대학교가 위성과 지상을 무선 연결하는 네트워크를 구축했고, 이를 ALOHAnet(Additive Links Online Hawwaii Area)이라고 불렀다. 그 외에도 텔넷(Telenet), 시분할 네트워크(Time-sharing networks), SNA(Systems Network Architecture) 등 수많은 네트워크가 구축됐다.

          +

          A Proliferation of Networks: 1980-1990

          +

          70년대 말에는 거의 200개의 호스트가 ARPAnet에 연결되었다. 그리고 80년대 말에는 수 많은 호스트가 공개 인터넷에 연결되었고, 네트워크는 오늘날의 인터넷과 비슷해 보였다.

          +

          1983년 1월 1일에는 공식적으로 TCP/IP가 NCP를 대체해 ARPAnet의 새로운 표준 호스트 프로토콜로 자리잡았다. 이때 DNS와 32비트 IP 주소가 개발되었다.

          +

          The Internet Explosion: The 1990s

          +

          90년대의 중요한 사건은 WWW(World Wide Web)의 탄생이다. WWW는 CERN의 Tim Berners-Lee가 1989에서 1991년 사이 고안한 인터넷 시스템이다. 이후 수 많은 컴퓨터들이 인터넷에 연결되었고, 구글이나 아마존, 페이스북 같은 어플리케이션들이 만들어졌다.

          + +
          +
          + +
          + +
          +
          +

          🔐 HTTPS는 어떻게 다를까?

          +

          진짜 데이터를 뜯어보았다

          +
          +
          +
          + + +
          + +
          +
          +

          📡 WSL에서 SSH 서버 열기

          +

          학교에서 아이패드로 코딩하기

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/24.html b/article/24.html new file mode 100644 index 0000000..12fb5d4 --- /dev/null +++ b/article/24.html @@ -0,0 +1,149 @@ + + + + + + 🔐 HTTPS는 어떻게 다를까? + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 🔐 HTTPS는 어떻게 다를까? +

          + +

          + 진짜 데이터를 뜯어보았다 +

          + + + + +
          +
          Table of Contents +

          +
          +

          이미 HTTPS의 중요성은 널리 알려져 있다. 크롬, 파이어폭스와 같은 브라우저는 HTTP 서버에 접속할 때 경고를 띄우니 사용자 경험을 위해서라도 HTTPS는 필수라고 할 수 있다. 내가 개떡같은 코드와 함께한 하루 리뉴얼 이야기에서 그렇게 삽질한 이유이기도 하다.

          +

          HTTPS를 사용하면 데이터가 암호화되고, 보안에 좋다는 것은 알았지만, HTTP 접속과 비교하며 실제로 오가는 데이터를 분석해보니 그 사실이 더욱 가깝게 다가왔다. 컴퓨터 네트워크에 대한 배경 지식은 Top-Down으로 접근하는 네트워크를 참고.

          +

          🦈 Wireshark 사용하기

          +

          Wireshark는 오픈소스 패킷 분석 프로그램이다. (패킷은 네트워크에서 오가는 데이터 조각을 말한다.) Wireshark를 사용하면 네트워크 상에서 이동하는 패킷을 캡쳐해 그 내용을 볼 수 있다. PCAP은 트래픽을 캡쳐하기 위한 인터페이스이기 때문에 Wireshark를 설치할 때 꼭 같이 설치해야 한다.

          +

          +

          첫 화면에 다양한 인터페이스가 보이는데, 와이파이에 연결된 노트북과 네트워크 사이에 오가는 패킷을 캡처하고 싶으니까 Wi-Fi를 선택했다.

          +

          필터를 설정하지 않으면 해당 인터페이스의 모든 패킷이 잡힌다. 그 양이 상상 이상으로 많고, 속도도 빠르기 때문에 필터를 잘 설정해 원하는 데이터만 골라서 보는 것이 중요하다. 상단 'Apply a display filter’에 필터 조건을 입력하면 해당 조건에 맞는 패킷만 골라서 보여준다.

          +

          🔌 HTTP 접속

          +

          이후 HTTP 접속과 HTTPS 접속을 비교해야 하므로 HTTP 주소에 접속해도 HTTPS로 리다이렉트되지 않는 사이트를 먼저 찾아야 했다. 예상대로(?) 학교 홈페이지는 http://www.ajou.ac.kr도 접속이 가능하고, https://www.ajou.ac.kr도 접속이 가능했다.

          +

          Wireshark를 켜둔 채 브라우저를 통해 http://www.ajou.ac.kr에 접속하면 Wireshark에 캡처된다. 수많은 데이터 속에 파묻혀 버리지 않도록 다음과 같은 필터를 만들었다:

          +
          ip.addr == 202.39.0.19 && http
          +
          +

          이제 source나 destination의 아이피가 202.39.0.19이고, 프로토콜이 HTTP인 패킷만 보여준다. 서버 아이피는 cmd에서 ping www.ajou.ac.kr을 실행해 확인했다.

          +

          브라우저에서 http://www.ajou.ac.kr을 새로고침하니 패킷이 쭉 나왔다. 그 중 하나를 선택해 내용을 살펴봤다.

          +

          HTTP Request Message

          +

          +

          http://www.ajou.ac.kr에게 /_resources/new/img/index/btn_pop_close.gif를 요청하는 HTTP 메시지이다. HTTP 요청 메시지는 크게 Request line, Header lines, Entity body로 나눌 수 있다. Request line에는 GET /_resources/new/img/index/btn_pop_close.gif HTTP/1.1\r\n이 있고, 그 아래 Header lines에는 Host, User-Agent, Accept-Langueage 등 여러 헤더들이 따라온다.

          +

          User-Agent 헤더를 통해 클라이언트가 파이어폭스를 사용하고 있다는 점을 알 수 있고, Referer를 통해 클라이언트가 http://www.ajou.ac.kr/main/index.jsp에서 넘어왔다는 것을 알 수 있다. Referer를 보면 블로그의 방문자 유입 경로와 같은 정보를 얻을 수 있다.

          +

          그리고 쿠키도 보이는데, 여기서는 PHAROS_VISITORJSESSIONID라는 쿠키가 사용되었다. JSESSIONID는 톰캣 서버에서 JSP를 실행할 때 세션ID를 구분하려는 목적으로 만들어진 쿠키다. PHAROS_VISITOR는 어떤 목적으로 쓰이는지 알 수 없었다.

          +

          HTTP Response Message

          +

          +

          앞선 요청에 대한 서버의 응답 메시지다. HTTP 응답 메시지는 Status line, Header lines, Entity body로 나눌 수 있으며, Status line에 요청이 잘 처리되었다는 의미인 200 OK가 담긴 것을 볼 수 있다.

          +

          한편 Last-Modified 헤더를 통해 캐시된 파일이 2017년 2월 23일 목요일에 마지막으로 수정되었다는 것과 Content-Type 헤더를 통해 GIF 이미지이라는 것도 알 수 있다. (사진 캡처한게 10월 11일 오후 11시쯤인데 Date가 왜 오후 1시로 나온건지 모르겠다.)

          +

          마지막으로 Body에는 GIF 파일이 담겨있다.

          +

          Conditional GET

          +

          사실 클라이언트는 오리진 서버에게 바로 요청 메시지를 보내지 않고, 중간에 있는 프록시 서버에게 메시지를 보내 요청하는 오브젝트가 캐시되어 있는지 확인한다. 이때, 프록시 서버로 하여금 오리진 서버에게 캐시된 파일이 변경됐는지 확인하도록 하는 것이 conditional GET이다.

          +

          +

          conditional GET의 동작을 보기 위해 또 다른 패킷을 살펴봤다. HTTP 요청 메시지에 If-Modified-Since 헤더가 포함되어 있으면 프록시 서버는 오리진 서버에게 요청을 보내 캐시된 파일이 변경됐는지 확인한다. 위 메시지에는 If-Modified-Since 헤더가 있으므로 프록시 서버가 오리진 서버에게 메시지를 보냈을 것이다.

          +

          +

          304 Not Modified 응답이 왔다. 파일이 변경되지 않았다는 의미로, 그냥 프록시 서버에 캐시된 리소스를 사용했다.

          +

          🔌 HTTPS 접속

          +

          HTTPS는 HTTP 프로토콜에 SSL 프로토콜을 더해 보안을 강화한 것이다. 즉, 새로운 프로토콜이 아니다. 더 엄밀히 말하자면 SSL 프로토콜은 과거에 사용되었고, 지금은 SSL을 발전시킨 TLS를 사용한다. 하지만 대체로 SSL과 TLS를 섞어서 말한다.

          +

          HTTP로 접속했을 때는 모든 패킷의 내용을 뜯어 볼 수 있었다. HTTPS는 어떨까? 먼저 Wireshark의 필터를 수정한다:

          +
          ip.addr == 202.39.0.19 && ssl
          +
          +

          그리고 브라우저에서 https://www.ajou.ac.kr로 접속하면 프로토콜이 TLS인 패킷만 캡쳐되는 것을 볼 수 있다.

          +

          +

          먼저 접속 과정에서 HTTP와 다른 점을 찾을 수 있다. HTTPS로 접속할 때는 TCP Three-way handshake와 별도로 TSL handshake 과정을 거친다.

          +

          +

          IBM의 An overview of the SSL or TLS handshake에 따르면 TLS handshake 과정에서는 서버와 클라이언트 인증서를 검증하고, 클라이언트 키를 교환한다. 앞서 캡처된 패킷에서 보듯, 이 과정에서 클라이언트와 서버는 Client Hello, Server Hello, Certificate, Server Hello Done 메시지를 주고받는다. 이 과정을 거친 후에야 클라이언트와 서버는 본격적으로 데이터를 교환한다.

          +

          Wireshark에서 TLS handshake 이후 주고받는 패킷 중 아무거나 열어봤다.

          +

          +

          클라이언트와 https://www.ajou.ac.kr가 주고받은 해당 패킷의 콘텐츠 타입이 Application Data라는 것을 제외하고는 정보를 알 수가 없다. HTTP로 접속할 때는 클라이언트가 요청한 리소스, 사용하는 브라우저, 유입 경로 등 패킷을 통해 각종 정보를 볼 수 있었지만, HTTPS로 접속할 때는 패킷이 암호화되어 내용을 알 수 없다.

          +

          만약 어떤 사람이 HTTP 서버에 접속했는데, 악의를 가진 해커가 그 사람의 패킷을 가로챈다면 그 사람이 어느 페이지에서 무엇을 했는지 모두 알아낼 수 있을 것이다.

          + +
          +
          + +
          + +
          +
          +

          🤖 컴퓨터가 코드를 읽는 아주 구체적인 원리

          +

          MIPS 어셈블리어 훑어보기

          +
          +
          +
          + + +
          + +
          +
          +

          🌐 Top-Down으로 접근하는 네트워크

          +

          Computer Networks and the Internet

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/25.html b/article/25.html new file mode 100644 index 0000000..277884d --- /dev/null +++ b/article/25.html @@ -0,0 +1,512 @@ + + + + + + 🤖 컴퓨터가 코드를 읽는 아주 구체적인 원리 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 🤖 컴퓨터가 코드를 읽는 아주 구체적인 원리 +

          + +

          + MIPS 어셈블리어 훑어보기 +

          + + + + +
          +
          Table of Contents +

          +
          +

          지난 학기 운영체제 공부를 하면서 더 낮은 레벨은 어떻게 동작하는지 궁금해졌다. David A. Patterson과 John L. Hennessy의 Computer Organization and Design 5th Edition의 전반부를 바탕으로 MIPS instruction set에 대해 정리했다.

          +

          Computer Abstractions and Technology

          +

          컴퓨터 아키텍처를 공부한다는 것은 컴퓨터를 구성하는 하드웨어와 명령어가 어떻게 함께 동작하는지 알아보는 것이다. 하드웨어를 다루기는 하지만 회로에 대한 부분을 자세히 다루지는 않는다. 회로는 로우레벨 아키텍처이고, 앞으로 다룰 내용은 회로(하드웨어)와 운영체제(소프트웨어) 사이에 있는 ISA(Instruction Set Architecture), microarchitecture와 같은 하이레벨 아키텍처다.

          +

          Instructions: Language of the Computer

          +

          ISA는 add, load와 같은 명령의 집합으로, 하드웨어와 소프트웨어 사이의 인터페이스를 정의한다. microarchitecture는 ISA의 구현체로, 프로세서와 입출력 subsystem의 조직이다. 파이프라인의 깊이나 캐시 사이즈가 microarchitecture라고 할 수 있다.

          +

          머신 설계에는 두 가지 중요한 원칙이 있다. 첫째는 컴퓨터는 모든 것을 bit로 이해하기 때문에 instruction과 데이터를 구분하지 못한다는 것이고, 둘째는 프로그램도 데이터와 똑같이 메모리에 저장된다는 것이다. 메모리를 여러 구획으로 구분한 것은 인간의 입장이지, 컴퓨터는 결국 바이너리로 받아들인다.

          +

          From a High-Level Language to the Language of Hardware

          +
          int main(int argc, char *argv[]) {
          +  int s0 = 0, s1 = 1, s2 = 2;
          +  s0 = s1 + s2;
          +  return 0;
          +}
          +
          +

          하이레벨 언어는 자연어와 가장 가까운 프로그래밍 언어다. 높은 생산성을 제공하며, C, C++, Java 등 흔히 사용되는 프로그래밍 언어들은 대부분 하이레벨 언어다. 하이레벨 언어는 컴파일러를 통해 어셈블리어로 변환된다.

          +
            .data
          +  .text
          +main:
          +  add $s0, $s1, $s2
          +
          +

          어셈블리어는 컴퓨터의 구체적인 동작을 텍스트로 표현한 것으로, instruction의 집합이라고 할 수 있다. 이 단계에서 하이레벨 코드는 명령줄 사이의 점프 수준까지 내려간다. 어셈블리어는 어셈블러에 의해 기계어로 변환된다.

          +
          00000000000000000000000000000100
          +00000000000000000000000000000000
          +00000010001100101000000000100000
          +
          +

          기계어는 컴퓨터가 이해할 수 있는 바이너리 숫자만으로 구성되어 있다. 컴퓨터는 메모리에 올라간 바이너리 코드를 CPU로 가져와 instruction을 수행한다.

          +

          Operations of the Computer Hardware

          +

          여러 종류의 ISA가 있다. 대표적으로 Intel과 AMD에서 만든 x86, AMD64가 있는데, 이들의 CPU 아키텍처는 CISC(Complex Instruction Set Computer) 구조이기 때문에 굉장히 복잡하다. 한편 RISC(Reduced Instruction Set Computer) 구조는 보다 간소하다. RISC 구조인 ARM 아키텍처는 스마트폰이나 태블릿과 같은 모바일 기기에 사용되고 있으며, 2020년에 출시된 M1 맥북도 ARM을 기반으로한 CPU를 사용한다. 우리가 사용할 MIPS(Microprocessor without Interlocked Pipeline Stages)도 RISC 구조 아키텍처다. MIPS는 명령어 세트가 깔끔해 컴퓨터 아키텍처를 공부하는 목적으로 적합하며, 실제로는 블루레이 기기나 플레이스테이션과 같은 디지털 홈, 네트워킹 장비에 사용되었다.

          +

          MIPS Instructions

          +

          앞서 하이레벨 언어가 어셈블리어로, 어셈블리어가 최종적으로 기계어로 변환된다는 것을 보았다. 하이레벨 언어는 알고 있다는 가정하에, 실제 MIPS에서 instruction이 어떻게 작성되는지, 그 instruction이 어떤 규칙에 따라 기계어로 변환되는 지 알아보자.

          +

          먼저 CPU가 매번 메인메모리에서 값을 읽어오는 것은 오버헤드가 큰 일이기 때문에 CPU는 레지스터라는 작고 빠른 메모리를 가지고 있다. 크기는 작지만 속도가 빨라서 레지스터에 데이터를 두면 instruction을 빠르게 수행할 수 있다. MIPS의 연산은 32x32bit 레지스터를 사용하며, 32bit 데이터를 word라고 부른다. 레지스터의 일부에는 미리 이름을 붙여 놓았는데, $t0부터 $t9까지는 임시 레지스터(temporary register)를 의미하며, $s0부터 $s7까지는 계속 사용되는 레지스터(saved register)를 의미한다.

          +

          Arithmetic Operations

          +

          산술 연산은 그리 복잡하지 않다.

          +
          a = (b + c) - (d + e);
          +
          +

          a, b, c, d, e가 각각 레지스터 $s0, $s1, $s2, $s3, $s4에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
          add $t0, $s1, $s2 # $t0 = $s1 + $s2
          +add $t1, $s3, $s4 # $t1 = $s3 + $s3
          +sub $s0, $t0, $t1 # $s0 = $t0 - $t1
          +
          +

          add는 값을 덧셈을, sub는 뺄셈을 수행한다.

          +

          Memory Operations

          +

          MIPS의 메모리 연산은 메모리에서 레지스터로, 레지스터에서 메모리로 데이터를 옮기는 일을 한다.

          +
          a = b + A[8]
          +
          +

          a, b은 각각 $s1, $s2에 대응되고, Abase address$s3에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
          lw $t0, 32($s3) # $t0 = $s3[8]
          +add $s1, $s2, $t0 # $s1 = $s2 + $t0
          +
          +

          lw는 메모리에서 레지스터로 값을 가져온다. 정수 배열의 원소들은 메모리상에 각자 1word(4bytes)씩 차지하며 저장된다. 즉, A[8]는 메모리 상에 다음과 같이 존재한다.

          +
          4byte   4byte     4byte          4byte
          +A[0],   A[1],     A[2],    ...,  A[8]
          +$s3,    4($s3),   8($s3),  ...,  32($s3)
          +
          +

          따라 32($s3)은 base address $s3에서 32bytes 떨어진 위치에 접근한다는 의미가 된다. 이때 앞에 붙는 숫자를 offset이라고 하며, base address를 가리키는 레지스터(위 경우 $s3)는 base register라고 한다.

          +
          A[12] = a + A[8]
          +
          +

          a$s2, A의 base address가 $s3에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
          lw $t0, 32($s3) # $t0 = $s3[8]
          +add $t0, $s2, $t0 # $t0 = $s2 + $t0
          +sw $t0, 48($s3) # A[12] = $t0
          +
          +

          sw는 레지스터에서 메모리로 데이터를 옮긴다. 위에서는 lw로 메모리에서 32($s3) 값을 가져와 레지스터의 $t0에 저장했고, 아래에서는 $t0의 값을 메모리의 48($s3) 위치에 저장했다.

          +

          Immediate Instructions

          +

          상수를 더할 때는 addi를 사용한다.

          +
          addi $s3, $s3, 4 # $s3 = $s3 + 4
          +addi $s2, $s2, -1 # $s2 = $s2 + (-1)
          +
          +

          상수에 대한 뺄셈 연산은 따로 없기 때문에 음수를 더해주면 된다.

          +

          Constant

          +

          MIPS 레지스터 $zero는 상수 0을 의미한다.

          +
          add $t0, $s1, $zero # $t0 = $s1 + 0
          +
          +

          위 처럼 다른 레지스터로 값을 그대로 대입할 때 사용할 수 있다.

          +

          Conditional Statement

          +
          if (i == j) {
          +  a = b + c;
          +} else {
          +  a = b - c;
          +}
          +
          +

          a, b, c, i, j가 각각 $s0부터 $s4에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
            bne $s3, $s4, Else # if ($s3 == $s4)
          +  add $s0, $s1, $s2 # { $s0 = $s1 + $s2 }
          +  j Exit
          +Else:
          +  sub $s0, $s1, $s2 # else { $s0 = $s1 - $s2 }
          +Exit:
          +  ...
          +
          +

          bne는 두 레지스터 값이 같은지 비교해 같다면 다음 구문을, 다르다면 지정된 라벨로 점프한다. 위에서는 bne를 통해 $s3$s4가 같은지 비교한 뒤, 다르다면 Else 라벨로 점프해 sub $s0, $s1, $s2를 실행하도록 했다. 라벨 이름은 개발자가 임의로 정할 수 있다. 꼭 ElseExit라는 이름일 필요는 없다는 것이다. 또한 라벨의 위치는 어셈블러가 계산한다.

          +

          Loop

          +
          while (save[i] == k) {
          +  i += 1;
          +}
          +
          +

          i, k가 각각 $s3, $s5에, save의 base address가 $s6에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
          Loop:
          +  sll $t1, $s3, 2 # $t1 = $s3 << 2
          +  add $t1, $t1, $s6 # $t1 = $t1($s6)
          +  lw $t0, 0($t1) # $t0 = 0($t1)
          +  bne $t0, $s5, Exit # if ($t0 != $s5) { goto Exit }
          +  addi $s3, $s3, 1 # $s3 += 1
          +  j Loop
          +Exit:
          +  ...
          +
          +

          상당히 복잡한데, 크게 두 부분으로 나눌 수 있다. bne부터 j 라인까지는 루프의 조건을 확인하고 i 값을 증가시키는 부분이다. 그 위의 sll부터 lw 라인까지는 레지스터에 save[i] 값을 가져오는 부분이다.

          +

          sll 자체는 control flow에 중요한 instruction이 아니고, 단순히 값을 left shift하는 기능을 한다. $s3의 값을 2만큼 left shift하면 4를 곱하는 것과 같다. 이는 $s3 값이 1증가할 때마다 4를 곱함으로써 $s6에 접근하는 주소값을 4bytes씩 옮기기 위한 코드다. right shift를 하기 위해서는 srl을 사용한다.

          +

          MIPS Procedure

          +

          procedure는 함수를 의미한다. 어셈블리 레벨에서 함수를 만들고 호출하는 것은 하이레벨 언어에서 하던 것과는 많이 다르다. 여기서 가장 중요한 것은 레지스터의 백업과 점프다.

          +

          Leaf Procedure

          +

          호출된 함수에서 다른 함수를 호출하지 않는 함수, 즉 leaf procedure를 가정해보자:

          +
          int leaf_procedure(int a, b, c, d) {
          +  int e;
          +  e = (a + b) - (c -d);
          +  return e;
          +}
          +
          +

          a부터 d까지가 $a0부터 $a3에 대응되고, e$s0에 대응된다고 하면 MIPS 코드는 다음과 같다:

          +
          main:
          +  jal leaf_procedure # leaf_procedure 호출
          +  j exit
          +leaf_procedure:
          +  addi $sp, $sp, -12 # 4bytes 레지스터 3개 백업을 위해 stack pointer 위치 -12 이동
          +  sw $t1, 8($sp) # 8($sp) = $t1
          +  sw $t0, 4($sp) # 4($sp) = $t0
          +  sw $s0, 0($sp) # 0($sp) = $s0
          +  add $t0, $a0, $a1
          +  add $t1, $a2, $a3
          +  sub $s0, $t0, $t1
          +  add $v0, $s0, $zero
          +  lw $s0, 0($sp) # $s0 = 0($sp)
          +  lw $t0, 4($sp) # $t0 = 4($sp)
          +  lw $t1, 8($sp) # $t1 = 8($sp)
          +  addi $sp, $sp, 12 # stack pointer 위치 복원
          +  jr $ra # $ra 위치로 점프
          +exit:
          +  ...
          +
          +

          …왜 이런 짓을 하는걸까? 하나씩 살펴보자.

          +
          main:
          +  jal leaf_procedure # leaf_procedure 호출
          +  j exit
          +
          +

          먼저 jal을 통해 leaf_procedure 라벨로 이동한다. 이때, 레지스터의 $ra(return address)는 program counter의 값을 가져온다. program counter는 프로세스가 자신의 instruction을 어디까지 실행했는지 체크하기 위한 값이다. 이 경우 $rajal leaf_procedure 바로 다음 라인 j exit를 가리킨다.

          +
          leaf_procedure:
          +  addi $sp, $sp, -12 # 4bytes 레지스터 3개 백업을 위해 stack pointer 위치 -12 이동
          +
          +

          leaf_proceduer에서는 먼저 레지스터의 $sp(stack pointer)에 4bytes 레지스터 3개를 백업하기 위해 $sp-12를 더했다. $sp는 메모리의 스택의 특정 주소를 가리키는 레지스터이며, 이 위치를 옮기는 것은 백업을 위한 공간을 확보하는 것이다.

          +
          sw $t1, 8($sp) # 8($sp) = $t1
          +sw $t0, 4($sp) # 4($sp) = $t0
          +sw $s0, 0($sp) # 0($sp) = $s0
          +
          +

          그리고 sw를 통해 $t1, $t0, $s0$sp의 각 공간에 담았다. (큰 값부터 접근하는 이유는 메모리의 스택 주소가 큰 쪽에서 작은 쪽으로 향하기 때문이다.) 이렇게 하는 이유는 leaf_procedure를 호출한 caller측(이 경우 main)에서 $t1이나 $t0, $s0 레지스터를 사용하고 있을 수도 있기 때문이다. 만약 스택에 백업하지 않는다면 caller에서 사용하던 값을 덮어씌워 버릴 것이다.

          +
          add $t0, $a0, $a1
          +add $t1, $a2, $a3
          +sub $s0, $t0, $t1
          +add $v0, $s0, $zero
          +
          +

          e = (a + b) - (c -d)에 해당하는 연산을 수행한다.

          +
          lw $s0, 0($sp) # $s0 = 0($sp)
          +lw $t0, 4($sp) # $t0 = 4($sp)
          +lw $t1, 8($sp) # $t1 = 8($sp)
          +
          +

          lw를 통해 $sp에 저장한 값을 다시 불러왔다. stack pointer도 다시 12bytes 당겼다.

          +
          jr $ra # $ra 위치로 점프
          +
          +

          jr을 통해 caller에 위치한 $ra로 돌아가 j exit를 실행한다.

          +

          여기서는 $t1$t0도 백업을 했는데, 사실 이럴 필요는 없다. $t 레지스터는 temporary register이기 때문에 언제나 임시 값만 저장하도록 약속되어있다. 따라서 $t 레지스터에는 값이 덮어씌워져도 문제가 없도록 코드를 짜야한다.

          +

          Non-Leaf Procedure

          +

          함수 안에서 다른 함수를 호출하는 non-leaf procedure는 어떨까? 이렇게 호출이 중첩된 경우에는 $ra에 저장된 callr의 위치가 덮어 씌워져 무한 루프에 빠질 위험이 있다. 일단 하이레벨 코드를 보자:

          +
          int factorial(int n) {
          +  if (n < 1) {
          +    return 1;
          +  } else {
          +    return n * factorial(n - 1);
          +  }
          +}
          +
          +

          recursive하게 동작하는 팩토리얼 함수다. n$a0에, 결과값이 $v0에 대응된다고 가정하면 MIPS 코드는 다음과 같다:

          +
          factorial:
          +  addi $sp, $sp, -8 # stack pointer 위치 -8 이동
          +  sw $ra, 4($sp)
          +  sw $a0, 0($sp)
          +  slti $t0, $a0, 1 # $t0 = ($a0 < 1)
          +  beq $t0, $zero, L1 # if ($t0 == 0) { goto L1 }
          +  addi $v0, $zero, 1
          +  addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +  jr $ra
          +L1:
          +  addi $a0, $a0, -1 # $a -= 1
          +  jal factorial # factorial($a0)
          +  lw $a0, 0($sp)
          +  lw $ra, 4($sp)
          +  addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +  mul $v0, $a0, $v0 # $v0 = $a0 * $v0
          +  jr $ra
          +
          +

          굉장히 복잡해보이지만, 하나씩 뜯어보면… 진짜 복잡하다. 작동 방식은 대략 재귀적으로 함수를 호출하면서 stack pointer를 밀어내며 값을 백업하다가, 이후 다시 stack pointer를 당기며 값을 가져와 결과를 내는 식이다. $a0가 3이라고 가정하고 모든 instruction 단계를 step by step으로 한 단계 한 단계 살펴보자.

          +
          factorial:
          +  addi $sp, $sp, -8 # stack pointer 위치 -8 이동
          +  sw $ra, 4($sp)
          +  sw $a0, 0($sp)
          +
          +

          먼저 factorial 함수에 진입해 stack pointer를 이동시키고 $ra$a0 레지스터를 백업했다. 이때 $rafactorial 함수를 호출한 위치가 될 것이고, $a0는 앞서 가정한 3이 된다. 스택의 모습은 다음과 같다:

          +
          +--------+------------+
          +| $a = 3 | caller $ra |
          ++--------+------------+
          +
          +

          이제 다음 라인을 보자:

          +
          slti $t0, $a0, 1 # $t0 = ($a0 < 1)
          +beq $t0, $zero, L1 # if ($t0 == 0) { goto L1 }
          +
          +

          $a0가 1보다 작은지 확인하고, 그 반환 값을 $t0에 저장한다. 그리고 $t0$zero와 같은지 확인해 값이 같다면 L1 라벨로 이동한다. 이때 $a0는 3이기 때문에 1보다 작지 않다. 따라서 $t0는 0이 되고, beq를 만족해 L1으로 넘어간다.

          +
          L1:
          +  addi $a0, $a0, -1 # $a -= 1
          +  jal factorial # factorial($a0)
          +
          +

          L1은 하이레벨 코드에서 return n * factorial(n - 1);에 해당하는 동작을 정의한다. 먼저 $a0에서 1을 뺀다. 그리고 다시 factorial 함수를 호출한다.

          +
          factorial:
          +  addi $sp, $sp, -8 # stack pointer 위치 -8 이동
          +  sw $ra, 4($sp)
          +  sw $a0, 0($sp)
          +
          +

          factorial$sp를 다시 -8 이동한다. 그리고 $ra$a0를 백업한다. 이때 $ra는 앞서 실행한 L1jal factorial의 바로 다음 라인이고, $a0는 2가 된다. 현재 스택에 저장된 레지스터 값을 표현하면 다음과 같다:

          +
          +--------+------------+--------+--------+
          +| $a = 3 | caller $ra | $a = 2 | L1 $ra |
          ++--------+------------+--------+--------+
          +
          +

          이어서 다음 라인을 살펴보자:

          +
          slti $t0, $a0, 1 # $t0 = ($a0 < 1)
          +beq $t0, $zero, L1 # if ($t0 == 0) { goto L1 }
          +
          +

          $a0가 2이므로 1보다 작지 않다. $t0가 0이 되고, beq를 만족해 다시 L1으로 넘어간다.

          +
          L1:
          +  addi $a0, $a0, -1 # $a -= 1
          +  jal factorial # factorial($a0)
          +
          +

          L1은 아까와 마찬가지로 $a에서 1을 빼 1로 만들고, 다시 factorial로 넘어간다.

          +
          factorial:
          +  addi $sp, $sp, -8 # stack pointer 위치 -8 이동
          +  sw $ra, 4($sp)
          +  sw $a0, 0($sp)
          +
          +

          factorial에서는 $sp를 다시 -8 이동한다. 이때 백업되는 $ra는 앞서 실행한 L1jal factorial의 바로 다음 라인이고, $a0는 1이다. 현재 스택에 저장된 레지스터 값은 다음과 같다:

          +
          +--------+------------+--------+--------+--------+--------+
          +| $a = 3 | caller $ra | $a = 2 | L1 $ra | $a = 1 | L1 $ra |
          ++--------+------------+--------+--------+--------+--------+
          +
          +

          다음 라인으로 넘어가자:

          +
          slti $t0, $a0, 1 # $t0 = ($a0 < 1)
          +beq $t0, $zero, L1 # if ($t0 == 0) { goto L1 }
          +
          +

          여전히 $a0가 1이므로 1보다 작지 않고, beq를 만족해 L1으로 넘어간다.

          +
          L1:
          +  addi $a0, $a0, -1 # $a -= 1
          +  jal factorial # factorial($a0)
          +
          +

          L1$a에서 1을 빼 0으로 만들고, factorial로 넘어간다.

          +
          factorial:
          +  addi $sp, $sp, -8 # stack pointer 위치 -8 이동
          +  sw $ra, 4($sp)
          +  sw $a0, 0($sp)
          +
          +

          factorial에서는 또 다시 $sp를 -8 이동한다. 이때 백업되는 $ra는 앞서 실행한 L1jal factorial의 바로 다음 라인이고, $a0는 0이다. 현재 스택에 저장된 레지스터 값은 다음과 같다:

          +
          +--------+------------+--------+--------+--------+--------+--------+--------+
          +| $a = 3 | caller $ra | $a = 2 | L1 $ra | $a = 1 | L1 $ra | $a = 0 | L1 $ra |
          ++--------+------------+--------+--------+--------+--------+--------+--------+
          +
          +

          다음 라인을 보자:

          +
          slti $t0, $a0, 1 # $t0 = ($a0 < 1)
          +beq $t0, $zero, L1 # if ($t0 == 0) { goto L1 }
          +
          +

          이젠 $a0가 0이므로 1보다 작아 $t0가 1이 된다. beq를 만족하지 않으므로 L1으로 넘어가지 않고 다음 라인을 실행한다. 여기까지 호출 과정이었고, 이제는 반환 값을 받아오는 과정을 반복한다.

          +
          addi $v0, $zero, 1
          +addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +jr $ra
          +
          +

          $v0에 0에 1을 더한 값, 즉 1을 저장한다. 그리고 stack pointer 위치를 8만큼 당기고 $ra 위치로 넘어간다. 이때 $raL1 $ra 위치다.

          +
          +--------+------------+--------+--------+--------+--------+
          +| $a = 3 | caller $ra | $a = 2 | L1 $ra | $a = 1 | L1 $ra |
          ++--------+------------+--------+--------+--------+--------+
          +
          +

          stack pointer 위치를 8만큼 당겼으니 스택의 모습은 위와 같이 된다.

          +
          lw $a0, 0($sp)
          +lw $ra, 4($sp)
          +addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +mul $v0, $a0, $v0 # $v0 = $a0 * $v0
          +jr $ra
          +
          +

          스택에서 $a0 값과 $ra 값을 가져온다. 앞선 스택의 모습을 확인해보면 $a0가 1이고 $raL1 $ra 위치다. 그리고 다시 stack pointer의 위치를 8 움직인다. 이어서 $v0$a0 * $v0, 즉 1 * 1 값을 저장하고, $ra 위치로 넘어간다. $ra는 여전히 L1 $ra 위치다.

          +
          +--------+------------+--------+--------+
          +| $a = 3 | caller $ra | $a = 2 | L1 $ra |
          ++--------+------------+--------+--------+
          +
          +

          앞서 stack pointer의 위치를 8 움직였기 때문에 현재 스택의 모습은 위와 같다.

          +
          lw $a0, 0($sp)
          +lw $ra, 4($sp)
          +addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +mul $v0, $a0, $v0 # $v0 = $a0 * $v0
          +jr $ra
          +
          +

          이 과정을 반복한다. $a0$ra를 복원한다. 이제 $a0가 2이고 $raL1 $ra 위치다. 그리고 다시 stack pointer의 위치를 8 움직인다. 이어서 $v0$a0 * $v0, 즉 2 * 1 값을 저장하고, $ra 위치로 넘어간다. $raL1 $ra 위치다.

          +
          +--------+------------+
          +| $a = 3 | caller $ra |
          ++--------+------------+
          +
          +

          stack pointer가 8만큼 이동한 현재 스택의 모습은 위와 같다.

          +
          lw $a0, 0($sp)
          +lw $ra, 4($sp)
          +addi $sp, $sp, 8 # stack pointer 위치 8 이동
          +mul $v0, $a0, $v0 # $v0 = $a0 * $v0
          +jr $ra
          +
          +

          이제 마지막 단계다. 스택에서 $a0$ra를 가져온다. 이제 $a0가 3이고 $racaller $ra 위치다. 다시 stack pointer의 위치를 8 움직이면 이제 stack pointer를 초기 값으로 돌려놓게 된다.

          +

          $v0$a0 * $v0, 즉 3 * 2 값을 저장하고, $ra 위치로 넘어간다. 이때 $ra는 caller 위치이며, caller에서 $v0에 접근하면 6을 얻을 수 있을 것이다. factorial이 제대로 동작했다.

          +

          마치 인간 컴퓨터가 된 기분이다.

          +

          Memory Layout

          +
          +---------------+ High address
          +|     Stack     |
          ++-------+-------+
          +|       |       |
          +|       v       |
          +|               |
          +|       ^       |
          +|       |       |
          ++-------+-------+
          +|     Heap      |
          ++---------------+
          +| Static (Data) |
          ++---------------+
          +|  Text (Code)  |
          ++---------------+ Low address
          +
          +

          그림의 Code Segment는 Text라고 불리며, 실제 프로그램 코드가 저장된다. Data Segment는 Static Data라고 불리며, 상수 배열이나 문자열, static 변수가 저장된다. Heap에는 동적할당된 배열 요소나 객체가 저장되며, Stack에는 함수나 메소드, 지역변수 등이 저장된다.

          +

          앞서 스택에 값을 저장하고 불러오는 과정이 위와 같은 메모리 구조를 바탕으로 이뤄진다. 스택은 높은 곳에서 낮은 곳으로 자라기 때문에 addi $sp, $sp, -8처럼 stack poniter를 음의 방향으로 움직여준 것이다. 값을 저장할 때도 sw $ra 4($sp)다음 sw $a0, 0($sp)를 했다. 스택과 달리 heap은 낮은 곳에서 높은 곳으로 자란다.

          +

          MIPS Instruction Formats

          +

          MIPS에서 어셈블러가 한 줄씩 instruction을 읽고 이를 기계어로 변환할 때, 기계어를 표현하는 세가지 형식 R(Register), I(Immediate), J(Jump)가 있다.

          +

          R-format

          +

          R-format은 레지스터를 이용해 연산하는 instruction을 담는 형식이다.

          +
          +----+----+----+----+-------+-------+
          +| op | rs | rt | rd | shamt | funct |
          ++----+----+----+----+-------+-------+
          +
          +
            +
          • op(6bits): 포맷과 동작을 구분하는 필드.
          • +
          • rs(5bits): first source register number
          • +
          • rt(5bits): second source register number
          • +
          • rd(5bits): destination register number
          • +
          • shamt(5bits): shift 연산에 사용되는 필드.
          • +
          • funct(6bits): op보다 구체적인 정보를 담은 필드.
          • +
          +

          만약 다음과 같은 MIPS 코드가 있다고 가정해보자:

          +
          add $t0, $s1, $s2
          +
          +

          이를 R-format으로 표현하면 이렇게 될 것이다:

          +
          +---------+-----+-----+-----+---+-----+
          +| special | $s1 | $s2 | $t0 | 0 | add |
          ++---------+-----+-----+-----+---+-----+
          +
          +

          register table에 따라 이것을 10진수로 변환하면:

          +
          +---+----+----+---+---+----+
          +| 0 | 17 | 18 | 8 | 0 | 32 |
          ++---+----+----+---+---+----+
          +
          +

          각 레지스터의 값과 add instruction의 opcode, funct 값, rs, rt, rd 위치 등은 모두 사전에 정의된 것으로, MIPS Green Sheet를 참고하면 된다. 이어서 각 필드를 2진수로 바꾸면 실제 메모리에 들어가는 값이 된다:

          +
          00000010001100100100000000100000
          +
          +

          컴퓨터는 이렇게 변환된 바이너리 숫자를 보고 명령을 수행한다.

          +

          이렇게 보면 바로 funct를 확인하면 되니까 op가 필요하지 않을 것 같다. 사실 32bit 시스템에서 쉽게 instruction을 읽기 위해 32비트를 맞추는 것이 복잡도를 줄이는 데 도움이 되기 때문에 메모리 손해를 감안한 것이다.

          +

          I-format

          +

          I-format은 상수 연산과 메모리 연산을 위해 사용된다.

          +
          +----+----+----+-----+
          +| op | rs | rt | IMM |
          ++----+----+----+-----+
          +
          +
            +
          • op(6bits): 포맷과 동작을 구분하는 필드.
          • +
          • rs(5bits), rt(5bits): source or destination register number
          • +
          • IMM(16bits): constant나 address가 담긴다. constant의 경우 -215부터 215 - 1까지의 상수를 저장할 수 있다. address의 경우 rs의 base address에 offset으로 기능하거나 점프해야 하는 instruction까지의 거리를 저장한다.
          • +
          +

          J-format

          +

          J-format은 다른 위치로 점프할 때 사용되는 포맷이다.

          +
          +----+----------------+
          +| op | pseudo-address |
          ++----+----------------+
          +
          +
            +
          • op(6bits): 포맷과 동작을 구분하는 필드.
          • +
          • pseudo-address(26bits): 점프할 instruction의 변환된 주소가 담기는 필드.
          • +
          +

          MIPS Addressing for 32-bit Immediates and Addresses

          +

          MIPS instruction format 중 I-format은 bne $t0, $s5, Exit와 같은 구문을 표현할 때도 사용된다. 그런데 I-foramt의 마지막 필드인 constant or address는 16bits밖에 되지 않아 Exit에서 수행될 동작 전체를 담는 것은 불가능하다. 따라서 이곳에는 Exit가 몇 줄 떨어져 있는지를 저장한다. 앞서 활용한 Loop 코드를 다시 보자:

          +
          Loop:                 # address: 8000
          +  sll $t1, $s3, 2     # R-format: | 0  | 0  | 19 | 9 | 4 | 0  |
          +  add $t1, $t1, $s6   # R-format: | 0  | 9  | 22 | 9 | 0 | 32 |
          +  lw $t0, 0($t1)      # I-format: | 35 | 9  | 8  |     0      |
          +  bne $t0, $s5, Exit  # I-format: | 5  | 8  | 21 |     2      |
          +  addi $s3, $s3, 1    # I-format: | 8  | 19 | 19 |     1      |
          +  j Loop              # J-format: | 2  |         2000         |
          +Exit:
          +  ...
          +
          +

          여기서 bne $t0, $s5, Exit 라인의 I-format을 보자. constant or address에 2가 있는 것을 볼 수 있는데, 이는 Exit가 해당 라인에서 2칸 떨어져 있다는 의미다.

          +

          J-format의 경우도 pseudo-address 필드가 26bits밖에 되지 않아 모든 instruction을 담을 수 없다. J-format의 pseudo-address 필드에는 점프해야 하는 위치의 주소를 2만큼 right shift한 값이 들어간다. 위 코드에서 j Loop 라인의 J-format을 보자. pseudo-address 필드에 2000이 있는데, 점프해야 하는 Loop의 address는 8000이다. 이때 2000에 2만큼 left shift(2000 << 2)하면 8000을 얻을 수 있다.

          +

          위와 같은 매커니즘을 수행하기 위해 어셈블러는 구체적인 instruction을 기계어로 변환하기 전 코드 전체를 스캔해 라벨의 위치를 파악한다. 이후 순서대로 instruction을 읽으며 기계어로 변환하다가 점프해야 하는 부분이 나오면 그 부분의 instruction을 기계어로 변환한다. 이런 식으로 모든 instruction을 순서에 맞춰 기계어로 변환하면 비로소 컴퓨터가 이해할 수 있는 결과물이 나온다.

          +

          여기까지가 MIPS instruction에 대한 기본적인 내용이다. 더 들어가면 프로세서, 메모리의 동작이나 디지털 회로를 이용한 바이너리 연산에 대한 내용도 다룰 수 있을 것 같은데, 일단은 컴퓨터 아키텍처의 개요와 컴퓨터가 코드를 해석하고 실행하는 과정만을 살펴봤다.

          + +
          +
          + +
          + +
          +
          +

          🎅 요정을 착취하는 방치형 게임 개발한 이야기

          +

          ES6 OOP와 타입스크립트, 그리고 제이쿼리(?)

          +
          +
          +
          + + +
          + +
          +
          +

          🔐 HTTPS는 어떻게 다를까?

          +

          진짜 데이터를 뜯어보았다

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/26.html b/article/26.html new file mode 100644 index 0000000..3050109 --- /dev/null +++ b/article/26.html @@ -0,0 +1,331 @@ + + + + + + 🎅 요정을 착취하는 방치형 게임 개발한 이야기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 🎅 요정을 착취하는 방치형 게임 개발한 이야기 +

          + +

          + ES6 OOP와 타입스크립트, 그리고 제이쿼리(?) +

          + + + + +
          +
          Table of Contents +

          +
          +

          Santa Inc.는 초국적 블랙 기업 '산타 주식회사’를 운영하는 방치형 게임(Idle game)이다. 산타의 목적은 오직 선물 생산을 최대로 끌어올리는 것. 산타는 직원을 고용하고, 정책을 채택하며 끝없이 선물을 생산한다.

          +

          +

          Santa Inc.를 처음 만든 건 2015년 겨울이었다. 웹 프로그래밍을 공부한 후 매년 크리스마스마다 이상한 걸 만들었는데, 이것도 그 일환이었다. 당시엔 크리스마스에 맞춰 런칭하겠다는 욕심으로 학기 중 20일만에 게임을 완성했다. 물론 코드가 엉망진창이었다.

          +

          그리고 2018년, 엉망이던 코드를 ES6 클래스 문법과 타입스크립트를 이용해 (아마도) 깔끔하게 정리했다.

          +

          😈 산타가 되어 루돌프와 요정을 착취해보세요!

          +

          +처음 아이디어는 '산타가 요정을 고용해 회사를 운영하는 게임’이었다. 어렸을 때 줄기차게 한 롤러코스터 타이쿤이나 로코모션급의 시뮬레이션 게임…이 이상적이긴 하지만 당장 만드는 건 무리였다. 대신 Cookie Clicker라는 게임을 재밌게 한 기억이 났고, 이것처럼 만들어봐야겠다고 결심했다.

          +

          크리스마스까지 시간이 많지 않았기 때문에 기획, 디자인, 개발을 모두 동시에 했다. (그러지 말았어야 했다! 개발하다가 갑자기 생각이 바뀌어 코드를 완전히 새로 짜거나 새로운 그림을 그려야 하는 상황이 여러번 발생했다.) 프로젝트 중간에 친구가 게임 OST까지 작곡해줬는데, 게임 분위기와 놀랍도록 잘 맞아 떨어져서 신의 한 수였던 것 같다.

          +

          확장 불가능한 아이템 코드

          +

          플레이어는 루돌프나 요정을 고용해서 선물을 생산하도록 할 수 있다. 게임 특성상 '구입할 수 있는 아이템’과 '아직 구입할 수 없는 아이템’이 나뉘어 있고, 특정 조건을 만족하면 아이템의 잠금이 풀리는 매커니즘이 필요했다. 따라서 아이템 리스트를 먼저 만들었다.

          +
          var htItem = [{
          +    k_name : "루돌프",
          +    name : "dolf",
          +    img : "img/dolf.gif",
          +    num : 0,
          +    prod : 2,
          +    addi : 1,
          +    cost : 30,
          +    lv : false
          +}, {
          +    k_name : "인턴 요정",
          +    name : "elf",
          +    img : "img/elf.gif",
          +    num : 0,
          +    prod : 5,
          +    addi : 2,
          +    cost : 200,
          +    lv : false
          +}, {
          +    // ...
          +
          +

          이때는 왠지 몰라도 배열에 객체담는 걸 그렇게 좋아했다. 쉽게 파악하기 힘든 프로퍼티는 둘째치고, 가장 큰 문제는 아이템을 오직 인덱스로 관리했다는 점이다. 플레이어가 가진 선물에 따라 n번째 아이템을 구입할 수 있도록 열고, 아이템 리스트에서 n번째 요소를 클릭하면 htItem[n] 아이템이 구입되는 방식이었다.

          +

          어떤 아이템이 몇 번째 순서인지 기억해야 했고, htItem 중간에 새로운 아이템을 끼워 넣을 수 없었다. 이 때문에 '루돌프’와 ‘인턴 요정’ 사이에 들어갈 '알바요정’의 그래픽 작업을 마쳤음에도 끝내 추가하지 못했다.

          +

          스파게티 주도 개발

          +

          아이템 관련 코드도 심각했지만, 정책 부분은 더 심각했다. 플레이어가 정책 리스트에서 정책을 구입하면 그 정책이 가진 효과를 실행해야 하는데, 그 부분을 이렇게 처리했다:

          +
          // 정책 선택에 따른 분기를 설정, 처리한다.
          +function updatePolicy(sPolicy) {
          +    if(sPolicy == "practice") { // 리본묶기
          +        nClickPres = 1.2;
          +        renderPolicyList(1);
          +    } else if(sPolicy == "smart-work") { // 스마트 업무 환경
          +        nClickPres = 1.5;
          +        renderPolicyList(2);
          +    } else if(sPolicy == "night") { // 야근문화
          +        htItem[0]["prod"] += 1;
          +        $("#dolf .pr").text(htItem[0]["addi"] + " ~ " + htItem[0]["prod"] + "개 생산");
          +        renderPolicyList(3); // 열정페이 오픈
          +        renderPolicyList(4); // 멀티태스킹 오픈
          +    } else if(sPolicy == "pashion") { // 열정페이
          +        // ...
          +
          +

          지옥이 있다면 이런 모습일까? 정책 리스트도 아이템처럼 객체 배열로 구성했지만, 정책마다 실행할 코드가 다르다는 점과 한 정책이 다음 정책을 여러 개 열 수 있다는 점이 아이템과는 달랐다. 가령 ‘리본묶기’ 정책을 구입하면 선물 상자를 클릭할 때 들어오는 선물이 1.2개가 되고, 다음 정책인 '스마트 업무 환경’이 열린다. 반면 '야근문화’를 구입하면 루돌프(htItem[0]이 루돌프를 의미하는 게 포인트.)의 최대 생산량이 1오르고, '열정페이’와 '멀티태스킹’이 열린다.

          +

          중간에 새로운 정책을 끼워 넣을 수 없을 뿐만 아니라, 정책이 열리는 순서와 각 정책의 인덱스, 효과, 이름을 모두 숙지해야 했다. 솔직히 이 부분에서 때려칠까 고민했다.

          +

          그리고…

          +

          크리스마스 직전, 친구들에게 무작위로 링크를 뿌려 베타테스트를 했다. 아이템이나 정책 추가는 불가능했고, 버그 수정과 밸런싱에 신경썼다. 중년기사 김봉식이라는 방치형 게임을 참고해 밸런스를 맞췄지만, 막상 테스트를 해보니 손볼 곳이 많았다.

          +

          이런 식의 스파게티 코드들을 santa.js에 때려 박았다. 어찌저치 런칭하기 했지만 당연히 유지보수가 불가능에 가까웠고, 게임 후반부에 게임이 멈춰버린다는 리포트도 많았다. 루돌프와 요정을 착취하려 했으나 결과적으로 내가 나를 착취한 상황이 됐다. 이후로 업데이트는 커녕 소스파일을 다시 열어보는 일도 없…을 줄 알았으나…

          +

          🌏 세계를 선도하는 글로벌 기업 (주)산타

          +

          2018년초 깃랩 프로젝트 몇 개를 깃허브로 옮기는 작업을 했다. 이때 Santa Inc. 코드를 다시 열어봤는데, 정말 '내 인생에 이딴 코드를 남겨두는 건 부끄러운 일이다’라는 생각이 들어 바로 뜯어 고치기 시작했다. 이때가 1월이었으니 크리스마스까지 시간은 충분했다.

          +

          모든 것을 모듈화하자

          +

          게임에 등장하는 주요 요소는 크게 두 가지, 직원과 정책이다. 먼저 Worker 클래스를 추상 클래스로 활용하면서 이를 RudolphInternElf와 같은 각각의 직원 클래스에 상속했다. ES6 덕분에 클래스 문법에 따라 개발할 수 있었다.

          +
          class Worker {
          +  static cost = 0;
          +  static minOutput = 0;
          +  static maxOutput = 0;
          +
          +  constructor() {
          +    this.output = 0;
          +    this.level = 1;
          +    // ...
          +    this.img = '';
          +  }
          +
          +  static getMinOutput() {
          +    return this.minOutput;
          +  }
          +
          +  static getMaxOutput() {
          +    return this.maxOutput;
          +  }
          +
          +  // ...
          +
          +  next() {
          +    throw new Error('next() must be implemented.');
          +  }
          +}
          +
          +export default Worker;
          +
          +

          코드가 너무 길어 많은 부분이 생략되었지만, 대략 이런 모습이다. 다른 OOP 언어와 비슷하게 클래스 문법을 사용할 수 있다. 다만 추상 메소드라는 개념이 없어서 next 메소드는 서브클래스에서 구현되어야 한다는 에러를 던지도록 했다.

          +

          그리고 각 직원 클래스들이 Worker를 상속받는다.

          +
          class Rudolph extends Worker {
          +  static cost = 1;
          +  static minOutput = 1;
          +  static maxOutput = 2;
          +
          +  constructor() {
          +    super();
          +
          +    this.name = 'rudolph';
          +    this.korName = '루돌프';
          +
          +    this.img = '/assets/rudolph.gif';
          +  }
          +
          +  next() {
          +    return new ParttimeElf();
          +  }
          +}
          +
          +export default Rudolph;
          +
          +

          Rudolph 인스턴스를 통해 next를 호출하면 다음 단계 직원인 알바요정(ParttimeElf)을 반환받는다. 이렇게 모든 종류의 직원을 각각의 클래스로 나눠서 관리하니 훨씬 편하게 개발할 수 있었다. 정책도 같은 방식으로 구현했다.

          +

          ‘감독관 배치’ 정책은 모든 직원의 생산량을 1늘려준다. 이 동작도 다른 모듈은 전혀 건드리지 않고 감독관 배치 클래스에서 아주 간단하게 구현할 수 있다.

          +
          class Supervisor extends Policy {
          +  // ...
          +  static execute() {
          +    const workers = Game.getHiredWorkers();
          +
          +    workers.forEach((worker) => {
          +      worker.setOutput(worker.getOutput() + 1);
          +      PersonnelInterface.updateOutput(worker.getName(), worker.getOutput());
          +      Game.addTotalOutput(1);
          +    });
          +  }
          +}
          +
          +

          Supervisor.execute()와 같이 호출되면 고용된 직원 객체로 구성된 배열을 가져와 각 직원의 setOutput 메소드를 호출해 생산량을 1씩 증가시킨다. 그리고 스크린의 사이드바에 있는 '인사 탭’을 업데이트하기 위해 PersonnelInterfaceupdateOutput 메소드를 사용했다.

          +

          이로써 직원과 정책, 인터페이스가 완전히 독립적으로 동작하며 public 메소드를 통해 최소한의 관계를 유지할 수 있게 되었다. (자바스크립트에선 모든 프로퍼티가 public하지만…) 예전 방식대로라면 if문 지옥안에서 이중루프를 돌리는 짓을 했을지도 모른다.

          +

          타입스크립트로 마이그레이션

          +

          아무리 ES6라도 자바 프로그래밍에서의 개발 경험에 비하면 불편한 점이 많았다. abstract나 interface같은 OOP 문법이 없다는 건 둘째치고, 다양한 클래스를 다루다보니 원시 타입만 생각할 수가 없다는 게 큰 문제였다. Worker 타입, Policy 타입 등이 마구 섞이기 시작했다. 여기에 더해 인스턴스 멤버의 자동 완성이 제대로 안 된다는 불편함도 있었다.

          +

          타입 문제, 역시 타입스크립트가 바로 떠올랐다. 기존 자바스크립트 코드를 타입스크립트로 그대로 옮기는 작업이 크게 어려울 것 같지는 않았다. 바로 새로운 브랜치를 만들어 타입스크립트로 마이그레이션하는 작업을 시작했다.

          +
          abstract class Worker {
          +  protected static cost: number = 0;
          +  protected static minOutput: number = 0;
          +  protected static maxOutput: number = 0;
          +
          +  protected output: number;
          +  protected level: number;
          +  protected levelCost: number;
          +  // ...
          +  protected img: string;
          +
          +  constructor() {
          +    this.output = 0;
          +    this.level = 1;
          +    this.levelCost = 0;
          +    // ...
          +    this.img = '';
          +  }
          +
          +  static getMinOutput(): number {
          +    return this.minOutput;
          +  }
          +
          +  static getMaxOutput(): number {
          +    return this.maxOutput;
          +  }
          +
          +  // ...
          +
          +  abstract next(): Worker;
          +}
          +
          +export default Worker;
          +
          +

          기존 코드와 거의 비슷하다! 타입스크립트는 public, private, protected와 같은 접근 제한자를 제공한다. public은 외부에서 해당 클래스의 멤버에 직접 접근할 수 있고, private은 해당 클래스 내부에서만 접근할 수 있다. 그리고 protected는 해당 클래스와 그 서브클래스에서만 접근 할 수 있다.

          +

          Worker의 프로퍼티들을 protected로 설정해 Worker의 서브클래스에서만 프로퍼티에 직접 접근할 수 있도록 제한했다. 이 프로퍼티들은 오직 getter, setter 메소드를 통해서만 접근할 수 있다.

          +

          추상 클래스와 추상 메소드도 만들 수도 있다. Worker 클래스를 추상 클래스로 만들어 new Worker()처럼 생성하지 못하도록 했다. 또한 앞서 직접 에러를 던져주던 next 메소드도 추상 메소드로 만들었다. 이제 서브클래스에서 next를 오버라이드하지 않으면 타입스크립트 컴파일러가 알아서 잡아낸다.

          +

          대체로 자바 문법과 굉장히 비슷하다는 느낌을 받았다.

          +

          📌 코딩말고 다른 작업들

          +

          프로그래밍뿐 아니라 기획과 그래픽도 중요한 작업이었다.

          +

          마우스로 도트찍기

          +

          +

          도트그래픽 외에는 선택지가 없었다. 다행인 것은 내가 도트그래픽을 좋아하고, 도트를 찍는 것도 좋아한다는 것이었다. 마우스로 열심히 도트를 찍어 프레임 포함 30개 정도의 어셋을 만들었다. 이 중에는 인게임에 적용된 것도 있고, 안타깝게 적용되지 못한 것도 있다. 빈 교실에서 눈오는 창밖을 보며 도트찍던(…) 그때가 생각난다.

          +

          사라진 콘텐츠와 추가된 콘텐츠

          +

          사실 Santa Inc.는 2015년 사회 이슈를 풍자하기 위해 만들었던 게임이었다. 당시에는 한창 열정페이나 노동개혁에 대한 논의가 활발했고, Molleindustria처럼 사회적 메시지를 담은 결과물을 만들고 싶었다. 그래서 첫 버전에는 플레이어가 악덕 정책을 다수 채택하는 후반부에 요정들이 노조를 결성해 파업에 돌입하고, 회사가 감시, 소송 절차를 거치며 노조를 파괴하는 시나리오까지 있었다.

          +

          게임을 다시 만든 2018년에 똑같은 요소를 넣기엔 시의에 맞지 않았다. 노동 이슈를 그대로 다루되, 몇가지 정책을 빼고 이를 대체하는 다른 정책들을 추가했다. 기존의 해고 탭도 인사 탭으로 바꿔 직원을 승진시키는 기능을 더했다.

          +

          기존에 직원으로 있던 '커플’도 뺐다. 예전에는 크리스마스마다 커플을 밈으로 사용하는 한국 전통(?)에 따라 커플을 직원으로 넣었으나, 지금 보니 별로 적절하지 않았다. 캐릭터에 젠더 고정관념이 그대로 반영되기도 했고, 최근에는 그런 밈이 거의 사라진 것 같다. 대신 그 자리에 '트리’를 추가했다. 크리스마스하면 트리인데 왜 진작에 안 넣었는지 모르겠다.

          +

          '아이들’도 빼려 했다. 아이들을 위해 선물을 주는 산타가 아동노동을 자행하는 모습을 통해 블랙기업 산타 주식회사의 정체성을 좀 더 명확히 하려는 의도였는데, 마음이 아파서 아이들은 고용하지 않았다는 말을 듣고 이번에는 없애려 했…으나 모 회사에서 심야 업무에 불법적으로 청소년 노동자를 동원했다는 뉴스가 나와 다시 넣게 되었다.

          +

          추가로, 예전에 추가하지 못했던 비운의 알바요정을 루돌프와 인턴요정 사이에 넣었다.

          +

          🤔 그런 짓은 하지 말아야 했는데

          +

          제이쿼리를 버리고 싶었지만 처음 만들 때 제이쿼리로 만들었더니 뒤바꾸는 게 쉽지 않았다. 아예 웹 게임 엔진을 쓰면 좋은데, 로직을 고치는 것만으로도 벅차서 이 부분은 잘 신경쓰지 못했다.

          +

          일단 로직과 html 코드 조각이 마구 섞여있는 게 신경쓰였다. 리액트에서 컴포넌트를 관리하는 것처럼 요소별로 모듈을 분리하면 어떨까 싶어서 시도해봤다.

          +
          const WorkerItem = (
          +  name: string,
          +  img: string,
          +  korName: string,
          +  cost: number,
          +  minOutput: number,
          +  maxOutput: number
          +) => {
          +  return (
          +    `<li id="${name}">` +
          +      `<img class="worker-img" src="${img}"/>` +
          +      `<p>${korName}` +
          +        '<img class="item-present-img" src="./assets/present.png">' +
          +        `<span class="t">${cost}</span>` +
          +        `<br/><span class="pr">${minOutput} ~ ${maxOutput}개 생산</span>` +
          +      '</p>' +
          +    '</li>'
          +  );
          +};
          +
          +export default WorkerItem;
          +
          +

          직원 목록의 각 아이템을 컴포넌트로 만들었다. 이제 필요한 곳에서 불러와 파라미터를 잘 넣어주면 된다.

          +
          import WorkerItem from './WorkerItem';
          +// ...
          +drawWorkerList(worker: Worker): void {
          +  // ...
          +  this.elements.workerList.append(
          +    WorkerItem(
          +      worker.getName(),
          +      worker.getImg(),
          +      worker.getKorName(),
          +      workerClass.getCost(),
          +      workerClass.getMinOutput(),
          +      workerClass.getMaxOutput()
          +    )
          +  );
          +  // ...
          +
          +

          파라미터도 많고 관심사의 분리도 완벽하지 않아서 여전히 마음에 안 들지만, 그래도 로직과 뒤섞여 있는 것보다는 조금 나아졌다. 분명 제이쿼리 장인들은 더 나은 방법을 썼을 것 같은데 잘 모르겠다.

          +

          평소 게임 개발쪽은 큰 관심이 없음에도 주기적으로 게임을 만들어지고 싶어지는 시기가 온다. 다음에는 아예 (예전에 파다가 관둔) 유니티를 공부해서 만들어볼까한다.

          +

          Santa Inc.의 코드는 GitHub 저장소에서 살펴볼 수 있다.

          + +
          +
          + +
          + +
          +
          +

          2학년 학부생의 신입 개발자 취업기

          +

          산업기능요원 결심부터 합격까지 3개월의 기록

          +
          +
          +
          + + +
          + +
          +
          +

          🤖 컴퓨터가 코드를 읽는 아주 구체적인 원리

          +

          MIPS 어셈블리어 훑어보기

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/27.html b/article/27.html new file mode 100644 index 0000000..08ef7b0 --- /dev/null +++ b/article/27.html @@ -0,0 +1,211 @@ + + + + + + 2학년 학부생의 신입 개발자 취업기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 2학년 학부생의 신입 개발자 취업기 +

          + +

          + 산업기능요원 결심부터 합격까지 3개월의 기록 +

          + + + + +
          +
          Table of Contents +

          +
          +
          +

          “네가 군대 갈 때는 통일이 되어 있을 거란다.”

          +
          +

          초등학교 4학년 때 삼청공원에서 농구를 하고 있었다. 지나가던 스님이 갑자기 내가 군대에 갈 때는 통일이 되어 군대에 가지 않을 것이라고 말씀하셨다. 길 가던 스님이 뜬금없이 미래를 예견하는 게 너무 민속 설화스러워서 지금도 기억이 난다.

          +

          대학교 2학년이 되면 대부분의 친구들이 군대에 간다. 작년까지만 해도 군대는 남의 일 같았지만, 올해 주변 친구들이 하나둘 입대하면서 내게도 운명의 순간이 다가오고 있음을 느낄 수 있었다. 아직 통일은 안 됐지만 스님이 말한 '군대 갈 때’가 된 것 같았다.

          +

          🤯 어디서부터 손을 대야 할지…

          +

          병역특례의 일종인 산업기능요원이 되면 회사에서 일하며 군복무를 대신할 수 있다는 것을 알고 있었다. 운이 좋게 난 개발을 할 줄 아는 보충역이었다.

          +

          한편 산업기능요원을 하지 말라는 조언도 있었다. 모교 선생님께서는 병특을 했다가 착취당한 사례가 있으니 고민을 해보라고 하셨다. 상담을 해주신 교수님께서는 공익으로 가서 남는 시간에 프로젝트를 하거나, 공무조직의 비효율적인 업무를 개선하는 것은 어떻냐고 하셨다. (그리고 몇 개월 뒤 반병현님을 알게 됐다.)

          +

          사회복무요원(공익)과 산업기능요원(병특) 중 하나를 선택해야 하는 상황이었다. 그러나 공익근무를 하려면 사회복무요원 장기 적체 때문에 기약없는 기다림을 감수해야 했다. 물론 당장 사회복무요원을 할 수 있다고 해도 내 진로와 전혀 관련이 없는 공익 근무로 시간을 보내는 것보다는 회사에서 일을 하는 것이 더 의미있을 것 같았다. 무엇보다 14년을 학교라는 공간에서 보내던 중 터닝포인트를 하나 만들고 싶었고, 학교 수업에 염증을 느끼고 있던 참이기도 했다. 대학원에 가기 전에 현장에서 구체적인 문제를 찾아보라는 감동근 교수님의 글도 참고가 됐다.

          +

          산업기능요원 시작하기

          +

          그냥 막연하게 "산업기능요원하고 싶다!"하던 상황이라서 아는 건 전혀 없었다. 산업기능요원 제도에 관한 정보는 찾기 쉬웠지만, 구체적으로 IT분야 산업기능요원의 취업에 대한 자료는 찾기 힘들었다. 취업기나 복무 후기도 거의 없고 학부생 취업에 관한 자료는 더욱 없었다. 그냥 맨땅에 헤딩하는 수밖에 없었다.

          +

          일단 매년 산업기능요원 수를 감축하고 있어서 TO가 엄청 줄어든 상태인데, 보충역은 장기 적체로 인해 걸어다니는 TO와 같다. 적체가 너무 심해서 종종 병무청에서 산업기능요원 편입하라는 우편이 올 정도다. (보충역 사회복무요원에서 산업기능요원으로 '편입’하는 것이다.) 심지어 산업기능요원 복무 기간도 1년 11개월로 단축되어 나에게는 정말 좋은 기회였다.

          +

          조건도 현역보다 널널하다. 현역의 경우 자격증이나 경력이 필요하지만, 보충역은 전산 관련 전공 전문학사 학위만 있으면 조건을 충족할 수 있다. 4년제 학부의 경우 2년 이상 수료하면 된다. 따라서 2018년 10월까지 회사를 알아보고 입사 지원을 한 뒤, 2019년 1월부터 복무를 시작하면 딱 적당했다. 이때가 9월이었다.

          +

          산업기능요원 관련 행정 절차는 회사에서 처리하기 때문에 일단 병역 지정 업체에 입사해야 산업기능요원으로 편입할 수 있다. 즉, 취업을 먼저 해야한다. 하지만 두 가지 중요한 문제가 있었다: 내가 전산관련 전공인가? 내가 취업할 정도의 실력이 되나?

          +

          디지털미디어 전공

          +

          다른 사람에게 디지털미디어 전공을 설명할 때마다 곤경에 빠진다. (보통 미디어학과라고 하면 신방과를 생각한다.) 학과소개 페이지는 기획, 디자인, 개발을 아우르는 '융합적 인재’를 양성하는 학과라며 장황하게 설명하고 있지만… 적당히 '컴퓨터공학도 배우고 시각디자인도 배우고 인문학도 적절히 배우는 학과’라고 하면 구구절절 설명할 필요가 없다.

          +

          암튼 확실하게 전산관련 전공이라고 할 수는 없었다. 개발쪽으로 가는 사람들도 있지만 기획이나 디자인, 애니메이션, 방송쪽으로 방향을 잡는 사람들도 있으니까. 산업기능요원 관련 자료에 ‘정보처리 직무분야 관련학과’ 목록과 ‘애니메이션 및 게임 직무분야 관련학과’ 목록이 정리되어 있었는데, 이게 정말 모호하게 적혀있어서 따로 병무청에 민원을 보냈다.

          +

          다행히 디지털미디어 전공은 전산 관련 전공이 맞았다.

          +

          신입사원의 실력

          +

          더 큰 문제는 내 실력이었다. 프로그래밍을 처음 시작한 건 중1이고, 중3 때 웹 프로그래밍 수업을 들은 걸 빼면 항상 독학을 해왔다. 대학에서는 원론적인 내용만을 배웠기 때문에 내 실무 능력에 대한 피드백을 받을 기회가 많지 않았다. 더불어 프로젝트는 많이 했지만, 대회에 나가거나 자격시험을 본 적이 없으니 내 실력이 어느 정도인지도 감을 잡기 힘들었다.

          +

          병특도 일반적인 신입사원과 똑같은 수준의 실력을 요구한다는데, 내가 그 정도가 될까? 신입사원에게 어느 정도의 실력을 기대할까? 산업기능요원은 성장 가능성보다 당장 실무에 투입할 수 있는지를 먼저 보지 않을까? 나는 아직 2학년인데 졸업생들이랑 비교하면 한참 모자라지 않을까? 사실 내가 자질이 없는 건 아닐까? 생각이 너무 많아서 취업 활동을 계속 미루기만 했다.

          +

          하지만 내가 시니어급은 돼야 답을 찾을 것 같아서 고민을 그만뒀다.

          +

          🔭 좋은 회사를 찾아보자

          +

          병역 지정 업체는 많지만, 아무 곳에나 들어갈 수는 없다. 워낙 주변에서 착취당했다는 얘기를 많이 들어서 걱정이 많았다. 일단 나만의 기준을 생각해봤다.

          +
            +
          • B2C 서비스를 만드는 회사
          • +
          • 개발자 문화와 오픈소스 문화에 기여하는 회사
          • +
          • 기술 경험 공유에 적극적인 회사
          • +
          • 퇴사율 25% 이하, 코드리뷰 문화, 수평적인 문화 등 (페넥여우님의 트윗을 참고했다.)
          • +
          +

          사실 좋은 회사의 기준이라기 보다는 개인적인 취향에 가깝다.

          +

          어떻게 찾지?

          +

          가장 좋은 방법은 '내가 사용하는 서비스를 만드는 회사’의 채용 공고를 찾는 것이었다. 물론 이 경우에는 해당 회사가 병특 업체인지, 내가 원하는 직군을 채용하고 있는지, 신입을 채용하는지 따져야 했다. 이때 아래 서비스들을 활용했다:

          +
            +
          • 로켓펀치: 병특 업체에 스타트업이 많기 때문에 큰 도움이 됐다.
          • +
          • 잡플래닛: 회사 리뷰나 면접 후기, 복지 정보를 확인할 수 있다.
          • +
          • 원티드: 원하는 회사를 북마크해두면 채용 공고가 떴을 때 알림을 받을 수 있다.
          • +
          • 크레딧잡: 연봉이나 입사, 퇴사 추이 등 구체적인 정보를 얻을 수 있다.
          • +
          • 플래텀, 블로터: 회사 관련 기사나 대표 인터뷰를 찾아볼 수 있다.
          • +
          • THE VC: 투자 정보를 찾아볼 수 있다.
          • +
          • 병역일터: 병특 업체를 찾아볼 수 있지만, 정부 사이트답게 매번 본인 인증을 해야한다.
          • +
          +

          회사 리크루트 페이지에서 지원 자격과 우대 사항도 살펴봤다. 프론트엔드 엔지니어의 경우 대부분의 회사가 HTML/CSS/JS는 기본이고 타입스크립트, 리액트, 리덕스, 디자인패턴 지식을 지원 자격으로 제시했다. 추가로 SPA 개발 경험이나 CI/CD 경험을 요구하는 회사도 있었고, 공채를 하는 큰 회사에서는 기본적인 CS 지식을 요구했다.

          +

          해당 기술을 사용해 서비스 가능한 어플리케이션을 완성해본 경험이 있으면 자격을 충족한다고 (멋대로) 생각했다. 기술 스택은 대부분 만족했으나, 디자인패턴이나 자료구조, 알고리즘 등 조금 부족하다고 느끼는 부분은 지원 전에 추가로 공부했다.

          +

          기술 문화는 회사에서 진행하는 오픈소스 프로젝트나 기술 블로그를 통해 알 수 있었다. 또한 그 회사가 어떤 개발자 행사에 참여했는지, 후원했는지 보며 내가 이 회사에서 기술적으로 얼마나 성장할 수 있을지 추측했다.

          +

          산업기능요원으로도 지원 가능한가요?

          +

          채용공고에 산업기능요원을 채용한다고 명시한 회사도 있었지만, 그렇지 않은 회사도 많았다. 여러 곳에 메일을 보내 산업기능요원으로도 지원할 수 있는지, 내년 초부터 일해도 괜찮은지 문의했다. 일단 지원해보라는 회사도 있었고, 병역특례 업체가 아니라는 회사도 있었다. 정말 좋아하는 회사였는데 채용 문의 메일에 답장이 오지 않아서 실망하기도 했다.

          +

          가장 큰 걸림돌은 많은 회사가 경력직만 뽑고 있었다는 것이다. 거의 모든 회사들이 2년 이상의 경력을 요구했다. 또한 병특업체지만 산업기능요원이 아닌 전문연구요원만 채용하는 회사도 많았다. 점점 지원할 수 있는 회사가 줄어들었다.

          +

          연봉의 경우 내가 얼마나 받아야 하는지 감이 잡히지 않았다. 특히 산업기능요원은 급여를 제대로 안 주는 사례가 많아서 원티드의 직군별 연봉을 확인하고 평균 정도만 받자는 생각을 했다. 회사의 연봉 정보는 크레딧잡에서 확인할 수 있었다.

          +

          그 외에 평가와 위치, 지원자격 등을 모아 스프레드 시트에 정리했다.자격을 충족하지 못하거나 나와 잘 맞지 않는 경우 빨간색으로 강조했다. 산업기능요원 지원 여부가 불확실한 회사는 노란색으로, 당장 지원할 수 있는 회사는 초록색으로 구분했다.

          +

          📮 이력서 만들고 지원하기!

          +

          그때그때 재밌는 프로젝트만 해왔기 때문에 취업 대비 같은 건 하나도 되어있지 않았다. 사실 준비만 안 된 정도가 아니었다.

          +

          새내기 때 필수로 참여하라는 학교 행사에 갔는데, 무려 1학년을 대상으로 취업 특강을 했다. '내가 취업하려고 대학에 온 게 아닌데’라는 생각이 들어 몰래 행사를 빠져나왔다. 이후로도 쓸데없는 반항심 때문에 취업이나 스펙같은 말만 들으면 거부 반응을 일으켰다(…)

          +

          그런데 덜컥 취업을 하게 된 것이다. 일단 이력서를 만들어야 했다.

          +

          담백한 이력서

          +

          막막했다. 주변에서 인정받을만한 성과는 고등학교까지였던 것 같고, 대학에 와서는 딱히 이룬 게 없다고 생각했다. 어디선가 이력서에 고등학교 때 활동은 넣지 말라는 내용을 본 것 같은데, 대학에 입학한 지 2년밖에 안 된 입장에선 고등학교 활동을 빼면 넣을 수 있는 내용이 많지 않았다. 이러다 입사 지원도 못하고 망할 것 같다는 생각까지 들었다.

          +

          이수진님의 신입 소프트웨어 엔지니어의 영문 이력서 작성하기와 원티드의 인사담당자가 직접 말하는, 서류 통과가 잘 되는 이력서를 참고해 간단히 이력서를 만들었다. 꾸역꾸역 뭘했는지 생각해내며 한줄 한줄 채우니 나름 의미있는 활동은 해온 것 같았다.

          +

          처음에는 technical skills를 front-end, back-end, others로 세 줄에 걸쳐 썼지만, 별로 의미있는 것처럼 보이지 않았다. 그냥 한 줄로 줄이고 부가 설명은 하지 않았다. C, Java, Python 등 지원하는 직군과 상관없는 기술도 모두 생략했다.

          +

          프로젝트에 대해서는 해당 프로젝트에서 내가 한 일, 프로젝트를 진행하며 배운 것, 그리고 성과를 적었다. 프로젝트의 규모나 성과는 모두 구체적인 수치로 나타냈다. 자기PR을 잘하는 성격이 아니라서 성과를 표현하는 게 좀 힘들었다.

          +

          +나중에 로켓펀치링크드인에 올린 이력서를 통해 면접 제의가 오기도 했다. 그중 한 곳은 정말 큰 회사라서 깜짝 놀랐다. 아쉽게도 이미 다른 회사 전형이 막바지여서 바로 확답할 수 없었다.

          +

          두근두근 입사 지원

          +

          입사 지원을 한다는 생각만으로 떨렸다. 너무 소심해서 지원을 계속 미뤘다. 컴퓨터 구조 과제 끝나면 지원하자, 이번 프로젝트만 마무리하고 지원하자, 중간고사 끝나면 지원하자, 그렇게 11월이 되어버렸다.

          +

          더 이상 미룰 수 없었다. 첫 번재로 이력서를 보낸 회사는 좋은 기술 문화를 갖췄다고 알려진 스타트업(A회사)이었다. 아직 유명하지는 않지만 잡플래닛 평가나 기술 블로그 내용이 좋았다. 그리고 결과는…

          +

          서류에서 광탈이었다. 지금이야 소년만화 클리셰가 떠오르지만, 그때는 역시 안 되는 걸까 자괴감에 빠졌다. 겁이 나서 다음 회사(B회사)는 2주가 지나서야 지원할 수 있었다. B회사는 이미 잘 알고 있는 회사였다. 내가 직접 서비스를 사용하지는 않지만, 주변에 물어보면 사용하는 사람이 꽤 많은 것 같았다. 두 번째 지원도 여전히 떨렸다.

          +

          이번에는 이력서와 함께 자기소개서도 보냈다. 대학 입시 때 쓴 자소서와 재작년 우아한 테크캠프 지원할 때 쓴 자소서를 참고했다. 베이스가 있어서 하루만에 끝낼 수 있었다. 내용은 단순했다. 프로그래밍을 시작한 계기부터 시작해 각 프로젝트에 어떤 기여를 했고, 어떤 것을 배웠는지 썼다. 마지막에는 해당 회사에 지원한 이유와 산업기능요원을 선택한 이유를 적었다.

          +

          종종 자소서를 존댓말로 써야 한다는 조언이 보였다. 몇 년 전 대입 자소서 초안을 존댓말로 썼는데, 존댓말로 글을 쓰면 구어체가 되어 문장이 자연스럽지 않게 늘어져 버렸다. 애초에 자소서가 존댓말이 아니라서 예의없다고 생각하는 회사는 안 가는 게 낫다고 생각했다.

          +

          일주일 뒤, 집에 가던 중 메일이 왔다. 합격이었다.

          +

          🎢 정신없이 흘러간 채용 프로세스

          +

          B회사의 입사 전형은 1차 서류, 2차 온라인 과제, 3차 기술 인터뷰, 4차 임원 인터뷰로 진행됐다. 뭐 이렇게 많아…싶었지만 오히려 사람을 대충 뽑는 회사는 아니라서 한편으로는 다행이었다. (넷플릭스는 면접만 9번 본다고 한다.)

          +

          입사 과제 vs 기말 프로젝트

          +

          모든 전형이 학기 중에 이루어졌다. 온라인 과제로 일주일의 기간과 알고리즘 문제 2개, 인터페이스 구현 문제 3개(택1)가 주어졌다. 하필 이때 기말 프로젝트가 시작되던 시기라서 본격적으로 바빠지기 시작했다. 학점과 병특 중 하나를 선택해야 했다.

          +

          둘 다 잡기로 했다. 딱 일주일만 버티자는 생각으로 자투리 시간을 끌어모아 온라인 과제를 했고, 잠을 줄여 기말 프로젝트를 했다. 알고리즘 문제는 크게 어렵지 않아 이틀 만에 끝냈다. 그리고 나머지 5일은 인터페이스 구현 문제에 집중했다. 문제 3개의 난이도는 다 비슷해서 가장 재밌어 보이는 SPA 구현 과제를 선택했다.

          +

          개발에는 리액트와 styled components를 사용했다. 가벼운 채팅 어플리케이션이기 때문에 webpack을 사용하지 않고 parcel로 빌드했다. 시간 관련 기능에도 moment.js가 아닌 day.js를 사용했다. 패키지 선택도 그렇고 로직 구현도 그렇고, 입사 과제라고 생각하니 괜히 더 신경쓰게 됐다.

          +

          제출일이 다가올수록 내 과제물에서 부족한 점만 보이기 시작했다. 그런 부분을 보완하다가 과제 지시사항에 없던 부분까지 구현해서 제출했다. 불안하고 후련했다. 한편 기말 프로젝트는 기대치보다 낮은 완성도로 마무리하고 말았다. 부족한 점이 많은 채로 제출해서 A+를 기대하기는 어려울 것이라고 생각했다.

          +

          어쨌든 둘 다 끝내긴 했지만, 그때는 정말 죽을 뻔했다. 학교 다니면서 취업 활동하는 선배들은 어떻게 하는 걸까, 회사 다니면서 이직하는 사람들은 정체가 뭘까 싶었다.

          +

          회사가 나를 평가할 때 나도 회사를 평가한다

          +

          온라인 과제를 통과하고 일주일 뒤 1차 면접을 진행했다. 이력서와 자소서를 바탕으로 기술적 경험을 이야기했고, 과제에 대한 이야기도 했다. (의외로 CS 기본 지식에 관한 질문은 없었다.) 대입 면접과 비교하면 정말 떨리지 않았고, 면접 분위기도 편했다. 그럼에도 내 능력을 평가받는 건 긴장할 수밖에 없는 일이다. 질문이 꼬리에 꼬리를 물며 들어올 때는 조금 횡설수설했다. 그러다 보니 마지막엔 생각해뒀던 질문도 잊어버렸다. 찝찝한 마음으로 강남역에서 쌀국수를 먹었다.

          +

          2차 면접 역시 이력서와 자소서를 바탕으로 이야기했으나, 기술적인 내용은 없었다. 첫 인터뷰에 비해 훨씬 편했다. 마지막에 1년 11개월 근무 이후 바로 복학해야 한다는 점이 조금 아쉽다고 말씀하셨는데 잘 대답하지 못했다. 그날 밤 잠들기 전 '아 이렇게 말할걸’하는 생각이 계속 들었다.

          +

          B회사 전형 중간에 지원한 다른 회사(C회사)는 내가 자주 쓰는 서비스를 만드는 회사로, 고등학생 때 인턴을 하고 싶다고 생각했을 정도로 애정이 있었다. C회사는 서류 합격 후 전화 인터뷰를 진행했다. 브라우저의 동작이나 네트워크에 대한 질문, 리액트, 리덕스같은 프론트엔드 기술에 대한 질문, 그리고 디자인 패턴에 대한 질문이 쏟아졌다.

          +

          간단한 전화 인터뷰라고 해서 이력서나 자소서 사실 확인 정도인 줄 알았는데, 기술 인터뷰라서 엄청 당황했다. 질문을 받을 때마다 오래전 본 수업 강의 노트와 기술 아티클들이 주마등처럼 스쳐가며 배경지식과 질문 속 단서들이 결합해 새로운 지식을 생산하는 경험을 했다. 더 최악은, 동아리 방에서 통화하는 바람에 다른 사람이 내 면접을 모두 듣고 있었다는 것이다.

          +

          입사 전형은 회사가 나를 평가하는 동시에 내가 회사를 평가하는 과정이었다. 간접적으로나마 회사 분위기를 알 수 있었고, CEO와 CTO의 생각도 들어볼 수 있었다. B회사의 경우 온라인 과제에서 불합격해도 피드백을 준다고 해서 굉장히 좋은 인상을 받았다.

          +

          🎉 꿈에 그리던 휴학

          +

          12월 셋째 주, 디지털 타이포그래피 기말 프로젝트 발표를 하러 가던 중 B회사 최종합격 메일을 받았다. 정말 다행이라는 생각이 드는 한편, 겁내지 말고 더 빨리 입사 지원을 했더라면 좋았을 것이라는 생각을 했다. 뭐가 그렇게 겁났던 걸까.

          +

          일주일 뒤 메일을 통해 입사 조건을 조정했고, 1월 14일 첫 출근을 하기로 했다. 그리고 2학년 수료를 증명하기 위한 성적증명서를 준비해야한다는 안내를 받았다. (훈련소 일정은 회사와 상의해서 결정하면 된다고 한다.) 그렇게 2019년부터 B회사와 함께하게 됐다.

          +

          B회사의 입사일이 먼저 결정되어 C회사는 어쩔 수 없이 전형 중간에 그만둬야 했다. 굉장히 좋아하는 회사인데 아쉬웠다. B회사에 지원하고 한참 뒤에 C회사의 공고가 떠서 그렇기도 했지만, 내가 온라인 과제를 기말고사 이후로 미룬 탓이 크다.

          +

          회사 생활이라고는 인턴 한 달 남짓한 게 전부라서 여전히 걱정이 많다. 인턴을 했던 회사는 워낙 규모가 큰 곳이라 선배 개발자께서 '부속품이 된 것 같다’고 하셨는데, 이곳에서는 다른 경험을 할 수 있을 것 같아 기대도 된다. 일단은 휴학을 하게 돼 너무 좋다 :D

          +

          📋 도움이 된 자료

          +

          모든 게 처음이라 낯설기만 했다. 나와 비슷한 상황에 놓인 분들에게 도움이 되면 좋겠다:

          + + +
          +
          + +
          + +
          +
          +

          Git 사용 중 자주 만나는 이슈 정리

          +

          코딩보다 어려운 버전 관리

          +
          +
          +
          + + +
          + +
          +
          +

          🎅 요정을 착취하는 방치형 게임 개발한 이야기

          +

          ES6 OOP와 타입스크립트, 그리고 제이쿼리(?)

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/28.html b/article/28.html new file mode 100644 index 0000000..7f0ea62 --- /dev/null +++ b/article/28.html @@ -0,0 +1,498 @@ + + + + + + Git 사용 중 자주 만나는 이슈 정리 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + Git 사용 중 자주 만나는 이슈 정리 +

          + +

          + 코딩보다 어려운 버전 관리 +

          + + + + +
          +
          Table of Contents +

          +
          +

          깃으로 버전 관리를 하다보면 각종 이슈가 자주 발목을 잡는다. 특히 복잡한 프로젝트의 경우 그 정도가 심한데… 입사 이후 '지금까지 내가 한 건 깃이 아니구나’를 깨닫고 더 공부해 보기로 했다. 그러다보니 매번 비슷한 문제 상황을 마주하게 되는 것 같아서 케이스별로 정리해보았다.

          +

          먼저 아주 간단한 예시로 주요 용어를 살펴보면:

          +
                      -add->      -commit->     -push->
          ++-------------+-------------+------------+-------------+
          +| Working dir |    Index    | Local repo | Remote repo |
          ++-------------+-------------+------------+-------------+
          +         <-checkout-                 <-fetch-
          +
          +
            +
          1. Alice는 깃허브에서 'project’라는 이름의 원격 저장소를 만들었다. +
              +
            • 원격 저장소(Remote repository): 일반적으로 GitHub, GitLab 또는 BitBucket과 같은 호스팅 서비스에서 호스팅된 저장소.
            • +
            +
          2. +
          3. 이 원격 저장소를 git clone https://github.com/alice/project.git 명령으로 자신의 컴퓨터에 복사해 로컬 저장소를 만들었다. +
              +
            • 로컬 저장소(Local repository): 컴퓨터의 로컬 환경에 위치한 저장소.
            • +
            +
          4. +
          5. 그리고 작업 디렉토리에서 index.html 파일을 작성했다. +
              +
            • 작업 디렉토리(Working directory): 실제 파일이 위치한 디렉토리.
            • +
            +
          6. +
          7. 이 파일을 git add index.html 명령으로 변경된(Modified) 파일들을 스테이징인덱스 영역에 등록했다. +
              +
            • 스테이징(Staging): 확정할 변경 사항을 준비시키는 것.
            • +
            • 인덱스(Index): 확정할 준비가 된 변경 사항들이 모인 영역.
            • +
            +
          8. +
          9. 이어서 git commit -m "index.html 추가" 명령으로 스테이징된(Staged) 변경 사항을 커밋해 로컬 저장소에 등록했다. +
              +
            • 커밋(Commit): 인덱스의 변경 사항들을 확정하는 것. 여기까지는 로컬 저장소에서 일어나는 일이며, 아직 다른 사람에게는 변경 사항이 공개되지 않은 상태다.
            • +
            • 헤드(HEAD): 작업 중인 브랜치의 선두를 가리키는 포인터. 헤드 이하의 커밋들을 확정된 것으로 취급하며, 필요에 따라 특정 커밋이나 브랜치를 가리키도록 헤드를 움직여 작업 디렉토리의 상태를 바꿀 수 있다.
            • +
            +
          10. +
          11. 마지막으로 git push origin master 명령으로 푸시해 커밋된(Committed) 변경 사항을 원격 저장소에 게시했다. +
              +
            • 푸시(Push): 확정된 변경 사항을 원격 저장소에 게시하는 것. 드디어 변경 사항이 공개된다.
            • +
            • origin: 로컬 저장소의 원본 원격 저장소. clone 과정에서 자동으로 등록된다. clone으로 로컬 저장소를 만든 것이 아니라면 따로 추가해야 한다.
            • +
            +
          12. +
          +

          포크(Fork)나 풀 리퀘스트(Pull request) 등 깃허브에 관한 내용은 오픈소스 입문을 위한 아주 구체적인 가이드에서 소개했다.

          +

          파일 스테이징 취소하기

          +
            +
          • Alice는 git add main.js 명령으로 파일을 스테이징했다.
          • +
          • main.js를 언스테이징(Unstaging)하고자 한다.
          • +
          +

          reset 명령을 사용하면 파일을 언스테이징 할 수 있다.

          +
          $ git reset main.js
          +
          +

          파일명을 명시하지 않으면 스테이지된 모든 파일을 언스테이징한다.

          +

          마지막 커밋 취소하기

          +
            +
          • Alice는 방금 lib.js 파일을 빠뜨리고 커밋했다.
          • +
          • 커밋을 취소하고 파일을 추가해 새로 커밋하고자 한다.
          • +
          +

          헤드를 옮겨 마지막 커밋을 취소한다. --soft 옵션은 작업 디렉토리와 인덱스를 보존해 파일이 스테이지된 상태를 유지하도록 한다. HEAD^는 헤드의 직전 위치를 의미한다. 즉, 현재 브랜치의 마지막 커밋을 뜻한다.

          +
          $ git reset --soft HEAD^
          +
          +

          빠뜨린 파일을 추가하고 다시 커밋한다.

          +
          $ git add lib.js
          +$ git commit -m "자바스크립트 파일 추가"
          +
          +

          마지막 커밋 메시지 수정하기

          +
            +
          • Alice는 방금 커밋한 커밋 메시지 "테스트ㅌ 코드 추가"에 오타가 있는 것을 발견했다.
          • +
          • 직전 커밋의 메시지를 "테스트 코드 추가"로 수정하고자 한다.
          • +
          +

          --amend 옵션으로 커밋해 직전 커밋 메시지를 수정한다.

          +
          $ git commit --amend -m "테스트 코드 추가"
          +
          +

          이미 푸시한 커밋 메시지 수정하기

          +
            +
          • Alice는 과거 커밋 메시지 "테스트ㅌ 코드 추가"에 오타가 있는 것을 발견했다.
          • +
          • 해당 커밋 메시지를 "테스트 코드 추가"로 수정하고자 한다.
          • +
          +

          먼저 해당 커밋으로 리베이스해야 한다. --interactive 또는 -i 옵션을 주면 텍스트 에디터가 열리며 커밋 내역이 나타난다.

          +
          $ git rebase -i
          +
          +
          pick 381cd2a 코드 품질 개선
          +pick f772ba1 테스트ㅌ 코드 추가
          +pick 2ad65fe 하단 버튼 추가
          +
          +

          여기서 수정하고자 하는 커밋 해시(f772ba1) 앞의 pickedit 또는 e로 바꾸고 저장한다.

          +
          pick 381cd2a 코드 품질 개선
          +edit f772ba1 테스트ㅌ 코드 추가
          +pick 2ad65fe 하단 버튼 추가
          +
          +

          이제 커밋 메시지를 새로 작성해 커밋하고, 리베이스를 진행한다. 마음이 바뀌었다면 리베이스 명령의 --abort 옵션으로 리베이스를 중단할 수 있다. 리베이스 이후엔 --force 또는 -f 옵션으로 푸시한다. 이렇게 하면 과거 커밋 이력이 변경되기 때문에 협업을 하는 상황이라면 반드시 사전에 협의를 해야한다.

          +
          $ git commit --amend -m "테스트 코드 추가"
          +$ git rebase --continue
          +$ git push -f origin master
          +
          +

          좀 더 직관적이고 쉽게 커밋 정보를 변경하고 싶다면 git-amend와 같은 유틸을 쓸 수도 있다.

          +

          커밋을 과거로 되돌리기

          +
            +
          • Alice는 버튼의 색깔을 바꾸는 작업들을 커밋했으나 모든 게 잘못됐다는 것을 깨달았다.
          • +
          • 모든 버튼의 색깔을 똑같이 만들기 위해 과거 버전으로 돌아가 코드를 다시 작성하고자 한다.
          • +
          +

          우선 돌아가고 싶은 버전의 커밋 해시(21929f8)를 확인한다.

          +
          $ git reflog
          +28ca4ca HEAD@{0}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
          +8eefd4a HEAD@{1}: commit: 가운데 버튼 색깔을 초록색으로 변경
          +21929f8 HEAD@{2}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
          +
          +

          그리고 reset을 이용해 헤드를 특정 커밋으로 옮긴다. reset의 기본 옵션은 --mixed이며, 작업 디렉토리는 그대로 유지한 채 헤드와 인덱스를 변경한다. --hard 옵션의 경우 작업 디렉토리와 헤드, 인덱스를 모두 변경한다. 마지막으로 --soft 옵션은 헤드만 변경한다.

          +
          $ git reset --hard 21929f8
          +
          +

          코드를 수정한 뒤, 파일을 스테이징하고 다시 커밋한다.

          +
          $ git add *
          +$ git commit -m "모든 버튼 색깔을 노란색으로 변경"
          +
          +

          이 방법을 사용하면 되돌린 커밋 히스토리가 모두 사라진다. 커밋이후 --force으로 푸시하면 이미 푸시한 커밋까지 되돌릴 수도 있지만, 권장하는 방법은 아니다.

          +

          푸시한 커밋을 과거로 되돌리기

          +
            +
          • Alice는 버튼의 색깔을 바꾸는 작업들을 커밋, 푸시했으나 모든 게 잘못됐다는 것을 깨달았다.
          • +
          • 모든 버튼의 색깔을 똑같이 만들기 위해 과거 버전으로 돌아가 코드를 다시 작성하고자 한다.
          • +
          +

          revert 명령을 사용하면 된다. revertreset과 달리 커밋 히스토리를 남긴다. 협업을 하는 상황에서 이미 푸시한 커밋을 되돌리고 싶다면 reset 보다는 revert를 사용하는 것이 좋다. 먼저 돌아리고 싶은 버전의 커밋 해시들(21929f8, 8eefd4a, 28ca4ca)을 확인한다.

          +
          $ git reflog
          +28ca4ca HEAD@{0}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
          +8eefd4a HEAD@{1}: commit: 가운데 버튼 색깔을 초록색으로 변경
          +21929f8 HEAD@{2}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
          +
          +

          그리고 되돌리고 싶은 커밋의 범위를 지정해 revert를 실행하고, 에디터에서 커밋 메시지를 작성한다. 만약 특정 하나의 커밋만 되돌리고 싶다면 커밋 해시를 하나만 입력해도 된다.

          +
          $ git revert 21929f8...28ca4ca
          +Revert "왼쪽 버튼 색깔을 빨간색으로 변경"
          +Revert "가운데 버튼 색깔을 초록색으로 변경"
          +Revert "오른쪽 버튼 색깔을 파란색으로 변경"
          +
          +

          푸시하면 버튼의 색깔을 바꾼 작업들이 취소된다.

          +
          $ git push origin master
          +
          +

          히스토리에는 세 개의 커밋이 추가된 것을 볼 수 있다.

          +
          $ git reflog
          +132bf27 HEAD@{0}: commit: Revert "오른쪽 버튼 색깔을 빨간색으로 변경"
          +b2c409f HEAD@{1}: commit: Revert "가운데 버튼 색깔을 초록색으로 변경"
          +4ef1104 HEAD@{2}: commit: Revert "왼쪽 버튼 색깔을 파란색으로 변경"
          +28ca4ca HEAD@{3}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
          +8eefd4a HEAD@{4}: commit: 가운데 버튼 색깔을 초록색으로 변경
          +21929f8 HEAD@{5}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
          +
          +

          푸시한 파일 삭제하기

          +
            +
          • Alice는 공유할 필요가 없는 파일을 원격 저장소에 푸시했다.
          • +
          • 실수로 푸시한 파일 setting.txt를 원격 저장소에서 삭제하고자 한다.
          • +
          +

          원격 저장소에 올라간 파일 setting.txt를 삭제한다. --cached 옵션은 인덱스에서만 파일을 삭제한다는 의미로, --cached 옵션을 주면 로컬 저장소의 파일은 지우지 않는다.

          +
          $ git rm --cached setting.txt
          +
          +

          이제 원격 저장소의 master 브랜치에 반영한다.

          +
          $ git commit -m "설정 파일 제거"
          +$ git push orgin master
          +
          +

          히스토리가 그대로 남기 때문에 민감한 파일의 경우 이 방법으로 지워선 안 된다. 커밋에서 제외할 파일은 미리 .gitignore 파일에 명시하도록 하자.

          +

          푸시한 파일 흔적없이 삭제하기

          +
            +
          • Alice는 절대 공개해서는 안되는 파일을 원격 저장소에 푸시했다.
          • +
          • 실수로 푸시한 파일 password.txt를 원격 저장소에서 삭제하고자 한다.
          • +
          +

          먼저 모든 히스토리에서 password.txt를 제거해야 한다. filter-branch를 사용하면 브랜치의 히스토리 전체에서 특정 커밋만 필터링해 수정할 수 있으며, 여기에 --index-filter 옵션을 주면 인덱스를 수정하게 된다. 아래 명령의 경우 모든 커밋에서 git rm --cached --ignore-unmatch password.txt 명령을 실행한다. 참고로 rm 명령의 --ignore-unmatch 옵션은 파일이 일치하지 않아도 0을 리턴하도록 해 명령이 반복되게 만든다.

          +
          $ git filter-branch -f --index-filter "git rm --cached --ignore-unmatch password.txt" HEAD
          +
          +

          파일을 삭제하는 과정에서 아무런 내용이 없는 빈 커밋이 생길 수 있다. 이제 모든 히스토리에서 빈 커밋을 제거하고 푸시한다.

          +
          $ git filter-branch -f --prune-empty HEAD
          +$ git push -f origin master
          +
          +

          원격 저장소에서 업데이트 받아오기

          +
            +
          • Alice는 오랜만에 로컬 저장소의 master 브랜치를 업데이트하려 한다.
          • +
          • 며칠전 Bob이 원본 원격 저장소 alice/project에 3개의 커밋을 추가했다.
          • +
          • 원본 저장소 alice/project에 추가된 3개의 커밋을 로컬 저장소로 가져와 업데이트하고자 한다.
          • +
          +

          fetchpull 두 가지 방법이 있다. fetch를 실행하면 원격 저장소의 내용을 로컬 저장소로 가져오며, 임시로 FETCH_HEAD라는 이름의 브랜치를 만든다. (git checkout FETCH_HEAD 명령으로 원격 저장소에서 가져온 업데이트를 확인할 수 있다.)

          +
          $ git fetch origin
          +
          +

          그리고 merge로 가져온 내용을 master 브랜치에 병합(Merge)한다. 만약 병합과정에서 충돌(Conflicts)이 발생하면 직접 파일을 수정해줘야 한다.

          +
          $ git checkout master
          +$ git merge origin/master
          +
          +

          pull의 경우 fetchmerge를 연달아 진행한다. 현재 체크아웃하고 있는 브랜치의 원격 저장소에서 내용을 가져오는 경우 매개변수(아래 예시에서는 origin master)를 생략해도 된다.

          +
          $ git pull origin master
          +
          +

          병합 커밋없이 풀하기

          +
            +
          • Bob은 alice/project 저장소의 master 브랜치에 main.js를 수정하고 커밋, 푸시했다.
          • +
          • Alice는 master 브랜치에서 index.html 파일을 수정하고 커밋했다.
          • +
          • Alice는 pull 명령으로 원격 저장소 alice/project에서 변경 사항을 가져오려 한다.
          • +
          • 이때 병합 커밋(Merge commit)을 만들지 않고 원격 저장소 내용을 가져오고자 한다.
          • +
          +

          원격 저장소와 로컬 저장소 모두 새로운 커밋을 가지고 있기 때문에 논 패스트 포워드(Non fast-forward)가 발생한다. 반대 경우인 패스트 포워드(Fast-forward)는 병합하려는 브랜치에 새로운 커밋이 없는 상황에서 발생하며, 이때는 HEAD를 최신 커밋으로 옮기는 것만으로 병합을 마칠 수 있다. 하지만 논 패스트 포워드는 별도의 병합 커밋이 필요하기 때문에 "Merge branch ‘master’ of https://github.com/alice/project"와 같은 메시지를 가진 커밋을 만든다.

          +

          혼자 작업하는 프로젝트라면 큰 상관이 없겠지만, 여러 사람이 프로젝트에 참여해 병합 커밋을 만들기 시작하면 히스토리가 지저분해진다.

          +
          ───O2───O2───┐ (origin/master)
          +   └────M1───M2 (master)
          +             Merge branch 'master' of https://github.com/alice/project
          +
          +

          이러한 병합 커밋을 만들지 않으려면 --rebase 옵션으로 리베이스 병합을 시키면 된다. (이때 스테이징되지 않은 변경 사항이 있으면 안 된다.)

          +
          $ git pull --rebase
          +
          +

          로컬 저장소의 브랜치가 원격 저장소의 브랜치의 최신 커밋으로 리베이스되어 병합 커밋이 남지 않는다.

          +
          ───O2───O2 (origin/master)
          +        └────M1 (master)
          +
          +

          포크한 로컬 저장소를 최신으로 유지하기

          +
            +
          • Alice는 bob/project 저장소를 포크해서 alice/project 저장소를 만들었다.
          • +
          • 그 사이 bob/project 저장소에는 3개의 커밋이 추가되었다.
          • +
          • 원본 저장소인 bob/project에서 3개의 커밋을 가져와 alice/project를 업데이트하고자 한다.
          • +
          +

          먼저 Alice가 포크한 Bob의 원격 저장소(bob/project)를 업스트림(Upstream)이라는 이름으로 추가한다.

          +
          $ git remote add upstream https://github.com/bob/project.git
          +
          +

          git remote 명령에 --verbose 또는 -v 옵션을 주면 원격 저장소 목록이 나오는데, 업스트림이 잘 추가됐다면 이 목록에서 확인할 수 있다.

          +
          $ git remote -v
          +origin https://github.com/alice/project.git
          +upstream https://github.com/bob/project.git
          +
          +

          최신 내용을 가진 업스트림(bob/project)의 master 브랜치를 가져와 로컬 저장소(alice/project)의 master 브랜치에 병합한다. 이후에 bob/project에 새 커밋이 추가될 때도 같은 명령을 실행하면 된다.

          +
          $ git fetch upstream
          +$ git checkout master
          +$ git merge upstream/master
          +
          +

          작업 내용을 백업하고 다른 브랜치 체크아웃하기

          +
            +
          • Alice와 Bob은 각각 featA, featB 브랜치에서 index.js를 수정했다.
          • +
          • Alice는 featA 브랜치에서 작업하던 중 Bob이 작업하고 있는 featB 브랜치를 확인할 일이 생겼다.
          • +
          • featA의 작업 내용을 백업하고 featB 브랜치를 체크아웃하고자 한다.
          • +
          +

          featA 브랜치에 커밋되지 않은 변경 사항을 남겨두고 git checkout featB를 실행해 featB 브랜치로 넘어가려하면 파일이 덮어쓰기 될 수 있다는 에러가 나온다.

          +
          $ git checkout featB
          +error: Your local changes to the following files would be overwritten by checkout:
          +  index.js
          +Please commit your changes or stash them before you switch branches.
          +Aborting
          +
          +

          체크아웃 뿐만 아니라 리베이스(Rebase)나 풀(Pull) 등 브랜치의 작업 디렉토리가 변경되는 상황에서 만날 수 있는 에러다. 작업 내용을 커밋하고 featB에 갔다가 다시 돌아와 마지막 커밋을 취소할 수도 있겠지만, 가장 깔끔한 방법은 stash를 이용하는 것이다. 이렇게 하면 스테이지되지 않은 파일들을 임시로 백업해 작업 디렉토리를 깨끗하게 만들 수 있다.

          +

          아래와 같이 한 줄의 명령으로 간단하게 작업 내용을 백업할 수 있다.

          +
          $ git stash
          +Saved working directory and index state WIP on featA: e32584d Add featA index.js
          +
          +

          작업 디렉토리가 헤드로 돌아갔으므로 안전하게 featB 브랜치를 체크아웃할 수 있게 됐다. 만약 백업한 내용을 되돌리고 싶다면 pop하면 된다.

          +
          $ git stash pop
          +
          +

          save <name>으로 이름을 붙여 백업하고, list로 목록을 확인할 수 있다. pop stash@<id>로 특정 백업을 복원할 수도 있다.

          +
          $ git stash save ADD_INDEX_JS
          +Saved working directory and index state On featA: ADD_INDEX_JS
          +
          +
          $ git stash list
          +stash@{0}: On featA: ADD_INDEX_JS
          +
          +
          $ git stash pop stash@{0}
          +
          +

          풀 리퀘스트에서 Squash and Merge된 커밋 제외하기

          +
          ───M1───M2───S1───M3 (master)
          +   └────A1───A2 (featA)
          +             └────B1───B2 (featB)
          +
          +
            +
          • Alice는 master 브랜치에서 featA 브랜치를 만들어 A1, A2 커밋을 추가하고 master에 squash and merge했다.
          • +
          • 그리고 featA 브랜치에서 featB 브랜치를 만들어 B1, B2 커밋을 추가했다.
          • +
          • 그 사이 Bob이 master 브랜치에 M3 커밋을 추가했다.
          • +
          • Alice는 featB 브랜치를 master 브랜치에 병합하기 위해 PR을 보냈다.
          • +
          • A1, A2는 master에 S1 커밋으로 squash and merge 됐기 때문에 PR에는 A1, A2, B1, B2 커밋이 모두 포함된다.
          • +
          • PR에서 A1, A2 커밋을 빼고 B1, B2 커밋만 포함시키고자 한다.
          • +
          +

          만약 Alice가 master 브랜치의 M3에서 featB 브랜치를 만들었다면 이런 일이 생기지 않았을 것이다. 설령 A1, A2, B1, B2 커밋이 그대로 병합돼도 내용에는 문제가 없을 것이다. 하지만 PR 커밋 내역에 이미 병합된 커밋들이 쌓여 있는 것은 보기 좋지 않다. 입사 후 이런 실수를 했는데, 너무 당황스러웠다.

          +

          B1, B2 커밋을 복사해 M3의 자식으로 만들기 위해 --onto 옵션으로 리베이스하고, --force 또는 -f 옵션으로 푸시한다. 보다시피 커밋을 제외하거나 뺀다는 개념이 아니다.

          +
          $ git checkout featB
          +$ git rebase --onto featA master
          +$ git push -f origin featB
          +
          +

          이제 B1, B2 커밋이 복사되어 같은 내용의 B1’, B2’ 커밋이 M3를 부모로 갖게 된다. (기존 B1, B2 커밋은 버려지지만 사라지지는 않는다.) 최종적으로 PR에서 A1과 A2 커밋도 깔끔히 사라진다.

          +
          ───M1───M2───S1─────────M3 (master)
          +   └────A1───A2 (featA) └───B1'───B2' (featB)
          +             └───B1───B2 [abandoned]
          +
          +

          병합 충돌 해결하고 풀 리퀘스트 보내기

          +
            +
          • Alice는 bob/project 저장소의 코드를 수정하고 bob/project의 master 브랜치에 풀 리퀘스트를 보냈다.
          • +
          • 그 사이 Carol이 Alice와 같은 코드를 수정했고, Carol의 풀 리퀘스트가 먼저 master에 병합되었다.
          • +
          • Alice의 풀 리퀘스트에 병합 충돌(Merge conflicts) 위험이 생겨 이를 해결하고자 한다.
          • +
          +

          Alice와 Carol이 같은 부분을 수정했기 때문에 병합 충돌이 발생했다. 이때는 master에 Alice의 변경 사항을 반영할지, Carol의 작업을 유지할지 수동으로 결정해줘야 한다.

          +

          이를 해결하는 데는 두 가지 방법이 있는데, 첫 번째는 bob/project의 master를 가져와 Alice의 브랜치 alice-branch에 병합하는 것이다.

          +
          $ git pull origin master
          +
          +

          이렇게하면 bob/project의 master 브랜치의 내용을 로컬로 가져와 Alice의 브랜치에 병합된다. 이때 병합 충돌이 발생하는데, 에디터에서는 아래와 같이 보인다.

          +
          function add(a, b) {
          +<<<<<< HEAD
          +  return a + b;
          +======
          +  return b + a;
          +>>>>>> master
          +}
          +
          +

          HEAD는 Alice의 변경 사항(Current chnage)이고, master는 Carol의 변경 사항(Incoming change)이다. 두 변경 사항 중 하나를 선택하면 된다. Alice는 ====== 부터 >>>>>> master 까지의 내용을 지움으로써 자신이 작성한 변경 사항을 선택했다.

          +
          function add(a, b) {
          +  return a + b;
          +}
          +
          +

          파일을 저장한 뒤 병합을 계속 진행한다. 마지막으로 해당 내용을 푸시한다.

          +
          $ git merge --continue
          +$ git push origin alice-branch
          +
          +

          이렇게 하면 Merge branch 'master' of https://github.com/bob/project와 같은 메시지를 가진 병합 커밋이 생긴다. 히스토리를 더 깔끔하게 만들고 싶다면 두 번째 방법을 사용하면 된다. 두 번째 방법은 Alice의 브랜치를 origin/master 브랜치의 최신 커밋에 리베이스하는 것이다.

          +
          $ git checkout alice-branch
          +$ git pull --rebase origin master
          +
          +

          bob/project의 master 브랜치를 가져와 최신 커밋에 Alice의 브랜치 alice-branch를 리베이스한다. 이제 Alice의 커밋은 master의 최신 커밋을 향해 한 발자국씩 나아간다.

          +
             ┌────C1───┐ (carol-branch)
          +───M1───M2───M3───M4 (master)
          +   └────A1 (alice-branch)
          +
          +

          원래 Alice의 브랜치가 M1 커밋을 베이스로 했기 때문에 다음 커밋인 M2로 리베이스된다.

          +
             ┌────C1───┐ (carol-branch)
          +───M1───M2───M3───M4 (master)
          +        └────A1 (alice-branch)
          +
          +

          이어서 M3 커밋에 리베이스된다.

          +
             ┌────C1───┐ (carol-branch)
          +───M1───M2───M3───M4 (master)
          +             └────F1 (alice-branch)
          +
          +

          M3 커밋에 리베이스하자 병합 충돌이 발생해 리베이스가 중단됐다. 첫 번째 방법과 마찬가지로 두 변경 사항 중 하나를 선택하고 저장해 병합 충돌을 해결한다. 이어서 --continue 옵션으로 리베이스를 계속 진행한다.

          +
          $ git rebase --continue
          +
          +

          마지막으로 Alice의 브랜치가 M4 커밋에 리베이스된다.

          +
             ┌────C1───┐ (carol-branch)
          +───M1───M2───M3───M4 (master)
          +                  └────F1 (alice-branch)
          +
          +

          M3 커밋에서 충돌이 있었으므로 그 다음 커밋인 M4에서도 같은 충돌이 발생한다. 앞선 방식과 똑같이 충돌을 해결하고, 리베이스를 마친 뒤 --force 또는 -f 옵션으로 푸시한다.

          +

          리베이스 방식을 사용할 때는 병합 충돌이 있는 모든 커밋들에 대해 충돌을 일일이 해결해줘야 한다. 즉, 충돌난 커밋이 너무 많을 때는 리베이스보다는 단순 병합하는 첫 번재 방법이 더 낫다. 할만할 것 같아서 시작했으나 도중에 무리라고 판단되면 git rebase --abort로 리베이스를 중단할 수 있다.

          +
          $ git rebase --continue
          +$ git push -f origin alice-branch
          +
          +

          --force 옵션으로 푸시하지 않으면 먼저 원격 저장소의 alice-branch를 풀해야 하는데, 그러면 로컬 저장소에서 리베이스한 커밋과 원격 저장소의 커밋이 중복되어 동일한 내용의 커밋이 생겨버린다.

          +

          다른 브랜치에서 특정 커밋 복사해오기

          +
            +
          • Alice는 featA 브랜치에서 index.js를 리팩토링해 커밋했다.
          • +
          • 이 커밋을 featB 브랜치로 복사하고자 한다.
          • +
          +

          먼저 featA 브랜치에서 복사할 커밋의 해시(4a391fc)를 확인한다.

          +
          $ git checkout featA
          +$ git reflog
          +183d9ba HEAD@{0}: commit: 버튼 동작 구현
          +4a391fc HEAD@{1}: commit: index.js 리팩토링
          +b51bf86 HEAD@{2}: commit: index.js 추가
          +
          +

          그리고 cherry-pick 명령으로 featB 브랜치에서 해당 커밋을 복사해온다.

          +
          $ git checkout featB
          +$ git cherry-pick 4a391fc
          +
          +

          브랜치 이름 수정하기

          +
            +
          • Alice는 feattA 브랜치에 오타가 있는 것을 발견했다.
          • +
          • feattA 브랜치의 이름을 featA로 수정하고자 한다.
          • +
          +

          branch 명령에 --move 또는 -m 옵션을 사용하면 간단하게 변경할 수 있다.

          +
          $ git branch -m feattA featA
          +
          +

          Git 명령어 축약하기

          +
            +
          • Alice는 김지현님 덕분에 git log --reflog --graph --oneline --decorate 명령을 쓰면 로그를 더 편하게 볼 수 있다는 사실을 알게됐다.
          • +
          • 하지만 명령이 너무 길어 축약해 사용하고자 한다.
          • +
          +

          .gitconfig 파일에서 alias를 설정해주면 된다. [alias] 섹션에 축약형과 축약할 명령을 작성하고 저장한다.

          +
          $ vim ~/.gitconfig
          +
          +
          [alias]
          +  lg = log --reflog --graph --oneline --decorate
          +
          +

          이제 git l 명령은 git log --reflog --graph --oneline --decorate와 같다. commit이나 checkout 등의 명령도 같은 방식으로 축약할 수 있다.

          +
          $ git lg
          +* 1d1eb90 (origin/master, origin/HEAD) 버튼 추가
          +|\
          +| * b0a574a (HEAD -> featB, origin/featB) 버튼 색상 변경
          +| * b2ff985 버튼 동작 구현
          +|/
          +* 02e5c44 (featA) 내비게이션바 추가
          +|\
          +
          +

          기계인간님의 편리한 git alias 설정하기를 참고하면 극한의 편리를 추구할 수도 있다.

          +

          참고자료

          + + +
          +
          + +
          + +
          +
          +

          💵 캐시가 동작하는 아주 구체적인 원리

          +

          하드웨어로 구현한 해시 테이블

          +
          +
          +
          + + +
          + +
          +
          +

          2학년 학부생의 신입 개발자 취업기

          +

          산업기능요원 결심부터 합격까지 3개월의 기록

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/29.html b/article/29.html new file mode 100644 index 0000000..da7fde1 --- /dev/null +++ b/article/29.html @@ -0,0 +1,268 @@ + + + + + + 💵 캐시가 동작하는 아주 구체적인 원리 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 💵 캐시가 동작하는 아주 구체적인 원리 +

          + +

          + 하드웨어로 구현한 해시 테이블 +

          + + + + +
          +
          Table of Contents +

          +
          +

          기술의 발전으로 프로세서 속도는 빠르게 증가해온 반면, 메모리의 속도는 이를 따라가지 못했다. 프로세서가 아무리 빨라도 메모리의 처리 속도가 느리면 결과적으로 전체 시스템 속도는 느려진다. 이를 개선하기 위한 장치가 바로 캐시(Cache)다.

          +

          캐시는 CPU 칩 안에 들어가는 작고 빠른 메모리다. (그리고 비싸다.) 프로세서가 매번 메인 메모리에 접근해 데이터를 받아오면 시간이 오래 걸리기 때문에 캐시에 자주 사용하는 데이터를 담아두고, 해당 데이터가 필요할 때 프로세서가 메인 메모리 대신 캐시에 접근하도록해 처리 속도를 높인다.

          +

          Principle of Locality

          +

          '자주 사용하는 데이터’에 관한 판단은 지역성의 원리를 따르며, 지역성 원리는 시간 지역성(Temporal locality)과 공간 지역성(Spatial locality)으로 구분해서 볼 수 있다.

          +

          시간 지역성은 최근 접근한 데이터에 다시 접근하는 경향을 말한다. 가령 루프에서 인덱스 역할을 하는 변수 i에는 짧은 시간안에 여러 번 접근이 이뤄진다.

          +
          for (i = 0; i < 10; i += 1) {
          +  arr[i] = i;
          +}
          +
          +

          공간 지역성은 최근 접근한 데이터의 주변 공간에 다시 접근하는 경향을 말한다. 위 루프의 경우 배열 arr의 각 요소를 참조하면서 가까운 메모리 공간에 연속적으로 접근하고 있다. 배열의 요소들이 메모리 공간에 연속적으로 할당되기 때문이다.

          +

          프로세스 실행 중 접근한 데이터의 접근 시점과 메모리 주소를 표시한 아래 그림은 시간 지역성과 공간 지역성의 특성을 잘 보여준다.

          +

          +

          한 프로세스 안에도 자주 사용하는 부분과 그렇지 않은 부분이 있기 때문에 운영체제는 프로세스를 페이지(Page)라는 단위로 나눠 관리하며, 위 그림은 페이지를 참조한 기록이다. 가로 축은 실행 시간이고, 세로 축은 메모리 주소다. 즉, 수평으로 이어진 참조 기록은 긴 시간에 걸쳐 같은 메모리 주소를 참조한 것이고, 수직으로 이어진 참조 기록은 같은 시간에 밀접한 메모리 주소들을 참조한 것이다. 페이지에 접근할 때도 지역성 원리가 적용된다는 것을 알 수 있다.

          +

          Caches

          +

          CPU 칩에는 여러 개의 캐시가 들어가며, 각각의 캐시는 각자의 목적과 역할을 가지고 있다.

          +
          +-------------+------+------+     +---------------+     +--------+
          +|             |  I$  |      | <-- |               | <-- |        |
          ++  Processor  +------+  L2  |     |  Main Memory  |     |  Disk  |
          +|             |  D$  |      | --> |               | --> |        |
          ++-------------+------+------+     +---------------+     +--------+
          +
          +
            +
          • L1 Cache: 프로세서와 가장 가까운 캐시. 속도를 위해 I$와 D$로 나뉜다. +
              +
            • Instruction Cache (I$): 메모리의 TEXT 영역 데이터를 다루는 캐시.
            • +
            • Data Cache (D$): TEXT 영역을 제외한 모든 데이터를 다루는 캐시.
            • +
            +
          • +
          • L2 Cache: 용량이 큰 캐시. 크기를 위해 L1 캐시처럼 나누지 않는다.
          • +
          • L3 Cache: 멀티 코어 시스템에서 여러 코어가 공유하는 캐시.
          • +
          +

          캐시에 달러 기호($)를 사용하는 이유는 캐시(Cache)의 발음이 현금을 뜻하는 'Cash’와 같기 때문이다 :)

          +

          +

          오늘날 CPU 칩의 면적 30~70%는 캐시가 차지한다. 1989년 생산된 싱글 코어 프로세서인 i486의 경우 8KB짜리 I/D 캐시 하나만 있었다. 한편 인텔 코어 i7 쿼드 코어 칩의 다이 맵(Die map)을 보면 4개의 코어에 각각 256KB L2 캐시가 있고, 모든 코어가 공유하는 8MB L3 캐시가 있는 것을 볼 수 있다. (L2 캐시 위에 있는 구역이 L1 캐시로 보이는데, 확실하지 않아서 따로 표시하지 않았다.)

          +

          Cache Metrics

          +

          캐시의 성능을 측정할 때는 히트 레이턴시(Hit latency)와 미스 레이턴시(Miss latency)가 중요한 요인으로 꼽힌다.

          +

          CPU에서 요청한 데이터가 캐시에 존재하는 경우를 캐시 히트(Hit)라고 한다. 히트 레이턴시는 히트가 발생해 캐싱된 데이터를 가져올 때 소요되는 시간을 의미한다. 반면 요청한 데이터가 캐시에 존재하지 않는 경우를 캐시 미스(Miss)라고 하며, 미스 레이턴시는 미스가 발생해 상위 캐시에서 데이터를 가져오거나(L1 캐시에 데이터가 없어서 L2 캐시에서 데이터를 찾는 경우) 메모리에서 데이터를 가져올 때 소요되는 시간을 말한다.

          +

          평균 접근 시간(Average access time)은 다음과 같이 구한다:

          +
          Miss rate=Cache missesCache acessesAverage access time=Hit latency+Miss rate×Miss latency +\begin{aligned} + \text{Miss rate} &= {\text{Cache misses} \over \text{Cache acesses}} \\ + \text{Average access time} &= \text{Hit latency} + \text{Miss rate} \times \text{Miss latency} +\end{aligned} +

          캐시의 성능을 높이기 위해서는 캐시의 크기를 줄여 히트 레이턴시를 줄이거나, 캐시의 크기를 늘려 미스 비율을 줄이거나, 더 빠른 캐시를 이용해 레이턴시를 줄이는 방법이 있다.

          +

          Cache Organization

          +

          캐시는 반응 속도가 빠른 SRAM(Static Random Access Memory)으로, 주소가 키(Key)로 주어지면 해당 공간에 즉시 접근할 수 있다. 이러한 특성은 DRAM(Dynamic Random Access Meomry)에서도 동일하지만 하드웨어 설계상 DRAM은 SRAM보다 느리다. 통상적으로 '메인 메모리’라고 말할 때는 DRAM을 의미한다.

          +

          주소가 키로 주어졌을 때 그 공간에 즉시 접근할 수 있다는 것은 캐시가 하드웨어로 구현한 해시 테이블(Hash table)과 같다는 의미다. 캐시가 빠른 이유는 자주 사용하는 데이터만을 담아두기 때문이기도 하지만, 해시 테이블의 시간 복잡도가 O(1)O(1) 정도로 빠르기 때문이기도 하다.

          +

          캐시는 블록(Block)으로 구성되어 있다. 각각의 블록은 데이터를 담고 있으며, 주소값을 키로써 접근할 수 있다. 블록의 개수(Blocks)와 블록의 크기(Block size)가 캐시의 크기를 결정한다.

          +

          Indexing

          +

          주소값 전체를 키로 사용하지는 않고, 그 일부만을 사용한다. 가령 블록 개수가 1024개이고, 블록 사이즈가 32바이트일 때, 32비트 주소가 주어진다면 다음과 같이 인덱싱 할 수 있다.

          +

          +

          전체 주소에서 하위 5비트를 오프셋(Offset)으로 쓰고, 이후 10비트를 인덱스(Index)로 사용하여 블록에 접근했다. 인덱스가 10비트인 이유는 2n2^n개 블록의 모든 인덱스를 표현하기 위해서는 log2blockslog{_2}\text{blocks}만큼의 비트가 필요하기 때문이다. 여기에선 블록 개수가 210=10242^{10} = 1024개이므로, log21024=10log{_2} 1024 = 10이 되어 10비트가 인덱스 비트로 사용되었다. (오프셋 비트에 대해서는 아래에서 설명하겠다.)

          +

          그러나 이렇게만 하면 서로 다른 데이터의 인덱스가 중복될 위험이 너무 크다.

          +

          Tag Matching

          +

          인덱스의 충돌을 줄이기 위해 주소값의 일부를 태그(Tag)로 사용한다. 블록 개수가 1024개이고, 블록 사이즈가 32바이트일 때, 32비트 주소 0x000c14B8에 접근한다고 가정해보자:

          +

          +
            +
          1. 먼저 인덱스(0010100101)에 대응하는 태그 배열(Tag array)의 필드에 접근한다.
          2. +
          3. 이어서 해당 태그 필드의 유효 비트(Valid bit)를 확인한다.
          4. +
          5. 유효 비트가 1이라면 태그 필드(00000000000011000)와 주소의 태그(00000000000011000)가 같은지 비교한다.
          6. +
          7. 비교 결과(true, 1)를 유효 비트(1)와 AND 연산한다.
          8. +
          +

          유효 비트가 1이라는 것은 해당 블록에 올바른 값이 존재한다는 의미다. 태그 필드와 주소의 태그가 같고, 유효 비트도 1이므로 위 예시의 결과는 히트다. 히트가 발생하면 데이터 배열(Data array)에서 해당 인덱스의 데이터를 참조한다. (참고로 데이터 배열과 태그 배열도 모두 하드웨어다.)

          +

          만약 유효 비트가 0이라면 블록에 값이 없거나 올바르지 않다는 뜻이므로 미스가 발생한다. 그러면 주소의 태그를 태그 필드에 작성하고, 데이터 필드에도 상위 캐시나 메모리에서 요청한 값을 가져와 작성한 뒤, 유효 비트를 1로 바꿔준다.

          +

          유효 비트가 1이라도 태그가 일치하지 않으면 미스가 발생한다. 이 경우 교체 정책(Replacement policy)에 따라 처리가 달라진다. 먼저 입력된 데이터가 먼저 교체되는 FIFO(First-In First-Out) 정책을 사용한다면 무조건 기존 블록를 교체한다. 태그 배열의 필드를 주소의 태그로 바꾸고, 상위 캐시나 메모리에서 요청한 데이터를 가져와 데이터 필드의 값도 새 데이터로 바꿔준다. (실제로는 요청한 데이터뿐 아니라 그 주변 데이터까지 가져온다.) 기존 데이터는 상위 캐시로 밀려난다.

          +

          주소의 상위 15비트가 태그 비트로 사용된 이유는 태그 비트가 Address bits(log2Block size+Index bits)\text{Address bits} - (\log{_2}\text{Block size} + \text{Index bits})로 결정되기 때문이다. 이 경우 32(5+10)=1732 - (5 + 10) = 17 비트가 태그 비트로 사용되었고, 남은 5비트는 오프셋 비트로 사용되었다.

          +

          Tag Overhead

          +

          태그 배열이 추가되면서 더 많은 공간이 필요하게 되었다. 하지만 여전히 '32KB 캐시’는 32KB 데이터를 저장할 수 있는 캐시라는 의미다. 태그를 위한 공간은 블록 크기와 상관없는 오버헤드(Overhead)로 취급하기 때문이다.

          +

          1024개의 32B 블록으로 구성된 32KB 캐시의 태그 오버헤드를 구해보자:

          +
          17bit tag+1bit valid=18bit18bit×1024=18Kb tags=2.25KB +\begin{aligned} + 17 \text{bit tag} + 1 \text{bit valid} &= 18 \text{bit} \\ + 18 \text{bit} \times 1024 = 18 \text{Kb tags} &= 2.25 \text{KB} +\end{aligned} +

          즉, 7%의 태그 오버헤드가 발생했다.

          +

          공간뿐 아니라 시간 비용도 발생한다. 태그 배열에 접근해 히트를 확인하고, 그 이후에 데이터 배열에 접근해 데이터를 가져오기 때문에 결과적으로 히트 레이턴시가 증가하게 된다.

          +

          그래서 두 과정을 병렬적으로 실행한다. 태그 배열에서 히트 여부를 확인하는 동시에 미리 데이터 배열에 접근하는 것이다. 이렇게 하면 히트 레이턴시가 줄어들지만, 미스가 발생했을 때의 리소스 낭비를 감수해야 한다.

          +

          Associative Cache

          +

          서로 다른 두 주소가 같은 인덱스를 가지면 충돌이 발생하고, 교체 정책에 따라 블록을 교체한다. 하지만 충돌이 발생할 때마다 캐시 내용을 바꾸면 더 많은 미스가 발생하게 되고, 한 자리의 내용을 끝없이 바꾸는 핑퐁 문제(Ping-pong problem)가 일어날 수 있다.

          +

          이 문제는 태그 배열과 데이터 배열을 여러 개 만드는 식으로 개선할 수 있다. 즉, 인덱스가 가리키는 블록이 여러 개가 되는 것이다. 인덱스가 가리키는 블록의 개수에 따라 캐시의 종류를 분류하면 아래와 같다:

          +
            +
          • Direct mapped: 인덱스가 가리키는 공간이 하나인 경우. 처리가 빠르지만 충돌 발생이 잦다.
          • +
          • Fully associative: 인덱스가 모든 공간을 가리키는 경우. 충돌이 적지만 모든 블록을 탐색해야 해서 속도가 느리다.
          • +
          • Set associative: 인덱스가 가리키는 공간이 두 개 이상인 경우. n-way set associative 캐시라고 부른다.
          • +
          +

          direct mapped 캐시와 fully associative 캐시 모두 장단점이 극단적이기 때문에 보통은 set associative 캐시를 사용한다.

          +

          Set Associative Cache Organization

          +

          간단하게 2-way set associative 캐시의 동작을 살펴보자:

          +

          +

          주소의 인덱스를 통해 블록에 접근하는 것은 지금까지 본 direct mapped 캐시와 동일하다. 다만 2개의 웨이(Way)가 있기 때문에 데이터가 캐싱되어 있는지 확인하려면 하나의 블록만이 아닌 2개의 블록을 모두 확인해야 한다. 마지막으로 두 웨이의 결과를 OR 연산하면 최종 결과를 낼 수 있다. 모든 웨이에서 미스가 발생하면 교체 정책에 따라 2개의 블록 중 한 곳에 데이터를 작성한다.

          +

          direct mapped 캐시와 비교해서 히트 레이턴시를 높이는 대신 충돌 가능성을 줄인 것이다.

          +

          Concrete Example

          +

          2바이트 캐시 블록으로 구성된 8바이트 2-way 캐시가 있고, 4비트 주소가 주어진다고 가정해보자.

          +

          +

          메모리 주소 0001을 참조하는 명령이 실행되었다. 인덱스 비트는 log22=1\log{_2} 2 = 1이고, 태그 비트는 4(log22+1)=24 - (log{_2} 2 + 1) = 2이다. 마지막으로 오프셋 비트는 1이 된다. 따라서 주소 0001의 인덱스는 0, 태그는 00이다. 즉, 해당 메모리 공간에 위치한 데이터는 인덱스가 0인 두 공간 중 한 곳에 캐싱될 수 있다.

          +

          +

          Way 0의 블록이 비어있으므로 Way 0에서 인덱스가 0인 블록에 데이터를 저장했다. 이때 주소 0010도 같이 캐싱되었는데, 이는 캐시 히트 비율을 높이기 위해 메모리에서 한 번에 캐시 블록 크기(2B)만큼 데이터를 가져오기 때문이다. (공간 지역성을 활용한 것이다.) 여기서는 참조한 데이터(0001)의 주변 공간인 0010을 함께 캐싱했다.

          +

          +

          메모리 주소 0101을 참조하는 명령이 실행되었다. 이번에도 인덱스가 0인 두 공간에 들어갈 수 있다.

          +

          +

          하지만 Way 0에서 인덱스가 0인 블록은 이미 데이터를 가지고 있기 때문에 데이터가 없는 Way 1에 캐싱된다. 이번에도 옆에 있는 0110 데이터가 함께 캐싱되었다. 또한 Way 0에 속한 두 블록의 LRU(Least Recently Used) 값이 증가했다. LRU는 사용한지 더 오래된 데이터를 우선적으로 교체하는 정책으로, 운영체제의 프로세스 스케줄링이나 페이지 교체 알고리즘으로도 사용된다. LRU 값이 증가했다는 것은 캐시 미스가 발생했을 때 우선적으로 교체될 가능성이 높아졌다는 의미다.

          +

          +

          이어서 메모리 주소 1000을 참조하는 명령이 실행되었다. 인덱스가 0인 두 블록을 확인했으나, 태그가 10인 데이터가 없어 캐스 미스가 발생했다.

          +

          +

          결국 캐싱된 데이터를 교체한다. 두 공간 중 Way 0 블록의 LRU 값이 더 크기 때문에 Way 0의 첫 번째 블록이 교체 되었고, 참조가 일어났으므로 LRU 값을 0으로 초기화했다. (캐시 히트가 발생하는 경우에도 LRU 값을 초기화한다.) 참조한 데이터의 주변 공간인 0111의 데이터도 같은 원리로 캐싱했다. 이때 Way 1에 속한 두 블록은 참조되지 않았기 때문에 LRU 값이 증가했다.

          +

          Handling Cache Writes

          +

          데이터를 읽는 동작이 아니라 입력하는 동작이 발생하고, 데이터를 변경할 주소가 캐싱된 상태(Write hit)라면 메모리의 데이터가 업데이트되는 대신 캐시 블록의 데이터가 업데이트된다. 이제 '캐시에서 업데이트된 데이터를 언제 메모리에 쓸 것인가?'에 관한 문제가 생긴다. 여기 두 가지 쓰기 정책(Write policies)이 있다.

          +

          하나는 Write-through 방식이다. 캐시에 데이터가 작성될 때마다 메모리의 데이터를 업데이트하는 것이다. 이렇게 하면 많은 트래픽이 발생하지만, 메모리와 캐시의 데이터를 동일하게 유지할 수 있다.

          +

          또 다른 방식은 Write-back 방식이다. 이 방식은 블록이 교체될 때만 메모리의 데이터를 업데이트한다. 데이터가 변경됐는지 확인하기 위해 캐시 블록마다 dirty 비트를 추가해야 하며, 데이터가 변경되었다면 1로 바꿔준다. 이후 해당 블록이 교체될 때 dirty 비트가 1이라면 메모리의 데이터를 변경하는 것이다.

          +

          데이터를 변경할 주소가 캐싱된 상태가 아니라면(Write miss) Write-allocate 방식을 사용한다. 당연한 얘기지만, 미스가 발생하면 해당 데이터를 캐싱하는 것이다. write-allocate를 하지 않는다면 당장은 리소스를 아낄 수 있겠지만 캐시의 목적을 달성하지는 못할 것이다.

          +

          Software Restructuring

          +

          여기까지 로우 레벨에서의 캐시 구조와 동작을 살펴봤는데, 코드 레벨에서 캐시의 효율을 증가시킬 수도 있다. 다음 이중 루프를 보자:

          +
          for (i = 0; i < columns; i += 1) {
          +  for (j = 0; j < rows; j += 1) {
          +    arr[j][i] = pow(arr[j][i]);
          +  }
          +}
          +
          +

          2차원 배열의 모든 요소를 제곱하는 루프다. 큰 문제가 없어보이지만 공간 지역성을 따져보면 비효율적인 코드다. 배열 arr의 요소들은 메모리에 연속적으로 저장되는데, 접근은 순차적으로 이뤄지지 않기 때문이다.

          +
          0          4          8          12         16         20         24
          ++----------+----------+----------+----------+----------+----------+
          +|  [0, 0]  |  [0, 1]  |  [0, 2]  |  [1, 0]  |  [1, 1]  |  [1, 2]  |
          ++----------+----------+----------+----------+----------+----------+
          +
          +
            +
          1. i = 0, j = 0: 첫 번째 공간 [0, 0]에 접근한다.
          2. +
          3. i = 0, j = 1: 네 번째 공간 [1, 0]에 접근한다.
          4. +
          5. i = 1, j = 0: 두 번째 공간 [0, 1]에 접근한다.
          6. +
          +

          이처럼 공간을 마구 건너뛰며 접근하게 된다. 따라서 아래 코드처럼 외부 루프는 rows를, 내부 루프는 columns를 순회해야 한다.

          +
          for (i = 0; i < rows; i += 1) {
          +  for (j = 0; j < columns; j += 1) {
          +    arr[i][j] = pow(arr[i][j]);
          +  }
          +}
          +
          +

          비슷하지만 시간 지역성을 활용하는 예시도 있다. 다음 루프를 보자:

          +
          for (i = 0; i < n; i += 1) {
          +  for (j = 0; j < len; j += 1) {
          +    arr[j] = pow(arr[j]);
          +  }
          +}
          +
          +

          배열 arr의 모든 요소를 제곱하는 동작을 n회 반복하는 이중 루프다. 현재 코드는 데이터를 캐싱한 뒤, 다음 접근 때 캐싱한 데이터에 접근한다는 보장이 없다. 전체 데이터가 캐시 크기보다 크다면 배열을 순회하는 과정은 아래 그림과 같을 것이다.

          +

          +

          루프의 전반부에서 데이터를 캐싱하지만, 루프가 끝날 때 캐시는 후반부에 접근한 데이터로 덮어씌워진 상태가 된다. 그래서 두 번째 루프를 돌 때는 전반부 데이터를 다시 캐싱해야 한다. 캐싱한 데이터에 다시 접근하기도 전에 캐시 블록 전체가 교체되어 버리는 것이다. 배열 순회 주기를 캐시 크기만큼 끊어주면 문제를 해결할 수 있다.

          +

          +

          루프를 도는 횟수는 늘었지만 캐시 히트 비율은 더 높아졌다. 세 번째 루프까지는 전반부 데이터만 처리하고, 그 이후로는 후반부 데이터만 처리하는 것이다. 코드로 구현하면 삼중루프(!)가 된다:

          +
          for (i = 0; i < len; i += CACHE_SIZE) {
          +  for (j = 0; j < n; j += 1) {
          +    for (k = 0; k < CACHE_SIZE; k += 1) {
          +      arr[i + k] = pow(arr[i + k]);
          +    }
          +  }
          +}
          +
          +

          물론 요즘은 컴파일러가 모두 최적화를 해주기 때문에 사용자 어플리케이션 개발자가 이런 부분까지 신경쓸 필요는 없다. ‘로우 레벨에서 이런 식으로 코드를 최적화할 수 있구나’ 정도만 알고 있어도 괜찮을 것 같다.

          +

          References

          +
            +
          • David Patterson, John Hennenssy, “Computer Organization and Design 5th Ed.”, MK, 2014.
          • +
          • Abraham Silberschatz, Peter Galvin, Greg Gagne, “Operating System Concepts 9th Ed.”, Wiley, 2014.
          • +
          • K. G. Smitha, “Para Cache Simulator”.
          • +
          + +
          +
          + +
          + +
          +
          +

          하나의 타입에 강아지와 고양이 담기

          +

          파라미터의 다형성과 제네릭

          +
          +
          +
          + + +
          + +
          +
          +

          Git 사용 중 자주 만나는 이슈 정리

          +

          코딩보다 어려운 버전 관리

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/3.html b/article/3.html new file mode 100644 index 0000000..1743f7c --- /dev/null +++ b/article/3.html @@ -0,0 +1,234 @@ + + + + + + 프로세스간 통신을 활용해 프로그래밍하기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 프로세스간 통신을 활용해 프로그래밍하기 +

          + +

          + 학적 관리 프로그램 만들기 +

          + + + + +
          +
          Table of Contents +

          +
          +

          컴퓨터는 여러 개의 프로세스를 동시에 돌릴 수 있다. (사실 정확히 '동시에’는 아니다. 자세한 설명은 공룡책으로 정리하는 운영체제 Ch.1를 참고.) 그렇다면 하나의 프로그램이 여러 개의 프로세스로 메모리에 로드될 수 있을까? 당연히 된다. 두 개의 프로세스를 동시에 실행시며 하나의 목적을 달성할 수 있다. 리눅스 환경에서 parent 프로세스와 child 프로세스가 통신하는 학생 정보 관리 프로그램을 만들어보았다.

          +

          프로그램 구조

          +

          +

          프로그램은 parent와 child로 나뉜다. parent는 클라이언트로서 사용자에게 메뉴를 출력해주고, 값을 입력받는다. child는 서버로서 parent에게 데이터를 전송받아 student.data 파일에 데이터를 쓰거나 읽는다. 전체 실행 흐름을 도식화하면 다음 플로우 차트와 같다.

          +

          +

          서버와 클라이언트는 파이프(Pipe)를 통해 데이터를 주고 받는다. 파이프가 무엇인가? 말 그대로 parent와 child 사이에 관을 설치해 데이터를 주고받는 프로세스간 통신 기법을 말한다. 공룡책으로 정리하는 운영체제 Ch.3에 파이프에 대한 설명이 있다.

          +

          Child 프로세스 생성

          +

          처음 프로그램이 실행됐을 때 fork() 시스템콜을 통해 child 프로세스를 만들어야 한다.

          +
          #include <stdio.h>
          +#include <unistd.h>
          +#include <sys/wait.h>
          +
          +int main(int argc, char* argv[]) {
          +  pid_t pid = fork(); // child를 생성한다.
          +
          +  if (pid < 0) { // 에러
          +    printf("[CLIENT] Fork failed.\n");
          +    return 1;
          +  } else if (pid == 0) { // Child (Server)
          +    printf("[SERVER] Child created.\n");
          +    return 0;
          +  } else { // Parent (Client)
          +    wait(NULL); // child를 기다린다.
          +  }
          +  return 0;
          +}
          +
          +
          $ ./debug
          +[SERVER] Child Created.
          +
          +

          fork()를 수행하면 child 프로세스가 생성된다. fork 이후 코드는 모두 parent와 child에서 동시에 실행된다. parent는 wait(NULL)을 통해 child의 작업이 끝날 때까지 대기한다. 이를 이용해 parent에서 사용자로부터 데이터를 입력받고 child에서 출력해보도록 하자.

          +
          #include <stdio.h>
          +#include <unistd.h>
          +#include <sys/wait.h>
          +
          +int main(int argc, char* argv[]) {
          +  int cmd = 0;
          +  pid_t pid  = fork(); // child를 생성한다.
          +
          +  if (pid < 0) { // 에러
          +    printf("[CLIENT] Fork failed.\n");
          +    return 1;
          +  } else if (pid == 0) { // Child (Server)
          +    printf("[SERVER] Received %d.\n", cmd);
          +    return 0;
          +  } else { // Parent (Client)
          +    printf("[1] search [2] create\n");
          +    printf("> ");
          +
          +    scanf("%d", &cmd);
          +    wait(NULL); // child를 기다린다.
          +  }
          +  return 0;
          +}
          +
          +
          $ ./debug
          +[1] search [2] create
          +[SERVER] Received 0.
          +>
          +
          +

          안타깝지만 생각대로 동작하지 않는다. 사용자로부터 cmd의 값이 할당되는 부분은 parent이며, 변경된 cmd는 parent에만 존재한다. 따라서 동시에 실행되고 있는 child에서는 cmd의 값이 변경되지 않고 0이 출력된다. cmd 값을 child에게 전송하고, child가 cmd값을 수신할 때까지 대기하는 부분은 따로 구현해야 한다.

          +

          파이프 통신 구현

          +

          child에게 cmd 값을 전송하려면 파이프가 필요하다. 만들어보자.

          +
          #include <stdio.h>
          +#include <unistd.h>
          +#include <sys/wait.h>
          +#include <sys/types.h>
          +
          +int main(int argc, char* argv[]) {
          +  int cmd = 0;
          +  int p[2]; // p[0]: read, p[1]: write
          +
          +  pipe(p); // 파이프를 생성한다.
          +  pid_t pid  = fork(); // child를 생성한다.
          +
          +  if (pid < 0) { // 에러
          +    printf("[CLIENT] Fork failed.\n");
          +    return 1;
          +  } else if (pid == 0) { // Child (Server)
          +    printf("[SERVER] Received %d.\n", cmd);
          +    return 0;
          +  } else { // Parent (Client)
          +    printf("[1] search [2] create\n");
          +    printf("> ");
          +
          +    scanf("%d", &cmd);
          +    wait(NULL); // child를 기다린다.
          +  }
          +  return 0;
          +}
          +
          +

          파이프는 단방향 통신만 되기 때문에 읽기 파이프와 쓰기 파이프 두 개가 필요하다. 이제 두 개의 파이프를 만들었으니 child에게 cmd를 전송해보자.

          +
          #include <stdio.h>
          +#include <unistd.h>
          +#include <sys/wait.h>
          +#include <sys/types.h>
          +
          +int main(int argc, char* argv[]) {
          +  int cmd = 0;
          +  int p[2]; // p[0]: read, p[1]: write
          +
          +  pipe(p); // 파이프를 생성한다.
          +  pid_t pid  = fork(); // child를 생성한다.
          +
          +  if (pid < 0) { // 에러
          +    printf("[CLIENT] Fork failed.\n");
          +    return 1;
          +  } else if (pid == 0) { // Child (Server)
          +    read(p[0], &cmd, sizeof(cmd)); // 파이프로 전송된 값을 받는다.
          +    printf("[SERVER] Received %d.\n", cmd);
          +    return 0;
          +  } else { // Parent (Client)
          +    printf("[1] search [2] create\n");
          +    printf("> ");
          +    scanf("%d", &cmd);
          +
          +    write(p[1], &cmd, sizeof(cmd)); // 파이프를 통해 값을 전송한다.
          +    wait(NULL); // child를 기다린다.
          +  }
          +  return 0;
          +}
          +
          +
          $ ./debug
          +[1] search [2] create
          +> 1
          +[SERVER] Received 1.
          +
          +

          child는 생선된 이후 파이프를 통해 데이터가 전달되기를 기다린다. parent는 사용자에게 cmd 값을 입력받고 쓰기 파이프인 p[1]를 통해 cmd 값을 전송한다. 그리고 수신을 기다리던 child는 p[0]을 통해 전달된 cmd 값을 읽어들인다.

          +

          child 프로세스 생성과 파이프를 통한 두 프로세스의 통신. 핵심적인 부분은 이정도다. 사실 이후 내용은 프로세스를 다루는 것과는 너무 관련이 없어서 제외했다.

          + +
          +
          + +
          + +
          +
          +

          차이를 중심으로 살펴본 UI디자인과 UX디자인

          +

          UI는 심미성, UX는 사용성?

          +
          +
          +
          + + +
          + +
          +
          +

          ♻️ 자바는 어떻게 Garbage Collection을 할까?

          +

          오브젝트의 일생

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/30.html b/article/30.html new file mode 100644 index 0000000..b9a6334 --- /dev/null +++ b/article/30.html @@ -0,0 +1,190 @@ + + + + + + 하나의 타입에 강아지와 고양이 담기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 하나의 타입에 강아지와 고양이 담기 +

          + +

          + 파라미터의 다형성과 제네릭 +

          + + + + +
          +
          Table of Contents +

          +
          +

          여기 강아지와 고양이가 있다.

          +
          data class Dog(val name: String)
          +data class Cat(val name: String)
          +
          +

          identity 함수는 항상 파라미터를 그대로 반환하는 항등 함수다. identity(dog: Dog) 함수는 Dog 타입 인자를 받아 반환하며, identity(cat: Cat) 함수는 Cat 타입 인자를 받아 반환한다.

          +
          fun identity(dog: Dog) = dog
          +fun identity(cat: Cat) = cat
          +
          +
          val dog = Dog("Jake")
          +identity(dog) // Dog("Jake")
          +
          +val cat = Cat("Cake")
          +identity(cat) // Cat("Cake")
          +
          +

          만약 Fox 타입에 대한 항등 함수를 사용하려 한다면 타입 에러가 발생할 것이다. Fox 타입을 인자로 받는 identity 함수를 정의하지 않았기 때문이다.

          +
          data class Fox(val name: String)
          +
          +
          val fox = Fox("Nick")
          +identity(fox) // None of the following functions can be called with the arguments supplied:
          +              // public fun identity(cat: Cat): Cat defined in root package in file File.kt
          +              // public fun identity(dog: Dog): Dog defined in root package in file File.kt
          +
          +

          새로운 타입이 추가될 때마다 인자의 타입만 다르고 똑같은 동작을 하는 identity 함수를 만드는 것은 효율적이지 않다. 이때 다형성(Polymorphism)으로 문제를 해결할 수 있다.

          +

          파라미터의 다형성

          +

          다형성은 하나의 엔티티를 여러 타입으로 사용할 수 있게 해준다. 다형성에는 다양한 종류가 있지만, 여기서는 파라미터의 다형성(Parametric polymorphism)에 대해서만 다룬다.

          +

          파라미터의 다형성은 표현식을 값이 아닌 타입으로 파라미터화(Parameterization)시킨다. 파라미터화는 함수가 파라미터를 이용해 표현식을 추상화하는 것을 말한다. 가령 fun twice(x: Int) = x + x 함수는 x + x라는 표현식을 파라미터 x로 파라미터화한 것이다. 이때 42 + 42twice(42)로 추상화된다. 함수는 파라미터를 실제 값으로 대체한다. twice 함수는 파라미터 x42라는 실제 값으로 파라미터화했다. 한편 타입으로 파라미터화를 하면 타입 파라미터를 실제 타입으로 대체하게 된다. 이를 함수와 구분하기 위해 타입 추상화(Type abstraction)라는 용어를 사용한다.[1]

          +

          앞서 본 항등 함수를 타입 파라미터 T를 이용하여 타입으로 파라미터화하면 아래와 같이 작성할 수 있다. 객체 지향 프로그래밍에서는 이런 식의 파라미터의 다형성을 제네릭(Generic)이라고 부른다.

          +
          fun <T> identity(x: T) = x
          +
          +

          타입 파라미터 T는 함수를 사용하는 시점에 실제 타입이 특정되며, Dog, Cat, Fox 뿐 아니라 Int, String 등 어떤 타입이든 적용할 수 있다. 더 이상 타입별로 identity 함수를 만들 필요가 없는 것이다.

          +
          val dog = Dog("Jake")
          +identity(dog) // Dog("Jake")
          +
          +val fox = Fox("Nick")
          +identity(fox) // Fox("Nick")
          +
          +val num = 10
          +identity(10) // 10
          +
          +

          제네릭으로 인한 성능 저하

          +

          타입 파라미터를 사용할 때마다 타입 캐스팅이 발생한다면 런타임 성능을 우려할 수도 있다. JVM은 타입 소거(Type erasure)를 통해 런타임에 타입 정보를 제거함으로써 제네릭을 구현한다. 모든 타입 파라미터는 Object로 취급되며, 타입 파라미터 T의 구체적인 타입을 런타임에 알 수 없다. 이렇게 JVM은 제네릭이 없던 시절에 작성된 코드에 대한 하위호환성을 보장하는 동시에 제네릭의 런타임 오버헤드를 해소한다.

          +
          fun <T> Any.isT() = this is T // Cannot check for instance of erased type: T
          +
          +

          인라인 함수의 경우 컴파일 타임에 함수의 내용이 사용처에 인라이닝되기 때문에 이러한 문제를 피할 수 있다. 코틀린은 인라인 함수를 사용할 때 런타임에 타입 파라미터의 구체적인 타입을 명시하는 reified 키워드를 지원한다.

          +
          inline fun <reified T> Any.isT() = this is T
          +
          +

          러스트의 경우 단형성화(Monomorphization)을 통해 제네릭의 런타임 오버헤드를 해소한다. 단형성화는 컴파일 타임에 제네릭 코드의 사용처를 바탕으로 구체적인 타입을 가진 코드를 생성하고, 이를 사용하도록 변경하는 과정을 말한다. 가령 아래와 같은 제네릭 구조체 Point<T>의 타입 파라미터에 i32, f64 타입을 전달해 사용한다고 가정하자.

          +
          struct Point<T> {
          +    x: T,
          +    y: T,
          +}
          +
          +fn main() {
          +    let integer = Point { x: 5, y: 10 };
          +    let float = Point { x: 1.0, y: 4.0 };
          +}
          +
          +

          컴파일러가 단형성화를 수행하면 아래와 같이 구체적인 타입을 명시한 구조체를 만들어 사용하게 된다. 이로써 러스트에서는 제네릭으로 인한 런타임 성능 저하가 발생하지 않는다.

          +
          struct Point_i32 {
          +    x: i32,
          +    y: i32,
          +}
          +
          +struct Point_f64 {
          +    x: f64,
          +    y: f64,
          +}
          +
          +fn main() {
          +    let integer = Point_i32 { x: 5, y: 10 };
          +    let float = Point_f64 { x: 1.0, y: 4.0 };
          +}
          +
          +

          References

          + +
          +
          +
            +
          1. Jaemin Hong, Sukyoung Ryu, “Introduction to Programming Languages”, 2021, pp. 21. ↩︎

            +
          2. +
          +
          + +
          +
          + +
          + +
          +
          +

          👻 CPU 보안 취약점을 공격하는 아주 구체적인 원리

          +

          멜트다운, 스펙터 페이퍼 읽기

          +
          +
          +
          + + +
          + +
          +
          +

          💵 캐시가 동작하는 아주 구체적인 원리

          +

          하드웨어로 구현한 해시 테이블

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/31.html b/article/31.html new file mode 100644 index 0000000..94d015d --- /dev/null +++ b/article/31.html @@ -0,0 +1,246 @@ + + + + + + 👻 CPU 보안 취약점을 공격하는 아주 구체적인 원리 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 👻 CPU 보안 취약점을 공격하는 아주 구체적인 원리 +

          + +

          + 멜트다운, 스펙터 페이퍼 읽기 +

          + + + + +
          +
          Table of Contents +

          +
          +

          지난 5월 인텔 CPU의 새로운 보안 취약점이 보고됐다. MDS(Microarchitectural Data Sampling)라고 불리는 이 취약점은 2018년 초 보고된 멜트다운(Meltdown), 스펙터(Spectre) 취약점과 달리 CPU 내부 3개의 버퍼(Line Fill Buffers, Load Ports, Store Buffers)로부터 사적인 데이터를 유출할 수 있다. MDS에 대해 공부하기 전 (뒤늦게) 멜트다운과 스펙터에 대해 알아보기로 했다.

          +

          Meltdown

          +

          +

          멜트다운은 인텔 프로세서에서 발견된 취약점이다. 공격자는 비순차적 명령 실행(Out of Order Execution)의 맹점을 이용해 접근할 수도, 접근해서도 안되는 데이터에 접근하고, 캐시와 같은 사이드 채널을 통해 정보를 알아낸다.

          +

          Background

          +

          비순차적 명령 실행에 대해 다루기 전에 파이프라인(Pipeline)에 대해 간단히 이해해야 한다. 우선 프로세서가 명령을 처리하는 과정은 다섯 부분으로 나눌 수 있다.

          +
            +
          1. IF (Instruction Fetch): 바이너리 형식의 명령을 메모리에서 CPU로 가져온다.
          2. +
          3. ID (Instruction Decode): 명령의 종류를 알아내고 레지스터에 입력한다.
          4. +
          5. EX (Execute): 명령의 데이터를 연산한다.
          6. +
          7. MEM (Memeory): 필요한 경우 메모리에 접근한다.
          8. +
          9. WB (Write Back): 연산 결과를 다시 레지스터에 입력한다.
          10. +
          +

          하드웨어 레벨에서 프로세서의 데이터패스(Datapath)를 도식화하면 아래 그림처럼 된다:

          +

          +

          메모리에서 프로세서로 바이너리 형식 데이터를 가져와 명령의 종류를 알아내고, 값을 적절한 레지스터에 저장한다. (컴퓨터가 코드를 읽는 아주 구체적인 원리에서 바이너리 데이터를 읽는 방식을 간단히 다뤘다.) 명령이 조건문이라면 레지스터의 값이 참인지 확인하는 과정을 거쳐 다른 코드로 점프하고, 조건문이 아니라면 ALU(Arithmetic Logic Unit)가 연산을 수행한다. 이어서 명령에 따라 메모리의 데이터를 가져오거나, 메모리에 데이터를 입력한다. 메모리에 접근할 필요가 없는 명령이라면 연산 결과를 다시 레지스터에 저장한다.

          +

          만약 한 명령이 ID 단계에 있다면 레지스터만 동작하고, 메모리나 ALU같은 요소들은 사용되지 않을 것이다. 프로세서의 일부만 사용하는 것은 비효율적이다. 한 명령이 ID 단계에 있을 때 다른 명령은 IF, 또 다른 명령은 EX 단계를 거칠 수 있다. 이렇게 병렬적으로 명령을 실행할 수 있도록 만든 것이 파이프라인(Pipeline)이며, 위 그림에서는 4개의 녹색 막대로 표현되었다. 파이프라인은 병렬적으로 실행되고 있는 각 단계의 명령들이 서로 데이터를 공유할 수 있도록 해준다.

          +

          파이프라인 덕분에 프로세서는 한 클럭 사이클(Clock cycle)에 최대 5개 명령을 동시에 실행할 수 있다.

          +
          +---+------+------+------+------+------+------+------+------+------+
          +| 1 |  IF  |  ID  |  EX  | MEM  |  WB  |                           |
          ++---+------+------+------+------+------+------+------+------+------+
          +| 2 |      |  IF  |  ID  |  EX  | MEM  |  WB  |                    |
          ++---+------+------+------+------+------+------+------+------+------+
          +| 3 |             |  IF  |  ID  |  EX  | MEM  |  WB  |             |
          ++---+------+------+------+------+------+------+------+------+------+
          +| 4 |                    |  IF  |  ID  |  EX  | MEM  |  WB  |      |
          ++---+------+------+------+------+------+------+------+------+------+
          +| 5 |                           |  IF  |  ID  |  EX  | MEM  |  WB  |
          ++---+------+------+------+------+------+------+------+------+------+
          +
          +

          다섯 번째 클럭 사이클에서 명령1이 WB 단계에 있을 때 명령2는 MEM, 명령3은 EX, 명령4는 ID, 명령5는 IF 단계를 거친다. 이것만으로 상당히 효율적이지만, 성능이 저하되는 상황도 있다.

          +
          1:  lw $t0, 0($sp)
          +2:  lw $t1, 4($sp)
          +3:  and $s1, $t0, $t1
          +4:  add $s2, $t0, $s1
          +5:  addi $s3, $t0, 20
          +
          +

          메모리에서 데이터를 가져와 $t0 레지스터에 저장하는 명령1과 $t0를 이용해 덧셈을 하는 명령3, 명령4, 명령5는 모두 의존성을 가지고 있다. 한편 $t1에 메모리의 데이터를 저장하는 명령2는 $t1을 이용해 덧셈을 하는 명령3과 의존성을 가진다.

          +

          순차적으로 명령을 실행한다면 1, 2, 3, 4, 5 순서가 맞다. 하지만 이렇게 하면 메모리에서 데이터를 로드하는 시간, 연산 결과를 레지스터에 작성하는 시간 동안 지연이 발생한다.

          +
          +---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +| 1 |  IF  |  ID  |  EX  | MEM  |  WB  |                                                       |
          ++---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +| 2 |      |  IF  |  ID  |  EX  | MEM  |  WB  |                                                |
          ++---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +| 3 |             |  IF  |  ID  |             |  EX  | MEM  |  WB  |                           |
          ++---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +| 4 |                                  |  IF  |  ID  |             |  EX  | MEM  |  WB  |      |
          ++---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +| 5 |                                                       |  IF  |  ID  |  EX  | MEM  |  WB  |
          ++---+------+------+------+------+------+------+------+------+------+------+------+------+------+
          +
          +

          명령 5는 명령 1과 의존성이 있지만, 다른 명령들과는 의존성이 없다. 따라서 명령의 실행 순서가 달라져도 같은 결과를 보장할 수 있다.

          +
          +---+------+------+------+------+------+------+------+------+------+------+------+
          +| 1 |  IF  |  ID  |  EX  | MEM  |  WB  |                                         |
          ++---+------+------+------+------+------+------+------+------+------+------+------+
          +| 2 |      |  IF  |  ID  |  EX  | MEM  |  WB  |                                  |
          ++---+------+------+------+------+------+------+------+------+------+------+------+
          +| 3 |             |  IF  |  ID  |             |  EX  | MEM  |  WB  |             |
          ++---+------+------+------+------+------+------+------+------+------+------+------+
          +| 5 |                                  |  IF  |  ID  |  EX  | MEM  |  WB  |      |
          ++---+------+------+------+------+------+------+------+------+------+------+------+
          +| 4 |                                         |  IF  |  ID  |  EX  | MEM  |  WB  |
          ++---+------+------+------+------+------+------+------+------+------+------+------+
          +
          +

          1, 2, 3, 5, 4 순서로 실행하니 2 클럭 사이클이 줄었다. 이처럼 프로세서는 성능을 위해 코드의 순서를 바꾼다. 순서를 바꿔 실행하는 것을 비순차적 명령 실행이라고 하며, 인텔은 의존성이 없는 명령을 같은 클럭 사이클에 실행하는 수퍼스칼라(Superscalar) 기술을 사용한다. 가령 n-way 수퍼스칼라의 경우 같은 클럭 사이클에 IF 단계의 명령을 n개 동시 처리할 수 있다.

          +

          Attack Overview

          +

          멜트다운 취약점 공격은 기본적으로 실행해서는 안되는 코드를 비순차적 실행으로 실행시키고, 이때 캐시에 올라간 데이터에 접근해 사적인 정보를 알아내는 방법을 사용한다.

          +

          A Toy Example

          +

          비순차적 실행의 취약점을 보여주는 간단한 예시 코드를 보자:

          +
          raise_exception();
          +// the line below is never reached
          +access(probe_array[data * 4096]);
          +
          +

          data는 접근할 수 없는 메모리 공간을 가리키는 값이며, 4096은 페이지 크기다. 공격자는 data에 4KB 페이지 사이즈를 곱해 data번째 페이지의 베이스 주소에 접근한다. (페이지란 메모리를 효율적으로 사용하기 위해 프로세스를 여러 조각으로 나눈 단위이다.) 즉, 공격자는 probe_array 배열을 이용해 접근할 수 없는 공간에 접근을 시도한다.

          +

          위 코드에서는 raise_exception이 항상 예외를 일으키기 때문에 이론적으로 access 함수는 절대로 실행되지 않는다. 그런데 비순차적 명령 실행에 의해 raise_exception 함수보다 access 함수가 먼저 실행되어 허용되지 않은 메모리 공간에 접근할 수 있다.

          +

          예외가 발생하면 프로세서는 비순차적으로 실행한 명령을 취소하지만, 이때는 캐시에 이미 시크릿 바이트(Secret byte) data * 4096이 올라간 상태다. 따라서 캐시에 저장된 시크릿 바이트를 찾으면 데이터를 알 수 있다. (캐시에 관한 보다 자세한 내용은 캐시가 동작하는 아주 구체적인 원리를 참고.) 이제 공격자는 프로세스의 모든 페이지에 하나씩 접근하며 시간을 측정한다.

          +

          +

          페이지의 베이스 주소(Base address)가 0 * 4096인 것부터 1 * 4096, 2 * 4096, ... , 255 * 4096까지 접근하면 캐싱된 페이지만 접근 시간(Access time)이 유난히 짧은 것을 볼 수 있다. 이것이 공격자가 노리는 data번째 페이지이며, 이를 통해 data의 값을 알게 된다. 만약 공격자가 노리는 data의 값이 'A’였다면 65번째 페이지에 접근하는 시간이 가장 짧게 나타났을 것이다. (아스키 코드상 'A’의 10진수 값이 65이기 때문이다.)

          +

          캐시를 비우고(Flush) 읽는(Reload) 방식으로 정보를 알아내는 공격법을 Flush-Reload 공격법이라고 한다. 앞서 캐시를 비우는 과정은 생략했는데, 만약 따로 캐시를 비우지 않으면 타겟 데이터가 어떤 것인지 분간할 수 없었을 것이다.

          +

          Attack Description

          +

          멜트다운 취약점 공격의 핵심 코드를 x86 어셈블리 명령으로 표현하면 아래와 같다:

          +
          ; rcx = kernel address, rbx = probe array
          +xor rax, rax
          +retry:
          +mov al, byte [rcx]
          +shl rax, 0xc
          +jz retry
          +mov rbx, qword [rbx + rax]
          +
          +

          rcx 레지스터는 커널 메모리 주소를 담고 있으며, rbx 레지스터는 시크릿 바이트를 찾기 위한 배열로 사용한다.

          +
            +
          1. 먼저 mov al, byte [rcx] 명령으로 커널 메모리에서 특정 값(rcx)을 로드해 al로 표현된 rax 레지스터에 저장한다. rax 레지스터는 x86 아키텍처에서 사용되는 64비트 범용 레지스터이며, alrax 레지스터의 하위 8비트 서브레지스터(Subregister)다. 누산기 레지스터(Accumulator register)라고 부르는 rax 레지스터는 산술 연산의 결과를 저장할 때 사용한다.
          2. +
          3. 유저 모드(User mode)에서는 커널 메모리에 접근할 수 없기 때문에 예외가 발생하고, 실행 결과는 반영되지 않은 채 취소된다. 예외가 발생하면 다음 명령들이 실행되지 않아야 하지만 비순차적 명령 실행에 의해 실제로는 다음 명령들이 실행될 수 있다.
          4. +
          5. 비순차적 명령 실행으로 다음 명령인 shl rax, 0xc 명령을 실행하면 rax 레지스터의 값에 페이지 사이즈인 0xc(4096B, 4KB)를 곱한다.
          6. +
          7. 이어서 mov rbx, qword [rbx + rax] 명령을 통해 rbx의 베이스 주소에 rax * 0xc를 더하고, 이 결과를 rbx 레지스터에 저장한다. rbx 레지스터는 베이스 레지스터(Base register)로, 스택의 베이스 주소를 가리킨다.
          8. +
          9. rbx 레지스터에는 rbx + rax * 0xc가 저장되어 있다. 즉, rax번째 페이지의 베이스 주소가 저장되어 있는 것이다.
          10. +
          11. 예외가 발생했기 때문에 위 과정은 모두 취소될 것이다. 그런데 실제로는 rax * 0xc에 접근했기 때문에 해당 값이 L1 캐시에 올라간다.
          12. +
          13. 앞선 예시에서 봤듯이 공격자는 256개 페이지를 모두 훑으면서 접근 시간을 측정한다. rax번째 페이지는 캐싱되어 있기 때문에 특히 접근 시간이 짧다. 이를 통해 공격자는 비밀값 rax를 알게 된다.
          14. +
          +

          멜트다운 취약점은 유저모드에서 커널 메모리를 무단으로 참조할 수 있는 매우 심각한 취약점이다. 성능 향상을 위한 비순차적 명령 실행 기법으로 인해 발생한 취약점이기 때문에 이를 해결하기 위해서는 성능 저하가 불가피하며, 동시에 프로세서 레벨의 취약점이기 때문에 근본적인 문제를 해결하기 위해서는 하드웨어 설계 차원에서도 조치가 필요하다.

          +

          Spectre

          +

          +

          스펙터는 인텔, AMD, ARM 프로세서에서 발견된 취약점으로, 공격 기반 원리는 멜트다운과 비슷하다. 컴퓨터는 조건문을 실행할 때 어떤 조건에 부합할지 예측하고 실행하는데, 당연히 잘못된 예측을 하는 경우도 있다. 스펙터 취약점은 예측이 실패하는 상황을 노려 외부에서 접근해서는 안 되는 메모리 영역에 접근하는 방식으로 공격한다.

          +

          Background

          +

          조건문의 실행을 예측하는 것을 브랜치 예측(Branch prediction)이라고 한다.

          +
          for (int i = 0; i < 10; i += 1) {
          +  result += 1;
          +}
          +
          +printf("%d", result);
          +
          +

          루프를 돌 때마다 i < 10이 참인지 거짓인지 판단하고, 루프 내부를 실행해야 할지 루프를 빠져나와야 할지 결정해야 한다. 위 루프에서는 같은 결과가 연속적으로 반복되는데, 매번 판단 과정을 거치면 성능이 저하될 수밖에 없을 것이다.

          +

          조건문을 실행할지 말지 예측하고 미리 명령을 실행하면 성능을 개선할 수 있다. 이를 추측 실행(Speculation execution)이라고 한다. 브랜치 예측을 위한 간단한 방법으로 BHT(Branch History Table)가 있는데, 기본적으로 과거 기록을 통해 미래를 추측하는 개념이다. 직전 예측이 참이였는데 이 예측이 성공했다면 다시 참으로 예측해 명령을 실행하고, 예측이 실패했다면 거짓으로 예측해 명령을 건너뛰는 식이다. 예측이 실패할 경우 미리 실행한 명령을 되돌려야 한다.

          +

          Attack Overview

          +

          스펙터 취약점은 브랜치 예측의 실패를 이용해 공격할 수 있다. 공격자는 캐시 메모리 등 사이드 채널을 통해 다른 프로세스의 데이터에 접근한다. 원래 다른 프로세스의 메모리 공간에 접근해서는 안 되고, 접근할 수도 없지만, 브랜치 예측의 맹점을 이용하면 가능하다.

          +

          스펙터 공격의 첫 번째 유형은 특정 코드 블록을 실행해 데이터를 알아내는 것이다.

          +
          if (x < array1_size) {
          +  y = array2[array1[x] * 4096];
          +}
          +
          +

          변수 x는 공격자가 설정하는 임의의 값으로, 배열의 메모리 범위를 넘는 값(Out of Bounds)을 넣는다. 따라서 array1[x]는 허용되지 않은 메모리 공간에 접근한다. 여기에 메모리에 있는 다른 페이지들을 읽기 위해 페이지 사이즈인 4096을 곱한다. 이때 공격자가 노리는 array1[x]를 시크릿 바이트(Secret byte) k라고 한다.

          +

          하지만 허용되지 않은 메모리 공간에 접근하면 세그먼트 폴트(Segment faults) 오류가 발생한다. 그래서 직접 해당 구문을 실행하지 않고 브랜치 예측을 이용한다.

          +
            +
          1. 프로세서가 조건문의 조건을 참으로 예측하도록 유도한다. (조건이 참인 상황을 여러 번 반복한다.)
          2. +
          3. 이제 조건이 거짓이 되도록 하면 브랜치 예측이 실패한다. 코드상으로는 조건문 내부의 명령이 실행되지 않는 것처럼 보이지만, 추측 실행으로 인해 실제로는 명령이 실행된다.
          4. +
          5. 예측이 실패했으므로 프로세서는 실행한 명령을 되돌린다. y에는 아무런 값도 저장되지 않는다.
          6. +
          7. 명령이 취소되기는 했지만, 실제로는 데이터를 읽었기 때문에 취소된 명령이 접근한 데이터 k * 4096은 캐시에 올라가 남아있게 된다. 이때 k는 허용된 메모리 범위를 넘는 값이기 때문에 해당 프로세스에서 접근할 수 없다.
          8. +
          9. 데이터에 직접 접근할 수 없기 때문에 공격자는 캐싱된 데이터를 이용한다. 공격자는 array2의 모든 요소를 무작위로 접근하면서 접근 시간을 측정한다. 접근 시간이 특히 짧은 데이터는 캐시에 저장되어 있다는 의미이므로, 이는 추측 실행으로 캐싱된 k * 4096를 뜻한다. 멜트다운 취약점을 공격할 때 사용한 방법과 동일하다.
          10. +
          +

          스펙터 취약점 공격을 위해서는 대상 프로세스 내에서 활용할 코드 블록인 가젯(Gadget)을 찾아야 할 뿐더러, 특정 메모리 주소를 가리키는 x를 설정하려면 공격 대상 머신의 VA(Virtual address)와 PV(Pysical address) 관계도 알아야 하기 때문에 실제로 공격하기는 어려운 점이 많다. 하지만 이 역시 심각한 취약점임은 틀림없다.

          +

          References

          + + +
          +
          + +
          + +
          +
          +

          하지만, 야크 털 깎기는 재미있다

          +

          밑바닥부터 만드는 즐거움

          +
          +
          +
          + + +
          + +
          +
          +

          하나의 타입에 강아지와 고양이 담기

          +

          파라미터의 다형성과 제네릭

          +
          +
          +
          + +
          +
          + +
          +
          + Articles +
          +
          +
          + + + + diff --git a/article/32.html b/article/32.html new file mode 100644 index 0000000..53da803 --- /dev/null +++ b/article/32.html @@ -0,0 +1,186 @@ + + + + + + 하지만, 야크 털 깎기는 재미있다 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + Articles +
          +

          + 하지만, 야크 털 깎기는 재미있다 +

          + +

          + 밑바닥부터 만드는 즐거움 +

          + + + + +
          +
          Table of Contents +

            +
            +

            +

            이 블로그는 지킬이나 휴고, 개츠비같은 정적 사이트 생성기/프레임워크를 사용하지 않았다. 처음엔 이것저것 써봤지만 커스터마이징 자유도가 낮아서 직접 블로그를 만들기로 했다. 초기에는 간단히 html로 글을 썼는데, 너무 불편해서 json 파일로 글을 쓰는 시스템을 만들었다. 이것도 장문의 글을 쓰기엔 불편해서 마크다운 파일을 html 파일로 변환하는 서비스를 개발했다. 그 다음엔 결과물로 나온 파일들을 빌드, 배포하기 위한 툴을 만들었다. 결국 밑바닥부터 정적 사이트 생성기를 만든 셈이 됐다.

            +

            이러한 작업을 야크 털 깎기(Yak shaving)라고 한다. 야크 털 깎기는 MIT AI Lab에서 박사과정을 밟던 대학원생 칼린 비에리(Carlin Vieri)가 만든 용어로, 목표한 일 하나를 위해 연관된 작업들을 하다가 결국 원래 목적을 잃고 완전히 관련없는 작업을 하게 되는 것을 말한다. LangDev IRC에서 언급된 예시를 보면 왜 야크 털 깎기라고 하는지 이해된다.

            +
              +
            1. 나무를 베기 위해 도끼를 구했다.
            2. +
            3. 도끼날이 너무 무뎌 날을 갈기 위한 돌을 구하려 한다.
            4. +
            5. 그런데 어떤 마을에 정말 좋은 돌이 있다는 이야기를 듣는다.
            6. +
            7. 그 마을에 가기 위해 야크를 구한다.
            8. +
            9. 야크 털이 너무 길어서 털을 깎기 시작한다.
            10. +
            +

            기업가이자 마케터, 작가인 세스 고딘(Seth Godin)의 예시도 있다.

            +
              +
            1. “오늘은 세차를 해야겠어.”
            2. +
            3. “이런, 호스가 망가졌네. 홈디포에서 새 호스를 사야겠군.”
            4. +
            5. “하지만 홈디포는 태펀지 다리 건너편에 있지. 톨게이트를 지나야 하니까 이지패스가 필요해.”
            6. +
            7. “잠깐! 이웃에게 이지패스를 빌릴 수 있을 것 같은데…”
            8. +
            9. “그렇지만 밥은 내 아들이 빌린 베개를 돌려주기 전까지 이지패스를 빌려주지 않을거야.”
            10. +
            11. “베개의 야크 털이 많이 빠져서 그냥 돌려줄 수 없네. 야크 털을 다시 채워야겠어.”
            12. +
            13. 결국 세차를 하기 위해 동물원에서 야크 털을 깎기 시작한다.
            14. +
            +

            두 이야기는 야크 털 깎기라는 용어가 만들어진 다음에 나온 것이고, 실제 용어가 탄생한 계기는 따로 있다. 당시 화요일 밤 늦게까지 하키를 한 칼린 비에리는 한밤중에 저녁을 먹으며 TV를 봤다. 그때 TV에는 애니메이션 렌과 스팀피(The Ren & Stimpy Show)의 야크 털 깎기의 날(Yak shaving day) 에피소드가 나오고 있었다. 그 줄거리는 이렇다:

            +
            +

            5일 뒤면 야크 털 깎기의 날이 온다. 렌과 스팀피는 더러운 기저귀를 벽에 걸고, 부츠에 코울슬로를 부어 집을 꾸민다. 그리고 제모한 야크가 마법의 카약을 타고 날아와 선물을 주길 기도하며 화장실 세면대에 면도 크림과 면도기를 둔다. 그날 밤, 야크가 욕조 배수구에서 나와 면도를 하고 세면대에 선물을 남긴 채 떠난다. 바로 야크가 면도하며 사용한 크림 찌꺼기다.[1]

            +
            +

            칼린 비에리는 이 내용을 이상하게 생각했다. 며칠 뒤 밤새 서류 작업(관리인에게 허락을 받고, DHL 계정을 만들고, 우체국을 찾는 등 짜증나는 일들)을 할 때 동료에게 자신이 야크 털 깎기를 하고 있다고 말했다. 그리고 이후 몇 달간 연구실 사람들에게 야크 털 깎기라는 말을 사용하며 용어가 알려졌다.[2] 애니메이션 내용이 워낙 괴상하고, 소프트웨어와도 별로 관련이 없어서 유래는 많이 알려진 것 같지 않다.

            +

            엔지니어가 (또는 엔지니어링팀을 운영하는 경영진이) 많이 하는 실수 중 하나가 '밑바닥부터 만들기’다. 엔지니어는 시중의 솔루션이 딱 마음에 들지 않을 수도 있고, 자신의 실력을 증명하고 싶어 할 수도 있다. 클라이언트나 경영진은 기존 솔루션에 대한 잘못된 이해가 있을 수도 있고, 기존 솔루션이 요구 사항을 정확히 만족하지 못한다고 생각할 수도 있다.

            +

            프로덕션이든 토이 프로젝트든, 대부분의 프로젝트에는 한정된 예산과 시간이 있다. 밑바닥부터 만들다보면 결국 야크 털을 깎게 되고, 야크 털을 깎기 시작하면 끝이 어딘지 종잡을 수 없게 된다. 그리고 결국 초기 목표를 포기하게 된다. 이런 경우엔 요구 사항의 핵심을 만족시킬 수 있는 대안을 찾아 작업량을 최대한 줄이는 것이 맞다.

            +

            하지만, 야크 털 깎기는 재미있다.

            +

            야크 털 깎기는 본질적으로 재밌을 수 밖에 없다. 세상에 없던 무언가를 만드는 행위, 문제를 발견하고 해결하는 행위, 원리를 알기 위해 연쇄적인 지식을 파고드는 행위, 모두 엔지니어를 이끈다. 애초에 '내가 원하는 것을 내가 직접 만든다’는 것은 꼭 엔지니어가 아니더라도 흥미로울만 하다. 프레더릭 브룩스(Frederick P. Brooks Jr.)는 '맨먼스 미신’에서 프로그래밍이 재밌는 이유를 아래와 같이 꼽았다.[3]

            +
              +
            1. 무언가를 만드는 데서 오는 순전한 기쁨
            2. +
            3. 다른 이에게 쓸모있는 것을 만드는 데서 오는 기쁨
            4. +
            5. 서로 맞물려 돌아가는 부속품으로 이루어진 복잡한 퍼즐같은 사물을 만들고, 거기 심어 놓은 여러 법칙이 미묘한 순환 속에서 펼쳐지는 것을 바라보는 매혹적인 경험
            6. +
            7. 지속적인 배움에서 오는 기쁨
            8. +
            9. 유연하고 다루기 쉬운 표현 수단으로 작업하는 데서 오는 기쁨
            10. +
            +

            TeX도 야크의 털을 깎아 태어났다. TeX은 스탠퍼드 대학교 교수 도널드 커누스(Donald Knuth)가 만든 조판 시스템으로, 조판 언어와 언어를 처리하는 컴파일러를 비롯해 프로그램을 운영하는 시스템 전체를 말한다.[4] 수식을 입력하기 편해서 사회과학 분야나 이공계 분야에서 널리 사용된다. (TeX을 쉽게 사용하기 위한 매크로인 LaTeX이 많이 쓰인다.)

            +

            가령 TeX 문법으로 아래와 같이 작성하면:

            +
            -b \pm \sqrt{b^2 - 4ac} \over 2a
            +
            +

            이렇게 예쁘게 출력된다:

            +
            b±b24ac2a +-b \pm \sqrt{b^2 - 4ac} \over 2a +

            1976년, 도널드 커누스는 컴퓨터 프로그래밍의 예술 2권 2판(The Art of Computer Programming Volume 2: Seminumerical Algorithms, 2nd Edition)을 준비하고 있었다. 그는 1판에서 사용한 것과 같은 타입셋인 핫 타입(Hot type)을 쓰려했으나, 더 이상 핫 타입은 사용할 수 없었다. 다른 타입셋에는 만족할 수 없었던 도널드 커누스는 같은 시기 디지털 조판된 패트릭 윈스턴(Patrick Winston)의 새 책을 보게 되었고, 여기에 고무되어 직접 디지털 조판 시스템을 만들기로 결심, TeX의 기본 기능을 구상했다.[5]

            +

            도널드 커누스는 SAIL 언어로 TeX의 첫 버전을 만들었고, 이후에 자신이 직접 만든 프로그래밍 언어인 WEB으로 개발해 완성했다.[6] WEB 소스는 문서와 코드가 섞인 형태이며, WEB 파일의 문서와 코드는 각각 위브(Weave)와 탱글(Tangle)이라는 프로그램을 통해 TeX 파일과 파스칼 파일로 추출할 수 있다. 그는 이러한 프로그래밍 패러다임을 문학적 프로그래밍(Literate programming)이라고 불렀다. 또한 도널드 커누스는 마이클 플래스(Michael Plass)와 함께 문장의 어느 지점에서 줄 바꿈을 할지 결정하는 Knuth-Plass line wrapping 알고리즘을 고안했다. 뿐만 아니라 TeX을 위한 폰트인 컴퓨터 모던(Computer Modern)을 디자인했고, 벡터 그래픽을 정의하기 위한 언어 METAFONT를 만들었다. 더불어 장치에 종속되지 않고 TeX을 출력하기 위해 DVI(Device Independent) 포맷까지 개발했다.[7]

            +

            결과적으로 도널드 커누스는 책을 쓰기 위해 프로그래밍 언어와 패러다임, 알고리즘, 도구, 서체를 만들어냈다. TeX을 만드는데 10년 가까운 시간이 걸렸고, 책도 그만큼 늦게 출간됐다. 그러나 헛된 시도는 아니었다.

            +

            물론 이건 극단적으로 성공적인 경우고, 대부분의 야크 털 깎기는 실패한다. 적절한 지점에서 중단해야 하는데,[8] 털을 깎기 시작하면 그 동안 쏟은 시간이 아까워서, 또는 그것 자체가 즐거워서 끊기 쉽지 않다. 아니면 진짜로 끝까지 가야하는데, 결국 '내가 지금 뭐하는거지’라는 생각이 들며 흥미가 떨어지거나 프로젝트에 주어진 자원이 바닥날 때 그만두게 된다.

            +

            한편 뭔가를 공부하는 입장에선 야크 털 깎기가 굉장히 효과적이라고 생각한다. 대부분의 CS 전공 과제는 교수의 의도와 상관없이 어느정도 야크 털 깎기를 요구하는데, 과제의 주요 지시 사항보다 그것과 연관된 지식을 파고 들면서 더 많은 것을 얻을 때도 있다. 반대로 말하자면, 야크 털 깎기를 하면 분명 뭔가 배우는 것이 있다. 가령 컴퓨팅 시스템을 야크 털 깎듯이 만들겠다면 불 논리부터 논리회로, 컴퓨터 아키텍처, 프로그래밍 언어, 운영체제를 공부해야 한다. 노암 니산(Noam Nisan)과 시몬 쇼켄(Shimon Schocken)의 밑바닥부터 만드는 컴퓨팅 시스템이 이런 과정을 담고있다. 그러니까 끝을 보지 못하더라도 야크 털을 깎으며 배운 것이 있다면 그 자체로 의미가 있다(고 믿고 싶다).

            +

            아무튼 야크 털 깎기는 재미있다.

            +
            +
            +
              +
            1. DeadPark, “Ren and Stimpy: The Quest for the Shaven Yak”. ↩︎

              +
            2. +
            3. Donavon West, “Yak Shaving: A Short Lesson on Staying Focused”, American Express, 2018. ↩︎

              +
            4. +
            5. 프레더릭 브룩스, “맨먼스 미신: 소프트웨어 공학에 관한 에세이”, 강중빈 역, 인사이트, 6-7쪽, 2015. ↩︎

              +
            6. +
            7. KTUG, “KTUGFaq: TeX”, 2009. ↩︎

              +
            8. +
            9. TUG, “History of TeX”, 2019. ↩︎

              +
            10. +
            11. 권현우 외 15명, “TeX: 조판, 그 이상의 가능성”, KTS 설립 10주년 기념문집, 한국텍학회, 경문사, 314쪽, 2017. ↩︎

              +
            12. +
            13. Florian Gilcher, “Donald Knuth - The Patron Saint of Yak Shaves”, 2017. ↩︎

              +
            14. +
            15. item4, “성공적인 Yak Shaving, 실패하는 Yak Shaving”, 2015. ↩︎

              +
            16. +
            +
            + +
            +
            + +
            + +
            +
            +

            해피 터미널 라이프

            +

            Dotfiles 세팅해 광명찾기

            +
            +
            +
            + + +
            + +
            +
            +

            👻 CPU 보안 취약점을 공격하는 아주 구체적인 원리

            +

            멜트다운, 스펙터 페이퍼 읽기

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/33.html b/article/33.html new file mode 100644 index 0000000..02ca635 --- /dev/null +++ b/article/33.html @@ -0,0 +1,193 @@ + + + + + + 해피 터미널 라이프 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 해피 터미널 라이프 +

            + +

            + Dotfiles 세팅해 광명찾기 +

            + + + + +
            +
            Table of Contents +

            +
            +
            +

            2023년 7월 25일에 내용을 업데이트했습니다. 2019년 8월판은 여기에서 확인할 수 있습니다.

            +
            +

            Terminal Emulator

            +

            Alacritty는 OpenGL을 이용해 렌더링에 GPU 가속을 지원받을 수 있는 터미널 에뮬레이터다. 러스트로 작성되었으며, 높은 성능을 내세우고 있다. Alacritty의 모든 설정은 ~/.alacritty.toml 파일로 관리할 수 있고, 저장하면 변경 사항이 즉시 반영된다.

            +

            다만 0.12.0 버전부터 한글 입력 버그가 발생하고 있다. 대신 사용할 수 있는 GPU 가속 터미널 에뮬레이터로는 kittywezterm이 있다.

            +

            터미널에서 각종 아이콘을 사용하기 위해서는 Nerd Fonts를 사용해야 한다. Nerd Font 글리프가 반영된 폰트 목록이 있는데, 이중에서 마음에 드는 폰트를 받아서 사용하면 된다.

            +

            Git

            +

            git은 분산 버전 관리 시스템이다. 프로젝트에 git을 사용하지 않더라도 다른 프로그램을 설치할 때 필요하다.

            +

            alias를 설정하면 긴 명령을 축약할 수 있다. 가령 git status 대신 git s를 사용하게 만드는 것이 가능하다. git에 관련된 설정은 .gitconfig 파일로 관리할 수 있다.

            +
            [alias]
            +  l = log --reflog --graph --oneline --decorate
            +
            +

            이제 git lgit log --reflog --graph --oneline --decorate와 동일하다. 기계인간님의 편리한 git alias 설정하기를 참고하면 극한의 편리를 추구할 수도 있다.

            +

            Fish Shell

            +

            +

            fish는 사용자 친화적인 셸이다. 히스토리를 기반으로 타이핑 중 명령어를 제안해주며, man 페이지를 바탕으로 인자 자동완성도 제공한다. 또한 bash 셸 스크립트보다 편리한 자체 스크립트를 제공한다. ~/.config/fish/functions 디렉토리에 fish 함수 파일을 작성해두면 해당 함수를 셸 명령처럼 사용할 수 있다는 점도 재밌다. zsh처럼 각종 플러그인도 설치할 수 있다. fisher는 fish를 위한 플러그인 매니저다.

            +
              +
            • z: 빠르게 특정 디렉토리로 점프할 수 있도록 해준다.
            • +
            • puffer-fish: 연속적인 ..../.., ../../..으로 확장해준다.
            • +
            +

            Neovim

            +

            +

            NeovimVim을 포크해 현대적으로 리팩토링하는 프로젝트다. 최근 Vim을 사용한다고 하면 대체로 Neovim을 사용한다는 의미로 받아들여질 정도로 광범위하게 인기를 얻고 있다. Neovim을 사용하면 각종 유용한 플러그인을 사용할 수 있고, Vimscript 대신 Lua를 이용해 설정 스크립트를 작성할 수도 있다. 가장 좋은 점은 Vim을 바탕으로 나만의 에디터를 구성할 수 있다는 점인데, 이 덕분에 나는 코드 작성뿐 아니라 문서 작성과 메모, 슬라이드쇼 제작까지도 Neovim으로 한다.

            +

            ~/.config/nvim/init.vim 파일로 Neovim의 설정을 관리할 수 있다. init.vim 대신 init.lua을 사용하면 Lua로 설정을 작성할 수도 있다. 설정 파일 내용을 수정해 줄번호 표시 여부, 문법 하이라이팅 여부, 들여쓰기 등 일반적인 옵션을 직접 설정할 수 있다.

            +

            Neovim에는 다양한 플러그인들이 있고, Vim에서 사용하는 플러그인은 물론, Neovim에서만 사용할 수 있는 플러그인도 있다. 플러그인 매니저를 사용하면 쉽게 플러그인을 설치, 적용하여 사용할 수 있다. 나는 Vim을 사용할 때부터 써온 vim-plug에 머물러 있는데, 요즘에는 lazy.nvim을 많이 쓰는 것 같다. 나는 아래와 같은 플러그인을 사용하고 있다.

            +
              +
            • lualine.nvim: 하단 상태라인과 상단 탭라인을 설정할 수 있다. 원하는 컴포넌트를 배치할 수도 있고, 다양한 테마를 골라서 적용할 수도 있다.
            • +
            • nvim-tree.lua: 사이드바에 파일 탐색 트리를 보여준다.
            • +
            • gitsigns.nvim: Git 동작을 사용할 수 있게 해준다. 각 라인의 변경 여부를 기호로 보여주며, 변경사항 미리보기, 변경 단위(hunk) 탐색, 단위 스테이징 등의 기능을 제공한다.
            • +
            • indent-blankline.nvim: 들여쓰기마다 수직선을 보여준다.
            • +
            • nvim-colorizer.lua: 컬러코드의 배경색을 해당 색상으로 보여준다.
            • +
            • vim-visual-multi: 커서를 여러 개 만들어 동시 편집을 할 수 있게 해준다. 특정 문자, 단어에 모두 커서를 만들거나, 여러 줄에 걸쳐 커서를 만들어 사용할 수 있다. 생산성이 차원이 다르게 높아진다.
            • +
            • coc.nvim: LSP(Language Server Protocol)를 통해 코드 자동완성, 심볼트래킹, 코드액션, 문법 진단 등 인텔리센스를 제공한다. Neovim이 자체적으로 LSP를 지원하고 있지만, coc.nvim을 사용하면 더 쉽게 언어를 추가할 수 있다. VSCode도 똑같이 LSP를 사용하는 방식으로 인텔리센스를 제공하기 때문에 말그대로 Neovim이 VSCode를 완전히 대체할 수 있게 해준다. Vim을 쓸 때부터 사용해왔기에 딱히 바꾸지 않고 있는데, 요즘에는 자체 LSP 지원과 nvim-cmp도 많이 사용하는 추세다.
            • +
            • nvim-treesitter: tree-sitter 파싱 라이브러리를 이용해 향상된 코드 하이라이팅을 제공한다.
            • +
            • nvim-scrollbar: 스크롤바에 다양한 기능을 제공한다. coc.nvim과 연동해 어떤 라인에 문제가 있는지 표시할 수 있고, gitsigns.nvim과 연동해 어떤 라인이 변경되었는지 표시할 수도 있다.
            • +
            • hop.nvim: 특정 문자나 단어로 즉시 커서를 옮길 수 있게 해준다.
            • +
            +

            이것저건 직접 설정하기가 번거롭고 혼란스럽다면, AstroNvim이나 LazyVim을 사용해보는 것도 괜찮다.

            +

            tmux

            +

            +

            tmux(termial multiplexer)는 터미널의 세션과 창을 분할 관리하기 위한 도구인데, 백그라운드에서 세션을 유지하거나 ssh 접속할 일이 많다면 필수라고 할만하다. 사용 목적은 기본으로 설치되어 있는 screen과 비슷하지만 tmux가 더 편리하다.

            +

            tmux는 세션(session), 윈도우(window), 팬(pane)으로 구성된다. 세션은 가장 큰 단위이며, 윈도우는 세션 내에서 탭처럼 사용할 수 있는 화면이다. 팬은 한 윈도우 내에서의 화면 분할을 의미한다.[1] tmux는 기본 prefix 키인 Ctrl + b를 누르고 명령 키를 입력하여 조작할 수 있다. 가령 윈도우를 수평 분할하는 키는 % 키이기 때문에 Ctrl + b + %를 입력하면 윈도우가 수평 분할된다.

            +

            tmux 설정은 ~/.tmux.conf 파일로 관리할 수 있다. 기본 prefix 키는 Ctrl + b이지만, 원한다면 설정 파일을 수정해 Ctrl + a 등 다른 키로 바꿀 수 있다. (screen의 기본 prefix가 Ctrl + a다.)

            +
            unbind C-b
            +set-option -g prefix C-a
            +bind-key C-a send-prefix
            +
            +

            tmux에는 다양한 플러그인이 있다. tpm을 사용하면 tmux 플러그인을 쉽게 관리할 수 있다.

            +
              +
            • tmux-powerline: 하단에 상태바를 보여준다.
            • +
            • tmux-resurrect: 세션과 윈도우, 팬 레이아웃 등 사용 환경을 저장하고 로드할 수 있다.
            • +
            +

            기타 CLI 도구

            +

            기본적으로 제공되는 ls, rm, cat 등 coreutils도 훌륭하지만, 기존 도구의 인터페이스나 기능을 개선, 대체하는 각종 대체 도구를 사용할 수도 있다.

            +
              +
            • exa, lsd: 파일 및 디렉토리 아이콘, 파일 트리 등을 제공하는 ls 대체. --tree 옵션으로 tree 명령도 대체할 수 있다.
            • +
            • bat: 문법 하이라이팅, 특수문자 표시, git 변경사항 등 다양한 기능을 지원하는 cat 대체.
            • +
            • fd: 사용자 친화적인 find 대체.
            • +
            • ripgrep: 빠르게 작동하는 grep 대체.
            • +
            • procs: 사용자 친화적인 ps 대체.
            • +
            • trash. 셸에서 rm 명령으로 파일을 지우면 휴지통으로 이동하지 않고 즉시 파일이 제거되는데, trash 도구를 통해 휴지통을 사용할 수 있다.
            • +
            • delta: 문법 하이라이팅, 줄 번호 표시 등 다양한 기능을 제공하는 diff 대체. git diff에 적용할 수 있다.
            • +
            • tldr: 셸 명령이 기억나지 않거나, 옵션이 헷갈릴 때 참고할 수 있는 매뉴얼 도구. man보다 간결한 설명을 제공하며, 다양한 사용 예시가 포함되어 있다.
            • +
            +

            나는 기존 명령을 대체하는 도구를 아예 config.fish 파일에 alias로 설정해뒀다.

            +
            alias ls='lsd'
            +alias tree='lsd --tree'
            +alias cat='bat'
            +alias find='fd'
            +alias grep='rg'
            +alias ps='procs'
            +alias rm='trash'
            +alias diff='delta'
            +
            +

            Dotfiles

            +

            터미널 환경을 세팅하며 작성한 무수한 설정들, alacritty.toml, .gitconfig, fish.conf, init.vim 등의 파일을 통틀어 'dotfiles’라고 부른다. (전통적인 설정 파일들은 모두 .으로 시작하는 숨김 파일이기 때문이다.) 이 파일들을 별도 git 저장소에 올려두면 다른 컴퓨터에서 터미널을 설정할 때 그대로 사용할 수 있다.

            +

            홈 디렉토리에 있던 기본 설정 파일들을 지우고, 저장소에 올려둔 설정 파일들의 심볼릭 링크를 만들면 모든 컴퓨터에서 터미널 환경을 쉽게 동기화할 수 있다.

            +
            $ rm ~/.gitconfig
            +$ ln -s ~/dotfiles/.gitconfig ~/.gitconfig
            +
            +
            +
            +
              +
            1. 김용균, “tmux 입문자 시리즈 요약”, 2014. ↩︎

              +
            2. +
            +
            + +
            +
            + +
            + +
            +
            +

            🗞️ 훈련소에서 매일 뉴스 받아보기

            +

            고립된 훈련병을 위한 종합 뉴스

            +
            +
            +
            + + +
            + +
            +
            +

            하지만, 야크 털 깎기는 재미있다

            +

            밑바닥부터 만드는 즐거움

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/34.html b/article/34.html new file mode 100644 index 0000000..2ae2508 --- /dev/null +++ b/article/34.html @@ -0,0 +1,242 @@ + + + + + + 🗞️ 훈련소에서 매일 뉴스 받아보기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🗞️ 훈련소에서 매일 뉴스 받아보기 +

            + +

            + 고립된 훈련병을 위한 종합 뉴스 +

            + + + + +
            +
            Table of Contents +

            +
            +

            훈련소에서 가장 답답한 것은 외부와의 단절이라는 말을 자주 들었다. 그래서 입대한 친구들에게 뉴스나 읽을거리를 요약해서 인터넷 편지로 보내주곤 했는데, 매일 주요 뉴스를 요약하는 것은 의외로 손이 많이 가는 일이었다. 무엇보다 인터넷 편지 발송을 지원하는 서비스인 더 캠프의 인터페이스가 너무 불편해서 사용하기 쉽지 않았다.

            +

            그러던 중 나에게도 군사교육 소집 통지서가 왔고, 4주간의 고립은 역시 중대한 문제였다. 그렇게 입소를 앞둔 일요일, 매일 인터넷 편지로 뉴스를 보내주는 프로그램을 만들게 됐다.

            +

            더 캠프 라이브러리

            +

            훈련병에게 편지를 보내려면 반드시 더 캠프를 거쳐야 한다. 더 캠프가 오픈 API를 지원하면 참 좋겠지만 안타깝게도 그렇지 않았다. 결국 클라이언트에서 HTTP 요청을 구성해 직접 더 캠프 서버로 보내는 방식으로 구현했다.

            +

            프로젝트 구조는 간단히 계획했다. src 디렉토리 하위의 models 디렉토리에 각종 인터페이스를 두고, services 디렉토리에는 실제 요청을 보내고 값을 얻는 함수들을 모은다. utils 디렉토리에는 여러 서비스에서 반복적으로 사용되는 함수를 두기로 했다.

            +
            .
            ++- examples
            ++- src
            +|  +- models
            +|  +- services
            +|  +- utils
            ++- test
            +
            +

            구체적으로 어디로 요청을 보내야 하는지 알아내기 위해 훈련소에 있는 지인(정확히는 친구의 학교 후배였다.)에게 인터넷 편지를 발송하고 브라우저의 개발자 도구에서 네트워크 로그를 확인했다.

            +

            +

            https://www.thecamp.or.kr/pcws/message/letter/insert.do에 요청을 보내면 된다는 것을 알게 됐다. 요청 본문에는 unit_code, group_id 등 알 수 없는 키들이 있었고, 포스트맨으로 테스트한 요청에는 로그인 정보를 찾을 수 없다는 응답이 돌아왔다.

            +

            로그인에 대한 응답 헤더를 살펴보니 JSESSIONIDSCOUTER 쿠키가 있었다. HTTP는 무상태(Stateless) 프로토콜이기 때문에 독립적인 쌍의 요청과 응답으로 연결이 끊긴다.[1] 로그인한 사용자를 식별하기 위해서는 세션 정보를 유지해야 하는데, 톰캣 서버는 이를 위해 JSESSIONIDSCOUTER 쿠키를 사용한다. 쿠키는 클라이언트단에 저장되는 데이터이기 때문에 값을 유지할 수 있으며, 요청 헤더에 쿠키를 포함하면 세션을 식별하는 것이 가능하다.

            +

            서버측에서 요청을 비동기적으로 처리하기 때문에 request-promise를 사용하여 세션 쿠키를 얻어오는 로그인 함수를 작성했다. 이때는 단순하게 쿠키를 반환하는 걸로 끝냈는데, 지금 생각해보면 세션 인스턴스의 프로퍼티로 할당해서 다른 서비스에서 바로 사용하도록 만들면 더 편했을 것 같다.

            +
            interface Cookie {
            +  scouter: string;
            +  jsessionid: string;
            +}
            +
            +
            async function login(id: string, password: string) {
            +  let result: Cookie | null = null;
            +  const options = {
            +    uri: buildRequestUrl('common/login.do'),
            +    method: 'POST',
            +    json: true,
            +    body: {
            +      'user-id': id,
            +      'user-pwd': password,
            +      subsType: 1,
            +    },
            +  };
            +
            +  await requestPromise.post(options, (err, res, body) => {
            +    // ...
            +  });
            +
            +  return result as Cookie;
            +}
            +
            +

            이어서 사용자가 가입한 카페 중에서 편지를 받을 훈련병이 속한 카페를 찾아야한다. 만약 훈련병이 25연대 5중대에 속해있다면 더 캠프에서 25연대 5중대 카페에 가입하고, 이 카페에서 편지를 보내는 방식이다.

            +

            카페 리스트 요청 헤더를 통해 앞서 본 unit_codegroup_id는 각각 연대/사단 식별 코드, 카페 아이디라는 것을 알게 됐다. 그리고 위와 같은 방식으로 인터페이스와 함수를 만들었다. 헤더에 로그인으로 얻은 세션 쿠키를 담아 요청을 보내면 사용자가 가입한 카페 리스트를 가져온다. 훈련병의 소속과 입소 날짜를 파라미터로 넘겨주면 해당 훈련병이 속한 카페만 탐색해 반환한다.

            +
            interface Group {
            +  unitName: string; // 연대/사단 이름
            +  fullName: string; // 카페 전체 이름
            +  enterDate: string; // 훈련병 입소 날짜 (YYYYMMDD)
            +  groupId: string; // 카페 식별 코드
            +  groupName: string; // 카페 이름
            +  groupImage: string; // 카페 대표 이미지
            +  accessDate: string; // 요청 날짜
            +  unitCode: string; // 연대/사단 식별 코드
            +  unitType: number; // 육군훈련소(1)/사단신교대(2) 여부
            +  grade: number;
            +}
            +
            +
            async function fetchGroups(cookies: Cookie, unitName?: string, enterDate?: string) {
            +  // ...
            +  return result as Group[];
            +}
            +
            +

            응답이 스네이크 케이스(snake_case)로 떨어지지만 코드의 캐멀 케이스(camelCase)를 포기하고 싶지 않았다. 때문에 응답을 위한 별도의 인터페이스를 만들었는데, 손이 많이 가지는 않지만 소모적인 일이었다. 뭔가 매핑해주는 툴이 있으면 좋겠다는 생각이 들었다.

            +
            interface GroupResponse {
            +  unit_name: string;
            +  full_name: string;
            +  enter_date: string;
            +  group_id: string;
            +  group_name: string;
            +  group_image: string;
            +  access_date: string;
            +  unit_code: string;
            +  unit_type: number;
            +  grade: number;
            +}
            +
            +

            실제 편지 전송은 간단했다. 훈련병의 이름, 생년월일, 훈련병과의 관계 정보와 함께 앞서 얻은 카페 정보들을 담아 요청을 보내면 인터넷 편지가 발송됐다.

            +

            이렇게 the-camp-lib를 '소기의 목적 달성을 위한 최소한의 수준’까지 만들어 npm 패키지로 배포했다.

            +

            훈련병을 위한 데일리 뉴스

            +

            인터넷 편지 내용은 다음뉴스의 RSS를 이용해 구성했다. 인터넷 편지의 글자 제한이 2000자이기 때문에 최대한 경제적으로 내용을 구성해야 했다. 먼저 기사의 첫 문장만 잘라냈다.

            +
            content = content.slice(0, content.indexOf('다.') + 1);
            +
            +

            그리고 기사 앞에 붙는 바이라인과 불필요한 문구를 지웠다.

            +
            content = content.replace(/^(\*그림\d\*)?(\(|\[|【)\s?.*=.*\s?(\)|\]|】)\s?/, '')
            +  .replace(/^[가-힣]{2,4}\s(기자|특파원)\s=\s/, '');
            +
            +

            npm install the-camp-lib으로 앞서 만든 패키지를 설치하고, 실제 편지를 보내는 코드를 작성했다.

            +
            async function setMessage() {
            +  // ...
            +}
            +
            +(async () => {
            +  // ...
            +
            +  const cookie = await thecamp.login(id, password);
            +  const [group] = await thecamp.fetchGroups(cookie, unitName, enterDate);
            +
            +  const trainee = {
            +
            +    traineeName,
            +    unitCode: group.unitCode,
            +    groupId: group.groupId,
            +    relationship: thecamp.Relationship.FATHER,
            +  };
            +
            +  const date = new Date();
            +  const message = {
            +    title: `${date.getMonth()}${date.getDate()}일 (${date.getHours()}${date.getMinutes()}분) - 다음뉴스 종합`,
            +    content: (await setMessage()),
            +  };
            +
            +  await thecamp.sendMessage(cookie, trainee, message);
            +})();
            +
            +

            훈련병의 이름이나 생년월일, 소속 등 훈련병에 대한 사전 정보는 .env 파일을 이용해 관리했다. 입소 전에는 소속을 알 수 없기 때문에 친구에게 나중에 소속이 나오면 값을 넣어달라고 부탁해뒀다. 편지를 보내는 계정은 아버지의 계정을 사용했다. 내가 나에게 편지를 보내도 될 것 같기는 하지만, 만에 하나 훈련소에 들어가서 문제가 생기면 디버깅을 할 수 없기 때문에 최대한 안전한 방법을 택해야 했다.

            +

            그렇게 만든 파일을 개인 서버에 올리고, 크론(Cron)을 이용해 하루 두 번씩 파일을 실행하도록 했다.

            +
            0 0 * * * cd ~/projects/daily-news-for-trainee && ts-node index.ts
            +0 18 * * * cd ~/projects/daily-news-for-trainee && ts-node index.ts
            +
            +

            편지를 두 번이나 보낸 이유는 더 많은 뉴스를 받기 위함이기도 하고, 한 번만 보내면 인편이 누락될 수 있다는 이야기를 들었기 때문이기도 하다. 하필 0시, 18시인 이유는 딱히 없었다. 나중에야 알게 됐지만, 편지를 나눠주는 시간이 대체로 17시 전이라서 18시는 좋은 선택이 아니었다.

            +

            daily-news-for-trainee까지 완성하며 훈련소에 들어가기 전 할 수 있는 일은 모두 끝마쳤다.

            +

            실행

            +

            8월 29일 논산 육군훈련소에 입소했다. 프로그램에 문제가 있어도 고칠 수 없는 상황이다보니 뉴스가 안 올까봐 불안해하며 일주일을 보냈다. 일주일 뒤 인사담당 훈련병들이 인터넷 편지를 나눠주기 시작했고, 성공적으로 뉴스가 도착했다!

            +

            +

            덕분에 매일 두 통씩 뉴스를 받아보며 바깥 소식을 알 수 있었다. 입소 바로 전날 밤에 예상치 못한 문제가 생겨서 node_modules 디렉토리의 트랜스파일된 자바스크립트 코드를 직접 수정하는 짓까지 했는데, 다행히 잘 동작했다.

            +

            몇 가지 실수한 것이 있다면 날짜가 0월부터 시작해서 제목의 날짜가 9월이 아니라 8월로 찍혀나왔다는 점, 불필요한 문구를 삭제하는 정규표현식을 잘못 작성하여 기사의 일부까지 날려버리는 경우가 있었다는 점이다.

            +

            또한 일반적으로 기사의 첫 문장은 제목을 그대로 반복하기 때문에 첫 문장을 보여주는건 내용 파악에 큰 도움이 되지 않았다. 가령 “14호 태풍 ‘가지키’ 베트남 다낭서 소멸” 기사의 첫 문장은 "베트남 다낭 부근에서 발생한 제 14호 태풍 '가지키’가 에너지를 잃고 소멸됐다"가 되는 식이다. 종합 뉴스라서 관심이 없는 뉴스가 많았던 것도 아쉬웠다. 섹션별로 뉴스를 따로 보냈으면 더 좋았을 것 같다. 우리 분대원들은 주로 연예 뉴스를 원했는데, 종합에는 연예 뉴스가 들어가는 일이 없었다.

            +

            결정적으로 매일오는 뉴스보다 사람이 보낸 편지가 훨씬 좋았다. 편지를 받았는데 뉴스면 실망감이 더 크다.

            +

            그래도 극도로 제한적인 환경에서 나의 문제를 해결해본 경험이 꽤 재밌었다. 개발할 수 있는 시간이 많지 않아서 더 재밌던 것 같기도 하다. 이번에 직접 찾아낸 문제들을 개선해 나중에 입대하는 친구에겐 더 나은 뉴스를 보내줘야겠다.

            +
            +
            +
              +
            1. MDN web docs, “An overview of HTTP”, 2019. ↩︎

              +
            2. +
            +
            + +
            +
            + +
            + +
            +
            +

            🦀 러스트의 멋짐을 모르는 당신은 불쌍해요

            +

            높은 성능과 신뢰를 확보하기 위한 언어

            +
            +
            +
            + + +
            + +
            +
            +

            해피 터미널 라이프

            +

            Dotfiles 세팅해 광명찾기

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/35.html b/article/35.html new file mode 100644 index 0000000..b590c5a --- /dev/null +++ b/article/35.html @@ -0,0 +1,479 @@ + + + + + + 🦀 러스트의 멋짐을 모르는 당신은 불쌍해요 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦀 러스트의 멋짐을 모르는 당신은 불쌍해요 +

            + +

            + 높은 성능과 신뢰를 확보하기 위한 언어 +

            + + + + +
            +
            Table of Contents +

            +
            +

            내가 만나온 개발자들은 대체로 자신이 사용하는 프로그래밍 언어에 딱히 만족하지 않았는데 (극단적으로는 자바스크립트와 PHP가 있다.) 유독 러스트 개발자들은 적극적으로 러스트를 추천했다.

            +

            하지만 그냥 그런 언어가 있구나 정도로 생각하고 있었다. 그런데 러스트 2018 에디션 발표 이후 근 1년간 러스트 코드를 웹어셈블리로 컴파일할 수 있다든지, deno의 코어가 러스트로 작성됐다든지하는 이야기들이 뉴스피드를 가득 채웠다. 심지어 스프린트 서울 6월 모임에서 RustPython의 인기를 본 뒤로는 러스트가 마치 피할 수 없는 시대의 흐름처럼 느껴졌다.

            +

            +

            무엇보다 러스트 커뮤니티의 비공식 마스코트인 Ferris가 귀여워서 반은 먹고 들어간다.[1] 러스트 사용자는 갑각류를 뜻하는 'Crustacean’에서 따와 'Rustancean’이라고 부른다. (한국에서는 '러스토랑스’가 많이 쓰이는데 더 적절한 것 같기도 하다…) 참고로 Ferris를 부를 때는 젠더 중립적인 "they/them"을 사용한다.[2] 이것마저 멋지다.

            +

            러스트는 신생 언어인 만큼 업데이트를 거치며 크게 변화해왔다. 각종 블로그나 스택오버플로우 등, 웹상의 많은 자료들이 이젠 유효하지 않다. 그래서 시간이 지나도 아마 크게 바뀌지 않을 것 같은 문법적 특징과 러스트의 주요 컨셉인 오너십(Ownership)을 중심으로 러스트의 안전성을 강조해보려 한다.

            +

            급성장하는 언어

            +

            러스트는 2006년 모질라의 개발자 그레이던 호어(Graydon Hoare)의 사이드 프로젝트에서 출발했다. 이후 모질라가 공식적으로 러스트를 후원하기 시작했고, 2015년 1.0 버전을 릴리즈했다. 모질라의 정책에 따라 러스트는 오픈소스 프로젝트로 진행된다. 코어 팀이 전체적인 방향을 리드하지만 누구나 러스트 개발에 기여할 수 있도록 하고 있으며, RFC(Request For Comments) 문서와 러스트 저장소에서 확인할 수 있다.

            +

            러스트는 공개 이후 꾸준히 높은 인기를 얻어왔다. 스택오버플로우 서베이에서 매년 사랑받는 언어 1위를 차지하고 있고, 2019년 기준 깃허브에서의 러스트 사용률은 2018년 대비 235% 증가했다.[3] 뿐만 아니라 많은 기업들이 프로덕션에도 러스트를 적용하고 있다. 모질라의 브라우저 엔진 프로젝트 서보가 러스트로 작성되었고, 페이스북의 암호화폐 리브라의 코어도 러스트로 구현되었다. 국내에서는 스포카가 POS 통합 SDK에 러스트를 사용한다.[4]

            +

            심플한 개발 환경

            +

            러스트를 처음 시작할 때 느낀 첫 번째 장점은 개발 환경이 단순하다는 것이었다. 먼저 러스트 툴체인 인스톨러 rustup을 다운로드한다.

            +
            $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
            +
            +

            rustup은 러스트 컴파일러인 rustc와 패키지 매니저인 카고(Cargo)를 설치한다. 러스트 개발은 빌드, 테스트, 문서화, 배포 모든 것을 카고로 커버할 수 있다. cargo new 명령으로 새 프로젝트를 생성한다. 프로젝트 디렉토리의 Cargo.toml 파일에 디펜던시를 추가하면 크레이트(Crate)라고 부르는 외부 패키지를 바로 사용할 수 있다. 또한 cargo build 명령으로 프로젝트를 빌드하며, cargo run 명령으로 컴파일, 실행할 수 있다.

            +
            $ cargo new hello_world --bin
            +$ cd hello_world
            +$ cargo build
            +$ cargo run
            +
            +

            러스트의 표준 스타일 가이드를 따르는 포매터 rustfmt, 코드 상의 실수와 개선점을 제안해주는 린터 clippy와 같은 도구도 rustup을 이용하면 쉽게 툴체인에 추가해 사용할 수 있다. 기본적인 컴포넌트들은 미리 설치해두는 것이 좋다.

            +
            $ rustup update
            +$ rustup component add rustfmt clippy rls rust-analysis rust-src
            +
            +

            랭귀지 서버가 있기 때문에 어떤 도구를 쓰든 높은 수준의 심볼 탐색, 포매팅, 자동완성 등의 개발환경을 보장 받을 수 있다. IntelliJ, VSCode 등 IDE를 사용한다면 플러그인을 설치해 개발을 할 수 있으며, Rust Playground에서 러스트를 설치하지 않고 코드를 실행해볼 수도 있다.

            +

            안전한 문법

            +

            자바스크립트나 파이썬과 같은 언어에 비하면 러스트의 문법은 굉장히 엄격하다. 간단한 예시를 보자.

            +
            fn main() {
            +    let x: i8 = 10;
            +    let y: i8 = 20;
            +
            +    println!("{}", x + y); // "30"
            +}
            +
            +

            러스트 프로그램의 엔트리 포인트는 main 함수다. 변수는 let 키워드로 선언할 수 있으며, 이렇게 선언된 변수는 기본적으로 불변(Immutable)하여 값을 변경할 수 없다. 가변적인(Mutable) 변수를 선언하려면 mut 키워드를 명시해야 한다.

            +
            fn main() {
            +    let mut x: i8 = 10;
            +    x = x + 20; // 30
            +
            +    println!("{}", x); // "30"
            +}
            +
            +

            const 키워드로 상수를 선언할 수도 있다. 불변 변수와 다르게 상수는 상수 표현식으로만 초기화할 수 있으며, 함수의 결과 등 런타임에 계산되는 값으로 초기화 할 수 없다.

            +

            코드를 보면 알 수 있지만, 러스트는 정적 타입 언어다. 선언 시에 값을 할당하지 않는 경우 타입을 명시해야 한다. i8, i32는 8비트, 32비트 정수 타입을 의미한다. f32는 32비트 부동소수점 타입, bool은 불리언 타입을 뜻한다. 그 외 튜플(let x: (i32, f64) = (10, 3.14))과 배열(let x = [1, 2, 3])도 지원한다.

            +

            러스트에서 구문(Statement)은 특정 동작을 수행하지만 값을 반환하지 않는 명령을 말한다. 한편 표현식(Expression)은 결과값을 반환하는 명령을 말한다. 구문 블록의 값은 블록 내의 마지막 표현식으로 결정된다. 기본적으로 함수 정의도 하나의 구문이기 때문에 마지막 표현식이 함수의 반환값이 된다.

            +
            fn square(x: i32) -> i32 {
            +    x * x
            +}
            +
            +

            변수 선언도 구문이다. 따라서 우변에 표현식을 넣을 수 있다:

            +
            let x = 1;
            +let y = {
            +    let x = 2;
            +    x + 1
            +};
            +
            +

            x + 1 뒤에 세미콜론이 붙지 않은 이유는 이것이 표현식이기 때문이다. 세미콜론을 붙이면 구문이 된다. 같은 원리로 아래와 같은 문법도 허용된다:

            +
            let y = if x == 1 {
            +        10
            +    } else if x > 1 {
            +        20
            +    } else {
            +        30
            +    };
            +
            +

            자바스크립트로 같은 표현을 하려면 가변 변수를 선언해야 한다. 아니면 중첩 삼항 조건 연산자를 사용해야 하는데, 권장되는 패턴은 아니다.

            +
            let y = 0;
            +if (x === 1) {
            +  y = 10;
            +} else if (x > 1) {
            +  y = 20;
            +} else {
            +  y = 30
            +}
            +
            +

            타 언어의 switch와 비슷한 match라는 컨트롤 플로우 연산자를 사용할 수도 있다.

            +
            match x {
            +    1 => println!("one"),
            +    3 => println!("three"),
            +    5 => println!("five"),
            +    _ => (),
            +}
            +
            +

            러스트 컴파일러는 문법이 조금이라도 잘못되면 에러를 낸다. 이러한 엄격함이 러스트의 러닝 커브를 높이기도 하지만, 동시에 흑마법을 쓰고 싶은 프로그래머의 폭주를 막는 역할도 한다. 또한 러스트에 익숙하지 않은 사람도 컴파일러 말을 잘 듣다보면 기본은 되어 있는 코드를 작성할 수 있도록 만들어준다. 다행히 러스트 컴파일러는 아주 친절하기 때문에 컴파일에 실패하면 어느 부분이 왜 잘못됐고, 어떻게 고쳐야하는지 알려주니까 겁먹을 필요는 없다.

            +

            안전한 Nullable

            +

            대부분의 언어에서 null값을 non-null값으로 사용하려 할 때 문제가 발생한다. 러스트에는 null이 없다. 대신 표준 라이브러리가 제공하는 Option 열거형의 멤버로 NoneSome이 있다.

            +
            enum Option<T> {
            +    Some(T),
            +    None,
            +}
            +
            +

            Option 타입은 어떤 값이 존재하지 않을 수 있는 상황에 대응하기 위해 사용한다. Option의 멤버인 None은 값이 존재하지 않음을 의미한다. 반대로 Some은 값이 존재하는 경우의 T 타입 값을 의미한다. Optionmatch를 이용해 다룰 수 있다. nullable한 값을 사용하기 위해 Option 타입을 받는 함수 plus_one이 있다:

            +
            fn plus_one(x: Option<i32>) -> Option<i32> {
            +    match x {
            +        None => None,
            +        Some(i) => Some(i + 1),
            +    }
            +}
            +
            +let one: Option<i32> = Some(1);
            +let two = plus_one(one); // Some(2)
            +let none = plus_one(None); // None
            +
            +

            Option<i32> 타입 파라미터 x가 존재하지 않으면 그대로 None을 반환하고, 값이 존재하면 1을 더한 값을 반환한다. matchOption 타입을 비교할 때는 반드시 SomeNone에 대한 처리를 모두 해야한다. 또한 Option<T> 타입을 T 타입과 연산하려면 Option<T>T로 변환하는 과정을 거쳐야 한다. 이렇게 하면 T 타입과 연산하는 대상이 존재한다는 사실을 보장 할 수 있다. 변수에 단순히 null을 할당해 사용하는 것보다 훨씬 안전하다.

            +

            안전한 메모리 관리

            +

            러스트는 오너십이라는 방식으로 메모리를 관리한다. C에서는 malloc이나 free같은 함수를 이용해 프로그래머가 직접 메모리를 할당, 해제한다. 자바에서는 가비지 컬렉터(Garbage collector)가 돌며 메모리를 정리한다. (Java는 어떻게 Garbage Collection을 할까? ) 개발자가 직접 메모리를 관리하면 실수할 위험이 너무 크고, 가비지 컬렉터를 이용하면 프로그램 성능이 저하된다. 이런 점에서 오너십이라는 새로운 방식의 메모리 관리 방식은 혁신적이다.

            +

            오너십은 말그대로 값에 대한 변수의 소유권에 관한 것이며, 오너십에는 세 가지 원칙이 있다:

            +
              +
            • 각 값은 오너(Owner)라고 불리는 변수를 갖는다.
            • +
            • 한 번에 하나의 오너만 가질 수 있다.
            • +
            • 오너가 스코프를 벗어나면 값이 버려진다.
            • +
            +

            Copy

            +
            let x = 5;
            +let y = x; // `y` is 5, and `x` is still valid
            +
            +

            위 코드는 정수 5를 변수 x에 바인딩(Binding)한 뒤, x 값의 복사본을 y에 바인딩한다. 두 값은 모두 5다. 값을 '복사’했기 때문이다. 정수, 불리언 등 스택 메모리를 사용하는 대부분의 원시 타입 값은 복사된다. 이처럼 스택 메모리 데이터는 크기가 고정되어 있기 때문에 값을 복사할 수 있지만, 힙 메모리를 사용하는 타입은 그렇지 않다.

            +

            Move

            +
            let s1 = String::from("hello");
            +let s2 = s1; // `s2` is "hello", and `s1` is no longer valid
            +
            +

            s1String::from("hello")를 할당했다. let s = "hello"처럼 문자열 리터럴(String literal)을 할당하는 것과는 다르다. 문자열 리터럴은 프로그램에 하드코딩되며, 문자열을 자르거나 이어 붙이는 등의 변경을 할 수 없다. 반면 String 타입은 힙 메모리에 할당되기 때문에 런타임에 문자열을 수정할 수 있다.

            +

            이어서 s2s1을 할당했다. s2가 "hello"인 것은 자명하다. 그런데 이제 s1은 유효하지 않다. 오너십이 '이동’했기 때문이다. String 타입은 메모리 포인터(ptr), 길이(len), 용량(capacity) 세 정보를 스택 메모리에 담는다. 처음에 s1의 포인터는 힙 메모리에 있는 데이터 "hello"의 0번 인덱스를 가리켰다.

            +
            let s1 = String::from("hello");
            +
            +println!("{:?}", s1.as_ptr()); // "0x56397fd89a40"
            +
            ++----------+---+        +---+---+
            +| ptr      | ---------->| 0 | h |
            ++----------+---+        +---+---+
            +| len      | 5 |        | 1 | e |
            ++----------+---+        +---+---+
            +| capacity | 5 |        | 2 | l |
            ++----------+---+        +---+---+
            +       s1               | 3 | l |
            +                        +---+---+
            +                        | 4 | o |
            +                        +---+---+
            +
            +

            또 다른 변수 s2를 만들어 s2s1을 할당하면 스택의 s1 데이터가 복사된다. 이때 힙에 있는 데이터는 복사되지 않는다. 단지 s1과 같은 포인터를 가진 s2가 만들어진다.

            +
            let s1 = String::from("hello");
            +let s2 = s1;
            +
            ++----------+---+         +---+---+
            +| ptr      | -------+--->| 0 | h |
            ++----------+---+    |    +---+---+
            +| len      | 5 |    |    | 1 | e |
            ++----------+---+    |    +---+---+
            +| capacity | 5 |    |    | 2 | l |
            ++----------+---+    |    +---+---+
            +       s1           |    | 3 | l |
            +                    |    +---+---+
            ++----------+---+    |    | 4 | o |
            +| ptr      | -------+    +---+---+
            ++----------+---+
            +| len      | 5 |
            ++----------+---+
            +| capacity | 5 |
            ++----------+---+
            +       s2
            +
            +

            합당한 동작같지만 여기엔 함정이 있다. 러스트는 변수가 스코프를 벗어났을 때 자동으로 drop 메소드를 호출해 힙 메모리를 정리한다. 그런데 위와 같이 s1s2가 같은 힙 메모리 주소를 가리키면 s1이 스코프를 벗어났을 때 메모리가 한 번 해제되고, 그 뒤에 s2가 스코프를 벗어날 때 같은 메모리 공간를 다시 해제하게 된다. 메모리를 두 번 해제하면 메모리 변형(Corruption)을 일으킬 수 있으며, 이는 보안 취약점으로 이어진다.

            +

            그래서 러스트는 할당된 스택 메모리를 복사할 때 기존에 할당한 변수 s1을 무효화한다. 따라서 s2 변수만이 힙 메모리 데이터 "hello"를 가리킨다.

            +
            let s1 = String::from("hello");
            +let s2 = s1;
            +
            +println!("{:?}", s1.as_ptr()); // error[E0382]: borrow of moved value: `s1`
            +println!("{:?}", s2.as_ptr()); // "0x56397fd89a40"
            +
            +                         +---+---+
            +                    +--->| 0 | h |
            +                    |    +---+---+
            +                    |    | 1 | e |
            +                    |    +---+---+
            +                    |    | 2 | l |
            +                    |    +---+---+
            +                    |    | 3 | l |
            +                    |    +---+---+
            ++----------+---+    |    | 4 | o |
            +| ptr      | -------+    +---+---+
            ++----------+---+
            +| len      | 5 |
            ++----------+---+
            +| capacity | 5 |
            ++----------+---+
            +       s2
            +
            +

            이제 s2가 스코프를 벗어날 때 한 번만 메모리를 해제하면 된다. 이를 s1의 오너십이 s2로 이동했다고 말한다. 힙 메모리를 사용하는 String 타입이나 Vec 타입 등 비원시 타입들은 오너십이 이동한다. 이런 타입들에 대해 깊은 복사를 하고 싶다면 clone 메소드를 사용해야 한다.

            +

            함수 인자로 값을 넘길 때도 마찬가지로 오너십이 이동한다:

            +
            let s = String::from("hello");
            +takes_ownership(s);
            +// `s` is no longer valid here
            +
            +

            변수 stakes_ownership 함수의 인자로 넘어가면서 오너십도 이동한다. 따라서 값을 넘긴 이후에는 s를 사용할 수 없다. 반대로 함수에서 값을 반환할 때도 오너십이 이동한다.

            +

            References & Borrowing

            +

            함수의 인자로 값을 넘기되, 오너십을 이동시키고 싶지 않을 때는 값의 참조(Reference)만 넘겨주면 된다. 이를 빌림(Borrowing)이라고 한다.

            +
            fn main() {
            +    let s1 = String::from("hello");
            +    let len = get_length(&s1);
            +    println!("{}: {}", s1, len); // "hello: 5"
            +}
            +
            +fn get_length(s2: &String) -> usize {
            +    s2.len()
            +}
            +
            +

            이렇게 하면 get_length 내에서 s2의 포인터가 s1을 가리키고, s1은 힙 메모리의 “hello” 데이터를 가리키게 된다. 함수가 참조만 받았기 때문에 함수 호출 이후에도 s1은 유효하다.

            +
            fn main() {
            +    let s1 = String::from("hello");
            +    let len = get_length(&s1);
            +    println!("{:?}", s1.as_ptr()); // "0x5581762b0a40"
            +}
            +
            +fn get_length(s2: &String) -> usize {
            +    println!("{:?}", s2.as_ptr()); // "0x5581762b0a40"
            +    s2.len()
            +}
            +
            ++----------+---+        +----------+---+        +---+---+
            +| ptr      | ---------->| ptr      | ---------->| 0 | h |
            ++----------+---+        +----------+---+        +---+---+
            +       s2               | len      | 5 |        | 1 | e |
            +                        +----------+---+        +---+---+
            +                        | capacity | 5 |        | 2 | l |
            +                        +----------+---+        +---+---+
            +                               s1               | 3 | l |
            +                                                +---+---+
            +                                                | 4 | o |
            +                                                +---+---+
            +
            +

            만약 참조를 이용해 값을 바꾸고 싶다면 가변 참조(Mutable reference)를 빌려야 한다:

            +
            fn main() {
            +    let mut hello = String::from("hello");
            +    change(&mut hello);
            +    println!("{}", hello); // "hello, world"
            +}
            +
            +fn change(s: &mut String) {
            +    s.push_str(", world");
            +}
            +
            +

            &mut 키워드를 이용해 가변 참조를 넘기면 change 함수 안에서 인자로 받은 값을 변경할 수 있다. 이를 가변 빌림(Mutable borrowing)이라고 한다. change 함수에서 가변 참조로 받은 문자열 s 뒤에 “, world” 문자열을 덧붙여 반환했는데, 겉으로 보면 힙 메모리의 “hello” 데이터 뒤에 문자열을 그대로 붙인 것 같다. 하지만 이미 할당한 메모리 공간을 마음대로 늘릴 수 없기 때문에 실제로는 힙 메모리에 새로운 데이터를 만들고 포인터가 가리키는 메모리 주소를 바꿔 값을 재할당해야 한다.

            +
            let mut s = String::from("hello");
            +println!("{:?}", s.as_ptr()); // "0x55765a598a40"
            +
            +s.push_str(", world");
            +println!("{:?}", s.as_ptr()); // "0x55765a598ba0"
            +
            +                                          0   1   2   3   4
            ++----------+----+        +----------------+---+---+---+---+---+
            +| ptr      |  -------+   | 0x55765a598a40 | h | e | l | l | o |
            ++----------+----+    |   +----------------+---+---+---+---+---+
            +| len      | 12 |    |
            ++----------+----+    |                    0   1   2   3   4   5   6   7   8
            +| capacity | 12 |    |   +----------------+---+---+---+---+---+---+---+---+---+
            ++----------+----+    +-->| 0x55765a598ba0 | h | e | l | l | o | , |   | w | o | ...
            +        s                +----------------+---+---+---+---+---+---+---+---+---+
            +
            +

            그런데 값을 추가할 때마다 매번 힙 메모리에 새로운 데이터를 만들면 성능에 문제가 생기기 때문에 러스트는 미래를 대비해 처음부터 메모리 공간을 조금 크게 잡아 둔다.[5]

            +
            let mut s = String::from("hello");
            +println!("{:?}", s.as_ptr()); // "0x55765a598a40"
            +
            +s.push_str(", world");
            +println!("{:?}", s.as_ptr()); // "0x55765a598a40"
            +
            +                                          0   1   2   3   4         10  11  12  13
            ++----------+----+        +----------------+---+---+---+---+---+     +---+---+---+---+
            +| ptr      |  ---------->| 0x55765a598a40 | h | e | l | l | o | ... | l | d |   |   | ...
            ++----------+----+        +----------------+---+---+---+---+---+     +---+---+---+---+
            +| len      | 12 |
            ++----------+----+
            +| capacity | 26 |
            ++----------+----+
            +        s
            +
            +

            capacity가 26이기 때문에 그보다 적은 개수의 문자를 추가할 때는 포인터가 가리키는 힙 메모리 주소나 capacity의 값이 변하지 않는다. 즉, 재할당이 필요하지 않다.

            +

            가변 참조를 빌려줄 때 주의할 점은 한 스코프 안에서 가변 참조는 한 번만 전달할 수 있다는 것이다.

            +
            let mut s = String::from("hello");
            +
            +let r1 = &mut s;
            +let r2 = &mut s;
            +
            +r1.push_str(", world"); // error[E0499]: cannot borrow `s` as mutable more than once at a time
            +
            +

            r1에 가변 참조를 빌려준 다음, 바로 r2에 가변 참조를 빌려줬기 때문에 r2가 아닌 r1을 이용해 값을 변경하려 하면 오류가 발생한다. 러스트 컴파일러가 에러의 이유와 위치를 친절하게 알려주기 때문에 쉽게 고칠 수 있다.

            +
            error[E0499]: cannot borrow `s` as mutable more than once at a time
            + --> src/main.rs:5:14
            +  |
            +4 |     let r1 = &mut s;
            +  |              ------ first mutable borrow occurs here
            +5 |     let r2 = &mut s;
            +  |              ^^^^^^ second mutable borrow occurs here
            +6 |
            +7 |     r1.push_str(", world");
            +  |     -- first borrow later used here
            +
            +

            이런 제약을 만듦으로써 러스트는 컴파일 타임에 경쟁 상태(Race condition)를 방지할 수 있다. 경쟁 상태는 (1)두 개 이상의 포인터가 동시에 같은 데이터에 접근하며 (2)최소 하나의 포인터가 데이터 변경을 시도하고 (3)데이터를 동기화하는 메커니즘이 없는 경우에 충족된다. 데이터 경쟁은 예상치 못한 문제를 일으키며, 런타임에 알아내기도 힘들다.

            +

            오너십 모델의 최대 장점이라면 컴파일 타임에 메모리 오류를 잡을 수 있다는 것이라고 생각한다. 다른 프로그래밍 언어를 사용할 때는 잘못된 메모리 공간을 참조해서 런타임 중에 세그먼트 폴트가 일어나고 프로그램이 죽는 일이 허다했다. 하지만 러스트에선 일단 컴파일만 잘 되면 런타임에 프로그램이 예상치 못하게 죽는 일이 거의 없으며, 코드 레벨에서 안전을 보장하기 때문에 런타임 오버헤드 역시 없다. 뿐만 아니라 동시성 프로그래밍에서 일어나는 많은 이슈를 피할 수도 있다.

            +

            안전을 위한 에러 핸들링

            +

            panic! 매크로는 에러 메시지를 출력하고 프로그램의 스택을 되돌린 뒤 종료시킨다. 이를 이용하면 프로그램을 중단해야 할 정도로 심각한 문제가 예상될 때 의도적으로 에러를 일으킬 수 있다.

            +
            panic!("crash and burn"); // thread 'main' panicked at 'crash and burn'
            +
            +

            하지만 모든 에러가 프로그램을 중단해야 할 정도로 심각한 것은 아니다. 그런 에러를 유연하게 처리하기 위해 러스트는 Result 열거형을 제공한다.

            +
            enum Result<T, E> {
            +    Ok(T),
            +    Err(E),
            +}
            +
            +

            함수의 결과를 Result로 반환하면 함수의 호출처에서는 match를 이용해 예외 처리를 해줄 수 있다.

            +
            let file = File::open("data");
            +let file = match file {
            +    Ok(f) => f,
            +    Err(error) => panic!("Failed to open the file: {:?}", error)
            +};
            +
            +

            data 파일을 여는 open 메소드가 잘 동작했으면 오픈한 파일을 그대로 file에 할당하고, 문제가 있으면 panic! 매크로를 통해 에러를 일으킨다. 에러의 종류에 따라 중첩해서 분기할 수도 있다. 아래 코드는 파일 열기를 시도했을 때 해당 파일이 없으면 파일을 생성하며, 그 외에는 에러를 일으킨다.

            +
            let file = File::open("data");
            +let file = match file {
            +      Ok(f) => f,
            +      Err(error) => match error.kind() {
            +          ErrorKind::NotFound => match File::create("data") {
            +              Ok(fc) => fc,
            +              Err(e) => panic!("Failed to create file: {:?}", e),
            +          },
            +          other_error => panic!("Failed to open file: {:?}", other_error),
            +      },
            +  };
            +
            +
            +

            Result 타입은 좀 더 간단한 에러 핸들링을 위해 unwrap 메소드를 제공한다.

            +
            let file = File::open("data").unwrap();
            +
            +

            unwrap 메소드는 ResultOkOk의 값을 그대로 반환하고, Err이면 panic! 매크로를 호출해 에러를 일으킨다. unwrap과 비슷하지만 에러 메시지를 직접 설정할 수 있는 expect 메소드도 있다.

            +
            let file = File::open("data").expect("Failed to open the data file");
            +
            +

            unwrap을 남용하는 것보다는 expect를 이용해 에러 메시지를 구체적으로 설정하는 것이 좋다. 함수 안에서 함수의 호출처로 에러를 전파할 수도 있다. 간단히 ?를 붙여주면 된다.

            +
            fn open_file() -> Result<File, io::Error> {
            +    let file = File::open("data")?;
            +    // do stuff with `file`
            +    Ok(file)
            +}
            +
            +

            위 코드는 openResultOkOk의 값을 그대로 반환 뒤 다음 내용을 계속 진행한다. 반대로 Err이면 Err을 반환하고 함수를 빠져 나온다. ? 연산자는 에러가 발생했을 때 함수의 결과로 Err을 반환하기 때문에 반드시 Result 타입을 반환하는 함수에서만 사용할 수 있다는 점에 주의해야 한다.

            +

            No Silver Bullet

            +

            사실 어떤 기술을 찬양하는 경우는 그 기술에 대한 이해가 부족하거나, 완벽히 마스터했거나 둘 중 하나다. 나는 전자에 가깝기 때문에 장점만 알고 있다. 단점을 알기 위해서는 더 많은 사용 경험이 필요하다. 이것도 토이 프로젝트에 적용한 경험과 프로덕션에 적용한 경험 사이에 큰 차이가 있는 것이 사실이다.

            +

            현재로써 러스트의 가장 큰 단점은 신생 언어이다 보니 자료가 많지 않고, 그나마 있는 것도 지금은 유효하지 않은 정보라는 점이다. 심지어 동명의 게임이 있어서 “rust lang” 또는 "러스트 언어"로 검색하지 않는 이상 검색도 잘 안 된다. 이런 상황과 동시에 언어 자체도 러닝커브가 있는 편이다. 사람의 실수를 언어 차원에서 방지하기 위해 다양한 제약 사항이 있고, 오너십 등 러스트의 핵심 개념이 생소하게 다가오기 때문이기도 하다.

            +

            러스트는 비슷한 목표를 가진 Go 언어와 자주 비교되곤 한다. Go는 단순한 문법과 Go 루틴을 이용한 가벼운 동시성 프로그래밍이 장점이다. Go를 설계한 롭 파이크(Rob Pike)가 "갓 졸업해서 훌륭한 언어를 이해할 능력이 없는 어린 구글 직원들을 위해 단순하게 만들었다"[6]라는 발언을 해서 논란이 되기도 했는데, 표현의 적절성과 별개로 학습과 구현이 쉽고 생산성이 높은 것은 큰 장점이다. 특히 러스트의 단점이 러닝 커브이기 때문에 Go의 쉬운 문법이 더욱 부각된다. 언어 스펙만 보면 러스트가 훨씬 안전하고 다양한 기능을 지원하는 것 같지만, 현실의 모든 상황에서 꼭 하나가 우위를 차지할 수는 없을테니 상황에 맞춰서 판단하면 되겠다.

            +

            내가 러스트를 선택한 가장 큰 이유는 언어 차원의 안전성과 더불어 생태계 때문이기도 하다. 러스트는 초기부터 카고를 통한 패키지 관리를 지원한 덕분에 튼튼한 라이브러리 생태계를 가지고 있다. 러스트 생태계와 방대한 웹 생태계 사이 교집합이 있기 때문에 미래도 밝다. 러스트 프로그램을 웹어셈블리로 컴파일하면 npm에 패키지를 배포할 수 있고, 이렇게 배포한 패키지를 자바스크립트 어플리케이션에서 그대로 설치해서 사용할 수 있다.[7] 웹어셈블리 뿐만 아니라 FFI(Foreign Function Interface)를 통해 C/C++, 파이썬 등 다른 언어로 작성된 외부 함수를 러스트로 가져와 사용하거나 러스트로 작성한 함수를 다른 언어에서 사용하도록 할 수 있다. 기술적인 생태계 뿐 아니라 커뮤니티도 굉장히 활발하고 친절하다.

            +

            이런 흐름이라면 'C/C++ 대체’라는 러스트의 큰 그림이 정말 이뤄질 수도 있을 것 같다.

            +

            References

            + +
            +
            +
              +
            1. Karen Rustad Tölva, “Hello, crustaceans”, rustacean.net. ↩︎

              +
            2. +
            3. American Psychological Association, “Singular ‘They’”, apastyle.apa.org. ↩︎

              +
            4. +
            5. GitHub, “The State of the Octoverse”, 2019. ↩︎

              +
            6. +
            7. The Rust Programming Language, “Production users”, rust-lang.org. ↩︎

              +
            8. +
            9. Rustdoc, “Struct Vec - Capacity and reallocation”, doc.rust-lang.org. ↩︎

              +
            10. +
            11. Rob Pike, “From Parallel to Concurrent”, 2014. ↩︎

              +
            12. +
            13. MDN web docs “Compiling from Rust to WebAssembly”, 2019. ↩︎

              +
            14. +
            +
            + +
            +
            + +
            + +
            +
            +

            인터넷이 동작하는 아주 구체적인 원리

            +

            학교에서 구글에 접속하는 과정

            +
            +
            +
            + + +
            + +
            +
            +

            🗞️ 훈련소에서 매일 뉴스 받아보기

            +

            고립된 훈련병을 위한 종합 뉴스

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/36.html b/article/36.html new file mode 100644 index 0000000..0da39e8 --- /dev/null +++ b/article/36.html @@ -0,0 +1,430 @@ + + + + + + 인터넷이 동작하는 아주 구체적인 원리 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 인터넷이 동작하는 아주 구체적인 원리 +

            + +

            + 학교에서 구글에 접속하는 과정 +

            + + + + +
            +
            Table of Contents +

            +
            +

            학교에서 노트북으로 구글(www.google.com)에 접속하는 과정을 살펴본다. 이 글에 등장하는 KT와 Google Fiber, 구글의 네트워크 구조, 노드의 IP 주소와 MAC 주소는 모두 간소화 또는 가정한 것이다. 또한 클라이언트와 서버가 패킷을 주고받는 전체 과정을 훑기 위해 프록시와 캐시는 생략했다.

            +

            학교 네트워크에는 여러 AP(Access Point)가 있고, AP들은 스위치에 연결되어 있다. 스위치들은 게이트웨이 라우터와 연결되어 있으며, 라우터는 SK브로드밴드나 KT와 같은 ISP(Internet Service Provider)의 네트워크에 연결되어 있다.

            +
            School network 68.80.2.0/24
            +
            +        +----+   +--------+    /------\    +--------+
            +Node ---| AP |---| Switch |---| Router |---| Switch |--- ...
            +        +----+   +--------+    \------/    +--------+
            +          |           |            |
            +         ...         ...           |
            +                                   |
            +###################################|#################
            +KT network 68.80.0.0/13            |
            +                                   |
            +                               /------\
            +                       ... ---| Router |
            +                               \------/
            +                                   |
            +                                  ...
            +
            +

            Getting Started: WLAN, DHCP, UDP and IP

            +

            먼저 노트북을 켜고 AP(Access Point)를 검색한다. 가까운 AP들이 자신의 범위에 있는 모든 노드에게 비컨(Beacon) 프레임을 브로드캐스팅하며 자신의 존재를 알리고 있기 때문에 클라이언트는 AP 리스트를 확인하고 연결할 AP를 선택할 수 있다. 이렇게 AP가 신호를 보내는 방식을 패시브 스캐닝(Passive scanning)이라고 하며, 반대로 노드가 AP를 탐색하는 방식은 액티브 스캐닝(Active scanning)이라고 한다.

            +
            +-----------------+        +----------------+   +----+
            +| Client (Laptop) |<---+---| SSID: iptime01 |---| AP |
            ++-----------------+    |   | Security: WPA2 |   +----+
            +                       |   +----------------+
            +              Node <---+
            +                       |
            +              Node <---+
            +
            +

            클라이언트가 iptime01을 선택하면 AP로 접속 요청 프레임을 전송한다. 요청 프레임을 받은 AP는 응답 프레임을 클라이언트에게 보내 연결을 마친다. 흔히 사용하는 Wi-Fi 통신이 이러한 IEEE 802.11 표준을 바탕으로 한다.

            +

            처음으로 학교 네트워크에 연결된 것이기 때문에 이 클라이언트에게는 아직 IP 주소가 없다. 이제 클라이언트에 IP 주소를 할당하기 위해 DHCP(Dynamic Host Configuration Protocol)가 동작한다. IP 할당을 서비스하는 DHCP 서버는 AP나 라우터에서 동작할 수 있다.

            +

            클라이언트의 운영체제가 클라이언트의 68번 포트에서 출발해 수신지의 67번 포트로 도착하는 DHCPD DISCOVER 메시지를 만든다. DHCP DISCOVER 메시지는 UDP로 전송될 것이다. 아직 클라이언트는 IP 주소를 할당받지 못한 상태이며, 클라이언트의 IP 주소가 없기 때문에 DHCP 메시지의 src, yiaddr 필드는 0.0.0.0이다. 또한 AP에 연결된 모든 노드에 메시지를 브로드캐스트할 것이므로 dest 필드는 255.255.255.255이다.

            +

            DHCP DISCOVER 메시지의 프레임은 수신지 MAC 주소(FF:FF:FF:FF:FF:FF)를 가지고 있으며, AP에 연결된 모든 장치에 브로드캐스트된다. 이때 프레임의 발신지 MAC 주소는 클라이언트의 MAC 주소(00:16:D3:23:68:8A)다.

            +
            +-------------------+   +---------------------------+    +----+
            +| Client (Laptop)   |---| src: 0.0.0.0, 68          |--->| AP | 68.85.2.9
            +| 00:16:D3:23:68:8A |   | dest: 255.255.255.255, 67 |    +----+ 00:1F:57:21:A8:3C
            ++-------------------+   | DHCPDISCOVER              |      |
            +                        | yiaddr: 0.0.0.0           |      +---> Node
            +                        | transaction ID: 654       |      |
            +                        +---------------------------+      +---> Node
            +
            +

            AP가 DHCP 메시지를 받으면 클라이언트의 MAC 주소(00:16:D3:23:68:8A)와 IP 데이터그램을 프레임에서 추출한다. 데이터그램의 수신지 IP 주소는 상위 레이어에 의해 처리된다.

            +

            AP에서 동작하는 DHCP 서버는 CIDR(Classless Inter-Domain Routing) 블록의 IP 주소를 할당할 수 있다. 교내 네트워크에서 사용 중인 모든 IP 주소는 KT의 주소 블록에 속한 것이다. DHCP 서버는 클라이언트에 할당하기 위한 IP 주소(68.85.2.101)를 포함해 DHCP OFFER 메시지를 만든다. 이때 DHCP OFFER 메시지를 담아 클라이언트에게 UDP로 전송되는 프레임은 AP의 발신지 MAC 주소(00:1F:57:21:A8:3C)와 수신지 MAC 주소(00:16:D3:23:68:8A)를 가지고 있다.

            +
            +-------------------+    +---------------------------+   +----+
            +| Client (Laptop)   |<---| src: 68.85.2.9, 67        |---| AP | 68.85.2.9
            +| 00:16:D3:23:68:8A |    | dest: 255.255.255.255, 68 |   +----+ 00:1F:57:21:A8:3C
            ++-------------------+    | DHCPOFFER                 |
            +                         | yiaddr: 68.85.2.101       |
            +                         | transaction ID: 654       |
            +                         | DHCP server ID: 68.85.2.9 |
            +                         | Lifetime: 3600 secs       |
            +                         +---------------------------+
            +
            +

            DHCP OFFER 응답을 받은 클라이언트는 DHCP OFFER의 값을 바탕으로 자신의 설정 값을 담아 DHCP REQUEST 메시지를 브로드캐스팅한다. 만약 여러 DHCP OFFER 메시지를 받았다면 그 중 하나를 선택한다.

            +
            +-------------------+   +---------------------------+    +----+
            +| Client (Laptop)   |---| src: 0.0.0.0, 68          |--->| AP | 68.85.2.9
            +| 00:16:D3:23:68:8A |   | dest: 255.255.255.255, 67 |    +----+ 00:1F:57:21:A8:3C
            ++-------------------+   | DHCPREQUEST               |      |
            +                        | yiaddr: 68.85.2.101       |      +---> Node
            +                        | transaction ID: 655       |      |
            +                        | DHCP server ID: 68.85.2.2 |      +---> Node
            +                        | Lifetime: 3600 secs       |
            +                        +---------------------------+
            +
            +

            DHCP REQUEST를 받은 라우터는 DHCP ACK 메시지를 브로드캐스팅해 요청받은 설정 값을 승인한다.

            +
            +-------------------+        +---------------------------+   +----+
            +| Client (Laptop)   |<---+---| src: 68.85.2.9, 67        |---| AP | 68.85.2.9
            +| 68.85.2.101       |    |   | dest: 255.255.255.255, 68 |   +----+ 00:1F:57:21:A8:3C
            +| 00:16:D3:23:68:8A |    |   | DHCPACK                   |
            ++-------------------+    |   | yiaddr: 68.85.2.101       |
            +                         |   | transaction ID: 655       |
            +                Node <---+   | DHCP server ID: 68.85.2.9 |
            +                         |   | Lifetime: 3600 secs       |
            +                Node <---+   +---------------------------+
            +
            +

            클라이언트는 DHCP ACK 메시지를 추출하고 자신의 IP 주소와 DNS 서버의 IP 주소를 기록한다. 이렇게 할당된 IP 주소는 기본적으로 임대한 것이기 때문에 일정 기간 사용하지 않으면 다시 반납되어 새로운 IP를 할당받아야 한다.

            +

            Still Getting Started: Ethernet, DNS and ARP

            +

            클라이언트가 브라우저에서 www.google.com과 같은 URL을 입력하면 여러 과정을 거쳐 구글의 홈페이지가 클라이언트의 브라우저에 보여질 것이다. 그전에 클라이언트는 www.google.com의 IP 주소를 알아내야 한다. 이때 도메인 네임을 IP 주소로 변환하기 위해 DNS 프로토콜이 사용된다.

            +

            DNS 쿼리를 위해 클라이언트는 DNS 쿼리 메시지를 담은 프레임을 교내 네트워크에 있는 로컬 DNS 서버에 보낼 것이다. DNS 서버는 라우터에서 동작할 수도 있는데, 이 경우에는 별도의 로컬 DNS 서버가 운영되고 있다.

            +

            로컬 DNS 서버가 없는 경우에는 ISP의 DNS 서버로 전송한다. 그런 경우 만약 ISP가 특정 사이트에 대한 접속을 차단하고자 한다면 요청 받은 도메인 네임의 IP 주소가 아닌 warning.or.kr의 IP 주소를 응답해줄 수 있다.

            +

            현재 클라이언트는 로컬 DNS 서버의 MAC 주소를 모른다. 이를 알아내기 위해 ARP(Address Resolution Protocol)를 사용한다.

            +

            클라이언트가 로컬 DNS 서버의 IP 주소와 함께 ARP 쿼리 메시지를 만든다. ARP 메시지는 AP로 전송되며, AP에 이더넷(Ethernet)으로 연결된 스위치로도 브로드캐스팅된다. 스위치 역시 로컬 DNS 서버의 MAC 주소를 모르기 때문에 자신에게 연결된 모든 장비에 프레임을 전송한다. 이더넷은 유선 LAN(Local Area Network)을 구성하기 위한 통신 표준이며, IEEE 802.3으로 정의되어 있다.

            +
            +-------------------+   +------------------------------+    +----+    +--------+     /------\
            +| Client (Laptop)   |---| Sender HA: 00-16-D3-23-68-8A |--->| AP |--->| Switch |--->| Router | 68.85.2.1
            +| 68.85.2.101       |   | Sender IP: 68.85.2.101       |    +----+    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |   | Target HA: 00-00-00-00-00-00 |                |    |
            ++-------------------+   | Target IP: 68.85.2.2         |                |    |     +------------------+
            +                        +------------------------------+                |    +---->| Local DNS Server | 68.85.2.2
            +                                                                        |          +------------------+
            +                                                                        |
            +                                                                        +---------> Node
            +
            +

            로컬 DNS 서버의 MAC 주소를 모르기 때문에 Target HA 필드의 값은 00-00-00-00-00-00으로 설정된다.

            +

            로컬 DNS 서버가 ARP 쿼리 메시지를 포함한 프레임을 받으면 자신의 MAC 주소를 담아 ARP 응답을 준비한다. ARP 응답 메시지는 수신지 주소와 함께 이더넷 프레임에 담기며, 프레임은 스위치로 전송된다. 이어서 스위치는 클라이언트에 프레임을 전송한다.

            +
            +-------------------+    +----+    +--------+    +------------------------------+   +------------------+
            +| Client (Laptop)   |<---| AP |<---| Switch |<---| Sender HA: 00-16-D3-23-68-8A |---| Local DNS Server | 68.85.2.2
            +| 68.85.2.101       |    +----+    +--------+    | Sender IP: 68.85.2.101       |   +------------------+ 00:07:89:1C:43:2F
            +| 00:16:D3:23:68:8A |                            | Target HA: 00:07:89:1C:43:2F |
            ++-------------------+                            | Target IP: 68.85.2.2         |
            +                                                 +------------------------------+
            +
            +

            클라이언트가 ARP 응답 메시지를 담은 프레임을 받고, ARP 응답 메시지에서 로컬 DNS 서버의 MAC 주소(00:07:89:1C:43:2F)를 추출한다. 이제 클라이언트가 로컬 DNS 서버의 MAC 주소를 알게 되었으므로 DNS 쿼리 메시지를 보낼 수 있다.

            +

            클라이언트의 운영체제는 DNS 쿼리 메시지를 만들고 메시지의 질의 섹션에 www.google.com 문자열을 넣는다. DNS 메시지에 이어 로컬 DNS 서버의 53포트로 향하는 UDP 세그먼트와 IP 데이터그램이 붙여진다. 이 프레임의 IP 데이터그램은 수신지 IP 주소를 가지고 있다. 클라이언트는 프레임을 AP로 보내고 AP는 스위치로 프레임을 전달한다. 스위치는 자신에게 연결된 로컬 DNS 서버에게 프레임을 전송한다.

            +
            +-------------------+   +---------------------------------------+    +----+    +--------+    +------------------+
            +| Client (Laptop)   |---| Identification, Flags (OP, Query type,|--->| AP |--->| Switch |--->| Local DNS Server | 68.85.2.2
            +| 68.85.2.101       |   | AA, TC, RD, RA, Response Type)        |    +----+    +--------+    +------------------+ 00:07:89:1C:43:2F
            +| 00:16:D3:23:68:8A |   | Questions (Query name, type, class )  |
            ++-------------------+   | Answers, Authority, Additional Info   |
            +                        +---------------------------------------+
            +
            +

            Still Getting Stated: Intra-Domain Routing to the DNS Server

            +

            로컬 DNS 서버는 DNS 쿼리 메시지를 보고 www.google.com의 IP 주소를 찾는다. 로컬 DNS 서버가 해당 IP 주소를 알고 있으면 클라이언트에게 바로 응답을 줄 수 있다. 하지만 지금 로컬 DNS 서버는 www.google.com의 IP 주소를 모르기 때문에 루트 DNS 서버에 DNS 쿼리를 전송할 것이다. 루트 DNS 서버는 전세계에 13대 뿐이며, 한국을 비롯한 다양한 나라에 미러 서버가 운영되고 있다.

            +

            루트 DNS 서버는 외부 네트워크에 있기 때문에 게이트웨이 라우터를 통과해야한다. 이때 클라이언트는 라우터의 MAC 주소를 모르기 때문에 ARP를 사용해 앞서 로컬 DNS 서버의 MAC 주소를 알아낼 때와 같은 과정을 거친다.

            +
            +-------------------+   +------------------------------+    +--------+     /------\
            +| Local DNS Server  |---| Sender HA: 00:07:89:1C:43:2F |--->| Switch |--->| Router | 68.85.2.1
            +| 68.85.2.2         |   | Sender IP: 68.85.2.2         |    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:07:89:1C:43:2F |   | Target HA: 00-00-00-00-00-00 |      |    |
            ++-------------------+   | Target IP: 68.85.2.1         |      |    |
            +                        +------------------------------+      |    +-----> Node
            +                                                              |
            +                                                              |
            +                                                              +----------> Node
            +
            +

            라우터는 프레임을 받고 DNS 쿼리를 담은 IP 데이터그램을 추출한 뒤, 데이터그램의 수신지 주소를 보고 포워딩 테이블에 따라 데이터그램을 보내야할 KT 네트워크의 라우터를 결정한다. IP 데이터그램은 링크 레이어 프레임에 담겨 있으며, 학교의 라우터와 KT 라우터 사이의 링크를 통해 프레임이 전송된다.

            +

            KT 네트워크의 라우터는 프레임을 받은 뒤 IP 데이터그램을 추출해 수신지의 IP 주소를 확인한다. 이어서 자신의 DNS 서버에서 요청받은 www.google.com에 대응하는 IP 주소를 찾는다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+    +--------+     /------\
            +| Local DNS Server  |<-->| Switch |<-->| Router | 68.85.2.1
            +| 68.85.2.2         |    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:07:89:1C:43:2F |                       ^
            ++-------------------+                       |
            +                                            |
            +############################################|#####
            +KT network 68.80.0.0/13                     |
            +                                            v
            +                  +---------------+     /-------\
            +                  | KT DNS Server |<-->| Routers |
            +                  +---------------+     \-------/
            +
            +

            만약 KT 자신의 DNS 서버에 일치하는 레코드가 없다면 루트 DNS 서버에게 요청을 보내야 한다. 먼저 출력 인터페이스를 결정하고, 루트 DNS 서버의 네트워크로 DNS 쿼리 프레임을 보낸다. DNS 쿼리 프레임을 받은 루트 DNS 서버는 쿼리 메시지를 추출해 www.google.com의 TLD(Top-Level Domain)가 .com인 것을 보고 com TLD 서버의 IP 주소를 응답한다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+    +--------+     /------\
            +| Local DNS Server  |<-->| Switch |<-->| Router | 68.85.2.1
            +| 68.85.2.2         |    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:07:89:1C:43:2F |                       ^
            ++-------------------+                       |
            +                                            |
            +############################################|#####
            +KT network 68.80.0.0/13                     |
            +                                            v
            +                                        /-------\
            +                                       | Routers |
            +                                        \-------/
            +                                            ^
            +                                            |
            +############################################|#####
            +Root DNS Server network                     |
            +                                            v
            +                +-----------------+     /-------\
            +                | Root DNS Server |<-->| Routers |
            +                +-----------------+     \-------/
            +
            +

            응답을 받은 로컬 DNS 서버는 com TLD 서버에 DNS 쿼리를 보낸다. com TLD 서버는 IP 주소를 담은 DNS 리소스 레코드를 찾는다. TLD 서버도 www.google.com의 IP 주소를 모른다면 구글의 네임서버 IP를 응답한다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+    +--------+     /------\
            +| Local DNS Server  |<-->| Switch |<-->| Router | 68.85.2.1
            +| 68.85.2.2         |    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:07:89:1C:43:2F |                       ^
            ++-------------------+                       |
            +                                            |
            +############################################|#####
            +KT network 68.80.0.0/13                    ...
            +############################################|#####
            +TLD DNS Server network                      |
            +                                            v
            +             +--------------------+     /-------\
            +             | com TLD DNS Server |<-->| Routers |
            +             +--------------------+     \-------/
            +
            +

            구글 네임서버는 호스트네임을 IP 주소로 매핑한 데이터를 담아 www.google.com의 IP 주소를 응답한다. 구글의 서버는 해외에 있기 때문에 KT 네트워크에 연결된 해저 케이블을 타고 구글 서버가 위치한 국가의 ISP 망을 거쳐야 한다. (TeleGeography Submarine Cable Map에서 해저 케이블 지도를 볼 수 있다.) 미국에는 AT&T, Comcast 등의 ISP가 있으며, 구글은 직접 운영하는 ISP인 Google Fiber의 네트워크를 사용한다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+    +--------+     /------\
            +| Local DNS Server  |<-->| Switch |<-->| Router | 68.85.2.1
            +| 68.85.2.2         |    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:07:89:1C:43:2F |                       ^
            ++-------------------+                       |
            +                                            |
            +############################################|#####
            +KT network 68.80.0.0/13                    ...
            +############################################|#####
            +Google Fiber network 172.80.0.0/13         ...
            +############################################|#####
            +Google network 172.217.20.0/19              |
            +                                            v
            +               +------------------+     /-------\
            +               | Auth. DNS Server |<-->| Routers |
            +               +------------------+     \-------/
            +
            +

            클라이언트는 DNS 메시지에서 www.google.com의 IP 주소를 추출한다. 이제 클라이언트는 www.google.com 서버를 만날 준비가 되었다.

            +

            Web Client-Server Interaction: TCP and HTTP

            +

            클라이언트는 HTTP GET 메시지를 보내기 위해 TCP 소켓을 만든다. 먼저 three-way handshake를 거쳐 www.google.com과 TCP 연결을 구축해야 한다. 클라이언트가 먼저 TCP SYN 세그먼트를 만들어 수신지 포트를 80 포트(HTTP)로 설정하고, 스위치에 프레임을 전송한다. 이때 프레임은 수신지 MAC 주소(00:22:6B:45:1F:1B)를 가지고 있다.

            +

            학교, KT, Google Fiber, 구글 네트워크의 라우터는 각자의 포워딩 테이블을 보고 TCP SYN을 담은 데이터그램을 www.google.com 웹서버로 보낸다. ISP의 네트워크와 구글의 네트워크 사이에 있는 도메인 내 링크를 통해 전달되는 패킷들은 BGP(Border Gateway Protocol)에 따라 통제된다.

            +

            실제 구글 네트워크는 방화벽이나 로드밸런서, API 게이트웨이 등으로 더 복잡하게 구성되어 있겠지만 구글의 네트워크 아키텍쳐는 여기서 다루고자하는 핵심이 아니므로 간소화했다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+   +------------+    +----+    +--------+     /------\
            +| Client (Laptop)   |---| Seq Num: 1 |--->| AP |--->| Switch |--->| Router | 68.85.2.1
            +| 68.85.2.101       |   | SYN        |    +----+    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |   +------------+                                 |
            ++-------------------+                                                  |
            +#######################################################################|#####
            +KT network 68.80.0.0/13                                               ...
            +#######################################################################|#####
            +Google Fiber network 172.80.0.0/13                                    ...
            +#######################################################################|#####
            +Google network 172.217.20.0/19                                         |
            +                                                                       v
            +                                            +----------------+     /-------\
            +                                            | Web Server     |<---| Routers |
            +                                            | 172.217.25.228 |     \-------/
            +                                            +----------------+
            +
            +

            www.google.com에 도달한 SYN 메시지는 데이터그램에서 추출되고, 80 포트로 디멀티플렉스(Demultiplex)된다. 구글 HTTP 서버는 클라이언트와의 TCP 연결을 위해 연결 소켓을 생성하며, 이어서 TCP SYNACK 세그먼트가 생성되면 클라이언트에 전송한다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+    +----+    +--------+     /------\
            +| Client (Laptop)   |<---| AP |<---| Switch |<---| Router | 68.85.2.1
            +| 68.85.2.101       |    +----+    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |                                 ^
            ++-------------------+                                 |
            +                                                      |
            +######################################################|#####
            +KT network 68.80.0.0/13                              ...
            +######################################################|#####
            +Google Fiber network 172.80.0.0/13                   ...
            +######################################################|#####
            +Google network 172.217.20.0/19                        |
            +                                                      |
            +          +----------------+   +------------+     /-------\
            +          | Web Server     |---| Seq Num: 5 |--->| Routers |
            +          | 172.217.25.228 |   | Ack Num: 2 |     \-------/
            +          +----------------+   | SYNACK     |
            +                               +------------+
            +
            +

            TCP SYNACK 세그먼트를 담은 데이터그램이 클라이언트에 도착한다. 데이터그램은 운영체제에서 디멀티플렉스되어 앞서 만들어진 TCP 소켓으로 간다. SYNACK 세그먼트를 받은 클라이언트는 TCP ACK 응답을 보내 TCP 연결을 구축한다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+   +------------+    +----+    +--------+     /------\
            +| Client (Laptop)   |---| Seq Num: 2 |--->| AP |--->| Switch |--->| Router | 68.85.2.1
            +| 68.85.2.101       |   | Ack Num: 6 |    +----+    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |   | ACK        |                                 |
            ++-------------------+   +------------+                                 |
            +                                                                       |
            +#######################################################################|#####
            +KT network 68.80.0.0/13                                               ...
            +#######################################################################|#####
            +Google Fiber network 172.80.0.0/13                                    ...
            +#######################################################################|#####
            +Google network 172.217.20.0/19                                         |
            +                                                                       v
            +                                            +----------------+     /-------\
            +                                            | Web Server     |<---| Routers |
            +                                            | 172.217.25.228 |     \-------/
            +                                            +----------------+
            +
            +

            구글 웹 서버는 HTTPS 통신을 하므로 TLS handshake 과정도 거쳐야한다. 먼저 클라이언트가 클라이언트 랜덤(Client random) 문자열을 생성하고 ClientHello 메시지에 담아 서버에게 보낸다. ClientHello를 받은 서버는 SSL 인증서와 서버 랜덤(Server random) 문자열을 ServerHello 메시지에 담아 클라이언트에게 응답한다.

            +

            클라이언트는 서버로부터 받은 SSL 인증서의 유효성을 검증해 현재 연결된 서버가 실제로 www.google.com의 소유자라는 것을 확인하고 서버에게 공개키로 암호화한 랜덤 문자열을 보낸다. 이 문자열은 프리마스터 시크릿(Premaster secret)이라고 부르며, 구글 서버의 비밀키로만 복호화할 수 있다.

            +

            서버는 클라이언트로부터 프리마스터 시크릿을 받고 이를 자신의 비밀키로 복호화한다. 서버와 클라이언트는 각자 서버 램덤, 클라이언트 랜덤, 프리마스터 시크릿을 이용해 세션 키(Session key)를 생성한다. 서버의 세션 키와 클라이언트의 세션 키는 동일해야 한다.

            +

            마지막으로 클라이언트와 서버가 세션 키로 암호화된 Finished 메시지를 주고 받으며 TLS handshake를 마친다. 이제 클라이언트와 서버는 세션 키를 이용해 대칭키 방식으로 TLS 통신을 할 수 있다.

            +

            클라이언트의 소켓은 www.google.com으로 데이터를 보낼 준비가 되었다. 클라이언트의 브라우저는 HTTP GET 메시지를 만들고 URL을 담는다. HTTP GET 메시지는 소켓에 쓰여 TCP 세그먼트의 페이로드가 된다. TCP 세그먼트는 데이터그램에 담기고 www.google.com으로 전송된다. 실제로 패킷 캡쳐를 해보면 TLS로 암호되어 내용을 확인할 수 없다. (HTTPS는 어떻게 다를까? 참고)

            +
            School network 68.80.2.0/24
            +
            ++-------------------+   +----------------------+    +----+    +--------+     /------\
            +| Client (Laptop)   |---| GET / HTTP/2         |--->| AP |--->| Switch |--->| Router | 68.85.2.1
            +| 68.85.2.101       |   | Host: www.google.com |    +----+    +--------+     \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |   | ...                  |                                 |
            ++-------------------+   +----------------------+                                 |
            +                                                                                 |
            +#################################################################################|#####
            +KT network 68.80.0.0/13                                                         ...
            +#################################################################################|#####
            +Google Fiber network 172.80.0.0/13                                              ...
            +#################################################################################|#####
            +Google network 172.217.20.0/19                                                   |
            +                                                                                 v
            +                                                      +----------------+     /-------\
            +                                                      | Web Server     |<---| Routers |
            +                                                      | 172.217.25.228 |     \-------/
            +                                                      +----------------+
            +
            +

            www.google.com의 HTTP 서버는 TCP 소켓에서 HTTP GET 메시지를 읽고, HTTP 응답 메시지를 만든다. HTTP 응답 메시지의 body에 요청받은 콘텐츠를 담아 TCP 소켓으로 전송한다.

            +

            HTTP 응답 메시지를 담은 데이터그램은 학교 네트워크로 향하고, 클라이언트에 도달한다. 클라이언트의 브라우저는 소켓의 HTTP 응답 메시지를 읽고 body에서 html을 추출해 웹페이지를 렌더링해준다.

            +
            School network 68.80.2.0/24
            +
            ++-------------------+     +----+     +--------+      /------\
            +| Client (Laptop)   |<----| AP |<----| Switch |<----| Router | 68.85.2.1
            +| 68.85.2.101       |     +----+     +--------+      \------/  00:22:6B:45:1F:1B
            +| 00:16:D3:23:68:8A |                                    ^
            ++-------------------+                                    |
            +                                                         |
            +#########################################################|#####
            +KT network 68.80.0.0/13                                 ...
            +#########################################################|#####
            +Google Fiber network 172.80.0.0/13                      ...
            +#########################################################|#####
            +Google network 172.217.20.0/19                           |
            +                                                         |
            ++----------------+   +-------------------------+     /-------\
            +| Web Server     |---| HTTP/2 200 OK           |--->| Routers |
            +| 172.217.25.228 |   | Content-Type: text/html |     \-------/
            ++----------------+   | ...                     |
            +                     | <html>...</html>        |
            +                     +-------------------------+
            +
            +

            드디어 클라이언트의 브라우저 화면에 구글이 보여진다.

            +

            References

            + + +
            +
            + +
            + +
            +
            +

            읽기 쉬운 웹을 위한 타이포그래피

            +

            조판 원칙으로 가독성 높이기

            +
            +
            +
            + + +
            + +
            +
            +

            🦀 러스트의 멋짐을 모르는 당신은 불쌍해요

            +

            높은 성능과 신뢰를 확보하기 위한 언어

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/37.html b/article/37.html new file mode 100644 index 0000000..466baf9 --- /dev/null +++ b/article/37.html @@ -0,0 +1,272 @@ + + + + + + 읽기 쉬운 웹을 위한 타이포그래피 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 읽기 쉬운 웹을 위한 타이포그래피 +

            + +

            + 조판 원칙으로 가독성 높이기 +

            + + + + +
            +
            Table of Contents +

            +
            +

            웹에서 장문의 글을 보여줄 때 보다 읽기 쉽게 디자인하기 위한 원칙을 소개하고자 한다. 전통적인 타이포그래피 규칙들을 바탕으로 웹에서 글을 어떻게 효과적으로 조판할지 소개하되, 활자 자체를 해부하지는 않는다. 이 글에서 제시하는 원칙을 무조건적으로 따르는 것보다는 상황에 따라 유연하게 적용하는 것이 좋고, 실험적인 시도를 원한다면 안 따르는 것이 낫다.

            +

            폰트

            +

            먼저 폰트에 대한 용어를 정리할 필요가 있다. 타이포그래피의 많은 부분이 과거 금속 활자를 조판하던 과정에서 유래되었기 때문에 배경을 알면 이해하기 수월하다. 가령 대문자를 'Uppercase’라고 일컫는 이유는 대문자 금속 활자를 위쪽에 있는 서랍(Case)에 보관했기 때문이고, 소문자를 'Lowercase’라고 일컫는 이유는 소문자 금속 활자를 아래쪽에 있는 서랍에 보관했기 때문이다.

            +

            폰트(Font)는 각각의 크기와 무게, 스타일로 이뤄진 활자 집합을 의미한다. 폰트 패밀리(Font family, 활자가족)는 유사한 특성을 공유하지만 다양한 시각적 변화를 지닌 폰트의 집합이다. 한편 타입페이스(Typeface, 활자꼴)는 폰트와 비슷하지만, 활자의 시각적 속성만을 의미한다.[1] 예를들어 ‘Helvetica’ 타입페이스의 폰트 패밀리에는 ‘Helvetica Roman’, ‘Helvetica Bold’, ‘Helvetica Italic’ 등이 포함되어있다. 'Helvetica Roman 10pt’와 ‘Helvetica Roman 12pt’, ‘Helvetica Bold 10pt’, 'Helvetica Italic 10pt’는 모두 다른 폰트다.

            +

            +

            오늘날에는 모든 것이 폰트로 통용된다. 일반적으로 'Helvetica Roman 10pt’와 'Helvetica Roman 12pt’가 다른 폰트라고 생각하는 사람은 거의 없다. 금속 활자를 조판할 때는 ‘Helvetica Roman 10pt’, ‘Helvetica Roman 12pt’, 'Helvetica Bold 10pt’에 모두 다른 금속 활자를 사용해야 한다. 이때는 폰트와 폰트 패밀리, 타입페이스의 구분이 명확하다. 반면 디지털 환경에서는 활자의 크기와 무게, 스타일을 쉽게 바꿀 수 있기 때문에 그 경계가 흐려진다. 워드에서 'Arial’을 'Helvetica’로 바꿀 때 우리는 "타입페이스를 바꾼다"라고 말하기 보다는 "폰트를 바꾼다"라고 말한다.[2] 이 글에서는 디지털 환경에서 일반적으로 사용하는 용어로써의 '폰트’를 사용할 것이고, 폰트 패밀리는 CSS 속성 font-family를 지칭하는 용어로만 사용할 것이다.

            +

            파일

            +

            주로 많이 사용하는 폰트 파일 형식에는 TTF, OTF, WOFF, WOFF2가 있다. TTF(TrueType)는 전통적, 범용적으로 사용되어 온 포맷이다. OTF(OpenType)는 TTF를 확장한 포맷이기 때문에 TTF 보다 많은 정보를 담을 수 있다.[3] 가장 큰 차이는 TTF는 2차원 베지어 곡선으로, OTF는 3차원 베지어 곡선으로 활자를 표현한다는 점이다. 그래서 일반 문서에는 TTF를, 고해상도 그래픽 작업에는 OTF를 사용하는 것이 일반적이다. WOFF(Web Open Font Format)는 웹에서 폰트를 사용하기 위해 만들어진 포맷으로, TTF, OTF 등 기존 포맷들을 압축한 것이다. 압축되어 있기 때문에 웹에서 빠르게 로드된다.[4] WOFF2는 WOFF를 개선해 압축률을 높인 버전이다. 폰트 파일의 크기가 크다면 웹 페이지를 로드할 때 병목으로 작용할 수 있으므로, 웹에서 폰트를 사용할 때는 WOFF2, WOFF, TTF/OTF 순으로 적용하는 것을 권장한다.

            +
            @font-face {
            +  font-family: 'Noto Sans';
            +  font-weight: 400;
            +  src: local('Noto Sans Regular'),
            +  url('/font/Noto-Sans-Regular.woff2') format('woff2'),
            +  url('/font/Noto-Sans-Regular.woff') format('woff'),
            +  url('/font/Noto-Sans-Regular.ttf') format('truetype');
            +  font-display: swap;
            +}
            +
            +.element {
            +  font-family: 'Noto Sans', sans-serif;
            +}
            +
            +

            위처럼 @font-face를 작성하면 가장 먼저 로컬에서 ‘Noto Sans Regular’ 적용을 시도한다. 만약 로컬에 폰트가 없어서 적용에 실패하면 웹에서 /font/Noto-Sans-Regular.woff2를 요청해 다운로드하고, 적용을 시도한다. WOFF2 폰트 적용도 실패하면 순서대로 WOFF, TTF 적용을 시도한다.

            +

            font-display 속성은 폰트를 다운로드하고 사용할 때 어떻게 표시할지 결정한다. 이 값을 swap으로 설정하면 요청한 폰트가 로드되지 않은 경우 대체 폰트를 렌더링하고, 이후 요청한 폰트가 로드됐을 때 빠르게 교체한다.[5]

            +

            한글 폰트 파일은 영문 폰트에 비해 크기때문에 성능을 저하할 수 있다. 폰트가 모든 현대 한글을 표현하려면 초성 19자와 중성 21자, 종성 27자를 조합한 11,172자가 필요하다. ASCII 코드가 128자만으로 A부터 Z까지의 영문, 심지어 일부 특수문자까지 지원하는 것과는 상반된다. 컴퓨터에서 한글을 표현하는 문제는 1974년으로 거슬러 올라간다. 당시 정부는 한글 51자모를 ASCII 코드에 일대일 대응해 KS C 5601-1974 규격을 제정했고, 1987년에는 자주 사용되는 한글 2,350자를 추려내 KS C 5601-1987 규격을 제정했다. 한글 완성형 코드의 최신 버전은 국가기술표준원이 표준화한 KS X 1001-2004으로, 일반적으로 디자이너가 한글 폰트를 디자인할 때는 한글 11,172자를 모두 디자인하지 않고 KS X 1001-2004에 포함된 2,350자만을 디자인한다.

            +

            다만 Noto처럼 한글 11,172자를 모두 정의해둔 한글 폰트도 있다. 한글 뿐만 아니라 한자, 가나(仮名), 잘 사용하지 않는 특수문자까지 포함된 폰트라면 파일 크기가 매우 커질 수 있다. 이때는 원본 폰트에서 사용할 문자만을 골라내 서브셋(Subset)을 만들면 파일 크기를 줄일 수 있다. 가령 akngs/noto-kr-vf-distilled 프로젝트는 Noto Sans에서 ASCII 코드에 포함된 문자 95자와 KS X 1001의 한글 2,350자, "KS 코드 완성형 한글의 추가 글자 제안"[6]에서 제안된 한글 228자만을 포함한 Noto 서브셋을 가변 폰트로 제공한다.

            +

            패밀리

            +

            좋은 폰트를 사용하는 것만으로도 심미성과 가독성, 판독성을 어느정도 얻을 수 있다. 폰트의 종류는 일반적으로 5개, 12개 정도로 구분하며, CSS의 font-family 속성에는 6가지의 generic-name 값을 사용할 수 있다. 아래와 같이 값을 설정하면 가장 먼저 ‘Helvetica’ 적용을 시도하고, 해당 폰트가 없으면 두 번째로 ‘Noto Sans’ 적용을 시도한다. 두 번째도 실패하면 기본값으로 설정되어 있는 산세리프 폰트를 적용한다.

            +
            font-family: 'Helvetica', 'Noto Sans', sans-serif;
            +
            +

            본문에 가장 많이 사용되는 유형은 세리프(serif)체와 산세리프(sans-serif)체일 것이다. 세리프는 글자 획의 삐침을 가리킨다. 이는 끌로 석판을 조각하던 고대의 조판 방식에서 유래했다. 산세리프는 '~없는’이라는 뜻의 프랑스어 sans와 세리프가 합쳐진 단어로, 세리프가 없는 폰트를 산세리프체라고 한다.

            +

            +

            본문 폰트로 세리프체의 가독성이 더 좋은지, 산세리프체의 가독성이 더 좋은지에 대해서는 다양한 의견이 있다. 이론적으로 세리프체는 수평 흐름을 만들기 때문에 읽기 수월하지만, 사실 독자가 폰트에 얼마나 익숙한지가 더욱 중요하다.[7] 이미 웹에서는 본문에 산세리프체를 널리 사용해 왔기 때문에 가독성에는 큰 영향을 끼치지 않는다.

            +

            프로그래머라면 세리프체와 산세리프체만큼 고정폭(monospace) 폰트를 자주 사용한다. 코드에 고정폭 폰트를 사용하는 것은 당연한 일이고, 본문에 숫자를 혼용할때도 숫자에 고정폭 폰트가 적용되도록 하는 것이 좋다. 오픈타입 폰트를 사용한다면 font-variant-numeric 속성을 이용해 숫자에만 고정폭을 적용할 수 있다.

            +
            font-variant-numeric: tabular-nums;
            +
            +

            스포카에서 만든 스포카 한 산스 네오의 경우 폰트 자체가 숫자 가독성을 높이도록 디자인되어 있다. 본문에 혼용된 외국어나 숫자, 부호 등에 각기 다른 스타일을 적용하기 위한 multilingual.js 같은 라이브러리도 있다.[8]

            +

            위계

            +

            요소 사이 위계가 잘 잡혀있으면 구조를 파악하기 쉽다. 하나의 책은 대단원, 중단원, 소단원으로 위계를 이룰 수 있고, 하나의 글은 제목, 부제목, 본문 등으로 위계를 이룰 수 있다. 내용상의 위계가 제대로 전달되려면 시각적인 위계가 필요하다. 시각적인 위계는 기본적으로 요소의 무게로 만들 수 있으며, 시각적인 무게는 요소와 요소 사이의 간격, 요소의 크기와 농도, 색깔, 위치, 정렬 등을 종합하여 만들 수 있다. 간격과 정렬에 대해서는 별도의 문단에서 설명할 것이고, 여기에서는 크기와 굵기, 농도에 대해서만 설명한다.

            +

            +

            크기

            +

            활자의 크기는 가장 직관적으로 받아들이고, 만들 수 있는 무게다. font-size 속성에 다양한 단위로 값을 부여해 설정할 수 있다.

            +

            픽셀(px)은 고정적인 단위다. 디스플레이 크기에 상관없이 1px은 항상 1px이다. 폰트 크기를 픽셀로 정의하면 다양한 디스플레이에 대응하기 어렵고, 접근성이 떨어지는 문제가 있다. 일부 브라우저에서는 폰트 크기를 키우지 못하기 때문이다.[9] 포인트(pt) 단위는 주로 인쇄물에 사용한다. 포인트 역시 픽셀과 마찬가지로 고정적인 단위다.

            +

            em은 대문자 'M’의 너비를 기준으로한 상대적인 단위다. 1em은 요소 자신의 font-size 값이다. font-size 속성은 상속되기 때문에 상위 요소의 값을 따르게 된다. 상속 값이 없다면 브라우저 기본값을 따른다.

            +
            .parent {
            +  font-size: 16px;
            +}
            +
            +.child {
            +  font-size: 1em; /* 16px */
            +}
            +
            +

            rem은 'root em’이다. em과 비슷하지만, 1rem은 자신이나 상위 요소의 font-size가 아닌 루트 요소의 font-size 값이다. 일반적으로 HTML 문서의 루트 요소는 html이므로 html 요소의 font-size 값이 기준이 된다.

            +
            html {
            +  font-size: 16px;
            +}
            +
            +.child {
            +  font-size: 1em; /* 16px */
            +}
            +
            +

            위계 관계에 있는 요소 사이의 크기 격차는 눈에 띄어야 한다. 20px 제목과 18px 본문은 분명히 수치적인 크기의 차이가 있지만, 눈으로는 쉽게 위계를 구분할 수 없다.

            +

            무게

            +

            굵기와 농도는 폰트의 무게에 큰 영향을 준다. 폰트는 굵기가 굵을수록, 농도가 높을수록 무거워지며, 무거워질수록 위계가 높아지는 효과가 있다.

            +

            폰트의 굵기를 정하는 font-weight 속성의 값은 normal, bold와 같이 설정할 수도 있고, 부모 요소에 상대적인 값인 lighterbolder를 사용할 수도 있다. 좀 더 정밀하게 굵기를 다루려면 100, 200, 300부터 900까지의 가중치를 직접 설정하면 된다.

            +
            font-weight: 100; /* Thin */
            +font-weight: 200; /* Extra Light */
            +font-weight: 300; /* Light */
            +font-weight: 400; /* Normal (normal)*/
            +font-weight: 500; /* Medium */
            +font-weight: 600; /* Semi Bold */
            +font-weight: 700; /* Bold (bold) */
            +font-weight: 800; /* Extra Bold */
            +font-weight: 900; /* Black */
            +
            +

            앞서 언급했듯이 농도가 높을수록 요소가 무거워지고, 낮을수록 가벼워진다. 농도가 너무 낮아 배경과의 대비가 미미해지면 가독성과 접근성이 떨어지는 문제가 발생할 수 있으므로 주의해야 한다. 아래 두 텍스트는 크기와 굵기가 동일하지만 농도로 인해 위계를 이룬다.

            +

            +

            위계를 만들 때는 어떤 요소의 우선순위가 높은지 골라내고, 어떤 요소를 강조할 것인지 잘 선택해야 한다. 모든 요소가 중요하다는 것은 어떤 요소도 중요하지 않다는 의미로 받아들여질 수 있다.

            +

            글자사이, 낱말사이

            +

            활자와 활자 사이의 간격을 글자사이 또는 자간(Letterspace), 단어와 단어 사이의 간격을 낱말사이 또는 어간(Wordspace)이라고 한다. 인쇄 조판을 할 때는 활자 하나하나, 단어 하나하나의 자간과 어간을 조정하는 일이 잦다. 의도적으로 자간을 조정하는 것을 커닝(Kerning)이라고 한다.

            +

            하지만 웹에서 자간을 일일히 조정하는 것은 쉽지 않다. 다행히 font-kerning 속성을 사용하면 브라우저가 폰트에 담긴 커닝 정보를 사용하도록 할 수 있다.

            +
            font-kerning: normal;
            +
            +

            자간과 어간을 직접 설정하고 싶다면 letter-spacing 속성과 word-spacing 속성을 사용하면 된다.

            +
            letter-spacing: 3px;
            +word-spacing: 2px;
            +
            +

            많은 폰트들이 합자(Ligature)를 가지고 있다. ff, fi, fl 등 두 개 이상의 활자를 연이어 사용할 때 조형적으로 부자연스러운 형태가 나타나기도 한다. 이런 경우 연속적인 활자들을 하나의 활자 형태로 만들 수 있다.

            +

            +

            font-variant-ligatures 속성으로 합자를 항상 활성화하거나 비활성화하도록 강제할 수 있다.

            +
            font-variant-ligatures: normal;
            +font-variant-ligatures: no-common-ligatures;
            +font-variant-ligatures: common-ligatures;
            +
            +

            no-common-ligatures 속성은 합자를 비활성화하고, common-ligatures 속성은 합자를 활성화한다.[10] text-rendering 속성도 합자에 영향을 준다.

            +
            text-rendering: auto;
            +text-rendering: optimizeSpeed;
            +text-rendering: optimizeLegibility;
            +text-rendering: geometricPrecision;
            +
            +

            optimizeSpeed 값은 렌더링 성능을 최적화하여 합자와 커닝을 비활성화한다. optimizeLegibility 값은 반대로 판독성을 최적화하며, geometricPrecision 값은 속도나 판독성이 아닌 정밀성을 최적화한다.[11]

            +

            대체로 합자는 연이어진 활자들과 비슷한 형태의 한 덩어리로 이뤄지지만, 완전히 다른 형태가 되는 경우도 존재한다. 가령 >=\geq 합자로 치환하는 폰트들이 있다. 이런 합자는 가독성과 심미성을 높여주지만, 편집이 빈번히 일어나는 경우에는 방해가 될 수 있다. \geq에서 백스페이스를 눌러 한 자를 지웠을 때 =이 지워지고 >만 남을 것이라는 것을 예측할 수 없기 때문이다.

            +

            글줄사이

            +

            글줄과 글줄 사이 간격을 글줄사이 또는 행간(Linespace)이라고 한다. 의도적으로 행간을 조정하는 것은 레딩(Leading)이라고 한다. 행간이 너무 좁으면 독자가 같은 줄을 여러 번 읽는 실수를 한다.

            +

            +

            행간은 line-height 속성으로 설정할 수 있다. 유저 에이전트마다 기본값이 다른데, 데스크탑 브라우저의 경우 1.2정도로 설정된다. 가독성과 접근성을 위해서 본문 행간은 최소 1.5 이상으로 설정하는 것이 좋다. 행간을 적절히 넓게 하면 저시력자 또는 난독증을 가진 사용자의 사용 경험을 높일 수 있고[12], 활자의 크기가 작을 때도 읽기 쉬워진다.

            +

            특히 행간을 넓게 했을 때 좋은 폰트 유형이 있다. x-height이 큰 폰트를 사용할 때는 글줄이 서로 가까워 보이기 때문에 행간을 넓게 잡아야 한다. (x-height은 소문자 'x’의 높이를 의미한다.) 수직 스트레스가 강한 폰트를 사용할 때는 수평 강세를 유지하기 위해 행간을 넓게 하는 것이 좋고, 산세리프 폰트는 수평 흐름이 없기 때문에 행간을 넓게 하는 것이 좋다. 그리고 한글 폰트도 행간을 넓게 해야 한다. 알파벳 활자에 비해 한글의 베이스라인이 아래에 있어 빽빽해 보이기 때문이다.[13]

            +

            행간이 좁을 때와 마찬가지로, 글줄이 너무 길어도 독자가 같은 줄을 여러 번 읽는 실수를 하게 된다. 한편 글줄이 너무 짧으면 눈을 자주 움직여야하기 때문에 피곤하다.

            +

            또한 어간이 행간보다 넓어선 안 된다. 어간이 행간보다 넓어지면 글의 덩어리가 좌우가 아닌 상하로 인식된다. 아래 설명할 양끝맞춤을 할 때 의도치 않게 이런 문제가 생길 수 있다.

            +

            +

            정렬

            +

            다양한 방식으로 문단을 정렬할 수 있다. 인쇄물에서 가장 인기있는 정렬은 양끝맞춤이다.

            +
            text-align: justify;
            +
            +

            +

            양끝맞춤은 보편적이고 전통적인 조판 방식이다. 양끝이 균일하여 편안한 느낌을 주지만, 글줄의 글자수에 관계없이 글줄길이를 모두 똑같이 맞춰야 하기 때문에 어간이 고르지 않게 되는 문제가 있다. 특히 글자수가 적을 때 그 문제가 부각된다. 이로 인해 넓은 어간이 여러 글줄에 반복적으로 나타나면 흰강(White river) 현상이 발생한다.

            +

            +

            되도록이면 왼끝맞춤을 하자. 왼끝맞춤은 어간이 일정하고, 오른끝의 흘려짐 덕분에 시각적인 활기를 얻을 수 있다.[14]

            +
            text-align: left;
            +
            +

            +

            사람은 문장의 시작 지점을 찾으며 글을 읽는다. 그래서 글줄의 시작 지점을 예측하기 쉽다면 가독성에는 영향을 끼치지 않기 때문에 오른끝이 균일하지 않아도 읽는 데 문제가 없다. 다만 왼끝맞춤을 할 때는 오른끝의 흘려진 모양에 주의할 필요가 있다. 웹에서는 이 모양을 아름답게 만드는게 쉽지 않기 때문에 아직까지는 감수해야 할 것 같다.

            +

            더 중요한 것은 word-break 속성을 이용해 단어의 끊김을 방지하는 것이다.

            +
            word-break: keep-all;
            +
            +

            +

            왼쪽은 keep-all을 하지 않아 글줄 끝에서 단어가 끊기는 조판이고, 오른쪽은 이를 방지한 조판이다. 웹에서 keep-all을 하지 않아 발생하는 끔찍한 사례는 @keepallvillain의 타임라인에서 모아볼 수 있다.

            +

            가운데맞춤은 공식적인 메시지를 전달할 때 사용한다. 글을 가운데맞춤하는 것으로 권위를 부여할 수 있다. 이때는 글줄의 시작 지점을 예측하기 어렵기 때문에 행간을 넓게 잡아야 한다.

            +

            +

            References

            + +
            +
            +
              +
            1. 이용제, “타이포그라피에서 ‘글자, 활자, 글씨’ 쓰임새 제안”, 글자씨 2(2), 한국타이포그라피학회, 2010, 492-507쪽. ↩︎

              +
            2. +
            3. John Brownlee, “What’s The Difference Between A Font And A Typeface?”, Fast Company, 2014. ↩︎

              +
            4. +
            5. “OpenType® Specification”, Microsoft Docs, 2020. ↩︎

              +
            6. +
            7. Jonathan Kew et al., “WOFF File Format 1.0”, W3C Recommendation, 2012. ↩︎

              +
            8. +
            9. “font-display”, MDN Web Docs. ↩︎

              +
            10. +
            11. 노민지, 윤민구, “KS 코드 완성형 한글의 추가 글자 제안”, 글자씨 7(2), 한국타이포그라피학회, 2015, 153-175쪽. ↩︎

              +
            12. +
            13. 원유홍 외 2명, “타이포그래피 천일야화”, 안그라픽스, 2012, 87쪽. ↩︎

              +
            14. +
            15. 강이룬, 소원영, “multilingual.js: 다국어 웹 타이포그래피를 위한 섞어쓰기 라이브러리”, 글자씨 8(2), 한국타이포그라피학회, 2016, 9-33쪽. ↩︎

              +
            16. +
            17. “font-size”, MDN Web Docs. ↩︎

              +
            18. +
            19. “font-variant-ligatures”, MDN Web Docs. ↩︎

              +
            20. +
            21. “text-rendering”, MDN Web Docs. ↩︎

              +
            22. +
            23. “line-height”, MDN Web Docs. ↩︎

              +
            24. +
            25. 이주호, “한글의 가독성과 ko.TEX의 타이포그래피”, The Asian Journal of TEX 2(2), 2008, 16쪽. ↩︎

              +
            26. +
            27. 우유니, “미움 받는 왼끝맞춤에 대한 변호”, FDSC, 2020. ↩︎

              +
            28. +
            +
            + +
            +
            + +
            + +
            +
            +

            🧱 Server Driven UI 설계를 통한 UI 유연화

            +

            클라이언트 배포없이 화면 구성 변경하기

            +
            +
            +
            + + +
            + +
            +
            +

            인터넷이 동작하는 아주 구체적인 원리

            +

            학교에서 구글에 접속하는 과정

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/38.html b/article/38.html new file mode 100644 index 0000000..208a488 --- /dev/null +++ b/article/38.html @@ -0,0 +1,380 @@ + + + + + + 🧱 Server Driven UI 설계를 통한 UI 유연화 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🧱 Server Driven UI 설계를 통한 UI 유연화 +

            + +

            + 클라이언트 배포없이 화면 구성 변경하기 +

            + + + + +
            +
            Table of Contents +

            +
            +

            웹과 달리 네이티브 모바일 앱은 빌드, 배포 후에는 수정이 불가능하다. 만약 잘못된 위치에 버튼을 배치한 채로 스토어에 앱을 배포했다면, 그리고 사용자가 잘못된 버전의 앱을 설치했다면 버튼의 위치를 수정할 방법이 없다. 유일한 방법은 사용자가 스스로 스토어에 들어가 수정된 버전의 앱으로 업데이트하는 것 뿐이다.

            +

            배포 후 수정이 불가능하다는 특성이 부딪히는 또 다른 상황은 A/B 테스팅이다. 소프트웨어를 사용하는 동안 일어나는 사용자의 행동과 경험은 화면 구성이나 문구에 따라 크게 달라지기 때문에 최적의 화면을 디자인하는 것이 중요하다. 그런데 사용자의 행동과 경험을 예측하는 것은 매우 어려운 일이기 때문에 현실의 사용자들에게 다양한 유형의 UI를 제공하고, 어떤 UI가 적합한지 실측할 필요가 있다. 실제로 많은 소프트웨어 기업들이 사용자를 A, B 그룹으로 나누고 (더 많은 그룹으로 나눌 수도 있다.) 각 그룹에게 서로 다른 UI를 제공해 가장 적합한 UI를 선정하는 A/B 테스팅을 하고 있다.

            +

            유연한 UI를 제공하려면 UI가 클라이언트의 빌드와 배포로부터 자유로워야 한다. 이러한 목표를 이루기 위해 웹뷰와 같이 네이티브 환경을 벗어난 다양한 방법을 선택할 수도 있겠지만, 현실에서는 다양한 이유로 웹뷰를 사용할 수 없는 상황이 있다. 이 글에서는 웹뷰를 사용하지 않는다는 전제 하에 유연하게 UI를 다루기 위한 Server Driven UI 설계에 대해 소개하고자 한다.

            +

            서버에서 UI 다루기

            +

            클라이언트과 달리 서버는 언제든 변경, 배포할 수 있다. 그렇다면 서버에서 제공하는 API를 이용해 동적으로 클라이언트의 UI를 구성하면 어떨까? 서버가 API 응답에 UI 정보를 담아 클라이언트에 제공하고, 클라이언트가 API 응답에 따라 화면을 렌더링한다면 서버에서 API 응답을 변경하는 것만으로 클라이언트의 화면 구성을 동적으로 변경할 수 있을 것이다.

            +

            예를 들어 사용자에게 홈 화면을 제공하는 경우, 서버가 제공하는 REST API screen을 통해 home 화면에 대한 UI 정보를 얻을 수 있을 것이다.

            +
            GET /screen/home
            +
            +

            이 API는 홈 화면을 구성하는 UI 요소 리스트를 JSON 포맷으로 응답한다. 따라서 클라이언트는 응답의 data 리스트를 순회하며 각각의 type에 해당하는 UI 요소를 화면에 그려주면 된다.

            +

            +

            이렇게 하면 클라이언트를 새로 배포하지 않아도 서버에서 data 리스트의 요소를 변경함으로써 클라이언트가 유연한 UI를 제공할 수 있을 것이다. 이처럼 UI에 대한 정보를 서버에서 관리, 제공하는 것이 Server Driven UI 설계의 기본 개념이다.

            +

            GraphQL: 재사용 가능한 UI 컴포넌트 제공하기

            +

            서버에서 UI를 관리하면 유연성을 확보할 수 있지만, 사용하는 UI 요소의 재사용성을 고려하지 않으면 다양한 화면에서 UI 요소를 교체하기 어려워진다. 이러한 문제를 피하려면 모든 UI 요소를 재사용 가능한 컴포넌트로 구성하고, UI 컴포넌트를 다양한 화면에서 조립해서 사용할 수 있도록 만들어야 한다.

            +

            또한 수시로 화면에 새로운 컴포넌트가 추가되고 제거되면 서버와 클라이언트 사이의 타입 정의에 불일치가 발생하기 쉽다. 이때 GraphQL을 사용하면 서버와 클라이언트가 공유하는 스키마를 통해 API의 타입 안전성을 보장할 수 있다.

            +

            쿼리 설계

            +

            서버는 UI 컴포넌트 리스트를 반환하는 screen 쿼리를 통해 특정 화면에 대한 UI 정보를 제공한다.

            +
            type Query {
            +  screen(screenType: ScreenType!): Screen!
            +}
            +
            +enum ScreenType {
            +  HOME
            +  SIGN_IN
            +}
            +
            +type Screen {
            +  components: [Component!]!
            +}
            +
            +

            클라이언트는 screen 쿼리를 호출하며 홈 화면에서는 screenType: HOME 인자를, 로그인 화면에서는 screenType: SIGN_IN 인자를 전달할 것이다. 서버는 쿼리를 받으면 해당 screenType에 맞는 컴포넌트를 조합하여 Screen 타입의 components 필드에 Component 리스트를 담아 응답한다.

            +

            Component는 유니온 타입이다. 유니온 타입은 다양한 타입의 컴포넌트를 Component라는 하나의 타입으로 다룰 수 있게 해준다. Screen 타입의 components 필드가 Component 리스트를 반환한다는 것은 리스트 안에 AppBar, TextButton, Image 타입이 섞일 수 있다는 의미다.

            +
            union Component = AppBar | TextButton | Image
            +
            +type AppBar {
            +  title: String!
            +}
            +
            +type TextButton {
            +  text: String!
            +  route: String
            +}
            +
            +type Image {
            +  url: String!
            +}
            +
            +

            만약 컴포넌트들이 공통 필드를 가진다면 Component를 유니온 타입 대신 인터페이스로 만들어도 된다.

            +
            interface Component {
            +  position: Int!
            +}
            +
            +type AppBar implements Component {
            +  position: Int!
            +  title: String!
            +}
            +
            +type TextButton implements Component {
            +  position: Int!
            +  text: String!
            +  route: String
            +}
            +
            +type Image implements Component {
            +  position: Int!
            +  url: String!
            +}
            +
            +

            유니온 타입은 단순히 독립적인 컴포넌트 타입들을 하나의 타입으로 사용하기 위한 방식이었다면, 인터페이스는 각각의 컴포넌트 타입들이 추상 타입인 Component를 구현하는 방식이기 때문에 어떤 타입이 UI 컴포넌트인지 명확해진다는 장점이 있다.

            +

            요청과 응답

            +

            GraphQL의 재사용 가능한 필드 묶음인 프래그먼트(Fragment)는 UI 컴포넌트를 주고받기에 매우 적합하다. 클라이언트에서 컴포넌트를 요청할 때는 사용 가능한 모든 컴포넌트 프래그먼트를 요청할 것이다.

            +
            query fetchScreen {
            +  screen(screenType: HOME) {
            +    components {
            +      ... on AppBar {
            +        __typename
            +        title
            +      }
            +      ... on TextButton {
            +        __typename
            +        text
            +        route
            +      }
            +      ... on Image {
            +        __typename
            +        url
            +      }
            +    }
            +  }
            +}
            +
            +

            주의할 점은 '사용 가능한 모든 컴포넌트’를 요청한다는 점이다. 만약 구현 당시에 사용할 컴포넌트만 요청하면 차후 서버에서 다른 컴포넌트를 화면에 추가해도 보여줄 수 없기 때문이다.

            +

            요청을 받은 서버는 홈 화면에서 사용할 컴포넌트를 골라서 반환한다. 이 예시에서는 홈 화면에 AppBar 컴포넌트 하나와 TextButton 컴포넌트 두 개를 응답한다.

            +
            impl QueryRoot {
            +    fn screen(screen_type: ScreenType) -> FieldResult<Screen> {
            +        Ok(
            +            Screen {
            +                components: match screen_type {
            +                    ScreenType::Home => home_components(),
            +                    ScreenType::SignIn => sign_in_components(),
            +                }
            +            }
            +        )
            +    }
            +}
            +
            +fn home_components() -> Vec<Component> {
            +    vec![
            +        Component::AppBar(AppBar {
            +            title: "Home".to_string(),
            +        }),
            +        Component::TextButton(TextButton {
            +            text: "Sign in".to_string(),
            +            route: Some("/sign_in".to_string()),
            +        }),
            +        Component::TextButton(TextButton {
            +            text: "Sign up".to_string(),
            +            route: None,
            +        }),
            +    ]
            +}
            +
            +

            러스트로 서버 코드를 작성한 이유는 순전히 개인 취향이며, Server Driven UI나 GraphQL과는 전혀 관련이 없다. 다양한 언어로 된 GraphQL API 서버 구현체가 있기 때문에 언어의 선택은 문제가 되지 않는다.[1]

            +

            요청이 성공하면 서버에서 의도한 GraphQL 응답을 받을 수 있다.

            +
            {
            +  "data": {
            +    "screen": {
            +      "components": [
            +        {
            +          "__typename": "AppBar",
            +          "title": "Home"
            +        },
            +        {
            +          "__typename": "TextButton",
            +          "text": "Sign in",
            +          "route": "/sign_in"
            +        },
            +        {
            +          "__typename": "TextButton",
            +          "text": "Sign up",
            +          "route": null
            +        }
            +      ]
            +    }
            +  }
            +}
            +
            +

            앞서 클라이언트가 components 필드 아래에 Image 프래그먼트도 요청했지만, 서버가 Image 컴포넌트를 응답하지 않았기 때문에 리스트에는 포함되지 않았다. 반대로 서버가 Image 컴포넌트를 응답했지만 클라이언트가 요청하지 경우에도 리스트에 포함되지 않는다. 따라서 서버가 신규 컴포넌트를 정의하거나 기존 컴포넌트에 신규 필드를 추가해도 구버전 클라이언트에서는 신규 컴포넌트와 필드를 요청하지 않기 때문에 클라이언트의 하위호환성을 확보할 수 있다. 단, 기존 컴포넌트에 대해 구버전 클라이언트에서 사용 중인 필드를 제거하거나 non-nullable 필드를 nullable 필드로 바꾸는 경우 하위호환성이 깨지므로 주의해야 한다.

            +

            Flutter: 견고한 디자인 시스템과 위젯으로 화면 그리기

            +

            통일감있는 컴포넌트를 사용하려면 디자인 시스템이 잘 잡혀 있어야 한다. 만약 UI 레벨에서 디자인 시스템이 정립되어 있지 않다면 애초에 컴포넌트를 개념을 도입하는 것이 어불성설일 뿐더러, 서버와 클라이언트, 디자인 사이에 사용하는 용어가 달라져 커뮤니케이션 비용도 증가한다.

            +

            플러터(Flutter)의 머티리얼 라이브러리(Material Library)는 구글의 머티리얼 디자인 시스템을 높은 수준으로 구현하고 있어 Server Driven UI를 바로 적용할 수 있다.

            +

            +

            프래그먼트-컴포넌트-위젯 대응

            +

            플러터가 가진 위젯(Widget) 개념이 컴포넌트 개념과 부합한다는 점도 Server Driven UI 설계와 잘 맞는다. 플러터의 위젯은 웹 프론트엔드 프레임워크인 리액트(React)의 컴포넌트 시스템으로부터 영감을 받아 만들어졌으며, 각 위젯은 자신의 현재 상태에 따른 UI를 표현한다.[2]

            +

            클라이언트의 추상 클래스 Component는 서버에서 응답하는 GraphQL 유니온 타입 Component에 대응된다. Component를 구현하는 클래스는 위젯을 반환하는 compose 메서드를 함께 구현해야 한다.

            +
            abstract class Component {
            +  Widget compose(Map<String, dynamic> args, BuildContext context);
            +}
            +
            +

            가령 앱 상단에 들어가는 앱바 UI를 의미하는 AppBarComponentComponent 클래스를 구현하며, AppBar 위젯을 반환하는 compose 메서드를 갖는다.

            +
            class AppBarComponent implements Component {
            +  Widget compose(Map<String, dynamic> args, BuildContext context) {
            +    return AppBar(
            +      title: Text(args['title']),
            +    );
            +  }
            +}
            +
            +

            compose 메서드는 args 인자의 title 프로퍼티에 접근해 앱바의 타이틀을 채운다. 이때 args 인자는 서버의 응답에 포함되는 AppBar 프래그먼트의 필드들이 Map<String, dynamic> 타입으로 전달될 것이다.

            +

            클라이언트가 서버로부터 받은 응답을 파싱한 다음, components 필드에 포함된 각각의 프래그먼트들을 자신의 컴포넌트에 대응시키고, 각 컴포넌트의 위젯에 대응시키려면 GraphQL 스키마를 바탕으로 한 컴포넌트 레지스트리가 필요하다.

            +
            class Registry {
            +  static final Map<String, Component> _dictionary = {
            +    'AppBar': AppBarComponent(),
            +    'TextField': TextFieldComponent(),
            +    'Image': ImageComponent(),
            +  };
            +
            +  static Widget getComponent(dynamic component, BuildContext context) {
            +    var matchedComponent = _dictionary[component['__typename']];
            +    if (matchedComponent != null) {
            +      return matchedComponent.compose(component, context);
            +    } else {
            +      return null;
            +    }
            +  }
            +
            +  static List<Widget> getComponents(dynamic components, BuildContext context) {
            +    var matchedComponent = components as List<dynamic>;
            +    return matchedComponent.map((component) => getComponent(component, context))
            +        .where((element) => element != null)
            +        .toList();
            +  }
            +}
            +
            +

            클라이언트는 응답 내용을 바탕으로 위젯 리스트를 얻기 위해 레지스트리의 getComponents 메서드를 호출하고, components 필드를 순회하며 getComponent 메서드를 통해 프래그먼트를 위젯으로 변환한다.

            +

            getCompnent 메서드는 프래그먼트에 포함된 메타 필드 __typename 값을 이용해 각 프래그먼트를 컴포넌트에 대응시키고, 해당 컴포넌트의 compose 메서드를 호출해 컴포넌트 각각의 위젯을 반환한다. 만약 클라이언트가 모르는(_dictionary에 등록되지 않은) 컴포넌트가 응답에 포함되어 있다면 필터링될 것이다.

            +

            컴포넌트 조립

            +

            앞서 살펴본 레지스트리를 이용해 서버에서 응답하는 모든 컴포넌트를 위젯으로 변환하고, 각 화면에 맞는 위젯을 구성할 수 있게 되었다. 지금까지의 흐름을 서버, API 응답, 클라이언트로 정리하면 아래와 같다.

            +

            +

            플러터 위젯 중 Container이나 Column과 같이 다른 위젯을 child 또는 children으로 담는 위젯도 같은 방식으로 만들 수 있다. 일종의 '컴포넌트의 컴포넌트’인 셈이다. 예를 들어 GridView 위젯을 생각해보자. 그리드 뷰는 격자 셀이 반복되는 레이아웃으로, 각 셀에는 다른 위젯을 배치시킬 수 있다. 그리드의 열 개수와 각 셀에 넣을 컴포넌트를 서버에서 관리하고자 한다면 아래와 같이 스키마를 구성할 수 있을 것이다.

            +
            type GridView {
            +  column_count: Int!
            +  chidren: [Component!]!
            +}
            +
            +

            클라이언트에서 요청할 때는 GridView 프래그먼트의 children 필드 아래에 사용 가능한 모든 컴포넌트를 요청해야 한다. 같은 프래그먼트를 재사용하므로 인라인 프래그먼트 대신 별도의 기명 프래그먼트를 만들었다.

            +
            query fetchScreen {
            +  screen(screenType: HOME) {
            +    components {
            +      ... AppBar
            +      ... TextButton
            +      ... Image
            +      ... on GridView {
            +        __typename
            +        column_count
            +        children {
            +          ... AppBar
            +          ... TextButton
            +          ... Image
            +        }
            +      }
            +    }
            +  }
            +}
            +
            +fragment AppBar on AppBar {
            +  __typename
            +  title
            +}
            +
            +fragment TextButton on TextButton {
            +  __typename
            +  text
            +  route
            +}
            +
            +fragment Image on Image {
            +  __typename
            +  url
            +}
            +
            +

            마지막으로 GridVew 위젯을 반환하는 GridViewComponent는 자신의 children 필드 값을 getComponents로 넘겨 위젯 리스트를 구성한다.

            +
            class GridViewComponent implements Component {
            +  Widget compose(Map<String, dynamic> args, BuildContext context) {
            +    return Expanded(
            +      child: GridView.count(
            +        padding: const EdgeInsets.all(20),
            +        crossAxisSpacing: 20,
            +        mainAxisSpacing: 20,
            +        crossAxisCount: args["columnCount"],
            +        children: Registry.getComponents(args["children"], context),
            +      ),
            +    );
            +  }
            +}
            +
            +

            여기서는 padding, crossAxisSpacing, mainAxisSpacing 프로퍼티를 상수 값으로 설정했지만, 만약 서버에서 관리하고 싶다면 간단히 필드를 추가하고 값을 넣어주기만 하면 된다.

            +

            이제 서버에서 GridViewchildren 필드에 TextButton 두 개를 응답하면 클라이언트에서는 그대로 두 개의 셀에 TextButton이 담긴 화면을 구성한다. 그리드의 열 개수나 각 셀의 내용은 서버 응답을 수정하는 것만으로 언제든 변경할 수 있다.

            +

            +

            실제 동작하는 코드는 github.com/parksb/server-driven-ui에서 확인할 수 있다. 동작 방식이나 개념은 이 글에서 설명한 것과 동일하지만, 컴포넌트 종류나 구체적인 필드는 다소 차이가 있다. 또한 카카오스타일에서도 Server Driven UI 설계를 적극적으로 사용하고 있는데, 카카오스타일 기술 블로그에서 자세히 볼 수 있다.

            +

            References

            + +
            +
            +
              +
            1. The GraphQL Foundation, “GraphQL Code Libraries, Tools and Services”. ↩︎

              +
            2. +
            3. Flutter, “Introduction to widgets”. ↩︎

              +
            4. +
            +
            + +
            +
            + +
            + +
            +
            +

            철도 시간표가 유닉스 시간이 되기까지

            +

            시간과 컴퓨터 공학

            +
            +
            +
            + + +
            + +
            +
            +

            읽기 쉬운 웹을 위한 타이포그래피

            +

            조판 원칙으로 가독성 높이기

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/39.html b/article/39.html new file mode 100644 index 0000000..0283f8e --- /dev/null +++ b/article/39.html @@ -0,0 +1,237 @@ + + + + + + 철도 시간표가 유닉스 시간이 되기까지 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 철도 시간표가 유닉스 시간이 되기까지 +

            + +

            + 시간과 컴퓨터 공학 +

            + + + + +
            +
            Table of Contents +

            +
            +

            컴퓨터 공학에서 시간을 다루는 것은 꽤나 고달픈 일이다. 특히 우리가 일상에서 직관적으로 이해하는 시간 체계와 일상에서 의식하지 못하는 시간 체계의 괴리로 인해 소프트웨어 엔지니어들은 고통받는다. 이 글에서는 태양시, 원자시 등 시간 체계에 대해 알아보고, 컴퓨터에서 어떻게 시간을 다루는지 설명하고자 한다.

            +

            태양의 시간

            +

            +Stanford University Libraries (CC0)

            +

            19세기초까지만 해도 지역마다 각자의 지방 평균시(Local Mean Time, LMT)를 사용했다. 지방 평균시는 각자의 지역에서 태양이 최고 고도에 이르는 시각을 기준으로 삼는 시간 체계이기 때문에 경도에 따라, 지방에 따라, 도시에 따라, 마을에 따라 사용하는 시간이 달랐다. 가령 런던에서 옥스포드로 가면 시간이 5분 앞으로 당겨졌고, 리즈에 가면 6분 앞으로 당겨졌다. 그래도 문제가 없었다. 마차를 끄는 말의 속도와 배를 움직이는 바람의 속도가 충분히 느렸기 때문에 그 정도의 시차는 중요치 않았다.

            +

            그런데 증기기관이 등장하고 각 지방을 연결하는 철도가 놓이면서 이동 시간이 비약적으로 줄어들자 정확한 시간이 중요해졌다. 8시 정각에 런던에서 출발한 기차가 정확히 1시간 뒤 옥스포드에 도착하면 옥스포드의 시계 기준으로는 9시가 아닌 8시 55분에 도착한다. 런던 시간에 맞춰진 기관사의 시계와 5분 차이가 발생하는 것이다. 기관사 시계 기준으로 9시 10분에 다시 런던으로 출발하면 옥스포드의 승객들은 5분 차이로 기차를 놓치게 된다. 승객은 몇 분의 시차로 인해 기차를 놓칠 수 있었고, 기관사는 몇 분의 시차로 인해 다른 기차와 충돌할 수도 있었다. 1840년 그레이트 웨스턴 철도는 런던의 그리니치 평균시(Greenwich Mean Time, GMT)를 모든 역과 시간표에서 사용하도록 했으며, 1847년 철도청산소[1]는 영국의 모든 철도 회사가 가능한 빨리 GMT를 채택할 것을 권고했다.[2] 결국에는 영국 전역에서 GMT를 표준시(Standard time)로 사용하게 된다.

            +

            GMT는 그리니치 천문대에서 관측하는 시간이다. 그리니치에서 태양이 최고 고도에 이르는 시각을 낮 12시로 정하고, 이로부터 다음날 태양이 최고 고도에 이르는 시각까지를 24시간으로 쪼갠다. 그러면 지구가 24시간 동안 360도를 회전하며, 1시간에 15도씩 돈다고 볼 수 있다. 따라서 그리니치를 지나는 자오선을 경도 0도의 기준으로 삼아 본초 자오선으로 정하고, 지구를 15도씩 쪼개면 경도 15도마다 1시간씩 차이가 발생한다. 이를 바탕으로 영역을 구분한 것을 시간대(Time zone)라고 부르며, 런던은 GMT+0, 베를린은 런던보다 1시간 빠른 GMT+1, 서울은 8시간 30분 빠른 GMT+08:30 시간대에 놓이게 된다. 여러 제국주의 열강들이 자국의 수도를 지나는 자오선을 본초 자오선으로 정하고자 했지만, 이미 곳곳에서 사용되던 GMT가 세계시(Universal time)의 기준이 되었다.

            +

            평균시라는 말을 쓰는 이유는, 오늘 태양이 최고 고도에 이르는 시각부터 내일 태양이 최고 고도에 이르는 시각까지 정확히 24시간이 걸리지 않기 때문이다. 지구는 타원으로 공전하고, 태양은 타원의 중심에 있지 않고, 지구가 태양과 가까워지면 공전 속도가 빨라지며, 멀어지면 공전 속도가 느려지고, 지구의 자전축은 23.5도 기울어져있다. 따라서 태양이 최고 고도에 이르는 시각을 무조건 12시 정각으로 삼는 겉보기 태양시를 사용하면 매일 하루의 길이에 몇 초씩 차이가 발생한다. 이러한 겉보기 태양시의 오차를 보정하기 위해 겉보기 태양시의 평균을 낸 시간 체계를 평균시라고 말한다. 즉, 하루가 24시간이라는 말은 하루가 평균적으로 24시간이라는 말이다. 앞서 GMT를 겉보기 태양시로 관측하는 것처럼 설명했는데, 사실 겉보기 태양시로부터 얻은 평균시라는 점에 유의해야 한다.

            +

            원자의 시간

            +

            +UK National Physical Laboratory (CC0)

            +

            GMT처럼 태양을 기준으로 하는 시간 체계를 통틀어 태양시(Solar time)라고 한다. 그런데 20세기에 조석력으로 인해 지구의 자전 속도가 불규칙하다는 사실을 알게 된다. 이전까지는 지구가 태양 주위를 한 바퀴 도는 시간인 31,556,992초를 기준으로 1 / 31556992를 1초의 정의로 사용했는데, 그 정의가 불규칙하다는 뜻이 된다. 그러다 1955년 세슘(Caesium, 55Cs_{55}\text{Cs})의 유일한 안정 동위원소 세슘-133을 이용한 원자 시계가 나왔고, 1967년 국제도량형총회(CGPM)는 세슘-133 원자가 방출, 흡수하는 전자기파가 9,192,631,770번 진동하기까지 걸리는 시간을 1초로 정의했다.[3] 이처럼 원자를 기준으로 하는 시간 체계를 원자시(Atomic Time)라고 하며, 전세계 수백 개의 원자 시계에서 측정하는 원자시를 바탕으로 국제원자시(International Atomic Time, TAI) 표준을 정한다.

            +

            1초의 기준이 태양시가 아닌 원자시로 변경되었기 때문에 세계시의 기준인 GMT도 다시 생각해볼 필요가 있었다. 이에 따라 원자시를 기반으로 한 협정 세계시(UTC)를 GMT 대신 사용하게 되었다. 약자가 UTC인 이유는 CUT(Coordinated Universal Time)를 추진한 영미권과 TUC(Temps Universel Coordonné)를 추진한 프랑스어권 사이의 절충안으로, 같은 알파벳 구성에 순서만 뒤바꾼 UTC를 채택했기 때문이다.[4]

            +

            그런데 UTC와 GMT는 소수점 단위에서만 차이가 날 뿐만 아니라, UTC도 그리니치 자오선과 거의 차이가 없는 자오선을 본초 자오선으로 삼고 있기 때문에 일상에서는 UTC와 GMT를 혼용한다. UTC의 시간대도 GMT와 마찬가지로 런던은 UTC+0, 베를린은 UTC+1, 서울은 UTC+08:30 시간대에 놓인다. 단, 각국의 표준 시간대는 이를 그대로 따르지 않고 각자의 결정권에 따라 채택한다. 파리는 UTC+0 시간대에 있지만 중앙유럽 표준시(Central European Time, CET)에 따라 UTC+1을 사용하며, 서울은 UTC+08:30에 있지만 한국 표준시(Korean Standard Time, KST)를 따라 UTC+9를 사용한다. 서머타임(Summer time) 혹은 일광절약시간제(Daylight Saving Time, DST)를 사용하는 국가에서는 계절에 따라 또 다른 시간대를 사용한다.

            +

            +US Central Intelligence Agency (CC0)

            +

            앞서 언급했듯이 지구의 자전 속도가 불규칙하기 때문에 천체의 움직임을 기반으로 하는 태양시와 원자시를 기반으로 하는 UTC 사이에는 오차가 발생한다. 국제지구자전좌표국(International Earth Rotation and Reference Systems Service, IERS)은 태양시와 UTC 사이의 오차가 0.9초를 넘으면 이를 보정하기 위해 UTC에 1초를 더하거나 빼주는데, 이를 윤초(Leap second)라고 한다.[5] 태양시를 중심으로 사용하던 과거에는 지구의 자전 속도에 따라 시간 체계가 통째로 영향을 받았지만, 이제는 자전 속도가 변한만큼 윤초를 이용해 시간을 보정할 수 있게 된 것이다. 윤초는 UTC 기준 6월 30일 또는 12월 31일 23시 59분 59초에 적용하며, 1초 뒤를 0시 0분 0초가 아닌 23시 59분 60초로 표현하는 방식으로 윤초를 추가한다. IERS는 통상 6개월 전에 윤초 적용을 예고하고 있다.

            +

            운영체제의 시간

            +

            +Raimond Spekking (CC BY-SA 4.0)

            +

            컴퓨터는 RTC(Real Time Clock)라는 하드웨어 장치를 이용해 시간을 측정한다. 오늘날 시간 정보가 필요한 거의 모든 전자기기에는 RTC가 들어있다. 대부분의 RTC는 수정 발진기(Quartz oscillator)를 사용하는데, 석영 결정에 전압을 걸었을 때 32.768kHz 주파수로 진동하는 것을 1초의 기준으로 삼는 원리다.[6] 전원이 분리되어 있어서 컴퓨터가 꺼져도 RTC는 꾸준히 시간을 측정할 수 있다.

            +

            운영체제 수준에서는 컴퓨터 시스템 전역에 설정된 시간을 시스템 시간(System time)이라고 한다. 리눅스에서 timedatectl 명령을 실행하면 시스템의 로컬 시각(Local time)과 UTC 시각, RTC 시각, 타임존 등의 정보를 볼 수 있다.

            +
            $ timedatectl
            +                      Local time: Sun 2022-01-02 00:00:00 KST
            +                  Universal time: Sat 2022-01-01 15:00:00 UTC
            +                        RTC time: Sat 2022-01-01 15:00:00
            +                       Time zone: Asia/Seoul (KST, +0900)
            +       System clock synchronized: yes
            +systemd-timesyncd.service active: yes
            +                 RTC in local TZ: n
            +
            +

            유닉스 계열 운영체제는 UTC 기준 1970년 1월 1일 0시 0분 0초로부터 몇 초가 지났는지를 기준으로 시스템 시간을 관리한다. 이를 유닉스 시간이나 POSIX 시간, 또는 에포크 시간(Epoch time)이라고 부른다. 에포크는 UTC 기준 1970년 1월 1일 자정을 일컫는다. 하필 이 날짜인 이유는 그리 대단치 않다. 벨 연구소에서 유닉스 시스템을 개발한 데니스 리치(Dennis Ritchie)는 그저 당분간 오버플로우가 발생하지 않을만한 기원 날짜를 하나 정하기로 했는데 우연히 1970년 1월 1일을 고르게 되었다고 말했다.[7]

            +

            유닉스 시간은 일반적으로 초 또는 밀리초 단위의 타임스탬프(Timestamp)로 표현한다. 가령 UTC 기준 2022년 1월 1일 0시 0분 0초의 타임스탬프는 1640995200이다. 이 타임스탬프는 시간대에 상관없이 특정 순간(Instant)을 표현하기 때문에 CET(UTC+1) 기준 2022년 1월 1일 1시 0분 0초에도 대응되고, KST(UTC+9) 기준 2022년 1월 1일 9시 0분 0초에도 대응된다. 2022-01-01T00:00:00Z, 1640995200(초), 1640995200000(밀리초) 모두 같은 시각에 대한 타임스탬프이다. 날짜, 시간 데이터에 대한 표준 규격은 ISO 8601에서 정의하고 있으며[8], 인터넷 표준으로는 RFC 3339에서 ISO 8601을 기반으로 정의하고 있다.[9]

            +

            32비트 정수형을 사용하는 유닉스 시간은 2,147,483,647까지 밖에 표현할 수 없는데, 이로 인해 UTC 기준 2038년 1월 19일 3시 14분 7초를 지나면 오버플로우가 발생한다. 이를 2038년 문제(Year 2038 Problem, Y2K38)라고 한다. 64비트 시스템에서는 이미 유닉스 시간에 64비트 정수형을 사용하고 있지만, 구형 시스템은 조치가 필요하다.

            +

            운영체제의 표준 인터페이스와 환경을 정의하는 IEEE Std 1003.1-2017에는 ‘에포크로부터 경과한 초’(Second Since the Epoch)를 ‘에포크로부터 경과한 초에 근사한 값’으로 정의한다. 단순한 사칙연산만으로 UTC로부터 유닉스 시간을 구할 수 있다.[10]

            +
            tm_sec + tm_min*60 + tm_hour*3600 + tm_yday*86400 +
            +    (tm_year-70)*31536000 + ((tm_year-69)/4)*86400 -
            +    ((tm_year-1)/100)*86400 + ((tm_year+299)/400)*86400
            +
            +

            유닉스 시간이 UTC를 근사할 뿐 일대응 대응하지 않는 이유는 윤초를 전혀 고려하지 않기 때문이다. 이로 인해 구현은 단순해졌지만, 시간이 단조 증가한다고 보장할 수 없는 문제가 발생한다. 실제로는 UTC에 윤초가 추가된다 해도 시스템에서 1일은 정확히 86,401초다. 따라서 12월 31일 23시 59분 60초의 타임스탬프와 1월 1일 0시 0분 0초의 타임스탬프가 동일하다. 즉, 같은 시간을 두 번 지나며, 소수점 단위에서 시간이 역행할 수도 있다.[11]

            +

            윤초를 차치하고도 시스템 시간은 꼭 단조 증가하지 않는다. 아주 단순한 파이썬 코드에도 문제가 생길 수 있다. 프로그램 실행 중에 사용자가 시스템에 설정된 시각을 12시간 앞으로 되돌리는 바람에 프로그램의 실행 시간이 -12시간이 걸렸다고 잘못 측정되는 케이스다.

            +
            import time
            +
            +start = time.time() # 2022-01-01T19:00:00
            +do_something() # 임의로 시스템에 설정된 시각을 과거로 되돌린다
            +end = time.time() # 2022-01-01T07:00:00
            +
            +print(end - start) # 실행에 -12시간이 걸렸다
            +
            +

            다행히 대부분 언어의 표준 라이브러리는 단조 시계(Monotonic clock)를 사용할 수 있는 API를 제공한다. 단조 시계는 현재 시각을 가리키는 것이 아니라, 일반적으로 운영체제 구동 이후 몇 초가 지났는지를 가리키기 때문에 시간이 역행하지 않음을 보장한다. 파이썬의 표준 라이브러리 모듈 time에는 monotonic 함수가 있다.[12]

            +
            import time
            +
            +start = time.monotonic() # 2022-01-01T19:00:00
            +do_something() # 임의로 시스템에 설정된 시각을 과거로 되돌린다
            +end = time.monotonic() # 2022-01-01T19:01:00
            +
            +print(end - start) # 실행에 1분이 걸렸다
            +
            +

            러스트의 표준 라이브러리 모듈 timeSystemTime 구조체에 대한 문서는 시간이 단조 증가하지 않는다는 유의 사항을 아주 구체적으로 밝히며 단조 증가하는 시간이 필요하다면 Instant 구조체를 사용하라고 안내하고 있다.[13] 리눅스에서는 clock_gettime 시스템 콜[14]을 통해 시스템 시간에 접근할 수 있는데, CLOCK_REALTIME 인자를 전달하면 시스템에 설정된 현재 시각을 얻을 수 있고, CLOCK_MONOTONIC 인자를 전달하면 단조 시계로 측정된 시간을 얻을 수 있다.[15]

            +

            네트워크 타임 프로토콜(Network Time Protocol, NTP)을 이용해 원자 시계와 동기화된 서버로부터 정확한 UTC 기준 타임스탬프를 받아와 RTC 시간 내지는 시스템 시간과 동기화하는 것도 가능하다.[16] 물론 여기에는 윤초도 적용되며, 실제로 리눅스는 윤초를 처리하기 위해 NTP를 사용하고 있다. 만약 양의 윤초가 추가되면 UTC가 시스템의 시계를 따라잡을 때까지 시계 속도를 늦추는 등의 방식으로 시스템 시간에 윤초를 적용할 수 있다.[17] NTP 시스템은 네트워크 지연을 최소화하기 위해 계층 구조를 이룬다. 0 계층(Stratum 0) 원자 시계와 직접 동기화되는 1 계층(Stratum 1) NTP 서버가 있고, 1 계층과 동기화되는 2 계층 NTP 서버가 있다. 한국에서는 한국표준과학연구원, 포항공과대학교 등에서 1 계층 NTP 서버를 운영하고 있다.[18]

            +

            애플리케이션의 시간

            +

            불특정 다수에게 서비스하는 웹 서버는 클라이언트의 접속 위치를 특정할 수 없으므로 다양한 시간대와 시간 표현 등을 고려해야 한다. 어떤 사용자는 UTC+9 시간대에서 서비스를 이용하고, 어떤 사용자는 UTC+05:45 시간대에서 이용한다. 그리고 또 어떤 사용자는 DST가 적용된 시간대에서 이용한다. 이들 모두에게 적절한 시간 정보를 제공하기 위해서는 서버가 일관된 시간대를 사용해야 한다. 일반적으로 직관적인 계산을 위해 서버 시간대는 UTC+0으로 설정한다. 따라서 서버와 클라이언트 사이에 사용하는 API도 UTC+0 시간대를 전제한다.

            +

            API를 통해 서버와 클라이언트가 시간 데이터를 주고받을 때는 주의할 필요가 있다. 연호를 사용하는 일본력은 어떻게 표현할지, 1970년 이전에 태어난 사람의 생일은 어떻게 표현할지 고민해야 한다. 마이크로소프트 REST API 가이드라인은 ECMAScript 언어 명세에서 정의한 YYYY-MM-DDTHH:mm:ss.sssZ 포맷[19]을 사용하는 DateLiteral 형식을 제안한다.

            +
            { "creationDate" : "2015-02-13T13:15Z" }
            +
            +

            또한 시간의 종류(kind)와 그 값(value)을 함께 제공할 수 있는 StructuredDateLiteral 형식도 함께 제시하고 있다.[20]

            +
            [
            +  { "creationDate" : { "kind" : "O", "value" : 42048.55 } },
            +  { "creationDate" : { "kind" : "E", "value" : 1423862100000 } }
            +]
            +
            +

            대부분의 현대 프로그래밍 언어들은 효과적으로 시간을 다루기 위한 인터페이스를 갖추고 있다. 코틀린의 경우 자바의 표준 라이브러리를 확장한 인터페이스를 제공한다. 자바의 시간 라이브러리는 각종 문제를 지닌 것으로 악명이 높았지만,[21] 다행히 현재 코틀린은 자바8부터 수정된 인터페이스를 사용하고 있다. 에포크 시간의 타임스탬프를 다루는 Instant 클래스와 기간을 다루는 Duration 클래스 등이 있으며, 시간대 정보가 없는 LocalDateTime 클래스, 시간대 정보를 지닌 ZonedDateTime 클래스도 제공된다. 만약 시간대 정보가 없는 LocalDateTime 시각을 Instant 객체로 변환하고 싶다면 시간대 정보가 필요하다.

            +
            LocalDateTime.of(2022, 1, 1, 0, 0, 0).toInstant(ZoneOffset.of("+0900"))
            +
            +

            LocalDateTime에 시간대 정보가 없기 때문에 toInstant 함수에 ZoneOffset을 인자로 전달해 해당 시각을 어떤 시간대로 취급할 것인지 명시했다. 2022년 1월 1일 자정을 UTC+0(ZoneOffset.UTC)으로 취급하면 타임스탬프가 1640995200000이 되는 반면, UTC+9(ZoneOffset.of("+9"))로 취급하면 1640962800000가 되어 32400000 밀리초(9시간)만큼 차이가 발생한다. 같은 시간에 대한 타임스탬프가 서로 다른 것처럼 느껴지지만, 사실은 시간대가 전제되는 것이다. LocalDateTime을 사용한다면 항상 UTC+0 기준임을 전제하는 것이 혼란을 줄이는 데 도움이 된다. 마찬가지로 LocalDateTime 시각을 특정 시간대의 시각으로 변환할 때 역시 취급할 시간대 정보를 명시해야 한다.

            +
            fun LocalDateTime.toKST(zoneId: ZoneId = ZoneId.of("UTC")) =
            +  ZonedDateTime.of(this, zoneId)
            +    .withZoneSameInstant(ZoneId.of("Asia/Seoul"))
            +    .toLocalDateTime()
            +
            +

            UTC 시각을 KST 시각으로 바꾸기 위해 plusHours(9)를 적용하는 것보다 훨씬 우아하다.

            +

            한 국가의 표준시는 정치적, 사회적 이유로 언제든 변경될 수 있다. 한국은 1954년에 표준시를 GMT+9에서 GMT+08:30으로 변경했다가 1961년부터 다시 GMT+9(UTC+9)를 쓰고 있다. 2013년에는 표준시를 UTC+08:30으로 변경하는 표준시법 개정안이 발의되기도 했다. 또한 1948년부터 60년까지, 그리고 87년부터 88년까지 DST를 시행했다. 많은 시스템이 과거와 현재, 그리고 미래의 시간대 정보까지 정확하게 보장받기 위해 별도의 표준 데이터베이스인 TZDB(IANA Time Zone Database)를 참조한다. TZDB는 엔지니어, 역사학자 커뮤니티가 운영하고 있어 상당히 신뢰도가 높다.[22]

            +

            여기까지 태양시부터 철도 시간, 원자시, 시스템 시간과 유닉스 시간, 그리고 애플리케이션 수준에서의 시간을 살펴봤다. 시간은 까다로운 개념이고, 그것을 다루는 것은 더 까다로운 일이다. 시간 체계를 이해하고 있어도 실수를 하겠지만, 적어도 문제가 발생했을 때 무엇이 왜 잘못됐는지는 이해할 수 있을 것이다. 나는 시간과 관련된 실수를 많이 했다. 그런 실수들의 원인을 총체적으로 이해한 것은 실제 시간 체계와 내가 사용하는 API의 시간 체계를 이해한 뒤였다. 이 글은 과거에 대한 반성문이자 오답 노트이며, 이 오답 노트가 앞으로 나와 같은 실수를 할 소프트웨어 엔지니어에게 도움이 되길 바란다.

            +
            +
            +
              +
            1. 철도 회사들이 서로의 철도를 사용했을 때 수익을 배분하기 위한 단체, 영국 철도 위원회 설립 전까지 영국 철도를 관리, 감독했다. ↩︎

              +
            2. +
            3. Greenwich Mean Time, “Railway Time - From natural time to clock time”. ↩︎

              +
            4. +
            5. BIPM, “The International System of Units 9th edition”, 2019, p.130. ↩︎

              +
            6. +
            7. NIST, “NIST Time Frequently Asked Questions”, 2019. ↩︎

              +
            8. +
            9. 이태형, “윤초와 1초의 의미”, The Science Times, 2017. ↩︎

              +
            10. +
            11. Kalpesh Lodhia, “Quartz clocks and watches - How do they work?”, Arnik Jewellers, 2015. ↩︎

              +
            12. +
            13. Farhad Manjoo, “Unix Tick Tocks to a Billion”, Wired, 2001. ↩︎

              +
            14. +
            15. Date and time - Representations for information interchange - Part 1: Basic rules, ISO 8601-1:2019, 2019. ↩︎

              +
            16. +
            17. G. Klyne, C. Newman, “Date and Time on the Internet: Timestamps”, RFC 3339, 2002. ↩︎

              +
            18. +
            19. The Open Group Base Specifications Issue 7, 2018 edition, “General Concepts”, IEEE Std 1003.1-2017, 2018. ↩︎

              +
            20. +
            21. 강성훈, “time()”, 메아리 저널, 2009. ↩︎

              +
            22. +
            23. Python 3.10.2 Documentation, “time - 시간 액세스와 변환”, 2022. ↩︎

              +
            24. +
            25. The Rust Standard Library Version 1.58.1, “SystemTime in std::time”, 2022. ↩︎

              +
            26. +
            27. 운영체제 위에서 동작하는 응용 프로그램이 커널의 서비스에 접근할 수 있도록 하기 위한 인터페이스를 말한다. ↩︎

              +
            28. +
            29. The Open Group Base Specifications Issue 7, 2018 edition, “clock_getres”, IEEE Std 1003.1-2017, 2018. ↩︎

              +
            30. +
            31. D. Mills et al., “Network Time Protocol Version 4: Protocol and Algorithms Specification”, RFC 5905, 2010. ↩︎

              +
            32. +
            33. Miroslav Lichvar, “Five different ways to handle leap seconds with NTP”, Red Hat Developer, 2015. ↩︎

              +
            34. +
            35. 이화여자대학교 사범대학 부속초등학교, “국내 타임서버 리스트”, 2021. ↩︎

              +
            36. +
            37. The ECMAScript Language Specification, “Date Time String Format”, “Standard ECMA-262 5.1 Edition”, 2011. ↩︎

              +
            38. +
            39. Dave Campbell et al., “Microsoft REST API Guidelines, Guidelines for dates and times”, Microsoft, 2021. ↩︎

              +
            40. +
            41. 정상혁, “Java의 날짜와 시간 API”, Naver D2, 2014. ↩︎

              +
            42. +
            43. 김동우, “자바스크립트에서 타임존 다루기 (1)”, NHN Cloud Meetup, 2017. ↩︎

              +
            44. +
            +
            + +
            +
            + +
            + +
            +
            +

            함수형 프로그래밍의 설득력

            +

            자바와 하스켈의 차이는 어디에서 비롯되는가?

            +
            +
            +
            + + +
            + +
            +
            +

            🧱 Server Driven UI 설계를 통한 UI 유연화

            +

            클라이언트 배포없이 화면 구성 변경하기

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/4.html b/article/4.html new file mode 100644 index 0000000..3b02cbc --- /dev/null +++ b/article/4.html @@ -0,0 +1,125 @@ + + + + + + 차이를 중심으로 살펴본 UI디자인과 UX디자인 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 차이를 중심으로 살펴본 UI디자인과 UX디자인 +

            + +

            + UI는 심미성, UX는 사용성? +

            + + + + +
            +
            Table of Contents +

            +
            +

            UX디자인이라는 말은 유행처럼 쓰이기 시작하더니 어느 날부터 UI디자인과 비슷한 개념, 또는 UI디자인을 조금 ‘있어 보이게’ 일컫는 말처럼 쓰이게 되었다. 하지만 UX디자인에 관한 수많은 서적과 자료들, 그리고 실제로 일어나고 있는 기업의 투자를 보면 정말로 UX디자인이 UI디자인과 같은 개념이거나 실체가 없는 것이라고 판단하기는 어렵다. 아무래도 UX디자인이라는 용어가 오남용되고 있는 것이라고 추측했는데, 생각해보니 나 역시 그 둘을 확실히 구분하지 못하고 있음을 깨닫게 되었다. 따라서 UX디자인과 UI디자인의 차이를 밝힘으로써 UX디자인의 개념을 명확히 알아보고자 한다.

            +

            +

            UI디자인과 UX디자인의 차이에 대해 찾아보면 대부분 UI디자인은 ‘예쁘게 하는 것’이고, UX디자인은 ‘편리하게 하는 것’이라는 설명이 많이 보인다. 그러나 UI디자인도 사용성을 고민하며, UX디자인도 심미성을 고민한다. (즉, 위 사진의 비유는 잘못되었다.) 또한 UX디자인을 심리학과 같은 선상에 놓는 경우도 있다. 디자인 과정에 ‘사람은 웹 페이지를 F자 형태로 읽는다’와 같은 심리학적인 요소가 접목된다고 UX디자인의 차별점이 생긴다고 보기는 어렵다. 물론 심리학이 UX를 증진시킬 수는 있겠지만, 그것이 UX디자인의 전부라면 UX디자인은 단지 고도화된 UI디자인에 불과할 것이다.

            +

            UI/UX디자인 연구, 컨설팅 회사인 닐슨 노먼 그룹(Nielsen Norman Group)은 UX를 ‘사용자가 기업, 서비스, 기업의 제품과 상호작용하면서 얻는 모든 측면의 경험’이라고 정의한다. 나는 여기서 ‘모든 측면’이라는 말에 주목했다. 사용자는 제품에 대해 생각-탐색-사용-성찰의 과정을 거친다. '필요해’부터 '뭐가 좋을까?'를 거쳐 ‘편리하다’ 그리고 '좋았다’까지의 경험 설계를 UX디자인이라고 할 수 있다.

            +
              +
            • 사용 전: 제품을 알게 된 과정은 어땠나? 비슷한 제품을 사용한 경험이 있나?
            • +
            • 사용 중: 제품의 심미성과 사용성은 어떤가? 기능은 편리한가?
            • +
            • 사용 후: 다시 사용할만한 가치가 있나? 지인에게 추천할만한가?
            • +
            +

            UI디자인은 사용자와 제품이 접하는 지점, 말그대로 인터페이스를 디자인함으로써 사용성을 높이는 영역이고, UX디자인은 사용자가 제품을 접하기 전부터 접한 이후까지의 총체적인 부분을 디자인함으로써 사용성을 높이는 영역이다.

            +

            +

            위 벤다이어그램이 거의 완벽하게 UX디자인의 범위를 표현하고 있다고 생각한다. UI보다는 UX가 더 넓은 범위로, UI는 UX를 구성하는 요소 중 하나다. 마케팅이나 기술적 성능, 개발, 품질 관리 등은 UI디자인과는 크게 관련이 없지만 결과적으로 사용자의 만족도와 경험을 개선할 수 있는 UX의 요소들이다. 가령 구글이나 애플이 구축한 '생태계’는 단지 한 부서나 분야에만 국한된 것이 아니라, 모든 UX 요소가 유기적으로 상호작용하며 만들어진 것이다. 같은 맥락에서, 흔히 거론되는 주제인 '심미성이 중요한가, 사용성이 중요한가’에 관한 논쟁도 UX의 관점에서 보면 의미있는 비교라고 할 수 없다. 서로가 서로를 압도하는 관계가 아니라, 심미성이 사용성을 증진하는 역할을 하고 있기 때문이다.

            +

            UI가 UX와 묶여서 언급되는 이유는 아무래도 UI가 UX에 큰 영향을 미치기 때문이 아닐까 싶다. 제품을 사용하는 과정에서 UI는 그 실체가 시각적으로 뚜렷이 드러나니 UX에 큰 영향을 미칠 수 밖에 없다. 그래서 현업에서는 인터페이스를 벗어나면 UX의 각 요소가 파편화되어 완전히 다른 분야로 취급된다. 특히 기능 조직에서는 인터페이스에서 멀어질수록 그 정도가 심해진다. 나는 각 분야가 전문화, 세분화될 때 사용자 경험이라는 한 맥락에서 마케팅, 기획, 디자인, 개발을 이해하는 UX디자이너가 필요하다고 생각한다. 또한 제품의 목표는 결국 사용자에게 보다 긍정적인 경험을 제공하는 것이므로, 서로 다른 분야를 통합하여 제품이 사용자 경험 증진이라는 하나의 목표를 이룰 수 있도록 만드는 것이 UX디자이너의 핵심적인 역할이라고 본다.

            +

            References

            + + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.1

            +

            Overview

            +
            +
            +
            + + +
            + +
            +
            +

            프로세스간 통신을 활용해 프로그래밍하기

            +

            학적 관리 프로그램 만들기

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/40.html b/article/40.html new file mode 100644 index 0000000..f820cf0 --- /dev/null +++ b/article/40.html @@ -0,0 +1,191 @@ + + + + + + 함수형 프로그래밍의 설득력 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 함수형 프로그래밍의 설득력 +

            + +

            + 자바와 하스켈의 차이는 어디에서 비롯되는가? +

            + + + + +
            +
            Table of Contents +

            +
            +

            이제 함수형 프로그래밍은 확실히 주류라고 할 만하다. 이미 많은 프로그래밍 언어가 함수형 프로그래밍의 핵심 개념을 차용했고, 그린랩스와 같은 회사들이 엔터프라이즈 규모에서 함수형 프로그래밍의 성공적인 사례를 만들어내고 있다. 함수형 프로그래밍에 관해 설명하는 도서나 자료도 쉽게 찾아볼 수 있다. 최근에는 프로그래밍 입문자들도 함수형 프로그래밍의 중요성을 인식하는 것 같다.

            +

            그러나 여전히 함수형 프로그래밍을 진지하게 생각하는 프로그래머는 많지 않다. 이제 막 함수형 프로그래밍이 무엇인지 알아보기 시작한 프로그래머는 스스로를 의심하곤 한다: ‘나는 매일 map, filter, reduce 함수를 사용하고 있으니 함수형 프로그래머인가?’. 자연스럽게 일급 함수를 다루고 리스트의 불변성을 보장하므로 맞는 말이지만, 프로그래머의 마음속에 있는 의심을 깨끗하게 씻어주지는 않는다. 마찬가지로 "여러분이 매일 사용하는 그 언어가 함수형 프로그래밍 언어이며, 우리는 모두 함수형 프로그래밍을 하고 있습니다"라는 말도 공허하다. 그것이 사실이고, 함수형 프로그래밍에 대한 심리적 장벽을 낮춰줄 수도 있겠지만, 사람들이 기대하는 함수형 프로그래밍은 그것이 아니기 때문이다.

            +

            그렇다고 펑터니, 모나드니 하는 개념은 소위 말하는 프로그래밍 언어 덕후들이나 파고드는 주제처럼 받아들여진다. 하스켈은 차라리 수학에 가까워 보인다. 함수형 프로그래밍의 각종 장점을 나열하며 이것이 당위인 것처럼 말해도 사람들에게는 전혀 설득력이 없다. 존 휴스(John Hughes)가 Why Functional Programming Matters[1]에서 언급했듯이, 여전히 함수형 프로그래머는 '고결해지기 위해 금욕적인 삶을 사는 중세 수도승’처럼 보인다. 휴스의 말대로 함수형 프로그래밍이 설득력을 얻기 위해서는 실질적인 이득을 증명해야 한다.

            +

            패러다임으로서의 함수형 프로그래밍

            +

            함수형 프로그래밍을 이해하기 위해 가장 먼저 하는 시도는 함수형 프로그래밍 언어라고 불리는 언어를 찾아보는 것이다. 무엇이 함수형 프로그래밍 언어인가? 프로그래머들 사이에서 주기적으로 논쟁거리가 되는 '순환 떡밥’이다. 하지만 이 논쟁은 함수형 프로그래밍에 대해 합의된 기준과 정의가 없어서 항상 소모적으로 흘러간다. 그런데 넓은 의미에서는 일급 함수(First-class function)를 지원한다면 함수형 프로그래밍 언어다. 어떤 언어에서 함수를 변수와 동일하게 다룰 수 있다면, 즉, 함수를 변수에 할당할 수 있고, 다른 함수의 인자로 함수를 전달할 수 있고, 함수가 함수를 반환할 수 있다면 그 언어는 일급 함수를 지원한다고 말할 수 있다. 흔히 함수형 프로그래밍의 핵심으로 언급되는 순수 함수나 불변성, 지연 평가, 참조투명성, 커링과 같은 것들은 언어 설계의 측면에서 봤을 때 일급 함수에 비하면 부차적이다.

            +

            이러한 정의에 따르면 일급 함수를 지원하는 자바스크립트, 파이썬, 자바는 모두 함수형 프로그래밍 언어라고 할 수 있다. 실제로 이들로 함수형 프로그래밍을 하는 것이 가능하다. 그런데 정말 자바가 하스켈과 같은 함수형 프로그래밍 언어라면 우리가 자바와 하스켈을 비교할 때 느끼는 그 미묘한 차이는 어디에서 오는 것일까? 일급함수에 대한 합의된 정의가 있음에도 불구하고 매번 무엇이 함수형 프로그래밍 언어인가에 대한 논쟁이 반복되는 이유는 이 미묘한 차이를 설명하기가 쉽지 않기 때문일지도 모른다.

            +

            이때 느껴지는 미묘한 차이에는 하스켈에 대한 오해도 한몫하겠지만, 나는 두 언어가 지향하는 패러다임이 가장 큰 차이라고 생각한다. 프로그래밍 패러다임이 토머스 쿤(Thomas Kuhn)이 말한 과학적 패러다임과 완전히 같다고 할 수는 없겠지만, 시대에 따라 프로그래머 커뮤니티가 일반적으로 지향하는 공통된 이론이나 법칙, 믿음, 가치가 있다는 점에서 개념을 끌어올 수 있을 것이다. 패러다임으로서 함수형 프로그래밍을 말하기 위해서는 함수형 프로그래밍 자체가 무엇인지 설명하는 것으로 충분치 않다. 프로그래밍 패러다임이 거쳐온 역사적 맥락을 살펴볼 필요가 있다.

            +

            현재 우리가 '함수’라고 부르는 프로그래밍 요소의 시초는 서브루틴(Subroutine)이다. 서브루틴은 그저 프로그램의 실행 흐름을 메인에서 서브로 점프시키는 용도였다. 이때 메인에 값을 반환하는 서브루틴을 함수라고 불렀다. 나중에는 함수와 서브루틴의 구분이 사라지고 모든 서브루틴을 함수라고 부르게 되었다. 이 함수가 수학에서의 함수와는 다르다는 점에 유의해야 한다. 함수 f(x)f(x)는 아무런 값을 의미하지 않거나(void), 인자 xx 자체를 변경하거나(call-by-reference), 함수 외부에 있는 임의의 변수를 변경할 수 있다.

            +

            실행 흐름을 다른 서브루틴으로 이동시키는 goto 구문은 실행 흐름을 마구 점프시키는 문제를 일으켰다. 이러한 문제의식을 바탕으로 구조적 프로그래밍 패러다임이 제시되었다. 다익스트라(Edsger Dijkstra)는 Go To Statement Considered Harmful[2]에서 goto 구문이 프로그램의 모듈화를 방해하며, 모든 프로그램은 순차, 분기, 반복이라는 세 가지 논리 구조만으로 표현할 수 있다고 주장했다. 오늘날 많은 프로그래머가 처음 C언어를 배울 때 goto 구문을 사용하지 말라는 경고를 받곤 하는데, 모두 구조적 프로그래밍 패러다임에서 비롯된 것이다. 대부분의 현대적 프로그래밍 언어에 goto와 같은 구문이 없는 이유도 구조적 프로그래밍 패러다임의 영향이라고 할 수 있다.

            +

            소프트웨어가 복잡해짐에 따라 프로그램의 모듈화는 더욱 중요한 문제가 되었다. 객체지향 프로그래밍 패러다임은 프로그램을 객체의 집합으로 보고, 그 객체 간의 상호작용으로 프로그램이 동작한다고 해석한다. 순수 객체지향 프로그래밍 언어를 표방하는 스몰토크(Smalltalk)는 오늘날 우리가 말하는 객체지향 프로그래밍 패러다임의 뼈대를 구축했다. 그리고 자바는 객체지향 프로그래밍의 상업적 성공을 추동했다. 지금도 기업, 정부, 학교 등 모든 곳에서 범용적으로 자바가 쓰이며, 이에 따라 자바가 추구하는 객체지향 프로그래밍 패러다임도 모든 곳에서 자연스럽게 받아들여졌다. 시간이 흐르면서 객체지향 프로그래밍 패러다임은 많은 수정과 변화를 거쳤지만, 지금도 객체지향 프로그래밍의 특성을 나열하는 것만으로 어느 정도 객체지향 프로그래밍 패러다임을 묘사할 수는 있다. 다만 일반적으로 객체지향 프로그래밍 패러다임의 핵심 특성으로 꼽는 캡슐화, 상속, 다형성, 메시지 패싱 등이 정말 핵심 특성인가에 대한 의견은 분분하다. 이들은 사실 객체지향 프로그래밍만의 전유물이 아니기 때문이다. 함수형 프로그래밍 언어에 대한 합의된 기준이 없어서 매번 논쟁거리가 되듯이, 객체지향 프로그래밍도 마찬가지인 것 같다.

            +

            이 모든 패러다임에서 함수는 특별 취급을 받았다. 다른 값과 달리 함수는 변수에 할당할 수 없고, 다른 함수에 인자로 전달할 수도 없고, 함수가 함수를 반환하는 것도 불가능했다. 함수가 수학에서의 함수와 다르게 동작하는 것도 여전했다. 따라서 함수를 호출했을 때 어떤 일이 일어날지 쉽게 예측할 수가 없었다. 함수형 프로그래밍 패러다임은 프로그램 자체를 입력과 출력이 있는 일종의 함수라는 관점으로 해석하고, 하나의 프로그램은 다양한 함수의 합성으로 구성된다고 말한다. 이런 식으로 프로그램을 해석함으로써 소프트웨어를 수학적으로 엄밀하게 정의하고, 증명할 수 있게 되었다. 이를 위해서는 이전과 달리 함수를 특별 취급하지 않도록 '정상화’할 필요가 있었다. 우리가 함수형 프로그래밍의 특성이라고 부르는 요소들이 모두 그것으로부터 출발한다.

            +

            다시 자바와 하스켈의 차이가 어디에서 비롯되는지에 대한 질문으로 돌아가자. 프로그래밍 언어의 정체성을 구분하는 가장 큰 기준 중 하나는 언어가 프로그래머에게 가하는 제약이다. 자바와 하스켈 모두 함수형 프로그래밍이 가능하지만, 그 제약에 차이가 있다. 자바는 순수 함수를 강제하지 않는다. 변수나 객체의 상태를 변경하는 데도 제약이 없다. 반면 하스켈에는 강한 제약이 있다. 하스켈로 객체지향 프로그래밍도 할 수 있지만, 그 역시 함수형 프로그래밍의 대원칙을 위배하지 않도록 고도로 추상화된 인터페이스 위에서만 가능하다.

            +

            패러다임은 그 언어를 만들고, 사용하는 커뮤니티를 비롯한 생태계를 지배한다. 자바에도 함수형 프로그래밍 요소가 많이 도입됐지만, 여전히 자바 생태계의 지배적 패러다임은 객체지향 프로그래밍이다. 자바의 표준 라이브러리나 외부 패키지는 객체지향 프로그래밍의 원칙을 따르며, 자바를 사용하는 기업은 신입 프로그래머에게 객체지향 프로그래밍의 원칙을 교육한다. 지난 수십 년간 자바로 작성된 거대한 레거시 코드는 객체지향 프로그래밍 패러다임을 충실히 반영하고 있다. 결국 커뮤니티 구성원들이 따르는 패러다임과 그 패러다임 안에서 옳다고 여겨지는 제약에 따라 언어가 구별되는 것이다. 그래서 일상에서 어떤 언어를 함수형 프로그래밍 언어라고 지칭할 때는 단순히 그 언어가 일급 함수를 지원하는 경우가 아니라, 그 언어의 생태계가 함수형 프로그래밍 패러다임을 지향하는 경우가 대부분이다.

            +

            여러 프로그래밍 패러다임을 지원하는 언어를 다중 패러다임 언어라고 부르기도 한다. 가령 스칼라에는 절차지향 프로그래밍의 제약과 함수형 프로그래밍의 제약이 공존한다. 여러 패러다임의 장점만 모았다니 너무나 혹하지만, 거의 모든 현대적 프로그래밍 언어가 여러 종류의 프로그래밍 패러다임을 채택하고 있기 때문에 다중 패러다임 언어라는 용어 자체에는 큰 의미가 없다. 요즘에는 어떤 언어가 다중 패러다임 언어라고 말할 때는 둘 중 하나인 것 같다: 첫 번째는 언어가 함수형 프로그래밍을 잘 지원한다는 의미일 때, 두 번째는 함수형 프로그래밍 언어가 대중성을 어필할 때.

            +

            함수형 프로그래밍의 실질적 이득

            +

            함수형 프로그래밍을 진지하게 생각하지 않더라도, 적어도 모나드가 무엇인지 궁금해하는 프로그래머는 상당히 많다. 그것을 이해하는 순간, 마치 '고급 프로그래머’로 거듭날 수 있다는 신화가 있다. 그만큼 모나드에 대해 설명하는 글도 엄청나게 많다. 하지만 무슨 의미가 있을까? 함수형 프로그래밍 패러다임에서 모나드는 너무나도 사랑스러운 개념이지만, 함수형 프로그래밍보다 모나드를 먼저 접한 프로그래머에게 모나드는 공포스러운 개념에 가깝다. 심지어 모나드가 엔도펑터 카테고리에 속한 모노이드에 불과하다[3]는 사실을 이해한 뒤에도 다른 프로그래머에게 함수형 프로그래밍의 현실적 이익을 설득하기는 쉽지 않다. 함수형 프로그래밍을 기웃거리는 프로그래머에게 모나드의 유용함을 설득하기 위해 상자에 값을 넣고 빼는 등 '초등학생도 이해할 수 있는 수준’의 예시를 들며 펑터, 모노이드, 모나드를 차례로 설명하는 것은 좋은 접근 방법이 아닐 수도 있다.

            +

            이때 독자에게는 프로그래밍 입문자가 객체지향 프로그래밍을 공부할 때 겪는 혼란과 완전히 같은 문제가 일어난다. 대규모 애플리케이션을 작성한 경험이 없는 프로그래머에게는 클래스니, 캡슐화니, SOLID니 하는 개념들이 모두 불필요해 보인다. 심지어 그 개념을 이해한 뒤에도 "그래서 뭐?"라는 질문에 대한 답을 얻지는 못한다. 함수형 프로그래밍의 수학적으로 엄밀한 개념은 정말 멋지고 중요하지만, 객체지향 프로그래밍의 방대한 원칙과 복잡성에 매몰되어 스스로를 자책하고 있는 프로그래머에게 그런 것은 크게 매력적으로 다가오지 않는다. 객체지향 프로그래밍 패러다임이 지배하는 생태계에서, 함수형 프로그래밍 패러다임을 통해 자연스레 달성하게 되는 그것은 프로그래머가 로버트 마틴(Robert Martin)의 클린코드[4] 원칙을 충실히 따름으로써 달성할 수 있는 것이기도 하기 때문이다. 각종 전술적인 개발 원칙과 디자인 패턴을 일일히 외우지 않아도 된다는 그 말은, 이미 객체지향 프로그래밍 패러다임에 따라 사고하는 프로그래머와 그러한 패러다임으로 점철된 코드베이스 위에서 그다지 설득력을 얻지 못한다.

            +

            패러다임의 변화를 마주한 프로그래머의 입장에서, 새로운 패러다임이 현재의 문제에 직접적인 해결책을 제시하지 않는 이상 그 패러다임을 받아들일 이유는 별로 없다. 그러나 결국 둘 중 하나다. 기존 패러다임에서 온갖 문제를 직접 경험하다가 환멸을 느껴 새로운 패러다임을 받아들이거나, 자신이 속한 생태계의 지배 패러다임이 변화하여 어쩔 수 없이 새로운 패러다임을 받아들여야 하는 것이다. 그 많은 모나드 튜토리얼과 "함수형 프로그래밍이란?"으로 시작하는 인터넷 글들, 함수형 프로그래밍을 지향하는 프로그래머들은 패러다임의 변화와 대중화를 위해 갖은 노력을 해왔다. 이제 함수형 프로그래밍은 대다수의 프로그래머가 어쩔 수 없이 받아들여야 하는 패러다임이 되었다. 어쩌면 높은 점유율을 가진 주류 언어의 설계에 함수형 프로그래밍 패러다임이 반영된 시점에 이미 그랬을지도 모르겠다. 쿤의 과학적 패러다임과 달리 프로그래밍 패러다임의 변화는 기존 패러다임에 대해 한편으로는 파괴적이기도, 한편으로는 상호보완적이기도 하다. 함수형 프로그래밍 패러다임은 객체지향 프로그래밍 패러다임이 쌓아 올린 성과를 완전히 폐허로 만들지 않는다. "은총알은 없다"[5]라는 오래된 소프트웨어 공학 격언은 다행스럽게도(?) 파괴적 혁명이 없을 것임을 예견한다.

            +

            내가 함수형 프로그래밍에 가장 큰 매력을 느낀 첫 번째 계기는 프로덕션 환경에서 아무 곳에서나 터지는 예외로 인해 큰 서비스 장애를 겪은 뒤였다. 프로그래밍 언어 차원에서 함수를 호출하는 쪽에서 해당 함수가 던지는 예외를 처리하도록 강제할 방법이 전혀 없었다. 프로그래머의 실수로 예외 처리를 하지 않으면 우리의 코드 베이스가 던진 예외로 인해서든, 외부 패키지가 던진 예외로 인해서든 API가 언제든 예상치 못하게 실패할 수 있었다. 그런 상황에서 러스트의 Result 타입은 대단히 매력적이었다. 하스켈이나 ML, 스칼라 등 다른 언어에서 Either라고 부르는 대수적 데이터 타입으로부터 영향을 받은 이 타입은 함수가 정상적으로 값을 반환할 수 있을 때는 Ok 타입에 값을 담아 반환하고, 그렇지 않을 때는 Err 타입에 에러를 담아 반환하여 에러 핸들링을 자연스럽게 강제하는 효과가 있다.

            +
            enum Result<T, E> {
            +    Ok(T),
            +    Err(E),
            +}
            +
            +
            fn divide(a: i32, b: i32) -> Result<i32, String> {
            +    if b != 0 {
            +        Ok(a / b)
            +    } else {
            +        Err("Cannot divide by zero".to_string())
            +    }
            +}
            +
            +fn main() {
            +    println!("{:?}", divide(8, 2)); // Ok(4)
            +    println!("{:?}", divide(8, 0)); // Err(...)
            +}
            +
            +

            비슷하게 Option 타입이 있다. 토니 호어(Tony Hoare)가 null을 고안한 과거에 대해 십억 달러짜리 실수[6]라고 회고했듯이, null은 오랜 시간 프로그래머를 고통에 빠뜨렸다. 다른 함수형 프로그래밍 언어에서 보통 Maybe라고 불리는 이 타입을 사용하면 코드에서 null을 완전히 없애는 것이 가능하다. 값이 존재하는 경우에는 Some 타입에 값을 담아 표현하고, 값이 존재하지 않는 경우에는 None 타입으로 표현하면 된다.

            +
            enum Option<T> {
            +    Some(T),
            +    None,
            +}
            +
            +
            fn divide(a: i32, b: i32) -> Option<i32> {
            +    if b != 0 {
            +        Some(a / b)
            +    } else {
            +        None
            +    }
            +}
            +
            +fn main() {
            +    println!("{:?}", divide(8, 2)); // Some(4)
            +    println!("{:?}", divide(8, 0)); // None
            +}
            +
            +

            두 번째는 클린코드를 읽은 뒤였다. 클린코드가 406 페이지에 걸쳐 제시하는 갖가지 원칙 중 많은 것들을 함수형 프로그래밍 패러다임의 대원칙을 따르면 자연스럽게 달성할 수 있거나, 아예 신경 쓰지 않아도 된다. 코드를 작성할 때 굳이 클린코드 원칙을 떠올리지 않아도, 함수형 프로그래밍 언어의 견고한 타입 시스템이 클린코드를 강제하기 때문이다. 같은 맥락에서, 쌓여 있는 풀 리퀘스트를 리뷰할 때도 함수형 프로그래밍 패러다임이 절실했다. 부수 효과를 일으키지 않는 순수 함수만으로 구성된 코드는 참조 투명하다. 참조 투명한 코드는 실행 흐름을 추적하기 쉽고, 읽기도 쉽다. 이때 '참조 투명하다’라는 말은 프로그램을 변경하지 않고 표현식을 값으로 치환해도 그 의미가 같다는 뜻이다. 아래와 같이 두 변수를 더하는 함수 add를 보자.

            +
            add a b = a + b
            +main = print (add 1 2) -- 3
            +
            +

            아래와 같이 (add 1 2)를 그 값인 3으로 치환해도 프로그램의 의미가 변하지 않으므로 함수 add는 참조 투명하다. add는 외부의 영향을 받지 않고 같은 인자에 대해 항상 같은 결과를 반환하며, 덕분에 프로그래머는 쉽게 함수의 결과를 예측할 수 있다.

            +
            main = print 3 -- 3
            +
            +

            어떻게 보면 함수형 프로그래밍의 핵심에는 그다지 특별한 것이 없어 보인다. 그저 함수를 아주 평범하게 다룰 수 있어야 한다는 대원칙이 있을 뿐이다. 하지만 그 간단한 원칙이 만들어 내는 효과는 거대하다. 함수형 프로그래밍 패러다임을 이해하는 데 도움이 된 자료 몇 개를 추천하며 글을 마친다.

            +
              +
            • 폴 키우사노, 루나르 비아르드나손, “스칼라로 배우는 함수형 프로그래밍”, 류광 역, 제이펍, 2015: 함수형 프로그래밍의 개념과 적용을 차근차근 익힐 수 있는 모범적인 교재. 스칼라가 생소하더라도 크게 방해되지는 않는다. 코틀린에 익숙하다면 조재용, 우명인, “코틀린으로 배우는 함수형 프로그래밍”, 인사이트, 2019도 좋다. 내용이 간결하고, 연습 문제를 풀지 않아도 다음 내용을 이해하는 데 지장이 없다는 점에서 조금 더 편하게 읽을 수 있다.
            • +
            • 홍재민, 류석영, “Introduction to Programming Languages”: 카이스트 ‘프로그래밍 언어’ 과목의 교과서. 2021년 처음 공개된 이후로 꾸준히 개정되고 있다. 프로그래밍 언어론을 다루는 책이지만, 함수형 프로그래밍 패러다임을 이해하는 데 큰 도움을 준다. 프로그래밍 언어에 대한 관점과 사고방식을 완전히 뒤바꿀 수 있고, 끝까지 읽은 뒤에는 코드 한 줄 한 줄이 완결된 수식으로 보이는 놀라운 경험을 할 수 있다.
            • +
            • Brent Yorgey, “CIS194”, 2013: 펜실베니아 대학교 ‘Introduction to Haskell’ 과목의 강의 자료. 하스켈 공식 홈페이지에서는 2013년 봄 학기 자료를 추천하는데, 학기마다 내용이 조금씩 다르다. (가장 최근 자료는 2016년 가을 학기) 하스켈은 개념이 생소할 뿐이지 문법은 정말 단순하고 직관적이라 배우기 어렵지 않다. 함수형 프로그래밍을 공부하기 위해 굳이 하스켈을 알아야 하나 싶기도 하지만, 외부 세계에 영향을 받을 수밖에 없는 소프트웨어가 어떻게 그 '순수함’을 지킬 수 있는지 배우는 데는 하스켈이 좋은 교재가 된다고 생각한다.
            • +
            +
            +
            +
              +
            1. John Hughes, “Why Functional Programming Matters”, 1990. (한국어) ↩︎

              +
            2. +
            3. Edgar Dijkstra, “Go To Statement Considered Harmful”, 1968. ↩︎

              +
            4. +
            5. 제임스 아이리(James Iry)가 'A Brief, Incomplete, and Mostly Wrong History of Programming Languages’에서 언급. 종종 모나드의 난해함을 상징하는 농담처럼 인용된다. ↩︎

              +
            6. +
            7. 로버트 C. 마틴, “클린 코드”, 이해영, 박재호 역, 인사이트, 2013. ↩︎

              +
            8. +
            9. Frederick Brooks, “No Silver Bullet - Essence and Accidents of Software Engineering”, 1986. ↩︎

              +
            10. +
            11. Tony Hoare, “Null References: The Billion Dollar Mistake”, 2009. ↩︎

              +
            12. +
            +
            + +
            +
            + +
            + +
            +
            +

            아치 리눅스로 15년차 넷북 되살리기

            +

            Eee PC 1000HE 위에 올린 아치 리눅스 32

            +
            +
            +
            + + +
            + +
            +
            +

            철도 시간표가 유닉스 시간이 되기까지

            +

            시간과 컴퓨터 공학

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/41.html b/article/41.html new file mode 100644 index 0000000..74431d9 --- /dev/null +++ b/article/41.html @@ -0,0 +1,425 @@ + + + + + + 아치 리눅스로 15년차 넷북 되살리기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 아치 리눅스로 15년차 넷북 되살리기 +

            + +

            + Eee PC 1000HE 위에 올린 아치 리눅스 32 +

            + + + + +
            +
            Table of Contents +

            +
            +

            우리집에는 2009년 구입한 넷북이 있다.

            +

            +

            Asus Eee PC 1000HE에는 인텔 아톰 N280과 DDR2 1GB 램이 탑재되어 있다. 아톰 N 시리즈는 인텔이 넷북을 위해 만든 저가, 저성능 CPU다. L1 캐시 56KB, L2 캐시 512KB가 제공되며, 클럭 속도는 1.667GHz다. 2023년 출시된 저가형 노트북용 프로세서 인텔 코어 i3 N305에는 L1 캐시 768KB, L2 캐시 4MB가 제공되고, 속도가 3.8GHz라는 점을 생각해보면 얼마나 열악한 성능인지 체감할 수 있다.

            +

            넷북이라는 정체성을 고려해 타협된 스펙이니 "성능이 이렇게나 안 좋다"며 따지는 것이 큰 의미는 없을 것이다. 당시에도 노트북과 비교해 좋은 성능은 아니었지만, 문서 편집이나 웹 서핑 등 가벼운 작업을 하는 데는 무리가 없었다. 그러나 2012년쯤부터는 일상적인 작업이 어려울 정도가 되었다. 프로그램에는 더 많은 기능이 추가되고 있었고, 웹 사이트는 더욱 인터랙티브해지고 있었으며, 넷북의 작은 HDD는 노후화되고 있었다. 결국 넷북의 자리는 울트라북이 대체했다.

            +

            그렇게 우리집 넷북은 창고에 잠들었다.

            +

            새 운영체제

            +

            그러다 10여 년이 흐른 2023년, 갑자기 그 넷북이 떠올랐다. 창고에서 꺼낸 넷북은 들어갈 때 모습 그대로였고, 부팅 후 나타난 촌스러운 윈도우XP UI도 그때 그 모습이었다. 탐색기를 여는 데 수 초가 걸렸고, 탐색기가 열리는 동안 커서가 버벅거렸다. 성능이 더 나빠진 것인지, 원래 이랬는데 그때는 이 정도도 빠르다고 느꼈는지 잘 기억나지 않았다. 넷북이 다시 창고를 벗어난 이상, 이 넷북을 되살려서 서버가 되든, 유튜브 머신이 되든 다시 사용해보겠다고 마음 먹었다.

            +

            우선 2014년에 지원이 종료된 윈도우XP를 대체할 필요가 있다고 생각했다. 윈도우7 이후의 윈도우 운영체제들은 최소 1GB 메인 메모리를 요구했다. 넷북에 장착된 메인 메모리가 딱 1GB 램이었기 때문에 보다 가벼운 운영체제를 설치해야 쾌적한 사용이 가능하겠다고 생각했다. 우분투는 최소 512MB 메인 메모리를 요구했는데, 과거 저성능 노트북에 우분투를 설치했을 때 그다지 좋은 성능을 체감하지 못했다.

            +

            나에게는 최소한의 기능만을 갖춘 아주 가벼운 운영체제가 필요했다. 불필요한 기본 기능이 포함되어 있지 않고, 밑바닥부터 나의 넷북만을 위한 환경을 구축할 수 있어야 했다. 답은 정해져 있었다. 아치 리눅스였다. 아치 리눅스는 단순성, 현대성, 실용성, 사용자 중심, 범용성을 원칙으로 하는 리눅스 배포판이다. 안 그래도 예전부터 아치 리눅스를 사용해보고 싶었는데 좋은 기회라고 생각했다. 설정 과정에서 운영체제에 대해 많은 공부를 하게 된다니 개강 직전에 시작하기 좋은 취미이기도 했다.

            +

            설치 전

            +

            아치 리눅스는 2017년 이후로 x86 아키텍처에 대한 공식적인 지원을 종료했다. 아톰 N2xx 프로세서는 32비트만을 지원하므로 나는 선택의 여지 없이 커뮤니티 주도로 유지보수되고 있는 아치 리눅스 32를 사용해야만 했다. 아치 리눅스 32의 설치 방법은 아치 위키에 설명된 아치 리눅스 설치 가이드와 크게 다르지 않았지만, 제한적인 성능으로 인해 예상치 못한 우여곡절이 있었다. 이 글에서는 넷북에 아치 리눅스 32를 설치한 개인적인 경험을 상세히 기록하고자 한다.

            +

            부팅 매체 준비

            +

            먼저 아치 리눅스 디스크 이미지가 필요하다. 아치 리눅스 32 다운로드 페이지에서 이미지 파일(.iso)과 시그니처 파일(.sig)을 받고, gpg 명령으로 위변조된 이미지인지 검증한다.

            +
            $ gpg --keyserver-options auto-key-retrieve --verify archlinux32-2023.03.02-i686.iso.sig
            +
            +

            문제가 없다면 부팅 매체를 만든다. 나는 윈도우 데스크탑에서 Rufus를 이용하여 앞서 받은 ISO 파일을 USB에 구웠다. 그리고 넷북에 USB를 꽂은 뒤 BIOS(Basic Input/Output System)에 진입해 USB를 1순위 부팅 옵션으로 설정하고 재부팅했다. 이렇게 하면 컴퓨터가 하드디스크 대신 USB로 부팅하게 된다.

            +

            인터넷 연결

            +

            아치 리눅스 설치 이미지는 zsh을 기본 셸로 제공한다. 아치 리눅스를 설치하려면 인터넷에 연결되어 있어야 한다. 먼저 네트워크 인터페이스가 활성화되어 있는지 확인한다.

            +
            $ ip link
            +1: lo ... state UNKNOWN ...
            +   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
            +2: enp3s0 ... state DOWN ...
            +   link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
            +4: wlan0 ... state UP ...
            +   link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
            +
            +

            여기서 lo는 루프백 인터페이스다. 루프백 인터페이스는 시스템이 자기 스스로에게 연결하기 위해 사용하는 가상의 네트워크 인터페이스다. 아이피 127.0.0.1은 이 인터페이스에 할당된 루프백 아이피다. enp3s0는 메인보드의 3번 PCI 버스(p3), 0번 슬롯(s0)을 사용하는 이더넷 디바이스(en)에 대응되는 인터페이스다. 이더넷은 유선 네트워크를 위한 규격이다. wlan0는 무선랜 인터페이스다. 와이파이에 연결할 것이니 wlan0stateUP임을 확인했다. 이어서 rfkill로 차단된 인터페이스가 있는지 확인한다.

            +
            $ rfkill
            +ID    TYPE         DEVICE             SOFT         HARD
            +0     wlan         eeepc-wlan         unblocked    unblocked
            +1     bluetooth    eeepc-bluetooth    unblocked    unblocked
            +2     wwan         eeepc-wwan3g       unblocked    unblocked
            +...
            +
            +

            유선 연결이 가능하다면 바로 LAN 케이블을 꽂으면 되지만, 와이파이에 연결하려면 iwctl을 이용해 직접 액세스 포인트를 검색하고 연결해야 한다. iwctl은 인텔이 만든 무선 네트워크 데몬 iwd(iNet wireless daeom)가 제공하는 클라이언트 프로그램이다. 아치 리눅스 설치 이미지는 iwd를 기본으로 제공한다.

            +
            $ iwctl
            +
            +[iwctl]# device list
            +                          Devices
            +-----------------------------------------------------------
            +Name     Address              Powered    Adapter    Mode
            +-----------------------------------------------------------
            +wlan0    xx:xx:xx:xx:xx:xx    on         phy0       station
            +
            +[iwctl]# station wlan0 scan
            +
            +[iwctl]# station wlan0 get-networks
            +        Available networks
            +----------------------------------
            +Network name    Security    Signal
            +----------------------------------
            +iptime          psk         ****
            +iptime_2.4G     psk         ****
            +
            +[iwctl]# station wlan0 connect iptime_2.4G
            +Type the network passphrase for iptime_2.4G psk.
            +Passphrase: ********
            +
            +[iwctl]# exit
            +
            +

            넷북의 랜카드가 5GHz 네트워크를 지원하지 않아 2.4GHz 네트워크에 연결할 수 밖에 없었다. 마지막으로 archlinux32.org에 핑을 보내 인터넷에 연결이 잘 됐는지 확인한다.

            +
            $ ping -c3 archlinux32.org
            +
            +

            시스템 시계 설정

            +

            시스템 시계를 업데이트한다. 먼저 NTP(Network Time Protocol)를 이용하도록 설정하고, 시계가 정확한지 확인한다. NTP는 서버로부터 정확한 시간을 받아 시스템 시계와 동기화하기 위한 프로토콜이다. 철도 시간표가 유닉스 시간이 되기까지에서 간략히 설명한다.

            +
            $ timedatectl set-ntp true
            +$ timedatectl
            +
            +

            디스크 파티셔닝

            +

            fdisk를 이용해 컴퓨터에 어떤 디스크가 장착되어 있는지 확인할 수 있다.

            +
            $ fdisk -l
            +Disk /dev/sda: 14.8 Gib, x bytes, x sectors
            +Disk model: Flash Disk
            +...
            +Disk /dev/sdb: 149.0 GiB, x bytes, x sectors
            +Disk model: ST9160410AS
            +...
            +
            +

            sda는 설치 USB이고, sdb가 넷북에 장착된 HDD이므로 sdb를 선택한다.

            +
            $ fdisk /dev/sdb
            +
            +

            기존 파티션은 윈도우에 맞게 하나의 HDD를 C 드라이브, D 드라이브로 나눠 사용하고 있었고, 추가로 EFI 파티션과 복구 파티션이 지정되어 있었다. 나는 더 이상 필요하지 않은 기존 파티션을 모두 지우고, 완전히 새롭게 구성하고자 했다. 그러려면 우선 시스템의 메인보드에 대해 알아야 했다.

            +

            메인보드의 ROM에는 시스템에 전원이 공급되면 가장 먼저 실행되어 부팅에 필요한 하드웨어를 초기화하는 펌웨어가 설치되어 있다. 펌웨어의 종류에는 BIOS와 EFI(Extensible Firmware Interface), UEFI(Unified Extensible Firmware Interface)로 크게 세 가지가 있다. EFI는 BIOS를 개선한 펌웨어고, UEFI는 EFI를 개선한 펌웨어이기 때문에 최근에는 UEFI가 제공되는 것이 일반적이지만, 구형 메인보드는 보통 BIOS를 제공했다. 그런데 Eee PC 1000HE의 펌웨어는 분명 BIOS인데도 불구하고 EFI 파티션이 만들어져 있었다. 나중에 알고보니 EFI 파티션은 펌웨어가 EFI이기 때문에 존재하는 것이 아니라, BIOS의 “Boot Booster” 옵션을 지원하기 위해 만들어진 것이었다[1]. 컴퓨터를 켜면 BIOS가 하드웨어를 초기화하기 전에 시스템의 각종 장치를 진단하는 POST(Power-On Self Test) 과정을 거치는데, 이로 인해 부팅에 수 초를 소요하게 된다. "Boot Booster"는 EFI 파티션에 POST 정보를 캐시함으로써 부팅 지연 시간을 줄여준다.

            +

            +American Megatrends International BIOS의 POST 화면. (CC0)

            +

            파티셔닝 형식에는 MBR(Master Boot Record)과 GPT(GUID Partition Table)가 있다. MBR은 저장장치의 첫 512 바이트를 차지하여 440 바이트에는 부트스트랩 코드를, 6 바이트에는 디스크 서명을, 64 바이트에는 16 바이트씩 최대 4개 파티션에 대한 파티션 테이블을, 나머지 2 바이트에는 부트 서명을 저장한다. 한편 GPT는 MBR의 각종 제약을 개선한 파티셔닝 형식으로, UEFI 명세의 일부이기도 하다. GPT 형식을 사용하면 MBR에 비해 더 많은 주 파티션을 만들 수 있으며, 더 큰 파티션도 만들 수 있다. 또한 MBR과 달리 별도의 부트 파티션을 사용한다. 여기서 나는 두 가지 실수를 했는데, 하나는 BIOS에는 GPT를 사용할 방법이 없다는 착각[2]으로 MBR을 선택한 것이고, 다른 하나는 EFI 파티션의 용도를 모른 채 아래와 같이 일반적인 MBR 형식으로 파티셔닝한 것이다. 조금 아쉽지만 굳이 다시 설정하지는 않았다.

            + + + + + + + + + + + + + + + + + + + + + + + +
            Mount pointPartitionPartition type IDBoot flag
            [SWAP]/dev/sdb182 (Linux swap)No
            //dev/sdb283 (Linux)Yes
            +

            스왑 파티션은 메인 메모리 용량이 부족한 경우 주 기억장치 공간의 일부를 메모리처럼 사용하기 위해 필요하다. 아치 위키는 스왑 파티션에 최소 512MB 이상 할당할 것을 권장하고 있다. 하지만 이 정도로는 부족하다. 메인 메모리가 1GB인 이 넷북에서 스왑 파티션까지 작게 잡으면 시도때도 없이 “No space left on device” 에러를 마주하게 될 것이다. RHEL은 메인 메모리가 2GB 이하인 경우에는 메인 메모리 용량의 2배를 스왑 파티션에 할당할 것을 권고한다. 따라서 1GB 메인 메모리를 가진 넷북에서는 스왑 파티션을 2GB로 잡으면 된다. 다만 소스를 직접 빌드할 때 4GB 이상의 메모리를 요구하는 경우가 있기 때문에 넉넉히 4GB로 잡았다. 스왑 파티션의 크기를 메인 메모리보다 크게 할당하는 것은 하이버네이션(Hibernation)을 생각해서라도 좋은 선택이다.

            +
            Command (m for help): n
            +
            +Command action
            +e    extended
            +p    primary partition (1-4)
            +p
            +
            +Partition number (1-4): 1
            +
            +First sector (2048-y, default 2048): <enter>
            +Using default value 2048
            +
            +Last sector, +sectors or +size(K,M,G) (2048-y, default y): +4G
            +
            +

            방금 만든 스왑 파티션의 타입을 82(Linux swap)으로 변경한다.

            +
            Command (m for help): t
            +Partition number (1-4): 1
            +Hex code: 82
            +Changed system type of partition 2 to 82
            +
            +

            이어서 루트 파티션을 만든다. 루트 파티션은 아치 리눅스가 설치될 파티션이다.

            +
            Command (m for help): n
            +
            +Command action
            +e    extended
            +p    primary partition (1-4)
            +p
            +
            +Partition number (1-4): 2
            +
            +First sector (x-y, default x): <return>
            +Using default value x
            +
            +Last sector, +sectors or +size(K,M,G) (x-y, default y): <return>
            +Using default value y
            +
            +

            마지막으로 결과를 확인해본다.

            +
            Command (m for help): p
            +Disk /dev/sdb: 149.0 GiB, x bytes, x sectors
            +Disk model: ST9160410AS
            +...
            +Device       Boot    Start    End    Sectors    Size    Id    Type
            +/dev/sdb1            2048     y      z          4G      82    Linux swap / Solaris
            +/dev/sdb2            x        y      z          145G    83    Linux
            +
            +

            후술하겠지만, GRUB 부트로더를 사용할 생각으로 부트 플래그는 지정하지 않았다. (GRUB은 부트 플래그를 무시한다.) 이제 저장(w)한 뒤 fdisk를 종료(q)한다.

            +
            Command (m for help): w
            +Command (m for help): q
            +
            +

            루트 파티션의 파일 시스템을 설정한다. 리눅스 파일 시스템으로는 일반적으로 ext4를 사용하므로 sdb2를 ext4로 포맷한다.

            +
            $ mkfs.ext4 /dev/sdb2
            +
            +

            mkswap으로 앞서 만든 스왑 영역을 스왑 파티션으로 초기화하고, 활성화한다.

            +
            $ mkswap /dev/sdb1
            +$ swapon /dev/sdb1
            +
            +

            설치

            +

            본격적으로 설치를 시작한다. 아치 리눅스를 설치할 루트 파티션에 접근하기 위해 루트 파티션을 /mnt 디렉토리에 마운트한다.

            +
            $ mount /dev/sdb2 /mnt
            +
            +

            이제 필수 패키지를 설치한다. pacstrap은 마운트된 루트 디렉토리에 패키지를 설치하는 명령이다. 이를 이용해 /mnt 디렉토리에 리눅스 커널과 모듈, 펌웨어 파일을 받는다. 하지만 바로 패키지를 설치하면 서명을 신뢰할 수 없다는 에러가 발생한다. 먼저 아치 리눅스의 패키지 매니저 pacman으로 아치 리눅스 32의 PGP 키링 archlinux32-keyring을 설치해야 한다.

            +
            $ pacman -S archlinux32-keyring
            +$ pacstrap -K /mnt base linux linux-firmware
            +
            +

            설치가 끝나면 fstab 파일을 생성한다. fstab 파일은 디스크 파티션 등 파일 시스템에 대한 정보를 담고 있으며, 시스템이 부팅될 때 설정에 따라 파일 시스템을 자동으로 마운트해준다. 아치 리눅스 설치 이미지가 제공하는 genfstab는 자동으로 fstab 내용을 생성해준다.

            +
            $ genfstab -U /mnt >> /mnt/etc/fstab
            +
            +

            루트를 /mnt 디렉토리로 변경한다.

            +
            $ arch-chroot /mnt
            +
            +

            지금부터는 /mnt 디렉토리가 루트 디렉토리인 상태, 즉, 아치 리눅스 시스템 안에 들어와 있다고 생각해야 한다.

            +

            시스템 설정

            +

            우선 /usr/share/zoneinfo 디렉토리 아래에 있는 시간대 파일을 /etc/localtime으로 링크해 로컬 시간대를 지정해준다. 대한민국 표준시를 사용하기 위해 ROK를 선택했다.

            +
            $ ln -sf /usr/share/zoneinfo/ROK /etc/localtime
            +
            +

            hwclock을 이용해 시스템 시계로 하드웨어 시계를 설정한다. /etc/adjtime의 타임스탬프가 업데이트된다.

            +
            $ hwclock --systohc
            +
            +

            로케일 파일을 생성한다.

            +
            $ locale-gen
            +
            +

            호스트네임을 설정한다.

            +
            $ echo eee-pc-1000he > /etc/hostname
            +
            +

            루트 계정의 패스워드를 설정한다.

            +
            $ passwd
            +
            +

            이로써 기본적인 설정을 마쳤다.

            +

            부트로더 설정

            +

            컴퓨터가 켜지면 BIOS가 POST를 수행하고, 부팅에 필요한 하드웨어를 초기화한다. 곧이어 BIOS에서 1순위 부팅 옵션으로 설정된 디스크의 첫 440 바이트을 실행하는데, 이 영역에 있는 MBR 부트스트랩 코드가 부트로더를 실행한다. 그리고 부트로더는 커널을 로드하여 운영체제를 실행한다[3]. 다양한 부트로더가 있지만, 나는 익숙한 GRUB을 선택했다. GRUB은 BIOS, MBR, ext4를 모두 지원하기 때문에 현재 환경에 사용하기에도 적합했다.

            +

            일반적인 패키지를 설치하듯 pacman으로 GRUB을 설치할 수 있다. 다만 바로 설치를 시도하면 서명을 신뢰할 수 없다는 에러가 발생하니 앞서 한 것과 같이 archlinux32-keyring을 먼저 받는다.

            +
            $ pacman -S archlinux32-keyring
            +$ pacman -S grub
            +
            +

            부팅 디스크를 지정해 GRUB을 설치한다. 인자로 넘긴 /dev/sdb가 파티션이 아닌 디스크임에 주의한다.

            +
            $ grub-install --target=i386-pc /dev/sdb
            +
            +

            GRUB 설정 파일을 생성한다.

            +
            $ grub-mkconfig -o /boot/grub/grub.cfg
            +
            +

            이제 시스템이 부팅될 때 /boot/grub 디렉토리에 설치된 GRUB이 /boot 디렉토리 아래에 있는 아치 리눅스 커널 vmlinuz-linux를 로드할 것이다. 이 시점에 아치 리눅스 설치가 끝났다고 할 수 있지만, 여기서 바로 재부팅하기 전에 네트워크 설정을 마쳐야 한다.

            +

            네트워크 설정

            +

            지금 네트워크 설정을 하지 않으면 아치 리눅스를 설치한 뒤 인터넷에 연결하지 못해 완전히 고립되는 상황에 놓일 수 있다. 나는 아치 리눅스 설치 이미지에서 사용하는 단순한 구성인 systemd-networkd + systemd-resolved + iwd 조합을 그대로 따르고자 했다. systemd-networkd는 네트워크 설정을 관리하는 시스템 데몬으로, 유선 네트워크 연결에 필요한 파일이 포함되어 있다. 먼저 유선 어댑터 설정을 작성한다.

            +
            $ cat <<EOF > /etc/systemd/network/20-wired.network
            +[Match]
            +Name=enp3s0
            +
            +[Network]
            +DHCP=yes
            +EOF
            +
            +

            무선 어댑터 설정도 작성한다. IgnoreCarrierLoss 옵션은 시스템이 다른 액세스 포인트에 로밍(Roaming)하는 중에 systemd-networkd가 현재 인터페이스 설정을 잠시 유지하도록 한다. 로밍 도중 짧은 시간 동안 손실을 무시함으로써 다운타임을 줄일 수 있다.

            +
            $ cat <<EOF > /etc/systemd/network/25-wireless.network
            +[Match]
            +Name=wlan0
            +
            +[Network]
            +DHCP=yes
            +IgnoreCarrierLoss=3s
            +EOF
            +
            +

            systemd-resolved는 DNS를 위한 서비스를 제공한다. systemd-resolved 설정은 stub-resolv.conf 설정을 링크해 사용한다.

            +
            $ ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
            +
            +

            따로 설치해야 하는 패키지는 무선 네트워크에 연결하기 위한 iwd뿐이다.

            +
            $ pacman -S iwd
            +
            +

            iwd 설정 파일도 작성한다. EnableNetworkConfiguration 옵션을 true로 하면 iwd가 네트워크 인터페이스를 IP 주소로 설정하도록 한다. 동적 주소를 사용하는 경우 빌트인된 DHCP 클라이언트로 IP를 할당 받는다. DHCP의 동작 과정은 인터넷이 동작하는 아주 구체적인 원리에서 설명한다. NameResolvingService 옵션은 DNS 결정 방식을 설정한다. systemd는 기본 값이기도 하다.

            +
            $ cat <<EOF > /etc/iwd/main.conf
            +[General]
            +EnableNetworkConfiguration=true
            +
            +[Network]
            +NameResolvingService=systemd
            +EOF
            +
            +

            hosts 파일을 작성한다. 이 파일은 DNS가 도메인으로 아이피 주소를 얻을 때 가장 먼저 참조하는 파일이다.

            +
            $ cat <<EOF > /etc/hosts
            +127.0.0.1   localhost
            +::1         localhost
            +127.0.1.1   eee-pc-1000he
            +EOF
            +
            +

            시스템이 부팅되면 자동으로 네트워크 서비스들이 실행되도록 등록한다.

            +
            $ systemctl enable iwd systemd-networkd systemd-resolved
            +
            +

            /mnt로 설정된 루트를 빠져나온다.

            +
            $ exit
            +
            +

            설치 후

            +

            드디어 설치가 끝났다. /mnt 디렉토리에 마운팅된 파티션을 언마운트한다.

            +
            $ umount -R /mnt
            +
            +

            시스템을 종료하고 재부팅한다. 이때 BIOS에 진입해 1순위 부팅 옵션을 USB가 아닌 하드디스크로 지정한 뒤, USB를 제거한다. 이제 컴퓨터가 하드디스크에 올린 시스템으로 부팅되며, GRUB에서 아치 리눅스 32를 선택하면 운영체제가 실행된다. 이후 루트 계정으로 로그인한 뒤 각종 필요한 패키지를 설치했다.

            +
            $ pacman -S coreutils sudo which git wget openssh neovim tmux fish
            +
            +

            또한 아치 리눅스의 General recommendations 문서를 참고해 각종 권장 사항을 반영했다.

            +

            Arch User Repository

            +

            AUR(Arch User Repository)은 아치 리눅스의 공식 패키지 저장소 외에 커뮤니티 주도로 운영되는 저장소다. 필요한 패키지가 공식 저장소가 아닌 AUR에 올라와 있는 경우가 종종 있다. AUR에서 직접 패키지를 받으려면 번거로운 절차를 거쳐야 하기 때문에 AUR에 대한 다양한 작업을 자동화하고, 사용자 친화적인 인터페이스를 제공하는 헬퍼를 사용하는 것이 여러모로 건강에 좋다.

            +

            Pacman 래퍼인 yay 또는 paru를 사용하면 pacman을 사용하듯 쉽게 AUR로부터 패키지를 설치할 수 있다. 어떤 것을 사용해도 되지만 둘 다 x86 바이너리를 제공하지 않으므로 직접 빌드해야 한다.

            +
            $ sudo pacman -S --needed base-devel
            +$ git clone https://aur.archlinux.org/paru.git
            +$ cd paru
            +$ makepkg -si
            +
            +

            4GB를 스왑 파티션에 할당했음에도 빌드 도중에 “No space left on device” 에러가 발생했다. 무시하고 다시 빌드를 시작하면 직전에 캐시한 빌드 데이터를 참조하여 빌드를 재개하기 때문에 성공적으로 빌드를 마칠 수 있다.

            +

            데스크탑 환경

            +

            아치 리눅스에는 데스크탑 환경(Desktop Environment, DE)이 포함되어 있지 않기 때문에 직접 설정해야 한다. DE는 아이콘이나 툴바, 배경화면과 같은 GUI 요소와 윈도우 매니저(Window Manager)를 함께 제공하기 때문에 쉽게 GUI를 구축할 수 있게 해준다.

            +

            유명한 DE로 GNOME이 있지만, 넷북에는 더 가벼운 DE가 필요했다. 나는 가벼운 DE로 유명한 LXDE(Lightweight X11 Desktop Environment)의 뒤를 잇는 LXQt를 선택했다. LXQt는 X Window System(X11 또는 그냥 X라고 부른다.) 위에서 동작한다. X11은 GUI 환경을 위한 프레임워크를 제공하며, 서버-클라이언트 모델을 따르고 있다. 'X 서버’라고 부르는 디스플레이 서버는 입력 장치로부터 이벤트를 받아 'X 프로토콜’을 이용해 클라이언트에게 전달한다. 이때 클라이언트는 브라우저와 같은 유저 애플리케이션부터 DE, 윈도우 매니저, 툴킷(GTK, Qt) 등 서버 위에 놓인 레이어를 통틀어 의미한다. 입력 이벤트를 받은 클라이언트가 서버에게 GUI 요청을 보내면 서버는 요청된 내용에 따라 출력 장치에 화면을 구성한다[4].

            +

            +X11 서버-클라이언트 모델. (Wikimedia Commons, CC BY-SA 3.0)

            +

            지금은 X11의 문제를 개선하고 현대적인 커널에 맞게 설계된 Wayland가 그 자리를 대체하고 있는데, 나는 LXQt를 사용하기 위해 X11을 설치하기로 했다. xorg 패키지에는 X11 서버를 비롯한 다양한 유틸리티 패키지를 포함하고 있다. xorg-xinitstartx 명령으로 X11 서버를 수동으로 실행할 수 있도록 해주는 프로그램이다. 이와 함께 LXQt와 아이콘 세트도 설치한다.

            +
            $ pacman -S xorg xorg-xinit lxqt breeze-icons
            +
            +

            startx 명령은 기본적으로 홈 디렉토리의 .xinitrc 파일에 설정된 내용에 따라 X11 세션을 초기화한다. 만약 홈 디렉토리에 .xinitrc 파일이 없으면 /etc/X11/xinit/xinitrc 파일을 참조한다. 유저에 따라 설정을 다르게 하기 위해 기본 xinitrc 파일을 홈 디렉토리로 복사한다.

            +
            $ cp /etc/X11/xinit/xinitrc ~/.xinitrc
            +
            +

            기본 설정은 Twm, xorg-xclock, Xterm을 실행하도록 되어있다. 이 내용을 모두 지우고 exec startlxqt를 추가한다. 이어서, 로그인하면 자동으로 X11 세션을 시작하도록 .profile 파일에 스크립트를 작성한다.

            +
            if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then
            +  exec startx
            +fi
            +
            +

            파이어폭스를 설치하고 LXQt 환경에서 실행시켜봤다. 넷북이 버거워한다.

            +

            +

            넷북을 서버로 사용하기 위해 *-desktop 계정으로 로그인할 때만 LXQt를 실행하고, 그 외에는 CLI를 사용하도록 했다. 필요하다면 항상 DE를 실행하도록 할 수도 있고, 패키지를 설치해 LXQt의 추가적인 기능을 활성화할 수 있다.

            +

            램 업그레이드

            +

            Eee PC 1000HE의 메인 메모리는 DDR2 1GB SO-DIMM 램이다. DDR2는 2003년 표준화된 SDRAM 인터페이스이며, 이후 2007년 DDR3, 2014년 DDR4를 거쳐 2020년에는 DDR5까지 나왔다. DIMM(Dual In-line Memory Module)은 여러 개의 램을 하나의 PCB에 올린 램 막대를 의미한다. SO-DIMM(Small Outline DIMM)은 DIMM의 길이를 절반 정도로 줄여 작은 디바이스에 사용할 수 있도록 만든 규격이다.

            +

            1GB 램으로 소스를 빌드하면서 메모리 부족으로 인한 에러를 여러 번 만났기에 우선 램을 업그레이드할 필요가 있다고 생각했다. 그런데 넷북의 인텔 아톰 N280 프로세서가 지원하는 램 최대 용량은 2GB에 불과했다. 요즘도 DDR2 램을 팔긴 하는지 의심하며 6900원짜리 2GB 램을 주문했다. (배송비가 3000원이었다.)

            +

            +

            새 램으로 교체한 뒤 잘 인식하는지 확인해봤다.

            +
            $ free -h
            +         total ...
            +Mem:     1.9Gi ...
            +Swap:    4.0Gi ...
            +
            +

            큰 기대를 한 것은 아니지만, 병목은 HDD와 CPU였기 때문에 눈에 띄는 성능 개선을 체감하지는 못했다. 넷북을 완전히 분해해서 아예 하드웨어를 하나씩 교체해보는 것도 재밌겠다 싶었다.

            +
            +
            +
              +
            1. Jon Cain, “Boot Booster (EFI) Partition on an Asus EeePC 1005P”, 2014. ↩︎

              +
            2. +
            3. ArchWiki, “Partitioning: Tricking old BIOS into booting from GPT”. ↩︎

              +
            4. +
            5. ArchWiki, “Arch boot process: System initialization”. ↩︎

              +
            6. +
            7. Chris Tyler, “X Power Tools”, O’Reilly, 2007. ↩︎

              +
            8. +
            +
            + +
            +
            + +
            + +
            +
            +

            스마트폰을 PC의 모션 컨트롤러로 만들기

            +

            멀티 디바이스 앱을 위한 라이브러리, Zap

            +
            +
            +
            + + +
            + +
            +
            +

            함수형 프로그래밍의 설득력

            +

            자바와 하스켈의 차이는 어디에서 비롯되는가?

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/42.html b/article/42.html new file mode 100644 index 0000000..bbffc39 --- /dev/null +++ b/article/42.html @@ -0,0 +1,160 @@ + + + + + + 스마트폰을 PC의 모션 컨트롤러로 만들기 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 스마트폰을 PC의 모션 컨트롤러로 만들기 +

            + +

            + 멀티 디바이스 앱을 위한 라이브러리, Zap +

            + + + + +
            +
            Table of Contents +

            +
            +

            한 명의 사용자가 보유한 기기 수가 꾸준히 늘어남에 따라 이들을 유기적으로 결합하고자 하는 사용자 요구도 증가하고 있다. 나는 맥북과 2개의 스마트폰, 아이패드, 우분투 데스크탑을 일상적으로 사용하는데, 애플의 유니버설 컨트롤을 벗어나면 서로 다른 플랫폼의 기기를 연동하기가 쉽지 않다. 특히 모바일 기기에는 많은 센서가 탑재되어 있지만, PC를 사용할 때는 이를 전혀 사용하지 못한다. 모바일 기기의 각종 센서를 하나의 모바일 기기에만 남겨두기에는 너무 값지다. 만약 모바일 기기의 각종 센서를 다른 기기에서 사용할 수 있다면 보다 입체적인 사용자 경험을 만들 수 있을 것이다. 스마트폰을 PC의 모션 컨트롤러로 사용할 수 있다면 어떨까?

            +

            +

            위 비디오에서 스마트폰은 랩탑에서 구동되는 게임의 컨트롤러로서 동작한다. 스마트폰의 가속도 센서를 이용해 기기를 기울이면 자동차의 방향을 조작할 수 있고, 버튼을 눌러서 가속/감속할 수 있다. 이처럼 여러 기기가 밀접히 결합된 멀티 디바이스 애플리케이션을 개발자가 쉽게 만들 수 있으면 좋겠다는 생각에서 애플리케이션 프로그래밍 라이브러리 Zap을 만들었다.

            +

            Zap은 학교 캡스톤 프로젝트의 결과물이다. 요즘 캡스톤 프로젝트를 한다고 하면 보통 완결된 형태의 웹 서비스를 만드는데, 대부분 프론트엔드는 리액트로 작성하고, 백엔드는 자바로 작성한 스프링부트 서버를 AWS에 배포하곤 한다. 하지만 웹은 내가 지난 수년간 지겨울 정도로 해온 일이기도 했고, 창업으로 이어질 만한 획기적인 서비스 아이디어가 있던 것도 아니었기에 지금까지 공부해 온 것과 완전히 다른 주제를 선택해 보고 싶었다. 그런 점에서 Zap은 나에게 새로운 영역을 공부해볼 기회이기도 했다.

            +

            Zap 라이브러리

            +

            Zap의 기본적인 컨셉은 개발자로 하여금 모바일 기기의 데이터소스로부터 얻은 데이터를 원격 기기로 쉽게 스트리밍할 수 있도록 돕는 라이브러리다. 연구실에 들어온 뒤 첫 번째로 읽은 논문이 Tap: An App Framework for Dynamically Composable Mobile Systems였는데, 이 연구에서 소개하는 Tap 프레임워크는 모바일 기기간 데이터소스 공유를 목표로 한다. Tap이 레퍼런스하는 각종 멀티 디바이스 컴퓨팅 프레임워크 연구들을 보며 아쉬웠던 부분은 ‘즉시 사용 가능한’ 솔루션이 공개된 경우가 없다는 점이었다. 좋은 아이디어가 연구실과 문서에만 남아있는 것은 연구자 입장에서도, 개발자 입장에서도, 그리고 사용자 입장에서도 비극일 것이다. 따라서 Zap은 Tap을 개선, 확장하여 실제로 누구나 사용 가능한 솔루션을 제공하고자 했다. 이를 위해 크게 두 가지 요구사항을 상정했다.

            +

            첫 번째, 애플리케이션 레벨에서 한 기기의 리소스를 원격 기기와 쉽게 공유할 수 있도록 추상화된 인터페이스를 제공해야 한다. 다른 기기와 데이터를 공유하기 위해서는 애플리케이션 개발자가 네트워크 통신과 멀티스레딩을 직접 신경써야 하는데, 이 부분을 추상화하여 단순한 API로 제공하는 것이 핵심이다. 이를 통해 개발자는 로컬 기기에서 데이터를 다루는 것과 같이 원격 기기의 데이터를 다룰 수 있다.

            +

            두 번째, 모바일 기기 뿐 아니라 PC나 TV, 키오스크와 같은 기기가 네트워크에 참여할 수 있어야 한다. 대부분의 PC, TV, 키오스크 기기에는 모바일 기기에 탑재된 것과 같은 장치(센서, 터치스크린 등)가 없거나 부족하다. 따라서 정말 유용한 멀티 디바이스 사용자 경험을 제공하려면 그런 장치를 갖춘 기기와, 장치가 결핍된 기기를 함께 결합할 필요가 있다.

            +

            다음은 안드로이드 기기의 가속도 센서로부터 얻은 데이터를 원격 서버로 전송하는 코틀린 코드다. 특정 서버 기기의 IP 주소를 바라보는 클라이언트를 만들고, 센서값이 변경될 때마다 호출되는 콜백 메서드를 정의해 서버로 가속도계 데이터를 전송한다.

            +
            class MainActivity: AppCompatActivity(), SensorEventListener {
            +  private lateinit var zap: ZapClient
            +
            +  override fun onCreate(state: Bundle?) {
            +    zap = ZapClient(InetAddress.getByName(...))
            +  }
            +
            +  override fun onSensorChanged(event: SensorEvent) {
            +    if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
            +      val (x, y, z) = event.values
            +      zap.send(ZapAccelerometer(x, y, z))
            +    }
            +  }
            +}
            +
            +

            클라이언트(안드로이드 기기)가 보낸 데이터를 수신하는 서버는 단순한 데스크탑 프로그램이다. 아래 Node.js 애플리케이션은 가속도계 데이터를 수신하고 로그를 출력한다. 이렇게 통신 절차를 추상화함으로써 개발자가 적은 양의 코드만으로 원격 기기와 데이터를 주고받을 수 있도록 했다.

            +
            (new class extends ZapServer {
            +  onAccelerometerReceived(info: MetaInfo, data: ZapAccelerometer) {
            +    console.log(`Data received from ${info.dgram.address}:
            +      (${data.x}, ${data.y}, ${data.z})`);
            +  }
            +}).listen();
            +
            +

            Zap 네트워크는 기본적으로 클라이언트-서버 모델이기 때문에 애플리케이션의 전체적인 구조를 단순한 형태로 유지할 수 있다. 모델의 구조적인 특성에 따라 하나의 기기에 여러 기기를 연동하는 1:N 연결도 자연스럽게 지원할 수 있으며, 기기 연결에 클라우드 인프라도 필요하지 않다.

            +

            +Zap 클라이언트-서버 모델.

            +

            위 그림은 클라이언트가 자신의 가속도 센서값을 실시간으로 서버에 전송하는 모습을 보여준다. Zap은 IP 기반 통신으로 높은 대역폭의 이점을 누릴 수 있게 설계되었다. 따라서 클라이언트가 서버에 연결하기 위해서는 우선 서버 기기의 IP 주소를 얻어야 한다. Tap 프레임워크의 경우 모바일 기기간 통신을 목표로 하기 때문에 NFC를 통해 IP 주소를 교환한 뒤, IP 통신을 수행하도록 만들어졌다. 하지만 Zap의 대상 기기는 NFC를 지원하지 않을 가능성이 높으므로 다른 방법을 취해야 했다. 연결 매커니즘이 라이브러리 차원에서 지원할 영역은 아니지만, Zap을 실제 애플리케이션 개발 환경에서 유용하게 사용될 수 있음을 보이려면 고민해볼 필요는 있었다.

            +

            여기에는 다양한 방법이 있겠지만, 일반적으로는 서버 기기의 디스플레이에 서버 IP 주소를 담은 QR 코드를 띄워서 클라이언트가 이를 스캔하도록 하는 방식을 선택할 수 있을 것이다. (Zap을 이용한 예시 애플리케이션들은 모두 이 방식으로 구현했다.) 아니면 상황에 따라 Tap처럼 NFC를 사용해 물리적 접촉으로 기기를 연결할 수도 있을 것이고, BLE를 사용해 주변 기기에 연결 요청을 브로드캐스팅할 수도 있을 것이다. 기기가 연결된 뒤에는 클라이언트의 데이터를 UDP 소켓을 통해 서버로 전송할 수 있댜. 수신 데이터는 그 유형에 따라 서버 측에 정의한 콜백 메서드로 전달된다.

            +

            ZAPP 통신 프로토콜

            +

            안드로이드 기기 간 통신하는 멀티 디바이스 앱을 만들 때는 IPC를 확장하는 개념으로써 원격 기기와 바인더를 공유하는 접근이 일반적이다. 하지만 Zap의 원격 기기는 안드로이드가 아닌 macOS가 구동되는 맥북이나 리눅스 데스크탑, 윈도우즈 키오스크가 될 수 있으므로 보다 범용적인 방식이 필요했다. 시시각각 변경되는 센서 데이터를 빠르게 전송해야 하므로 UDP 소켓을 활용하는 것은 쉽게 생각할 수 있었다.

            +

            하지만 데이터그램에 데이터를 어떻게 담을 것인지가 문제였다. HTTP에 익숙한 나는 큰 고민 없이 JSON을 담아봤다. 그러나 센서에서 얻은 데이터 객체를 JSON으로 인코딩하고, 이를 전송한 뒤, 다시 객체로 디코딩하는 방식은 수십 밀리초 정도로 너무 느렸다. 구분자로 연결된 단순 텍스트를 담는다고 해도 글자 하나당 최소 1바이트를 차지하게 되므로 데이터그램의 크기가 너무 커지는 것이 문제였다. BSON, Protobuf 등의 방식은 목적에 맞지 않는 것처럼 느껴졌다. 결국에는 높은 성능을 달성하기 위해 UDP 위에 자체적인 네트워크 프로토콜 ZAPP(Zap Protocol)을 정의했다.

            +

            +ZAPP 오브젝트의 헤더 파트 구성.

            +

            ZAPP 오브젝트의 첫 9바이트는 헤더 파트로 구성된다. 순서가 중요한 데이터라면 헤더의 8바이트 타임스탬프 필드를 기반으로 애플리케이션에서 순서를 보장할 수 있다. 이어지는 페이로드 파트에는 실제 값이 담겨있으며, 리소스의 종류에 따라 그 인코딩 형식이 다르게 구성된다. 페이로드의 형식은 헤더 파트의 1바이트 리소스 필드를 통해 판별할 수 있다.

            +

            +ZAPP 오브젝트의 가속도계 데이터 페이로드 파트 구성.

            +

            가령 헤더 파트의 리소스 필드 값이 ACC라면 가속도계 데이터를 의미하는 것으로, 페이로드가 x, y, z 축 각각에 대한 값이 4바이트씩, 총 12바이트로 인코딩되어 있음을 알 수 있다. Zap은 가속도 센서와 같은 동작 센서, 조도 센서를 비롯한 환경 센서, 그리고 UI 이벤트, 텍스트, 지오포인트 등의 리소스를 지원한다.

            +

            데이터 형식에 대한 유연성은 다소 떨어졌지만, ZAPP를 적용해보니 클라이언트에서 서버로 10만개의 ZAPP 오브젝트를 전송했을 때 하나의 오브젝트 당 인코딩부터 디코딩까지 평균 0.03ms 이내의 성능을 얻을 수 있었다.

            +

            멀티 디바이스 애플리케이션

            +

            Zap 구현체를 실제로 유용하게 사용할 수 있음을 보이기 위해, 앞서 제시한 모션 컨트롤러를 비롯한 멀티 디바이스 애플리케이션을 몇 가지 만들었다. 첫 번째는 스마트폰을 프레젠테이션 리모컨으로 사용하는 예시다.

            +

            +

            화면상 버튼 UI 뿐만 아니라 측면 음량 버튼을 눌러서 슬라이드를 넘기는 모습도 볼 수 있다.

            +

            또 다른 예시는 태블릿에서 펜으로 작성한 글자를 랩탑에 공유하는 애플리케이션이다. 일반적인 랩탑에는 터치스크린이 없기 때문에 모바일 기기를 결합하면 높은 시너지 효과를 낼 수 있다. 특히 한자나 카나 문자를 작성해야 하는 상황에 유용하다.

            +

            +

            이 예시는 ML Kit Digital Ink를 수정한 것이기 때문에 태블릿 기기 내에서 글자 인식이 이루어진다. 하지만 이를 역으로 확장하면 GPU 연산은 고성능 기기에서 수행하고, 모바일 기기에서는 그 연산 결과만을 전달받아 응용하는 분산 컴퓨팅도 가능하다. 또한 Zap의 주요 목표는 모바일 기기와 PC의 결합이지만, 모바일-모바일 연동이나 PC-PC 연동도 문제없다.

            +

            Zap 라이브러리와 예시 애플리케이션은 아직 프로토타입 수준이기 때문에 더 고민해볼 지점이 많이 남아있다. 우선 UDP 데이터그램을 암호화하고, 클라이언트 기기에 대한 권한 인증 절차가 필요하다. 또한 처리량을 높이기 위해 서버가 접속 클라이언트 수에 따라 스레드를 유동적으로 관리하도록 개선해야 한다. 추가로 서버가 클라이언트로부터 수신한 데이터를 더 이상 처리하지 못하는 상황에 대응할 필요도 있다. 현재 Zap 구현체는 푸시 모델로 구현되어 있으며, 버퍼가 꽉 차서 서버가 더 이상 수신 데이터를 처리할 수 없는 상황에는 추가로 수신하는 데이터를 버리게 된다. 성능이 좋지 않는 랩탑에서 앞서 구현한 모션 컨트롤러를 사용해봤을 때 딜레이가 체감될 정도였는데, 아예 처리율을 서버 측에서 설정하는 방식으로 개선할 수도 있을 것 같다.

            +

            나에게 Zap은 나름 도전적인 과제였는데, 우수상을 수상하며 캡스톤 프로젝트를 잘 마무리 지었다. Zap의 구현체와 모든 예시는 github.com/zap-lib에서, 문서는 zap-lib.github.io에서 확인할 수 있다.

            + +
            +
            + + +
            + +
            +
            +

            아치 리눅스로 15년차 넷북 되살리기

            +

            Eee PC 1000HE 위에 올린 아치 리눅스 32

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/43.html b/article/43.html new file mode 100644 index 0000000..7fc2b42 --- /dev/null +++ b/article/43.html @@ -0,0 +1,126 @@ + + + + + + 7년만에 학부를 마치며 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 7년만에 학부를 마치며 +

            + +

            + 화석의 회고 +

            + + + + +
            +
            Table of Contents +

            +
            +

            나에게 컴퓨터는 무한 캔버스였다.

            +

            새내기의 마음가짐

            +

            미디어는 스무 살을 찬란하게 묘사한다. 스무 살은 두 번 다시 오지 않을 가장 반짝이는 나이이며, 모든 가능성이 열려 있는 나이이고, 하루하루가 가슴 뛰어야 하는 나이다. 하지만 현실에 미디어가 그려내는 스무 살은 없었다.

            +

            대부분의 수업은 시시하게 느껴졌고, 아무것도 모르는 신입생들에게 교재 발제를 맡긴 수업에는 알맹이가 있을 리 없었다. 대학이 순수한 목적으로 학문을 탐구하는 곳이길 바랐지만, 순진한 생각이었다. 학교는 새내기에게도 취업을 강조했다. 하루는 대기업에 취업한 선배들이 발표하는 특강을 듣기 위해 주말에도 학교에 나와야 했고, 교수님들은 수업 중 ‘성공한’ 제자들의 사례를 자랑스럽게 나열하곤 했다. 여기서 '성공’이란 당연히 대기업에 입사해서 높은 연봉을 받는 것을 의미했다. 스무 살이 되자마자 취업이라는 과업을 받아드니 공허함이 밀려왔다.

            +

            딱히 마음을 둘 곳도 없었다. 솔직히 말하자면 입학 직후 새내기 배움터(새터, OT)에서 억지로 장기자랑을 시킬 때부터 이미 학과에는 정나미가 떨어졌고, 다른 학과의 소학회나 중앙동아리를 전전하며 내 자리를 찾고 있었다. 더군다나 4인1실의 기숙사 방은 너무 좁아서 숨이 막힐 지경이었고, 1층이었던지라 대낮에도 방 안은 어두웠다. 같은 방에는 내가 초등학교 5학년이었을 때부터 대학생이었을 09학번 선배가 있었다. 선배는 8년간 어떤 사연으로 아직까지 학교에 남아 있는 것일까 궁금했다. 그러나 입소 첫날 서로 인사를 한 뒤로는 7시에 기상해서 12시에 취침하는 그 선배와 제대로 된 대화를 나눌 기회가 없었다. 선배의 책장에 꽂혀 있던 CPA 책이 몇몇 추리의 단초만을 제공할 뿐이었다.

            +

            낮에 수업이 없을 때는 도서관에서 시간을 보내곤 했다. 도서관은 교내에서 거의 유일하게 혼자 시간을 보낼 수 있는 장소였고, 높은 책장 사이에서 완전히 긴장을 풀 수 있는 장소였다. 대학 도서관에는 없는 책이 없었고, 만에 하나 찾는 책이 없으면 구입 신청도 할 수 있었다. 전공서부터 소설까지 온갖 책을 손에 잡히는 대로 읽었다. 한편 밤에는 대부분의 시간을 술집에서 보냈다. 더 많은 술자리에 나갈수록 스무 살이 더욱 반짝인다고 생각한 것마냥 알코올을 찾아 다녔다. 밤새 술을 마시고 첫차를 기다리며 거리를 서성이거나, 담배 냄새나는 친구 자취방에서 아침을 맞이하곤 했다. 술자리는 성년의 자유를 즐기는 자리이기도 했지만, 때로는 내가 대학 사회에 어울리는 인간임을 증명하는 자리이기도 했다. 입학 전 '예비 신입생 환영회’부터 입학 후 ‘신입생 환영회’, 새터 등 온갖 행사에 참석하며, 가고 싶지도 않은 술자리와 뒷풀이를 굳이 찾아가며, 각종 동아리에 중복 가입하며, 맥주 광고같은 스무살을 실현하기 위해 부단히 노력했다.

            +

            하지만 나의 스무 살은 집에서 홀로 크리스마스를 보내는 듯한 묘한 기분으로 흘러갔다.

            +

            학교에서 배우기

            +

            2학년이 되자 친구들은 진지해지기 시작했다. 이제는 속수무책으로 졸업을 향해 달려가고 있다는 생각이 들었다. 종강이 다가올 때마다 무엇을 할 것인가하는 걱정에 사로잡혔다. 불확실한 미래에 대한 불안감이 있었지만, 가장 즐거운 대학 생활을 보낸 시기이기도 했다. 전공 수업은 물론이고 교양 수업까지도 모든 내용이 흥미로웠고, 이제서야 학교에서 의미를 찾을 수 있었다.

            +

            미디어학과에는 크게 컴퓨터공학, 시각디자인, 영상미디어 분야의 수업들이 개설된다. 미디어학도는 자신의 진로에 맞게, 혹은 끌리는대로 관심 분야의 수업을 선택해 수강할 수 있다. 내가 2학년이 되는 해부터는 자유의 폭이 더욱 넓어져서 말그대로 나만의 커리큘럼을 구성할 수 있었다. 이와 더불어, 소프트웨어학과 수업을 눈여겨 보고 있던 내게 타과 수업 수강에 제한을 두지 않는 학교 정책도 큰 행운이었다. 수강신청날에 선착순 안에만 든다면 듣고 싶은 수업은 마음껏 수강할 수 있었다.

            +

            컴퓨터구조 수업은 특히 재미있었다. 나는 중학교 1학년 때 처음 C언어를 공부하면서 컴퓨터 프로그래밍을 시작했다. 취미로써, 가끔은 일로써 수많은 애플리케이션 코드를 작성하면서도 논리 세계의 코드가 도대체 어떻게 물리 세계의 전자회로를 조작한다는 것인지 이해하지 못하고 있었다. 컴퓨터라는 기계를 제대로 이해하지 못한 채로 컴퓨터를 다루고 있다는 사실이 답답하기도, 불안하기도, 부끄럽기도 했다. 컴퓨터구조 수업은 컴퓨터의 실체를 밝혀준 수업이었다. MIPS 어셈블러를 만드는 첫 과제는 쉽지 않았다. 손으로 직접 어셈블리 코드를 32비트 바이너리로 해석해봐야 했고, WSL이 없어서 버벅거리는 VMWare 위에 우분투를 올리고 코딩해야 했다. 크리스 소이어(Chris Sawyer)는 대체 이런 언어로 어떻게 롤러코스터 타이쿤을 만든 것인지 경이로웠다. 이때 C로 작성한 MIPS 어셈블러는 3년뒤 러스트로 재작성하기도 했다.

            +

            +컴퓨터구조 수업에서 MIPS 어셈블러를 만들기 위해 작성한 노트

            +

            자료구조 수업에서는 데이터를 수학적으로 해석하고, 효율적인 구조를 실제로 구현하는 방법을 배웠다. 운영체제 수업의 교수님은 전공책을 처음부터 끝까지 정독해본 경험이 필요하다고 말했다. 그 덕분에 <Operating System Concepts>은 지금까지도 내가 가장 좋아하는 컴퓨터 과학 서적으로 남아있다. 이때 학교에서 배운 내용들을 바탕으로 공룡책으로 정리하는 운영체제, 캐시가 동작하는 아주 구체적인 원리 같은 블로그 글을 쓰기도 했다.

            +

            미디어학과 전공 커리큘럼에는 시각디자인 수업들도 있었다. 그래픽 디자인 수업에서는 기초적인 조형요소와 원리를 배우고 벡터 그래픽을 다루는 방법을 익혔다. 넘치는 열정으로 4학년 수업이었던 UX디자인 수업을 들었는데, 엄청난 과제량에 압도되면서도 사용자 경험을 디자인한다는게 무엇인지, 디지털 제품을 어떤 과정으로 설계하는지 배우는 것이 즐거웠다. (이렇게 강도높은 수업은 수명을 깎아내지만, 종강하면 기억이 미화되기 때문에 같은 선택을 반복하게 된다.) 디자인 수업들은 '좋아 보이는 것’이 왜 좋아 보이는지 설명하는 훈련의 연속이었다. 좋은데 이유가 어디있냐는 말이 낭만있긴 하지만, 왜 좋은지 자신의 언어로 풀어보면 더 좋아지기도 한다. 이때 내가 이른바 스위스 스타일로 불리는 국제주의 양식의 형식미를 좋아한다는 사실을 알게 됐다. 지금도 나는 포스터를 모은다.

            +

            +UX디자인 수업에서 만든 고객 여정 지도

            +

            시각디자인은 확실히 재미있었으며, 내 적성에도 잘 맞았다. 하지만 그 전문성을 인정받기가 쉽지 않았다. 디자인 결과물에는 누구나 한마디씩 얹기를 좋아했다. 클라이언트가 만족할만한 결과물을 만들기 위해 협상과 타협을 반복하는 것도 지치는 일이었고, 제작 과정이 결과물에 투영되어 함께 평가받는 점도 부담스러웠다. 이 시기에 보다 분명히 스스로를 엔지니어로 정체화했던 것 같다.

            +

            여러 동아리를 돌아다닌 끝에 정착한 사진 동아리와 프로그래밍 동아리에서 적극적으로 활동하기는 했지만, 그저 좋은 사람을 넘어서 가치관까지 맞는 사람을 찾기는 쉬운 일이 아니었다. 1학년 때는 함께 어울리던 사람들이 특정 지점에서 나와 가치관이 어긋난다는 사실을 알게 되면 마음이 떠나버리곤 했다. 10대 시절을 함께 보내면서 가치관이 동기화된 친구가 아닌 이상, 성인이 되어 그런 관계를 찾는 것은 기적이 필요하다는 사실을 깨닫고 나서야 현실과 타협하며 관계를 확장해 나갔다. 좋은 말로하면 사회화였다. 하지만 아무리 친절하고 성실한 사람이더라도 '장애인’이라는 단어를 욕설로 사용한다면 친구로 지낼 수는 없는 노릇이었다. 우연히 알게 된 교지편집위원회(교편위)는 나와 비슷한 가치관을 가진 사람들이 모인 곳이었다. 나를 제외한 모든 편집위원들이 사회학도나 인문학도였고, 어떤 아젠다에는 의견 차이가 있었지만, 마음은 편했다. 그들은 적어도 3년전 일어난 세월호 참사를 애도할줄 아는 사람들이었다.

            +

            2학년 때는 대부분의 시간을 동아리방에서 보냈다. 청춘의 변두리에서 새내기 시절을 보냈지만, 각종 대학조직의 주도권이 2학년에게 이양되는 흐름과 함께 교편위 편집장이 되었다. 교편위는 학생운동이 수축되고 학생자치가 와해되며 학내에서도 입지를 잃어왔다. 대학의 탈정치화가 이어지면서 자치교지는 학생사회로부터 외면받았고, 결국 등록금 고지서에서 교지비가 삭제되며 기부금으로 교지를 발행하는 상황에 처해 있었다. 자치교지의 열악한 상황은 다른 학교도 별반 다르지 않았다. 전국 대학교지 커뮤니티에는 OO대학교 자치교지의 마지막호를 발간한다는 글이 심심찮게 올라오곤 했다. 내 입장에서 교편위 활동이 전공지식이나 커리어에 그다지 도움되는 것은 아니었지만, 학생 자치 언론 기구 하나가 사라지는 것을 가볍게 생각할 수는 없었기에 큰 책임감을 느끼고 임했다. 또한 같은 기숙사 방을 쓰던 동기의 제안으로 프로그래밍 동아리 학술부장이라는 애매한 직책을 맡기도 했다. 이때 동아리의 새내기 전공자와 비전공자를 대상으로 1년간 타입스크립트와 jQuery, PHP, MySQL로 간단한 웹 애플리케이션을 만드는 워크숍을 운영했는데, 그 결과 깨달은 점이 있었다: 무언가에 대해 안다는 것과 그것을 교육한다는 것은 완전히 다른 문제라는 점이다. 이와 동시에 학교의 지원을 받는 창업 동아리에 창업 멤버로 합류해 리액트 네이티브 앱을 개발했고, 청소년 교육 비영리단체에서 개발과 디자인 작업을 하기도 했다. 벚꽃이 필 때 동아리 부스를 지키며, 초여름에 트럭을 타고 교지를 배부하며, 방학에도 매일 학교에 나오며, 그 모든 활동에 진지하게 임했고, 때로는 부담감을 느끼며 분주한 시간을 보냈지만, 지금 돌이켜 보면 많은 기회와 위기의 상황에서 너무 아마추어스럽게 행동한 같아서 부끄럽다. 뭐라도 해야한다는 조급증에 사로잡힌 학부 2학년 학생이 이제 막 전공 수업을 듣고 우매함의 봉우리에 올랐던 순간이 아니었나 싶다.

            +

            한국에서 대학생은 최고의 신분이라고 생각했다. 가능하다면 평생 학부생으로 살고 싶고, 하다못해 최대한 학부생으로 오래 남기 위해 최선을 다하겠다고 생각했다. 그러나 한편으로는 이 모든 것이 소꿉놀이 같았다.

            +

            필드에서 배우기

            +

            대학교는 도전적인 시도를 장려하는 곳이고, 주기적으로 컴포트 존(Comfort zone)을 벗어나볼 수 있는 역동적인 곳이다. 수업에서는 나보다 먼저 문제를 다뤄본 이들이 문제에 어떻게 접근했는지, 그 문제를 어떤 과정으로 해결했는지 배울 수 있었고, 이를 통해 그들의 사고흐름과 방식을 어설프게나마 따라해볼 수 있었다. 하지만 "그래서 뭐?"라는 질문에 대한 대답은 내가 스스로 찾아야 했다. 가령 객체지향프로그래밍 수업을 들으면 추상화나 다형성, 상속, 은닉, 캡슐화 같은 개념을 배우고, 이러한 개념을 응용해서 실제 프로그램을 작성해보기도 한다. 그러나 많은 개발자가 참여하는 대규모 코드베이스에서 작업해보지 않고서는 정말로 왜 OOP가 유용한지 알 수 없고, 작은 프로그램을 하나 만드는데 왜 그렇게 까지 장황한 코드를 작성해야 하는지 의문이 생길 수 밖에 없다. 수업을 듣고, 코드를 작성하고, 시험을 보고, 과제를 하고, 성적을 받으면서도 나는 그저 교수님이 주최하는 보물찾기 놀이에 너무 진지하게 참여하고 있다는 생각이 들었다.

            +

            1학년을 마친 뒤로 많은 친구들이 군에 입대했다. 병역은 휴학의 명분이 되었다. 초등학교부터 중고등학교, 그리고 대학교 1학년까지 도합 13년간 이 굴레에서 벗어나본 적이 없었다. 대학생이 되어 얻은 휴학이라는 선택지는 막연하게 자유롭게 정할 수 있는 방학처럼 느껴지기도 했기에 1학년 개강 첫날부터 나는 휴학을 노래해왔다. 그럼에도 굴레를 스스로 끊으려면 용기가 필요했다. 내 성격에 그럴듯한 이유와 정당한 명분, 뚜렷한 계획이 없다면 절대로 휴학을 선택할 수 없을 것 같았다. 2학년이 되자 휴학은 방학이 아니라, 거침없이 졸업을 향해 달려가는 초시계를 멈추기 위한 방법으로 느껴졌다. 다들 휴학할 때 함께 휴학하는 것이 좋겠다는 생각으로 2학년 초부터 군 휴학을 알아보기 시작했다. 중학생때부터 알고 있던 병역특례 제도를 드디어 활용할 때가 온 것 같았다. 10월 안에 지원서 제출을 시작하고 다음해 1월부터 복무를 시작하면 딱 좋겠다고 생각했다. 일단은 산업기능요원 복무가 가능한 기업들을 찾아 스프레드 시트에 나열하고 지원할만한 포지션을 정리했다. 병무청에서 미디어 전공이 '전산관련 전공’임을 확인받고, 꾸역구역 활동을 끌어담아 담백한 이력서를 작성하자 사실상 준비는 끝났다. 하지만 자신이 없었다. 내가 필드에서 돈받고 일할 정도의 역량이 되는지, 기업은 어느정도의 실력을 기대하는지, 경쟁력이 부족하지 않은지, 사실 애초에 자질이 없는 것은 아니었는지, 끝없는 고민과 의심이 이어졌다. 걱정과 함께 지원 시기를 계속 미루면서 산학협력 교수님과 취업센터에서 상담을 받고 이력서만 계속 수정해나갔다. 1학년 가을에 여기어때 윤진석 CTO가 학교에서 특강을 한 적이 있는데, 실제 채용 공고를 보여주며 일단 지원해보라고 독려했던 기억이 났다. 11월이 되자 더 이상 물러날 곳이 없었고, 메일로 이력서를 제출했다. 맨 땅에 헤딩하는 심정이었다.

            +

            어디선가 들은 "회사는 학교가 아니다"라는 말 때문에 회사는 막연히 두려운 곳으로 느껴지기도 했다. 하지만 그런 말이 무색할 정도로 회사는 친절했다. 첫 출근 이후 3개월 남짓한 기간 동안 CTO의 도움을 받으며 백오피스 작업을 했고, 프로덕션 환경에 첫 배포를 했다. 배포할 때는 엔터를 살살쳤다. 학교와 달리 회사에서는 책임이 따랐기 때문이다. 프로덕션에 내 코드가 올라갈 때, 태스크 마감 기한이 다가올 때, 담당자로서 미팅에 참석해 무엇이 가능하고 무엇이 불가능한지 말할 때 그 책임이 손에 잡힐듯 느껴졌다. "알아보고 말씀드릴게요"와 "~라고 알고 있습니다"를 입에 달고 살았다. 왜 시니어들이 텍스트에 끝없이 마침표를 찍어대는지 알 것 같았다. 무엇도 함부러 단언해서 말할 수 없기 때문이었다.

            +

            입사할 때 회사에는 50명이 채 안 되는 사람들이 있었다. 4주간 훈련소를 갔다온 뒤로 회사는 더욱 빠르게 성장했다. 매주 새로운 사람들이 입사했고, 휑했던 사무실이 북적였다. 1년차 개발자가 그 성장에 얼마나 기여했겠냐마는,

            +

            개발자들이 모여 백로그에서 원하는 태스크를 선택해 업무를 진행하던 방식은 이 시기에만 즐길 수 있는 낭만이었다.

            +

            어떤 일을 무작정 오래한다고 실력이 좋아지지는 않는다고 한다. 나는 지난 20년간 적어도 매일 6분씩 하루도 빠지지 않고 양치질을 했지만, 23살이 되어서야 지금까지 양치질을 잘못하고 있었음을 깨달았다. 4년차가 됐을 때 기분이 그랬다. 이제 업무도 익숙해졌고, 크게 당황할 일도, 딱히 즐거울 일도 없어졌다. 새로운 기능을 구현할 때 어떤 데이터를 어디에 요청하고, API는 어떻게 설계하고, 캐시는 어디에 하고, 배치는 언제 돌리고, DB 테이블은 어떻게 구성할지 대략 머릿속에 그려졌다. 반복적인 업무는 거의 무의식 중에 PR을 날리고 배포했다. 지난 3년간 명함에 소프트웨어 엔지니어라는 이름을 달고 일을 해왔지만, 내가 나의 일을 정확하게 알지 못한 채로 일하고 있다는 생각이 들었다. 내가 프랙티컬하게 하는 일 뒤에 숨겨진 이론이나 법칙이 있을 것 같았다.

            +

            학교에 뭔가 두고 왔다는 느낌을 지울 수 없었다.

            +

            다시 학교로 돌아가기

            +

            4년만에 돌아온 학교는 많은 것이 변해 있었다. 처음보는 건물이 있었고, 존경하던 교수님들이 학교를 떠났고, 그 자리를 젊은 교수님들이 채웠고, 무관심 속에 교지는 사라졌고, 학식 가격은 2배 정도 올라있었고, 새터에서 새내기들에게 시키던 장기자랑은 폐지되어 있었다. 다음 해 입학할 신입생들이 중학교 1학년이었을 때 나는 대학생이었다. 간혹 출석부에 16, 17학번이 보이면 무슨 사연이 있는지 대뜸 물어보고 싶었다. 5년전 기숙사 방을 함께 썼던 09학번 선배가 다시 떠올랐다.

            + +
            +
            + + +
            + +
            +
            +

            스마트폰을 PC의 모션 컨트롤러로 만들기

            +

            멀티 디바이스 앱을 위한 라이브러리, Zap

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/5.html b/article/5.html new file mode 100644 index 0000000..4fc03f6 --- /dev/null +++ b/article/5.html @@ -0,0 +1,293 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦕 공룡책으로 정리하는 운영체제 Ch.1 +

            + +

            + Overview +

            + + + + +
            +
            Table of Contents +

            +
            +

            Abraham Silberschatz의 Operating System Concepts는 운영체제의 바이블로 불린다. 이번에 운영체제 수업을 들으면서 Operating System Concepts 9th Edition의 내용을 정리해보기로 했다.

            +

            Ch.1은 책 전체 내용이 담겨있는 가장 중요한 챕터다. 이 부분을 제대로 정독하고 책을 읽으면 훨씬 읽기 수월하다.

            +

            What Operating Systems Do

            +

            운영체제(Operating System)는 컴퓨터의 하드웨어를 관리하고, 하드웨어와 소프트웨어, 사용자를 매개하는 프로그램이다. 커널(Kernel)은 운영체제의 핵심이며, 실체다. 운영체제는 커널과 커널 모듈(Kernel module)들로 구성되는데, 커널이 운영체제의 핵심이다보니 일반적으로 운영체제와 커널은 동일시 된다. 커널이 같다면 같은 운영체제로 취급한다.

            +

            컴퓨터 시스템의 요소는 하드웨어(CPU, 메모리, 입출력장치 등), 운영체제, 어플리케이션 프로그램(웹 브라우저, 워드프로세서 등), 유저로 나뉜다. 운영체제는 정부와 비슷하다. 정부 그 자체만으로는 쓸모있는 기능을 못하지만, 사람들에게 더 나은 환경을 제공한다. 마찬가지로 운영체제는 프로그램들이 유용한 일을 할 수 있는 환경을 제공한다. 운영체제의 역할은 사용자 관점(User View)과 시스템 관점(System View)으로 나눠볼 수 있다.

            +

            User View

            +

            일반적으로 사용자는 컴퓨터 앞에 앉아 키보드와 마우스를 조작한다. 이 경우 운영체제는 사용자가 컴퓨터 자원 사용(Resource utilization)을 신경쓰지 않게 도우며, 사용자가 컴퓨터를 쉽게 이용할 수 있도록 만든다. 또 다른 경우, 사용자는 메인프레임(Mainframe)에 연결된 터미널을 사용하거나 미니컴퓨터(Minicomputer)를 사용한다. 이 상황에서는 컴퓨터의 자원을 여러 사용자가 나눠쓰게 되는데, 운영체제는 사용자들이 자원을 공평하게 사용할 수 있도록 돕는다.

            +

            System View

            +

            시스템에게 운영체제는 자원 할당자(Resource allocator)다. 컴퓨터 시스템은 CPU 시간, 메모리 공간, 파일 저장소 공간, 입출력 장치 등 다양한 문제를 해결해야 한다. 운영체제는 이러한 컴퓨터 자원들을 관리하는 제어 프로그램(Control program)으로서 동작한다.

            +

            Computer-System Organization

            +

            현대의 일반적인 컴퓨터 시스템은 여러개의 CPU와 장치 컨트롤러(Device controllers)로 구성되어 있다. 그리고 이들은 공통버스(Commmon bus)로 이어져 메모리를 공유한다.

            +
                           disks     mouse, keyborad, printer     monitor
            +                 |                   |                   |
            ++-----+ +--------+--------+ +--------+-------+ +---------+--------+
            +| CPU | | disk controller | | USB controller | | graphics adapter |
            ++--+--+ +--------+--------+ +--------+-------+ +---------+--------+
            +   |             |                   |                   |
            +   +-------------+---------+---------+-------------------+
            +                           |
            +                       +---+----+
            +                       | memory |
            +                       +--------+
            +
            +

            Computer Startup

            +

            컴퓨터를 켜면 부트스트랩 프로그램(Bootstrap program)이라는 초기화 프로그램이 실행된다. 이 프로그램을 컴퓨터의 ROM(Read-Only Memory)이나 EEPROM(Electrically Erasable Programmable Read-Only Memory)에 저장되어 있으며, 주로 펌웨어(Firmware)라고 불린다. 부트스트랩 프로그램은 시스템을 초기화하고, 부트로더(Boot loader)를 실행한다. (멀티부팅 컴퓨터의 경우 부트로더가 여러 운영체제를 가리키고 있는데, 이 경우엔 어떤 운영체제를 실행할지 선택해야 한다.) 그리고 부트로더는 최종적으로 운영체제를 실행하게 된다.

            +

            커널이 로드, 실행되면 시스템과 사용자에게 서비스를 제공해야 한다. 이때 일부 서비스는 커널 외부에서 제공되는데, 이들은 부팅할 때 메모리에 로드되는 시스템 프로세스(System processes)나 시스템 데몬(System daemons)이다. UNIX의 경우 첫 시스템 프로세스는 init이며, 이 프로세스는 또 다른 데몬들을 실행시킨다. 데몬은 프로세스로 백그라운드에서 돌면서 시스템 로그는 남기는 등의 여러 작업을 한다. 이러한 과정이 끝나면 시스템이 완전히 부팅되고, 이벤트가 발생하기를 기다리게 된다.

            +

            Computer-System Operation

            +

            입출력 장치와 CPU는 동시에 실행될 수 있다. 장치 컨트롤러는 CPU에게 이벤트 발생을 알리는데, 이벤트 발생을 알리는 것을 인터럽트(Interrupt)라고 부른다. 인터럽트는 '방해하다’라는 뜻인데, 컴퓨터에서는 신호를 보내 이벤트 발생을 알리는 것을 의미한다. 보통 컴퓨터는 여러 작업을 동시에 처리하는데, 이때 당장 처리해야 하는 일이 생겨서 기존의 작업을 잠시 중단해야 하는 경우 인터럽트 신호를 보낸다. 그러면 커널은 작업을 멈추고 인터럽트를 처리한 뒤 다시 기존 작업으로 돌아온다.

            +
            CPU     user          |                |                |          |
            +        process       |                |                |          |
            +        executing ----|----------------|--+  +----------|----------|--+    +-----
            +                      |                |  |  |          |          |  |    |
            +        I/O interrupt |                |  +--+          |          |  +----+
            +        processing    |                |                |          |
            +                      |                |                |          |
            +I/O     idle      ----|---+            +----------------|--+       +-------------
            +device                |   |            |                |  |       |
            +        transferring  |   +------------+                |  +-------+
            +                      |                |                |          |
            +======================+================+================+==========+=============
            +                      I/O              transfer         I/O        transfer
            +                      request          done             request    done
            +
            +

            인터럽트는 하드웨어나 소프트웨어에 의해 발생할 수 있으며, 소프트웨어에 의해 발생하는 인터럽트는 트랩(Trap)이라고 부른다. 하드웨어의 경우 시스템 버스(System bus)를 통해 CPU에 신호를 보냄으로써 인터럽트를 발생시키고, 소프트웨어는 시스템 콜(System call)이라는 특별한 명령으로 인터럽트를 발생시킨다.

            +

            Common Functions of Interrupts

            +

            보통 컴퓨터는 여러 작업을 동시에 처리하는데, 만약 CPU가 인터럽트 신호를 받으면, 앞서 말했듯이 하던 일을 잠시 멈추고 메모리의 어떤 고정된 위치(Fixed location)를 찾는다. 이 위치는 인터럽트 벡터(Interrupt vector)에 저장되어 있다. 인터럽트 벡터는 인터럽트를 처리할 수 있는 서비스 루틴(Service routine)들의 주소를 가지고 있는 공간으로, 파일 읽기/쓰기와 같은 중요한 동작들이 하드코딩되어 있다. 이렇게 인터럽트를 처리하고나면 CPU는 다시 원래 작업으로 돌아온다. 이 과정은 사용자가 눈치채지 못할 정도로 매우 빠르게 일어날 수도 있고, 너무 느려서 오랜 시간을 기다려야 할 수도 있다. 참고로 어떤 값을 0으로 나누는 것(Division by zero)도 인터럽트이며, 이러한 내부 인터럽트(Internal interrupt)는 예외(Exception)라고 부른다.

            +

            Interrupt Handling

            +

            현대 운영체제들은 대부분 인터럽트 주도적(Interrupt driven)이다. 인터럽트가 발생하기 전까지 CPU는 대기상태에 머문다. 반면 폴링(Polling)의 경우 주기적으로 이벤트를 감시해 처리 루틴을 실행한다. 이렇게 하면 컴퓨팅 자원을 낭비하게 되기 때문에 인터럽트 주도적으로 설계하는 것이다.

            +

            Storage Structure

            +

            커널은 실행기(Executor)를 통해 프로그램을 실행시킨다. 실행기는 기억장치(Storage)에서 exe파일(Windows의 경우)을 가져오고, 커널이 이것을 메모리에 할당해 실행시킨다. 이처럼 모든 프로그램은 메인 메모리에 로드되어 실행되며, 메인 메모리는 보통 RAM(Random-Access Memory)이라고 부른다. 하지만 RAM은 모든 프로그램을 담기엔 너무 작고 비싸다. 또한 전원이 나가면 저장된 데이터가 모두 사라지는 휘발성(Volatile) 장치다. 그래서 보조기억장치(Secondary storage)가 필요하다. 자기테이프(Magnetic tapes), 광학디스크(Optical disk), 자기디스크(Magnetic disk), SSD(Soli-State Disk)는 비휘발성(Non-volatile) 기억장치다. 반면 메인 메모리, 캐시(Cache), 레지스터(Registers)는 휘발성 기억장치다. 보조기억장치는 용량이 크고 저렴한 반면, 캐시나 레지스터는 용량이 작고 비싸다. (공학도에겐 가격이 중요하다.)

            +

            요즘 컴퓨터에는 최대절전 모드가 있는데, 이 방식이 꽤 재밌다. 컴퓨터가 절전모드에 들어가면 메모리의 모든 데이터를 덤프해서 보조기억장치에 담아두고, 다시 절전모드 빠져나오면 덤프해둔 데이터를 불러와 그대로 작업을 수행한다. 영어로는 하이버네이트(Hibernate)라고 한다.

            +

            I/O Structure

            +

            기억장치는 여러 입출력장치(I/O devices) 중 하나일 뿐이다. 컴퓨터는 다양한 입출력 장치를 가지고 있으며, 입출력 컨트롤러는 각각 다른 장치를 담당한다. 컴퓨터는 이 컨트롤러 덕분에 다양한 장치를 사용할 수 있다. 또한 운영체제는 각 장치 컨트롤러를 제어하기 위한 장치 드라이버(Device driver)를 가지고 있다.

            +

            입출력 명령을 수행하기 위해 장치 드라이버는 장치 컨트롤러의 레지스터를 로드한다. 장치 컨트롤러는 레지스터에서 "키보드로부터 문자 읽어오기"와 같은 동작을 읽고, 장치에서 로컬 버퍼(Local buffer)로 데이터를 전송하기 시작한다. 데이터의 전송이 끝나면 장치 컨트롤러는 장치드라이버에게 인터럽트를 보내 동작이 끝났음을 알리고, 장치 드라이버는 통제권을 운영체제에게 돌려준다. 이때 입력받은 데이터나 상태 정보를 넘겨주기도 한다.

            +

            사용자 프로그램은 커널과 사용자 프로그램을 매개하는 인터페이스인 시스템 콜(System call)을 통해 입출력을 요청할 수 있다.

            +

            Direct Memory Access Structure

            +

            과거에는 장치 데이터를 처리하기 위해 CPU를 거쳐 메모리에 로드하는 방식을 사용했으나, CPU 자원이 너무 많이 소모되기 때문에 이젠 DMA(Direct Memory Access)를 사용한다. DMA는 장치와 메모리를 직접 연결하는 방식으로, 버스가 지원하는 기능이다. 메모리의 일정 부분은 DMA에 사용될 영역으로 지정되며, DMA를 제어하는 컨트롤러는 DMA 컨트롤러라고 부른다. 참고로 CPU를 거치는 방식은 PIO(Programmed I/O)라고 부른다. 장치의 데이터는 장치 컨트롤러에 의해 직접 메모리에 전달되며, CPU에서는 데이터 이동이 완료되었다는 인터럽트만 한 번 일어난다. 이렇게 하면 결과적으로 CPU가 하는 일이 줄어드니까 성능이 좋아진다.

            +

            Computer-System Architecture

            +

            현대 컴퓨터 시스템은 기본적으로 폰 노이만 구조를 따른다.

            +
            CPU (*N)                                                                        Memory
            ++------------------------------+-------+<-instruction execution cycle->+--------------+
            +| thread of execution          | cache |                              |              |
            ++---+--------------------------+-------+<--------data movement------->|              |
            +    |           ^        ^                                            |              |
            +    | I/O       | data   | interrupt                                  | instructions |
            +    | request   |        |                                            | and data     |
            +    v           v        |                                            |              |
            ++------------------------+-------------+                              |              |
            +| device (*M)                          |<-------------DMA------------>|              |
            ++--------------------------------------+                              +--------------+
            +
            +

            Single-Processor Systems

            +

            과거 대부분의 컴퓨터는 싱글 프로세서를 사용했다. 싱글 프로세서 컴퓨터에는 하나의 메인 CPU만 탑재되며, 장치에 따라 특별한 목적을 가진 프로세서가 들어갔다. 가령 디스크 프로세서는 디스크 연산만 수행하고, 키보드 프로세서는 키보드 연산만 수행하는 식이다.

            +

            Multiprocessor Systems

            +

            멀티 프로세서 시스템은 이젠 일반적인 컴퓨터 시스템이 되었다. 멀티 프로세서 컴퓨터는 2개 이상의 프로세서를 가지고 있다. 처음에는 서버 컴퓨터에 처음 적용됐는데, 지금은 모바일 기기도 멀티 프로세서 시스템으로 만들어진다. 멀티 프로세서 시스템은 몇가지 장점을 가지고 있다.

            +
              +
            1. 처리량(Throughput)의 증가: 당연하겠지만 프로세서가 늘어나면 더 빠른 시간 안에 연산을 수행할 수 있다. 물론 프로세서를 계속 늘린다고 성능이 한없이 좋아지는 것은 아니며, 증가 비율이 1:1인 것도 아니다.
            2. +
            3. 규모의 경제: 멀티 프로세서 시스템은 여러 대의 싱글 프로세서 시스템을 구축하는 것보다 돈이 적게 든다. 멀티 프로세서 시스템은 주변장치(Peripherals)를 공유할 수 있기 때문이다.
            4. +
            5. 신뢰성의 증가: 만약 기능이 여러 프로세서에 분산될 수 있다면, 하나의 프로세서가 작동을 멈춰도 전체 시스템은 느려질 뿐 멈추지 않는다. 이런 식으로 성능이 나빠지지만 작동은 가능하도록 하는 것을 우아한 성능저하(Graceful degradation)라고 부른다. 그리고 이렇게 성능을 저하함으로써 작업을 계속 유지하는 시스템을 장애 허용 시스템(Fault tolerant)이라고 부른다.
            6. +
            +

            멀티 프로세서 컴퓨터는 2개 이상의 프로세서를 가지고 있다. 멀티 프로세서 시스템은 비대칭 멀티프로세싱(Asymmetric multiprocessing)과 대칭 멀티프로세싱(Symmetric multiprocessing) 두 가지로 나뉜다. 비대칭 멀티프로세싱은 관료주의적인 회사다. 보스 프로세서(Boss processor)가 시스템을 제어하고, 다른 프로세서들은 보스의 지시를 받게 된다. 이렇게 하면 부하 분산(Load balancing)을 효율적으로 할 수 있다. 대신 보스 프로세서가 작동을 멈추면 일꾼 프로세서들도 멈추게 된다. 대칭 멀티프로세싱은 보스가 없는 자유로운 회사다. 모든 프로세서들은 하나의 메모리를 공유하고, 동일한 작업을 병렬적으로 수행한다. 만약 프로세서에 이상이 생겨 작동을 멈춰야 한다면 자신이 수행하던 작업을 다른 프로세서들에게 나눠주고 자신만 재부팅한다. 재부팅 후 문제가 해결된다면 다시 작업을 나눠 받는다. 비대칭 멀티프로세싱 시스템의 단점을 보완할 수 있는 아키텍처이기 때문에 대부분의 컴퓨터 시스템은 대칭 멀티프로세싱을 사용한다.

            +

            멀티 프로세서 시스템의 CPU들은 각자의 레지스터와 캐시를 갖고 있다. 만약 CPU가 여러 개라면 돌아가면서 작업을 해야 하는데, 그러면 다른 CPU가 작업을 하는 동안 다른 CPU들은 놀게 된다.

            +
            +---------------+  +---------------+  +---------------+
            +| CPU 0         |  | CPU 1         |  | CPU 2         |
            +| +-----------+ |  | +-----------+ |  | +-----------+ |
            +| | registers | |  | | registers | |  | | registers | |
            +| +-----+-----+ |  | +-----+-----+ |  | +-----+-----+ |
            +|       |       |  |       |       |  |       |       |
            +| +-----+-----+ |  | +-----+-----+ |  | +-----+-----+ |
            +| |   cache   | |  | |   cache   | |  | |   cache   | |
            +| +-----+-----+ |  | +-----+-----+ |  | +-----+-----+ |
            +|       |       |  |       |       |  |       |       |
            ++-------+-------+  +-------+-------+  +-------+-------+
            +        |                  |                  |
            +        +------------------+------------------+
            +                           |
            +                       +---+----+
            +                       | memory |
            +                       +--------+
            +
            +

            A Dual-Core Design

            +

            CPU가 늘어나면 프로세서간 통신을 하는 데 많은 비용이 들기 때문에 효율이 계속 좋아지지는 않는다. x축을 CPU 수, y축을 성능이라고 하면 그래프는 로그함수 형태로 나타난다. 조별 과제를 생각해보자. 내 아이큐 150, 네 아이큐 150을 합치면 총 300이지만, 머리가 많다고 좋은 것은 아니다. 사람이 늘어날수록 커뮤니케이션의 어려움은 커진다.

            +

            최근 CPU 설계 트렌드는 하나의 칩(Chip)에 코어(Cores)를 늘리는 것이다. 이러한 멀티 프로세서 시스템을 멀티코어(Multicore)라고 부른다. 코어는 동일한 성능의 CPU 여러 개를 1개의 칩 속에 집접한 것이라고 보면 된다. 칩 내부의 통신(On-chip communication)이 칩 사이의 통신(Between-chip communication)보다 더 빠르기 때문에 여러 개의 칩에 하나의 코어만 두는 시스템보다 더 효율적이다. 뿐만 아니라 하나의 칩에 여러 코어를 담으면 전력을 더 적게 사용한다.

            +
            +---------------+   +---------------+
            +| CPU core 0    |   | CPU core 1    |
            +| +-----------+ |   | +-----------+ |
            +| | registers | |   | | registers | |
            +| +-----+-----+ |   | +-----+-----+ |
            +|       |       |   |       |       |
            +| +-----+-----+ |   | +-----+-----+ |
            +| |   cache   | |   | |   cache   | |
            +| +-----+-----+ |   | +-----+-----+ |
            +|       |       |   |       |       |
            ++-------+-------+   +-------+-------+
            +        |                   |
            +        +---------+---------+
            +                  |
            +              +---+----+
            +              | memory |
            +              +--------+
            +
            +

            위 다이어그램은 듀얼 코어 시스템이다. 각 코어는 자신만의 레지스터와 로컬 캐시를 갖는다. (하나의 캐시를 공유하기도 한다.)

            +

            Clustered Systems

            +

            멀티프로세서 시스템의 일종인 클러스터 시스템(Clustered system)은 여러개의 CPU을 모아 놓은 구조다. 클러스터 시스템은 여러개의 개별 시스템(또는 노드)들이 하나로 모여있다는 점에서 앞서 설명한 멀티프로세서 시스템과는 조금 다르다. 멀티프로세서 시스템은 여러CPU가 하나의 시스템을 이루는 것이지만, 클러스터 시스템은 여러 독립적인 시스템이 모여 하나의 시스템을 이루는 것이다. 이런 시스템을 약결합(Loosely coupled)라고 부르며, 각 노드들은 싱글 프로세서 시스템일수도 있고, 멀티코어 시스템일 수도 있다.

            +

            클러스터의 정의가 딱 명확히 정해져 있지는 않다. 단지 클러스터 컴퓨터들이 하나의 저장소를 공유하고, 이를 LAN(Local-Area Network)과 같은 네트워크로 연결한 시스템을 보통 클러스터 시스템이라고 부른다. 클러스터링은 고가용성(High-availability) 서비스를 제공하기 위해 사용되며, 단일 컴퓨터보다 훨씬 저렴하게 비슷한 성능을 낼 수 있다.

            +

            클러스터 시스템은 비대칭 클러스터링(Asymmetric clustering)과 대칭 클러스터링(Symmetric clustering)으로 나뉜다. 비대칭 클러스터링에서 하나의 장비는 상시 대기 모드(Hot-standby mode)로 작동하며, 서버를 동작시키고 있는 다른 노드들을 모니터링할 뿐 별도의 작업은 수행하지 않는다. 만약 서버에 문제가 생기면 이 상시 대기 노드가 서버로서 작동하게 된다. 대칭 클러스터링은 두개 이상의 노드가 작업을 수행하는 동시에 다른 노드들을 모니터링하는 구조다. 이러한 구조는 하드웨어의 자원을 최대로 사용할 수 있어 더 효율적이다.

            +

            클러스터 시스템은 여러개의 컴퓨터 시스템이 네트워크로 연결되어 있는 구조이기 때문에 고성능 컴퓨팅 환경(High-performance computing environments)을 제공할 수 있다. 다만 단일 시스템에 비해 유지보수가 힘들고, 시스템의 성능이 네트워크 환경에 많은 영향을 받는다는 단점이 있다.

            +

            Operating System Structure

            +

            운영체제의 가장 중요한 부분 중 하나는 멀티프로그램(Multiprogram) 능력이다. 멀티프로그래밍(Multiprogramming)은 여러 프로그램을 메모리에 로드해 두고 한 프로세스가 대기 상태가 되면 다른 프로세스의 작업을 수행하는 시스템이다. 이렇게 하면 CPU의 사용 효율을 높일 수 있다.

            +

            여기서 더 확장된 시스템이 시분할(Time sharing) 시스템이다. 다른 말로는 멀티태스킹(Multitasking)이라고도 부른다. 이는 프로세스마다 작업 시간을 정해두고 번갈아가면서 작업하는 방식이다. 프로세스들이 빠르게 번갈아가며 메모리를 사용하면 사용자 입장에서는 마치 동시에 작동하는 것처럼 보이게 된다. 이때는 반응 시간(Response time)을 줄이는 것이 중요하다.

            +

            시분할 시스템과 멀티프로그래밍 시스템은 여러 작업들을 동시에 메모리에 올리는 방식이다. 때문에 운영체제는 메모리에 자리가 없는 경우를 고려해 어떤 작업을 먼저 처리할지 정해야한다. 이러한 과정을 작업 스케줄링(Job scheduling), CPU 스케줄링(CPU Scheduling)이라고 하는데, 이건 5장에서 다룬다. 만약 메모리를 너무 많이 사용하게 되는 경우, 반응 시간을 줄이기 위해 가상 메모리(Virtual memory)를 사용한다. 가상 메모리는 보조기억장치의 일부를 메인 메모리처럼 사용하는 기술로, 실제 물리 메모리(Physical memory)보다 더 큰 프로그램을 구동할 수 있도록 해준다.

            +

            Operating-System Operations

            +

            앞서 언급했듯이 운영체제는 인터럽트 주도적이다. 인터럽트가 없다면 시스템은 조용히 인터럽트를 기다린다. 만약 사용자의 프로그램이 멋대로 하드웨어에 접근해 인터럽트를 보낸다면 큰 문제가 생기길 것이다. 운영체제와 사용자는 컴퓨터의 하드웨어, 소프트웨어 자원을 공유하기 때문에 사용자 프로그램이 오류를 일으키지 않도록 방지해야 한다.

            +

            Dual-Mode and Multimode Operation

            +

            운영체제는 사용자 프로그램이 함부로 시스템에 접근하지 못하도록 모드(Mode)를 나눠둔다. 유저 모드(User mode)와 커널 모드(Kernel mode)가 그것이며, 하드웨어의 모드 비트(Mode bit)가 0은 커널 모드, 1은 유저 모드임을 가리킨다.

            +
            user process
            ++-----------------------------------------------------------------------------------------+
            +| +------------------------+   +-------------------+          +-------------------------+ |
            +| | user process executing +-->| calls system call |          | return from system call | | user mode
            +| +------------------------+   +--------------+----+          +-------------------------+ | (mode bit = 1)
            ++---------------------------------------------|--------------------^----------------------+
            +==============================================|====================|======================================
            ++---------------------------------------------|--------------------|----------------------+
            +|                                             | trap               | trap                 |
            +|                                             v mode bit = 0       | mode bit = 1         | kernel mode
            +|                                         +------------------------+----+                 | (mode bit = 0)
            +|                                         |      calls system call      |                 |
            +|                                         +-----------------------------+                 |
            ++-----------------------------------------------------------------------------------------+
            +kernel
            +
            +

            이러한 이중 모드(Dual-mode) 방식을 사용하면 나쁜 의도를 가진 사용자로부터 운영체제, 하드웨어를 비롯한 시스템과 사용자를 보호할 수 있다. 하드웨어는 커널 모드일 때만 특권 명령(Privileged instructions)를 실행한다. 만약 유저 모드에서 특권 명령을 실행하려 한다면 하드웨어는 이 동작을 막고 운영체제에게 트랩을 보낼 것이다. 유저 모드에서 합법적으로(?) 커널 모드의 기능을 호출하고 싶다면 시스템 콜(System call)이라는 인터페이스를 통해야 한다.

            +

            Timer

            +

            운영체제는 사용자의 프로그램이 제어권을 운영체제에게 넘겨주지 않는 상황을 방지하기 위해 타이머(Timer)를 사용한다. 타이머는 운영체제에게 제어권을 보장하기 위해 특정 주기에 인터럽트를 발생시킨다. 운영체제는 카운터를 설정하고, 매 틱(Ticks)마다 감소시킨다. 그렇게 카운터가 0에 도달하면 인터럽트가 발생한다.

            +

            Process Management

            +

            디스크에 있으면 프로그램, 메모리에 로드되면 프로세스다. 프로그램은 하나지만 프로세스는 여러 개일 수 있다. 강조할 점은, 프로그램은 디스크에 저장되어 있는 수동적(Passive) 존재인 반면 프로세스는 능동적(Active) 존재다. 또한 프로세스는 프로그램이 어디까지 실행되었는지 북마크하는 프로그램 카운터(Program counter)를 가지고 있다. 싱글쓰레드(Single-thread) 프로세스는 하나의 프로그램 카운터를 가지고 있으며, 멀티쓰레드(Multi-threads) 프로세스는 여러개의 프로그램 카운터를 가지고 있다.

            +

            운영체제는 프로세스 관리를 위해 CPU에게 프로세스와 쓰레드를 스케줄링하고, 프로세스를 생성하거나 제거하는 활동을 한다. 뿐만 아니라 일시정지하거나 재실행하고, 프로세스의 동기화(Synchronization)와 통신도 제공한다.

            +

            Memory Management

            +

            메인 메모리는 현대 컴퓨터 시스템의 핵심이며, 방대한 바이트의 배열이다. 그리고 각 바이트는 그들만의 주소를 가지고 있다. 이후 프로그램이 실행될 때 프로그램은 절대 주소(Abolute addresses)로 매핑(Mapping)되어 메모리에 로드된다. 메모리 관리는 여러 요인을 고려해야 하는 작업이며, 특히 시스템의 하드웨어 설계를 신경써야 한다.

            +

            운영체제는 메모리 관리를 위해 메모리의 어떤 부분이 어디에 쓰이는지, 누가 사용하는지 추적하고, 어떤 프로세스와 데이터가 메모리의 안팎으로 옮겨질지 결정한다. 또한 메모리 공간을 할당하고 해제하는 것도 운영체제가 하는 일이다.

            +

            Storage Management

            +

            운영체제는 저장장치의 물리적 속성을 추상화해 파일(File)이라는 논리적 저장 단위로 정의하며, 파일을 물리적 매체(Physical media)에 담거나 저장장치의 파일에 접근하기도 한다.

            +

            File-System Management

            +

            파일 관리는 운영체제가 하는 일 중 가장 눈에 잘 보이는 요소다. 운영체제는파일을 생성, 제거하며, 당연히 읽기, 쓰기도 한다.

            +

            Mass-Storage Management

            +

            프로그램은 디스크에 담겨 있으며, 메인 메모리에 로드되어 실행된다. 많은 사람들이 제3의 저장 장치(Tertiary storage devices)를 사용한다. 저장 장치들은 WORM(Write-Once, Read-Many-Times)과 RW(Read-Write) 형식에 차이가 있다. 한번쯤 봤을 NTFS, FAT가 파일 저장 형식이며, 이를 파일 시스템(File system)이라고 부른다. 파일 시스템이 다르면 읽기는 가능하지만 쓰기가 불가능하다.

            +

            Caching

            +

            캐싱(Caching)은 컴퓨터 시스템에 있어 정말 중요한 부분이다. 캐시는 굉장히 빠르고 작은 저장장치이며, 캐싱은 캐시 메모리를 사용해 컴퓨터의 속도를 높이는 기술이다. 데이터를 디스크에서 직접 가져오는 것은 너무 느리기 때문에 캐시에 자주 사용될 것 같은 데이터를 미리 담아두고, CPU나 디스크가 캐시의 데이터를 참조할 수 있도록 한다. 파일의 중복성이 증가하지만, 속도 역시 증가한다. 캐싱은 지역성(Locality) 원리를 사용한다. 지역성은 시간지역성(Temporal locality)과 공간지역성(Spatial Locality)으로 나뉜다. 시간지역성은 한 번 접근한 데이터에 다시 접근할 확률이 높다는 것이다. 공간지역성은 특정 데이터와 가까운 메모리 주소에 있는 다른 데이터들에도 접근할 가능성이 높다는 것이다.

            +
            for (int i = 0; i < 10; i++) { ... }
            +
            +

            위 코드에서 변수 i가 그 예시다.

            +

            공간지역성은 특정 데이터와 가까운 메모리 주소에 있는 다른 데이터들에도 접근할 가능성이 높다는 것이다. 다음 코드를 보자.

            +
            for (int i = 0; i < 10; i++) {
            +  arr[i] += 1;
            +}
            +
            +

            배열 변수 arr의 0번 요소부터 순서대로 9번 요소까지 접근한다. 메모리 공간에 배열의 요소들은 그 순서대로 붙어 할당되니까 실제로 가까운 메모리 공간에 연속적으로 접근하고 있다. 캐시는 한 메모리 주소에 접근했을 때 그 주변의 메모리 주소도 함께 가져온다.

            +

            캐시에 대한 보다 자세한 내용은 캐시가 동작하는 아주 구체적인 원리에서 설명했다.

            +

            I/O System

            +

            운영체제는 모든 입출력장치를 파일로 취급한다. 오직 장치드라이버(Device driver)만이 장치의 자세한 정보를 알고 있다.

            +

            Protection and Security

            +

            컴퓨터 시스템은 여러 사람들이 사용하기 때문에 보호와 보안도 매우 중요하다. 운영체제는 내외부로부터 컴퓨터를 위험하게 만드는 요소를 막기 위해 다양한 활동을 한다. (유저 모드와 커널 모드를 나눈 것도 보호의 일종이다.) 권한 확대(Privilege escalation)는 컴퓨터 시스템의 권한을 여러 층으로 나누고, 사용자의 권한을 구분해 어떤 행동이나 기준에 따라 사용자의 권한을 상승시키는 시스템이다. 참고로 권한 확대는 수직 권한 확대와 수평 권한 확대로 나눌 수 있는데, 가령 임의의 코드를 실행시켜 더 높은 권한을 얻는 행위는 수직 권한 확대, 안드로이드 루팅, iOS 탈옥은 수평 권한 확대라고 한다.

            +

            Kernel Data Structures

            +

            커널 구현에는 기본적인 리스트(List), 스택(Stack), 큐(Queue), 링크드리스트(Linked list) 등의 자료구조가 사용된다. 특히 트리(Tree)는 상당히 효율적인 O(logn)O(\log n)의 시간복잡도를 가질 수 있기 때문에 자주 사용된다.

            +

            Computing Environments

            +

            모바일 컴퓨팅(Mobile computing) 환경은 컴퓨터의 접근성을 높였고, 다양한 센서를 통해 사용자와의 인터페이스를 확장시켰다. 또한 분산형 컴퓨팅(Distributed computing), 클라이언트-서버 컴퓨팅(Client-Server computing), P2P 컴퓨팅(Peer-to-Peer computing) 등 다양한 컴퓨팅 환경이 있다. 특히 클라우드 컴퓨팅(Cloud computing)은 AWS(Amazon Web Service)를 통해 상당히 잘 알려졌다. 현대 컴퓨팅 환경의 가장 큰 특징을 꼽자면 휴대성, 가상화, 멀티코어가 있다.

            +

            Open-Source Operating Systems

            +

            세상에는 많은 오픈소스 운영체제들이 있다. 당장 깃허브에서도 리눅스의 코드를 찾아볼 수 있다. 오픈소스 운영체제 개발에는 누구나 참여할 수 있고, 이를 이용해 새로운 운영체제를 만들 수도 있다.

            + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.2

            +

            System Structures

            +
            +
            +
            + + +
            + +
            +
            +

            차이를 중심으로 살펴본 UI디자인과 UX디자인

            +

            UI는 심미성, UX는 사용성?

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/6.html b/article/6.html new file mode 100644 index 0000000..05f8c8b --- /dev/null +++ b/article/6.html @@ -0,0 +1,246 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦕 공룡책으로 정리하는 운영체제 Ch.2 +

            + +

            + System Structures +

            + + + + +
            +
            Table of Contents +

            +
            +

            챕터1에서 다룬 시스템에 대해 보다 자세히 다루는 챕터인데, 그렇게 어렵지는 않다. 복잡한 내용이 별로 없어서 쉽게 읽을 수 있다.

            +

            Operating-System Services

            +

            운영체제는 사용자와 시스템에게 다양한 서비스를 제공한다.

            +
            +-----------------------------------------------------------------------------------------------------------------------+
            +| user and other system programs                                                                                        |
            ++-----------------------------------------------------------------------------------------------------------------------+
            +| +-----+-------+-------------+                                                                                         |
            +| | GUI | batch | commandline |                                                                                         |
            +| +-----+-------+-------------+                                                                                         |
            +| | user interfaces           |                                                                                         |
            +| +---------------------------+                                                                                         |
            ++-----------------------------------------------------------------------------------------------------------------------+
            +| system calls                                                                                                          |
            ++-----------------------------------------------------------------------------------------------------------------------+
            +| +-------------------------------------------------------------------------------------------------------------------+ |
            +| | +-------------------+ +----------------+ +-------------+ +---------------+ +---------------------+ +------------+ | |
            +| | | program execution | | I/O operations | | file systems| | communication | | resource allocation | | accounting | | |
            +| | +-------------------+ +----------------+ +-------------+ +---------------+ +---------------------+ +------------+ | |
            +| | +-----------------+                                                                   +-------------------------+ | |
            +| | | error detection |                                                                   | protection and security | | |
            +| | +-----------------+                                                                   +-------------------------+ | |
            +| +------------------------------------------------------services-----------------------------------------------------+ |
            ++----------------------------------------------------operating system---------------------------------------------------+
            +| hardware                                                                                                              |
            ++-----------------------------------------------------------------------------------------------------------------------+
            +
            +
              +
            • UI(User Interface): UI는 말그대로 사용자와 컴퓨터 시스템이 만나는 지점을 말한다. 키보드 타이핑이나 마우스 클릭과 같은 행동으로 사용자는 컴퓨터를 조작할 수 있다. 인터페이스는 크게 CLI(Command-Line Interface)와 배치 인터페이스(Batch interface), 그리고 GUI(Graphical User Interface)로 나눌 수 있다. CLI는 사용자가 텍스트 명령을 통해 명령을 내리는 인터페이스다. 그리고 이러한 인터페이스를 제공하는 프로그램을 셸(Shell)이라고 부른다.과거 MS-DOS나 애플소프트 베이직이 CLI를 기반으로 했다. 배치 인터페이스는 명령을 파일에 넣어두고, 파일이 실행되면서 명령을 실행하는 인터페이스다. CLI가 널리 쓰이기 이전, 40~60년대 컴퓨터는 이러한 방식을 사용했다. GUI는 현재 가장 흔하게 찾아볼 수 있는 인터페이스다. GUI 환경에서 사용자는 키보드 타이핑, 마우스 클릭, 손가락 터치 등 다양한 방법으로 화면에 띄워진 그래픽을 조작하며, 이를 통해 컴퓨터에게 명령을 내린다.
            • +
            • 프로그램 실행(Program execution): 시스템은 프로그램을 메모리에 로드하고, 이를 실행할 수 있어야 한다. 또한 프로그램은 정상적으로든 그렇지 않든 실행을 끝낼 수 있어야 한다.
            • +
            • 입출력 명령(I/O operations): 만약 프로그램이 입출력을 필요로한다면, 운영체제는 입출령 명령을 수행해야 한다. 이때 효율과 보안을 위해 운영체제는 사용자가 직접 입출력 장치를 조작하지 않고, 자신을 거치도록한다.
            • +
            • 파일 시스템 조작(File-system manipulation): 파일을 쓰고, 읽고, 만들고, 지운다. 또한 사용자가 파일에 접근하지 못하도록 막기도 한다.
            • +
            • 통신(Communications): 어떤 프로세스가 다른 프로세스와 정보를 교환해야 하는 상황에서 운영체제는 공유 메모리(Shared memory)나 메세지 패싱(Message passing)이라는 방법을 사용한다. 공유 메모리는 여러개의 프로세스가 메모리의 한 부분을 공유하도록 하는 것이고, 메세지 패싱은 프로세스 간에 정보 패킷(Packets)을 주고 받는 것을 말한다. (공유 메모리 방식보다 메세지 패싱 방식의 속도가 더 느리다.)
            • +
            • 에러 탐지(Error detection): 운영체제는 CPU나 메모리와 같은 하드웨어, 입출력장치, 그리고 사용자 프로그램 등에서 일어나는 에러를 탐지하고, 바로 잡아야 한다.
            • +
            +

            운영체제는 사용자에게 직접적인 도움은 안 되지만, 시스템을 위한 작업도 수행한다.

            +
              +
            • 자원 할당(Resource allocation): 여러 사용자나 여러 작업을 동시에 처리해야 한다면, 컴퓨팅 자원은 각각 잘 배분되어야 한다. 이러한 상황에서 운영체제는 다양한 종류의 자원을 관리한다.
            • +
            • 회계(Accounting): 시스템은 어떤 유저가 어떤 종류의 자원을 얼마나 사용하고 있는지 계속 추적해야 한다. 이 기록은 회계나 사용량 통계를 위해 사용될 수 있다. 직역하면 회계지만, 대략 관리, 통계 정도로 받아들이면 될 것 같다.
            • +
            • 보호와 보안(Protection and security): 몇 번을 말해도 모자랄 정도로 중요한 부분이다. 인텔 CPU의 멜트다운, 스펙터 취약점 사태를 생각해보자…
            • +
            +

            System Calls

            +

            시스템 콜은 커널과 사용자 프로그램을 이어주는 인터페이스 역할을 한다. 좀 생소하게 느껴질 수도 있겠지만, 그냥 로우 레벨 작업을 하는 코드라고 생각하면 된다.

            +
                                       +------------------+
            +                      +----+ user application |<----+
            +               open() |    +------------------+     |
            +user                  v                             |
            +mode    +-------------------------------------------+--------------+
            +--------+ system call interface                                    |
            +kernel  +---+------------------------------------------------------+
            +mode        |     +-----+                                      ^
            +            +---->| ... |                  open()              |
            +                  +-----+                    implementation    |
            +                i |     +------------------> of open()         |
            +                  +-----+                    system call       |
            +                  | ... |                    ...               |
            +                  +-----+                    return -----------+
            +
            +

            사용자 프로그램이 디스크에 있는 파일을 연다는 것은 파일 시스템에 접근한다는 의미다. 시스템에 접근하기 위해서는 커널 모드로 전환되어야 하는데, 이때 시스템 콜을 사용한다. 메모리의 특정 주소 범위에는 어떤 동작들이 할당되어 있다. 이것을 시스템 콜 테이블(System call table)이라고 부르며, 인터럽트 벡터(Interrupt vector)라고도 부른다. 예를 들어 fopen() 함수를 호출한다면, 운영체제는 파일을 여는 함수를 찾기 위해 시스템 콜 테이블을 참조한다. 시스템 콜 테이블은 메모리 주소의 모음인데, 해당 메모리 주소는 인터럽트 서비스 루틴(Interrupt service routine)을 가리키고 있다. 인터럽트 서비스 루틴은 일반적으로 C로 짜여진 코드이며, 시스템 콜 테이블이 가리키는 특정 메모리 주소가 구체적으로 어떤 동작을할지 정의해놓은 것이다.

            +

            시스템 콜에는 fork(), exit(), read(), write()와 같은 함수들이 있다. 하지만 개발자가 이것을 직접 조작하는 것은 불편하고 위험한 일이므로, 표준 라이브러리(Standard library)를 사용한다. stdio.h가 그 일종이다.

            +

            사용자 프로그램이 운영체제에게 매개변수를 넘기는 방법은 3가지가 있다.

            +
              +
            1. Call by value: 매개변수의 값 자체를 복사해서 CPU 레지스터에 전달한다.
            2. +
            3. Call by reference: 값의 메모리 주소를 전달한다. 많은 값을 전달한다면 이렇게 하는 것이 효율적이다.
            4. +
            5. 프로그램에을 통해 스택(Stack)에 매개변수를 추가하고, 운영체제를 통해 값을 뺀다.
            6. +
            +

            Types of System Calls

            +

            시스템 콜은 크게 6가지로 분류할 수 있다.

            +
              +
            • 프로세스 제어: end, abort, load, execute
            • +
            • 파일 관리: create, delete, open, close, read, write
            • +
            • 장치 관리: read, write, request, release
            • +
            • 정보 유지: get/set time or date
            • +
            • 통신: send/receive messages, transfer status
            • +
            • 보호
            • +
            +

            Operating System Structure

            +

            현대 운영체제는 계층을 나눠서 시스템을 관리한다.

            +

            Simple Structure

            +
            +-------------------------------------+
            +| application program                 |
            ++--+-------------------------------+--+
            +   |                               |
            +   v                               |
            ++------------------------------+   |
            +| resident system program      |   |
            ++--+------------------------+--+   |
            +   |                        |      |
            +   v                        |      |
            ++-----------------------+   |      |
            +| MS-DOS device drivers |   |      |
            ++--+--------------------+   |      |
            +   |                        |      |
            +   v                        v      v
            ++-------------------------------------+
            +| ROM BIOS device drivers             |
            ++-------------------------------------+
            +
            +

            과거에는 사실상 계층이 구분되어 있지 않았다. MS-DOS에서는 사용자 프로그램이 입출력 루틴에 접근해 디스플레이와 디스크 드라이브에 직접 쓰기를 할 수 있었다. 따라서 만약 사용자 프로그램에 문제가 생기면 전체 시스템에 문제가 생겼다. UNIX 시스템은 이것을 개선했다.

            +
            +------------------------------------------------------------------+
            +| (the users)                                                      |
            ++------------------------------------------------------------------+
            +| shells and commands                                              |
            +| compilers and interpreters                                       |
            +| system libraries                                                 |
            ++------------------------------------------------------------------+ -+
            +| system-call interface to the kernel                              |  |
            ++------------------------------------------------------------------+  |
            +| signals terminal       file system              CPU scheduling   |  |
            +| handling               swapping block I/O      page replacement  |  | kernel
            +| character I/O system   system                  demand paging     |  |
            +| terminal drivers       disk and tape drivers   virtual memory    |  |
            ++------------------------------------------------------------------+  |
            +| kernel interface to the hardware                                 |  |
            ++----------------------+--------------------+----------------------+ -+
            +| terminal controllers | device controllers | memory controllers   |
            +| terminals            | disks and tapes    | physical memory      |
            ++----------------------+--------------------+----------------------+
            +
            +

            전통적인 UNIX 시스템 구조는 MS-DOS에 비해 기능이 분리되었지만, 여전히 하나의 계층이 너무 많은 일을 했다. 하드웨어 계층 위, 사용자 계층 아래에 있는 커널이 모든 기능을 제공했다. 이러한 모놀리딕(Monolithic) 구조는 구현과 유지보수가 쉽지 않았다.

            +

            Layered Approach

            +
            +-------------------------------+
            +| layer N: user interface       |
            +| +---------------------------+ |
            +| | ...                       | |
            +| | +-----------------------+ | |
            +| | | layer 1               | | |
            +| | | +-------------------+ | | |
            +| | | | layer 0: hardware | | | |
            +| | | +-------------------+ | | |
            +| | +-----------------------+ | |
            +| +---------------------------+ |
            ++-------------------------------+
            +
            +

            운영체제를 더 세분화해 계층을 분리한 것이 계층적 접근(Layered approach) 방식이다. 가장 아래에 있는 계층(레이어 0)은 하드웨어고, 가장 높은 계층(레이어 N)은 사용자 인터페이스다. 이 방식은 유지보수가 아주 편한데, 하나의 계층에만 신경쓰면 다른 계층에는 아무런 신경을 쓰지 않아도 되기 때문이다.

            +

            Microkernels

            +

            마이크로커널(Microkernel)은 커널에서 핵심적인 요소만 남긴 가벼운 커널을 말한다. 커널이 커질수록 문제가 생길 가능성이 높아지고, 유지보수가 힘들어지기 때문에 커널을 더 가볍게 만들 필요가 있었다. 마이크로커널은 코드 양이 훨씬 적어 컴파일, 테스트 시간이 비교적 짧고, 다른 시스템에 이식(Porting)하기도 쉽다. 다만 시스템 프로그램을 추가해 기능을 늘리려하면 속도가 느려진다.

            +

            OS X의 커널(Darwin)의 일부가 마이크로커널 Mach를 기반으로 만들어졌으며, IoT에도 마이크로커널이 사용된다.

            +

            Modules

            +

            모듈은 커널을 확장하기 위한 기술로, OOP에서 말하는 그 모듈화와 같은 개념이다. 프로세스에 실시간으로 모듈을 붙여 작동시킬 수 있고, 각 기능들을 독립적으로 관리할 수 있어 효과적으로 시스템을 유지할 수 있다. 장치 드라이버는 모두 모듈로 구현되어 있으며, 윈도우에서 .dll파일이 바로 모듈이다.

            +

            Hybrid Systems

            +

            스마트폰은 OS 구조의 최신판이라고 할 수 있다. 하이브리드 시스템은 커널의 핵심만 남기고 나머지는 따로 구현한 시스템이다. OS X의 경우 BSD가 핵심이지만 나머지는 모두 애플이 자체 구현했다. 안드로이드는 리눅스 커널위에 자체 구현한 라이브러리를 올린 시스템이다.

            + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.3

            +

            Process Concept

            +
            +
            +
            + + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.1

            +

            Overview

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/7.html b/article/7.html new file mode 100644 index 0000000..e6540a8 --- /dev/null +++ b/article/7.html @@ -0,0 +1,187 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦕 공룡책으로 정리하는 운영체제 Ch.3 +

            + +

            + Process Concept +

            + + + + +
            +
            Table of Contents +

            +
            +

            본격적으로 프로세스에 대해서 다루기 시작한다. Ch.1 Overview에서 나왔듯이 디스크에 있는 것은 프로그램, 메모리에 로드된 것은 프로세스라고 한다. 프로세스는 Stack, Heap, Data, Code로 나뉜다.

            +
            +---------------+ max
            +|     stack     |
            ++-------+-------+
            +|       |       |
            +|       v       |
            +|               |
            +|       ^       |
            +|       |       |
            ++-------+-------+
            +|     heap      |
            ++---------------+
            +|     data      |
            ++---------------+
            +|     text      |
            ++---------------+ 0
            +
            +

            Process State

            +

            프로세스의 상태는 현재 활동에 따라 달라진다.

            +
                                     +------------interrupt------------+
            +                         v                                 |
            ++-----+              +-------+                        +----+----+          +------------+
            +| new +---admitted-->| ready +---scheduler dispatch-->| running +---exit-->| terminated |
            ++-----+              +-------+                        +----+----+          +------------+
            +                         ^           +---------+           |
            +                         +-----------+ waiting |<----------+
            +         I/O or event completion     +---------+     I/O or event wait
            +
            +
              +
            • New: 프로세스가 처음 생성되었을 때.
            • +
            • Ready: 프로세스가 프로세서에 할당되기를 기다릴 때.
            • +
            • Running: 프로세스가 할당되어 실행될 때.
            • +
            • Waiting: 프로세스가 이벤트를 기다릴 때.
            • +
            • Terminated: 프로세스가 실행을 마쳤을 때.
            • +
            +

            Process Control Block (PCB)

            +

            각각의 프로세스는 자신의 정보 묶음인 PCB를 가지고 있다. PCB에는 프로세스 상태와 프로그램 카운터, 메모리 한계, 레지스터 정보 등이 담겨있다.

            +
              +
            • Process state: 프로세스의 상태.
            • +
            • Program counter: 해당 프로세스가 이어서 실행해야 할 명령의 주소를 가리키는 카운터.
            • +
            • CPU registers: 프로세스가 인터럽트 이후 올바르게 작업을 이어가기 위해 참조하는 CPU 레지스터 값.
            • +
            • CPU-scheduling information: 프로세스의 중요도, 스케줄링 큐 포인터 등 스케줄링 파라미터 정보.
            • +
            • Memory-management information: base, limit 레지스터 값, 페이지 테이블 등 메모리 시스템 정보.
            • +
            • Accounting information: 사용된 CPU 총량, 프로세스 개수, 시간 제한 등.
            • +
            • I/O status information: 프로세스에 할당된 입출력 장치 목록, 열린 파일 목록 등.
            • +
            +

            Threads

            +

            프로세스를 쪼개 하나의 프로세스 안에서 동시에 여러 작업을 처리할 수 있다. 지금까지는 싱글 스레드 프로세스를 전제하고 살펴봤다. 싱글 스레드 프로세스는 한번에 하나의 작업만 할 수 있다. 가령 워드 프로세서 프로그램을 실행한다면, 글자를 타이핑할 때 같은 프로세스 안에서 동작하는 문법 교정기가 동시에 동작할 수 없다. 챕터 4에서 자세히 다룬다.

            +

            Process Scheduling

            +

            멀티프로그래밍의 목적은 CPU를 최대로 사용하기 위해 항상 일부 프로세스를 실행하는 것이다. 타임쉐어링의 목적은 프로세스 간에 CPU를 자주 전환함으로써 사용자가 각 프로그램이 실행되는 동안 서로 상호작용할 수 있도록 만드는 것이다. 이러한 목적을 달성하기 위해 프로세스 스케줄러는 CPU에서의 프로그램을 실행을 위해 사용 가능한 프로세스를 선택하며, 어떤 프로세스를 프로세서에 할당할 것인가 결정하는 일을 프로세스 스케줄링이라고 한다.

            +

            Scheduling Queues

            +

            프로세스가 시스템에 들어오면 잡 큐(Job queue)에 들어간다. 잡 큐는 시스템의 모든 프로세스로 구성되어있다. 메인 메모리에서 실행을 기다리는 ready 상태의 프로세스들은 레디 큐(Ready queue)에 쌓인다. 입출력 장치를 기다리는 프로세스들은 디바이스 큐(Device queue)로 들어간다.

            +

            Schedulers

            +

            레디 큐에 프로세스를 옮기는 것은 잡 스케줄러, 또는 Long-term 스케줄러라고 한다. 프로세스를 프로세서에 할당하는 것은 CPU 스케줄러, 또는 Short-term 스케줄러라고 한다. Long-term 스케줄러는 CPU 밖에서 가끔 수행된다. Short-term 스케줄러는 그 반대다.

            +

            Context Switch

            +

            프로세스가 실행되다가 인터럽트가 발생해 운영체제가 개입하여 프로세서에 할당된 프로세스를 바꾸는 것을 말한다. 시스템 콜을 사용해야 하는 경우 프로세스가 자체적으로 처리할 수 없기 때문에 운영체제가 개입해야 한다. 프로세서가 다른 프로세스로 스위치할 때, 시스템은 작업중이던 프로세스의 상태를 저장하고 새로운 프로세스의 상태를 로드한다. 컴퓨터과학에서 컨텍스트는 내 시스템에서 활용 가능한 모니터링된 정보들을 의미한다. 프로세서 입장에서 컨텍스트는 PCB이기 때문에 PCB 정보가 바뀌는 것을 컨텍스트 스위치라고 부른다. 컨텍스트 스위치는 오버헤드가 발생하는 작업이기 때문에 너무 자주 일어나면 성능을 저하한다.

            +

            Operations on Processes

            +

            대부분의 시스템에서 프로세스는 동시에 실행될 수 있고, 이들은 동적으로 생성되거나 삭제될 수 있다. 시스템은 프로세스 생성, 삭제 메커니즘을 제공해야 한다.

            +

            Process Creation

            +

            프로세스는 트리 구조로 되어 있다. 즉, 부모 프로세스가 자식 프로세스를 만든다. PCB에 저장된 pid값으로 프로세스를 식별하는데, 이는 운영체제가 정해준 고유 번호다. 프로세스 생성은 플라나리아 번식과 유사하다. 시스템 콜의 fork() 함수를 호출하면 부모 프로세스는 자신과 똑같은 자식 프로세스를 생성한다. 자식 프로세스는 exec()를 통해 내용을 모두 바꾼다. fork() 함수는 부모 프로세스에겐 자식 프로세스의 pid를, 자식프로세스에겐 0을 반환한다. 부모 프로세스와 자식 프로세스는 동시에 작동한다.

            +

            Process Termination

            +

            exit()를 호출하면 프로세스를 종료시킬 수 있다. 부모 프로세스가 자식 프로세스보다 먼저 종료되면 자식 프로세스는 그 상위 프로세스를 부모 프로세스로 바라본다. 자식 프로세스가 종료되었는데, 부모 프로세스가 자식 프로세스가 반환한 정보를 회수하지 않으면 자식 프로세스는 종료되었음에도 정보가 메모리에 남아 있는 좀비 프로세스가 된다.

            +

            Interprocess Communication (IPC)

            +

            프로세스는 독립적으로 동작하거나 서로 협력하며 동작할 수 있다. 협력하는 프로세스들은 통신하며 서로에게 영향을 미친다. IPC 모델에는 메시지 패싱(Message passing)과 공유 메모리(Shared memory)가 있다.

            +

            Message Passing

            +

            메시지 패싱은 우편이다. 송신 프로세스가 정보를 받는 수신 프로세스에게 커널을 통해 정보를 전달하며, 수신 프로세스도 커널에 접근해 정보를 수신한다. 메시지 패싱은 컨텍스트 스위치가 발생하기 때문에 속도가 느리다. 다만 커널이 기본적인 기능을 제공하므로 공유 메모리 방식에 비해선 구현이 쉽다.

            +

            Shared Memory

            +

            공유 메모리는 게시판이다. 특정 메모리 공간을 두 프로세스가 함께 사용하며 정보를 주고 받는다. 커널을 거치지 않기 때문에 속도가 빠르지만 메모리에 동시 접근하는 것을 방지하기 위해 프로그래머가 따로 구현을 해줘야 한다.

            +

            Producer-Consumer Problem

            +

            협력하는 프로세스 중 정보를 생산하는 프로세스를 생산자(Producer), 정보를 소비하는 프로세스를 소비자(Consumer)라고 부른다. 생산자-소비자 문제는 두 프로세스가 동시에 동작할 때 일어나는 이슈를 말한다. 보통 정보가 생산되는 속도가 소비하는 속도보다 빠르기 때문에 동기화 문제가 발생하는데, 이를 해결하기 위해 생산된 데이터를 담아두는 버퍼(Buffer)를 사용한다. 크기에 한계가 있는 버퍼를 유한 버퍼(Bounded buffer), 버퍼의 시작과 끝을 이어붙여 크기가 무한한 버퍼를 무한 버퍼(Unbounded buffer)라고 한다.

            +

            Synchronization

            +

            메시지 패싱의 동기화 문제를 해결하기 위해 blocking 방식과 non-blocking 방식이 사용된다.

            +
              +
            • Blocking send: 수신자가 메시지를 받을 때까지 송신자는 block된다.
            • +
            • Blocking receive: 메시지를 수신할 때까지 수신자는 block된다.
            • +
            • Non-blocking send: 송신자가 메시지를 보내고 작업을 계속한다.
            • +
            • None-blocking receive: 수신자가 유효한 메시지나 Null 메시지를 받는다.
            • +
            +

            Sockets

            +

            소켓은 서버와 클라이언트가 통신하는 방식이다. IP주소와 포트 정보가 있으면 클라이언트는 네트워크를 통해 서버 프로세스에 접근할 수 있다. RPC(Remote Procedure Calls)는 프로세스와 프로세스가 네트워크로 이어져 있을 때 발생하는 호출을 말한다. 서버와 클라이언트가 통신할 때는 IP주소와 포트를 래핑해서 Stub으로 만들어 전송한다.

            +

            Pipes

            +

            파이프는 부모 프로세스와 자식 프로세스가 통신할 때 사용하는 방식이다. 말 그대로 프로세스 사이에 파이프를 두고 정보를 주고 받는 건데, 파이프는 단방향 통신만 가능하기 때문에 양방향으로 정보를 주고 받으려면 두 개의 파이프가 필요하다. (파이프는 파일이다.) 파이프에 이름을 붙인 named pipe를 사용하면 꼭 부모-자식 관계가 아니더라도 파이프를 이용해 통신할 수 있다.

            + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.4

            +

            Multithreaded Programming

            +
            +
            +
            + + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.2

            +

            System Structures

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/8.html b/article/8.html new file mode 100644 index 0000000..bd62981 --- /dev/null +++ b/article/8.html @@ -0,0 +1,130 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦕 공룡책으로 정리하는 운영체제 Ch.4 +

            + +

            + Multithreaded Programming +

            + + + + +
            +
            Table of Contents +

            +
            +

            스레드에 대해 다루는 챕터로, 구체적인 멀티스레드 구현 방법이나 코드가 많이 나온다.

            +

            Threads

            +

            스레드는 프로세스의 작업 흐름을 말한다. 하나의 프로세스가 한 번에 하나의 작업만 수행하는 것은 싱글스레드(Single thread)이며, 하나의 프로세스가 동시에 여러 작업을 수행하는 것은 멀티스레드(Multi thread)라고 한다. 프로세서와 메모리가 발전하며 가능해진 기술이다. 멀티프로그래밍 시스템이니까 프로세스를 여러개 돌려도 되는데 굳이 스레드를 나누는 데는 이유가 있다.

            +
              +
            1. 두 프로세스가 하나의 데이터를 공유하려면 메시지 패싱이나 공유 메모리 또는 파이프를 사용해야 하는데, 효율도 떨어지고 개발자가 구현, 관리하기도 번거롭다.
            2. +
            3. 프로세스 사이 컨텍스트 스위치가 계속 일어나면 성능 저하가 발생한다. 스레드 전환에도 컨텍스트 스위치가 일어나지만 속도가 더 빠르다.
            4. +
            +

            Multithreaded Server Architecture

            +

            서버와 클라이언트 사이에도 멀티스레드를 구현한다. 클라이언트가 서버에게 요청을 보내면 서버는 새로운 스레드를 하나 생성해 요청을 수행한다. 프로세스를 생성하는 것보다 스레드를 생성하는 것이 더 빠르기 때문이다.

            +

            Multicore Programming

            +

            이렇게 멀티코어 또는 멀티프로세서 시스템을 구현할 때는 동시성(Concurrency)와 병렬성(Parallelism)을 알아야 한다. 동시성은 싱글 프로세서 시스템에서 사용되는 방식으로, 프로세서가 여러 개의 스레드를 번갈아가며 수행함으로써 동시에 실행되는 것처럼 보이게 하는 방식이다. 병렬성은 멀티코어 시스템에서 사용되는 방식으로, 여러 개의 코어가 각 스레드를 동시에 수행하는 방식이다.

            +

            User Threads and Kernel Threads

            +

            유저 스레드는 사용자 수준의 스레드 라이브러리가 관리하는 스레드다. 스레드 라이브러리에는 대표적으로 POSIX Pthreads, Win32 threads, Java threads가 있다. 커널 스레드는 커널이 지원하는 스레드다. 커널 스레드를 사용하면 안정적이지만 유저 모드에서 커널 모드로 계속 바꿔줘야 하기 때문에 성능이 저하된다. 반대로 유저 스레드를 사용하면 안정성은 떨어지지만 성능이 저하되지는 않는다.

            +

            Multithreading Models

            +

            유저 스레드와 커널 스레드의 관계를 설계하는 여러가지 방법이 있다.

            +

            Many-to-One Model

            +

            하나의 커널 스레드에 여러 개의 유저 스레드를 연결하는 모델이다. 한 번에 하나의 유저 스레드만 커널에 접근할 수 있기 때문에 멀티코어 시스템에서 병렬적인 수행을 할 수가 없다. 요즘에는 잘 사용되지 않는 방식이다.

            +

            One-to-One Model

            +

            하나의 유저 스레드에 하나의 커널 스레드가 대응하는 모델이다. 동시성을 높여주고, 멀티프로세서 시스템에서는 동시에 여러 스레드를 수행할 수 있도록 해준다. 유저 스레드를 늘리면 커널 스레드도 똑같이 늘어나는데, 커널 스레드를 생성하는 것은 오버헤드가 큰 작업이기 때문에 성능 저하가 발생할 수 있다.

            +

            Many-to-Many Model

            +

            여러 유저 스레드에 더 적거나 같은 수의 커널 스레드가 대응하는 모델이다. 운영체제는 충분한 수의 커널 스레드를 만들 수 있으며, 커널 스레드의 구체적인 개수는 프로그램이나 작동 기기에 따라 다르다. 멀티프로세서 시스템에서는 싱글프로세서 시스템보다 더 많은 커널 스레드가 만들어진다.

            +

            Two-level Model

            +

            Many-to-Many 모델과 비슷한데, 특정 유저 스레드를 위한 커널 스레드를 따로 제공하는 모델을 말한다. 점유율이 높아야 하는 유저 스레드를 더 빠르게 처리해줄 수 있다.

            +

            Thread Pools

            +

            스레드를 요청할 때마다 매번 새로운 스레드를 생성하고, 수행하고, 지우고를 반복하면 성능이 저하된다. 그래서 미리 스레드 풀에 여러 개의 스레드를 만들어두고 요청이 오면 스레드 풀에서 스레드를 할당해주는 방법을 사용한다.

            + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.5

            +

            Process Scheduling

            +
            +
            +
            + + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.3

            +

            Process Concept

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/article/9.html b/article/9.html new file mode 100644 index 0000000..2265318 --- /dev/null +++ b/article/9.html @@ -0,0 +1,399 @@ + + + + + + 🦕 공룡책으로 정리하는 운영체제 Ch.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Articles +
            +

            + 🦕 공룡책으로 정리하는 운영체제 Ch.5 +

            + +

            + Process Scheduling +

            + + + + +
            +
            Table of Contents +

            +
            +

            운영체제가 어떤 프로세스를 프로세서에 할당할 것인가 정하는 프로세스 스케줄링(Process scheduling)에 대해 다루는 챕터다. FCFS, SJF, RR 등 다양한 프로세스 스케줄링에 대해 소개한다.

            +

            Scheduling Criteria

            +

            운영체제가 프로세스를 프로세서에 할당하는 것을 디스패치(Dispatch)라고 한다. (이때 프로세스 상태가 ready에서 running으로 바뀐다.) 그리고 운영체제가 레디 큐(Ready queue)에 있는 프로세스들 중에서 어떤 프로세스를 디스패치할 것인가 정하는 것이 프로세스 스케줄링(Process scheduling)이다.

            +

            스케줄링 알고리즘에는 대표적으로 FCFS, SJF, SRF, RR 네 가지 방식이 있고, 알고리즘을 평가할 때는 수행 시간(Burst time)과 CPU 사용량(CPU utilization), 단위 시간 당 끝마친 프로세스의 수(Throughput), 하나의 프로세스가 레디 큐에서 대기한 시간부터 작업을 마칠 때까지 걸리는 시간(Turnaround time), 프로세스가 레디 큐에서 대기한 시간(Wating time), 프로세스가 처음으로 CPU를 할당받기까지 걸린 시간(Response time)을 기준으로 한다.

            +

            선점(Preemptive) 방식과 비선점(Non-Preemptive) 방식으로 나뉜다. 선점 스케줄링은 운영체제가 강제로 프로세스의 사용권을 통제하는 방식이고, 비선점 스케줄링은 프로세스가 스스로 다음 프로세스에게 자리를 넘겨주는 방식이다. 즉, 선점 스케줄링 방식에서는 CPU에 프로세스가 할당되어 있을 때도 운영체제가 개입해 다른 프로세스에게 CPU를 할당할 수 있다.

            +

            FCFS (First-Come, First-Served)

            +
              +
            • 먼저 들어온 프로세스를 먼저 프로세서에 할당하는 방식이다.
            • +
            • Queue의 FIFO(First-In First-Out)와 동일하다.
            • +
            • 구현이 쉬워서 간단한 시스템에 자주 사용된다.
            • +
            • 프로세스 처리 순서에 따라 성능이 크게 달라질 수 있다.
            • +
            • 수행 시간이 큰 프로세스가 먼저 들어오면 그 뒤에 들어온 프로세스들이 불필요하게 오랜 시간을 기다리게 되는 콘보이 효과(Convoy effect)가 발생한다.
            • +
            • 먼저 온 프로세스가 끝날 때까지 운영체제가 개입하지 않는 비선점 스케줄링 방식이다.
            • +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            ProcessBurst timeResponse timeTurnaround timeWaiting time
            P19090
            P219109
            P31101110
            +
            +----+----+----+----+----+----+----+----+----+----+----+
            +|                     P1                     | P2 | P3 |
            ++----+----+----+----+----+----+----+----+----+----+----+
            +0                                            9    10   11
            +
            +
            Average wating time=0+9+103=6.33 +\text{Average wating time} = {0 + 9 + 10 \over 3} = 6.33 +

            P1, P2, P3 프로세스가 들어온 순서대로 할당됐다. P2, P3는 수행 시간이 짧음에도 P1이 끝날 때까지 기다리게 되어 평균 대기 시간이 늘어났다.

            +

            SJF (Shortest Job First)

            +
              +
            • 프로세스의 수행 시간이 짧은 순서에 따라 프로세서에 할당한다.
            • +
            • FCFS에서 발생하는 콘보이 효과를 해결할 수 있다.
            • +
            • 최적 알고리즘이지만 수행 시간을 정확히 알 수 없다. (앞서 처리한 프로세스들의 기록을 보고 추측한다.)
            • +
            • 버스트 시간이 큰 프로세스는 계속 뒤로 밀려나는 기아(Starvation)가 발생한다.
            • +
            • 버스트 시간이 짧은 프로세스가 끝날 때까지 운영체제가 개입하지 않는 비선점 스케줄링 방식이다.
            • +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            ProcessBurst timeResponse timeTurnaround timeWaiting time
            P16393
            P28162416
            P379169
            P43030
            +
            +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +|      P4      |              P1             |                P3                |                   P2                  |
            ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +0              3                             9                                  16                                      24
            +
            +
            Average wating time=3+16+9+04=7 +\text{Average wating time} = {3 + 16 + 9 + 0 \over 4} = 7 +

            프로세스의 수행 시간을 정확히 예측했다는 가정하에, 수행 시간이 짧은 순서대로 프로세서에 할당됐다.

            +

            SRF (Shortest Remaining Time First)

            +
              +
            • 프로세스의 남은 수행 시간이 짧은 순서에 따라 프로세서에 할당한다.
            • +
            • SJF에서 발생하는 기아 문제를 해결할 수 있다.
            • +
            • 수행 중 다른 프로세스보다 남은 수행 시간이 적어지면 운영체제가 개입해 자리를 바꾸는 선점 스케줄링 방식이다.
            • +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            ProcessArrival timeBurst timeResponse timeTurnaround timeWaiting time
            P1080179
            P214150
            P329172415
            P435572
            +
            +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +| P1 |         P2        |           P4           |                P1                |                     P3                     |
            ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +0    1                   5                        10                                 17                                           26
            +
            +
            Average wating time=9+0+15+24=26 +\text{Average wating time} = {9 + 0 + 15 + 2 \over 4} = 26 +

            P1이 수행되던 중, 1ms에 P2가 들어왔다. 이때 P1의 남은 수행 시간은 7ms이고, P2의 남은 수행 시간은 4ms이기 때문에 운영체제가 개입해 P1의 수행을 중단하고 P2를 프로세서에 할당한다. P2가 프로세서에 할당된 사이, 2ms에 P3가 들어왔으나 P2의 남은 수행 시간은 3ms이고, P3의 남은 수행 시간은 9ms이기 때문에 프로세서는 P2를 계속 수행한다. 이어서 3ms일 때 P4가 들어왔지만 P2의 남은 수행 시간은 2ms이고, P4의 남은 수행 시간은 5ms이기 때문에 여전히 P2가 수행된다. 이후에도 같은 방식으로 프로세스의 작업이 끝나거나 새로운 프로세스가 들어올 때마다 남은 수행 시간을 비교해 자리를 바꿔준다.

            +

            RR (Round Robin)

            +
              +
            • 일정 시간 할당량(Time quantum) 단위로 여러 프로세스를 번갈아가며 프로세서에 할당한다.
            • +
            • 시스템의 time-sharing과 같은 방식이다.
            • +
            • 반응성이 좋다.
            • +
            • 주로 우선순위 스케줄링(Priority scheduling)과 결합해 프로세스의 시간 할당량을 조절하는 방식으로 활용한다.
            • +
            • 시간 할당량에 따라 운영체제가 계속 개입하는 선점 스케줄링 방식이다.
            • +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            ProcessBurst timeResponse timeTurnaround timeWaiting time
            P1150194
            P22353
            P32575
            +
            Time quantum = 3ms
            +
            ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +|      P1      |    P2   |    P3   |      P1      |      P1      |      P1      |      P1      |
            ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
            +0              3         5         7              10             13             16             19
            +
            +
            Average wating time=4+3+53=4 +\text{Average wating time} = {4 + 3 + 5 \over 3} = 4 +

            모든 프로세스들이 동일하게 3ms씩 프로세스에 할당된다. P2와 P3의 경우 수행 시간이 2ms이기 때문에 할당된 3ms를 모두 사용하지 않았다.

            +

            Priority Scheduling

            +
              +
            • 특정 기준으로 프로세스에게 우선순위를 부여해 우선순위에 따라 프로세서에 할당한다.
            • +
            • 프로세스를 에이징(Aging)해서 오래 대기한 프로세스의 우선순위를 높이는 방식으로 사용된다.
            • +
            • SRF의 경우 남은 수행 시간을 기준으로 우선순위를 부여한다고 할 수 있다.
            • +
            • 다른 스케줄링 알고리즘과 결합해 사용할 수 있으므로 선점, 비선점 모두 가능하다.
            • +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            ProcessPriorityBurst timeResponse timeTurnaround timeWaiting time
            P135494
            P211010
            P3429119
            P451111211
            P523141
            +
            +----+----+----+----+----+----+----+----+----+----+----+----+
            +| P2 |      P5      |           P1           |    P3   | P4 |
            ++----+----+----+----+----+----+----+----+----+----+----+----+
            +0    1              4                        9         11   12
            +
            +
            Average wating time=4+0+9+11+15=5 +\text{Average wating time} = {4 + 0 + 9 + 11 + 1 \over 5} = 5 +

            우선순위에 따라 프로세스가 할당되었다. 사용자가 자주 사용하는 프로세스의 우선순위를 높게 부여하는 식으로 기준을 만들 수 있다. 다만 특정 프로세스의 우선 순위가 계속 밀려 기아가 발생할 수 있으므로, 시간이 지날 때마다 프로세스의 나이를 증가시켜 오래 대기한 프로세스의 우선순위를 높여주는 조치가 필요하다.

            + +
            +
            + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.6

            +

            Synchronization

            +
            +
            +
            + + +
            + +
            +
            +

            🦕 공룡책으로 정리하는 운영체제 Ch.4

            +

            Multithreaded Programming

            +
            +
            +
            + +
            +
            + +
            +
            + Articles +
            +
            +
            + + + + diff --git a/articles.html b/articles.html new file mode 100644 index 0000000..964f1b5 --- /dev/null +++ b/articles.html @@ -0,0 +1,387 @@ + + + + + 박성범 Simon Park + + + + + + + + + + + + + + + + + + + + + + + + + + +
            + +
            + + + + diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..dea84b0 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/images/cover0.png b/assets/images/cover0.png new file mode 100644 index 0000000..aec45cf Binary files /dev/null and b/assets/images/cover0.png differ diff --git a/assets/images/cover1.png b/assets/images/cover1.png new file mode 100644 index 0000000..3d13350 Binary files /dev/null and b/assets/images/cover1.png differ diff --git a/assets/images/cover2.png b/assets/images/cover2.png new file mode 100644 index 0000000..0580332 Binary files /dev/null and b/assets/images/cover2.png differ diff --git a/assets/images/cover3.png b/assets/images/cover3.png new file mode 100644 index 0000000..f628f09 Binary files /dev/null and b/assets/images/cover3.png differ diff --git a/assets/images/cover4.png b/assets/images/cover4.png new file mode 100644 index 0000000..fb75124 Binary files /dev/null and b/assets/images/cover4.png differ diff --git a/assets/images/cover5.png b/assets/images/cover5.png new file mode 100644 index 0000000..41eef2f Binary files /dev/null and b/assets/images/cover5.png differ diff --git a/assets/images/cover6.png b/assets/images/cover6.png new file mode 100644 index 0000000..4134683 Binary files /dev/null and b/assets/images/cover6.png differ diff --git a/assets/images/thumbnail.png b/assets/images/thumbnail.png new file mode 100644 index 0000000..7d91700 Binary files /dev/null and b/assets/images/thumbnail.png differ diff --git a/assets/images/thumbnail_about.png b/assets/images/thumbnail_about.png new file mode 100644 index 0000000..200bda9 Binary files /dev/null and b/assets/images/thumbnail_about.png differ diff --git a/assets/images/thumbnail_article.png b/assets/images/thumbnail_article.png new file mode 100644 index 0000000..bb0cee4 Binary files /dev/null and b/assets/images/thumbnail_article.png differ diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..3b074df --- /dev/null +++ b/feed.xml @@ -0,0 +1,261 @@ + + +Simon Park +https://parksb.github.io/ +Recently published articles written by Simon Park +ko-kr +Fri, 17 May 2024 00:54:37 +0900 + +스마트폰을 PC의 모션 컨트롤러로 만들기 +https://parksb.github.io/article/42.html +한 명의 사용자가 보유한 기기 수가 꾸준히 늘어남에 따라 이들을 유기적으로 결합하고자 하는 사용자 요구도 증가하고 있다. 나는 맥북과 2개의 스마트폰, 아이패드, 우분투 데스크탑을 일상적으로 사용하는데, 애플의 유니버설 컨트롤을 벗어나면 서로 다른 플랫폼의 기기를 연동하기가 쉽지 않다. 특히 모바일 기기에는 많은 센서가 탑재되어 있지만, PC를 사용할 때는 이를 전혀 사용하지 못한다. 모바일 기기의 각종 센서를 하나의 모바일 기기에만 남겨두기에는 너무 값... +Mon, 1 Jan 2024 00:00:00 +0900 + + +아치 리눅스로 15년차 넷북 되살리기 +https://parksb.github.io/article/41.html +우리집에는 2009년 구입한 넷북이 있다.Asus Eee PC 1000HE에는 인텔 아톰 N280과 DDR2 1GB 램이 탑재되어 있다. 아톰 N 시리즈는 인텔이 넷북을 위해 만든 저가, 저성능 CPU다. L1 캐시 56KB, L2 캐시 512KB가 제공되며, 클럭 속도는 1.667GHz다. 2023년 출시된 저가형 노트북용 프로세서 인텔 코어 i3 N305에는 L1 캐시 768KB, L2 캐시 4MB가 제공되고, 속도가 3.8GHz라는 점을 생각해보면 ... +Fri, 1 Sep 2023 00:00:00 +0900 + + +함수형 프로그래밍의 설득력 +https://parksb.github.io/article/40.html +이제 함수형 프로그래밍은 확실히 주류라고 할 만하다. 이미 많은 프로그래밍 언어가 함수형 프로그래밍의 핵심 개념을 차용했고, 그린랩스와 같은 회사들이 엔터프라이즈 규모에서 함수형 프로그래밍의 성공적인 사례를 만들어내고 있다. 함수형 프로그래밍에 관해 설명하는 도서나 자료도 쉽게 찾아볼 수 있다. 최근에는 프로그래밍 입문자들도 함수형 프로그래밍의 중요성을 인식하는 것 같다.그러나 여전히 함수형 프로그래밍을 진지하게 생각하는 프로그래머는 많지 않다. 이제 막... +Tue, 1 Nov 2022 00:00:00 +0900 + + +철도 시간표가 유닉스 시간이 되기까지 +https://parksb.github.io/article/39.html +컴퓨터 공학에서 시간을 다루는 것은 꽤나 고달픈 일이다. 특히 우리가 일상에서 직관적으로 이해하는 시간 체계와 일상에서 의식하지 못하는 시간 체계의 괴리로 인해 소프트웨어 엔지니어들은 고통받는다. 이 글에서는 태양시, 원자시 등 시간 체계에 대해 알아보고, 컴퓨터에서 어떻게 시간을 다루는지 설명하고자 한다.태양의 시간Stanford University Libraries (CC0)19세기초까지만 해도 지역마다 각자의 지방 평균시(Local Mean Time... +Tue, 1 Feb 2022 00:00:00 +0900 + + +🧱 Server Driven UI 설계를 통한 UI 유연화 +https://parksb.github.io/article/38.html +웹과 달리 네이티브 모바일 앱은 빌드, 배포 후에는 수정이 불가능하다. 만약 잘못된 위치에 버튼을 배치한 채로 스토어에 앱을 배포했다면, 그리고 사용자가 잘못된 버전의 앱을 설치했다면 버튼의 위치를 수정할 방법이 없다. 유일한 방법은 사용자가 스스로 스토어에 들어가 수정된 버전의 앱으로 업데이트하는 것 뿐이다.배포 후 수정이 불가능하다는 특성이 부딪히는 또 다른 상황은 A/B 테스팅이다. 소프트웨어를 사용하는 동안 일어나는 사용자의 행동과 경험은 화면 구... +Thu, 1 Jul 2021 00:00:00 +0900 + + +읽기 쉬운 웹을 위한 타이포그래피 +https://parksb.github.io/article/37.html +웹에서 장문의 글을 보여줄 때 보다 읽기 쉽게 디자인하기 위한 원칙을 소개하고자 한다. 전통적인 타이포그래피 규칙들을 바탕으로 웹에서 글을 어떻게 효과적으로 조판할지 소개하되, 활자 자체를 해부하지는 않는다. 이 글에서 제시하는 원칙을 무조건적으로 따르는 것보다는 상황에 따라 유연하게 적용하는 것이 좋고, 실험적인 시도를 원한다면 안 따르는 것이 낫다.폰트먼저 폰트에 대한 용어를 정리할 필요가 있다. 타이포그래피의 많은 부분이 과거 금속 활자를 조판하던 ... +Fri, 1 Jan 2021 00:00:00 +0900 + + +인터넷이 동작하는 아주 구체적인 원리 +https://parksb.github.io/article/36.html +학교에서 노트북으로 구글(www.google.com)에 접속하는 과정을 살펴본다. 이 글에 등장하는 KT와 Google Fiber, 구글의 네트워크 구조, 노드의 IP 주소와 MAC 주소는 모두 간소화 또는 가정한 것이다. 또한 클라이언트와 서버가 패킷을 주고받는 전체 과정을 훑기 위해 프록시와 캐시는 생략했다.학교 네트워크에는 여러 AP(Access Point)가 있고, AP들은 스위치에 연결되어 있다. 스위치들은 게이트웨이 라우터와 연결되어 있으며, ... +Wed, 1 Jan 2020 00:00:00 +0900 + + +🦀 러스트의 멋짐을 모르는 당신은 불쌍해요 +https://parksb.github.io/article/35.html +내가 만나온 개발자들은 대체로 자신이 사용하는 프로그래밍 언어에 딱히 만족하지 않았는데 (극단적으로는 자바스크립트와 PHP가 있다.) 유독 러스트 개발자들은 적극적으로 러스트를 추천했다.하지만 그냥 그런 언어가 있구나 정도로 생각하고 있었다. 그런데 러스트 2018 에디션 발표 이후 근 1년간 러스트 코드를 웹어셈블리로 컴파일할 수 있다든지, deno의 코어가 러스트로 작성됐다든지하는 이야기들이 뉴스피드를 가득 채웠다. 심지어 스프린트 서울 6월 모임에서... +Fri, 1 Nov 2019 00:00:00 +0900 + + +🗞️ 훈련소에서 매일 뉴스 받아보기 +https://parksb.github.io/article/34.html +훈련소에서 가장 답답한 것은 외부와의 단절이라는 말을 자주 들었다. 그래서 입대한 친구들에게 뉴스나 읽을거리를 요약해서 인터넷 편지로 보내주곤 했는데, 매일 주요 뉴스를 요약하는 것은 의외로 손이 많이 가는 일이었다. 무엇보다 인터넷 편지 발송을 지원하는 서비스인 더 캠프의 인터페이스가 너무 불편해서 사용하기 쉽지 않았다.그러던 중 나에게도 군사교육 소집 통지서가 왔고, 4주간의 고립은 역시 중대한 문제였다. 그렇게 입소를 앞둔 일요일, 매일 인터넷 편지... +Sun, 1 Sep 2019 00:00:00 +0900 + + +해피 터미널 라이프 +https://parksb.github.io/article/33.html +2023년 7월 25일에 내용을 업데이트했습니다. 2019년 8월판은 여기에서 확인할 수 있습니다.Terminal EmulatorAlacritty는 OpenGL을 이용해 렌더링에 GPU 가속을 지원받을 수 있는 터미널 에뮬레이터다. 러스트로 작성되었으며, 높은 성능을 내세우고 있다. Alacritty의 모든 설정은 ~/.alacritty.toml 파일로 관리할 수 있고, 저장하면 변경 사항이 즉시 반영된다.다만 0.12.0 버전부터 한글 입력 버그가 발생... +Thu, 1 Aug 2019 00:00:00 +0900 + + +하지만, 야크 털 깎기는 재미있다 +https://parksb.github.io/article/32.html +이 블로그는 지킬이나 휴고, 개츠비같은 정적 사이트 생성기/프레임워크를 사용하지 않았다. 처음엔 이것저것 써봤지만 커스터마이징 자유도가 낮아서 직접 블로그를 만들기로 했다. 초기에는 간단히 html로 글을 썼는데, 너무 불편해서 json 파일로 글을 쓰는 시스템을 만들었다. 이것도 장문의 글을 쓰기엔 불편해서 마크다운 파일을 html 파일로 변환하는 서비스를 개발했다. 그 다음엔 결과물로 나온 파일들을 빌드, 배포하기 위한 툴을 만들었다. 결국 밑바닥부터... +Mon, 1 Jul 2019 00:00:00 +0900 + + +👻 CPU 보안 취약점을 공격하는 아주 구체적인 원리 +https://parksb.github.io/article/31.html +지난 5월 인텔 CPU의 새로운 보안 취약점이 보고됐다. MDS(Microarchitectural Data Sampling)라고 불리는 이 취약점은 2018년 초 보고된 멜트다운(Meltdown), 스펙터(Spectre) 취약점과 달리 CPU 내부 3개의 버퍼(Line Fill Buffers, Load Ports, Store Buffers)로부터 사적인 데이터를 유출할 수 있다. MDS에 대해 공부하기 전 (뒤늦게) 멜트다운과 스펙터에 대해 알아보기로 했... +Sat, 1 Jun 2019 00:00:00 +0900 + + +하나의 타입에 강아지와 고양이 담기 +https://parksb.github.io/article/30.html +여기 강아지와 고양이가 있다.data class Dog(val name: String)data class Cat(val name: String)identity 함수는 항상 파라미터를 그대로 반환하는 항등 함수다. identity(dog: Dog) 함수는 Dog 타입 인자를 받아 반환하며, identity(cat: Cat) 함수는 Cat 타입 인자를 받아 반환한다.fun identity(dog: Dog) = dogfun identity(cat: Cat) =... +Wed, 1 May 2019 00:00:00 +0900 + + +💵 캐시가 동작하는 아주 구체적인 원리 +https://parksb.github.io/article/29.html +기술의 발전으로 프로세서 속도는 빠르게 증가해온 반면, 메모리의 속도는 이를 따라가지 못했다. 프로세서가 아무리 빨라도 메모리의 처리 속도가 느리면 결과적으로 전체 시스템 속도는 느려진다. 이를 개선하기 위한 장치가 바로 캐시(Cache)다.캐시는 CPU 칩 안에 들어가는 작고 빠른 메모리다. (그리고 비싸다.) 프로세서가 매번 메인 메모리에 접근해 데이터를 받아오면 시간이 오래 걸리기 때문에 캐시에 자주 사용하는 데이터를 담아두고, 해당 데이터가 필요할... +Fri, 1 Mar 2019 00:00:00 +0900 + + +Git 사용 중 자주 만나는 이슈 정리 +https://parksb.github.io/article/28.html +깃으로 버전 관리를 하다보면 각종 이슈가 자주 발목을 잡는다. 특히 복잡한 프로젝트의 경우 그 정도가 심한데… 입사 이후 '지금까지 내가 한 건 깃이 아니구나’를 깨닫고 더 공부해 보기로 했다. 그러다보니 매번 비슷한 문제 상황을 마주하게 되는 것 같아서 케이스별로 정리해보았다.먼저 아주 간단한 예시로 주요 용어를 살펴보면: -add-&gt; -commit-&gt; -push-&gt;+-------... +Fri, 1 Feb 2019 00:00:00 +0900 + + +2학년 학부생의 신입 개발자 취업기 +https://parksb.github.io/article/27.html +“네가 군대 갈 때는 통일이 되어 있을 거란다.”초등학교 4학년 때 삼청공원에서 농구를 하고 있었다. 지나가던 스님이 갑자기 내가 군대에 갈 때는 통일이 되어 군대에 가지 않을 것이라고 말씀하셨다. 길 가던 스님이 뜬금없이 미래를 예견하는 게 너무 민속 설화스러워서 지금도 기억이 난다.대학교 2학년이 되면 대부분의 친구들이 군대에 간다. 작년까지만 해도 군대는 남의 일 같았지만, 올해 주변 친구들이 하나둘 입대하면서 내게도 운명의 순간이 다가오고 있음을 ... +Mon, 1 Jan 2018 00:00:00 +0900 + + +🎅 요정을 착취하는 방치형 게임 개발한 이야기 +https://parksb.github.io/article/26.html +Santa Inc.는 초국적 블랙 기업 '산타 주식회사’를 운영하는 방치형 게임(Idle game)이다. 산타의 목적은 오직 선물 생산을 최대로 끌어올리는 것. 산타는 직원을 고용하고, 정책을 채택하며 끝없이 선물을 생산한다.Santa Inc.를 처음 만든 건 2015년 겨울이었다. 웹 프로그래밍을 공부한 후 매년 크리스마스마다 이상한 걸 만들었는데, 이것도 그 일환이었다. 당시엔 크리스마스에 맞춰 런칭하겠다는 욕심으로 학기 중 20일만에 게임을 완성했다... +Sat, 1 Dec 2018 00:00:00 +0900 + + +🤖 컴퓨터가 코드를 읽는 아주 구체적인 원리 +https://parksb.github.io/article/25.html +지난 학기 운영체제 공부를 하면서 더 낮은 레벨은 어떻게 동작하는지 궁금해졌다. David A. Patterson과 John L. Hennessy의 Computer Organization and Design 5th Edition의 전반부를 바탕으로 MIPS instruction set에 대해 정리했다.Computer Abstractions and Technology컴퓨터 아키텍처를 공부한다는 것은 컴퓨터를 구성하는 하드웨어와 명령어가 어떻게 함께 동작하는... +Mon, 1 Oct 2018 00:00:00 +0900 + + +🔐 HTTPS는 어떻게 다를까? +https://parksb.github.io/article/24.html +이미 HTTPS의 중요성은 널리 알려져 있다. 크롬, 파이어폭스와 같은 브라우저는 HTTP 서버에 접속할 때 경고를 띄우니 사용자 경험을 위해서라도 HTTPS는 필수라고 할 수 있다. 내가 개떡같은 코드와 함께한 하루 리뉴얼 이야기에서 그렇게 삽질한 이유이기도 하다.HTTPS를 사용하면 데이터가 암호화되고, 보안에 좋다는 것은 알았지만, HTTP 접속과 비교하며 실제로 오가는 데이터를 분석해보니 그 사실이 더욱 가깝게 다가왔다. 컴퓨터 네트워크에 대한 배... +Mon, 1 Oct 2018 00:00:00 +0900 + + +🌐 Top-Down으로 접근하는 네트워크 +https://parksb.github.io/article/23.html +James F. Kurose, Keith W. Ross의 Computer Networking: A Top-Down Approach는 잘 모르는 책이었는데 의외로 많은 학교에서 교재로 쓰이는 것 같다. 컴퓨터 네트워크 수업을 들으며 Computer Networking: A Top-Down Approach 7th Edition의 첫 챕터를 정리해보기로 했다.첫 챕터는 컴퓨터 네트워크의 전반을 둘러보는 챕터다. 언제나 그렇듯 overview 챕터가 책에서 가장... +Sat, 1 Sep 2018 00:00:00 +0900 + + +📡 WSL에서 SSH 서버 열기 +https://parksb.github.io/article/21.html +SSH(Secure Shell)는 안전하게 원격 접속을 하기 위해 사용하는 프로토콜이다. 윈도우 데스크탑에서 SSH 서버를 열면 아이패드에서 원격으로 데스크탑 쉘에 접속을 할 수 있다. WSL(Windows Subsystem for Linux)은 윈도우의 서브시스템에 리눅스를 탑재하는 기술이다. 아직 부드럽게 작동하지 않는 부분들이 조금 있지만, 마이크로소프트에서 WSL에 신경을 많이 쓰고 있기 때문에 충분히 쓸만하다.내 목적은 학교에서 아이패드로 집에 ... +Sat, 1 Sep 2018 00:00:00 +0900 + + +윈도우즈에서 React Native 개발 환경 세팅하기 +https://parksb.github.io/article/20.html +이번에 참여하게 된 프로젝트에서 리액트 네이티브를 이용하게 됐다. 리액트 네이티브는 리액트 아키텍처를 모바일에 적용한 것으로, ES6 문법과 리액트를 이용해 모바일 어플리케이션을 개발할 수 있도록 해주는 프레임워크다. 기존의 웹 프로그래밍 문법을 (거의)그대로 사용할 수 있다는 점과 안드로이드/iOS 버전을 따로 개발할 필요가 없다는 점이 매력적이다.시작에 앞서, 윈도우 이용자라면 WSL(Windows Subsystem for Linux)을 설치하는 것이... +Wed, 1 Aug 2018 00:00:00 +0900 + + +🐧 윈도우에서 우분투 돌리기 +https://parksb.github.io/article/19.html +윈도우로 개발을 하는 입장에서 터미널을 다루기란 조금 까다롭다. 리눅스나 OSX 환경에 맞춰진 프로젝트에 참여하면 명령이 다르게 동작해 삽질하고, OSX의 패키지 매니저인 Homebrew같은 것도 없어서 또 삽질을 하곤 한다.때문에 윈도우에서 우분투를 가상머신으로 돌리거나 멀티부팅을 하는 등 다양한 방법들을 시도했는데, 역시 번거롭고 불편했다. 그러던 중 WSL을 알게됐다.WSL은 Windows Subsystem for Linux의 약자로, 윈도우 서브시... +Fri, 1 Jun 2018 00:00:00 +0900 + + +📊 파이썬으로 정리하는 Quick-Sort +https://parksb.github.io/article/18.html +quick sort는 말 그대로 빠른 정렬을 의미한다. 큰 문제를 쪼개 작은 문제로 만들어서 정복하는 divide-and-conquer 패러다임에 바탕을 두고 있어 퍼포먼스가 좋다.https://idea-instructions.com/quick-sort/기본적인 매커니즘은 그리 어렵지 않다.리스트에서 값 하나를 골라 pivot으로 정한다.pivot보다 작은 것은 pivot의 왼쪽으로 보낸다.pivot보다 큰 것은 pivot의 오른쪽으로 보낸다.pivot의... +Fri, 1 Jun 2018 00:00:00 +0900 + + +Java Design Pattern: Singleton +https://parksb.github.io/article/17.html +Singleton 패턴은 하나만 존재해야 하는 오브젝트를 만들 때 유용한 디자인 패턴이다. 간단히 구현해보자.public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } r... +Fri, 1 Jun 2018 00:00:00 +0900 + + +Race condition 발생시키고 Mutex lock으로 해결하기 +https://parksb.github.io/article/16.html +race condition은 두 개 이상의 프로세스나 스레드가 하나의 데이터를 공유할 때 데이터가 동기화되지 않는 상황을 말한다. (공룡책으로 정리하는 운영체제 Ch.6에 정리했다.) 그리고 코드에서 이러한 문제가 발생할 수 있는 부분을 critical section이라고 하며, 이 문제를 해결하기 위해 한 번에 하나의 스레드만 critical section에 진입할 수 있도록 제어하는 기법을 mutex lock이라고 한다.CentOS에서 3개의 스레드를... +Fri, 1 Jun 2018 00:00:00 +0900 + + +🌞 개떡같은 코드와 함께한 하루 리뉴얼 이야기 +https://parksb.github.io/article/15.html +2개월이나 지났지만, 더 시간이 지나면 잊어버릴 것 같아서 하루 리뉴얼에 대한 이야기를 해보려 한다. 워낙 충격적이고 힘든 경험이어서 아직도 어땠는지 기억이 난다.결과적으로 더러운(…) 경험이었다. 예상치 못한 오류, 쏟아져 나오는 버그, 개떡같은 코드, 완전 뒤죽박죽이었다. 이하 내용은 약 10개월간 내가 무슨 삽질을 했는지에 관한 회고다.🔥 발단'하루’는 내가 중학교 3학년 때(2013년) 친구와 졸업작품으로 만든 SNS다. 게시글은 하루만 유지되며... +Fri, 1 Jun 2018 00:00:00 +0900 + + +ES6 화살표 함수의 this에 관하여 +https://parksb.github.io/article/14.html +동아리 웹 세미나 중 jQuery를 이용해서 요소를 클릭하면 요소의 내용이 바뀌는 예제를 시연했다.$(&#x27;#el&#x27;).on(&#x27;click&#x27;, function() { $(this).html(&#x27;clicked!&#x27;);});물론 잘 된다. 그런데 개발환경을 설명하는 부분에서 npm과 babel까지 설치하고 ES6로 같은 예제를 돌렸는데 왠지 작동하지 않았다.import $... +Tue, 1 May 2018 00:00:00 +0900 + + +🏕️ 오픈소스 입문을 위한 아주 구체적인 가이드 +https://parksb.github.io/article/13.html +작년 겨울부터 오픈소스에 관심이 생겨 이곳저곳에 이슈도 올리고 풀 리퀘스트도 보내고 있다. 오픈소스 기여의 가장 큰 장점은 남의 코드를 많이 읽을 수 있다는 점과 기술 트렌드를 계속 확인할 수 있다는 점이다. 그리고 영작 실력도 미세하게 (…) 향상된 것 같다. 처음 오픈소스 활동을 시작할 때 네이버 오픈소스 가이드가 큰 도움이 됐다. 오픈소스에 대한 감은 잡히지만 생각보다 구체적인 내용을 다루진 않는다.Git이 설치되어 있지 않다면 설치하고, GitHu... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.8 +https://parksb.github.io/article/12.html +메모리에 로드된 프로세스를 효율적으로 관리하는 방법을 다루는 챕터로, 복잡한 매커니즘과 계산이 나오기 시작해 조금 어려워지는 단계다.BackgroundCh.1 Overview에서 언급했듯 메모리는 현대 컴퓨터 시스템의 핵심이다. 프로세스는 독립적인 메모리 공간을 차지하며, 시스템은 프로세스가 자신의 영역 외에는 접근할 수 없도록 막아야 한다.Basic HardwareCPU는 레지스터를 참조하며 메모리 공간을 보호하며, 레지스터 정보는 PCB에 담겨있다. ... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.7 +https://parksb.github.io/article/11.html +Operating System Concepts Ch.7 DeadlocksCh.6 Synchronization에서 잠시 언급했듯이 데드락은 프로세스가 리소스를 점유하고 놓아주지 않거나, 어떠한 프로세스도 리소스를 점유하지 못하는 상태가 되어 프로그램이 멈추는 현상을 말한다.System Model프로세스는 다음과 같은 흐름으로 리소스를 이용한다.Request: 리소스를 요청한다. 만약 다른 프로세스가 리소스를 사용중이라서 리소스를 받을 수 없다면 대기한다.U... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.6 +https://parksb.github.io/article/10.html +프로세스는 동시에 실행될 수 있으며, 여러 개의 프로세스가 협력할 때는 프로세스 사이에 데이터가 동기화되지 않는 문제가 발생할 수 있다.Background만약 두 프로세스가 동시에 어떤 변수의 값을 바꾼다면 프로그래머의 의도와는 다른 결과가 나올 것이다. 이처럼 프로세스가 어떤 순서로 데이터에 접근하느냐에 따라 결과 값이 달라질 수 있는 상황을 경쟁 상태(race condition)라고 한다.The Critical-Section Problem코드상에서 경... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.5 +https://parksb.github.io/article/9.html +운영체제가 어떤 프로세스를 프로세서에 할당할 것인가 정하는 프로세스 스케줄링(Process scheduling)에 대해 다루는 챕터다. FCFS, SJF, RR 등 다양한 프로세스 스케줄링에 대해 소개한다.Scheduling Criteria운영체제가 프로세스를 프로세서에 할당하는 것을 디스패치(Dispatch)라고 한다. (이때 프로세스 상태가 ready에서 running으로 바뀐다.) 그리고 운영체제가 레디 큐(Ready queue)에 있는 프로세스들 ... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.4 +https://parksb.github.io/article/8.html +스레드에 대해 다루는 챕터로, 구체적인 멀티스레드 구현 방법이나 코드가 많이 나온다.Threads스레드는 프로세스의 작업 흐름을 말한다. 하나의 프로세스가 한 번에 하나의 작업만 수행하는 것은 싱글스레드(Single thread)이며, 하나의 프로세스가 동시에 여러 작업을 수행하는 것은 멀티스레드(Multi thread)라고 한다. 프로세서와 메모리가 발전하며 가능해진 기술이다. 멀티프로그래밍 시스템이니까 프로세스를 여러개 돌려도 되는데 굳이 스레드를 나... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.3 +https://parksb.github.io/article/7.html +본격적으로 프로세스에 대해서 다루기 시작한다. Ch.1 Overview에서 나왔듯이 디스크에 있는 것은 프로그램, 메모리에 로드된 것은 프로세스라고 한다. 프로세스는 Stack, Heap, Data, Code로 나뉜다.+---------------+ max| stack |+-------+-------+| | || v || || ^ || | ... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.2 +https://parksb.github.io/article/6.html +챕터1에서 다룬 시스템에 대해 보다 자세히 다루는 챕터인데, 그렇게 어렵지는 않다. 복잡한 내용이 별로 없어서 쉽게 읽을 수 있다.Operating-System Services운영체제는 사용자와 시스템에게 다양한 서비스를 제공한다.+-----------------------------------------------------------------------------------------------------------------------+| user... +Tue, 1 May 2018 00:00:00 +0900 + + +🦕 공룡책으로 정리하는 운영체제 Ch.1 +https://parksb.github.io/article/5.html +Abraham Silberschatz의 Operating System Concepts는 운영체제의 바이블로 불린다. 이번에 운영체제 수업을 들으면서 Operating System Concepts 9th Edition의 내용을 정리해보기로 했다.Ch.1은 책 전체 내용이 담겨있는 가장 중요한 챕터다. 이 부분을 제대로 정독하고 책을 읽으면 훨씬 읽기 수월하다.What Operating Systems Do운영체제(Operating System)는 컴퓨터의 하... +Tue, 1 May 2018 00:00:00 +0900 + + +차이를 중심으로 살펴본 UI디자인과 UX디자인 +https://parksb.github.io/article/4.html +UX디자인이라는 말은 유행처럼 쓰이기 시작하더니 어느 날부터 UI디자인과 비슷한 개념, 또는 UI디자인을 조금 ‘있어 보이게’ 일컫는 말처럼 쓰이게 되었다. 하지만 UX디자인에 관한 수많은 서적과 자료들, 그리고 실제로 일어나고 있는 기업의 투자를 보면 정말로 UX디자인이 UI디자인과 같은 개념이거나 실체가 없는 것이라고 판단하기는 어렵다. 아무래도 UX디자인이라는 용어가 오남용되고 있는 것이라고 추측했는데, 생각해보니 나 역시 그 둘을 확실히 구분하지 ... +Tue, 1 May 2018 00:00:00 +0900 + + +프로세스간 통신을 활용해 프로그래밍하기 +https://parksb.github.io/article/3.html +컴퓨터는 여러 개의 프로세스를 동시에 돌릴 수 있다. (사실 정확히 '동시에’는 아니다. 자세한 설명은 공룡책으로 정리하는 운영체제 Ch.1를 참고.) 그렇다면 하나의 프로그램이 여러 개의 프로세스로 메모리에 로드될 수 있을까? 당연히 된다. 두 개의 프로세스를 동시에 실행시며 하나의 목적을 달성할 수 있다. 리눅스 환경에서 parent 프로세스와 child 프로세스가 통신하는 학생 정보 관리 프로그램을 만들어보았다.프로그램 구조프로그램은 parent와 ... +Tue, 1 May 2018 00:00:00 +0900 + + +♻️ 자바는 어떻게 Garbage Collection을 할까? +https://parksb.github.io/article/2.html +프로그램이 실행되는 내내 프로그램에서 사용하는 변수, 함수를 비롯한 각종 데이터들은 메모리에 할당되고, 해제되기를 반복한다. 이때 ‘언제, 어떤 데이터를 해제할 것인지’ 정하는 것이 중요한 문제다. C로 프로그래밍할 때는 개발자가 직접 메모리의 할당, 해제 시점을 정해준다.int *ptr = (int*)malloc(sizeof(int) * 3); // 메모리 할당for (int i = 0; i &lt; 3; i++) { ptr[i] = i;}fo... +Sun, 1 Apr 2018 00:00:00 +0900 + + +ES6와 함께 JavaScript로 OOP하기 +https://parksb.github.io/article/1.html +객체지향 프로그래밍(OOP, Object-Oriented Programming)은 절차지향 프로그래밍(Procedural Programming)과 대비되는 프로그래밍 방법론이다.절차지향 프로그래밍 방식 언어로는 대표적으로 C언어가 있다. 일반적으로 C코드는 특정 기능을 수행하는 함수들로 구성되어 있다. C 프로그래머는 프로그램의 각 기능을 구현하고, 이 기능들이 어떤 절차로 수행되는가를 중심으로 개발한다. 객체지향 프로그래밍 방식 언어는 Java가 대표적... +Sun, 1 Apr 2018 00:00:00 +0900 + + +📋 프론트엔드 개발자를 위한 토막상식 +https://parksb.github.io/article/0.html +프로젝트하면서 알게 된 것들과 코딩테스트를 통해 배운 것들, 자바스크립트&amp;제이쿼리: 인터랙티브 프론트엔드 웹 개발 교과서(Jon Duckett)와 JavaScript: The Good Parts(Douglas Crockford)를 통해 공부한 것들, 그리고 Front-end Job Interview Questions에 나와있는 질문들에 대한 대답을 간략하게 정리했다.일반적인 것들헝가리안 표기법변수 이름에 데이터 타입을 접두어로 붙이는 표기법... +Thu, 1 Feb 2018 00:00:00 +0900 + + + diff --git a/googleb1e5dbcc1d32e7b1.html b/googleb1e5dbcc1d32e7b1.html new file mode 100644 index 0000000..dfc34ea --- /dev/null +++ b/googleb1e5dbcc1d32e7b1.html @@ -0,0 +1 @@ +google-site-verification: googleb1e5dbcc1d32e7b1.html \ No newline at end of file diff --git a/images/0747813d-dd75-4fbb-a5d9-a87c592fa8f8.webp b/images/0747813d-dd75-4fbb-a5d9-a87c592fa8f8.webp new file mode 100644 index 0000000..af32eb3 Binary files /dev/null and b/images/0747813d-dd75-4fbb-a5d9-a87c592fa8f8.webp differ diff --git a/images/099994db-2468-44ad-9339-db34e1580d4b.webp b/images/099994db-2468-44ad-9339-db34e1580d4b.webp new file mode 100644 index 0000000..9e0f83f Binary files /dev/null and b/images/099994db-2468-44ad-9339-db34e1580d4b.webp differ diff --git a/images/103454631-5c272800-4d29-11eb-834e-b0931c693f16.webp b/images/103454631-5c272800-4d29-11eb-834e-b0931c693f16.webp new file mode 100644 index 0000000..56a9cd3 Binary files /dev/null and b/images/103454631-5c272800-4d29-11eb-834e-b0931c693f16.webp differ diff --git a/images/103454638-63e6cc80-4d29-11eb-85cd-c9b0574de7a8.webp b/images/103454638-63e6cc80-4d29-11eb-85cd-c9b0574de7a8.webp new file mode 100644 index 0000000..196089d Binary files /dev/null and b/images/103454638-63e6cc80-4d29-11eb-85cd-c9b0574de7a8.webp differ diff --git a/images/103454974-58e16b80-4d2c-11eb-9105-e57bc2ce510c.webp b/images/103454974-58e16b80-4d2c-11eb-9105-e57bc2ce510c.webp new file mode 100644 index 0000000..e83ecf8 Binary files /dev/null and b/images/103454974-58e16b80-4d2c-11eb-9105-e57bc2ce510c.webp differ diff --git a/images/103455164-f38e7a00-4d2d-11eb-966f-fdeeb77aa66d.webp b/images/103455164-f38e7a00-4d2d-11eb-966f-fdeeb77aa66d.webp new file mode 100644 index 0000000..7ff1178 Binary files /dev/null and b/images/103455164-f38e7a00-4d2d-11eb-966f-fdeeb77aa66d.webp differ diff --git a/images/103456120-964af680-4d36-11eb-9d15-7f97e83f08aa.webp b/images/103456120-964af680-4d36-11eb-9d15-7f97e83f08aa.webp new file mode 100644 index 0000000..969f281 Binary files /dev/null and b/images/103456120-964af680-4d36-11eb-9d15-7f97e83f08aa.webp differ diff --git a/images/103459315-d4a1df00-4d51-11eb-8500-ff204b2903c3.webp b/images/103459315-d4a1df00-4d51-11eb-8500-ff204b2903c3.webp new file mode 100644 index 0000000..5d0c0ec Binary files /dev/null and b/images/103459315-d4a1df00-4d51-11eb-8500-ff204b2903c3.webp differ diff --git a/images/103473237-c5667400-4dd9-11eb-8974-9a066723a6e0.webp b/images/103473237-c5667400-4dd9-11eb-8974-9a066723a6e0.webp new file mode 100644 index 0000000..e62d3b5 Binary files /dev/null and b/images/103473237-c5667400-4dd9-11eb-8974-9a066723a6e0.webp differ diff --git a/images/103481091-d59f4300-4e1b-11eb-8582-92d9105762dc.webp b/images/103481091-d59f4300-4e1b-11eb-8582-92d9105762dc.webp new file mode 100644 index 0000000..453b12c Binary files /dev/null and b/images/103481091-d59f4300-4e1b-11eb-8582-92d9105762dc.webp differ diff --git a/images/103481426-519a8a80-4e1e-11eb-97d4-5a82675880c0.webp b/images/103481426-519a8a80-4e1e-11eb-97d4-5a82675880c0.webp new file mode 100644 index 0000000..2b9dab9 Binary files /dev/null and b/images/103481426-519a8a80-4e1e-11eb-97d4-5a82675880c0.webp differ diff --git a/images/103536441-ddbbb900-4ed5-11eb-974a-03cb29cd8a7f.webp b/images/103536441-ddbbb900-4ed5-11eb-974a-03cb29cd8a7f.webp new file mode 100644 index 0000000..d4db251 Binary files /dev/null and b/images/103536441-ddbbb900-4ed5-11eb-974a-03cb29cd8a7f.webp differ diff --git a/images/103541449-c92fee80-4ede-11eb-8b4c-66be64aa97f0.webp b/images/103541449-c92fee80-4ede-11eb-8b4c-66be64aa97f0.webp new file mode 100644 index 0000000..4ef345c Binary files /dev/null and b/images/103541449-c92fee80-4ede-11eb-8b4c-66be64aa97f0.webp differ diff --git a/images/103651395-df07e700-4fa4-11eb-9ae9-121bf2237905.webp b/images/103651395-df07e700-4fa4-11eb-9ae9-121bf2237905.webp new file mode 100644 index 0000000..77738a6 Binary files /dev/null and b/images/103651395-df07e700-4fa4-11eb-9ae9-121bf2237905.webp differ diff --git a/images/10679e77-3fe9-48cf-87d3-95e1b815bcf9.webp b/images/10679e77-3fe9-48cf-87d3-95e1b815bcf9.webp new file mode 100644 index 0000000..5ce2640 Binary files /dev/null and b/images/10679e77-3fe9-48cf-87d3-95e1b815bcf9.webp differ diff --git a/images/123917302-995d3180-d9bd-11eb-8c4f-706ee9e92565.webp b/images/123917302-995d3180-d9bd-11eb-8c4f-706ee9e92565.webp new file mode 100644 index 0000000..34390d4 Binary files /dev/null and b/images/123917302-995d3180-d9bd-11eb-8c4f-706ee9e92565.webp differ diff --git a/images/124346021-6a41fc80-dc17-11eb-8bf1-ef446ae0dfca.webp b/images/124346021-6a41fc80-dc17-11eb-8bf1-ef446ae0dfca.webp new file mode 100644 index 0000000..8f0f361 Binary files /dev/null and b/images/124346021-6a41fc80-dc17-11eb-8bf1-ef446ae0dfca.webp differ diff --git a/images/124375746-bf951100-dcde-11eb-8b50-9362fbf46ff9.webp b/images/124375746-bf951100-dcde-11eb-8b50-9362fbf46ff9.webp new file mode 100644 index 0000000..a3dc294 Binary files /dev/null and b/images/124375746-bf951100-dcde-11eb-8b50-9362fbf46ff9.webp differ diff --git a/images/124376806-afcbfb80-dce3-11eb-8661-6cebf1f787c5.webp b/images/124376806-afcbfb80-dce3-11eb-8661-6cebf1f787c5.webp new file mode 100644 index 0000000..b45e34b Binary files /dev/null and b/images/124376806-afcbfb80-dce3-11eb-8661-6cebf1f787c5.webp differ diff --git a/images/154424888-252d99a9-68ac-4d34-8c99-26510b42b240.webp b/images/154424888-252d99a9-68ac-4d34-8c99-26510b42b240.webp new file mode 100644 index 0000000..ce9b61f Binary files /dev/null and b/images/154424888-252d99a9-68ac-4d34-8c99-26510b42b240.webp differ diff --git a/images/154784238-9f37e239-1787-4687-ad85-9d940993219c.jpg b/images/154784238-9f37e239-1787-4687-ad85-9d940993219c.jpg new file mode 100644 index 0000000..00b8116 Binary files /dev/null and b/images/154784238-9f37e239-1787-4687-ad85-9d940993219c.jpg differ diff --git a/images/154784465-691551bd-e7af-42ec-8824-fb86ec9b6517.webp b/images/154784465-691551bd-e7af-42ec-8824-fb86ec9b6517.webp new file mode 100644 index 0000000..35a63cc Binary files /dev/null and b/images/154784465-691551bd-e7af-42ec-8824-fb86ec9b6517.webp differ diff --git a/images/169650971-29af61d4-94a0-47f9-8f08-edb34eb996f5.webp b/images/169650971-29af61d4-94a0-47f9-8f08-edb34eb996f5.webp new file mode 100644 index 0000000..2a754ed Binary files /dev/null and b/images/169650971-29af61d4-94a0-47f9-8f08-edb34eb996f5.webp differ diff --git a/images/169650973-688ef6f9-c55c-437a-8b19-3a6d1d4b33f8.gif b/images/169650973-688ef6f9-c55c-437a-8b19-3a6d1d4b33f8.gif new file mode 100644 index 0000000..5f39a9d Binary files /dev/null and b/images/169650973-688ef6f9-c55c-437a-8b19-3a6d1d4b33f8.gif differ diff --git a/images/169651195-b2cb3ac0-2c83-4372-b160-3f0366c259e9.webp b/images/169651195-b2cb3ac0-2c83-4372-b160-3f0366c259e9.webp new file mode 100644 index 0000000..52be3cd Binary files /dev/null and b/images/169651195-b2cb3ac0-2c83-4372-b160-3f0366c259e9.webp differ diff --git a/images/1c63192f-294f-4f16-9c32-532dae19a23e.webp b/images/1c63192f-294f-4f16-9c32-532dae19a23e.webp new file mode 100644 index 0000000..92ddc6f Binary files /dev/null and b/images/1c63192f-294f-4f16-9c32-532dae19a23e.webp differ diff --git a/images/39bd8e07-0a27-4c7b-ba90-b946196234fa.webp b/images/39bd8e07-0a27-4c7b-ba90-b946196234fa.webp new file mode 100644 index 0000000..c374e41 Binary files /dev/null and b/images/39bd8e07-0a27-4c7b-ba90-b946196234fa.webp differ diff --git a/images/3c050167-45b3-4c5e-88c9-9cae3173c106.svg b/images/3c050167-45b3-4c5e-88c9-9cae3173c106.svg new file mode 100644 index 0000000..ed0dbf6 --- /dev/null +++ b/images/3c050167-45b3-4c5e-88c9-9cae3173c106.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/4045ae32-0956-469b-a6b3-4fe763ced61e.webp b/images/4045ae32-0956-469b-a6b3-4fe763ced61e.webp new file mode 100644 index 0000000..244cb12 Binary files /dev/null and b/images/4045ae32-0956-469b-a6b3-4fe763ced61e.webp differ diff --git a/images/43831554-7d0c6466-9b3f-11e8-8da3-a5c293503a5a.webp b/images/43831554-7d0c6466-9b3f-11e8-8da3-a5c293503a5a.webp new file mode 100644 index 0000000..75ece09 Binary files /dev/null and b/images/43831554-7d0c6466-9b3f-11e8-8da3-a5c293503a5a.webp differ diff --git a/images/43831555-7d8fb438-9b3f-11e8-96e0-ccfd782d089d.webp b/images/43831555-7d8fb438-9b3f-11e8-96e0-ccfd782d089d.webp new file mode 100644 index 0000000..8ae6790 Binary files /dev/null and b/images/43831555-7d8fb438-9b3f-11e8-96e0-ccfd782d089d.webp differ diff --git a/images/45cc34e8-28ae-49e8-9225-7f7a560390a5.webp b/images/45cc34e8-28ae-49e8-9225-7f7a560390a5.webp new file mode 100644 index 0000000..1f52dea Binary files /dev/null and b/images/45cc34e8-28ae-49e8-9225-7f7a560390a5.webp differ diff --git a/images/46553442-75598800-c918-11e8-859b-35d70b56d9de.webp b/images/46553442-75598800-c918-11e8-859b-35d70b56d9de.webp new file mode 100644 index 0000000..31f6e53 Binary files /dev/null and b/images/46553442-75598800-c918-11e8-859b-35d70b56d9de.webp differ diff --git a/images/46553450-7c809600-c918-11e8-85d5-6f18f5bec225.webp b/images/46553450-7c809600-c918-11e8-85d5-6f18f5bec225.webp new file mode 100644 index 0000000..80b301b Binary files /dev/null and b/images/46553450-7c809600-c918-11e8-85d5-6f18f5bec225.webp differ diff --git a/images/46553451-7c809600-c918-11e8-8bad-672e67b1e20a.webp b/images/46553451-7c809600-c918-11e8-8bad-672e67b1e20a.webp new file mode 100644 index 0000000..dd433ba Binary files /dev/null and b/images/46553451-7c809600-c918-11e8-8bad-672e67b1e20a.webp differ diff --git a/images/46553452-7d192c80-c918-11e8-8446-da60e40b017c.webp b/images/46553452-7d192c80-c918-11e8-8446-da60e40b017c.webp new file mode 100644 index 0000000..489c788 Binary files /dev/null and b/images/46553452-7d192c80-c918-11e8-8446-da60e40b017c.webp differ diff --git a/images/46553463-83a7a400-c918-11e8-9db7-833b64cb3701.webp b/images/46553463-83a7a400-c918-11e8-9db7-833b64cb3701.webp new file mode 100644 index 0000000..0561e8d Binary files /dev/null and b/images/46553463-83a7a400-c918-11e8-9db7-833b64cb3701.webp differ diff --git a/images/46553464-83a7a400-c918-11e8-9814-b5695f255cc2.webp b/images/46553464-83a7a400-c918-11e8-9814-b5695f255cc2.webp new file mode 100644 index 0000000..beec2a3 Binary files /dev/null and b/images/46553464-83a7a400-c918-11e8-9814-b5695f255cc2.webp differ diff --git a/images/46553465-83a7a400-c918-11e8-9f7f-0eda2d318aeb.webp b/images/46553465-83a7a400-c918-11e8-9f7f-0eda2d318aeb.webp new file mode 100644 index 0000000..a5659f4 Binary files /dev/null and b/images/46553465-83a7a400-c918-11e8-9f7f-0eda2d318aeb.webp differ diff --git a/images/46553473-8a361b80-c918-11e8-94be-fe306ac87607.webp b/images/46553473-8a361b80-c918-11e8-94be-fe306ac87607.webp new file mode 100644 index 0000000..c961df2 Binary files /dev/null and b/images/46553473-8a361b80-c918-11e8-94be-fe306ac87607.webp differ diff --git a/images/46553474-8a361b80-c918-11e8-9386-faadced7f6ec.webp b/images/46553474-8a361b80-c918-11e8-9386-faadced7f6ec.webp new file mode 100644 index 0000000..3968e03 Binary files /dev/null and b/images/46553474-8a361b80-c918-11e8-9386-faadced7f6ec.webp differ diff --git a/images/46553475-8a361b80-c918-11e8-89e5-ad218114b0d1.webp b/images/46553475-8a361b80-c918-11e8-89e5-ad218114b0d1.webp new file mode 100644 index 0000000..4e7ef63 Binary files /dev/null and b/images/46553475-8a361b80-c918-11e8-89e5-ad218114b0d1.webp differ diff --git a/images/46553482-902bfc80-c918-11e8-99c5-7b6ba44fec2d.webp b/images/46553482-902bfc80-c918-11e8-99c5-7b6ba44fec2d.webp new file mode 100644 index 0000000..207cfda Binary files /dev/null and b/images/46553482-902bfc80-c918-11e8-99c5-7b6ba44fec2d.webp differ diff --git a/images/46553483-902bfc80-c918-11e8-8173-17316f7389ab.webp b/images/46553483-902bfc80-c918-11e8-8173-17316f7389ab.webp new file mode 100644 index 0000000..0fc56b2 Binary files /dev/null and b/images/46553483-902bfc80-c918-11e8-8173-17316f7389ab.webp differ diff --git a/images/46553489-96ba7400-c918-11e8-9cbc-e80fa1e84bcd.webp b/images/46553489-96ba7400-c918-11e8-9cbc-e80fa1e84bcd.webp new file mode 100644 index 0000000..7f8b9c5 Binary files /dev/null and b/images/46553489-96ba7400-c918-11e8-9cbc-e80fa1e84bcd.webp differ diff --git a/images/46553490-97530a80-c918-11e8-9c8c-c86c213540df.webp b/images/46553490-97530a80-c918-11e8-9c8c-c86c213540df.webp new file mode 100644 index 0000000..d115866 Binary files /dev/null and b/images/46553490-97530a80-c918-11e8-9c8c-c86c213540df.webp differ diff --git a/images/46553496-9de18200-c918-11e8-9ecb-05d560306e5d.webp b/images/46553496-9de18200-c918-11e8-9ecb-05d560306e5d.webp new file mode 100644 index 0000000..4cb723c Binary files /dev/null and b/images/46553496-9de18200-c918-11e8-9ecb-05d560306e5d.webp differ diff --git a/images/46553508-a46ff980-c918-11e8-9f5f-db5d4c198ae3.webp b/images/46553508-a46ff980-c918-11e8-9f5f-db5d4c198ae3.webp new file mode 100644 index 0000000..c8b3251 Binary files /dev/null and b/images/46553508-a46ff980-c918-11e8-9f5f-db5d4c198ae3.webp differ diff --git a/images/46553510-a46ff980-c918-11e8-8793-ea781e49d2f7.webp b/images/46553510-a46ff980-c918-11e8-8793-ea781e49d2f7.webp new file mode 100644 index 0000000..62d1d93 Binary files /dev/null and b/images/46553510-a46ff980-c918-11e8-8793-ea781e49d2f7.webp differ diff --git a/images/46553522-af2a8e80-c918-11e8-8dce-111bb08c4fc1.webp b/images/46553522-af2a8e80-c918-11e8-8dce-111bb08c4fc1.webp new file mode 100644 index 0000000..707be2f Binary files /dev/null and b/images/46553522-af2a8e80-c918-11e8-8dce-111bb08c4fc1.webp differ diff --git a/images/46553523-afc32500-c918-11e8-82e4-b04d95fae4ba.webp b/images/46553523-afc32500-c918-11e8-82e4-b04d95fae4ba.webp new file mode 100644 index 0000000..9886f6a Binary files /dev/null and b/images/46553523-afc32500-c918-11e8-82e4-b04d95fae4ba.webp differ diff --git a/images/46553524-afc32500-c918-11e8-9176-6b4de15dd988.webp b/images/46553524-afc32500-c918-11e8-9176-6b4de15dd988.webp new file mode 100644 index 0000000..a28c23e Binary files /dev/null and b/images/46553524-afc32500-c918-11e8-9176-6b4de15dd988.webp differ diff --git a/images/46553527-b3ef4280-c918-11e8-985a-da537d1e37fc.webp b/images/46553527-b3ef4280-c918-11e8-985a-da537d1e37fc.webp new file mode 100644 index 0000000..ba3ccf3 Binary files /dev/null and b/images/46553527-b3ef4280-c918-11e8-985a-da537d1e37fc.webp differ diff --git a/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.jpg b/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.jpg new file mode 100644 index 0000000..5ae53aa Binary files /dev/null and b/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.jpg differ diff --git a/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.webp b/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.webp new file mode 100644 index 0000000..0cd4b0b Binary files /dev/null and b/images/46553529-b3ef4280-c918-11e8-832b-04c459ad7eb7.webp differ diff --git a/images/46553536-b8b3f680-c918-11e8-8e0b-e41c0103c8de.webp b/images/46553536-b8b3f680-c918-11e8-8e0b-e41c0103c8de.webp new file mode 100644 index 0000000..698ce8e Binary files /dev/null and b/images/46553536-b8b3f680-c918-11e8-8e0b-e41c0103c8de.webp differ diff --git a/images/46553537-b8b3f680-c918-11e8-92c4-1eb1479a4a15.webp b/images/46553537-b8b3f680-c918-11e8-92c4-1eb1479a4a15.webp new file mode 100644 index 0000000..be2108e Binary files /dev/null and b/images/46553537-b8b3f680-c918-11e8-92c4-1eb1479a4a15.webp differ diff --git a/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.jpg b/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.jpg new file mode 100644 index 0000000..645a2e5 Binary files /dev/null and b/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.jpg differ diff --git a/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.webp b/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.webp new file mode 100644 index 0000000..3c0b6c4 Binary files /dev/null and b/images/46553546-be114100-c918-11e8-8a59-20ce4fa489d9.webp differ diff --git a/images/46820151-7713c780-cdc0-11e8-9375-c29b9d82addf.webp b/images/46820151-7713c780-cdc0-11e8-9375-c29b9d82addf.webp new file mode 100644 index 0000000..cd84170 Binary files /dev/null and b/images/46820151-7713c780-cdc0-11e8-9375-c29b9d82addf.webp differ diff --git a/images/46820152-77ac5e00-cdc0-11e8-8883-5e0f072437e2.webp b/images/46820152-77ac5e00-cdc0-11e8-8883-5e0f072437e2.webp new file mode 100644 index 0000000..4e65215 Binary files /dev/null and b/images/46820152-77ac5e00-cdc0-11e8-8883-5e0f072437e2.webp differ diff --git a/images/46820153-77ac5e00-cdc0-11e8-946c-6d07497e4e6f.webp b/images/46820153-77ac5e00-cdc0-11e8-946c-6d07497e4e6f.webp new file mode 100644 index 0000000..e5fff6f Binary files /dev/null and b/images/46820153-77ac5e00-cdc0-11e8-946c-6d07497e4e6f.webp differ diff --git a/images/46820155-77ac5e00-cdc0-11e8-9193-8465aaddf255.webp b/images/46820155-77ac5e00-cdc0-11e8-9193-8465aaddf255.webp new file mode 100644 index 0000000..374a3e9 Binary files /dev/null and b/images/46820155-77ac5e00-cdc0-11e8-9193-8465aaddf255.webp differ diff --git a/images/46820157-7844f480-cdc0-11e8-9016-4eb02a644f95.webp b/images/46820157-7844f480-cdc0-11e8-9016-4eb02a644f95.webp new file mode 100644 index 0000000..2ebb02e Binary files /dev/null and b/images/46820157-7844f480-cdc0-11e8-9016-4eb02a644f95.webp differ diff --git a/images/46820158-7844f480-cdc0-11e8-9c0a-952badf7662b.webp b/images/46820158-7844f480-cdc0-11e8-9c0a-952badf7662b.webp new file mode 100644 index 0000000..6f52cb4 Binary files /dev/null and b/images/46820158-7844f480-cdc0-11e8-9c0a-952badf7662b.webp differ diff --git a/images/46820899-416fde00-cdc2-11e8-95ac-74230c5c0476.png b/images/46820899-416fde00-cdc2-11e8-95ac-74230c5c0476.png new file mode 100644 index 0000000..99acb40 Binary files /dev/null and b/images/46820899-416fde00-cdc2-11e8-95ac-74230c5c0476.png differ diff --git a/images/50655360-37313a80-0fd3-11e9-8a6c-16deb3bc7b5f.webp b/images/50655360-37313a80-0fd3-11e9-8a6c-16deb3bc7b5f.webp new file mode 100644 index 0000000..d32e6c6 Binary files /dev/null and b/images/50655360-37313a80-0fd3-11e9-8a6c-16deb3bc7b5f.webp differ diff --git a/images/50655371-43b59300-0fd3-11e9-95f7-5354e64df78a.webp b/images/50655371-43b59300-0fd3-11e9-95f7-5354e64df78a.webp new file mode 100644 index 0000000..1953d37 Binary files /dev/null and b/images/50655371-43b59300-0fd3-11e9-95f7-5354e64df78a.webp differ diff --git a/images/50655465-a3ac3980-0fd3-11e9-856b-a5f67445bb09.webp b/images/50655465-a3ac3980-0fd3-11e9-856b-a5f67445bb09.webp new file mode 100644 index 0000000..61c93a1 Binary files /dev/null and b/images/50655465-a3ac3980-0fd3-11e9-856b-a5f67445bb09.webp differ diff --git a/images/50655469-a575fd00-0fd3-11e9-86c4-54add9d239f4.webp b/images/50655469-a575fd00-0fd3-11e9-86c4-54add9d239f4.webp new file mode 100644 index 0000000..88566e2 Binary files /dev/null and b/images/50655469-a575fd00-0fd3-11e9-86c4-54add9d239f4.webp differ diff --git a/images/50655470-a870ed80-0fd3-11e9-9917-b1fede3dcea0.webp b/images/50655470-a870ed80-0fd3-11e9-9917-b1fede3dcea0.webp new file mode 100644 index 0000000..7a14409 Binary files /dev/null and b/images/50655470-a870ed80-0fd3-11e9-9917-b1fede3dcea0.webp differ diff --git a/images/5426bee0-fd26-42e4-b081-fda577126d1f.webp b/images/5426bee0-fd26-42e4-b081-fda577126d1f.webp new file mode 100644 index 0000000..82a8fbf Binary files /dev/null and b/images/5426bee0-fd26-42e4-b081-fda577126d1f.webp differ diff --git a/images/54861461-66eb1580-4d6c-11e9-8e89-806544c7a1cd.gif b/images/54861461-66eb1580-4d6c-11e9-8e89-806544c7a1cd.gif new file mode 100644 index 0000000..f0f63a1 Binary files /dev/null and b/images/54861461-66eb1580-4d6c-11e9-8e89-806544c7a1cd.gif differ diff --git a/images/54875045-80f32980-4e3a-11e9-8854-5cef63c9c58e.jpg b/images/54875045-80f32980-4e3a-11e9-8854-5cef63c9c58e.jpg new file mode 100644 index 0000000..b1fe419 Binary files /dev/null and b/images/54875045-80f32980-4e3a-11e9-8854-5cef63c9c58e.jpg differ diff --git a/images/54875998-d71c9880-4e4b-11e9-80b6-ecf955e971e3.webp b/images/54875998-d71c9880-4e4b-11e9-80b6-ecf955e971e3.webp new file mode 100644 index 0000000..8b5af89 Binary files /dev/null and b/images/54875998-d71c9880-4e4b-11e9-80b6-ecf955e971e3.webp differ diff --git a/images/54876163-83f81500-4e4e-11e9-9ff7-605149fc4e1c.webp b/images/54876163-83f81500-4e4e-11e9-9ff7-605149fc4e1c.webp new file mode 100644 index 0000000..e3ff98c Binary files /dev/null and b/images/54876163-83f81500-4e4e-11e9-9ff7-605149fc4e1c.webp differ diff --git a/images/54877425-e73f7280-4e61-11e9-9526-d33a04c189f3.webp b/images/54877425-e73f7280-4e61-11e9-9526-d33a04c189f3.webp new file mode 100644 index 0000000..8e01797 Binary files /dev/null and b/images/54877425-e73f7280-4e61-11e9-9526-d33a04c189f3.webp differ diff --git a/images/54878224-042d7300-4e6d-11e9-9036-8646a8bb935a.webp b/images/54878224-042d7300-4e6d-11e9-9036-8646a8bb935a.webp new file mode 100644 index 0000000..4f42350 Binary files /dev/null and b/images/54878224-042d7300-4e6d-11e9-9036-8646a8bb935a.webp differ diff --git a/images/54880531-cd655600-4e88-11e9-86b6-0904fcb6c0a5.webp b/images/54880531-cd655600-4e88-11e9-86b6-0904fcb6c0a5.webp new file mode 100644 index 0000000..9bb30ce Binary files /dev/null and b/images/54880531-cd655600-4e88-11e9-86b6-0904fcb6c0a5.webp differ diff --git a/images/54880532-cdfdec80-4e88-11e9-8d6b-7c54aeb6c5d1.webp b/images/54880532-cdfdec80-4e88-11e9-8d6b-7c54aeb6c5d1.webp new file mode 100644 index 0000000..eaba8ff Binary files /dev/null and b/images/54880532-cdfdec80-4e88-11e9-8d6b-7c54aeb6c5d1.webp differ diff --git a/images/54880533-cdfdec80-4e88-11e9-8893-e21456590d04.webp b/images/54880533-cdfdec80-4e88-11e9-8893-e21456590d04.webp new file mode 100644 index 0000000..41c59a1 Binary files /dev/null and b/images/54880533-cdfdec80-4e88-11e9-8893-e21456590d04.webp differ diff --git a/images/54880534-cdfdec80-4e88-11e9-88cb-ba3ea41b97a7.webp b/images/54880534-cdfdec80-4e88-11e9-88cb-ba3ea41b97a7.webp new file mode 100644 index 0000000..306fd47 Binary files /dev/null and b/images/54880534-cdfdec80-4e88-11e9-88cb-ba3ea41b97a7.webp differ diff --git a/images/54880535-ce968300-4e88-11e9-80da-1ea0ea49f557.webp b/images/54880535-ce968300-4e88-11e9-80da-1ea0ea49f557.webp new file mode 100644 index 0000000..69b6fa4 Binary files /dev/null and b/images/54880535-ce968300-4e88-11e9-80da-1ea0ea49f557.webp differ diff --git a/images/54880536-cf2f1980-4e88-11e9-80c2-e84f77d9ecee.webp b/images/54880536-cf2f1980-4e88-11e9-80c2-e84f77d9ecee.webp new file mode 100644 index 0000000..43e480e Binary files /dev/null and b/images/54880536-cf2f1980-4e88-11e9-80c2-e84f77d9ecee.webp differ diff --git a/images/54995939-5d330d80-500b-11e9-9f68-8e8a55ced91a.webp b/images/54995939-5d330d80-500b-11e9-9f68-8e8a55ced91a.webp new file mode 100644 index 0000000..8d3dc3a Binary files /dev/null and b/images/54995939-5d330d80-500b-11e9-9f68-8e8a55ced91a.webp differ diff --git a/images/54995941-5efcd100-500b-11e9-90fe-f45048ba7641.webp b/images/54995941-5efcd100-500b-11e9-90fe-f45048ba7641.webp new file mode 100644 index 0000000..b71a583 Binary files /dev/null and b/images/54995941-5efcd100-500b-11e9-90fe-f45048ba7641.webp differ diff --git a/images/5609429a-bc49-447a-8571-25d0ae3e19d2.webp b/images/5609429a-bc49-447a-8571-25d0ae3e19d2.webp new file mode 100644 index 0000000..71d0739 Binary files /dev/null and b/images/5609429a-bc49-447a-8571-25d0ae3e19d2.webp differ diff --git a/images/58715400-bc454200-8401-11e9-971e-d1d898f9b6f0.webp b/images/58715400-bc454200-8401-11e9-971e-d1d898f9b6f0.webp new file mode 100644 index 0000000..1dedb4a Binary files /dev/null and b/images/58715400-bc454200-8401-11e9-971e-d1d898f9b6f0.webp differ diff --git a/images/58715402-bea79c00-8401-11e9-9138-796d62943371.webp b/images/58715402-bea79c00-8401-11e9-9138-796d62943371.webp new file mode 100644 index 0000000..c207815 Binary files /dev/null and b/images/58715402-bea79c00-8401-11e9-9138-796d62943371.webp differ diff --git a/images/58715429-ccf5b800-8401-11e9-8e64-77dc79ad75ac.webp b/images/58715429-ccf5b800-8401-11e9-8e64-77dc79ad75ac.webp new file mode 100644 index 0000000..50e4876 Binary files /dev/null and b/images/58715429-ccf5b800-8401-11e9-8e64-77dc79ad75ac.webp differ diff --git a/images/58715433-cf581200-8401-11e9-9b39-eb9631bf8ac6.webp b/images/58715433-cf581200-8401-11e9-9b39-eb9631bf8ac6.webp new file mode 100644 index 0000000..bbdcee6 Binary files /dev/null and b/images/58715433-cf581200-8401-11e9-9b39-eb9631bf8ac6.webp differ diff --git a/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.jpg b/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.jpg new file mode 100644 index 0000000..75d83fb Binary files /dev/null and b/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.jpg differ diff --git a/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.webp b/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.webp new file mode 100644 index 0000000..b34cefb Binary files /dev/null and b/images/60662976-d78bed00-9e98-11e9-9832-908c731a6989.webp differ diff --git a/images/61590282-982c0300-abf1-11e9-9845-04e6bd174230.gif b/images/61590282-982c0300-abf1-11e9-9845-04e6bd174230.gif new file mode 100644 index 0000000..c941deb Binary files /dev/null and b/images/61590282-982c0300-abf1-11e9-9845-04e6bd174230.gif differ diff --git a/images/61639214-00ecab80-acd6-11e9-970a-b4f5970b7497.webp b/images/61639214-00ecab80-acd6-11e9-970a-b4f5970b7497.webp new file mode 100644 index 0000000..795361c Binary files /dev/null and b/images/61639214-00ecab80-acd6-11e9-970a-b4f5970b7497.webp differ diff --git a/images/63c1cffb-f0cd-404f-86a3-c891a8ad8925.webp b/images/63c1cffb-f0cd-404f-86a3-c891a8ad8925.webp new file mode 100644 index 0000000..0119788 Binary files /dev/null and b/images/63c1cffb-f0cd-404f-86a3-c891a8ad8925.webp differ diff --git a/images/65763301-0810b980-e15e-11e9-9ca1-16a6178ca466.webp b/images/65763301-0810b980-e15e-11e9-9ca1-16a6178ca466.webp new file mode 100644 index 0000000..4b61c4b Binary files /dev/null and b/images/65763301-0810b980-e15e-11e9-9ca1-16a6178ca466.webp differ diff --git a/images/65825439-dc9ee380-e2b1-11e9-8610-b4cae8efbb5a.jpg b/images/65825439-dc9ee380-e2b1-11e9-8610-b4cae8efbb5a.jpg new file mode 100644 index 0000000..8c49b49 Binary files /dev/null and b/images/65825439-dc9ee380-e2b1-11e9-8610-b4cae8efbb5a.jpg differ diff --git a/images/68747470733a2f2f72757374616365616e2e6e65742f6173736574732f72757374616365616e2d666c61742d68617070792e737667.svg b/images/68747470733a2f2f72757374616365616e2e6e65742f6173736574732f72757374616365616e2d666c61742d68617070792e737667.svg new file mode 100644 index 0000000..c7f240d --- /dev/null +++ b/images/68747470733a2f2f72757374616365616e2e6e65742f6173736574732f72757374616365616e2d666c61742d68617070792e737667.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/70389251-36353400-1a00-11ea-91af-42a12b06c383.webp b/images/70389251-36353400-1a00-11ea-91af-42a12b06c383.webp new file mode 100644 index 0000000..7062d44 Binary files /dev/null and b/images/70389251-36353400-1a00-11ea-91af-42a12b06c383.webp differ diff --git a/images/74082458-7fa40d00-4a9d-11ea-9df0-1528eaafc793.webp b/images/74082458-7fa40d00-4a9d-11ea-9df0-1528eaafc793.webp new file mode 100644 index 0000000..cbb0456 Binary files /dev/null and b/images/74082458-7fa40d00-4a9d-11ea-9df0-1528eaafc793.webp differ diff --git a/images/74082599-132a0d80-4a9f-11ea-92f8-3b8da0a86004.webp b/images/74082599-132a0d80-4a9f-11ea-92f8-3b8da0a86004.webp new file mode 100644 index 0000000..bdede21 Binary files /dev/null and b/images/74082599-132a0d80-4a9f-11ea-92f8-3b8da0a86004.webp differ diff --git a/images/89177f34-e7db-460b-88a2-02d3b925934f.svg b/images/89177f34-e7db-460b-88a2-02d3b925934f.svg new file mode 100644 index 0000000..cc831fe --- /dev/null +++ b/images/89177f34-e7db-460b-88a2-02d3b925934f.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/images/95677293-70aec500-0bff-11eb-9b86-cb1e4060873b.webp b/images/95677293-70aec500-0bff-11eb-9b86-cb1e4060873b.webp new file mode 100644 index 0000000..17f779f Binary files /dev/null and b/images/95677293-70aec500-0bff-11eb-9b86-cb1e4060873b.webp differ diff --git a/images/95956615-86a7ca00-0e39-11eb-9791-52c8fe2ae162.webp b/images/95956615-86a7ca00-0e39-11eb-9791-52c8fe2ae162.webp new file mode 100644 index 0000000..cace116 Binary files /dev/null and b/images/95956615-86a7ca00-0e39-11eb-9791-52c8fe2ae162.webp differ diff --git a/images/990A1B3B5B102DFC0A.webp b/images/990A1B3B5B102DFC0A.webp new file mode 100644 index 0000000..ea6d658 Binary files /dev/null and b/images/990A1B3B5B102DFC0A.webp differ diff --git a/images/991D10345AEDBC6D13.webp b/images/991D10345AEDBC6D13.webp new file mode 100644 index 0000000..b8c5faf Binary files /dev/null and b/images/991D10345AEDBC6D13.webp differ diff --git a/images/992F123E5AEDBC1F34.webp b/images/992F123E5AEDBC1F34.webp new file mode 100644 index 0000000..c3e5054 Binary files /dev/null and b/images/992F123E5AEDBC1F34.webp differ diff --git a/images/994BC2425AEDB4A825.webp b/images/994BC2425AEDB4A825.webp new file mode 100644 index 0000000..444079a Binary files /dev/null and b/images/994BC2425AEDB4A825.webp differ diff --git a/images/9950C1455B214D5B27.webp b/images/9950C1455B214D5B27.webp new file mode 100644 index 0000000..44615d7 Binary files /dev/null and b/images/9950C1455B214D5B27.webp differ diff --git a/images/995B74355AD5C39A1C.webp b/images/995B74355AD5C39A1C.webp new file mode 100644 index 0000000..12fa6a0 Binary files /dev/null and b/images/995B74355AD5C39A1C.webp differ diff --git a/images/996704355B1029D433.webp b/images/996704355B1029D433.webp new file mode 100644 index 0000000..3cee4c9 Binary files /dev/null and b/images/996704355B1029D433.webp differ diff --git a/images/99747A3A5B1010D311.webp b/images/99747A3A5B1010D311.webp new file mode 100644 index 0000000..4ae7741 Binary files /dev/null and b/images/99747A3A5B1010D311.webp differ diff --git a/images/997FFB415AEDC17B2E.webp b/images/997FFB415AEDC17B2E.webp new file mode 100644 index 0000000..0259ccb Binary files /dev/null and b/images/997FFB415AEDC17B2E.webp differ diff --git a/images/9981413B5AEDAF964E.webp b/images/9981413B5AEDAF964E.webp new file mode 100644 index 0000000..3146c4b Binary files /dev/null and b/images/9981413B5AEDAF964E.webp differ diff --git a/images/9999D24C5AEDBF1D22.webp b/images/9999D24C5AEDBF1D22.webp new file mode 100644 index 0000000..3bcdddd Binary files /dev/null and b/images/9999D24C5AEDBF1D22.webp differ diff --git a/images/99A6CE3A5B10311A2F.webp b/images/99A6CE3A5B10311A2F.webp new file mode 100644 index 0000000..69d19ca Binary files /dev/null and b/images/99A6CE3A5B10311A2F.webp differ diff --git a/images/99B66C365B10216030.webp b/images/99B66C365B10216030.webp new file mode 100644 index 0000000..69f8c41 Binary files /dev/null and b/images/99B66C365B10216030.webp differ diff --git a/images/99EA8E4F5B10115118.webp b/images/99EA8E4F5B10115118.webp new file mode 100644 index 0000000..840435f Binary files /dev/null and b/images/99EA8E4F5B10115118.webp differ diff --git a/images/a67ee64a-a63e-4323-bce0-a3b08eda3925.svg b/images/a67ee64a-a63e-4323-bce0-a3b08eda3925.svg new file mode 100644 index 0000000..8ae5a8f --- /dev/null +++ b/images/a67ee64a-a63e-4323-bce0-a3b08eda3925.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/images/b02b09c6-db90-4570-a7f0-aafa29bc3ae9.webp b/images/b02b09c6-db90-4570-a7f0-aafa29bc3ae9.webp new file mode 100644 index 0000000..64bcf96 Binary files /dev/null and b/images/b02b09c6-db90-4570-a7f0-aafa29bc3ae9.webp differ diff --git a/images/bf208821-d12c-4f7f-8910-d6f1e3281767.webp b/images/bf208821-d12c-4f7f-8910-d6f1e3281767.webp new file mode 100644 index 0000000..80d0fc8 Binary files /dev/null and b/images/bf208821-d12c-4f7f-8910-d6f1e3281767.webp differ diff --git a/images/bf975a6c-8af7-4b71-9683-206726727014.webp b/images/bf975a6c-8af7-4b71-9683-206726727014.webp new file mode 100644 index 0000000..9da4238 Binary files /dev/null and b/images/bf975a6c-8af7-4b71-9683-206726727014.webp differ diff --git a/images/c1245efe-9597-46e4-a36d-662046f3ead7.webp b/images/c1245efe-9597-46e4-a36d-662046f3ead7.webp new file mode 100644 index 0000000..ff98240 Binary files /dev/null and b/images/c1245efe-9597-46e4-a36d-662046f3ead7.webp differ diff --git a/images/cb8fdf6a-50b9-11e5-80f0-da960d3f88cc.gif b/images/cb8fdf6a-50b9-11e5-80f0-da960d3f88cc.gif new file mode 100644 index 0000000..2b83135 Binary files /dev/null and b/images/cb8fdf6a-50b9-11e5-80f0-da960d3f88cc.gif differ diff --git a/images/d6e212d2-dda9-4bce-ab49-0e677fa31272.svg b/images/d6e212d2-dda9-4bce-ab49-0e677fa31272.svg new file mode 100644 index 0000000..ce7f222 --- /dev/null +++ b/images/d6e212d2-dda9-4bce-ab49-0e677fa31272.svg @@ -0,0 +1,322 @@ + + + + + + + + image/svg+xml + + + + + + + + Keyboard + + + + + + X Server + X client(browser) + X client(xterm) + X client(xterm) + + + + Mouse + + Screen + + + + + + + + + + + + + + + + + + User’s workstation + Remote machine + Network + X client(xterm) + diff --git a/images/e254ffda-502a-4ea2-9437-4d1b9843e883.webp b/images/e254ffda-502a-4ea2-9437-4d1b9843e883.webp new file mode 100644 index 0000000..dff68bc Binary files /dev/null and b/images/e254ffda-502a-4ea2-9437-4d1b9843e883.webp differ diff --git a/images/faa56b3c-bcd8-4983-9ae3-b2e38d335a27.webp b/images/faa56b3c-bcd8-4983-9ae3-b2e38d335a27.webp new file mode 100644 index 0000000..36e161e Binary files /dev/null and b/images/faa56b3c-bcd8-4983-9ae3-b2e38d335a27.webp differ diff --git a/images/ff042ef1-1579-40ad-8e7a-3c2a9b255884.webp b/images/ff042ef1-1579-40ad-8e7a-3c2a9b255884.webp new file mode 100644 index 0000000..faea5ee Binary files /dev/null and b/images/ff042ef1-1579-40ad-8e7a-3c2a9b255884.webp differ diff --git a/images/icons/ajou.webp b/images/icons/ajou.webp new file mode 100644 index 0000000..15343ab Binary files /dev/null and b/images/icons/ajou.webp differ diff --git a/images/icons/github.webp b/images/icons/github.webp new file mode 100644 index 0000000..8504795 Binary files /dev/null and b/images/icons/github.webp differ diff --git a/images/icons/kakao.webp b/images/icons/kakao.webp new file mode 100644 index 0000000..2d97997 Binary files /dev/null and b/images/icons/kakao.webp differ diff --git a/images/icons/keybase.webp b/images/icons/keybase.webp new file mode 100644 index 0000000..f348bd3 Binary files /dev/null and b/images/icons/keybase.webp differ diff --git a/images/icons/linkedin.webp b/images/icons/linkedin.webp new file mode 100644 index 0000000..d98f9ca Binary files /dev/null and b/images/icons/linkedin.webp differ diff --git a/images/icons/me.webp b/images/icons/me.webp new file mode 100644 index 0000000..d8a99b3 Binary files /dev/null and b/images/icons/me.webp differ diff --git a/images/icons/mozilla.webp b/images/icons/mozilla.webp new file mode 100644 index 0000000..fd861a9 Binary files /dev/null and b/images/icons/mozilla.webp differ diff --git a/images/icons/naver.webp b/images/icons/naver.webp new file mode 100644 index 0000000..19e83ef Binary files /dev/null and b/images/icons/naver.webp differ diff --git a/images/icons/neovim.webp b/images/icons/neovim.webp new file mode 100644 index 0000000..1f6b4e4 Binary files /dev/null and b/images/icons/neovim.webp differ diff --git a/images/icons/rss.webp b/images/icons/rss.webp new file mode 100644 index 0000000..7f2a9d0 Binary files /dev/null and b/images/icons/rss.webp differ diff --git a/images/icons/rust.webp b/images/icons/rust.webp new file mode 100644 index 0000000..7f8dc52 Binary files /dev/null and b/images/icons/rust.webp differ diff --git a/images/icons/spreadsheet.webp b/images/icons/spreadsheet.webp new file mode 100644 index 0000000..0607191 Binary files /dev/null and b/images/icons/spreadsheet.webp differ diff --git a/images/icons/twitter.webp b/images/icons/twitter.webp new file mode 100644 index 0000000..8fa652e Binary files /dev/null and b/images/icons/twitter.webp differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..f2b3b71 --- /dev/null +++ b/index.html @@ -0,0 +1,105 @@ + + + + + 박성범 Simon Park + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            + +
            + + + + + diff --git a/keybase.txt b/keybase.txt new file mode 100644 index 0000000..99aa883 --- /dev/null +++ b/keybase.txt @@ -0,0 +1,56 @@ +================================================================== +https://keybase.io/parksb +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of https://parksb.github.io + * I am parksb (https://keybase.io/parksb) on keybase. + * I have a public key ASADCeIfcyTmOwTWfFizBMTo2wE_S-gRUJHw0uHXKax-hQo + +To do so, I am signing this object: + +{ + "body": { + "key": { + "eldest_kid": "01200309e21f7324e63b04d67c58b304c4e8db013f4be8115091f0d2e1d729ac7e850a", + "host": "keybase.io", + "kid": "01200309e21f7324e63b04d67c58b304c4e8db013f4be8115091f0d2e1d729ac7e850a", + "uid": "d7d88ae6892cbbf4ac00d64a2c08a319", + "username": "parksb" + }, + "merkle_root": { + "ctime": 1577625158, + "hash": "cdef2ea6e810a17c9ef4148ca7114aed60b0bd2ea37c39d4960c74a44e09b97617175c6a9cf24cab1e1d055e9f9963c506964d2dcbe5714147b7be7684d13e1a", + "hash_meta": "7c664ec06304e58e54f8aba9efed4dda8e7b079f33cf0027830dc79ffe014932", + "seqno": 14002626 + }, + "service": { + "entropy": "Wo+s5OiS2MS6LZq58vajEpL4", + "hostname": "parksb.github.io", + "protocol": "https:" + }, + "type": "web_service_binding", + "version": 2 + }, + "client": { + "name": "keybase.io go client", + "version": "5.1.1" + }, + "ctime": 1577625180, + "expire_in": 504576000, + "prev": "239854bf101f433d5c4f9bb2980d6ea6ac9a2340726b0a01f3c2fdc8f16657da", + "seqno": 7, + "tag": "signature" +} + +which yields the signature: + +hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgAwniH3Mk5jsE1nxYswTE6NsBP0voEVCR8NLh1ymsfoUKp3BheWxvYWTESpcCB8QgI5hUvxAfQz1cT5uymA1upqyaI0ByawoB88L9yPFmV9rEII9iX1xXgz+hE0G2/xL6psMB/wSizPngs+RLj/x5LYxmAgHCo3NpZ8RADPQZ3u/UfmpNPyhdik8erAIYo78iwFhTuMxSdqMh3CRRqjViwcdpKEVkHqlO9V8gTtjo7V5qVSeub7u3FN32D6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIJKmDb64UanfLwLpXkMYUP9itJ46rFPHMQRwIakJkJ18o3RhZ80CAqd2ZXJzaW9uAQ== + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/parksb + +================================================================== \ No newline at end of file diff --git a/naverd45a752610287b11d12a3437f749faf0.html b/naverd45a752610287b11d12a3437f749faf0.html new file mode 100644 index 0000000..d6b8304 --- /dev/null +++ b/naverd45a752610287b11d12a3437f749faf0.html @@ -0,0 +1 @@ +naver-site-verification: naverd45a752610287b11d12a3437f749faf0.html \ No newline at end of file diff --git a/projects.html b/projects.html new file mode 100644 index 0000000..18c9b5e --- /dev/null +++ b/projects.html @@ -0,0 +1,171 @@ + + + + + Simon Park + + + + + + + + + + + + + + + + + + + + + + + + + + +
            + +
            + + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..f6e8731 --- /dev/null +++ b/robots.txt @@ -0,0 +1,9 @@ +User-agent: * +Allow: / +Disallow: /navigation.*.html +Disallow: /magic-conch-shell/ +Disallow: /cv/ +Disallow: /storage/ +Disallow: /santa-inc/ + +Sitemap: http://parksb.github.io/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..fb5f9b5 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,68 @@ + + + +https://parksb.github.io/2024-05-17T00:54:39+09:00daily1.00 +https://parksb.github.io/work/0.htmldaily1.00 +https://parksb.github.io/work/1.htmldaily1.00 +https://parksb.github.io/work/2.htmldaily1.00 +https://parksb.github.io/work/3.htmldaily1.00 +https://parksb.github.io/work/4.htmldaily1.00 +https://parksb.github.io/work/5.htmldaily1.00 +https://parksb.github.io/work/6.htmldaily1.00 +https://parksb.github.io/work/7.htmldaily1.00 +https://parksb.github.io/work/8.htmldaily1.00 +https://parksb.github.io/work/9.htmldaily1.00 +https://parksb.github.io/work/10.htmldaily1.00 +https://parksb.github.io/work/11.htmldaily1.00 +https://parksb.github.io/work/12.htmldaily1.00 +https://parksb.github.io/work/13.htmldaily1.00 +https://parksb.github.io/work/14.htmldaily1.00 +https://parksb.github.io/work/15.htmldaily1.00 +https://parksb.github.io/work/16.htmldaily1.00 +https://parksb.github.io/work/17.htmldaily1.00 +https://parksb.github.io/work/18.htmldaily1.00 +https://parksb.github.io/work/19.htmldaily1.00 +https://parksb.github.io/work/20.htmldaily1.00 +https://parksb.github.io/article/0.htmldaily1.00 +https://parksb.github.io/article/1.htmldaily1.00 +https://parksb.github.io/article/2.htmldaily1.00 +https://parksb.github.io/article/3.htmldaily1.00 +https://parksb.github.io/article/4.htmldaily1.00 +https://parksb.github.io/article/5.htmldaily1.00 +https://parksb.github.io/article/6.htmldaily1.00 +https://parksb.github.io/article/7.htmldaily1.00 +https://parksb.github.io/article/8.htmldaily1.00 +https://parksb.github.io/article/9.htmldaily1.00 +https://parksb.github.io/article/10.htmldaily1.00 +https://parksb.github.io/article/11.htmldaily1.00 +https://parksb.github.io/article/12.htmldaily1.00 +https://parksb.github.io/article/13.htmldaily1.00 +https://parksb.github.io/article/14.htmldaily1.00 +https://parksb.github.io/article/15.htmldaily1.00 +https://parksb.github.io/article/16.htmldaily1.00 +https://parksb.github.io/article/17.htmldaily1.00 +https://parksb.github.io/article/18.htmldaily1.00 +https://parksb.github.io/article/19.htmldaily1.00 +https://parksb.github.io/article/20.htmldaily1.00 +https://parksb.github.io/article/21.htmldaily1.00 +https://parksb.github.io/article/23.htmldaily1.00 +https://parksb.github.io/article/24.htmldaily1.00 +https://parksb.github.io/article/25.htmldaily1.00 +https://parksb.github.io/article/26.htmldaily1.00 +https://parksb.github.io/article/27.htmldaily1.00 +https://parksb.github.io/article/28.htmldaily1.00 +https://parksb.github.io/article/29.htmldaily1.00 +https://parksb.github.io/article/30.htmldaily1.00 +https://parksb.github.io/article/31.htmldaily1.00 +https://parksb.github.io/article/32.htmldaily1.00 +https://parksb.github.io/article/33.htmldaily1.00 +https://parksb.github.io/article/34.htmldaily1.00 +https://parksb.github.io/article/35.htmldaily1.00 +https://parksb.github.io/article/36.htmldaily1.00 +https://parksb.github.io/article/37.htmldaily1.00 +https://parksb.github.io/article/38.htmldaily1.00 +https://parksb.github.io/article/39.htmldaily1.00 +https://parksb.github.io/article/40.htmldaily1.00 +https://parksb.github.io/article/41.htmldaily1.00 +https://parksb.github.io/article/42.htmldaily1.00 + diff --git a/styles/article.css b/styles/article.css new file mode 100644 index 0000000..e93d0c0 --- /dev/null +++ b/styles/article.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676}.hljs{color:#000;background:#fff}.hljs-comment,.hljs-punctuation{color:#6a737d}.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-doctag,.hljs-literal,.hljs-number,.hljs-variable{color:#e36209}.hljs-params{color:#24292e}.hljs-function{color:#6f42c1}.hljs-built_in,.hljs-class,.hljs-tag,.hljs-title{color:#22863a}.hljs-builtin-name,.hljs-keyword,.hljs-meta-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type{color:#d73a49}.hljs-string,.hljs-undefined{color:#032f62}.hljs-regexp{color:#032f62}.hljs-symbol{color:#005cc5}.hljs-bullet{color:#e36209}.hljs-section{color:#005cc5;font-weight:700}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-emphasis{color:#e36209;font-style:italic}.hljs-strong{color:#e36209;font-weight:700}.hljs-deletion{color:#b31d28;background-color:#ffeef0}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-link{color:#032f62;font-style:underline}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-version:after{content:"0.11.1"}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathdefault{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px;min-width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline{display:inline-block;width:100%;border-bottom-style:dashed}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .op-limits>.vlist-t{text-align:center}.katex .accent>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;height:inherit;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex img{border-style:none;min-width:0;min-height:0;max-width:none;max-height:none}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{text-align:left}#article-title{font-size:1.5rem;font-weight:700}#article-subtitle{display:inline;font-size:1.2rem;font-weight:400}time{font-size:.8rem}#article-content-container{margin-top:30px}#article-navigation{margin-top:50px;overflow:hidden}#article-navigation .article-navigation-item{margin-bottom:1rem}#article-navigation .article-navigation-arrow{font-size:.9rem}#article-navigation .article-navigation-next{text-align:left;float:left;margin-right:5px}#article-navigation .article-navigation-prev{text-align:right;float:right;margin-left:5px}#article-navigation .article-navigation-title{font-weight:700;font-size:1rem}#article-navigation .article-navigation-subtitle{font-weight:400;font-size:.8rem}#article-navigation a:hover{color:#005ccc}div.back-to-article-list{font-size:1rem;margin-bottom:1rem}#article-comments .utterances{max-width:100%;margin:0}#article-content-container h1,#article-content-container h2,#article-content-container h3,#article-content-container h4,#article-content-container h5,#article-content-container h6{margin-bottom:1rem;font-weight:700}#article-content-container h2{font-size:1.4rem;padding-bottom:5px;border-bottom:1px #000 solid}#article-content-container h3{font-size:1.3rem}#article-content-container h4{font-size:1.2rem}#article-content-container h5{font-size:1.1rem}#article-content-container p{font-size:1rem;line-height:1.7rem;margin-bottom:1rem}#article-content-container img{max-width:100%;height:auto}#article-content-container p>img{margin-bottom:-7px}#article-content-container img+em{display:block;font-size:.8rem;line-height:1.5rem}#article-content-container video{max-width:100%;height:auto}#article-content-container>ol,#article-content-container>ul{margin-bottom:1rem}#article-content-container ol,#article-content-container ul{margin-left:2rem}#article-content-container li{line-height:1.7rem}#article-content-container a{color:#005ccc}#article-content-container a:focus,#article-content-container a:hover{text-decoration:underline}#article-content-container pre{display:block;overflow-x:auto;padding:1rem;margin-bottom:1rem;line-height:1.3rem;background-color:#f6f8fa}#article-content-container code,#article-content-container code span{font-family:'RobotoMono VF',monospace}#article-content-container li code,#article-content-container p code{word-break:break-word}#article-content-container blockquote{border-left:3px #dfdfdf solid;padding-left:15px}#article-content-container table{margin-bottom:1rem;border-spacing:unset;border-collapse:collapse;width:100%}#article-content-container thead{background-color:#f6f8fa}#article-content-container td,#article-content-container th{padding:5px 10px 5px 10px;border:1px #000 solid}#article-content-container sup{font-size:11px;line-height:0}#article-content-container details{display:inline;margin-bottom:25px}#article-content-container summary{font-weight:700;cursor:pointer}#article-content-container .table-of-contents{font-size:15px;margin-bottom:30px}#article-content-container .table-of-contents ul{list-style-type:none;counter-reset:item;margin:0;padding-left:1rem}#article-content-container .table-of-contents ul li:before{content:counters(item, ".") ". ";counter-increment:item}#article-content-container section{margin-bottom:15px}#article-content-container .katex-display{margin:0}#article-content-container section .katex *{font-family:'Times New Roman',serif;font-size:1.1rem}#article-content-container p .katex *{font-family:'Times New Roman',serif;font-size:1.1rem}#article-content-container p .katex .msupsub *{font-size:11px}#article-content-container .footnotes{margin-left:-1rem}#article-content-container .footnotes-sep{margin-top:30px;margin-bottom:30px;border-top:0;border-bottom:1px #000 solid}#article-content-container .footnotes>.footnotes-list{padding:0;font-size:.9rem;counter-reset:list}#article-content-container .footnotes>.footnotes-list>.footnote-item{list-style-position:inherit;list-style:none;margin:2px}#article-content-container .footnotes>.footnotes-list>.footnote-item:before{content:"[" counter(list) "] ";counter-increment:list}#article-content-container .footnotes>.footnotes-list>.footnote-item>p{display:inline}#article-content-container .footnotes>.footnotes-list a.footnote-backref{font-family:'Pretendard VF',sans-serif} \ No newline at end of file diff --git a/styles/articles.css b/styles/articles.css new file mode 100644 index 0000000..3761fd7 --- /dev/null +++ b/styles/articles.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676}#main-container>ul>li{margin-top:.5rem;list-style:none;font-size:1.3rem;font-weight:700}#main-container>ul>.selected-container{margin-top:1rem;margin-bottom:1rem;font-size:1rem;line-height:1.5rem}ul#articles-container{list-style:none}ul#articles-container>li span.heading{font-size:1rem;font-weight:650}ul#articles-container>li span.subheading{font-weight:400}ul#articles-container>li time{font-size:.8rem;font-weight:400}ul#articles-container>li{width:fit-content;margin-bottom:7px} \ No newline at end of file diff --git a/styles/highlight.css b/styles/highlight.css new file mode 100644 index 0000000..4836457 --- /dev/null +++ b/styles/highlight.css @@ -0,0 +1 @@ +.hljs{color:#000;background:#fff}.hljs-comment,.hljs-punctuation{color:#6a737d}.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-doctag,.hljs-literal,.hljs-number,.hljs-variable{color:#e36209}.hljs-params{color:#24292e}.hljs-function{color:#6f42c1}.hljs-built_in,.hljs-class,.hljs-tag,.hljs-title{color:#22863a}.hljs-builtin-name,.hljs-keyword,.hljs-meta-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type{color:#d73a49}.hljs-string,.hljs-undefined{color:#032f62}.hljs-regexp{color:#032f62}.hljs-symbol{color:#005cc5}.hljs-bullet{color:#e36209}.hljs-section{color:#005cc5;font-weight:700}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-emphasis{color:#e36209;font-style:italic}.hljs-strong{color:#e36209;font-weight:700}.hljs-deletion{color:#b31d28;background-color:#ffeef0}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-link{color:#032f62;font-style:underline} \ No newline at end of file diff --git a/styles/index.css b/styles/index.css new file mode 100644 index 0000000..04d777b --- /dev/null +++ b/styles/index.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676}#main-container>ul>li{margin-top:.5rem;list-style:none;font-size:1.3rem;font-weight:700}#main-container>ul>.selected-container{margin-top:1rem;margin-bottom:1rem;font-size:1rem;line-height:1.5rem}section#index-container a{color:#005ccc}section#index-container a:hover{text-decoration:underline}section#index-container p{margin-bottom:1rem} \ No newline at end of file diff --git a/styles/init.css b/styles/init.css new file mode 100644 index 0000000..9a8402d --- /dev/null +++ b/styles/init.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676} \ No newline at end of file diff --git a/styles/katex.css b/styles/katex.css new file mode 100644 index 0000000..a5b570f --- /dev/null +++ b/styles/katex.css @@ -0,0 +1 @@ +.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-version:after{content:"0.11.1"}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathdefault{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px;min-width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline{display:inline-block;width:100%;border-bottom-style:dashed}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .op-limits>.vlist-t{text-align:center}.katex .accent>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;height:inherit;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex img{border-style:none;min-width:0;min-height:0;max-width:none;max-height:none}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{text-align:left} \ No newline at end of file diff --git a/styles/list.css b/styles/list.css new file mode 100644 index 0000000..44f1d1a --- /dev/null +++ b/styles/list.css @@ -0,0 +1 @@ +#main-container>ul>li{margin-top:.5rem;list-style:none;font-size:1.3rem;font-weight:700}#main-container>ul>.selected-container{margin-top:1rem;margin-bottom:1rem;font-size:1rem;line-height:1.5rem} \ No newline at end of file diff --git a/styles/work.css b/styles/work.css new file mode 100644 index 0000000..90d32db --- /dev/null +++ b/styles/work.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676}#work-container #work-title{width:auto;font-size:1.5rem;font-weight:700}#work-container #work-subtitle{font-size:1.2rem;word-break:keep-all;font-weight:400}#work-container img{display:block;max-width:500px;width:100%;margin-bottom:1rem;border:1px solid #000;height:auto}#work-container #work-content-container{margin-top:30px}#work-container p{font-size:1rem;line-height:1.7rem;margin-bottom:1rem}#work-container div.back-to-work-list{font-size:1rem;margin-bottom:1rem}#work-content-container a{color:#005ccc}#work-content-container a:focus,#work-content-container a:hover{text-decoration:underline}#work-content-container>ol,#work-content-container>ul{margin-bottom:1rem}#work-content-container ol,#work-content-container ul{margin-left:2rem}#work-content-container li{line-height:1.7rem} \ No newline at end of file diff --git a/styles/works.css b/styles/works.css new file mode 100644 index 0000000..a96ff78 --- /dev/null +++ b/styles/works.css @@ -0,0 +1 @@ +@font-face{font-family:'Noto Serif VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/NotoSerifKR/woff2/NotoSerifKR-VF-Distilled.woff2') format('woff2');font-display:swap}@font-face{font-family:'Pretendard VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/Pretendard/woff2/PretendardKR-VF-Distilled-Specials.woff2') format('woff2');font-display:swap}@font-face{font-family:'RobotoMono VF';src:url('https://cdn.jsdelivr.net/gh/parksb/cdn@master/font/RobotoMono/woff2/RobotoMono-VF-Distilled.woff2') format('woff2');font-display:swap}*{font-family:'Noto Serif VF',serif;margin:0;padding:0;word-break:keep-all;font-feature-settings:normal;text-rendering:optimizeLegibility}body{font-size:16px;margin:auto;padding:20px 30px 50px 30px}a{color:#000;text-decoration:none;cursor:pointer}a:hover{color:#005ccc}.symbol{font-family:'Pretendard VF',sans-serif}img.icon{display:inline;margin-right:1px;max-width:.8rem;max-height:.8rem}time{font-family:'RobotoMono VF',monospace}#main-container{max-width:800px;margin-top:30px;margin-bottom:30px}#top-container span.t1{font-size:1.5rem;font-weight:800}#top-container span.t2{font-size:1rem;font-weight:400}footer{font-size:.8rem;color:#767676}#main-container>ul>li{margin-top:.5rem;list-style:none;font-size:1.3rem;font-weight:700}#main-container>ul>.selected-container{margin-top:1rem;margin-bottom:1rem;font-size:1rem;line-height:1.5rem}ul#works-container{list-style:none}ul#works-container>li span.heading{font-size:1rem;font-weight:650}ul#articles-container>li span.subheading{font-weight:400}ul#works-container>li{width:fit-content;margin-bottom:7px}ul#works-container img.thumbnail{display:none;position:absolute;width:150px;height:auto;border:1px solid #000;left:55px}ul#works-container>li:hover img.thumbnail{display:block} \ No newline at end of file diff --git a/work/0.html b/work/0.html new file mode 100644 index 0000000..61ee120 --- /dev/null +++ b/work/0.html @@ -0,0 +1,75 @@ + + + + + + 하루 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + 하루 +

            +

            + 24시간이 지나면 글이 사라지는 SNS +

            +
            +

            +

            +

            하루 (2013) harooo.com

            +

            온라인 대화와 오프라인 대화의 가장 큰 차이는 무엇일까요? 오프라인 대화는 잊혀집니다. 단점처럼 여겨지지만 가장 큰 장점이기도 한 ‘잊혀짐’. 이를 구현하고자 24시간이 지나면 글이 사라지는 SNS를 만들었습니다. 2013년 이우중학교 졸업작품으로 출품했고, 언론을 통해 알려졌습니다.

            +

            (web) html/css, javascript, php, mysql, jquery

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/1.html b/work/1.html new file mode 100644 index 0000000..72fd639 --- /dev/null +++ b/work/1.html @@ -0,0 +1,76 @@ + + + + + + Whyπ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Whyπ +

            +

            + 청소년이 만드는 디지털 저널리즘 +

            +
            +

            +

            +

            +

            Whyπ (2014)

            +

            와이파이는 청소년의 시각으로 사회 문제를 다루는 언론입니다. 이우고등학교 교무실 뒷방을 빌려 활동하던 저널리즘 소모임 와이파이는 1년후 공식 동아리가 되었고, 계속해서 새로운 정보 전달 매체를 고민하면서 성장을 거듭했습니다.

            +

            (web) html/css, javascript, php, mysql, jquery, mdl

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/10.html b/work/10.html new file mode 100644 index 0000000..13f5ac7 --- /dev/null +++ b/work/10.html @@ -0,0 +1,74 @@ + + + + + + DoiT Web Project Team + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + DoiT Web Project Team +

            +

            + 프로그래밍 동아리 웹 개발 워크숍 +

            +
            +

            +

            +

            DoiT Web Project Team (2018)

            +

            아주대학교 프로그래밍 중앙동아리 DoiT의 웹 프로젝트 팀에서 세미나와 프로젝트를 진행했습니다.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/11.html b/work/11.html new file mode 100644 index 0000000..dde8e24 --- /dev/null +++ b/work/11.html @@ -0,0 +1,73 @@ + + + + + + Front-End Performance Checklist (번역) + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Front-End Performance Checklist (번역) +

            +

            + 더 빠르게 작동하는 프론트엔드 성능 체크리스트 +

            +
            +

            +

            Front-End Performance Checklist (2018) github.com/parksb/Front-End-Performance-Checklist

            +

            프론트엔드에서 신경써야 하는 성능 이슈를 정리한 체크리스트를 번역했습니다.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/12.html b/work/12.html new file mode 100644 index 0000000..7eebbe1 --- /dev/null +++ b/work/12.html @@ -0,0 +1,75 @@ + + + + + + 과학기술로 세상을 바꾸고 싶은 사람들 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + 과학기술로 세상을 바꾸고 싶은 사람들 +

            +

            + The Table Setter & 마이소사이어티 과학기술 워크숍 +

            +
            +

            과학 기술로 세상을 바꾸고 싶은 사람들 포스터. 모자를 쓴 사람이 높은 빌딩을 올려다보는 뒷모습 사진. 2018년 8월 11일부터 18일까지. 주말 4회 워크숍, 회당 3시간.

            +

            과학 기술로 세상을 바꾸고 싶은 사람들 포스터. 도시 야경 사진. 2018년 8월 11일부터 18일까지. 주말 4회 워크숍, 회당 3시간.

            +

            과학기술로 세상을 바꾸고 싶은 사람들 (2018)

            +

            The Table Setter와 적정기술 소셜벤처 마이소사이어티와 함께 진행한 ‘과학기술로 세상을 바꾸고 싶은 사람들’ 포스터를 만들었습니다. The Table Setter의 BI 유지를 목적으로, 이미지만 교체하면 차후 같은 포맷의 포스터를 활용할 수 있도록 디자인했습니다.

            +

            (print) 297 x 420 mm

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/13.html b/work/13.html new file mode 100644 index 0000000..fd918d4 --- /dev/null +++ b/work/13.html @@ -0,0 +1,75 @@ + + + + + + Multilingual Fox + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Multilingual Fox +

            +

            + A simple dictionary extension for Firefox +

            +
            +

            로고. 다양한 언어로 된 문자가 나열된 배경 위에 여우 얼굴 아이콘이 있음.

            +

            사용법을 보여주는 녹화 영상.

            +

            Multilingual Fox (2019) github.com/parksb/multilingual-fox

            +

            A simple dictionary extension supporting English-Korean, Chinese-Korean and Japanese-Korean for Firefox browser. To float a tooltip on the page, hold Alt key (Option key on Mac) and select a word for search.

            +

            (web) typescript, request, cheerio

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/14.html b/work/14.html new file mode 100644 index 0000000..63cd0db --- /dev/null +++ b/work/14.html @@ -0,0 +1,74 @@ + + + + + + THE CAMP Library + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + THE CAMP Library +

            +

            + Unofficial library for THE CAMP, an army communication service +

            +
            +

            라이브러리 GitHub 캡쳐 이미지.

            +

            THE CAMP Library (2019) github.com/parksb/the-camp-lib

            +

            대국민 국군 소통 서비스 더 캠프를 사이트 외부에서 이용하기 위해 만든 비공식 라이브러리입니다.

            +

            (library) typescript, request, mochajs, chai, sinon

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/15.html b/work/15.html new file mode 100644 index 0000000..cce5fd9 --- /dev/null +++ b/work/15.html @@ -0,0 +1,75 @@ + + + + + + Handmade Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Handmade Blog +

            +

            + A static blog generator for people who want to start a blog quickly +

            +
            +

            GitHub 캡쳐 이미지.

            +

            생성된 블로그 예시 화면.

            +

            Handmade Blog (2019) github.com/parksb/handmade-blog

            +

            Handmade Blog is a classic static blog generator for people who want to start a blog quickly. It supports article type document for a blog post, work type document for portfolio, code highlights, KaTeX syntax, footnotes, and others.

            +

            (web) html, sass, ejs, typescript, shell script, markdown-it

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/16.html b/work/16.html new file mode 100644 index 0000000..492f6c5 --- /dev/null +++ b/work/16.html @@ -0,0 +1,75 @@ + + + + + + Darim + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Darim +

            +

            + A personal journal service that supports client-side encryption +

            +
            +

            Darim 문구와 인물 일러스트.

            +

            캘린더 화면 위에 다양한 일기가 나열된 캡쳐 화면.

            +

            Darim (2020) github.com/parksb/darim

            +

            Darim is a personal journal service that supports encryption, calendar view, and markdown syntax. You can keep your diary a secret even from the developer using client-side encryption.

            +

            (web) typescript, react, styled-components, rust, actix-web

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/17.html b/work/17.html new file mode 100644 index 0000000..1aa4ff8 --- /dev/null +++ b/work/17.html @@ -0,0 +1,76 @@ + + + + + + Rust 커맨드라인 애플리케이션 (번역) + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Rust 커맨드라인 애플리케이션 (번역) +

            +

            + 러스트를 이용해 커맨드라인 애플리케이션을 개발하는 방법 +

            +
            +

            +

            Rust 커맨드라인 애플리케이션 (2023) github.com/parksb/rust-cli-book-ko-kr

            +

            Command Line Applications in Rust는 범용 프로그래밍 언어 러스트(Rust)를 이용해 커맨드라인 애플리케이션을 개발하는 방법에 대해 소개하는 책이다. 독자는 간단한 CLI 애플리케이션을 만들며 러스트의 핵심 개념과 타입 시스템, 툴 체인, 에코시스템을 익히게 된다.

            +

            러스트는 높은 성능과 안전성을 확보하기 위한 정적 컴파일 언어로, 모질라 재단에서 독립한 Rust Foundation이 메인테이닝하는 오픈소스 프로젝트이다. 러스트는 Stackoverflow Survey에서 매년 연속으로 "가장 사랑받는 언어"이자, “가장 배우고 싶은 언어” 1위로 선정되고 있다. 이처럼 러스트의 가능성은 충분히 인정받고 있지만, 오너십(Ownership)이라는 특유의 메모리 관리 방식이 큰 러닝커브로 작용하여 배우기 어려운 언어로도 알려져 있다. 이 때문인지 실제 업무에 러스트를 사용하고 있다고 응답한 비율은 순위권에 든 적이 없다.

            +

            하지만 러스트는 확실히 실용적인 언어다. 러스트의 메모리 관리 방식은 코드 레벨에서 메모리 세이프티를 강제하기 때문에 런타임에 발생할 수 있는 세그먼트 폴트 등의 문제를 컴파일 타임에 방지할 수 있다. 또한 러스트에는 GC가 없기 때문에 C/C++에 상당하는 성능을 달성할 수 있으며, 이러한 측면에서 C/C++로 작성된 코드베이스를 메모리 세이프하게 개선하기 위한 가장 현실적인 방안으로 주목받고 있다. 실제로 마이크로소프트, 구글, 애플, 클라우드플레어, 삼성 등 많은 기업이 이미 러스트를 후원, 사용하고 있다. 최근에는 리눅스 커널에 러스트 코드가 포함되기도 했고, 많은 CLI 도구가 러스트로 다시 작성되고 있다.

            +

            Command Line Applications in Rust는 러스트 코어팀의 CLI Working Group에서 공식적으로 작성한 책이기 때문에 매우 정확한 설명을 포함하고 있고, 그 내용도 상당한 공신력이 있다. 하지만 책의 훌륭한 내용에 비해 사람들에게 잘 알려지지는 않았고, 언어도 영문과 중문으로만 제공되고 있다. 러스트의 문법이나 개념, 입문자를 위한 튜토리얼 자료는 많지만, 러스트를 이용한 애플리케이션 개발에 대해 밀도 높게 다루는 문서는 많지 않다. 심지어 한국어 문서는 더욱 찾기 어렵기 때문에, 러스트 문서를 한국어로 번역하는 기여가 필요하다. Command Line Applications in Rust의 한국어 번역본을 통해 더 많은 한국 개발자들이 러스트를 접하고, 러스트의 문제의식과 철학에 공감해 주길 기대한다. 특히 러스트의 대략적인 문법과 개념은 살펴봤지만, 아직 완결된 프로그램을 만들어 보지 못한 사람들이 Command Line Applications in Rust로 시작해 보면 좋을 것 같다.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/18.html b/work/18.html new file mode 100644 index 0000000..ef740a1 --- /dev/null +++ b/work/18.html @@ -0,0 +1,83 @@ + + + + + + Collie + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Collie +

            +

            + A minimal RSS reader just for you +

            +
            +

            +

            +

            +

            Collie (2023) github.com/parksb/collie

            +

            Collie is a minimal RSS feed reader application running on your desktop. With Collie, you can:

            +
              +
            • subscribe to multiple RSS/Atom feeds to organize your own news feed.
            • +
            • receive a real-time notification when a new item is added to the subscribed feed. (By default, it is checked every two minutes.)
            • +
            • and save the items to read again or later.
            • +
            +

            All you need is a local machine and the Internet. No virtual machine, no cloud infrastructures, no always-on database, and no account registration with privacy information required.

            +

            I’ve been getting tech news from HackerNews, Lobsters, etc. on Twitter (It’s X now, but I’ll keep calling it Twitter anyway), but many of them have been terminated due to changes in Twitter’s API policy. I went from place to place: Bluesky, Mastodon, Slack, and newsletter. However, I couldn’t settle anywhere. The social media services such as Bluesky and Mastodon had too many unnecessary features as news feed. Slack RSS was good to get the news in real-time, but the notifications mixed with other workspaces overwhelmed me. The newsletters gave me a lot of high-quality information, but not in real-time.

            +

            Then, I remembered Miniflux, the “minimalist and opinionated feed reader” that I had used past. This is the best option for my goal, but I had to pay for the hosted version or keep running docker machine on my local computer which did not have enough resources. Additionally, I didn’t need a system that maintains multi-user sessions. Eventually, I had no choice but to create my own application, and that’s why I made Collie, the minimal RSS reader just for me.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/19.html b/work/19.html new file mode 100644 index 0000000..090c3f9 --- /dev/null +++ b/work/19.html @@ -0,0 +1,76 @@ + + + + + + Zap + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Zap +

            +

            + A library for building multi-device applications +

            +
            +

            +

            +

            Zap (2023) zap-lib.github.io

            +

            Zap is an application programming library for building multi-device application that enable communication with other devices. While mobile devices offer a wide range of data sources, such as motion sensors, biometrics devices, microphones, touchscreens and more, traditional PCs like laptops and desktops are typically lack these resources.

            +

            The data sources available on mobile devices are valuable, but are often device-dependent, limiting their widespread use. Imagine if PCs could use the series of data from the accelerometer sensor on a mobile device. A simple example is using smartphone as motion controller for PC.

            +

            The main goal of Zap is to support mobile-PC communication, but it also extends its capabilities to enable mobile-mobile and PC-PC communication. Furthermore, it’s not limited to PCs; any devices capable of running Zap implementations(e.g., Kiosk device, Smart TV, etc.) can also participate in this communication.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/2.html b/work/2.html new file mode 100644 index 0000000..50c9216 --- /dev/null +++ b/work/2.html @@ -0,0 +1,76 @@ + + + + + + 이우학교 온라인 석식신청 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + 이우학교 온라인 석식신청 +

            +

            + 성남 이우중고등학교 재학생/교사를 위한 저녁 급식 신청 서비스 +

            +
            +

            +

            +

            +

            이우학교 온라인 석식신청 (2016)

            +

            종이 신청서를 돌려 저녁 급식을 신청받던 기존의 방식을 바꾸기 위해 온라인 석식신청 서비스를 만들었습니다. 학생 페이지는 편의성에 집중해 완전히 새롭게 디자인했고, 교사 페이지는 종이 양식과 유사하게 디자인했습니다.

            +

            (web) html/css, javascript, php, mysql, jquery, moment.js, mdl

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/20.html b/work/20.html new file mode 100644 index 0000000..7df5b60 --- /dev/null +++ b/work/20.html @@ -0,0 +1,74 @@ + + + + + + Blockbuster + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Blockbuster +

            +

            + Reliability evaluation of blockchain networks using on-chain data +

            +
            +

            +

            +

            Blockbuster (2023) blockbuster-dashboard.web.app

            +

            블록체인 생태계가 꾸준히 안정화를 거듭하며 블록체인 네트워크의 신뢰성을 평가하기 위한 프레임워크의 필요성이 대두되고 있다. 그러나 대다수 투자자는 네트워크의 본질적인 측면을 반영하는 온체인 데이터가 아닌, 표면적인 마켓 데이터만을 바탕으로 네트워크를 평가하고 있다. 이러한 문제의식에서 출발하여, 온체인 데이터를 기반으로 본질적인 차원에서 블록체인 네트워크의 신뢰성 및 안정성을 평가하기 위한 종합 지표를 제안한다. 또한 이와 더불어, 종합 지표를 바탕으로 다양한 네트워크를 비교 분석할 수 있는 대시보드 도구를 함께 소개한다.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/3.html b/work/3.html new file mode 100644 index 0000000..a464c31 --- /dev/null +++ b/work/3.html @@ -0,0 +1,76 @@ + + + + + + The Table Setter + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + The Table Setter +

            +

            + 청소년과 청년 전문가들의 상생과 공존을 추구하는 교육 플랫폼 +

            +
            +

            +

            +

            +

            The Table Setter (2017)

            +

            The Table Setter는 청소년과 청년의 상생의 삶을 모색하는 비영리단체입니다. 청년-청소년이 만나 함께 꿈꾸고 배우며 지속가능한 삶의 생태계를 만들어가고자 합니다.

            +

            (web) html/css, javascript, php, mysql, jquery, vue.js, moment.js, bootstrap

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/4.html b/work/4.html new file mode 100644 index 0000000..77fb168 --- /dev/null +++ b/work/4.html @@ -0,0 +1,75 @@ + + + + + + The Table Setter 창립기념식 포스터 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + The Table Setter 창립기념식 포스터 +

            +

            + 청소년과 청년 전문가의 상생을 위한 출발 +

            +
            +

            +

            +

            The Table Setter 창립기념식 (2017)

            +

            비영리단체 The Table Setter의 창립기념식 포스터를 만들었습니다. BI를 유지하기 위해 이전 작업물의 접시 컨셉을 차용했습니다.

            +

            (print) 420 x 594 mm

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/5.html b/work/5.html new file mode 100644 index 0000000..b7ac737 --- /dev/null +++ b/work/5.html @@ -0,0 +1,75 @@ + + + + + + The Table Setter 창립기념식 웹사이트 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + The Table Setter 창립기념식 웹사이트 +

            +

            + 청소년과 청년 전문가의 상생을 위한 출발 +

            +
            +

            +

            +

            The Table Setter 창립기념식 (2017)

            +

            비영리단체 The Table Setter의 창립기념식 프로모션 페이지를 만들었습니다. BI를 유지하기 위해 이전 작업물의 접시 컨셉을 차용했습니다.

            +

            (web) html/css, javascript, jquery, bootstrap

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/6.html b/work/6.html new file mode 100644 index 0000000..7f35284 --- /dev/null +++ b/work/6.html @@ -0,0 +1,73 @@ + + + + + + Airbnb JavaScript Style Guide (번역) + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Airbnb JavaScript Style Guide (번역) +

            +

            + 대체로 합리적인 자바스크립트 접근 방법 +

            +
            +

            +

            Airbnb JavaScript Style Guide 한국어 (2018) github.com/parksb/javascript-style-guide

            +

            에어비앤비의 자바스크립트 스타일 가이드 전문을 번역했습니다. 오랜 기간 업데이트되지 않은 기존 번역문을 대체해 공식 문서에 등록되었습니다.

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/7.html b/work/7.html new file mode 100644 index 0000000..d3bccfc --- /dev/null +++ b/work/7.html @@ -0,0 +1,75 @@ + + + + + + DoiT 신입회원 모집 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + DoiT 신입회원 모집 +

            +

            + 전공과 실력에 상관없이 개발을 하고 싶다면 +

            +
            +

            +

            +

            DoiT 신입회원 모집 (2018)

            +

            아주대학교 프로그래밍 중앙동아리 DoiT의 2018년 1학기 신입회원 모집 포스터를 만들었습니다. 프로그래밍에 자주 쓰이는 고정폭 글꼴인 Consolas 서체를 사용했습니다.

            +

            (print) 420 x 594 mm

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/8.html b/work/8.html new file mode 100644 index 0000000..332192f --- /dev/null +++ b/work/8.html @@ -0,0 +1,73 @@ + + + + + + 아주대학교 자치교지 아주문화 수습편집위원 모집 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + 아주대학교 자치교지 아주문화 수습편집위원 모집 +

            +

            + 학생이 주체로서 발행하는 자치교지 +

            +
            +

            +

            아주대학교 자치교지 아주문화 수습편집위원 모집 (2018)

            +

            (print) 420 x 594 mm

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/work/9.html b/work/9.html new file mode 100644 index 0000000..6421269 --- /dev/null +++ b/work/9.html @@ -0,0 +1,75 @@ + + + + + + Intergrated Git Profile + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            +
            + Works +
            +

            + Intergrated Git Profile +

            +

            + Github와 Gitlab 프로필을 한 페이지에서 +

            +
            +

            +

            +

            Intergrated Git Profile (2018) github.com/parksb/integrated-git-profile

            +

            Github 프로필과 Gitlab 프로필을 한 페이지에 보여주는 웹앱을 만들었습니다.

            +

            (web) html/css, javascript, jquery, react, billboard.js, moment.js

            + +
            +
            + Works +
            +
            +
            + + + + diff --git a/works.html b/works.html new file mode 100644 index 0000000..fa947fe --- /dev/null +++ b/works.html @@ -0,0 +1,227 @@ + + + + + 박성범 Simon Park + + + + + + + + + + + + + + + + + + + + + + + + + + +
            + +
            + + + +