가끔 공부나 일 하기 싫을때 글을 한번씩 쓰려고 하는데, 이번에는 캐시 메모리에 대해 이야기 해보고자 한다. 사실 캐시와 관련된 배경 지식들이 짜잘한 것들이 꽤나 있는데 이것들을 한번에 다 다루려고 하면 글이 길어지고 작성하기도 귀찮아지니, 조금씩 점진적으로 글 내용을 추가해서 작성해보고자 한다.

 

일단 캐시 메모리와 관련된 내용은 컴퓨터공학과 학부 전공 과목 중 컴퓨터구조(Computer Architecture) 과목에서 자세히 다룬다. 하지만 컴퓨터공학을 전공하지 않았거나, 해당 과목을 수강하지 않았거나, 아니면 오래되어서 까먹었다던지 하는 사람들에게 다시 리마인드 시켜주거나 개괄적인 내용을 전달할 수 있는 글을 작성해보고자 한다.

캐시와 버퍼의 차이

캐시(Cache)와 많이 혼용되는 개념 중 버퍼(buffer)라는 것이 있는데, 이 둘은 일단 존재 목적이 다르다. 캐시는 처리속도가 차이나는 장치간의 상호작용 시, 이 처리속도 차이로 인한 성능 저하를 완화시켜 성능을 향상시켜주는 역할을 한다. 캐시와는 달리 버퍼는 데이터를 일시적으로 옮기면서 손실을 방지하는 보조 장치 같은 느낌이다.

 

이렇게만 설명하면 아직 감이 잘 안올 것인데, 간단한 예시를 들어보겠다. 부대찌게 식당에 가서 큰 부대찌게 그릇이 있고, 내 앞에는 앞접시가 있다. 보통 이 앞접시에 부대찌개를 일정량 덜어서 먹을 것인데, 부대찌게 그릇이 주 메모리(Main Memory)이고, 앞접시가 캐시 메모리(Cache Memory)라고 볼 수 있다.

 

이때 중요한 점은, 큰 부대찌게 그릇은 양이 크고, 앞접시는 양이 작다. 그리고 밥 먹는 사람이 부대찌개 그릇까지 수저를 가져가서 음식을 퍼나르는데는 시간이 많이 걸리고, 앞접시까지 수저를 가져가는 데에는 시간이 조금 걸린다. 부대찌게 그릇까지 닿는데에 100초가 걸리고, 앞접시 까지 닿는데에 1초가 걸린다면 이는 실제 주 메모리와 캐시 메모리와의 관계가 비슷해진다. (주 메모리와 캐시 메모리의 읽기쓰기의 속도 차이는 100배 정도라고 한다). 여기 까지 이해했으면 여러분은 캐시 메모리에 대해 어느정도 감을 잡았을 것이다.

 

그러면 버퍼에 대한 예시도 한번 들어보자.

여러분들은 C 언어 에서 swap 함수에 대해 한번들어본 적이 있을 것이다. swap 함수는 인자로 들어온 2개의 변수의 값을 서로 바꾸어 주는 역할을 한다.

void swap(int& a, int& b) {
    int buffer = a;
    a = b;
    b = tmp;
}

자 위에 코드에서 여러분은 이제 버퍼가 뭔지 알게 되었다. a변수와 b변수의 값을 서로 바꿀 때, 그냥 대입(assignment)를 하면 기존의 a변수에 있던 값을 잃어버리게 되므로, 이 값을 임시로 저장할 공간이 필요하다. 그래서 임시 지역 변수를 하나 선언하는데 이 변수의 이름이 buffer이고 실제로 버퍼이다. 보통 버퍼라 하면 배열 형태로 된 것들만 생각하지만, 용도만 생각해 보면 이러한 변수도 버퍼가 된다. 

메모리 계층구조 구성요소들

자 이제 캐시가 왜 필요한지 구체적으로 알아보자.

컴퓨터는 다양한 장치들로 구성되어 있지만 가장 필수적인 두가지를 한번 고르자면 CPU와 주 메모리(Main memory)이다. CPU는 기계어 Instruction 들을 fetch 해서 실행하고, 이 기계어 내용들에 따른 적절한 연산을 수행한다. 이 연산들은 대부분 레지스터나 메모리에서 값을 읽거나 쓰고, 산술연산을 한다.

레지스터

레지스터는 CPU에 있는 매우 작은 메모리 여럿이며, CPU 아키텍쳐에 따라 레지스터의 크기, 종류와 개수가 다르다. 범용 레지스터의 크기에 따라 CPU bit를 정의한다.

 즉 32bit CPU는 범용 레지스터의 크기가 32bit이며, 64bit CPU는 범용 레지스터의 크기가 64bit이다. 그리고 이 때 범용 레지스터의 크기를 CPU의 워드 사이즈라고 하며, 이 워드 사이즈는 CPU가 기본적으로 처리하는 데이터의 크기이다.

 

그리고 레지스터의 경우 한번에 처리할 수 있는 데이터의 양이 수십 비트 밖에 안되는 매우 작은 메모리 이지만, 그 처리 속도는 매우 빠르다.

주 메모리

하지만 이렇게 적은 양의 데이터만 처리할 수는 없기 때문에 큰 크기의 메모리가 필요한데, 이것이 주 메모리, 우리가 흔히 말하는 램(RAM: Random Access Memory)라고 부르는 장치이다. 요즘 사용하는 주 메모리의 크기는 다양한데 대부분 8GB이상의 주 메모리를 사용하는 것 같다. 주 메모리는 레지스터를 구성하는 메모리 반도체 소자보다 값싼 소자 및 회로를 사용하므로 레지스터보다 큰 용량 대비 가격이 저렴하지만, 읽기 쓰기 속도가 100배 이상 차이날 정도로 성능 차이가 심하다.

CPU 레지스터와 주 메모리의 처리 속도 차리

CPU에서 연산을 하려면 주 메모리에서 레지스터로 값을 가져와야 하는데, 레지스터와 주 메모리의 읽기 쓰기(I/O) 속도 차이가 심하므로 주 메모리에서 극심한 병목 현상을 겪게 된다. CPU는 빠른 처리 속도를 가지고 있지만, 주 메모리에서 값을 받아오는 것을 기다리다 보니 처리 속도가 주 메모리의 속도로 하향 평준화가 되는 것이다.

캐시 메모리

첫 단락에서 언급 했듯, 이러한 레지스터와 주 메모리의 I/O 성능 차이를 완화시키기 위해 레지스터와 주 메모리 사이에 캐시 메모리를 도입했다. 캐시 메모리는 레지스터 보다는 메모리 사이즈가 더 크고, 주 메모리 보다는 메모리 사이즈가 작다. 그리고 레지스터와 같이 고속으로 처리할 수 있는 비싼 반도체 소자를 활용해서 빠른 읽기 쓰기가 가능하도록 제작하였다.

캐시 메모리의 동작 예시

그래서 이 캐시 메모리는 부대찌개의 앞접시 처럼, 레지스터가 바로 주 메모리로 접근 하는 것이 아닌 고속의 캐시 메모리에 데이터를 덜어서 가져가도록 하는 것이다. 캐시 메모리는 다음과 같은 시나리오로 동작한다.

  1. CPU가 1번 주소의 메모리 값을 읽어오고자 한다.
  2. CPU는 캐시 메모리를 참조해서 1번 주소의 데이터 값이 있는지 확인한다.
  3. 캐시 메모리에 1번 주소의 데이터가 값이 없다 = 이를 캐시 미스(Cache miss)라고 한다.
  4. 주 메모리의 1번 주소의 데이터 값을 Copy해서 캐시에 저장한다.
  5. CPU가 1번 주소의 메모리 값을 다시 읽어오고자 한다.
  6. CPU는 캐시 메모리를 참조해서 1번 주소의 데이터 값이 있는지 확인한다.
  7. 캐시 메모리는 1번 주소의 데이터가 값이 있다. = 이를 캐시 히트(Cache hit)라고 한다.
  8. CPU는 느린 주 메모리 까지 접근할 필요 없니 캐시 메모리에서 1번 주소의 메모리 값을 읽어온다.

주 메모리에 접근할 때 100의 latency가 발생하고, 캐시 메모리에 접근할 때 1의 latency가 발생한다고 했을 때, 같은 값을 한번 불러서 여러번 읽는다면 굉장한 속도 효율을 낼 수 있다.

비슷하게 메모리에 값을 쓰는(Write) 경우도 캐시 메모리에만 값을 쓰는 방식으로 latency를 줄일 수 있다. 그러면 일시적으로 캐시 메모리의 데이터 값과, 주 메모리의 데이터 값이 다른 Memory inconsistency가 발생할 수 있는데 캐시 메모리에서 해당 주소값이 캐시 메모리에서 나가게 되는 경우 주 메모리에 바뀐 내용을 Write하게 된다. 이 정책을 Write back이라고 한다.

이와 달리 Write through라는 정책을 활용하면 메모리에 값을 써야 할 때, 캐시 메모리 뿐만 아니라 주 메모리에도 값을 쓴다. 이렇게 하면 Memory inconsistency가 발생하지 않지만 성능적으로는 느려진다.

스토리지

앞서 이야기한 레지스터, 캐시 메모리, 주 메모리는 전기의 공급이 끊기면 데이터가 사라지는 기억장치들이다. 이와 달리 전기의 공급이 끊겨도 데이터가 남아있는 보조기억장치가 있는데 주로 이들을 스토리지(Storage)라고 부르겠다. 우리에게 친숙한 하드디스크나 SSD와 같은 친구들이 이 스토리지에 해당한다. 이 스토리지는 용량 대비 가격이 매우 싸서 많은 데이터를 저장하기 적합하지만, 읽기 쓰기 속도는 주 메모리 보다도 더 느리다.

메모리 계층구조(Memory Hierarchy)

위에서 언급한 저장장치들을 피라미드 형태로 그려놓은 이미지이다. 직접 그리려고 했는데 너무 귀찮은 나머지 위키에서 퍼왔다.

위와 같은 형태를 메모리 계층구조라고 한다. 아래로 갈 수록 저장 공간의 크기가 커진다. 그 이유는 단위 용량 당 메모리 반도체 & 소자 & 구성요소의 가격이 값싸서 그렇다. 그리고 위쪽으로 갈 수록 처리속도가 빨라진다.

이러한 메모리 계층구조를 갖는 컴퓨터를 만드는 이유는 가성비 때문이다. 사실 캐시메모리 등을 구성하는 정적 메모리 소자로만 주 메모리를 만들면 굉장히 빠른 고성능의 컴퓨터를 쓸 수 있지만, 가격이 기하급수적으로 비싸지기 때문에 그렇게 하지않는다. 그렇다고 캐시 메모리를 빼서 가격을 낮추면 처리 속도가 너무 느려진다.

그런데 메모리가 위쪽으로 올라갈 수록 크기가 급격히 작아지는데 이러한 계층구조로 생각보다 꽤 괜찮은 가성비의 속도가 나오는 이유가 무엇일까? 이 이유로는 지역성이라는 소프트웨어의 특징 덕분이다.

지역성(Locality)

메모리 계층구조가 가성비를 가질 수 있는 이유로는 지역성이라는 소프트웨어, 컴퓨터 프로그램의 특징 덕분이다. 그리고 이 지역성은 시간 지역성(Temporal Locality)와 공간 지역성(Spatial Locality)로 나뉜다.

 

간단하게 이야기 하자면, 시간 지역성은 지금 1번지 메모리에 접근된 적이 있으면 가까운 시일 내에도 1번지 메모리에 접근될 확률이 높다는 것이다. 공간 지역성은 비슷하게 1번지 메모리에 접근된 적이 있으면 2,3번지 메모리에 접근될 확률이 높다는 것이다.

 

왜 그럴까?

우리가 프로그램을 작성하는 방식을 한번 생각해보자.

 

만약 CPU가 함수안의 코드를 실행한다고 하면, 그 함수 안에서는 함수의 인자와 지역변수들을 많이 활용할 것이다. 이것이 일단 시간 지역성이다. 그리고 for-loop와 같은 반복문의 경우 반복문 instruction 들이 계속 반복되므로 이 경우에도 시간 지역성이 만족된다.

그리고 배열같은 경우도 반복문으로 배열 순회를 한다던지 하면 arr[1]에 접근된 뒤 곧이어 arr[2]를 접근할 확률이 높을 것이다. 이러한 이유로 공간 지역성도 만족된다.

 

따라서 이런 특성 때문에 캐시 메모리와 같이 매우 작은 크기로도 꽤 괜찮은 성능 향상을 이룰 수 있다. 그리고 이런 지역성을 고려해서 캐시 메모리의 스케쥴링도 설계되어 있다.

 

LRU Cache scheduling 알고리즘과 Cache block size와 같은 내용들을 추가적으로 언급해야 하는데, 나중에 다시 써 보도록 하겠다.

레퍼런스

https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EB%A6%AC_%EA%B3%84%EC%B8%B5_%EA%B5%AC%EC%A1%B0

developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox

stackoverflow.com/questions/90178/make-a-div-fill-the-height-of-the-remaining-screen-space

developer.mozilla.org/ko/docs/Web/CSS/CSS_Flexible_Box_Layout/Flexbox%EC%9D%98_%EA%B8%B0%EB%B3%B8_%EA%B0%9C%EB%85%90

 

CSS에 있는 flexbox라는 컨셉이 있는데, 비교적 최근에 나온 컨셉이다. 공식문서를 좀 읽어보자.

 

이 Flexbox라는 컨셉은 1차원적인 레이아웃모델에 공간배분, 정렬기능을 제공한다고 한다.

끝 선 맞추기 등 여러가지가 있는데, 일단 제일 평범하면서도 중요한 기능인 크기 조절에 대해 살펴보자.

 

일단 용어를 조금 정리하고 넘어가자면, 항목(item)은 내부에 내용(contents)를 포함한다. 항목이 parents element라면 내용은 child elements라고 볼 수 있겠다.

 

많은 기능들이 있는데, 일단 on-demand로 당장 필요한 기능을 이야기 해보고자 한다.

 

이걸 알게 된 이유는, 화면 레이아웃에서, 이렇게 구성하고 싶은데 크기를 잘 조절못해서 구글링하다가 알게 되었다.

<div class="box">
    <div class="one">One</div>
    <div class="two">Two</div>
    <div class="three">Three</div>
</div>

내가 원하는 건, one은 고정된 20px크기를 갖고, 아래의 three도 고정된 50px의 크기를 가지며,

중간의 two는 남는 공간을 죄다 차지하도록 하게 하고 싶었다.

이런 경우 이렇게 지정하면 된다고 한다.

.box {
    display: flex;
    flex-flow: column;
    height: 100%;
}

.one {
    flex: 0 1 20px;
}

.two {
    flex: 0 1 auto;
}

.three {
    flex: 0 1 50px;
}

 

이제 방법은 알았으니 원리를 좀 알아보자.

크기 조절(분배)

크기 조절 컨셉은 3개의 css property를 사용하는데, 기본 컨셉 이러하다.

기본 크기를 지정하고, 공간이 남거나 모자라면 weight값에 따라 비례적으로 나눠 가진다.

이때 기본 크기가 flex-basis 값이다.

그리고 공간이 남는 경우 더 커질 수 있는 경우 flex-grow값에서 관장한다.

그리고 공간이 모자라서 더 작아지는 경우 flex-shrink라는 값에서 관장한다.

 

flex-grow가 양수이면, flex 항목의 크기가 커질 수 있다.

flex-shrink가 양수이면, flex 항목의 크기가 작아질 수 있다.

 

양수라면 그 크기의 비율에 따라서 많이 작아지거나 많이 커지거나 하게 된다.

flex-basis

flex-basis property는 남는 공간이 있는 경우 항목의 크기를 결정하는 속성이다.

기본 값은 auto인데, 브라우저가 해당 항목이 크기가 있다고 생각하는 경우, 그 크기를 flex-basis로 갖는다는 뜻이다.

만약 항목이 크기를 갖지 않는다면 그 항목의 컨텐츠 크기가 flex-basis로 사용된다. 따라서 단지 부모 항목에 display: flex; 만 설정해줘도, 내부 항목들이 한 줄로 정렬되면서 자기자신의 내용들을 표현할 크기만큼만 공간을 가져가는 이유이다.

flex-grow

flex-grow가 0이면 동작하지 않고, 양수이면 주축 방향으로 크기가 flex-basis이상 커질 수 있도록 해주는 특성이다.

flex-grow를 1로 하면, 남은 공간들은 각 항목에 동일하게 분배된다. 만약 1이상의 값이면 남은 공간이 분배될 때, 그 값의 비율만큼 분배가 되게 된다.

flex-shrink

flex-grow와 비슷한 방식으로 동작하는데, 이는 남은 공간을 추가적으로 할당해서 커지는 것이 아니라 basis만큼 분배하기에 남은 공간이 부족할 시 각 사이즈를 줄이는 방식을 정의한다. 이 값이 클 수록 더 많이 작아지게 된다.

 

축약형 속성 flex

flex-basis, flex-grow, flex-shrink 항목을 한 줄에 한번에 표현할 때 쓰는 속성 이름이 flex이다.

flex: 1 2 auto; 라고 지정하면 각각 왼쪽부터 순서대로 flex-grow, flex-shrink, flex-basis가 되게 된다.

이산적인 컴퓨터 내부

컴퓨터는 내부적으로 0과 1로 이루어진 디지털이면서 2진수로 동작을 한다고 합니다.

실제로 논리회로 수준으로 내려가 보면 0과 1을 간단하게 아래처럼 정의를 해볼 수 있겠지요.

 

전류가 통하면 1, 안통하면 0

전압이 높으면 1, 낮으면 0

이런식으로 1아니면 0으로 나타낼 수 있습니다. 이렇게 중간이 없고 "1아니면 0이다" 라고 딱딱 나뉘어져 있는것을 이산적(Discrete)라고 합니다.

 

그래서 이산수학(Discrete Mathematics)이 컴퓨터공학과 밀접하게 관련이 있는 것입니다.

이산적이지 않다면 연속적이라고 하겠지요. 0도 있고 1도 있고 0.5도 있고 0.75도 있고 이런식으로 실수 범위까지 확장이 된다면 연속적이라고 부를 수 있을 것 같습니다.

 

하지만 값들이 0, 1, 2, 3, 4 처럼 딱딱 나뉘어져 양자화 되어 있다면 이는 이산적인 특징을 가졌다고 볼 수 있겠지요.

 

컴퓨터에서의 자연수 표기

어쨋든 컴퓨터는 기본적으로 Compute를 하기 위한 기기이고, 계산을 하기 위한 계산기에서 시작을 했습니다. 사칙연산이 가장 기본이 되는 연산이지요. 그래서 컴퓨터에서는 일단 수를 표현해야 하는데, 실수범위를 표현하기에 앞서서 정수를 먼저 표현해봅시다.

 

사실 정수보다도 자연수를 표현하는 것이 더 쉽겠지요.

0과 1로만 이루어져 있으면, 요즘 공교육 교육과정에서는 초등학교때 배우는지, 중학교때 배우는지는 모르겠지만 어쨋든 2진법이란게 생각이 날 수 있습니다.

 

흔히 사람들이 사용하는 10진법은 0부터 9까지의 총 10가지 아라비아 숫자로 크기를 표현하는데, 2진법은 0과 1이라는 2가지의 아라비아 숫자로 크기 값을 표현합니다.

 

간단하게 2진법으로 표현을 하면 컴퓨터에서 쉽게 자연수를 표기할 수 있겠지요. 그리고 0까지도 표현할 수 있습니다.

컴퓨터의 워드사이즈

이제 자연수를 컴퓨터에서 표현하기 위해, 컴퓨터의 워드사이즈와 비트(bit)라는 정보 단위를 설명하도록 하겠습니다.

일단 bit는 binary digit의 약자로, 이진수 숫자 한 자리라는 뜻이라고 보시면 됩니다.

이 bit는 정보의 단위로, 이진수 숫자 한 자리만큼의 정보량이라고 보면 됩니다.

이는 즉슨 1bit의 정보는 0아니면 1을 표현할 수 있습니다.

2bit의 정보는 00, 01, 10, 11의 4가지 종류의 정보를 표현할 수 있습니다.

 

컴퓨터에는 워드사이즈 라는게 있는데, 워드 사이즈는 워드(word)의 크기(size)입니다. 워드는 컴퓨터가 처리하는 최소 데이터 크기라고 보면 됩니다.

옛날 컴퓨터들 보면 8bit 컴퓨터, 16bit 컴퓨터 이런식으로 이름을 가지고 있는 녀석들이 있는데, 앞에 8bit, 16bit는 그 컴퓨터의 워드 사이즈를 뜻하는 것입니다.

8bit로 자연수 표현하기

 

이번에는 이해하기 쉽고, 그림그리기도 쉽게 8bit 워드사이즈를 갖는 컴퓨터를 기준으로 설명해보겠습니다.

8bit면 총 8자리로 된 2진수 값을 표현할 수 있을 것입니다.

위 표는 모든 비트가 0일때, 당연히 2진수로 값이 0이고 어떻게 평가되는지를 나타내는 모식표입니다.

오른쪽 부터 인덱스를 0부터 센다고 치고, 0번째 인덱스에 있는 비트는 2의 0제곱의 값을 나타내고, 1번째 인덱스는 2의 1제곱, 이렇게 n번 인덱스의 값은 2의 n제곱의 값을 나타냅니다.

 

이때 0번째 인덱스 비트는 가장 작은 값을 나타내고 따라서 가장 덜 중요한 비트(LSB: Least Significant Bit)라고 부르며, 7번째 인덱스 비트는 가장 큰 값을 나타내고 따라서 가장 중요한 비트(MSB: Most Significant Bit)라고 부릅니다.

만약 워드사이즈가 16bit라면, 15번 인덱스 비트가 MSB가 되겠지요.

 

어쨋든 저렇게 1을 나타낼 수 있습니다.

비슷하게 5를 나타낸다면 아래와 같이 되겠지요.

8bit 자연수 중 가장 큰 값은 255가 될 것이며 2진수로는 1이 8개 있는 형태가 나옵니다.

이걸로 왜 스타크래프트 유즈맵에서 공격력 방어력 업그레이드가 255까지 밖에 안되고, 유닛 1마리의 킬 수를 총 255까지 밖에 못 세는지에 대한 실마리를 대충 얻을 수 있을 것입니다. 해당 정보를 8bit, 즉 1byte 자료구조로 저장하고 있기 때문이지요.

8bit로 음의 정수 표현하기

그런데 말입니다. 그러면 음수는 어떻게 표현할까요? 자연수의 경우 그냥 우리가 알고 있는 2진수를 때려박으면 쉽게 됩니다. 이제 음수를 표현해보도록 하겠습니다.

일단 음수와 양수를 둘 다 표현하는데는 크게 3가지 방법이 있습니다.

1. Sign & Magnitude (부호와 크기)

2. 1의 보수

3. 2의 보수

 

Sign & Magnitude

일단 이 방식은, bit하나를 부호 비트로 두고, 나머지로 크기를 나타내는 방식입니다.

대충 요런 식인데, 0번 비트부터 6번 비트는 우리가 아는 그 2진수 그대로 가게 되고, 7번 비트는 1이면 음수라서 -1을 곱해주고, 7번비트가 0이면 양수라서 아무일도 하지 않는(+1을 곱하는) 식입니다.

 

일단 이 방식으로 가장 큰 양수를 표현해보도록 하겠습니다.

7번을 뺀 나머지 비트가 다 1이면 127라는 가장 큰 양수를 표현할 수 있습니다.

가장 작은 음수는 모든 비트가 1인 -127을 표현할 수가 있습니다.

-127를 표현한 모습입니다.

 

그런데 이 방식은 조금 문제가 있습니다.

지금 보이는 값은 -0에 해당합니다. 

그렇습니다. 이 방식은 +0과 -0이 둘 다 존재하게 됩니다.

 

그리고 음수에 대한 덧셈이나 뺄셈 계산 시 기존 덧셈과는 다른 방식의 로직 구현이 필요하게 됩니다.

1의 보수

그래서 이러한 점을 보완하기 위한 1의 보수라는 개념(1's Complement)이 있습니다.

1의 보수를 취하게 되면 0인 값들은 1이 되고 1인 값들은 0이 됩니다.

1의 보수 관계에 있는 수는 기존 수에 부호를 전환한 것과 동일하게 됩니다.

즉 00000001 이 +1이면 11111110는 -1이 되는 방식입니다.

 

하지만 이 역시 -0이 존재하는 문제가 있습니다.

위는 -0 아래는 +0입니다. 서로 모든 bit를 negation한 값에 해당합니다.

 

다만 Sign & Magnitude와는 조금 나은 부분이 있는데, 덧셈 연산의 경우 입니다.

-1과 +1을 더한 경우를 한번 보겠습니다.

이 경우가 -1에 해당하고 11111110의 bit 값을 갖습니다.

1의 값을 가지며 00000001의 bit값을 갖습니다.

같이 더하면 11111111이 되고 -0이 됩니다. 

 

0이 2개라서 음수에서 양수가 되는 경우 1의 오차가 있는 것 빼고는 덧셈도 어느정도는 되는 모습입니다.

그리고 표현할 수 있는 값의 범위는 부호&크기 의 경우와 마찬가지로 -127 ~ +127입니다.

2의 보수

2의 보수(2's Complement)에 대해 한번 설명해보겠습니다. 현대 CPU들은 거의 모두 2의 보수를 쓰고 있고, 이전에 소개한 방법들에 비해 장점들만이 명확합니다.

표현법 자체는 간단한것이 정수 표현때와 비슷한데 MSB가 나타나는 값을 +128가 아닌 -128를 하도록 하면 됩니다.

그리고 2의 보수를 취한다는 것은 1의 보수를 취한 뒤 1을 더하면 됩니다.

이 표현법에서 서로 2의 보수 관계에 있는 수는 부호가 바뀐 관계를 갖습니다.

위와 같은 배열을 갖습니다. 가장 큰 양수와 가장 작은 음수를 표현해보겠습니다.

가장 큰 양수의 경우는 아까들과 동일한 +127입니다.

가장 작은 음수는 -128이 됩니다. 이전에는 -127이었는데 왜 -128까지 표현이 가능할까요?

2의 보수 표현법에서는 0이 2개가 아닌 1개이므로 하나의 -0이 차지하던 것이 의미있는 값을 하나 더 표현할 수 있게 된 것입니다.

 

그리고 덧셈을 한번 해보죠. -128과 +127를 더해봅시다.

정확한 값인 -1이 나옵니다.

 

여기서 8을 한번 더 더해봅시다.

11111111 + 00001000을 하면 마지막 올림수인 8번째 bit의 값은 버려지게 되므로 00000111이 됩니다.

정확한 값인 7를 표현할 수 있습니다.

 

이렇게 2의 보수는 다음과 같은 장점이 있습니다.

  • 0이 1개이다.
  • 가장 넓은 범위의 수를 표현 가능하다
  • 음수를 더하면 뺄셈처럼 정확히 동작한다

이러한 이유들로 1의보수나 Sign&Magnitude에 비해 명확한 장점들이 있어서 널리 쓰이는 방식입니다.

개요

이 글은 리눅스 민트에서 도커 컨테이너를 설치하다가 겪은 간단한 에러를 해결한 경험 공유를 하고자 작성했습니다.

 

일단 리눅스 민트는 우분투라고 생각하고 이것저것을 설치하면 된다고 하는데, 그래서 리눅스 민트에서 도커 컨테이너를 설치하면서 도커 공식 홈페이지의 Debian 기준 설치 가이드라인을 따라갔었습니다.

docs.docker.com/get-docker/

 

Get Docker

 

docs.docker.com

위 링크로 가면 도커 설치법이 나옵니다.

일단 리눅스에 설치를 할 것이니, 리눅스를 눌렀죠.

docs.docker.com/engine/install/debian/

 

Install Docker Engine on Debian

 

docs.docker.com

일단 데비안 기준으로 도커 설치법을 하나하나 따라가면서 터미널에 명령어를 쳤습니다.

 

아마 이쯤이었을 것입니다. add-apt-repository 명령어가 자꾸 안되는 현상이 있었습니다.

Malformed repository name 라는 메시지가 자꾸 뜹니다.

 

그러다가 아래 링크를 봤습니다.

linuxhint.com/install_docker_linux_mint/

$(lsb_release -cs)가 들어갈 부분에 bionic 이라는 string이 대신 들어가 있습니다.

 

해결법

sudo add-apt-repository "deb https://download.docker.com/linux/ubuntu bionic stable"

위와 같이 입력하면 됩니다. 보면 [arch=amd64]라는 문자열이 빠져있죠?

그리고 나머지는 도커 공식 홈페이지에서 하라는데로 그대로 하면됩니다.

 

원인파악

정확한 이유는 아닌데, 아마 민트에서 쓰는 add-apt-repository의 다른 버전이 설치가 된 경우가 있을 수 있는데, 그 경우 [arch=amd64]와 같은 문자열이 들어있는 경우를 체크를 못하는 상황이 있는 것 같습니다.

 

사실 add-apt-repository의 심볼릭 링크를 따라가서 처리하는 파이썬 스크립트를 하나하나 분석해보다가 알게된 내용 입니다.

 

도커 설치의 경우만 그런게 아니라 add-apt-repository로 추가 레포지토리를 추가하고 뭔가 설치를 해야 하는 경우 리눅스 민트에서 있을 수 있는 에러입니다.

아마 초등학교 2학년때인가, 학교 방과후 수업의 일환으로 워드프로세서 및 컴퓨터 기초 활용 능력을 배웠던 적이 있다. 그때 처음 Ctrl + C / Ctrl + V와 클립보드의 존재를 배웠었다.

 

클립보드는 복사, 붙여넣기를 할 때 복사를 한 데이터가 운영체제 레벨에서 저장되는 공간이다. 근데 우리는 이렇게 간편하게 클립보드를 사용해서 다양한 데이터들을 복사 및 붙여넣기를 하면서도 정작 클립보드가 어떻게 생겨먹었는지는 잘 모르고 있긴 하다.

 

서식이 있는 텍스트를 복사 붙여넣기 할 때, 서식을 날리기 위해 잠시 메모장에다가 복붙을 하는 경우, 이미지를 복사한 경우 PC버전 카카오톡에 붙여넣기를 하면 바로 이미지가 전송이 되기도 한다. 파일 자체를 복사하는 경우도 있고 이렇게 다양하게 활용을 하고 있는데, 클립보드에는 데이터가 어떻게 저장이 되길래 그렇게 동작하는지 다소 궁금해서 찾아보게 되었다.

 

클립보드 데이터 확인

http://www.peterbuettner.de/develop/tools/clipview/

 

Windows clipboard raw viewer

A developer tool This is a little tool to inspect the windows clipboard in a raw/text way, i use it to find bugs in applications. There is only little help now: Hover with the mouse over the image below, in the application you must hover over the elements.

www.peterbuettner.de

클립보드이 데이터를 볼 수 있는 프로그램이다.

 

좀 오래된 프로그램이긴 하지만, 뭔가를 복사를 한 상태에서 저 프로그램에서 확인 버튼을 누르면 좌측에 현재 클립보드에 있는 데이터의 포맷들이 보이고, 이 포맷들을 누를 시 그 포맷에서 어떻게 데이터가 보이는지 Text 형태로 보여지게 된다.

 

이로서 Format - Text 형태의 Pair들이 여러개 묶여있는 식으로 클립보드에 저장된다는 것을 알 수 있다.

 

그리고 어플리케이션에 붙여넣기를 하면 해당 어플리케이션이 자신이 받아들일 수 있는 포맷 넘버를 기반으로 데이터를 받아서 처리를 하는 것으로 예상된다.

 

위 프로그램의 경우 클립보드의 데이터를 보는 것만 잘 동작하는 것 같다. 그렇다면 클립보드에 내가 원하는 형식의 데이터(crafted data)를 집어넣을수는 없을까?

 

win32clipboard python api

python api중에 win32clipboard라는 녀석이 있다. pywin32라는 걸 설치를 하면 같이 설치가 되는 모양이다.

스택오버플로의 한 부분인데 위와 같이 설치를 해 볼 수 있겠다.

그리고 있는 api들을 사용해보면서 데이터를 써넣을 수 있다.

아래와 같이 코드를 짜보자.

#!/usr/bin/env python3
import win32clipboard

win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardText("plaintext", win32clipboard.CF_TEXT)
win32clipboard.CloseClipboard()

win32clipboard.GetClipboardData(win32clipboard.CF_HDROP)

OpenClipboard를 호출을 하게 되면 다른 앱에서 클립보드를 사용할 수 없게 잠기는 모양이다. 그래서 다시 풀어줘야 한다.

자세한 API 리스트들은 아래 Reference에 링크가 있으므로 거기를 참고해보자.

 

그리고 이 pywin32 api는 결과적으로는 Win32 api를 파이썬으로 포팅한 것이므로, Win32 API를 이용하면 클립보드를 제어할 수 있다는 뜻이다.

 

이 클립보드를 제어 스크립트를 통해서 먼가 재미있는 장난질들을 할 수 있을것도 같은데, 일단은 포스팅은 여기서 줄이도록 하겠다.

 

References

stackoverflow.com/questions/15310121/trying-to-install-module-win32clipboard/15310362

docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard

timgolden.me.uk/pywin32-docs/win32clipboard__GetClipboardData_meth.html

timgolden.me.uk/pywin32-docs/win32clipboard.html

m.blog.naver.com/popqser2/221358295907

www.peterbuettner.de/develop/tools/clipview/

개발이든 뭐든 IT, SW쪽에 몸담고 있는 사람이라면 도커 컨테이너에 대해 한번 쯤 들어봤을 수도 있을 것 같다. 그래서 이건 뭐에 쓰는 놈이고 대략적으로 어떤 특징을 가지고 있으며, 어떤 거를 좀 알아야 할 지를 간단하게 설명하는 글을 작성해보고자 한다.

 

도커 컨테이너는 어디에 쓰는 녀석인가?

도커 컨테이너는 컨테이너라는 이름에 어울리게 무언가를 감싸고 있는 녀석이라고 생각하면 된다. 사용하는 이유와 Use-Case는 다양하겠지만, 사용 목적 자체는 가상머신(Virtual Machine)과 비슷하다.

가상머신을 사용하는 이유는 실제 머신, PC 하드웨어가 여러개 있지 않는 경우 하드웨어가 여러개 있는 것 처럼 SW적으로 사용하기 위해서 가상머신을 활용을 한다. 

하나의 PC에서 두개의 운영체제(윈도우와 리눅스 처럼)를 동시에 돌려서 무언가를 한다던지 하는 경우 가상머신을 사용할 수 있겠고, 두개의 PC가 서로 네트워크 통신을 하는 것 처럼 만들고 싶은 경우도 가상머신을 이용해서 비슷한 환경을 구축할 수 있다.

대충 사용하는 이유는 아래와 같을 수 있다.

  • 샌드박싱을 위해서
  • 간편한 환경 구축을 위해서

샌드박싱을 하는 경우는 일반적으로 보안적인 이유가 클 것이라고 생각된다. 보안회사에서 악성코드 분석 업무를 하시는 분들은 대부분 악성코드 분석을 가상머신 안에서 수행을 한다. 더군다나 동적 분석이라면 무조건적일 것이다. 그리고 환경 구축하는 방법이 복잡할때, 동일한 환경을 다른 하드웨어에서 구성을 해야하거나 배포를 해야 하는 경우, 가상머신 이미지 형태로 배포를 하게 되는 경우 환경 구축이 꽤나 쉬워진다. 집단 교육 등을 하거나 할 때 이런 방식이 편리할 수 있다.

가상머신과 비슷하다면, 그와 비교했을때 도커의 장점은?

가상머신은 하지만 큰 단점이 있다. 일단 리소스 사용량이 엄청나다. 메모리와 CPU를 호스트 OS의 것을 가져다가 게스트 OS에서 쓰는 것이므로, 실제 머신 하나 당 가상머신은 보통 1개정도만 띄우며 사용하며, 여러개를 띄우게 될 경우 스토리지 및 메모리, CPU 모두 사용량이 엄청나다. 그리고 설치 시 운영체제를 각각 새로 설치를 해 주어야 하기 때문에 구성에도 시간이 오래 걸리고, 배포 시에도 VM(Virtual Machine) 이미지는 용량이 매우 큽니다.

하지만 도커 컨테이너의 경우 가상머신과 비슷한 목표(goal)을 이룸에도, 구조가 호스트의 OS와 라이브러리들을 공유하는 형식이므로 구성 및 설치도 훨씬 빠르고 하나의 하드웨어에 수십개의 도커를 띄울 수 있습니다.

도커보다 가상머신이 갖는 강점은?

하지만 또 가상머신이 도커보다 좋은 장점이 있습니다. 아무리 비슷하게 한다고 하지만, 실제 OS까지 별도로 설치하는 가상머신의 경우 실제 환경 재현이 더욱 유사합니다. 물론 Host 운영체제를 설치를 해서 하이퍼바이저 위에서 도는 가상머신보다는 네이티브 베어메탈 운영체제가 제일 잘 맞긴 하겠지만, SW적으로는 가상머신이 환경구축의 일치율은 가장 높습니다. 도커는 그보다는 조금 낮은 일치율을 갖게 되겠지요.

예를 들어서 하드웨어적으로 붙는 장치 드라이버 등을 개발하면서 USB 포트에 해당 장비를 꽂아서 디버깅 및 테스트를 할 때, 네이티브 운영체제에 드라이버를 개발하는 것과, 가상머신안에 있는 운영체제에서 드라이버를 개발하는 것, 분명히 차이가 있겠지요?

그리고 도커 컨테이너의 경우 태생이 LXC(Linux Container)이므로, 아무래도 비 리눅스 계열의 운영체에서는 호환성이 떨어지는 편이라고 볼 수 있습니다. 초창기에는 윈도우 같은 운영체제는 아예 지원을 안하다가 지금은 지원을 하는 것으로 알고 있습니다. Linux Host에서 Linux Docker를 띄우는 경우가 가장 추천되는 상황이며, 윈도우 운영체제에서는 지원은 하지만 호환이 잘 안되는 부분이 아직 있을 수 있습니다.

(이 부분은 나중에 추가적으로 조사해서 덧붙이도록 하겠습니다.)

 

도커와 관련된 것들

도커를 사용할 때 자주 쓰이는 것들에 대해 학습을 해 놓으면 좋습니다.

Dockerhub

도커는 도커 이미지와 도커 컨테이너 이렇게 두개로 있다고 보면 되는데, 도커 이미지는 붕어빵 틀에 해당되며, 도커 컨테이너는 붕어빵이라고 보면 됩니다. 쉽게 생각하면 OOP에서 class와 object입니다. 도커 컨테이를 생성할려면 이미지를 기반으로 생성을 하게 되는데, 이 도커 이미지들이 모여있는 것이 도커허브입니다. 깃허브같은 느낌이지요. 그래서 깃허브에서 git clone을 하듯이 도커는 docker pull이라는 명령어로 도커허브에서 이미지를 받아올 수 있습니다. 이 도커허브에는 Ubuntu18.04 처럼 베이스가 되는 이미지부터, 이것저것 설정이 잘 되어 있는 이미지까지 다양하게 있으며, 여러분들도 도커 허브에 이미지를 올릴 수 있습니다.

Dockerfile

도커 이미지는 파일이 몇백 MB부터 GB단위 이상으로 커질 수 있습니다. 따라서 공유할때 엄청 무거운 대상이 되는데요, Dockerfile이라는 텍스트 형태의 파일을 이용해서 도커이미지의 내용을 기술해줄 수 있습니다. 깃헙 같은 곳에 dockerfile만 공유해도 쉽게 도커 이미지를 공유할 수 있는 셈이지요. 정확하게는 도커 이미지와 1대 1 대응이 되지는 않지만, 간편하게 환경을 공유할 수 있는 것은 사실입니다.

Docker compose

도커 컨테이너를 여러개 구성한 뒤, 포트포워딩 등을 이용해서 여러 컴포넌트가 서로 상호작용하는 마이크로 서비스를 배포하고자 할 때, Docker compose라는 걸 사용하면 편리합니다. 도커 이미지 하나를 쉽게 만들기 위해서 dockerfile을 사용한다면, 여러개의 도커 컨테이너들의 구성을 쉽게 구성하고 공유하기 위해서 docker-compose를 사용할 수 있습니다.

 

직전 포스팅에 이어서, 이번 포스팅에서는 Flat File Database의 일종인 SQLite3를 이용하여 데이터베이스의 튜토리얼에 해당하는 CRUD 연산들을 실습해보도록 하겠습니다.

 

일단 실습 환경은 저는 Windows 10에서 실시를 할 예정인데, 사실 운영체제는 크게 중요하지는 않습니다.

 

SQLite DB Browser 설치

SQLite3는 파일 하나만 설치하면 되므로 비교적 설치가 간단합니다.

sqlitebrowser.org/

 

DB Browser for SQLite

DB Browser for SQLite The Official home of the DB Browser for SQLite Screenshot What it is DB Browser for SQLite (DB4S) is a high quality, visual, open source tool to create, design, and edit database files compatible with SQLite. DB4S is for users and dev

sqlitebrowser.org

SQLite3 파일을 다룰 수 있는 프로그램은 다양한 종류가 있지만, 이번에는 DB Browser for SQLite라는 프로그램을 이용해보도록 하겠습니다. 위 공식 사이트 링크로 이동한 뒤, Download 탭에 들어가서 자신의 운영체제에 맞는 프로그램을 설치해주시면 됩니다.

저같은 경우는 Installer를 이용해서 64bit 운영체제용으로 설치를 했습니다. 3번째에 해당하는 DB Browser for SQLite - Standard installer for 64-bit Windows를 눌러서 설치를 하면 되겠습니다.

그리고 시작메뉴에 검색을 해서 실행을 하면 되겠습니다.

데이터베이스와 테이블 생성

이제 데이터베이스와 테이블을 생성해야 합니다. SQLite3에서 데이터베이스는 하나의 파일에 해당합니다. 그리고 테이블은 이전 포스팅에서 언급했던 Relation(표)에 해당됩니다.

그리고 새 데이터베이스를 눌러서, 파일을 하나 생성합니다. 이제 이 파일에 데이터베이스 값들이 저장되게 됩니다.

그러면 곧 이어 테이블을 생성하라는 창이 뜨게 됩니다.

이 창에서 테이블을 생성할 수 있는데, 아래에는 SQL 구문이 있고, 중간에 있는 필드 부분을 GUI로 생성을 하면 테이블을 생성하는 SQL 구문이 자동으로 생성됩니다.

간단하게 사람이름과 생년월일을 입력으로 받는 테이블을 만들어보도록 하겠습니다.

NN, PK, AI, U와 같은 필드가 있는데 각각은 Not Null, Primary Key, Auto Increment, Unique입니다. 이 부분들은 나중에 언급하도록 하고, 일단 위와 같은 방식으로 만들어 봅니다.

테이블에 데이터 CRUD 연산

이제 생성한 테이블에 CRUD 연산 별 SQL 구문을 간단하게 알아보겠습니다. 일단 데이터를 생성을 해야겠지요.

데이터를 user 테이블에 삽입해보도록 하겠습니다.

Create - Insert into 구문

SQL 실행이라는 탭을 눌러서 위와 같이 입력 해 보도록 합니다. 그리고 재생버튼같이 생긴 버튼을 눌러서 SQL 구문을 실행해보도록 합니다.

그러면 아래에 결과가 나옵니다.

이제 데이터 보기를 눌러서 데이터가 들어간 것을 확인할 수 있습니다.

Read - Select 구문

이제 SQL Select 구문을 이용해서 데이터를 조회해보도록 하겠습니다.

위와 같이 입력하면 user 테이블에 있는 모든 정보를 확인할 수 있습니다.

위와 같이 where에 조건을 넣어서 number가 1인 row만 확인해볼 수 있습니다.

아니면 위와 같이, number가 1보다 큰 row의 name column만 확인도 가능합니다.

Update - Update 구문

이제 데이터를 수정해봅시다. number가 2인 리처드 파인만은 컴퓨터공학자가 아니므로, 이를 찰스 배비지로 바꾼다고 해봅시다.

잘 변경이 되었는지 select 문으로 확인해봅시다.

원하는대로 잘 바뀌었습니다.

Delete - Delete from 구문

이제 row를 삭제하는 SQL 구문을 한번 해보도록 합시다.

number가 3인 필드를 삭제하는 SQL 구문입니다.

잘 삭제된 것을 확인해볼 수 있습니다.

직전 포스팅에서 엑셀과 데이터베이스를 간단하게 비교를 해 봤는데, 이번 글은 직전 포스팅의 후속글입니다. 따라서 예상 독자도 이전글과 동일하게 엑셀로 현업 실무를 하는, 개발자가 아닌 사람입니다.

 

이번 포스팅에서는 DBMS의 몇가지 종류와 특징들, 그리고 SQL이 무엇인지와 스키마에 대해 간략하게 알아보고자 합니다.

데이터베이스 프로그램(DBMS: Database Management System)의 종류

데이터베이스를 다룰 수 있는 컴퓨터 프로그램들이 어떤 것이 있는지를 구체적으로 알아봅시다. 사실 데이터베이스 프로그램 뿐만 아니라 데이터베이스의 패러다임도 다양한 종류가 있습니다. 일단은 가장 전통적이면서도 강력한 관계형 데이터베이스(Relational Database, RDB) 프로그램에 대해 알아보도록 하겠습니다.

햄버거에 맥도날드, 롯데리아, KFC가 있듯이 DBMS에는 sqlite3, mysql, mariadb, postgre-sql, oracle db 시리즈, mongodb, cassandra 등등 여러가지가 있습니다.

사실 근래들어서 새로운 database의 한 패러다임인 nosql db라는 것도 있는데 해당 내용은 나중에 시간이 남게 되면 추가적으로 다루며, 이번에는 다루지 않겠습니다.

일단 데이터베이스를 다루는 프로그램을 두개의 범주로 나누어 보겠습니다.

파일형 Database(Flat-file database)

엑셀의 경우 그 데이터 값들은 하나의 파일에 다 들어가 있습니다. 마찬가지로 데이터베이스 중에서 하나의 파일 형태로 존재하는 데이터베이스가 있습니다. 대표적인 예시로 SQLite3가 있습니다. 그 외에도 마이크로소프트 액세스 같은 것들도 파일형 데이터베이스를 지원합니다. 이어서 나오는 서버형 데이터베이스에 비해서는 처리할수 있는 규모나 성능은 다소 떨어질 수 있으나, 파일 하나에 데이터가 다 들어간다는 간편성이 있습니다.

서버형 데이터베이스

개발자들이 자주 사용하는 형식의 데이터베이스인 서버형 데이터베이스입니다. 실제 IT 서비스에서 자주 사용되는 데이터베이스들이며 파일형 데이터베이스에 비해 큰 규모의 데이터들도 쉽게 처리하며, 하나의 파일 형태로 존재하는 것이 아닌 하나의 컴퓨터에서 지속적으로 실행되고 있는 형태의 프로그램으로 존재합니다. 따라서 그 컴퓨터와 네트워크 연결이 가능한 다른 컴퓨터에서 원격으로 데이터들을 조회하거나 다룰 수 있으며 따라서 여러명의 사용자가 동시에 사용하는 것도 가능합니다. 이에 따라 인증 및 권한과 동시성과 관련된 다양한 복잡한 기능들도 제공을 합니다. 이에 해당하는 DBMS에는 Mysql, MariaDB, PostgreSQL, 오라클 데이터베이스 등이 있습니다.

 

SQL(Structured Query Language)

직역하면 구조화된 질의어에 해당합니다. SQL은 선언형 프로그래밍 언어에 해당하며, 관계형 데이터베이스 관리 시스템(RDBMS)을 사용하기 위한 컴퓨터용 언어입니다. 엑셀의 경우 GUI(Graphical User Interface)를 지원하기 때문에, 눈에 보이는 버튼들을 누르고 셀을 클릭한 뒤 값을 입력하거나 하는 식으로 직관적인 방법으로 데이터들을 처리할 수 있지만, 앞서 위에서 언급한 (관계형) 데이터베이스들은 데이터를 처리하기 위해서 SQL이라고 하는 프로그래밍 언어를 작성한 뒤 이를 이용해서 명령을 내려야 합니다.

데이터를 조회할때는 Select 구문, 수정할때는 Update 구문, 생성할때는 Insert into 구문, 제거할때는 delete from 구문이라는 프로그래밍 언어 문법(Syntax)에 맞는 SQL 구문을 작성한 뒤, 이를 DBMS에 전송해서 작업들을 수행할 수 있습니다.

따라서 이 RDBMS 종류를 다루기 위해서는 SQL이라는 언어를 잘 알아야 하는 것이지요. 혹은 이 SQL 구문들을 자동으로 생성해서 동작하게 끔 하는 프로그램을 별도로 개발한다면, 엑셀을 다루듯이 클릭 등의 직관적인 방법으로 데이터를 다룰 수 있게 할 수 있습니다.

 

관계형 데이터베이스와 스키마

관계형 데이터베이스(Relational-Database)

앞에서 관계형 데이터베이스에 대해서 자세한 설명을 하지 않고 넘어왔지만, 여기서 간략하게 관계형 데이터베이스가 무엇인지 짚고 가겠습니다.

데이터베이스를 어떤 식으로 저장할지는 여러 패러다임들이 있었지만 지금 가장 많이 쓰이는 형식이 관계형 데이터베이스(Relational Database)이며, 데이터베이스를 표 형태로 표현한다라는 뜻이라고 보시면 됩니다.

위 표를 보시면, 간단한 용어를 확인할 수 있는데, Relation(관계)는 표 자체를 뜻하고, Attribute는 특성값이라고 보시면 됩니다. 사실 엑셀도 표 형식이니 이해하기 어렵지 않을 것입니다.

엑셀에 위와 같은 데이터가 있다고 하면, 첫번째 행에 있는 이름, 국어, 영어, 수학 들은 attribute에 해당하며, 각각의 2번째 3번째 행은 tuple에 해당하게 됩니다. 이것이 표 형태로 있는 것이 relation이 됩니다.

이런 용어들은 크게 신경쓸 필요 없고, 다만 관계형 데이터베이스는 데이터를 위와 같은 표 형태로 나타낸다 라는 것만 기억하시면 됩니다.

데이터베이스 스키마

앞에서 관계형 데이터베이스가 표 형식을 따른다고 했습니다. 이때 이 "표의 형태가 데이터베이스 스키마"입니다. 

위에 예시에서 들은 2명의 학생의 국어,영어,수학 성적에 대한 Relation의 스키마를 표현하자면 다음과 같겠습니다.

이름이라는 attribute는 text 데이터를 저장하고, 국어와 영어 수학이라는 attribute는 정수 값을 저장하는 형태를 띱니다.

이 내용 자체가 성적과 관련된 저 Relation의 스키마입니다.

그리고 이 Relation은 RDBMS 및 SQL에서는 테이블이라고 부릅니다. 테이블은 직역하면 표라는 뜻을 갖지요.

 

References

ko.wikipedia.org/wiki/%EA%B4%80%EA%B3%84%ED%98%95_%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

namu.wiki/w/Microsoft%20Access

namu.wiki/w/DBMS

ko.wikipedia.org/wiki/%ED%94%8C%EB%9E%AB_%ED%8C%8C%EC%9D%BC_%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

이 글은 개발자가 아닌 주변 지인들로부터, 현업에서 엑셀로 데이터를 처리하다가 여러가지 한계점을 느끼고 데이터 베이스를 배우고자 하는 니즈가 있다는 것을 알게 되어, 이와 비슷한 경험을 겪은 사람들을 위해 작성된 글입니다.

따라서 추가적으로 질문이나 요청사항이 있다면 이 글에 댓글 등의 방법으로 피드백을 남겨주시면 글에 내용을 추가하거나 추가 글을 작성하도록 하겠습니다.

------------------------------------------------------------------

엑셀 vs 스프레드 시트

일단 용어에 대해 살짝 명확하게 하고 가자면, 스프레드시트와 엑셀은 의미에 있어서 차이가 있습니다. 스프레드시트는 표 형식으로 데이터를 처리할 수 있는 컴퓨터 프로그램을 뜻합니다. 엑셀은 마이크로소프트라는 회사에서 만든 스프레드 시트 프로그램의 상표명이라고 볼 수 있죠. 마치 스프레드 시트와 엑셀의 관계는 유리테이프와 스카치테이프, 고체풀과 딱풀, 라면과 삼양라면, 햄버거와 불고기버거의 관계라고 볼 수 있습니다. 엑셀외에도 한컴오피스에서 만든 한셀이라는 프로그램도 스프레드시트 프로그램 중 하나입니다. 엑셀이 제일 유명한 스프레드 시트 프로그램이니, 앞으로는 편의상 엑셀이라고 칭하겠습니다.

 

엑셀을 왜 쓸까요?

엑셀은 표 형식으로 데이터를 처리하는 스프레드시트 프로그램입니다. 따라서 표 형식으로 나타내면 편리한 데이터들을 처리하기 위해서 씁니다.

CRUD 데이터 처리

여기서 데이터 처리라는 것은 데이터 쓰기, 읽기, 바꾸기, 지우기와 같은 동작들인데, 이 용어들을 영어로 표현하면 Create, Read, Update, Delete인데 이 단어들의 앞글자를 따면 CRUD가 됩니다. 이 CRUD는 컴퓨터 프로그램에서 데이터를 처리할때 가장 기본이 되는 4가지 동작들입니다. 엑셀은 이 4가지 동작이 다 손쉽게 가능합니다.

Create를 할 때에는 엑셀의 빈 셀에 값을 쓰면 되고, Read는 해당 셀을 클릭해서 데이터를 확인해볼 수 있습니다. 검색이 필요한 경우 Ctrl + F를 눌러서 검색이 가능합니다. Update(수정)가 필요할때는 셀을 클릭해서 새로운 값을 입력하거나, F2를 누른 뒤 값을 변경하면 됩니다. Delete(삭제)는 해당 셀을 클릭한 뒤 키보드 delete 키를 누르면 됩니다.

다소 복잡한 데이터 처리

데이터 정렬이 필요할때에는, 칼럼 이름에 필터를 건 뒤, 오름차순 혹은 내림차순으로 정렬하기를 누르면 쉽게 정렬이 됩니다. 그 외에도 평균값을 구하거나, 총합을 구하거나, 순위를 구하거나 등의 다소 복잡한 데이터 연산이 필요한 경우 엑셀에 내장된 함수들을 이용해서 계산을 할 수 있습니다.

 

데이터베이스는 뭘까요?

사전적인 의미로는 여러사람들이 공유하여 사용할 목적으로 체계화하여 통합 및 관리하는 데이터 집합이라고 합니다. 데이터를 쉽게 잘 다룰 수 있도록 잘 해놓은 데이터 덩어리라고 보면 된다. 사실 기본적인 내용들은 엑셀과 다를 바 없는 것이, 똑같이 데이터를 다루고, CRUD 연산도 당연히 잘 됩니다.

Database vs DBMS(Database management system)

여기서 데이터베이스(Database)와 DBMS(Database Management System)의 차이를 살짝 짚고 넘어갑시다. 데이터베이스는 데이터들 모여있는 것, 데이터 그 자체를 의미하고, DBMS는 데이터 베이스를 처리하는 컴퓨터 프로그램입니다.

눈치채었을 수 있겠지만, 이제 언급할 것들은 사실 데이터베이스가 아닌 DBMS의 특징들이라고 볼 수 있습니다. 그냥 데이터베이스는 추상적인 개념들이고, DBMS는 우리가 당장 실무에서 써먹을 수 있는 녀석들이니 말입니다.

 

엑셀 vs 데이터베이스(DBMS)?

엑셀과 데이터베이스의 장단점을 매우 간단하게 비교하면 이렇습니다. 엑셀이 배우기는 훨씬 쉽고 직관적입니다. 데이터베이스는 익히고 사용하는데 지식이 더 많이 필요하고 어렵습니다. 

대신 데이터베이스는 엑셀보다 더 좋은 성능과 기능들을 가지고 있습니다. 여기서 더 좋은 성능이라 함은, 엑셀의 경우 데이터가 몇 만개 정도만 되어도 컴퓨터가 버벅이고 느려지고 처리가 힘들어질 수 있으며, 데이터 개수의 제한이 명확하지만 데이터베이스의 경우는 그 보다 더 많은 개수의 데이터도 쉽고 빠르게 처리할 수 있습니다. 또한 엑셀에서 처리하는 것 보다 더 복잡할 수 있는 연산들을 처리할 수 있습니다.

그리고 엑셀의 경우 남과 데이터를 공유하려면, 엑셀파일을 저장한 뒤, 이 파일을 이메일 등을 통해서 공유를 해야 하는데, 데이터베이스의 경우 실시간으로 데이터 편집한 부분을 반영시켜서 남과 동시에 작업이 가능합니다.

구분 엑셀(스프레드시트) 데이터베이스(DBMS)
장점 직관적, 익히기 쉽다 사용 시 쿼리 언어에 능숙해야함
데이터 모델링 시 지식과 숙련도 필요
단점 많은 데이터 처리시 느려짐(저성능)
다소 복잡한 내용 처리 어려움
동시작업 불가능
많은 데이터 쉽게 처리(고성능)
복잡한 처리 가능
동시작업 가능

뭐 쉽게 생각하면, 데이터베이스가 익히기는 어렵지만 엑셀에서 할 수 있는 것들 대부분을 다 할 수 있고 성능도 더 좋고 강력합니다. 하지만 데이터 베이스를 잘 쓰기 위해서는 데이터베이스용 쿼리 언어인 SQL을 사용할 줄 알아야 하고, 데이터 모델링에 대한 지식과 숙련도도 필요합니다.

실제 소프트웨어 회사에서 데이터베이스의 성능을 극대화까지 끌어내기 위해서, 이러한 SQL 쿼리 작성과 데이터 모델링만 전문적으로 행하는 DBA라는 직무가 따로 있을 정도로 깊게 들어가면 매우 어려운 영역입니다.

하지만 그렇게까지 복잡하지 않은 데이터들을 처리할때에는 가볍게 공부해서 시도해볼 수 도 있을 것 같습니다.

References

ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

www.oppadu.com/%EC%97%91%EC%85%80-vs-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-1%ED%83%84/

www.oppadu.com/%EC%97%91%EC%85%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B9%84%EA%B5%90/

opentutorials.org/course/3162/19527

https://www.datacamp.com/community/tutorials/role-underscore-python

 

Role of Underscore(_) in Python

In this tutorial, you're going to learn about the uses of underscore(_) in python.

www.datacamp.com

이 글은 위 글의 번역 및 정리 글입니다.

 

파이썬관련 찾아볼 일이 있어서 보다가 언더바에 대한 내용들이 있어서 한번 정리합니다.

파이썬 개발자들 중 많은사람들이 파이썬에서 언더바의 역할에 대해 잘 모르는데, 이를 이용해서 코드 생산성을 늘릴 수 있습니다. 이번기회에 한번 알아봅시다.

for _ in range(100)
__init__(self)
_ = 2

만약 당신이 파이썬 개발자라면, 위와 같은 파이썬 코드들을 본 적이 있을 것입니다.

위에서 _는 각각 다른 조건에서 특별한 의미를 갖습니다.

 

일단 6개의 다른 용도가 있는데 차근차근 알아봅시다.

  1. 인터프리터에서의 사용
  2. 무시하는 값
  3. 루프에서 사용
  4. 숫자값의 분리
  5. 명명용
    1. 앞에 하나가 쓰이는 경우
    2. 뒤에 하나가 쓰이는 경우
    3. 앞에 두개가 쓰이는 경우
    4. 앞 뒤로 두개씩 쓰이는 경우

이제 예제들과 함께 알아봅시다.

 

1. 인터프리터에서 사용

파이썬 인터프리터에서 가장 마지막 표현식의 결과값을 자동적으로 "_"라는 변수에 저장합니다. 물론 여기 저장된 값을 다른데다가 저장할 수 있습니다.

일반적인 값처럼 쓸 수 있습니다.

>>> 5 + 4
9
>>> _
9
>>> _ + 6
15
>>> _
15
>>> a = _
>>> a
15

위와 같이 동작하는걸 볼 수 있습니다.

2. 무시하는 값

언더바는 무시하는 값으로도 쓰일 수 있습니다. 해당 값을 unpack하기 싫다면, 그냥 _에다가 할당하면 됩니다.

## 값을 버립니다.
a, _, b = (1, 2, 3) # a = 1, b = 3, _에 2가 할당됩니다.
print(a, b)

## 여러개 값 버리기
## *(변수) 는 unpack할때, 여러개의 값을 하나의 변수에 저장할때 쓰입니다.
## 이는 확장된 Unpacking이라고 불리며,Python 3.x 버전에서만 가능합니다.
a, *_, b = (7, 6, 5, 4, 3, 2, 1)
print(a, b)

테스트를 해보니, _에다가 할당하면 _도 일반 변수처럼 쓰이고, 1번처럼 Last expression을 저장하지 않게 됩니다. del _를 통해 해당 변수를 삭제하면 1번때처럼 Last expression을 저장하게 됩니다.

 

3. 루프에서 사용

for 루프를 돌 때 사용할 수 있습니다. 아래처럼 쓰는 것도 하나의 방법이 됩니다.

## _를 이용해서 루프를 돕니다.
for _ in range(5):
    print(_)

## 리스트 순회를 _를 이용해서 합니다.
## _ 를 일반 변수처럼 사용할 수 있습니다.
languages = ["Python", "JS", "PHP", "Java"]
for _ in languages:
    print(_)

_ = 5
while _ < 10:
    print(_, end = ' ') # 'end'의 기본값은 '\n'인데 이걸 변경해줍니다.
    _ += 1

실행결과는 예상한 것과 같게 아래처럼 나옵니다.

0
1
2
3
4
Python
JS
PHP
Java
5 6 7 8 9

4. 숫자값의 구분

숫자값이 길다면, 자릿수 구분을 위해 _를 중간중간에 넣어줄 수 있습니다.

이진수값이나 16진수, 8진수 값도 동일하게 적용할 수 있습니다.

## 여러 숫자 표현법
## 아래 값들이 정확한지 확인하기위해 int 함수를 써 볼수도 있습니다.
million = 1_000_000
binary = 0b_0010
octa = 0o_64
hexa = 0x_23_ab

print(million)
print(binary)
print(octa)
print(hexa)

실행 결과는 아래와 같습니다.

1000000
2
52
9131

5. 언더바를 포함한 변수명들

 변수, 함수, 클래스 명 등에 언더바가 사용될 수 있습니다.

  • 앞에 하나의 언더바 _variable
  • 뒤에 하나의 언더바 variable_
  • 앞에 둘의 언더바 __variable
  • 앞과 뒤에 두개의 언더바 __variable__

5.1 앞의 하나의 언더바

앞에 하나의 언더바로 시작하는 이름은, 내부 사용용입니다(internal use only). 일단 예시부터 봅시다.

class Test:

    def __init__(self):
        self.name = "datacamp"
        self._num = 7

obj = Test()
print(obj.name)
print(obj._num)

위 코드를 실행하면 아래처럼 됩니다.

datacamp
7

변수명 앞에 _를 하나 붙였다고 해서, 해당 변수를 접근하지 못하게 되진 않습니다. 하지만 모듈을 import해서 쓰는 경우 효과가 발생합니다.

아래와 같은 코드를 한번 확인해봅시다.

## filename:- my_functions.py

def func():
    return "datacamp"

def _private_func():
    return 7

이제 my_functions.py를 가져오기 위해 import 구문을 써보자. 파이썬은 언더바 하나로 시작한 이름들은 import하지 않는다.

>>> from my_functions import *
>>> func()
'datacamp'
>>> _private_func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_private_func' is not defined

위의 코드 결과를 보다시피, _로 시작한 _private_func는 찾지를 못한다.

위와 같은 에러를 방지하기 위해, from module import *가 아닌 모듈 자체를 import를 해보자. 

>>> import my_functions
>>> my_functions.func()
'datacamp'
>>> my_functions._private_func()
7

앞에 _ 1개로 시작한 이름은 내부 사용 전용이라는 뜻이다.

5.2 뒤의 하나의 언더바

가끔 파이썬 키워드에 해당하는 이름으로 변수명, 함수명, 클래스명으로 쓰고 싶을때가 있을 수 있다. 이럴때 이 방법이 유용하다. 마지막 부분에 언더바를 하나 추가함으로써 파이썬의 기본 키워드들과 충돌하는거를 방지할 수 있다.

>>> def function(class):
  File "<stdin>", line 1
    def function(class):
                 ^
SyntaxError: invalid syntax
>>> def function(class_):
...     pass
...
>>>

마지막에 하나의 언더바를 추가하는 명명법은 파이썬 키워드와 겹치는 경우를 방지하고 싶을때이다.

5.3 앞의 두개의 언더바

앞에 두개의 언더바로 시작하는 명명법은 name mangling이다. mangle은 짓이기다라는 뜻인데, 알아보기 힘든 모양으로 만든다라는 의미로 볼 수 있겠다.

앞에 두개의 언더바로 시작하는 명명법은 파이썬 인터프리터에게 해당 서브클래스의 attribute 이름을 바꾸어서 이름 충돌이 나지 않게 하라고 말하는 것이다. 서브클래스 이야기가 나온걸로 봐서 상속하는 경우와 관련이 있는 것 같다.

 

일단 예시를 보자.

class Sample():

    def __init__(self):
        self.a = 1
        self._b = 2
        self.__c = 3
obj1 = Sample()
dir(obj1)

dir한 결과는 아래와 같이 나온다.

['_Sample__c',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_b',
 'a']

dir함수는 클래스 객체의 모든 attribute들을 리턴해준다. 선언한 변수들을 dir 리턴값 중에서 한번 찾아보자.

a와 _b는 잘 보인다. 맨 마지막에 있다. _b는 내부 사용용으로만 쓰인다.

Sample클래스에 분명히 __c라는 맴버변수를 선언했는데, __c는 없고 대신 _Sample__c라는 값이 있다.

이것이 name mangline이다. 나중에 다른 클래스가 Sample이라는 클래스를 상속할때, 그 클래스에서 이 변수를 override하는 걸 방지해준다.

 

다른 클래스를 만들어서 Sample이라는 클래스를 상속해보자.

class SecondClass(Sample):

    def __init__(self):
        super().__init__()
        self.a = "overridden"
        self._b = "overridden"
        self.__c = "overridden"
obj2 = SecondClass()
print(obj2.a)
print(obj2._b)
print(obj2.__c)

상속을 한 뒤, 초기화를 하는 과정에서 Sample의 맴버변수들에 값을 다 대입하고 있다.

overridden
overridden



---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-2-4bf6884fbd34> in <module>()
      9 print(obj2.a)
     10 print(obj2._b)
---> 11 print(obj2.__c)


AttributeError: 'SecondClass' object has no attribute '__c'

__c를 찾지 못해서 에러가 발생했다. name mangline때문에 obj2.__c가 아닌 obj2.__SecondClass__c로 바뀌었다. obj2._SecondClass__c를 한번 출력해보자.

print(obj2._SecondClass__c)
overridden

편의상 결과도 같이 썼다.

그러면 _Sample__c는 어떻게 되었는지 한번 보자.

print(obj1._Sample__c)
3

mangle 된 이름으로 잘 있다.

 

맴버함수(메소드)를 이용해서 mangle 된 두개의 언더바로 시작하는 변수를 접근할 수 있다. 예시를 보자.

class SimpleClass:

    def __init__(self):
        self.__datacamp = "Excellent"

    def get_datacamp(self):
        return self.__datacamp

obj = SimpleClass()
print(obj.get_datacamp()) ## "Excellent"를 출력한다. __datacamp에 해당하는 값이다.
print(obj.__datacamp)     ## 여기서 에러가 발생한다. 변수 이름이 바뀐다.

메소드(getter, setter)안에서는 잘 접근을 한다. 하지만 외부에서 멤버 변수로 직접 접근하려고 하면 에러가 난다.

 

언더바 2개로 시작하는 명명법은 변수뿐만 아니라 메소드 이름에도 적용이 가능하다. 예시를 보자.

class SimpleClass:

    def __datacamp(self):
        return "datacamp"

    def call_datacamp(self):
        return self.__datacamp()

obj = SimpleClass()
print(obj.call_datacamp()) ## __datacamp()의 리턴값과 같다.
print(obj.__datacamp())    ## 여기선 에러가 난다.

__로 시작한 함수는 안에서만 호출이 되는 용도라고 보면 될 것 같다.

다른 객체지향 언어에서 private access modifier(접근 지정자)의 역할이라고 보면 될 것 같다.

 

다른 예시를 한번 보자.

_SimpleClass__name = "datacamp"

class SimpleClass:

    def return_name(self):
        return __name

obj = SimpleClass()
print(obj.return_name()) ## "datacamp"를 출력하게 된다.

위와 같은 코드도 정상적으로 동작을 한다.

 

좀 특이한 방식의 개념이다.

5.4 앞뒤로 2개의 언더바

파이썬에서 앞뒤로 2개의 언더바로 둘러쌓인 명명법을 본 적이 있을 것이다. 이녀석들은 매직 메소드(magic method) 혹은 dunder 메소드라고 불린다.

class Sample():

    def __init__(self):
        self.__num__ = 7

obj = Sample()
obj.__num__

이 명명방식을 변수 이름으로 사용할 경우 변수명 충돌이 일어날 수 있기 때문에 가급적 삼가하는게 좋다.

dunder method에서 dunder는 double under(score)를 뜻하며, 보통 연산자 오버로딩을 할 때 많이 사용한다고 한다.

매직 메소드는 __init__, __add__, __len__, __repr__등이 있다.

 

__init__메소드는 생성자라고 보면된다. 호출없이 초기화할때 호출되는 함수이며, 클래스의 인스턴스가 생성될때, 실행되게 된다.

 

__repr__ 메소드는 해당 클래스를 출력하고자 할때 내부적으로 호출되는 메소드라고 보면 된다. 자바로 치면 toString() 함수와 유사하다고 볼 수 있겠다.

References

https://www.geeksforgeeks.org/dunder-magic-methods-python/

+ Recent posts