본 포스팅은 개인적으로 진행한 토이프로젝트를 진행하면서 문제를 해결한 과정등을 정리한 포스팅입니다.


웹 브라우저에서 동작하는 네이버 뮤직은 어떻게 플러그인 하나 없이 스트리밍으로 노래를 들을 수 있을까? 라는 단순한 의문으로 간단하게 조사를 해 본적이 있는데, 네이버 뮤직 PC버전은 Http Live Streaming이라는 프로토콜을 이용해서 음원 스트리밍을 한다는 것을 알게 되었다.


HLS는 기존의 HTTP 웹서버로 쉽게 구성할 수 있도록 되어있어서 웹 서버 개발 경험이 있으면 쉽게 구현할 수 있을 것으로 보였습니다. 또한 스푼이라고 하는 라디오 방송 서비스에서도 HLS를 이용해서 라이브 음성 스트리밍으로 방송을 하도록 구현되어 있어서, 한번 비슷하게 만들어보기로 결정했다.


만들려고 하는 앱의 기능적 요구사항들은 다음과 같습니다.


1. 방송을 하는 사람은 웹 브라우저로 해당 방송을 하는 페이지로 접속합니다. 그러면 브라우저에 있는 마이크 API를 통해서 마이크에 녹음된 음성 정보를 일 정주기마다 서버로 보냅니다.

2. 서버는 받은 음성 정보를 처리해서 HLS로 전송가능한 형태로 바꿉니다.

3. 방송을 청취하는 사람은 웹 브라우저로 해당 방송을 청취하는 페이지로 접속합니다. 그러면 HLS를 통해서 방송하는 사람의 음성을 실시간으로 듣습니다.


HLS(Http Live Streaming) 프로토콜에 대한 내용은 네이버 d2에 잘 정리된 글이 있어서 해당 글을 보며 공부했다.

http://d2.naver.com/helloworld/7122


개략적인 HLS의 동작 방식을 확인한 후 비슷한 프로젝트가 있는지 한번 찾아보았더니 유사한 프로젝트가 있었다.


https://github.com/mjrusso/livestreaming-js


다만 조금 걱정이 되는 것은, 사용하는 nodejs 버전이 v0.2대 버전이고 최근 커밋이 2011년도이므로 현재 2018년인 상황에서 코드가 제대로 동작할지가 의문이었습니다. git clone을 받아서 실제로 필요한 파일들을 설치해서 구동을 시도했는데, 아니나 다를까 많은 에러 투성이었습니다.


nodejs 버전업이 많이 되면서 해당 프로젝트에서 사용한 API가 이미 deprecated되서 사라진 경우도 있었고, 결정적으로 ffmpeg의 SDK를 이용해서 미디어 데이터를 처리하는 C소스코드가 포함되어 있는데, 이 C API가 맞지 않아서 컴파일이 안되었다. 2011년도 버전 ffmpeg SDK를 사용하려고 했는데, 홈페이지에서도 구할 수 없을 만큼 오래된 SDK이라서 해당 프로젝트의 아키텍쳐와 문서를 참고해서 직접 새로 만들기로 했다.

또한 실제로 빌드가 된다고 하더라도, 해당 프로젝트는 파일 업로드를 직접 해서 스트리밍을 시키는 방식이고, 내가 원하는 프로젝트는 브라우저 마이크 API로 실시간으로 녹음되는 음성을 스트리밍 하는 것으로 조금 차이가 있다.


해당 서버를 단계별로 개발해보기로 했다.


일단 네이버 뮤직에서 m3u8파일과 ts파일들을 다운로드받아서 추출한 뒤, nodejs로 만든 간단한 서버로 뿌려주는 식으로만 작성해보았다.

Nodejs 패키지중에 hls-server라는 패키지가 있어서 확인해보았다.

https://www.npmjs.com/package/hls-server


문서가 많이 친절하지는 않지만, 대략적인 사용방법 예제코드들이 있다.

HLS 서버는 http 서버의 미들웨어 형태로 삽입되는 형식으로, http 요청 처리하는 로직 이전에 먼저 프로세싱을 하게 된다.


 

var server = http.createServer(function(req, res) { //HTTP 요청을 처리함. }); var hls = new HLSServer(server, { provider: { exists: function (req, callback) { // hls 미들웨어가 삽입된 서버에서 요청이 올 때마다 호출되는 함수 callback(null, true) // 파일이 존재하고 스트리밍을 할 경우 호출하는 콜백 callback(new Error("Server Error!")) // 500 error시 호출하는 콜백 callback(null, false) // 404 error시 호출하는 콜백 }, getManifestStream: function (req, callback) { // 적절한 .m3u8 파일을 리턴한다. // "req" is the http request // "callback" must be called with error-first arguments callback(null, myNodeStream) // or callback(new Error("Server error!"), null) }, getSegmentStream: function (req, callback) { // 적절한 .ts 파일을 리턴한다. callback(null, myNodeStream) } } }) server.listen(PORT);


npm 홈페이지에 있는 Using In-Memory Stream이란 탭에 있는 예제코드이다. 추가적으로 http 서버를 만드는 코드도 추가되었다.


HLSServer 함수의 첫번째 인자로 준 server에 해당 미들웨어가 삽입된다.

provider 객체 내의 exists함수는 해당 서버로 오는 모든 요청에 대해서 호출이 되며, Error을 던지게 되면 그 다음으로 진행하지 않고 에러를 응답하게 된다.

exists함수에서 callback(null, true)가 호출되게 되면, 요청 url의 path중 확장자를 추출해서 판단하게 되는데, 확장자가 .m3u8이면 getManifestStream 함수를 호출해서 요청을 처리하게 되고, 확장자가 .ts 이면 getSegmentStream 함수를 호출해서 요청을 처리하게 된다.

그리고 해당 확장자 (.m3u8이나 .ts)과 일치하지 않는 요청이면, httpServer 자체의 요청 처리 로직으로 넘어가서 처리되게 된다.


hls 패키지 미들웨어가 동작하는 방식이 잘 이해가 가지 않아서 소스코드를 확인해보았었다.


github에 공개된 hls-server node 패키지 소스코드 url이다.

https://github.com/RationalCoding/hls-server/blob/master/src/index.js


 

HLSServer.prototype._middleware = function (req, res, next) {
  var self = this

  var uri = url.parse(req.url).pathname
  var relativePath = path.relative(self.path, uri)
  var filePath = path.join(self.dir, relativePath)
  var extension = path.extname(filePath)

  req.filePath = filePath

  // Gzip support
  var ae = req.headers['accept-encoding'] || ''
  req.acceptsCompression = ae.match(/\bgzip\b/)

  if (uri === '/player.html' && self.debugPlayer) {
    self._writeDebugPlayer(res, next)
    return
  }

  self.provider.exists(req, function (err, exists) {
    if (err) {
      res.statusCode = 500
      res.end()
    } else if (!exists) {
      res.statusCode = 404
      res.end()
    } else {
      switch (extension) {
        case '.m3u8':
          self._writeManifest(req, res, next)
          break
        case '.ts':
          self._writeSegment(req, res, next)
          break
        default:
          next()
          break
      }
    }
  })
}


self.provider.exists 부분에 보면, 파일 확장자(extension)을 기준으로 알맞는 함수를 호출하고, 해당 확장자에 해당하지 않으면 해당 서버의 원래 처리 로직으로 가도록 되어있다. 그리고 두 확장자에 둘다 해당되지 않으면, next()를 호출해서 미들웨어에서 제어권을 넘겨준다. 이러한 함수들을 이용해서 이미 세그먼팅 된 .ts파일들과 .m3u8 매니페스트 파일로 브라우저단에서 잘 재생이 되는지 확인해보았다.


주로 쓰는 크롬 브라우저와 윈도우10에 디폴트로 설치되어있는 엣지 브라우저를 이용해서 테스트를 해 보았는데, 엣지 브라우저에서는 음악이 잘 재생되었는데, 크롬 브라우저에서는 재생이 되지 않았다. 어떻게 된 것인지 확인해보기 위해 HLS와 관련된 문서들을 찾아보았다.

자바스크립트 동시성 모델


자바스크립트는 이벤트 루프에 기반한 동시성(Concurrency) 모델이라는 것을 가지고 있습니다. 이 모델은 다른 C나 Java와 같은 언어가 실행되는 방식과 다릅니다.


본디 자바스크립트는 웹 브라우저 상에서 동작하는 스크립트 언어로 만들어졌습니다. 그리고 자바스크립트로 DOM API에 접근해서 브라우저의 UI를 제어하기 위해 UI스레드를 공유할 수 밖에 없는 환경입니다.


만약 자바스크립트가 일반적인 C나 Java언어 처럼 동작하고, 동시성을 제공하지 않는다면 다음과 같은 상황이 벌어질 수 있습니다.


서버에서 비동기적으로 데이터를 받아오기 위해 XHR을 통해 Ajax요청을 보내고, 해당 데이터를 받아오기 까지 시간이 소요됩니다. 그러면 해당 데이터를 받아오기 전 까지 UI를 제어하는 자바스크립트는 멈추게 되므로 사용자는 UI를 제어할 수 없습니다.


이러한 경우가 Ajax가 잦은 페이지에서는 클라이언트 리소스문제가 아니라도 브라우저가 매우 자주 버벅일 것이고 이는 사용자에게 매우 나쁜 UX를 제공합니다.


이러한 상황을 피하기 위해서 자바스크립트는 이벤트 기반 동시성 모델을 제공합니다. Ajax요청을 보내는 것과 응답을 받는 것, 각각을 다 이벤트로 처리하며 Ajax와 같이 I/O 요청 이후 결과를 계속 기다리는 것이 아니라 곧바로 다른 처리할 수 있는 작업을 처리하게 되며 UI 쓰레드가 멈추는 것을 막을 수 있습니다.

동시성과 병렬성

동시성(Concurrent)는 병렬성(Parallel)과는 다릅니다. 둘 다 여러개의 실행 흐름이 동시에 일어나는 것이라는 공통점이 있습니다.


동시성

동시성(Concurrent)는 실제 물리적으로 동시에 일어나는 것이 아니라, 흐름을 실행시키는 것은 하나(e.g. CPU 코어 혹은 쓰레드)지만 작은 타임 슬라이스(Time slice, Time quantum)단위로 다른 흐름을 돌아가면서 실행시켜서 동시에 일어나는 것 처럼 보이게 하는 방식입니다. 논리적인 의미에 동시 실행으로 볼 수 있습니다.


병렬성

병렬성(Parallel)은 실제로 흐름을 실행시키는 것(e.g. CPU 코어 혹은 쓰레드)이 복수개여서, 각각 실행 흐름을 할당받아 동시에 실행시킵니다. 물리적인 의미에 동시 실행입니다.




자바스크립트는 언어 스펙이 싱글쓰레드이므로, 병렬성(Parallel)을 지원할 수 없습니다. 대신 동시성(Concurrent)는 지원할 수 있습니다.


자바스크립트 런타임 모델

자바스크립트는 브라우저에서 더 좋은 UX를 제공하기 위해 동시성 모델을 제공한다. 하지만 이 동시성 모델은 자바스크립트 언어 자체의 스펙이 아니라, 자바스크립트를 구동하는 환경인 웹 브라우저의 자바스크립트 엔진에서 구현한 모델이다. 또한 범용 언어로서 자바스크립트 런타임인 Node.js도 비슷한 방식의 동시성 모델이 구현되어 있다.

다음 그림은 런타임의 구성요소들을 시각화 한 것이다.



image/svg+xml Stack Heap Queue


스택, 힙, 큐로 이루어져 있다.


스택

스택은 C와 같은 다른 프로그래밍 언어에서 사용하는 함수 호출시 할당, 리턴 시 해제되는 그 콜 스택이다.

함수가 호출될 때 마다 해당 함수에 대한 스택 프레임이 생성되고, 해당 스택 프레임은 해당 함수의 인자와 지역변수들을 저장한다.

함수가 리턴될 때 해당 함수의 스택 프레임은 해제된다.


일반적으로 힙은 동적으로 할당된 변수나 메모리 등을 포함한다. 자바스크립트는 인터프리터형 스크립트 언어이므로 선언된 것들을 저장할 수 있지만, 지역변수나 함수 인자들은 스택에 저장되므로, 전역 객체와 같은 것들이 힙에 저장되게 된다.

자바스크립트 런타임에서 큐는 메시지 큐이다. 이 큐를 이용해서 자바스크립트는 동시성 모델을 지원하며, 큐에는 처리해야 할 메시지들이 저장되어 있다.
메시지는 처리해야할 함수들의 셋이라고 볼 수 있으며, 큐는 메시지 단위로 처리한다. 큐가 메시지를 처리하는 로직은 다음과 같다.

1. 콜 스택이 비었으면 큐에서 메시지를 꺼내서 처리한다.
2. 콜 스택이 비지 않았으면 콜 스택이 빌 때 까지 처리하던 함수를 계속 처리한다.

메시지 처리는 콜 스택에 처음 비어서 해당 메시지를 큐에서 꺼내어서 해당 메시지에 대한 함수를 호출하면서 메시지 처리가 시작이 된다.
콜 스택에 다시 비워질 경우 해당 메시지 처리는 종료된다.

메시지 큐 동작 방식을 확인할 수 있는 간단한 예제를 보자.

 

function foo(a) {

	while(true) {
		a += bar(5);
	}
}

function bar() {
	var a = 1;
	return a;
}

function baz() {
	console.log('Another task');
}

foo();
baz();


브라우저에서 위 코드를 실행하면 Another task라는 메시지는 보이지 않는다. foo함수가 맨 처음 메시지 큐에 들어가서 처리되게 되는데, foo 함수는 bar함수를 호출, 리턴을 무한 반복하게 되며 콜 스택에 비워지지 않기 때문이다. 그러므로 다음 메시지 큐에 baz함수가 있지만 계속해서 실행이 되지 않게 된다.
 

function foo(a) {
	setTimeout(0, bar);
}

function bar(x) {
	setTimeout(0, foo);
}

function baz() {
	console.log('Another task');
}

foo();
baz();


브라우저에서 위 코드를 실행하게 되면 Another task라는 메시지는 잘 보인다. setTimeout(time, function); 함수를 호출하면, time시간 이후에 function 함수를 메시지 큐에 추가하게 된다.

메시지 큐에는 다음과 같은 방식으로 메시지가 들어가고, 처리되게 된다.

foo 

 baz

 bar

 foo

 bar



이러한 이벤트 루프의 특성을 잘 파악해야지, 여러개의 테스크를 동시성있게 실행되게 자바스크립트 코드를 작성할 수 있다. 이러한 이벤트 루프 모델에 반하는 방식으로 코드를 작성하면 나쁜 UX를 사용자에게 제공하게 되며, 이는 거의 버그나 마찬가지이다.



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

참고

https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop

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

- 이 글은 2020-01-08에 마지막으로 수정되었습니다.

- 글 상에 오류나 틀린 내용이 있을 수 있습니다.

- 잘못된 내용에 대한 신고나, 피드백을 주시고자 한다면 댓글을 달아주시면 반영하도록 하겠습니다.

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


개발을 시작하면서 접하기 쉬운 단어 중 햇갈리기 쉬운 API, ABI, 라이브러리, 프레임워크의 뜻을 알기쉽게 한번 설명해보는 포스팅이다.


계속 개발을 진행하다 보면 이런 용어들에 대한 대략적인 의미는 감이 잡히지만 정확한 뜻을 알거나 남에게 개념을 설명하기에는 햇갈릴 수 있다. 이번 포스팅을 계기로 한번 정리해보자.


라이브러리

일단 첫번째로 라이브러리이다.


컴퓨팅에서 라이브러리(Library)의 위키백과에서의 정의는 다음과 같다.


https://ko.wikipedia.org/wiki/라이브러리_(컴퓨팅)


소프트웨어를 개발할 때, 컴퓨터 프로그램이 사용하는 비휘발성 자원의 모임이다. 구성 데이터, 문서, 도움말 자료, 메시지 틀, 미리 작성된 코드, 서브루틴(함수), 클래스, 값, 자료형 사양 등을 포함할 수 있다.


예를 들어서, 누군가가 이전 C++포스팅에서 언급한 big_integer 클래스를 만들었다고 가정해보자. 아주 잘 만들어서 버그도 거의 없고, 정의된 함수만 잘 호출하면 잘 동작한다고 하자. 이 big_integer 클래스를 만든 사람이, 이 클래스를 혼자서 쓰기 아까워서 남들도 쉽게 사용할 수 있도록 공개를 해서 남들도 사용할 수 있도록 하되, 소스코드는 공개를 하고 싶지 않다면 어떻게 할까?


일반적으로 다음과 같은 절차를 밟는다. 클래스의 멤버변수와 멤버 함수들을 선언한 헤더파일과, 함수 정의부분을 컴파일한 목적 파일(Object File)을 만들고 사용 방법에 대한, 즉 클래스의 생성자 인자라던지 함수 사용 별 동작되는 방식이라던지 이런 부분에 대한 설명이 들어간 문서를 같이 공유하면 된다.


그러면 이 big_integer 클래스 사용하고 싶은 다른 개발자는, big_integer의 소스코드는 볼 수 없지만, 제공받은 헤더파일과 컴파일된 목적 파일, 그리고 사용법을 확인해서 자신의 프로젝트에서 그 개발자가 작성한 big_integer 클래스를 자유로이 쓸 수 있게 된다. 이 때, big_integer 클래스의 헤더파일과 컴파일된 목적 파일, 그리고 사용 방법에 대한 문서는 "C++ big_integer 라이브러리"가 된다.


그리고 여기서 big_integer 클래스의 생성자의 인자와, add 함수의 인자 및 리턴 타입 등 big_integer 클래스를 사용하기 위한 메소드 프로토타입들은 이 C++ big_integer 라이브러리의 "API"가 된다.


API

API의 정의에 대해 다시 자세히 설명해보겠다.


API는 일단 Application Programming Interface의 약자이다. 여기서 Application이란 Application Program이란 뜻으로, 한글로는 응용 프로그램이다.


컴퓨터 프로그램은 크게 응용 프로그램과 시스템 프로그램으로 나뉘는데, 시스템 프로그램이 아니면 모두 응용 프로그램이라고 보면 된다. 시스템 프로그램은 쉽게 말하면 운영체제이다. 따라서 간단하게 생각하면 'Application -> 운영체제가 아닌 모든 프로그램' 이다.


Programming은 프로그램을 만드는 것을 뜻한다. 따라서 API는 응용 프로그램을 만들 때 사용하는 인터페이스라는 뜻이 되는데, 그러면 인터페이스는 무엇을 뜻하는가?


예를 한번 들어보겠다. 노트북에 USB 메모리를 연결해서 데이터를 전송하려고 한다. 이 때, 노트북과 USB 메모리의 인터페이스는 무엇일까?

USB포트가 이 서로 다른 두 물체의 인터페이스다. 인터페이스는 간단하게 서로 다른 두 개 이상의 것들을 이어주는 '매개체'라고 생각하면 된다.


API는 따라서 응용 프로그램을 작성할 때 필요한 매개체라는 해석이 가능하다. 그러면 매개체가 왜 필요한 것일까?


실무 개발에서는 프로그램의 크기가 커지면 혼자서 밑단 부터 윗단 까지 모두 다 개발할 수 없다. 따라서 서로간의 협업이나 이미 만들어진 소프트웨어 컴포넌트를 결합해서 만드는데, 이 때 라이브러리도 그 중 하나이다. 그러한 컴포넌트들을 결합하기 위한 매개체들을 API라고 하는 것이다.


C++ 어플리케이션을 작성할 때 다른 C++ 클래스 라이브러리를 사용하는 경우 해당 클래스에 정의된 메소드들을 실행함으로써 해당 라이브러리를 활용한다. 따라서 해당 메소드들의 필요한 인자나 리턴 타입 등이 C++ 클래스 라이브러리의 API가 되는 것이다.


그런데 보통 외부 컴포넌트의 경우 라이브러리 형태로 제공받는 경우가 많아서 라이브러리와 API의 뜻을 햇갈려하는 경우가 많다. 하지만 라이브러리는 이러한 컴포넌트 자체를 뜻하고, API는 이 컴포넌트를 활용하는 규약이라고 보면 된다. 다음 예에서 라이브러리가 아니지만 API를 제공하는 경우를 확인해보자.


구글 클라우드에서 제공하는 Speech API가 있다.(이름 부터 대놓고 API이다)

음성 데이터를 넣으면 그 데이터를 원하는 언어의 텍스트로 인식해서 돌려주는 방식이다. 그리고 기계학습을 이용한 인공지능 기술로 해당 기능을 구현하였다고 하고, 내부적인 동작은 공개하지 않고 있다. 그리고 여기서 제공하는 RESTful API의 경우는 특정한 포맷에 맞추어서 HTTP 음성 데이터를 포함한 요청을 보내면 HTTP 응답으로 해석해낸 텍스트가 돌아오는 방식이다. 이 때 HTTP 요청/응답 포맷 역시 API이며, 이는 개발자의 로컬 컴퓨터에 설치된 라이브러리를 통해 제공받는 방식이 아니다. 외부 원격에 있는 서버에서 부터 이러한 음성 인식 서비스를 제공받는 것이다.

이 경우는 라이브러리가 아니지만 API를 제공한다.


ABI

이제 API와 차이점을 잘 알기 어려워 할 수 있는 ABI에 대해 알아보자.


API라는 단어는 흔히 접하게 되어서 대략적으로도 뜻을 알기 쉬운데 ABI는 자주 접하는 단어는 아니다. ABI는 Application Binary Interface의 약어로 직역해보면 응용프로그램 이진 인터페이스이다. 응용프로그램과 다른 컴포넌트간에 이진 인터페이스인데, 이때 이진은 Binary로 0과 1을 뜻하는데 컴퓨터에서 가장 로우레벨, 가장 기계와 가까운 수준으로 내려가게 되면 0과 1로 이루어진 이진값들로 가게 된다. 기계 수준, 이진값 수준에서 인터페이스를 뜻한다. API는 소스코드 레벨에서 호환이 된다면, ABI는 바이너리 수준에서 호환이 된다고 한다.


바이너리 수준에서 호환이 된다고 하면 바이너리로 이루어진 기계어들과 관련이 있다고도 볼 수 있겠다.

이 기계어들은 결국 물리적인 프로세서, 즉 CPU에서 실행을 하고, CPU들이 실행하는 기계어 명령어 셋들을 ISA(Instruction Set Architecture)라고도 부른다. 이 ABI는 실질적으로 ISA와 밀접한 관련이 있다고 볼 수 있다.

아까 라이브러리와 API를 햇갈려 하듯이, ABI와 ISA는 햇갈릴 수 있는 관계라고 볼 수 있다. 

하지만 의미상으로는 ISA는 해당 하드웨어 프로세서가 실행할 수 있는 기계어 명령어 세트를 의미하고, ABI는 바이너리 코드 레벨에서 호환되는 인터페이스를 의미한다. 


아래는 안드로이드 스튜디오에서 에뮬레이터를 생성하려고 할 때 고르는 화면이다. 우측에 보면 CPU/ABI라고 되어 있는 항목이 있다. x86이라고 쓰여 있는데 이는 흔히 쓰이는 Intel에서 개발한 유명한 32bit CPU 아키텍쳐 중 하나이다. CPU라고도 같이 쓰여있는 것으로 보아 ABI와 CPU 아키텍쳐는 여기서는 사실상 같은 의미로 쓰이고 있다고 볼 수 있다.


API라고 쓰여있는 부분도 있는데, 여기서 API는 안드로이드 API 버전을 의미하며, API level 28는 안드로이드 PIE 버전을 의미한다. API Level 27 안드로이드 환경에서 앱을 만들때와, API Level 28에서 안드로이드 환경에서 앱을 만들 때, 소스코드의 형태가 다를 수 있다는 것을 시사하는 것이다.

android studio emulator creation abi에 대한 이미지 검색결과



이렇게 이야기를 하면 이해가 어려우므로 예를 들어서 설명해보도록 하겠다.


Windows 운영체제에서 다음과 같은 Hello World 코드를 작성해서 컴파일 후 실행해 보았다.


 

#include <stdio.h>
int main() {
	printf("Hello World!\n");
	return 0;
}

이 코드를 그대로 복사해서 Ubuntu Linux 환경에서 컴파일 후 실행하면 같은 결과가 나올까?


정답은 당연히 Yes이다. 왜냐하면 윈도우 환경과 리눅스 환경에서 표준 입출력 라이브러리에 구현된 printf 함수의 스펙이 같기 때문이다. 받는 인자와 결과가 같기 때문이다. 

이것은 윈도우와 리눅스의 printf 함수의 API가 호환이 되기 때문이라고 볼 수 있다. 호환이 된다는 것은 같은 입력값을 넣었을 때 같은 결과값이 나타난 다는 것이다.


그러면 만약 x86 CPU를 사용하는 윈도우 시스템에서 대충 다음과 같은 어셈블리 코드를 실행한다고 생각해보자.


[BITS 32] mov ax, 0x01

ax레지스터에 1이라는 값을 넣는 결과가 나타날 것이다.

그러면 이 똑같은 코드를 기반으로 x86 CPU를 사용하는 리눅스 시스템에서 같은 코드를 실행한다면 같은 결과가 나올까?


이 질문의 답도 Yes이다. 어셈블리 언어는 바이너리 값과 1대 1 매칭이 되므로, 어셈블리 수준에서 호환이 된다는 것은 바이너리 수준에서 호환이 된다고 볼 수 있다. 같은 바이너리 값을 실행했을 때 같은 결과가 나타난다면 바이너리 수준에서 호환이 된다고 볼 수 있다.

이 경우 ABI가 호환된다고 볼 수 있다.


두 시스템 다 x86 계열 CPU를 사용을 하기 때문에 나타나는 것으로, 같은 기계어 Instruction에 대하여 똑같이 동작하기 때문에 ABI가 호환이 되는 것이다.


"""

여기서 살짝 궁금증이 있을 수 있는 부분은 ABI가 호환된다면, 윈도우에서 컴파일한 Hello World 실행파일이 리눅스에서는 왜 똑같은 결과를 내면서 실행이 되지 않느냐라는 것인데, 이는 윈도우와 리눅스의 운영체제별 실행파일의 포멧이 다르기 때문이다. 그리고 공유 라이브러리의 존재 유무와 공유 라이브러리의 포맷 등 다른 여러가지 요인들이 섞여있기 때문이다. 따라서 ABI에 대한 예제로 해당 Hello World 실행파일을 주지 않았다.

"""


이러한 개념이다. 따라서 API는 소스코드 레벨의 호환성을 보장하고, ABI는 바이너리 레벨의 호환성을 보장하는 의미이다.

그러면 다음의 사례는 어떠할까?


A 라는 라이브러리를 사용하는 소프트웨어가 있다. A라이브러리의 버전이 업그레이드 되어서 API가 바뀌었다. 이 때 해당 소프트웨어는 A라이브러리와의 호환성을 유지하기위해 어떠한 조치를 취해야 하는가?

=> API가 바뀌었으므로 소스코드를 수정해야 한다.


B라는 소프트웨어는 x86 시스템에서 사용하도록 컴파일되었다. 하지만 이 소프트웨어를 ARM 시스템에서도 사용할 수 있는 버전을 만들려면 어떻게 해야하는가?

=> ABI가 다른 시스템으로 이식해야 한다. 소스코드->바이너리로의 변환은 컴파일러가 해줄 수 있으므로, ARM용 컴파일러나 크로스 컴파일러를 이용해서 새로 컴파일하면 된다.




프레임워크

이제는 프레임워크에 대하여 이야기 해 보겠다. 보통 프레임워크와 라이브러리를 햇갈려 하는 사람들이 많다.

사실 프레임워크라는 단어는 소프트웨어 분야 외에서도 쓰이는 단어이다. 직역하면 뼈대, 골조라는 뜻이고, 만약 경영학과를 전공 한 사람이라면, 프레임워크라는 단어를 한번 쯤은 들어봤을 것이다.


이러한 SW가 아닌 분야에서 프레임워크라고 하면 "문제를 바라보는 틀", 혹은 "대상을 바라보는 시각과 관점" 등 의 다양한 의미를 지니고 있다.



위키에서의 정의는 다음과 같다.


소프트웨어 프레임워크(software framework)는 복잡한 문제를 해결하거나 서술하는 데 사용되는 기본 개념 구조이다. 간단히 뼈대, 골조(骨組), 프레임워크(framework)라고도 한다. 이렇게 매우 폭넓은 정의는 이 용어를 버즈워드(buzzword)로서, 특히 소프트웨어 환경에서 사용할 수 있게 만들어 준다.


SW분야에서 프레임워크와 일반적인 프레임워크에서 공통되는 부분은 "틀"이라는 부분이다.


무언가를 하기 위한 '틀'에 해당된다.


흔히 이야기하는 "틀에 박힌 사고방식"라는 관용어구에 있는 그 "틀"과 같다고 볼 수 있다.


이 틀이 있음으로써 가지는 장점은 무언가를 해결해야 하거나 할 것이 있을 때, 그 틀에 맞추어서 행동을 하면 누구나 쉽게 따라하면서 해결 해 나갈 수 있다.


반면 단점으로는 그 틀에 맞지 않는 무언가는 해결할 수 없다는 점이 있다.


소프트웨어 프레임워크도 마찬가지이다. SW 프레임워크는 어떠한 문제를 해결하기 위한 소프트웨어적 틀에 해당한다.



예시로 매우 유명한 프레임워크 중 하나인 안드로이드 프레임워크를 들어보겠다.

(사실 안드로이드 시스템은 안드로이드 커널 위에 안드로이드 프레임워크가 올라가는 형식으로 이루어져 있다.)


안드로이드 프레임워크는 안드로이드 시스템에서 구동가능한 안드로이드 앱을 만들 수 있는 API들을 제공을 한다.


OnClickListener라는 java나 코틀린 메소드를 오버라이딩 하면 어떤 View를 눌렀을 때 어떤 동작을 할 것인지를 지정할 수 있고, 실제로 그 뷰를 클릭했을 때 해당 동작이 일어나게 해준다.

이러한 기타 등등의 작업들만 하고 앱을 빌드하면 원하는 대로 동작하는 안드로이드 앱이 생성이 된다. 


API대로만 구현을 하면 앱 내부적으로는 어떠한 최적화나 연산을 하는지는 알 수 없지만, 원하는 대로 동작한다는 것 만 알 수 있다.


이 안드로이드 프레임워크는, 안드로이드 앱을 만들기 위한 정해진 틀이라고 볼 수 있는 것이다.


하지만 안드로이드 프레임워크로는 웹 서버를 만들 수 없다. 왜냐하면, 안드로이드 프레임워크는 안드로이드 앱을 만들기 위한 "틀"이므로, 틀의 원래 목적과 다른 웹 서버를 만들 수는 없다.




그렇다면 라이브러리와 프레임워크는 어떻게 다른 것일까?


일반적으로 프레임워크 안에 라이브러리가 포함되는 경우가 많다. SW 프레임워크는 보통 어떤 거대한 응용프로그램 등을 만들기 위한 "틀"인 경우가 많은데, 이를 위해서 미리 코딩된 코드조각들은 거의 당연히 들어가있다고 볼 수 있다.

이 "미리 코딩된 코드조각"들은 라이브러리에 해당된다고 볼 수 있다.


다만 라이브러리만 가져다 쓰는 경우는, 프로그램의 메인은 개발자가 직접 쓰고, 중간중간 필요한 부분만 라이브러리로 가져와서 쓰는 형태인 반면, 프레임워크를 가져다 쓰는 경우, 그 프레임워크의 본디 목적과 다른 프로그램은 만들 수가 없다.


프레임워크가 라이브러리에 비해 해 주는 일이 더 많고, 규모가 거대한 만큼 원래 목적과 다른 일은 할 수 없다는 점이 다르다고 볼 수 있다.


아래 KLDP에서 프레임워크와 라이브러리 차이에 대한 쓰레드가 열린 적이 있는데, main문을 바꿀 수 있으면 라이브러리고 아니면 프레임워크다 하는 식의 이야기도 나오곤 한다.

https://kldp.org/node/124237


사실 프레임워크의 정의 자체가 매우 엄밀한 정의는 아니므로 몹시 까다롭게 따질 필요는 없으나, 소프트웨어 분야에 종사하는 사람으로써 매우 자주 쓰이는 용어 중 하나이므로, 한번 짚고 넘어갈 필요는 있다고 생각한다.

struct big_integer { char *digit; int length; char sign; struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs); };


지난 시간에 이어 포스팅을 하겠다. 지난 시간에는 C 구조체를 이용해서 위와 같은 big_integer 구조체에 필요한 맴버변수들과 관련 함수들을 묶어서 캡슐화를 할 수 있다는 것 까지 진행했다. 이렇게 확장된 구조체를 C++에서는 클래스라고 부른다. 따라서 다음과 같이 나타내어질 수 있다.

class big_integer { char *digit; int length; char sign; struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs); };


위 구조체와 달라진 점은, struct가 class로 바뀌었다는 것 뿐이다. 실제로 C++에서 구조체와 클래스의 문법적인 차이는 매우 미미하다. 하지만 단지 구조체에 함수를 포함시켰다는 것의 작은 의미가 아니라, 절차지향 프로그래밍에서 객체지향 프로그래밍이라는 패러다임의 변화가 함축되어 있는 그러한 부분이다. 따라서 이러한 상징성을 위해서 class라는 새로운 이름을 부여한 것이다.


게다가 실제로 객체지향 프로그래밍을 할 때, 다른 객체지향 개념이 내재된 언어에서 구조체라는 단어는 쓰지 않고 모두 클래스와 객체라고 부른다. 그러면 문법적인 부분에서 C++의 구조체와 클래스의 차이는 어떠한 것이 있을까?


이를 알기 위해선 우선 접근지정자(Access Modifier)와 정보 은닉(Data Hiding)에 대하여 알아야 한다.


객체지향 패러다임에서는 절차지향 패러다임 보다 코드의 모듈화와 재사용성에 많이 관심을 가지면서도, 사람에 의한 실수들을 줄이는 방향으로 발전했다.


객체지향 패러다임에서는 남이 작성한 클래스와 모듈들을 쉽게 재사용하도록 하면서 생산성을 늘렸다. 남이 미리 작성한 클래스들의 내부적인 복잡한 로직은 알 필요가 없도록 추상화되어 숨겨져있고, 어떠한 상황에서 어떠한 함수를 호출해야 하는지 정도만 알면 된다.


하지만 또 다른 관점으로 보면 다른 누군가가 작성한 그 클래스를 사용하는 개발자는 그 클래스의 내부 로직에 대해 잘 모를 것이고, 내부의 중요한 값들을 잘못 건드렸다가 내부적 로직이 꼬이거나 버그를 유발할 수도 있을 것이다.


예를 들어서 big_integer 클래스를 사용하는 누군가가 big_integer.length += 5; 와 같은 코드를 추가하게 된다면 어떠한 일이 발생할까? 실제 자릿수의 값들은 digit이라는 char 포인터 변수가 가리키는 곳에 문자열 형태로 있을 것이고, 이 때 문자열의 길이는 length라는 변수로 계산이 될 것이다. 따라서 허용되지 않은 메모리에 접근하게 될 수도 있다. 그러므로 big_integer 클래스를 사용하는 개발자는 length라는 변수의 값을 읽는 것은 가능하게 하되, length의 변수의 값을 마음대로 바꾸도록 하면 안된다. length의 값은 대입, 덧셈, 뺄샘, 곱셈, 나눗셈 등의 연산으로 인하여 자연스럽게 변화되는 경우에만 변해야 한다. 이럴 때 적용할 수 있는 것이 접근지정자이다.

class big_integer {
private:
	char *digit;
	int length;
	char sign;
	struct big_integer* add(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);
};

private라는 이름의 접근 지정자를 설정함으로써, private 접근지정자 밑의 맴버변수와 메소드들은 모두 외부에서 접근이 불가능하도록 변경되었다. 하지만 이렇게 되는 경우는 메소드들도 접근할 수 없게 되므로 좀 곤란해진다. 따라서 메소드들은 외부에서 접근이 가능하도록 변경해 보았다.


class big_integer {
private:
	char *digit;
	int length;
	char sign;
public:
	struct big_integer* add(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);
};

이렇게 해서 메소드들은 외부에서 접근이 가능하지만, 멤버변수들은 그렇지 않도록 설정되었다. 그러면 big_integer의 자릿수를 length 변수를 통해서 알아내고 싶다면 어떻게 해야할까?


length 변수에 대한 getter 함수를 지정해주면 된다. 다음과 같은 형태가 될 것이다.

class big_integer {
private:
	char *digit;
	int length;
	char sign;
public:
	struct big_integer* add(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);
	int get_length() {
		return length;
	}
};

위와 같이 get_length 함수를 정의해주면, 외부에서 length 멤버변수의 값을 알아야 할 때, get_length 함수를 호출하면서 간단하게 값을 알아낼 수 있다.

일반적으로 클래스의 메소드들을 디자인할 때, 이러한 점에 입각해서 멤버변수들은 모두 private 접근지정자를 설정하고, 메소드들은 public 접근지정자를 설정하는 것이 일반적이다. 그리고 접근해야 할 필요가 있는 멤버변수들에 대해서는 각각 getter 함수와 setter 함수를 지정한다. 만약 setter 함수를 지정하게 된다면 다음과 같은 모습이 될 것이다.


class big_integer {
private:
	char *digit;
	int length;
	char sign;
public:
	struct big_integer* add(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs);
	struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);
	int get_length() {
		return length;
	}
	void set_length(int length) {
		this->length = length;
	}
};

하지만 이 big_integer 클래스에서는 length의 값을 임의로 바꾸면 안되므로, set_length 함수가 없는 것이 낫다. 그리고 만약 set_length 함수를 사용한다고 하더라도 저렇게 간단하게 값만 대입하는 방식이 아닌, 입력받은 값이 정당한 값인지 확인하는 과정이 필요할 것이다. 예를 들면 length의 인자에 음수가 들어올 경우 0값을 대입한다는 식으로 말이다.


이렇게 맴버변수와 같이 잘못 건드릴 수 있는 값들을 접근지정자 등을 통해서 외부에서 잘못 바꾸지 않도록 안전장치를 만드는 것을 정보 은닉(Data Hiding)이라고 한다.


이 정보 은닉을 객체지향 프로그래밍의 특징의 한 카테고리고 분류해서 넣는 경우도 있지만, 일반적으로는 저번 포스팅에서 언급했던 "캡슐화"의 특징 중 하나로 보는 경우가 많다. 따라서 "캡슐화"의 특징은 두개로 나뉜다고 볼 수 있다.

1) 관련된 데이터(멤버변수)와 동작(메소드)를 한 곳에 묶어음

2) 외부에서 몰라도 되거나, 몰라야 하는 정보들을 자유롭게 접근하지 못하도록 제한하고 은닉함 (정보 은닉)


이제 C++ 언어에서 구조체와 클래스의 문법적 차이점을 알아보자. 이 두 개의 문법상 차이는 기본 접근 지정자(Default access modifier)가 구조체의 경우 public이고, 클래스의 경우 private라는 것이다. 다시 말하면, 접근 지정자를 설정하지 않을 경우 구조체는 모두 public이고, 클래스는 모두 private이다.

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

다음 포스팅에서 이어진다.

이번에는 객체지향 프로그래밍의 개념에 대해서 간단간단하게 설명해보는 포스팅을 올려볼까 한다. 


> 일단 객체지향 프로그래밍이라는 프로그래밍 패러다임이 새로 생겨나게 된 배경에 대해서 설명해보겠다. 

객체지향 프로그래밍 패러다임이 있기 전에는 절차지향 프로그래밍이라는 패러다임이 있었는데, 이는 문제를 해결하는 절차들, Step by Step에 포커스를 맞춘 방식의 프로그래밍 패러다임이었다. 그런데 하드웨어는 급속도로 발달하지만, 소프트웨어는 개발 속도도 늦고 버그도 많고 해서 하드웨어의 발전 속도를 못 따라가는 일이 생겼는데, 이를 간단히 이야기 해서 '소프트웨어 위기'라고 불렀다.


이를 해결하기 위해서 소프트웨어도 하드웨어 부품들을 조립하는 것 처럼, 소프트웨어도 객체라는 부품으로 나누어서 개발을 하고, 이런 객체들을 조립하는 식으로 해보자 라는 아이디어가 객체지향 프로그래밍이다.


이제 객체지향 프로그래밍이 어떤 것인지 예제를 통해서 알아보자. 이 포스팅의 독자는 C언어에 대해서는 기본적은 다 알고 객체지향 개념은 잘 모르는 사람이라고 가정한다.


C언어에서의 문자열(String)은 char형 배열로 표현된다. 하지만 이 방식은 여러모로 불편한 점이 많다. 예를 들어서 scanf("%s");와 같은 코드로 문자열을 입력받을 때, 문자열의 크기가 얼마나 될 지 모르기 때문에 배열의 크기를 너무 작게 지정하면 버퍼 터짐(Buffer overflow) 현상이 나타날 것이고, 너무 크게 지정하면 메모리의 낭비가 될 것이다.


다음은 C언어로 문자열을 입력받는 코드이다.

#include <stdio.h> int main() { char str[100]; // 입력받을 문자열의 길이가 99보다 크다면 Buffer overflow 발생 scanf("%s", str); }


하지만 C에서 객체지향의 개념이 추가된 C++언어로 같은 동작을 하는 코드를 작성하면 다음과 같이 나타난다.


 
#include <iostream>
#include <string>
using namespace std;
int main() {
	string str;
	cin >> str;
}


위에 있는 C로 작성된 코드의 경우는 입력 받을 문자열의 길이가 99이하 라고 가정하고 100만큼의 메모리만 할당된 char 배열을 선언해서 입력을 받았다. 따라서 99보다 길이가 작은 문자열을 받으면 안쓰는 메모리가 필연적으로 생기고, 99보다 길이가 크다면 버퍼 오버플로가 발생한다.

하지만 C++로 작성한 코드의 경우 string이라는 객체를 사용하면 입력 받을 문자열의 길이에 대하여 상관할 필요가 없다. string 객체 내부적으로 알아서 입력크기에 맞게 메모리 할당을 유동적으로 해주도록 하는 코드가 이미 작성이 되어 있고 그 코드는 흔히 나타날 수 있는 버그들은 알아서 잘 수정이 되어있는 상태이다. 따라서 우리는 이전에 어떤 누군가가 작성한 string이라는 객체 클래스를 사용함으로써 문자열을 이렇게 쉽게 다룰 수 있게 된 것이다.


그리고 이 string 객체를 사용하는 개발자는 string 객체 내에 멤버변수나 메소드가 어떻게 정의되어있는지는 알 필요가 없다. 단지 해당 객체에 어떤 함수가 있고, 그 함수는 어떤 인자를 받고, 어떤 리턴값을 주는지만 알면 된다. 예를 들면 해당 문자열의 길이를 알고 싶다면 개발자는 다음 length함수를 호출하기만 하면 된다.


#include <iostream> #include <string> using namespace std; int main() { string str("Lorem ipsum"); size_t len = str.length(); cout << len << '\n'; return 0; }

단지 개발자는 string 클래스에 length라는 함수가 있고 이 함수의 원형은 size_t length(void); 이며 리턴값이, 해당 문자열의 길이라는 것만 알면 되고, 내부적으로 해당 함수가 어떻게 구현되어있는지 알 필요가 없다. 따라서 string 클래스 내부 구조에 신경을 쓸 필요도 없다.


이러한 특징을 "추상화"라고 하며 이는 객체지향 프로그래밍의 4대 특징 중 하나이다. 해당 클래스 사용자는 클래스에 대해서 '추상적'으로만 알면 된다는 것이다. length함수 -> 문자열 길이 리턴 이라고 '추상적'으로만 알면 되고 length 함수 호출 시 어떠한 내부 기작이 일어나고 메모리에서 어떤 일이 일어나는지 '구체적'으로 알 필요가 없다는 뜻이다.


여기까지가 객체지향 프로그래밍에 있는 객체를 '사용하는 입장'에서 알아본 것이다.


이제 객체라는 개념이 C언어 문법에서 C++문법으로 확장될 때 프로그래밍 언어의 문법적인 관점에서는 어떻게 해석되는지 알아보자.


C언어에는 구조체라는 문법이 있다. Composite Data Type의 대표격이며, Primitive Data Type들과 Composite Data Type들을 묶어서 한 덩어리로 만들 수 있다. 한 예를 들어서 C언어에서 64bit 정수형으로 나타낼 수 있는 값 보다 훨씬 큰 정수 값을 다루기 위한 big_integer라는 구조체를 만든다고 생각해보자. 아마 다음과 같이 구성할 수 있을 것이다.


struct big_integer { char *digits; int length; char sign; // 0이면 양수, 1이면 음수를 나타낸다. };


숫자의 길이를 알 수 없으므로 char 형 포인터를 이용해서 동적 메모리 할당을 통해서 배열에 자릿수별 숫자를 저장한다고 설계하자. 그리고 그 숫자의 길이는 length라는 변수에 저장되고, 양수 음수의 여부는 sign이라는 값을 통해 저장한다. 1bit의 값만 저장하면 되므로 bool 자료형을 쓰면 좋겠지만, 안타깝게도 C언어에는 bool 자료형이 없다.


이제 저렇게 구조체를 구성했으니, 해당 big_integer에 기본적인 연산능력들을 넣어주기 위한 함수들을 만들어야 한다. 일단 대충만 봐도 값 할당을 위한 함수와, big_integer간의 사칙연산을 위한 함수들과 C 문자열을 big_integer로 바꿔주는 함수, 그리고 big_integer를 C 문자열로 바꿔주는 함수도 필요할 것 같다. 일단 사칙연산 함수들의 프로토타입만 한번 생각해보자.

struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);


함수에서 각 좌항 우항을 넣어서 호출 시, 함수 내부에서 big_integer 구조체를 malloc으로 동적할당한 뒤 필요한 값들을 넣고 리턴을 해준다고 생각해보면 위와 같은 프로토타입이 나타날 수 있다.


함수 명들은 직관적으로 add, subtract, multiply, divide등으로 명명하였다. 하지만 만약에 big_integer 구조체 외에 큰 수의 부동소수점을 나타내기 위한 big_float라는 구조체가 더 있다면 어떠할까?


비슷한 방식으로 big_float라는 구조체가 선언될 것이다. 그리고 해당 구조체에서 사용할 함수들도 생겨나야 하는데 그 함수들도 모두 big_float 구조체의 사칙연산을 처리해야할 것이다. 이렇게 해당 구조체에서만 사용될만한 함수 이름이 겹치기 쉬워진다. 따라서 해당 함수들의 이름 앞에 사용될 구조체의 이름을 붙이거나, 아니면 함수 오버로딩과 같은 방식을 통해서 이러한 문제를 해결해야 한다.


그리고 만약 big_integer라는 구조채를 다른 사람들도 사용할 수 있도록 배포한다고 하면, 구조체 뿐만 아니라 해당 구조체용 함수들도 같이 배표해야 한다.


물론 그렇게 해도 된다. 그래서 C언어에서도 객체지향 프로그래밍 패러다임을 따르는 것 처럼 프로그래밍이 가능하다. 하지만 큰 소프트웨어 프로젝트를 진행할 때는 이러한 부분들을 프로그래밍 언어차원에서 지원을 해 주면 개발자들이 좀 더 직관적이고 편하게 개발을 할 수 있다. 사람이 할 수 있는 실수들과 햇갈릴 수 있는 것들을 도구 차원에서 덜 햇갈리게 하고 실수를 방지하는 장치들을 추가해주는 것이다.


그렇게 객체지향 프로그래밍 언어인 C++에서는 객체지향 프로그래밍을 위하여 C에서 사용되는 구조체들을 다음과 같이 확장해 나갔다.

struct big_integer { char *digit; int length; char sign; struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs); };

big_integer라는 구조체에 해당 구조체를 위해 필요한 함수들을 구조체에 포함시켜버렸다. 언어차원에서 C절차지향 -> C++객체지향으로 발전한 것은 간단하게 생각하면 구조체에 함수도 같이 포함시킬 수 있다는 점인 것이다.


이렇게 관련있는 데이터(구조체 맴버변수)에 함수들도 묶어버린 것을 "캡슐화"라고 한다. 이 특징 역시 추상화와 같이 객체지향 프로그래밍 4대 특징 중 하나이다. 데이터와 동작을 묶어서 하나의 캡슐로 묶어버린 것이라고 생각하면 이해하기 쉽다.


이렇게 해서 이 하나의 big_integer라는 구조체에 함수를 포함시키는 확장을 함으로써, 이 구조체가 소프트웨어의 부품으로서 동작할 수 있게 된 것이다.

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

다음 포스팅에서 이어진다.

본 포스팅은 다음 링크의 내용을 통해 간추려서 번역한 포스팅입니다.

https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-centos-7


LAMP 스택은 리눅스 서버에서 간단하게 동적 웹 앱을 호스팅 할 수 있는 간단한 스택 중 하나이다. LAMP는 Linux, Apache, MySQL(혹은 Maria DB), PHP의 머릿글자를 짠 오픈소스 소프트웨어 스택이다.


이제 CentOS 7에 LAMP 스택을 구축해보도록 하겠다.


<선행사항>

일단 Root유저가 아닌 유저가 시스템에 추가되어 있어야 한다.



<아파치 설치>


다음 명령어로 아파치 서버를 설치할 수 있다.


sudo yum install httpd




다음 명령어로 아파치 서버를 실행시킬 수 있다.

sudo systemctl start httpd.service



아파치 서버 실행 이후 웹 브라우저를 켜서 주소창에 localhost라고 입력하면 아파치 테스트 페이지를 확인할 수 있다.

테스트 페이지가 나타난다면 아파치 서버가 정상적으로 설치 및 실행된 것이다.


다음 명령어를 입력하면, 컴퓨터가 부팅될 때 자동적으로 아파치 서버가 실행되도록 설정할 수 있다.


sudo systemctl enable httpd.service




<서버 아이피를 확인하는 방법>

다음 명령어를 입력하면 현재 장비에서 연결된 네트워크 인터페이스들이 나타난다.



ip addr show


그 중 해당되는 인터페이스의 inet 항목의 첫번째 나타나는 4개의 10진수 숫자가 IP주소이다. 물론 이 주소는 IPv4(IP 버전4)의 주소이며, IPv6(IP 버전6)의 주소의 경우는 inet6라고 나타나는 필드의 바로 우측에 나타난다.


<MySQL혹은 MariaDB 설치>

이제 웹 서버에서 사용할 DBMS를 설치할 것이다. MySQL과 MariaDB는 서로 상호호환 되므로 아무거나 깔아도 무방하다. 이번 포스팅에서는 MariaDB를 설치 해 보도록 하겠다.




sudo yum install mariadb-server mariadb



설치가 끝나면 MariaDB를 다음 명령어를 통해서 서비스 실행을 해 보도록 하겠다. DBMS는 서버와 클라이언트로 이루어져 있는데, 일단 서버를 구동해야 하며, 서버가 구동 중 인 경우 클라이언트가 서버에 접속해서 원하는 동작들을 수행할 수 있다. 다음 명령어는 서버를 구동하는 명령어이다.



sudo systemctl start mariadb



MariaDB를 설치한 뒤, 기본적으로 설정되어 있는 DBMS 설정 중 보안적으로 위험한 것들을 대화형 모드로 하나하나 체크하기 위해 다음 명령어를 실행한다.



sudo mysql_secure_installation


대화형 모드로 MariaDB의 Root 패스워드 변경, 익명 유저의 제거, 원격에서 Root 권한 로그인 불가 설정, 테스트 Database와 그 접근 권한 제거, 권한 테이블 반영과 같은 기본 보안 설정들을 처리해주게 된다.


또한 MariaDB도 부팅 시 자동으로 서비스가 동작하게 하기 위해 다음 명령어를 입력한다.



sudo systemctl enable mariadb.service



<PHP 설치>

PHP의 기능을 강화하기 위해서, 선택적으로 PHP의 추가 모듈들을 설치할 수 있습니다. 이러한 추가적인 PHP 모듈들과 라이브러리 리스트를 확인하기 위해 다음의 명령어를 입력할 수 있다.



yum search php-


나타나는 패키지 중, 필요한 패키지들을 설치한다. 패키지 설치는 다음과 같은 명령어를 사용하면 된다.

다음 명령어는 php-fpm 패키지, pacakge1 라는 이름을 갖은 패키지, package2라는 이름을 갖는 패키지를 설치하는 명령어이다.


sudo yum install php php-common php-mysql php-devel



<PHP의 동작 확인>


PHP가 아파치 서버에서 잘 동작하는지 확인하기 위해 간단한 PHP 코드를 웹을 통해서 실행해보도록 한다.

CentOS 7에서 아파치 기본 디렉토리에 php 스크립트 파일을 생성해 보도록 한다.

다음 명령어로 텍스트 편집기를 실행한다.


sudo vi /var/www/html/info.php


그리고 해당 파일에 다음과 같이 입력한다.



<?php phpinfo(); ?>


그리고 파일을 저장한다.


만약 방화벽을 구동중이라면 다음의 명령어로 HTTP와 HTTPS 트래픽을 허용해주어야 한다.


sudo firewall-cmd --permanent --zone=public --add-service=http sudo firewall-cmd --permanent --zone=public --add-service=https sudo firewall-cmd --reload


그리고 웹 브라우저에서 다음 URL을 입력하여 접속하여 php의 정보가 잘 출력되는지 확인해본다.


http://localhost/info.php

혹은

http://127.0.0.1/info.php


만약 php 잘 동작하지 않는다면 다음 명령어로 아파치 서버를 재시작 해본다.


sudo systemctl restart httpd.service




이클립스로 스프링 프레임워크 개발을 할 시 이클립스 환경 구축은 크게 두가지 방법이 있습니다.


1) 이클립스를 설치 한 후 STS를 별도로 설치

2) STS플러그인이 통합된 이클립스를 설치.



1번 방법의 설치는 이클립스를 설치한 뒤 다음 메뉴를 선택합니다.

이후 STS를 검색하여 설치하면 됩니다.

Install을 누릅니다.

이후 Confirm을 누릅니다.

라이센스의 동의를 체크한 뒤, Finish를 누르면 됩니다.


설치가 다 되면, 이클립스를 재시작할 것인지를 물어보는 대화상자가 나타납니다. 이 때 Yes를 눌러서 재시작합니다.


기존 이클립스에 STS를 설치한 경우 스프링 개발환경 뿐만 아니라 다른 개발환경도 지원하는 이클립스이므로, 스프링의 관점(Perspective)로 바꾸어주어야 합니다. 다음과 같은 과정을 통해서 스프링의 관점으로 변경합니다.


Windows - Perspective - Open Perspective - Others를 누릅니다. 만약 해당 엔트리 중에 Spring이 있다면 Spring을 바로 눌러주어도 상관없습니다.


Spring을 선택한 후 Open을 눌러주시면 됩니다.


이후에 다시 변경할 일이 생긴다면 해당 위치에 있는 Spring Perspective 아이콘을 눌러주시면 됩니다.



2번 방법의 설치는 다음 링크로 진입한 뒤, Download STS 버튼을 누르면 됩니다.

https://spring.io/tools/sts

해당 패키지를 다운로드 받은 뒤, 압축을 해제하고 실행하면 이미 STS가 설치되어있는 이클립스가 실행됩니다. 스프링을 개발하기 위해서 이클립스를 설치한다면 2번 방법을 이용하는 것이 훨씬 간단합니다.


만약 이미 이클립스를 설치하고 사용하고 있다면 조금 번거롭더라도 1번 방법을 사용하면 이클립스로 다양한 개발을 진행할 수 있습니다.

WIn32 API란, 32비트 윈도우즈 운영체제 응용프로그램을 만들기 위한, 마이크로소프트에서 제공해주는 라이브러리 셋이라고 보시면 됩니다. 일반적으로 처음 프로그래밍을 접할 때 만들어보는 응용프로그램들은 까만 화면에 흰 글자로 이루어진 콘솔 어플리케이션이라고 불리는 CUI 앱입니다. 하지만 윈도우에서 동작하는 프로그램들은 대부분 GUI기반 앱들이죠. 이러한 윈도우즈에서 동작하는 GUI 앱의 첫 발걸음인 Hello World를 만들어 보겠습니다.


일단 비주얼 스튜디오 2017를 실행하도록 합니다.


그리고 파일 - 새로만들기 - 프로젝트를 누릅니다.

아래와 같은 창이 나타나게 되는데요, Windows 데스크톱 마법사를 누릅니다.


응용 프로그램 종류를 Windows 응용 프로그램으로 선택하고, 추가 옵션에 빈 프로젝트를 체크합니다.



프로젝트를 생성하고, 소스파일에 main.c혹은 main.cpp 파일을 하나 추가합니다.


그리고 소스코드에 다음 내용을 붙여넣습니다.



#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <tchar.h> 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	MessageBox(NULL, _T("\tHello, World!"), _T("My First Win32 App"), NULL);
	return 0;
}


그리고 소스코드를 저장한 뒤, Ctrl + F5 키를 눌러서 빌드 및 실행을 해 봅니다.

다음과 같은 화면이 나타나면 성공!


이제 소스코드에 대한 간략한 설명을 해 보겠습니다.


첫번째 줄 #define WIN32_LEAN_AND_MEAN 이라는 구문은 불필요한 라이브러리들을 빌드에서 제외시키기 위한 내용이다. 빌드 시간을 줄일 수 있다. #include <windows.h> 를 선언하기 전에 define 해야 하며, 외부 MFC 헤더들을 포함시키지 않는다는 뜻입니다.


windows.h 는 메인 윈도우즈 헤더이며, Win32 API를 이용하여 앱을 작성할 시 필요합니다.

tchar.h 헤더는 _T(x)라는 매크로 함수를 사용하기 위해서 포함했다. _T(x)라는 매크로 함수는, 앱에서 유니코드 문자열 셋을 사용함에 따라 c언어에서 ascii 기반 char 배열형태의 문자열이 유니코드 기반 문자열과 호환되게 하는 매크로 함수이다. _T(x) 함수를 제거하고 프로그램을 실행할 시 에러메시지와 함께 컴파일이 안되는 모습을 확인할 수 있을 것입니다.


일반적인 콘솔 앱과는 달리 main 함수 대신 WinMain이라는 함수가 있다. 콘솔 앱에서 main함수와 같은 프로그램의 진입점(entry point)역할을 한다. WINAPI라고 되어 있는 부분은 해당 함수의 호출 규약(calling convention)을 규정해주는 구문이다. 호출 규약을 지정하지 않으면 C 호출 규약이 기본값으로 지정되게 된다. 윈도우즈 응용프로그램에서는 WINAPI 호출규약을 지정해야 합니다.


이제 WinMain 함수의 인자에 대해 알아보도록 합시다.

첫번째 인자 hInstance는 응용 프로그램의 인스턴스에 대한 핸들이다. 인스턴스란 실제 실행되어 메모리에 할당된 놈이라고 보면 된다. 실행되기 전 프로그램은 틀만 가지고 있을 뿐이고, 실행된 프로그램은 메모리를 할당받은 하나의 실체, 즉 인스턴스이다. 핸들이라고 하는 것은 해당 인스턴스에 개별적으로 부여된 아이디값 같은 것으로 보면 된다. 파일에 대한 핸들은 파일 디스크립터(File descriptor)라고 하듯이 윈도우 앱 프로세스에 대한 핸들은 hInstance 입니다.


두번째 인자인 hPrevInstance는 항상 NULL이다. 이 값은 Windows 3.1 시절에 사용되던 값으로, 가튼 앱의 다른 인스턴스가 이미 실행중이면 그 인스턴스의 핸들이 넘어 왔었습니다.


세번째 인자 IpCmdLine은 앱 실행 시 커맨드 라인 인자(Command Line Argument)를 담은 문자열 포인터이다. 예를들어 커맨드 라인에서 앱을 실행할 때 "HelloWorld.exe asdf -d 123" 이라는 명령어로 앱을 실행했다면, "asdf -d 123"이라는 문자열을 가리키게 됩니다.


제번째 인자인 nShowCmd는 앱이 실행된 후, 윈도우 상단 제목 줄에 나타날 앱의 제목을 결정합니다.



OpenGL을 사용한 예제 프로그램을 구동해보도록 하겠다. 일단 실행환경은 다음과 같다.


- Windows 10 64bit

- Microsoft Visual Studio 2017 Community

- GLUT


GLUT은 다음 링크에서 다운받을 수 있다.

https://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip


GLUT 라이브러리를 다운로드 받아서 압축을 풀면 다음과 같이 나타나게 된다.




예제들은 다음 링크에 있는 녀석들을 참조할 예정이다.

https://www.opengl.org/archives/resources/code/samples/glut_examples/examples/examples.html


일단 비주얼 스튜디오를 실행한다.






상단 메뉴에서 파일 - 새로 만들기 - 프로젝트 를 실행한다.

Win32 콘솔 응용프로그램이라는 항목이 있으면 그녀석을 선택하면 되는데, Windows 콘솔 응용프로그램이 있다. 

이 경우 좌측의 Windows 데스크톱을 눌러서 Windows 데스크톱 마법사를 통하여 프로젝트를 생성해주도록 한다.





Windows 데스크톱이라는 항목을 선택한 모습이다. 가장 아래에 Windows 데스크톱 마법사라는 항목이 나타나게 된다. 그냥 Windows 콘솔 응용 프로그램을 선택할 경우 '빈 프로젝트'라는 옵션이 나타나지 않게 된다.


Windows 데스크톱 프로젝트라는 추가 설정 창이 나타나게 되는데, 추가 옵션에 '빈 프로젝트'라는 항목에 체크를 해 주도록 한다. 그리고 프로젝트를 생성한 뒤, 프로젝트가 생성된 디렉토리로 이동하도록 한다. 기본 설정에 따라서  C:\Users\<유저명>\source\repos 라는 디렉토리에 Visual Studio 프로젝트가 생성되었다.





그리고 다운받은 GLUT에 들어있는 파일들을 아래와 같이 디렉토리를 만들어서 구성해주도록 한다. opengl_ex는 프로젝트를 생성해서 나타난 디렉토리이고, 같은 레벨에 3rd_party라는 디렉토리를 만든다.


그리고 3rd_party라는 디렉토리 하위에 GLUT이라는 디렉토리를 만든다.

그리고 GLUT이라는 디렉토리 하위에 GL이라는 디렉토리를 만들고, 같은 레벨에 glut.lib, glut32.lib 파일을 복사해서 집어넣어준다.

그리고 GL 디렉토리 안에는 glut.h 파일을 복사해서 집어넣어준다.


그리고 나서 Visual Studio에서 해당 외부 라이브러리 포함에 관련된 설정을 해 주어야 한다.





아래 그림에서 프로젝트 이름에 마우스 오른쪽 버튼을 눌러서 속성을 누른다.


그러면 아래와 같은 창이 뜨게 되는데, C/C++라는 필드가 보이지 않는 경우가 있다. 이러한 경우 소스코드를 하나 추가해주면 해당 필드가 나타나게 된다. 따라서 아래 창을 끄고 이전 화면으로 돌아간다.

소스파일이란 항목에 마우스 오른쪽 버튼을 클릭해서 추가 - 새 항목을 누른다.

C++ 파일을 선택하고 파일명에 main.cpp라고 작성한 뒤 추가를 누른다. 사실 구동해보려는 예제 코드가 c 소스코드이므로 main.c 라고 해도 무방하다.






openGL 예제코드가 있는 사이트에서 아무 코드나 고른다.

URL https://www.opengl.org/archives/resources/code/samples/glut_examples/examples/examples.html

선택한 소스코드를 Ctrl + A 키를 눌러서 모두 선택한 뒤 복사한다.


생성한 소스코드 파일에 고스란히 붙여넣기 한다.

그리고 다시 프로젝트에 마우스 오른쪽 버튼을 눌러서 속성 탭을 띄우면 C/C++라는 필드가 활성화 된 것을 볼 수 있다. C/C++ - 일반 탭에서 추가 포함 디렉터리 항목에 다음과 같이 입력한다.


../../3rd_party/GLUT/;

추가한 디렉토리 경로와 일치하는 것을 알 수 있다.

그리고 링커 - 일반 탭에서 추가 라이브러리 디렉터리에도 아래와 같이 입력한다.

../../3rd_party/GLUT/

링커 - 입력 탭에서 추가 종속성 탭의 오른쪽 끝에 있는 아래 꺽쇠를 누른다.


나타난 매뉴 중 <편집..> 을 누른다.



아래와 같이 3개의 항목을 추가해준다.

opengl32.lib

glu32.lib

glut32.lib


확인을 누르고 적용 버튼을 누른 뒤 변경사항을 저장한다.

그리고 main.c파일이 있는 곳에 dll파일들을 붙여넣기 해 준다. glut.dll 파일과 glut32.dll파일이 있다.




그리고 프로젝트 화면에서 Ctrl + F5를 누르면 정상적으로 실행되게 된다.




마이크로소프트사에서 개발한 대표적인 IDE인 비주얼 스튜디오(Microsoft Visual Studio)는 강력한 IDE입니다. 주로 C/C++언어 개발을 지원하지만, 최근에는 C#을 이용한 데스크탑 앱 등 다양한 언어와 어플리케이션을 지원합니다.


원래 수백만원대의 라이센스 비용을 지불해야지만 사용할 수 있는 IDE였으나, 마이크로소프트사에서 돈을 많이 벌어서 무료버전인 Community Edition을 제공합니다. 해당 IDE를 한번 설치해보도록 하겠습니다.


아래의 링크로 이동합니다.

https://www.visualstudio.com/ko/free-developer-offers/

우리가 설치할 것은 Visual Studio Community Edition이므로 가장 좌측에 보이는 '다운로드'를 클릭합니다. 그러면 잠시 뒤 아래와 같은 화면이 나타나면서 vs_Community 라는 파일이 다운로드되게 됩니다.

다운받은 파일을 실행하면 다음과 같은 화면이 나옵니다. '계속'을 눌러줍니다.


vs_Community 라는 파일은 PC에 비주얼 스튜디오를 설치하도록 하는 클라이언트 앱입니다. 대신 설치해주는 프로그램이라고 보시면 됩니다. 조금 기다리면 다음과 같은 화면이 나타나게 됩니다. 어떠한 용도로 Visual Studio 를 설치하느냐에 따라 선택사항이 달라질 수 있습니다.


저 같은 경우는 C++을 이용한 데스크탑 앱 개발 및 DirectX를 이용한 게임 개발이 목적이므로, 두개를 체크했습니다. 설치크기가 수백 MB에서 갑자기 5GB가량으로 올랐습니다. 용도를 선택하면 필요한 의존 라이브러리 등을 알아서 다 설치를 해 줍니다. 필요없는 것 까지 선택을 하면 용량을 많이 사용하고, 설치 시간도 많이 걸리게 됩니다. 필요최소한 내용을 선택해서 설치하시면 됩니다.

위와 같이 설치과정 화면이 나타나게 됩니다. 


완료되면 다음과 같은 화면이 나타나며 컴퓨터를 재시작하면 계속해서 설치가 이어지게 됩니다.




시작 메뉴에서 검색을 해서, Visual Studio를 실행합니다.



로그인을 하면 다양한 기능을 제공해준다고 하네요. 저는 일단 이 기능을 사용하지 않고 사용하도록 '나중에 로그인'을 눌렀습니다.


위와 같은 화면이 나타나는데요. 테마는 본인이 기호에 맞게 설정하시면 되고, 개발 설정은 개발하려는 앱의 형태에 따라서 고릅니다. 저는 C++ 어플리케이션을 개발할 것이므로, C++ 을 선택했습니다. 혹시 C언어로 개발하실 분도 C++를 선택하셔도 무방합니다.



Visual Studio가 실행된 모습입니다. 이제 마음껏 개발하시면 됩니다.

+ Recent posts