티스토리 툴바

잘 모르면 그냥 가만히 있어라.

일년 전 이맘때쯤 진행하던 프로젝트에서 기존 JAVA MVC 패턴의

프로젝트를 ASP.NET 으로 Conversion하는 프로젝트가 있었습니다.

당시 PM의 강력한 의견은 ASP.NET의 Server Control들은 성능상 불리하니

서버 컨트롤들을 사용하지 말고 개발하라는 지시를 내렸습니다.

 

사실 그 사람에게 서버컨트롤=성능불리 라는 고정관념을 심어준 것은 우리회사와

진행했던 지난 프로젝트에서 모 신입개발자의 어이없는 실수 때문이었습니다.

String vs StringBuilder의 성능차이는 이미 잘 알려진 사실입니다.

대부분의 신입개발자들이 흔히 이런 실수를 저지르지만 당시 문제의 코드를

너무 오랫동안 방치해서 웹사이트의 전반적인 퍼포먼스가 최악이었던 상황이었습니다.

 

당시 나를 포함한 많은 개발자들이 서버컨트롤이 성능상 전혀 불리하지 않다는

결정적 증거를 보여줄 수 없었기 때문에 개발 생산성이 얼마나 떨어지는지에 대한

감을 못 잡는 PM을 원망하면서 울며 겨자먹기식으로 힘들게 프로젝트를 진행했던 기억이 나네요.

 

차라리 코딩을 아예 모르는 PM이라면 이런 경우 더 수월한 면이 있습니다.

조금 안다고 생각하는 사람을 만나면 그 사람을 이해시키기 위하여

프로그래밍 언어교육 수준의 강의를 펼쳐야 하는 경우도 있습니다.

 

최근 ScottGu's Blog에서 ASP.NET MVC 1.0 RC가 발표 소식을 들었는데요

곧 있으면 정식버전이 나올 테니 RC버전으로 테스트 개발을 진행하려고 합니다.

아직 MVC 패턴에 대해 잘 모르겠다 싶으신 분들이 있다면

DevDays2008에서 장현희님께서 발표하신 세션(ASP.NET 웹 개발의 새로운 패러다임 – ASP.NET MVC)을

들어보시면 조금 이해하는데 도움이 되리라 생각합니다.

세션의 발표자료와 소스코드를 다운로드 할 수 있습니다.

그런데 MVC RC버전이 발표되기 전 작성된 예제 소스코드라서

RC버전에서 바뀐 System.Web.Mvc.IModelBinder 인터페이스 때문에

예제코드를 실행하게 되면 컴파일 에러가 발생합니다.

SchedulePartial.cs 파일의 소스코드를 아래처럼 변경해 주시면 됩니다.

크리에이티브 커먼즈 라이선스
Creative Commons License

Telnet 기능 활성화 방법

Vista에서 기본적으로 telnet 기능이 비활성화 되어 있습니다.

활성화 하는 방법은 시작->제어판->프로그램-> 프로그램 기능 사용/사용 안 함 을 선택해서

telnet을 체크해 주면 됩니다.








 

저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

HISTORY OF .NET

Dev 2008/09/27 10:00 |



SOA(Service Oriented Architecture) 관련하여 RFP를 준비하면 이런저런 자료를 모으는 중에

조금 지난 자료이긴 하지만 눈에 띄는 동영상이 있어 포스팅 합니다.


MSDN의 POP & Conference를 보면 다양한 동영상이 올라와 있습니다.
대부분의 컨텐츠들이 한국 Microsoft에서 자체 제작하여 올린것 인데요.
개발자에게 좀더 다가가기 위한 한국MS의 많은 노력이 옆보입니다.


그중에 눈에 띄는 것은 
닷넷의 역사를 소개하는 동영상 입니다.
과거부터 현재까지 그리고 앞으로 개발 방향을 짚어보고
최신의 기술 동향 및 개발 기술들을 잘 소개하고 있습니다.


제가 소개한 내용 이외에도 많은 정보가 있으니 관심 있는 분들께서는

아래 주소를 참고하세요.
http://blogs.msdn.com/popcon/


올라와 있는 동영상은 모두 soapbox 라는 MSN Video 서비스를 이용 했네요.

MS에서는 역시 구글의 YouTube를 의식하지 않을수 없겠지요.


경쟁을 통해 더욱 발전할수 있다는걸 두 회사를 보면 느낄수 있습니다.

2000 가을 NGWS (Next Generation Windows Services)
2002 .NET 1.0 VS.NET 2002 - C#, Smart Client, Web Services, ASP.NET
2003 .NET 1.1 VS.NET 2003 - .NET CF
2005 .NET 2.0 VS 2005 - SLM, Software Factories, DSL
2007 .NET 3.0 - XAML, WPF(XBAP), WCF, WF, CardSpace
2008 .NET 3.5 VS 2008 -






저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License
내가 겪어온 경험에 비추어 밑에서 언급한 내용중 뜨끔한것이 한둘이 아니다.
프로세스의 필요성은 느끼고 있으나 거기에 투자하는 시간이 아깝다는 결론이다.


정보기술, 즉 IT는 기업의 비즈니스에 있어 점점 중요한 의존 요소(dependencies)로 작용하고 있다. 제조업체의 모든 설비는 IT 기술을 통해 무인화, 실시간 모니터링 기능을 제공하고 있으며 금융 업무를 하는 직원은 대부분의 업무 자료나 처리 기록을 IT시스템을 통해 조회하거나 저장하고 있다.

IT가 중단되거나 수준이 저하되는 경우 비즈니스에 미치는 영향이 점점 커지고 있으므로 좋은 IT를 제공받는 다는 것은 비즈니스의 안정성과 직결된다고 할 수 있다. 그렇다면 좋은 IT를 어떻게 판별할 수 있을까? 눈에 보이는 웹 화면이 멋지다고 좋은 IT인가?

온갖 기능들이 제공되어 척척박사 역할을 하는 어플리케이션이 좋은 IT인가? 밤낮을 가리지 않고 언제나 사용 가능할 때 좋은 IT라고 할 수 있는가? 아무래도 IT조직의 밖에서 바라보는 IT는 겉모습에 따라 평가하기가 십상이다.

그러나 IT를 실제 사용해야 하는 사용자 또는 IT를 선택하여 도입하는 책임이 있는 사람에게는 좋은 IT를 판별하기 위해 IT의 외양뿐만 아니라 IT 내부에 어떤 일들이 일어나고 있으며 그런 결과로 IT가 어떻게 달라질 수 있는 지를 이해하는 것이 필요하리라고 생각한다.

이 컬럼에서는 필자가 관찰하고 경험한 IT 내부 이야기를 IT외부인이 이해할 수 있는 수준으로 전달하고자 하며 좋은 IT를 판별하는 안목을 가질 수 있도록 하기 위해 역설적으로 IT 문제점을 중심으로 구성하였다.

IT에서의 프로세스
IT는 손으로 만질 수 있는 제품이 아니다. 그러나 IT가 제공하는 기능을 활용하여 업무를 처리한다는 점에서는 제품으로 볼 수도 있다. 좀더 정확하게 말하면 서비스에 가깝다. 서비스에 대한 사전적인 정의는 서비스 제공자의 자원과 기술을 통해 가치를 제공받는 것(참조: ITIL V3)이다.

좋은 제품과 좋은 서비스를 가능하게 하는 것은 제품과 서비스를 생산하는 능력에 달려있다. 그런데 그 능력을 일관성 있게 유지하기 위해서는 프로세스라는 체계 또는 틀이 있어야만 가능하다는 것은 일반적인 상식이다. 엄밀하게 말하면 프로세스와 체계(system)는 서로 높낮이가 다른 의미로 사용되지만 설명하자면 복잡한 얘기가 될 것 같아서 여기서는 생략하도록 하겠다.

그런데 프로세스 없이 일한다는 것이 있을 수 있는가 의아해 하는 사람이 있을 것이다. 이 컬럼에서 사용하는 ‘프로세스가 없다’는 표현은 프로세스가 전무하다는 것 보다는, 프로세스가 있기는 있지만 드문 드문 작동한다거나, 프로세스간의 활동들이 연결되지 않는 상황을 모두 포함하는 것이라고 먼저 이해를 하기 바란다.

프로세스가 없는 조직의 특징
회사 내의 한 직원이 업무를 하고 있는 데 이 업무가 어떤 고객의 요청으로 말미암아 본인에게 할당되었는지를 알 수 없거나 또는 고객에게 서비스나 제품을 제공하는 데 있어서 이 업무가 어떤 관련이 있는지를 아무리 생각해도 알 수 없는 경우, 그리고 이 업무가 제대로 끝난 것인지 또 추가로 진행해야 하는 것인지를 어느 누구도 명쾌하게 설명해주지 못하는 경우는 전형적으로 프로세스가 없는 조직의 경우다.

반대로 프로세스가 있는 조직의 경우는 회사 내의 대부분의 업무가 특정 프로세스 틀 내에서 소속이 되어 실행이 되고, 업무를 수행하는 직원은 그 업무의 원천이 고객 요청인지 또는 제품이나 서비스의 개선을 위해 자체적으로 발의한 내부 요청인지를 명확하게 이해하고 있으며 이 업무의 시작, 처리 및 종료를 포함하는 진행상황이 조직 내에 명확하게 알려진다.

IT에서는 프로세스가 없는 경우 어떤 현상이 나타날까? 경험에 의하면 프로세스가 없는 IT조직은 다음과 같은 특징을 보인다.

. 신입사원들이 할 일이 없다.

. 요청한 사항이 어떻게 처리되고 있는지 또는 누락이 되었는지 내부에서는 알 길이 없다.

. 새로운 장비가 들어오거나 장애가 발생하면 그제야 바쁘게 움직인다.

. 업무노하우는 IT부서의 고참만이 알고 있다.

. 유능한 직원에게는 전화도 몰린다. 따라서 바쁜 사람만 늘 바쁘다.

. 며칠 동안 열심히 IT 업무를 했지만 그 노력은 어디에도 나타나지(심지어 월간보고에도) 않는 경우가 있다.

. 업무의 시작과 끝이 불분명하여 늘 찜찜하다.

. 다른 팀으로 넘기거나 문의한 업무가, 처리 되거나 피드백이 왔는지 또는 무시되었는지 파악하기가 어렵다.

프로세스가 없는 IT조직의 가장 큰 특징(문제점)은 문제 해결 능력이 없고 개선이 일어나지 않아 정체된다는 것이다. IT의 결함, 장애 또는 불만사항이 발생하더라도 사전에 정의한 프로세스가 없다 보니 어떤 인과 관계로 인해 이러한 이슈 사항이 발생하였는지를 파악하기가 불가능하다.

그러다 보니 위와 같은 문제들은 대부분이 임시 해결책으로만 처리되고 동일한 문제들이 재발하지 않기를 소원할 뿐이다. 프로세스가 없는 IT조직의 부조리한 상황에 대해서는 차후 연재되는 컬럼을 통해 상세하게 다뤄보겠다.

사용자 삽입 이미지
◇ ISO/IEC 20000* 표준에서 정의한 IT프로세스


부실한 IT조직이 생존 가능한 이유
이런 IT조직이 꿋꿋하게 생존해 올 수 있는 이유는 무엇인가? 이것은 눈에 잘 보이지 않는 IT 프로세스의 비가시성(invisibility)과 프로세스를 받아들이지 않으려고 하는 IT의 저항에 기인한다고 볼 수 있다. 제품을 생산하는 제조업의 경우 프로세스라는 것이 물리적인 구역과 설비로 구성되어 있어 프로세스라는 것의 좋고 나쁨을 눈으로 판단할 수 있는 여지가 있다.

그러나 IT의 경우 프로세스는 전혀 눈에 보이지 않는다. 다만 IT 담당자의 행동을 관찰하거나 문서 또는 기록으로만 확인이 가능하다. 이런 까닭에 프로세스가 탄탄하게 갖추어진 IT조직과 프로세스가 부실한 IT조직을 외부에서 판별하기가 어려울 수 밖에 없다.

우습게도 이러한 비가시성은 부실한 IT조직에게 자신의 부실함을 외부에 들킬 위험이 줄어드는 안전장치인 셈이다. 또 부실한 IT조직은 프로세스의 도입을 거부하는 경우가 많다. 개인에 의존하는 전통적인 IT 운영방식을 고집하는 것이다. 그들은 왜 프로세스를 갖추려 하지 않는가? 프로세스 도입을 거부하기 위해 늘어놓는 부실한 IT들의 변명은 다음과 같다.

. 조직의 규모가 작아서 프로세스를 구축하기가 어렵다.

. 일 처리하는 것이 우선이다. 프로세스를 따르느라 소비하는 노력이 아깝다.

. 프로세스를 따르지 않아도 결과물은 잘 나올 수 있다.

. 그렇게 살아와도 (비즈니스에) 별 문제가 없었다.

. 감시하려고 하는 것이 아니냐

이들 변명들은 프로세스에 대해 오해나 프로세스 도입에 대한 편견을 나타내고 있다. 프로세스가 선의의 의도로 도입되지 않는 다면 이러한 변명은 타당성이 있을 수 있다. 그러나 IT의 소비자인 IT 사용자에게 좋은 IT를 제공하고자 하는 목적으로 도입되는 프로세스는 IT제공자로서의 선의이자 더 나아가서는 책무라고 할 수 있다.

생존을 위한 IT 프로세스 도입
IT가 국내에 본격적으로 등장한 80년대와 90년 초반까지는 IT의 놀라운 기술을 뽐내던 시대였다. 수동으로 처리하던 업무를 자동화 시켜주는 혁신적인 기술에 매료되어 IT가 좋은 대접을 받았었다.

그러나 90년대 중반을 들어서면서 이러한 입지에 변화가 일어나게 되었다. IT기술이 보편화된 것이다. 많은 IT조직이 IT 기술을 구현하고 제공할 수 있는 능력을 가지게 되었다는 것이다. IT의 보편화는 IT를 사용하는 입장에서는 축복이지만 IT의 호시절을 경험한 IT 입장에서는 고난의 서막이다.

어떠한 업무라도 IT기술로 자동화시킬 수 있다는 것을 사용자가 눈치채고 있다면, 이제 차별화된 IT의 능력은 서비스 수준으로 판가름 나게 된다. 1990년 초반에 영국에서 출판한 ITIL 책들은 이러한 IT 서비스의 중요성을 강조하며 기술 위주의 IT에서 서비스로의 전환을 강조하였고 이러한 서비스 중심의 IT는 현재 전세계적으로 보편화된 IT 철학으로 점차 자리잡아 가고 있다.

호텔이나 레스토랑의 좋은 서비스가 내부 프로세스 없이 가능하리라고 보는 사람은 없을 것이다. 좋은 IT서비스를 제공한다는 것은 내부에 견고한 IT 프로세스를 갖추어야만 가능하다. IT는 이제 생존을 위해서 프로세스를 갖추어야 한다.

탄탄한 프로세스를 갖추고 좋은 IT서비스를 제공하는 IT조직의 수가 늘어나게 되고 사용자의 IT판별 능력이 높아질수록 부실한 IT조직의 생존 가능성은 낮아 질 수 밖에 없다. 국내의 많은 IT조직의 변화를 수 년 동안 관찰한 결과 이러한 시대가 다가오는 속도는 더욱 빨라지고 있다고 본다.

출처:ZDNET (http://www.zdnet.co.kr/itbiz/column/anchor/yscho/0,39044217,39172470,00.htm)
크리에이티브 커먼즈 라이선스
Creative Commons License

IIS 404 ERROR

Dev 2008/08/05 11:03 |

보통은 디폴트로 사용하지만 가끔 부주의로 인하여 MIME 형식이 지워진 경우가 있다.

IIS 6.0에서는 알수없는 MIME형식을 처리하지 않는다 ㅡ.ㅡ

그래서 실제 경로에 파일은 존재하는데 404 ERROR(File Not Fount)를 만나는 경우가 있다.

http://support.microsoft.com/kb/326965/ko

이렇게 해서 대부분의 문제가 해결되겠지만

아주 가끔은 .exe 확장자에서 여전히 404 에러가 발생한다.

좀더 알아보았더니 스크립트 전용으로 설정되어 있어야 하는군 ㅡ.ㅡ

http://blogs.msdn.com/david.wang/archive/2005/07/11/Allow_file_downloads_on_IIS_6.aspx

가끔 겪는 또 다른 문제는 .NET FRAMEWORK 설치후 IIS를 설치한 경우

이런경우는 C:\Windows\Microsoft.NET\Framework\v2.0.50727 폴더의 aspnet_regiis -i 옵션으로

등록해주면 OK

크리에이티브 커먼즈 라이선스
Creative Commons License

WINDOW2003 SHUTDOWN EVENT

Dev 2007/07/04 18:16 |

새로 설치한 WINDOW2003이 아침이면 리부팅 되어있어서

적잖이 당황하게 만들어 찾아보니 아래와 같은 방법으로

리부팅된 시간을 이벤트 추적기에 표시할수가 있군 ^^;

 

시스템 종료시 이벤트 추적기 표시하기
시작 > 실행 > gpedit.msc입력 > 그룹정책개체편집기 > 컴퓨터구성 > 관리 템플릿 > 시스템 > 우측창에서 시스템 종료이벤트 추적표시 더블클릭 > 사용 으로 변경

크리에이티브 커먼즈 라이선스
Creative Commons License

사용자 삽입 이미지

소켓 강좌


차례[-]

Network의 기본

네트워크의 기본적인 사항에 대해 먼저 알아 보도록 하겠습니다.%%% 거의 인터넷 표준으로 자리잡은 TCP/IP에 대해서만 알아보도록 하겠습니다. 그러나 TCP/IP 주제만 가지고도 몇 개의 강좌를 해야 되므로, 자세한 내용은 다른 서적이나 강좌를 참고하세요.%%% 제가 추천하는 책은

  • TCP/IP Illustrated, Volume 1 (W. Richard Stevens 저)
  • TCP/IP Protocol Suite (Behrouz A, Forouzan 저)

를 추천합니다. 둘 다 원서입니다. 영어가 부담스러우신 분은 각 서적에 대한 번역서도 있으니 번역의 질 등을 잘 알아보고 선택하시기 바랍니다.%%% 다른 책을 참고하셔도 크게 상관은 없으나 간략한 소개만 되어 있는 것이 아닌 자세한 내용이 있는 것을 보세요. 왜냐하면 정확한 TCP/IP의 동작을 알아야지 네트워크에 오류가 있을 때 보다 쉽게 오류를 수정할 수 있기 때문입니다.

클라이언트와 서버구조

대부분 네트워크 프로그램은 서버와 클라이언트로 구분할 수 있습니다. 두 개를 구분하는 기준은 그 역할이 무엇이냐 입니다. 그 기준을 간단히 설명하면

  • 서버 : 클라이언트가 어떤 작업의 요청이 들어오면 요청을 처리하고 그 결과를 클라이언트에게 알려 주는 역할
  • 클라이언트 : 서버에게 작업을 요청하고 그 작업의 결과를 받아서 보여주는 역할

이렇게 설명할 수 있습니다.%%% 클라이언트/서버 구조는(이하 C/S) 우리 일상에도 많은 예가 있습니다. 가장 흔한 예가 웹 브라우저와 웹 서버입니다. 인터넷 익스플로러나 네스케이프가 클라이언트 역할을 하고 접속한 그리고 우리에게 서비스를 제공하는 컴퓨터가 서버라고 생각하시면 됩니다. 정확히는 아파치나 IIS 같은 http 서버를 말합니다.

여기서 클라이언트와 서버의 차이는 클라이언트는 서버 하나와 통신을 하지만, 서버는 여러 개의 클라이언트를 상대합니다. 그래야만 더 많은 사용자들에게 서비스를 제공할 수 있습니다.

그러나 모든 것이 C/S 구조로 된 것은 아닙니다. 클라이언트와 서버 역할을 모두 하는 것도 있습니다. P2P(Peer To Peer)가 그것입니다. 대표적인 소프트웨어로 소리바다나 구루구루 같은 것들이 여기에 속합니다. P2P는 클라이언트와 서버의 역할을 동시에 하고 있습니다.(여기에 대한 내용은http://extremendl.net에서 논의되고 있으니 참고하세요.)

TCP/IP

TCP/IP 를 알아보기 전에 간단히 프로토콜(Protocol)의 정의에 대해서 알아보도록 하겠습니다.

"둘 이상의 통신 개체 사이에 교환되는 메시지의 형태, 의미, 전송 순서, 그리고 메시지 송수신 및 기타 사전에 수행할 동작을 정의한 규약"

두 개 이상의 호스트(컴퓨터) 사이에서 데이터를 약속에 의한 방법으로 주고 받자는 것입니다. TCP/IP 도 이런 프로토콜의 한 종류입니다. 보통 TCP/IP 라 부르지만 정확히는 TCP/IP 프로토콜 그룹(패밀리)라고 부릅니다. 그룹이므로 TCP와 IP 프로토콜만 있는 것은 아닙니다. TCP, UDP, ARP, RARP, ICMP 등 여러 가지가 같이 있는 프로토콜입니다. 여기에서 대표적인 프로토콜?TCP와 IP 이기 때문에 TCP/IP 라고 부릅니다.

네트워크 프로토콜들은 대부분 계층(Layer)이라는 개념을 가지고 있습니다. 여기서 OSI 7 Layer 를 낯?드리고 싶지만, 이번 글은 소켓?련?글이므로 다른 서적이나 강좌를 참고하세요. 이것은 꼭 알아야 할 기본적인 네트워크 개념이니 꼭 익히셔야 합니다.%%% 계층의 개념을 간단히 말씀 드리면, 어떤 계층의 통신 상대의 같은 계층과 의미 있는 통신을 합니다. 그리고 각 계층들은 그 밑 계층이 제공하는 서비스를 이용하여 그 상위 계층에 서비스를 제공합니다. 만약 한 계층의 인터페이스가 변한다면, 그 바로 상위 계층에만 영향을 줍니다. 그리고 어느 한 계층에서 생성된 메시지들은 상대방의 같은 계층에서 분석되고 작동합니다. 무슨 말인지 조금 어렵게 느껴질 수 있습니다. 저도 계층 개념을 이해하는데 어려움을 느꼈습니다. 일단 OSI 7 Layer 를 보시면서 네트워크의 개념을 잡으시기 바랍니다. 간단히 그림을 보도록 하겠습니다.

<그림> 네트워크 계층과 흐름

그럼 TCP/IP 의 계층은 어떻게 되어 있는지 알아보도록 하겠습니다.

 

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

응용계층 :  TELNET, FTP등

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

트랜스포트 계층  : TCP, UDP

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

네트웍 계층 : IP, ICMP, IGMP

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

링크 계층 : 장치 드라이버와 인터페이스 카드

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

<그림> TCP/IP의 계층

 

TCP/IP는 위 그림과 같이 4계층으로 이루어져 있습니다. 혹은 링크 계층을 물리 계층과 데이터링크 계층으로 나누기도 하는데 크게 다르지 않습니다. OSI 7 Layer 를 보시면 큰 무리 없이 이해됩니다. 그럼 각 계층을 하나씩 살펴보도록 하겠습니다.

1) 링크 계층

물리적인 인터페이스와 관련된 하드웨어적인 부분을 제어합니다. 운영체제와 디바이스 드라이버나 그와 관련된 랜카드, 그와 연결된 케이블 같은 것을 제어하는 계층을 말합니다.

2) 네트워크 계층

네트워크상의 패킷의 이동을 제어하는 계층입니다. 패킷이라는 말이 처음 나왔는데, 패킷은 네트워크를 통해 데이터를 전달할 때 헤더와 데이터의 묶음을 말합니다. 정확히 이 계층에서는 IP 데이터그램이라고 합니다.(IP를 이용하여 신뢰성 없이 전달됩니다.) 이 패킷에는 송/수신지의 정보가 포함되어 있습니다. 이 계층의 역할은 한마디로 우편물의 주소와 같습니다. 어느 곳에 편지를 보낸다고 할 때, 여러 우체국을 거쳐 목적지의 우편함까지 옵니다. 우편물을 패킷이라면 우체국들은 라우터나, 게이트웨이라고 할 수 있습니다. 패킷이 가려고 하는 호스트(컴퓨터)까지의 이동을 담당하는 곳이 네트워크 계층입니다. 주로 IP가 이 역할을 하는데 IP는 신뢰성이 보장되지 않습니다. 확실히 갔는지 아닌지 알 수 없습니다. 또 다른 기능들이 많이 숨어 있는 계층이지만 여기까지 하도록 하겠습니다.

3) 트랜스포트 계층

상위 응용 층에 대해 두 호스트간의 데이터 흐름을 제공합니다. TCP/IP 에는 TCP 와 UDP라는 트랜스포트 프로토콜이 있습니다.

TCP는 위의 상위계층이 준 데이터를 목적지로 전달과 흐름제어의 기능을 제공합니다. 흐름의 제어란 데이터를 언제 보내야 하는지 얼마큼의 크기로 보내야 하는지 어떤 것을 보내야 하는지를 제어한다고 간단히 생각해도 될 것 같습니다. 위의 네트워크 계층에서 우편물과 같아 잘 보내어 졌는지 잘 받았는지 확인할 길이 없다고 했습니다. 그러나 TCP는 이러한 것까지 알아서 해줍니다. 즉 전화라고 보시면 됩니다. 우리가 타인에게 전화를 걸면, 신호음이 가고 상대편이 받을 때까지 기다립니다. 만약 상대방이 받지 않는다면, 우리는 다시 전화를 걸 수 있습니다. TCP도 마찬가지 입니다. 보내고 잘 받지 못했다면 다시 보내는 것이죠. TCP 는 네트워크 계층의 상위계층입니다. 이전 Layer 를 설명할 때, 하위계층의 인터페이스를 이용한다고 했습니다. 즉. TCP도 IP 데이터그램을 이용하여 정보가 전달 되는 것입니다. 그래서 패킷만 두고 본다면, TCP도 신뢰성이 없습니다. 그러나. IP의 상위 계층인 TCP는 이러한 점을 보안하여 서비스를 해줍니다. 즉, “시간이 얼마나 지났는데 와야 할 패킷이 안 온다 무언가 문제가 있다.” 이런 식으로 보안합니다. TCP는 연결지향 서비스이고 두 호스트간의 신뢰성 높은 데이터 흐름을 제공합니다. 연결지향이라는 말은 TCP는 데이터를 주고 받기 전에 클라이언트와 서버가 이제 연결해서 데이터를 주고 받겠다는 약속을 하는 것입니다. 그리고 데이터를 보내면 그 데이터가 반드시 목적지에 도착하고, (일정 시간 내에 받지 못하면 패킷이 손실 되었다고 보고, 다시 보내달라고 요청을 합니다.) 보낸 순서 또한 똑같다는 것입니다. TCP로 보내는 패킷을 TCP 세그먼트(segment)라고 보통 부릅니다.

UDP는 비연결형 서비스입니다. 즉, 클라이언트와 서버가 연결 약속은 하지 않고 바로 데이터를 주고 받는다는 것을 말합니다. 그리고 신뢰성이 없습니다. 즉, 데이터가 목적지에 반드시 도착하리라는 보장이 없습니다. 물론 보낸 순서도 마찬가지입니다. UDP로 보내는 패킷을 UDP 데이터그램(datagram)이라고 부릅니다.%%% 주로 TCP를 사용하기는 합니다만 UDP도 쓰이는 곳도 많습니다. UDP가 속도가 비교적 빠르기 때문에 패킷 하나 없어져도 크게 관계없는 실시간 방송이 라던지 그런 곳에 쓰입니다. 요즘 게임에도 TCP 와 UDP를 같이 섞어서 많이 사용한다고 합니다.

예를 들어 TCP와 UDP를 조금 더 알아 봅시다. 만약 서버에서 3개의 패킷을 보낸다고 가정을 한다면 여러 가지 라우터나 게이트웨이들은 패킷의 IP를 보고 이 패킷의 경로를 정해 목적지까지 보내 줍니다. 3개의 패킷은 가는 도중의 네트워크의 상태에 따라서 다른 경로를 통해 전달 될 수도 있습니다. 1번 패킷이 도중에 손실되고 3번이 2번 보다 먼저 목적지에 도착할 수도 있습니다. 만약 TCP 연결이라면 1번이 도착하지 않았으므로 다시 1번을 보내달라고 하고 서버는 다시 1번부터 3번까지 보내 줍니다. 여기서 1번만 보낼 수도 있지만 알고리즘이 복잡하고 네트워크가 충분히 빠르므로 1, 2, 3을 모두 보냅니다. 그런데 UDP는 3, 2번 패킷을 그대로 받습니다. 1번이 있는 지도 모릅니다. 그냥 받은 대로 쓰는 것입니다. 그런 특성들은 프로그래머들이 짜는 응용 계층에서 별도로 처리를 해주어야 합니다.

4) 응용 계층

간단히 우리가 쓰는 네트워크 응용 프로그램을 말합니다.

IP

IP는 인터넷상의 고유의 주소입니다. 전세계에서 유일합니다. 4바이트(32비트)의 숫자로 구성된 주소입니다. 랜카드와 1:1로 대응 됩니다. 예를 들면 104.245.123.24과 같은 식으로 되어있습니다. 이런 표시 방식을 dotted-decimal 방식이라고 합니다. 사람이 알기 쉽게 이런 식으로 쓰는 것입니다. 실제로는 11010100110... 이런 식으로 되어야 컴퓨터가 알아 볼 수 있습니다. IP는 클래스 A, 클래스 B, 클래스 C, 클래스 D, 클래스 E 가 있습니다.

클래스 | 범위 A 클래스 | 0.0.0.0 - 127.255.255.255 B 클래스 | 128.0.0.0 - 191.255.255.255 C 클래스 | 192.0.0.0 - 233.255.255.255 D 클래스 | 224.0.0.0 - 239.255.255.255 E 클래스 | 240.0.0.0 - 255.255.255.255

각 클래스들은 이런 범위를 가지고 있습니다. 클래스 E는 나중을 위해 예약되어 있는 클래스 입니다. 클래스 D는 멀티캐스트를 위한 IP입니다. 한마디로 우편물이 가기 위한 자신의 집의 주소라고 보시면 됩니다.

도메인 주소

컴퓨터는 IP를 인식하지만 사람이 외우기는 조금 불편합니다. 그래서 도메인 주소라는 것을 사용합니다. bgda.org 이런 식으로 쓰면 사람이 보다 알기겠지요. 컴퓨터는 도메인이 입력되어 들어오면, 그 도메인에 대항하는 IP로 변환해서 사용합니다. 이런 서비스를 DNS (Domain Name Service) 라고 합니다.

Port

포트(Port)는 하나의 컴퓨터에 실행 중인 여러 네트워크 프로그램을 구분하기 위해 부여된 번호입니다. 16비트로 구성된 번호입니다. 즉, 우편물이 집에 도착했는데 그 우편물이 누구의 것이냐는 것입니다. 여기서 집을 하나의 컴퓨터(호스트)라 하고, 주소를 컴퓨터의 IP, 우편물에 적힌 이름은 포트 번호라고 이해하시면 쉬울 것입니다. 즉, 컴퓨터까지는 왔는데 그 컴퓨터의 어느 프로그램이 패킷을 받을지를 알아야 하니 이런 번호가 부여됩니다.%%% 우리가 자주 쓰는 웹 서버나 ftp 서버 같은 것들도 전부 포트번호가 있습니다. 그런데 이런 것들은 자주 많이 쓰이기 때문에 포트번호를 지정해 놓았습니다. 그래서 우리들은 인터넷 주소만 입력하면 바로 웹 페이지를 열 수 있는 것입니다. 포트 번호를 따로 적지는 않습니다. 왜냐하면, 미리 이 포트번호는 http의 번호이다라고 정해놓았기 때문입니다. 그렇게 많이 쓰는 서버들의 포트들을 well-known 포트라고 합니다. 1 - 1024까지는 well-known 포트로 되어있습니다. 그래서 보통 새로운 서버를 만든다면 이 포트(well-know port)는 되도록 피하는 것이 좋습니다. 포트 번호가 16비트니 포트번호는 충분할 것입니다.

루프백(loopback)

클라이언트와 서버가 같은 호스트에서 TCP/IP를 이용하여 서로 통신할 수 있도록 하는 것입니다. 127.0.0.1 - 127.255.255.255 까지가 루프백 주소로써 localhost라는 이름으로도 할당하고 있습니다. 루프백으로 보내어진 데이터는 밖으로는 보내어지지 않습니다. 그러나 브로드캐스트나 멀티캐스트주소로 보내어진 것은 루프백에 복사된 다음 밖으로 나가게 됩니다. 그리고 자신의 IP로 보내어진 것도 루프백으로 보내어집니다.

MTU

MTU(Maximum Transmission Unit) 최대 전송단위라는 것인데 대부분의 네트워크는 패킷의 상한선이 정해져 있습니다. 그것보다 많은 양은 그보다 작게 쪼개어서 보냅니다. 이런 것을 단편화(Fragmentation)라고 합니다. 단편화된 패킷은 받을 때 합쳐지게 됩니다.

Path MTU

두 호스트의 네트워크는 다를 수 있습니다. 즉, 누구는 LAN이고 누구는 전화선일 수도 있는 겁니다. 그때 두 네트워크의 MTU는 다릅니다. 그리고 두 호스트 사이에 어떠한 네트워크도 있을 수 있습니다. 이 두 호스트 사이에 패킷을 전송하는 링크상의 최소 MTU 크기가 Path MTU라 합니다. 만약에 A 와 D가 통신을 한다고 하면 A와 D사이에는 B, C 라는 네트워크가 있다고 해봅시다.

A - B - C - D

A의 MTU가 100 B 200 C 70 D 80이라는 MTU를 가지고 있다면 A와 D의 Path MTU는 70이 되는 것입니다.

TTL

Time-to-live 라는 것으로 패킷이 통과할 수 있는 라우터의 수를 제한하기 위해 사용됩니다. 하나의 라우터를 거칠 때마다 TTL 값이 1씩 줄어들어 0이 되면 패킷은 버려지게(삭제) 됩니다. 라우터를 많이 안거치는 로컬에서는 작아도 상관없지만, 외국이나 그런 먼 곳(거쳐야 할 라우터가 많은) 곳에 보내려면 TTL값은 충분히 커야 합니다. 그렇지 않으면, 가는 도중에 TTL값이 0이 되어 패킷이 삭제될 수 있습니다. 라우터는 두 개의 같은 네트워크를 연결하는 중간 다리 역할을 하는 것이라고 보시면 됩니다. 게이트웨이란 것도 있는데, 이것은 서로 다은 네트워크를 연결하는 역할을 한다고 보시면 됩니다.

TCP 연결 (Three-way Handshake)

위에서 TCP는 연결지향 서비스라고 했습니다. TCP 연결 설정은 다음의 시나리오로 이루어 집니다.

  1. 서버는 들어오는 연결을 받을 준비가 되어 있도록 준비되어야 합니다.
  2. 클라이언트가 접속을 요청합니다. (클라이언트가 서버에게 SYN 세그먼트를 보냄)
  3. 서버는 클라이언트의 SYN 도착을 클라이언트에게 알립니다. (서버가 클라이언트에게 SYN을 보냄)
  4. 클라이언트는 서버에게 SYN도착을 알림(클라이언트가 서버에게 ack를 보냄)

이때 교환하는 패킷이 3개인데 그래서 Three-way Handshake 라 합니다. 쉽게 말해서 클라이언트가 서버에게 “접속한다.” 그러면 서버는 “그래 접속해라.” 합니다. 그리고 클라이언트는 “알았다.” 라고 하고 접속이 완료 되는 겁니다.

TCP 연결 종료

다음과 같은 시나리오 입니다.

  1. 클라이언트에서 close를 호출하면 데이터를 그만 보내겠다는 FIN 세그먼트를 서버에 보냅니다.
  2. 서버가 FIN을 받으면 FIN을 받았다는 ack를 클라이언트에게 보내고 close가 호출됩니다.
  3. 그러면 서버도 FIN을 클라이언트에게 보내게 됩니다.
  4. 그러면 마지막으로 클라이언트는 FIN을 받고 ack를 서버에게 보냅니다.

여기서는 클라이언트가 close를 먼저 했는데. 서버가 먼저 할 수도 있습니다.

소켓 API

이 글에 있는 소스는 TCP/IP 소켓프로그래밍 version C <사이텍미디어>를 참고 하였음을 알려드립니다.

소켓 API란?

네트워크 상에서 호스트간에 통신을 가능하게 해주는 일반적인 인터페이스 입니다. 응용층과 트랜스포트 계층 사이의 중간에 위치해있습니다.

소켓 주소 구조

소켓 API는 소켓과 관련된 주소를 지정하기 위해 일반적인 소켓 구조체를 정의해 놓았습니다.(소켓 주소에는 TCP/IP를 위한 주소만 있는 것이 아니라 다양하게 많이 존재합니다.) 그 형태는

 

struct sockaddr
{
    unsigned short sa_family; /* Address family */
    char sa_data[14]; /* Family-specific address */
};

이렇게 생겼습니다. 실제 TCP/IP 소켓 주소를 지정할 때는 이 구조체를 사용하지 않습니다. TCP/IP에 맞추어 사용합니다. TCP/IP 소켓 주소를 위해 사용하는 구조체를 보면,

 

struct in_addr
{
    unsigned long s_addr;
};

struct sockaddr_in
{
    unsigned short sin_family;
    unsigned short sin_port;
    struct in_addr sin_addr;
    char sin_zero[8];
};

sockaddr_in의 sin_family 인자는 인터넷 주소 패밀리입니다. 이번 강좌에서는 AF_INET를 사용합니다. (일단 이렇게만 알아두세요.) 이건 IPv4 프로토콜이라는 것을 말합니다. sin_port 는 포트 번호를 지정하는 것입니다. short니 2바이트, 16비트 정수입니다. sin_addr은 IP주소가 들어 가는 부분입니다. 위에 struct in_addr의 정의가 나와있습니다. 정의에서 나오는 것과 같이 여기에는 IP의 32비트 주소가 들어 갑니다. 그러니 "128,23,23,14" 이런 문자열이 들어가진 않습니다. 이걸 110010101.. 이런 식으로 변환하여 넣어 주어야 합니다. 두 개를 변환하는 함수 물론 있습니다. 그리고 그 다음이 sin_zero[8]이 부분인데 여기에는 0값이 들어 갑니다. 즉, 사용하지 않습니다. 사용하지 않는데 왜 여기에 있는가 하면, 앞에 일반적인 구조체를 보여 드렸습니다. 그것의 크기에 맞추어서 8바이트 더미 값이 들어가 있습니다.

바이트 순서(Byte Ordering)

컴퓨터 메모리에 데이터가 저장되는 순서를 말합니다. 이것은 수행되는 기계에(CPU) 의존됩니다. 여기에는 Little-Endian 과 Big-Endian이 있습니다. Little-Endian 은 가장 낮은 바이트부터 저장되고 Big-Endian은 가장 높은 바이트부터 저장됩니다. 그림을 보면 쉽게 이해가 가실 겁니다. 4바이트 정수를 저장한다고 하면,

|   1byte   |
+-----------+-----------+-----------+-----------+
|    1      |    2      |    3      |    4      |
+-----------+-----------+-----------+-----------+
|   1byte   |
+-----------+-----------+-----------+-----------+
|    4      |    3      |    2      |    1      |
+-----------+-----------+-----------+-----------+
   addr A     addr A+1     addr A+2    addr A+3

위에 것이 Little-Endian 밑에 그림이 Big-Endian 입니다. Little-Endian 방식에 대표적인 것이 Intel계열의 CPU이고 Big-Endian은 Sparc 계열의 CPU입니다. 같은 기종의 통신이면 바이트 순서는 중요하지 않지만, 서로 다른 기종의 통신이라면 중요해 집니다. 통신을 할 때는 Big-Endian을 따릅니다. 그래서 네트워크로 보내기 전에 이 바이트 순서를 조정해 주어서 보내어야만 합니다.%%% 그럼 네트워크 순서와 호스트 순서를 바꾸는 함수에 대해서 알아 보도록 하겠습니다. 함수 이름에는 규칙이 있는데,

  • h - host
  • n - network
  • l - long
  • s - short

이렇습니다. 예를 하나 들면,

long int htonl (long int hostLong);

무슨 말일까요? 함수 이름이 htonl 입니다. 호스트에서 네트워크로 바꾸라는 얘기입니다. type은 long 형이고요. 이렇게 각각 long와 short에 대해 함수가 있습니다.

long int htonl (long int hostLong);
long int ntohl (long int netLong);
short int htons (short int hostShort);
short int ntohs (short int netShort);

사용의 예를 들어 보죠. 포트번호가 2바이트입니다. 이것을 네트워크로 보내기 위해서는 바이트 순서를 조정해야 합니다. 소켓 주소 구조체에 대입은

SocketAddress.sin_port = htons(appPort);

이렇게 합니다.

소켓기술자란?

소켓 기술자는 유닉스의 파일 기술자와 동일합니다. 다만 그 I/O가 네트워크일 뿐입니다. 쉽게 말해 컴퓨터에서 네트워크로 나가는 문의 고유번호라고 생각하시면 됩니다. 네트워크로 데이터를 보내거나 받으려면 커널에게 소켓을 만들어 달라고 요청을 합니다. 그럼 커널은 소켓 하나를 열고 우리에게 그 소켓의 고유번호를 줍니다. 그것이 소켓 기술자입니다. 유닉스의 파일기술자에 대해 잘 아신다면, 똑같다고 보면 이해가 빠르실 겁니다. 이제부터 소켓번호라고 하면 이 소켓 기술자를 얘기하는 것으로 하겠습니다.

TCP 소켓

이제 TCP 서버와 클라이언트의 기본적인 함수 호출 구조를 살펴 보도록 하겠습니다.

서버 : socket() -> bind() -> listen() -> accept() -> send()/recv()

서버가 여기까지 호출을 하게 되면 클라이언트의 접속이 들어 오는지 기다립니다.

클라이언트 : socket() -> connect() -> send()/recv()

여기서 클라이언트와 서버는 three-way handshake를 하여 연결을 합니다. 그 후에 서버와 클라이언트는 send()와 recv() 를 이용해 데이터를 주고 받습니다. 그리고 close() 함수를 호출하여 접속을 끊게 되는 것입니다.

그럼 서버와 클라이언트에서 공통으로 사용하는 소켓 생성과 소멸에 관련된 함수에 대해서 알아 보도록 하겠습니다. 그 전에 여기서 설명하는 함수는 다른 말이 없으면 리턴 값이 에러면 -1을 리턴하고 errno이라는 전역변수에 에러 값을 넣어줍니다. 이 errno의 값을 보고 무슨 에러가 났는지 알 수 있습니다.

int socket( int protocolFamily, int type, int protocol );

socket 함수의 역할은 커널에 소켓을 열어 달라고 요청을 하여 그 소켓번호를 우리에게 넘겨 줍니다. protocolFamily는 AF_INET를 씁니다. IPv4 protocol 을 사용한다는 것을 알리는 것입니다. 물론 다른 것도 있지만 이 글에는 이것만 씁니다. type은 TCP를 사용할 땐 SOCK_STREAM, UDP를 사용할 땐 SOCK_DGRAM 을 넣어서 어떤 서비스를 사용하는지 커널에 알려 줍니다. protocol은 raw소켓을 쓸 때 말고는 0을 설정합니다. 그러니 여기서는 항상 0을 사용할 것입니다.(IPPROTO_TCP, IPPROTO_UDP를 이용하셔도 됩니다.) raw소켓은 IP계층의 서비스를 직접 이용할 때 쓰는 것입니다.

int close( int sockfd );

소켓을 닫고 통신을 종료합니다. sockfd는 닫을 소켓 번호입니다. 성공하면 0을 실패하면 -1을 반환합니다. 닫힌 소켓은 더 이상 사용할 수 없습니다. 여기선 TCP를 설명하니 TCP에 대해 조금 더 설명하겠습니다. 내부적으로 TCP는 send buffer 와 recv buffer가 있습니다. 만약에 close를 호출 하였는데 send buffer에 보낼 데이터가 남아 있으면 그것을 모두 보낸 후에 앞서 설명 드린 TCP 연결 종료 절차를 따릅니다.

위 두 함수에는 좀더 볼 것이 있는데. 그것은 참조 카운터입니다. socket로 소켓을 열면 참조 카운터가 1 증가 합니다. 그리고 다른 자식 프로세스로 복사될 때도 참조 카운터가 1증가 합니다. close는 참조 카운터를 1감소 시킵니다. 그러다가 참조카운터가 0이 되면 소켓을 닫습니다. 소켓 참조 카운터가 0이 아니라면 그것은 열린 상태가 되는 것입니다.

int shutdown( int sockfd, int howto );

이 함수도 네트워크 연결을 종료시키는 데 사용합니다. close()와 다른 점은 close는 참조 카운터를 1감소시키고 참조 카운터가 0이 되면 종료하는데 shutdown()은 참조 카운터와 상관없이 TCP의 연결 종료 절차를 시작합니다. 그런데 close()함수는 양방향(send recv) 둘 다 종료시키는데 반해 shutdown함수는 howto인자에 따라 동작이 달라집니다. 위에서 close함수를 설명할 때 약점이 하나 있었습니다. close()호출 후에 받을 데이터가 있다면 어떻게 할까요? 그건 받을 수 없습니다. 그러나 shutdown의 howto 인자를 설정하면 그것이 가능합니다. 그 값에는 다음과 같은 것이 있습니다.

  • SHUT_RD : 연결의 recv 한쪽만 닫습니다. 이제 이 소켓으로는 데이터를 받을 수 없습니다. 그리고 recv buffer도 폐기됩니다.
  • SHUT_WR : 연결의 send 한쪽만 닫습니다. 이제 이 소켓으로는 어떤 데이터도 보낼 수 없게 됩니다.send buffer에 남아 있는 데이터는 모두 보낸 뒤에 TCP 연결 종료 절차가 뒤따릅니다.
  • SHUT_RDWR : 연결의 양쪽 다 받습니다.

만약에 자신은 데이터를 다 보냈다 하면, SHUT_WR인자를 설정하여 shutdown()을 호출하면 다른 쪽이 보내는 데이터를 받을 수 있게 되는 것입니다.

이제는 TCP 서버의 기본적인 함수에 대해서 알아 보도록 하겠습니다.

TCP 서버는 통신의 종단에서 클라이언트의 연결요구를 수동적으로 기다리는 역할을 합니다. 그럼 그 과정을 요약하면,

  1. socket() 함수로 소켓을 생성
  2. bind() 함수로 생성된 소켓에 포트번호를 연결
  3. listen() 함수를 이용해 클라이언트의 연결요구를 받도록 함
  4. 클라이언트의 연결요청이 들어오면 accept() 함수로 새로운 소켓을 얻음
  5. send() recv()를 사용하여 클라이언트와 통신
  6. 서비스가 끝나면 close()함수를 이용하여 클라이언트의 연결을 닫음

이제 여기에 관련된 함수에 대해 알아 보겠습니다.

bind ()

int bind( int sockfd,
          struct sockaddr * localAddress,
          unsigned int addressLen );

bind() 함수의 원형입니다. bind함수는 localAddress 변수에 있는 IP 주소와 Port 번호를 연결시켜 주는 역할을 합니다. 만약, 클라이언트(ip 123.145.234.1 포트 6000)가 ip 203.229.234.13 port 5000 번으로 접속을 요청해 왔다고 합시다. 그럼 서버는 그 ip와 포트 변호를 보고 어느 프로그램의 패킷인지를 알 수 있어야 합니다. 바로 그것을 알려 주는 그리고 명시 하는 함수가 바로 bind입니다. 그런데 서버는 여러 개의 클라이언트를 처리 한다고 했습니다. 클라이언트는 똑같은 IP와 포트 번호를 이용해서 (사실 IP는 다를 수 있습니다.) 접속을 해올 것입니다. 그 예를 설명해 보죠.%%% A 라는 서버에 x y라는 두 개의 클라이언트가 접속을 해왔다고 합시다.

서버의 IP 는 111.222.333.44 Port 5000%%% x클라이언트 IP 222.222.222.22 Port 6000%%% y클라이언트 IP 222.222.222.33 Port 7000%%% 이라고 가정하면,

x가 먼저 클라이언트에게 접속을 하고 y가 접속을 합니다. 그러면 TCP는 연결을 위해 소켓이 새로이 생성되는데 그곳에 socket pair이라는 구조체에 서버의 IP와 Port 클라이언트의 IP와 Port를 같이 저장합니다. 그러면 두 개의 x, y클라이언트가 구분이 되겠죠?%%% 이렇게 클라이언트와 서버 IP, Port를 모두 사용하여 구분한다고 생각하시면 됩니다. 그리고 아까 서버의 IP가 다를 수 있다고 했는데 그것은 하나의 컴퓨터에 하나의 IP. 즉, 하나의 네트워크 인터페이스(랜카드) 만이 있는 것은 아닙니다. IP는 인터페이스당 유일하게 하나입니다. 그럼 만약에 서버가 2개의 인터페이스를 가지고 있다면 서버 프로그래밍의 설정에 따라서 동시에 두 IP에서 오는 패킷을 받을 수 있습니다.%%% 나중에 서버 프로그래밍 예제를 보시면 나옵니다. INADDR_ANY 를 서버설정에서 IP부분에 설정을 하면 모든 인터페이스로부터 패킷을 받는다는 의미입니다. 물론 특정 인터페이스에서 오는 것만 받을 수도 있습니다. 그때 서버를 bind할 때 그 받을 IP만 지정하면 되는 것입니다.

listen()

int listen ( int socket, int backlog );

첫 번째 인자는 소켓 번호입니다. 두 번째 인자가 설명이 조금 필요한데, 간단히 설명하면 연결요구 개수의 최대값을 나타냅니다. 무슨 말이신지 이해가 잘 안되실 수도 있는데 조금 더 설명을 드리면, TCP가 접속을 할 때 three-way handshake를 한다고 했습니다. 이 도중에 또 다른 클라이언트가 접속을 요구 할 수도 있습니다. 그럼 어떻게 해야 할까요? 일단 어디에다 저장해두어서 지금 하고 있는 연결설정을 끝내고 차례대로 들어온 순서대로 연결 설정을 해주면 되겠지요? 그 저장해둘 클라이언트 연결 요구의 수를 말하는 것입니다. 내부적으로는 연결이 완료된 것 대기중인 것 이렇게 두 개의 큐가 있습니다. 이 큐는 FIFO(First In First Out) 로 동작합니다. 연결완료 된 것과 대기중인 것 모두 합친 것의 수입니다.%%% 만약에 그 대기수도 다 차있는 상태에서 다시 연결 요구가 들어 오면 어떻게 할까요? TCP는 거기에 대해서 아무 것도 안 합니다. 그럼 클라이언트는 아무 응답이 없으므로 일정 시간 뒤에 다시 연결 요구를 합니다. 예전에는 이 수는 5 를 사용했던 것 같습니다. 많은 예제들이 아직도 5를 사용하고 있는데, 5 라고 해서 꼭 5개만 되는 것은 아닙니다. 운영체제나 네트워크 인터페이스 드라이브에 따라서 다를 수 있습니다. 어떤 것은 입력된 값 그대로 쓰고 어떤 건 여기에다가 1.5를 곱한 수를 사용하는 등 다양합니다. 그리고 이제 5 라는 제한도 없어 졌습니다. 더 큰 수도 지원합니다. 만약에 운영체제에서 지원하는 수보다 더 큰 수를 넣는다면 어떻게 될까요? 그렇게 해도 운영체제가 알아서 최대값을 안 넘게 해준다고 합니다.

 

accept()

int accept( int socket,
            struct socket * clientAddress, u
            nsigned int * addressLen );

 

이 함수의 기능은 listen 함수가 연결 요구의 개수를 지정하고 내부 큐에는 연결 설정이(three-way handshake) 완료된 큐와 대기중인 큐 두 개가 있다고 했습니다. 그 완료된 큐에서 순서대로(FIFO) 하나 가져와서 상대방과 연결된 하나의 소켓을 만드는 역할을 합니다. 만약에 완료된 큐에 아무것도 없다면 생길 때까지 블록 됩니다.%%% 함수가 성공하면 새롭게 생성된 소켓 번호를 리턴 합니다. 이 소켓을 통해 클라이언트와 통신을 합니다.

 

 

send()/recv()

int send( int socket, const void * msg,
          unsigned int msgLength, int flag );

int recv( int socket, void * recvBuffer,
          unsigned int bufferLength, int flag );


 

이 함수들은 이름 그대로 데이터를 주고 받는 함수입니다. send 함수의 socket 인자는 보낼 곳의 소켓번호이고, msg는 보낼 메시지의 시작 포인터입니다. msgLength 는 보낼 메시지의 길이입니다. recv 함수의 socket 인자는 send 함수와 동일합니다. recvBuffer은 받을 버퍼의 시작 포인터이고, bufferLength는 해당 버퍼의 크기입니다. Send 함수의 리턴 값은 보낸 데이터의 byte수입니다. 그리고 recv 함수의 리턴 값은 recvBuffer에 넣은 데이터의 수를 리턴 합니다. 만약 상대방이 접속을 끊으면 recv 함수의 리턴 값은 0이 됩니다.

 

 TCP에서 데이터를 주고 받을 때 잊지 말아야 할 중요한 것이 있습니다. 예를 들어 설명을 하면, 데이터 100바이트를 보내겠다고 가정합시다.

 

send( socket, buffer, 100, 0 );

 

이런 식으로 보내게 됩니다.

그런데send함수는 100바이트 전부를 보낼 때까지 블록 됩니다.(blocking mode 일 때)

그리고 send로 보낸 데이터를 받을 때는 recv함수를 사용합니다.

받을 버퍼로 char buffer[512]를 선언했다고 하지요.

recv( socket, buffer, 512, 0 );

 

그런데이 recv함수는 우리가 원하는 데이터의 양만큼 데이터를 받지 못합니다.

내부적으로 TCP는 보내는 버퍼와 받는 버퍼 두 개가 있다고 했습니다.

send 함수는 보내는 버퍼에 보낼 데이터를 옮기고 리턴 됩니다.

recv 함수는 받는 버퍼에 1바이트라도 있으면 그것을 가져옵니다.

우리가 512 바이트를 선언하고 512를 인자로 넘겨 512바이트 이상은 가져 오지 않습니다.

 

blocking mode일 때 recv 함수는 받는 버퍼가 비어 있으면 데이터가 들어올 때까지 기다렸다가 들어오면 그것을 받아서 리턴 합니다.

즉, 우리가 받기를 원하는 만큼 받을 수 없다는 것입니다. recv 해서 받을 데이터의 양은 아무도 알 수 없습니다. 그래서 항상 리턴 값을 체크해서 얼마나 받았는지 확인을 해야 하는 것입니다.

 

그리고 TCP는 stream 방식입니다. 클라이언트에서 100바이트와 50바이트 250바이트를 이렇게 3번을 보냈다고 합시다. 그런데 이 데이터들을 구분할 수가 없다는 것입니다. 그것을 구분하는 것은 응용프로그램의 몫입니다.

데이터는 받는 버퍼에 구분 없이 연결되어 들어가 있는 것입니다. 이점을 항상 염두하고 프로그래밍을 해야 합니다.

즉, 서버에서 recv 를 했을 때 받은 크기는 100 이 아닐 수 있습니다. 100보다 클 수 있고 작을 수도 있습니다.

 

 

TCP 클라이언트 함수에 대해서 알아보도록 하겠습니다.

먼저 TCP 클라이언트 작성 순서를 알아보면,

  1. socket()함수로 소켓을 생성
  2. connect() 서버와의 연결
  3. send() recv() 사용하여 통신
  4. close()로 연결 닫음

이런 식으로 작성 합니다. connect()란 함수 말고는 나머지는 서버와 비슷합니다. 클라이언트는 bind() 함수를 사용하지 않는데 그것은 포트번호를 꼭 일정하게 묶을 필요가 없기 때문입니다. 커널이 알아서 적당하고 사용하지 않는 포트번호를 할당해 줍니다.

int connect( int socket, struct sockaddr * foreignAddress, unsigned int addressLength );

첫 번째 socket는 생성한 소켓의 번호입니다. 그리고 두 번째는 쉽게 말해 서버의 주소를 넣어서 보내는 것입니다. 그렇게 하면 그 쪽으로 연결을 요청해서 there-why handshake 를 하는 것입니다. addressLength 는 sockaddr 의 크기입니다.

소켓옵션

소켓옵션은 일반적으로 디폴트로 사용해도 문안하게 사용할 수가 있습니다. 그러나 보다 세밀한 설정을 하길 바란다면 이러한 옵션들을 설정하여 그 어플리케이션에 맞게 사용할 수 있습니다. 소켓 옵션에는 많은 것들이 있으니 Unix Network Programming vol 1 의 7장에 소켓 옵션에 대해서 잘 설명해 놓았습니다. 그것을 참고하시길 바랍니다. 여기서는 자주 쓰이는 옵션에 대해서 알아 보도록 하겠습니다. 그 전에 소켓을 옵션을 설정하고 설정된 것을 얻어 오는 함수를 알아보도록 하겠습니다.

 

int getsockopt( int socket, int level, int optname,
                void * optVal, unsigned int * optLen );
int setsockopt( int socket, int level, int optname,
                const void * optVal, unsigned int * optLen );


 

위의 두 개의 함수입니다. 하나는 얻어 오는 함수고 하나는 설정하는 함수입니다. 첫 번째 인자는 소켓 옵션을 얻어오거나 설정할 소켓의 번호입니다. 두 번째 인자는 소켓의 레벨인데, 어떤 것을 설정 혹은 얻을 것이냐 하는 겁니다. 일반적인 소켓의 옵션인가 IP에 관한 내용인가? TCP에 관한 내용인가? 아니면 IPv6의 내용인가? 하는 것을 나타냅니다. 여기에는 다음과 같은 것들이 있습니다.

  • SOL_SOCKET : 일반적인 소켓의 옵션들이 있습니다.
  • IPPROTO_IP : IP설정과 관계있는 곳: 주로 멀티캐스트와 관련된 것들이 있습니다.
  • IPPROTO_TCP : TCP와 관련있는 옵션들이 있습니다.

세 번째 인자는 그 레벨에서 어떤 것을 말하느냐입니다. 일반적인 소켓의 옵션에서도 그 중에 무엇을 변경 혹은 얻어 올 것인가 하는 것입니다. 버퍼의 크기를 변경할 건지 아니면 브로드캐스드인지 등 그런 세부적인 옵션을 말합니다.%%% 네 번째 인자는 setsockopt 함수에서는 설정될 값이 무엇이냐 하는 것이고, getsocketopt 함수에서는 얻은 값을 저장할 변수의 포인터입니다.%%% 다섯 번째는 네 번째 변수의 길이입니다. 변수라 말했지만 이것은 구조체로 된 것도 있습니다.%%% 네 번째 인자를 보면 void * 형 입니다. 이것은 무엇을 말할까요. 즉, 변수의 형이 정해지지 않았다는 것입니다. 각 옵션에 따라 int도 있고, unsigned char도 있고, 구조체도 있습니다.

이제 자주 사용하는 옵션들에 대해서 알아보도록 하겠습니다.

1) SOL_SOCKET Level

  • SO_RCVBUF, SO_SNDBUF

버퍼의 크기를 바꾸는 옵션입니다.커널의 recv Buffer, send Buffer의 크기를 조절하는 데 사용합니다. 이것을 어떻게 잘 설정하느냐에 따라 성능이 향상된다고 합니다. 버퍼의 크기는 테스트와 네트워크의 상태에 따라서 달라진다고 합니다. 그런데 보통은(대역폭 * 지연율) * 2의 공식에 따라 버퍼의 크기를 설정한다고 합니다. recv Buffer 의 크기를 변경하는 코드를 보도록 하겠습니다.

 

int rcvBufferSize;
int sockOptSize;
.
.
.


// 소켓 리시브 버퍼 크기 얻기
sockOptSize = sizeof( rcvBufferSize );
if( getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &rcvBufferSize, &sockOptSize ) < 0 )
{
        printf( "getsockopt() failed
" );
        exit( 1 );
}

 


// 리시브 버퍼의 크기를 2배로 만든다.
rcvBufferSize *= 2;
if( setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &rcvBufferSize, sizeof( rcvBufferSize ) < 0 ) )
{
        printf( "setsockopt() failed
" );
        exit( 1 );
}

 

 

여기에서 보면 SOL_SOCKET, SO_RCVBUF 가 나옵니다. 즉, 일반적인 소켓의 옵션 중에 recv Buffer 의 크기를 말합니다. recv Buffer 와 마찬가지로 send Buffer 변경 옵션은 SOL_SOCKET level에 있습니다. 그리고 SO_RCVBUF 대신에 SO_SNDBUF라는 것을 넣어 주면 변경이 가능합니다.

 

 

그런데 하나 주의 하실 점이 있습니다. 바로 호출 순서입니다. 소켓의 옵션들의 대부분이 호출 순서가 중요합니다. 클라이언트의 경우 connect() 하기 전에 recv Buffer 를 변경해야 하는데, 왜냐하면 three-way handshake 할 때 MSS를 알려 주기 때문입니다. 그리고 서버의 경우에 listensocket (listen() 함수 호출 시 전달되는 소켓번호) 의 설정은 listen() 호출 전에 먼저 설정을 해주어야 합니다. 쉽게 설명해서 연결이 성립될 때 그러니까 three-way handshake할 때 한번에 보낼 수 있는 TCP 세그먼트(패킷)의 크기의 최대값을 알려 줍니다. 연결이 성립되면 최대 세그먼트의 크기(MSS)를 변경할 수 없어 연결하기 전에 미리 바꾸어 두는 것입니다.

  • SO_LINGER

SO_LINGER 옵션이 있습니다. 이것은 TCP 에서 적용되는 것인데 close함수의 행동을 지정하는 옵션입니다. close() 하면 recv Buffer 나 send Buffer 에 보내거나 받을 데이터가 있다면 전부 처리 후 close() 를 합니다. 그 방법을 바꾸는 것입니다. 먼저 전달되는 구조체에 대해서 알아 보도록 합시다.

 

struct linger
{
    int l_onoff;
    int l_linger;
};

 

setsockopt( sock, SOL_SOCKET, SO_LINGER, &linger 구조체 주소, sizeof( linger ) );

이런 식으로 호출하면 되겠죠. 그리고 세부적인 동작 설정은 linger구조체의 변수 설정에 있습니다.

  1. l_onoff가 0이면 기본적인 TCP동작이 적용됩니다.
  2. l_onoff가 0이 아니고(주로 1을 넣습니다.) l_linger가 0이면 연결이 닫힐 때 버퍼의 내용을 버리고 연결을 끊어 버립니다.
  3. l_onoff가 0이 아니고 l_linger도 0이 아니면 소켓이 닫힐 때 블럭 당한다고 합니다.

이 소켓옵션을 쓸 땐 2번을 주로 씁니다. 쓰는 이유는 만약 서버가 종료되고 다시 시작 할 때 입니다. 연결이 끊어지고 남은 데이터를 전송합니다. 그때 남은 데이터를 보낸다면 클라이언트에게 ack 메시지(받았다는 확인 메시지)를 받아야 완전한 종료가 이루어집니다. 그 메시지를 기다리는 시간이 있습니다. 만약 그것을 다 받지 못했다면 다시 보내야 하지요. 그런 상황에서 다시 서버를 시작하려고 하면 이미 사용 중인 포트라는 에러를 내게 됩니다. 그래서 이런 옵션을 사용하는 것입니다. 그런데 그것은 바람직한 해결 방법이 아니라고 합니다. 그래서 이런 옵션은 추천되고 있지 않습니다. 이에 대한 해결책은 따로 있습니다. 그것이 다음에 설명할 포트 재사용 옵션입니다.

  • SO_REUSEADDR

이 옵션을 선택하여 주면 위의 예에서 말한 서버 재 시작 시 다시 시작할 수 있습니다. 간단히 사용법을 알아보도록 하지요.

int nResue = 1; setsockopt( ListenSocket, SOL_SOCKET, SO_REUSEADDR, &nReuse, sizeof( nReuse ) );

이것도 호출 순서가 있는데 bind() 하기 전에 이 옵션을 설정해 놓아야 합니다. 이렇게 하면 소켓의 포트를 재 사용할 수 있습니다.

2) IPPROTO_IP Level

여기에는 주로 멀티캐스트와 관련된 옵션들이 있습니다. 다음에 멀티캐스트를 하실 때 그때 사용법을 참고하기면 됩니다.

3) IPPROTO_TCP Level

  • TCP_NODELAY

TCP에 보면 잔잔한 패킷들을 하나씩 다 보내는 것이 아니라 네트워크상에 작은 패킷들을 줄이기 위해 Nagle 알고리즘을 사용하여 어느 정도 묶어서 한꺼번에 보내는 것이 있습니다. 이것을 사용 할지 안 할지를 설정하는 옵션입니다. 이것은 주로 서버에서는 이 알고리즘을 사용 안 한다고 합니다. 왜냐하면 다른 일을 해야 하기 때문에 그냥 바로 보내버리는 것이 더욱 효과적이라는 것입니다. 패킷의 개수가 많아지기는 하지만 그런 알고리즘의 딜레이를 버림으로 보다 빠른 처리를 할 수 있다는 것입니다. 그러나 클라이언트의 경우는 Nagle알고리즘을 사용합니다. 작은 패킷을 묶어 보내 네트워크의 부하를 줄이자는 것입니다.

소켓 옵션은 다양하고 많은 것들이 있고 주의 사항들이 있습니다. 항상 자신의 프로그램에 맞게 올바른 설정을 하시고 사용하시기 전에 여러 가지로 알아보시고 하시기를 바랍니다. 그리고 옵션에서 인자 값을 넘길 때 인자의 형이 다릅니다. 위의 예제에서도 Linger옵션은 구조체를 사용하고 다른 것은 int 형이었습니다. 그리고 윈속(Winsock)도 다릅니다. 구조체로 된 부분은 거의 같지만 int로 된 부분은 BOOL로 사용하는 부분이 많이 있습니다. 잘 알아 보시고 사용하시기 바랍니다.

에코 프로그램

그럼 이제 소스를 보면서 이제까지의 내용들을 정리하도록 하겠습니다. 여기의 모든 소스는 유닉스나 리눅스 용입니다. 윈도우에서는 실행이 되지 않습니다.

구현할 것은 에코 서버와 클라이언트입니다. 에코 서버는 에코 클라이언트가 보낸 데이터를 받아서 그대로 다시 에코 클라이언트에게 보내는 것입니다. 그럼 에코 서버부터 보도록 하겠습니다.

에코 서버

서버의 실행은 <실행파일명 포트번호> 입니다.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> // socket() bind() connect()
#include <arpa/inet.h>  // socketaddr_in,  inet_ntoa()
#include <netinet/in.h> // 만약 FreeBSD 라면 이 해더를 추가해야 한다.
#include <string.h>
#include <unistd.h>
#include <stdlib.h>#define RCVBUFSIZE 128
#define MAXPENDING 5int main( int argc, char * argv[] )
{
        struct sockaddr_in echoServAddr, echoClntAddr;
        int servSock, clntSock;
        unsigned short echoServPort;
        unsigned int clntLen;
        char echoBuffer[BUF_LEN];
        int recvMsgSize;        if( argc != 2 )
        {
                printf( "Usage : %s port
", argv[0] );
                exit( 1 );
        }        echoServPort = atoi( argv[1] );        if( ( servSock = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ) ) < 0 )
        {
                printf( "socket() failed
" );
                exit( 1 );
        }        memset( &echoServAddr, 0, sizeof( echoServAddr ) );
        echoServAddr.sin_family = AF_INET;
        echoServAddr.sin_addr.s_addr = htonl( INADDR_ANY );
        echoServAddr.sin_port = htons( echoServPort );        if(bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0)
        {
                printf("bind() failed
");
                exit(1);
        }        if(listen(servSock, MAXPENDING) < 0)
        {
                printf("listen() failed
");
                exit(1);
        }        for(;;)
        {
                clntLen = sizeof(echoClntAddr);                if((clntSock = accept(servSock, (struct sockaddr *)
                    &echoClntAddr, &clntLen)) < 0)
                {
                        printf("accept() failed");
                        exit(1);
                }                if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0))
                   < 0)
                {
                        printf("recv() failed");
                        exit(1);
                }
                while(recvMsgSize > 0)
                {
                        if(send(clntSocket, echoBuffer, recvMsgSize, 0)
                            != recvMsgSize)
                        {
                                printf("send() failed");
                                exit(1);
                        }
                        if((recvMsgSize = recv(clntSocket, echoBuffer,
                            RECVBUFSIZE, 0)) < 0)
                        {
                                printf("recv() failed");
                                exit(1);
                        }
                }
                close(clntSocket);
        }
}

자, 이게 에코서버의 모습입니다. 우선 소스부터 분석하도록 합시다. 우선 서버의 소켓을 열고 서버의 주소 정보를 채우고 bind 시키고 클라이언트의 요구를 듣는 상태로 들어 갔습니다. 그리고 클라이언트의 연결요청이 들어오면 accept함수를 호출하여 클라이언트와 연결을 하고 그리고 에코 서비스를 해주는 과정으로 되어있습니다. 지금 제가 설명한 과정이 분석이 되실 겁니다. 그래도 좀더 자세히 설명에 들어가도록 합시다. 이 서버는 TCP 서버입니다. 그것을 알 수 있는 부분은 어디입니까? 소켓을 처음 생성하는 부분입니다.

 

servSock = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP );

 

여기서 SOCK_STREAM 으로 되어있습니다. 이것은 연결형 서버를 말하는 겁니다. 즉, TCP의 서비스를 이용하겠다는 것입니다. 여기서 마지막 인자를 IPPROTO_TCP 라고 되어있는데 꼭 이렇게 써주는 것은 아닙니다. 보통은 0 값을 전달합니다. 어느 것을 사용하나 상관은 없습니다. 명시적인 것이 좋으신 분은 IPPROTO_TCP라고 써주시면 되겠습니다. 여기서 PF_INET 를 사용했는데 AF_INET 와 무엇이 다를까요? PF 는 Protocol Family 의 약자이고 AF는 Address Family의 약자입니다. 글자는 달라고 내부적으로는 구분하지 않는다고 합니다. 그래서 여러 곳에서는 각각 다릅니다. PF_INET를 사용하는 것이 있고 AF_INET를 사용하는 곳도 있습니다. 여기에 접두어가 자신이 알기 쉽다고 생각하시는 것을 사용하시면 될 듯합니다. 중요한 것은 IPv4라는 겁니다. IPv6은 AF_INET6이라는 것을 사용합니다. 만약에 IP프로토콜 독립적으로 구성하고자 하시려면 이것을 고려 해주셔야 합니다. 다음으로 서버의 주소를 지정하고 bind 시켰습니다.

memset( &echoServAddr, 0, sizeof( echoServAddr ) );
echoServAddr.sin_family = AF_INET;
echoServAddr.sin_addr.s_addr = htonl( INADDR_ANY );
echoServAddr.sin_port = htons( echoServPort );if( bind( servSock, ( struct sockaddr * ) &echoServAddr, sizeof( echoServAddr ) ) < 0 )
{
        printf( "bind() failed
" );
        exit( 1 );
}

이 부분입니다. 여기서 3번째 줄에 INADDR_ANY 라는 단어가 들어 갑니다. 서버의 IP 주소를 넣어 주는 부분입니다. 직접 IP 주소를 넣어 주어도 상관은 없습니다. 그런데 이렇게 하면 만약에 서버의 IP가 바뀌거나, 다른 곳에서 서버를 가동시켜야 한다면, 이 부분도 바꾸어야 합니다. 그러면 위 소스에서 쓰는 것처럼 쓰는 것이 더욱 좋을 것입니다. 그런데 중요한 것은 그것이 아닙니다. 서버는 (꼭 서버만 아니고) IP가 여러 개인 서버도 있습니다. 그럴 경우 예를 들면, 여기서 bind 한 포트를 5000번이라고 합시다. 그리고 서버의 IP가 203.241.228.57 과 203.241.228.66 두 개의 IP를 가지고 있다고 하면, 클라이언트가 서버의 아무 IP를 가지고 포트 5000으로 들어오면 우리의 서버 어플리케이션에서 받겠다는 것입니다. 즉, IP : 203.241.228.57, Port 5000 ... IP : 203.241.140.66, Port 5000 으로 접속하는 모든 클라이언트의 요청을 받겠다는 것입니다. 내부적으로는 INADDR_ANY는 0의 값이 들어가 있다고 합니다. 위 서버 코드에서 서버의 IP를 출력해 보세요. 그럼 0.0.0.0 이 출력 될 겁니다.

간단히 실험하나 해보도록 하겠습니다. 도스 명령프롬프트 창을 열어서

 >nslookup daum.net

라고 쳐봅니다. 물론 네트워크가 되는 컴퓨터에서요. daum의 IP주소가 나오는데 여러 개가 나옵니다. 위 명령은 네임서버에 daum의 정보를 얻어 오는 것입니다.

서버에서 에코 서비스를 처리하는 부분을 보도록 하겠습니다.

for(;;)
{
        clntLen = sizeof(echoClntAddr);

        if((clntSock = accept(servSock,
             (struct sockaddr *) &echoClntAddr, &clntLen)) < 0)
        {
                printf("accept() failed");
                exit(1);
        }

        if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0)
        {
                printf("recv() failed");
                exit(1);
        }

        while(recvMsgSize > 0)
        {
                if(send(clntSocket, echoBuffer, recvMsgSize, 0)
                      != recvMsgSize)
                {
                        printf("send() failed");
                        exit(1);
                }
                if((recvMsgSize = recv(clntSocket, echoBuffer,
                      RECVBUFSIZE, 0)) < 0)
                {
                        printf("recv() failed");
                        exit(1);
                }
        }
        close(clntSocket);
}

 

이 부분입니다. 클라이언트가 보낸 문자를 되돌려 보내는 부분입니다. 일단 무한 루프로 서버는 끝나지 않습니다. 여기에서 보면, 처음에 recv를 받고 send를 하고 다시 recv를 받습니다. 클라이언트는 처음 에코 요구를 하면 문자열을 한번만 보내는데 여기서는 한번 받고 루프를 돌면서 보내고 받고 그럽니다. 왜냐면 네트워크의 상태에 따라서 TCP는 얼마나 받을지 모른다는 것입니다. 그래서 루프를 돌면서 못 받은 데이터가 있으면 받아서 보내 줍니다.

여기서 에코 서버와 에코 클라이언트의 시나리오를 생각해 보도록 합시다. 서버가 일단 가동되고 있고, 그리고 클라이언트가 Hello라는 문자를 보내 서버에게 에코 요구를 보냅니다. 그런데 네트워크 상황이 안 좋아서 서버는 Hell 까지만 받고 말았습니다. 이것은 첫 번째의 recv 에서 받은 데이터입니다. 그러고 while로 들어가 send에서 서버는 Hell을 클라이언트에게 보냅니다. 그리고 다시 recv 로 들어갑니다. 그리고 o 라는 문자가 옵니다. 그리고 서버는 recv 로 받아서 다시 클라이언트에게 o를 보내어 줍니다. 그리고 다시 recv로 들어 갑니다. 그러면 여기서 서버는 recv 에서 블럭 됩니다. 네트워크에 어떤 데이터가 들어올 때까지 블록 되는 것입니다. 그럼 언제 recv가 리턴 되는가 하면 TCP는 연결을 끝내고 close를 할 때 recv에 0을 리턴 합니다. 설정될 때 three-way handshake하는 것처럼 연결을 끊을 때도 이와 비슷한 행동을 합니다. 그러니까 명시적으로 끊겠다고 하는 것이죠. 서버가 먼저 끊을 수 있고 클라이언트가 먼저 끊을 수도 있습니다. 어느 한쪽에서 close를 하면 “끊겠다”는 패킷을 다른 쪽에 보내면 다른 한쪽에서는 “알았다는 그리고 끊겠다”는 그런 종류의 단계를 취하는 것입니다. 그러면 여기서는 클라이언트가 먼저 끊었다고 합시다. 그럼 서버의 블록 된 recv는 0을 리턴 합니다. 그리고 while 루프를 끝냅니다. 그리고 마지막에 서버가 close를 해서 클라이언트와 연결을 닫습니다.

이제 서버의 구조에 대해 알아 봅시다. 위 서버는 클라이언트당 하나의 소켓이 열립니다. 처음 서버에서 소켓을 열어 listen() 함수에 전달하는 소켓을 보통 듣는 소켓(listen socket)이라고 합니다. 연결 요청은 이 소켓으로 들어 옵니다. 연결요청이 들어 오면 accept()로 다시 소켓 하나를 열어서 새로이 열린 소켓으로 클라이언트와 통신을 하는 것입니다. 그럼 이제 이 서버의 문제점을 알아 봅시다. recv에서 데이터를 받으면 블록 된다고 했습니다. 그런 클라이언트가 접속을 끝내지 않는다면, 다른 클라이언트의 서비스는 어려움이 많이 있습니다. 그리고 서버가 여러 가지 복잡한 일을 하는 것이라면, recv가 블록 되 프로세스가 놀고 있게 되어 효율적이지 못합니다. 그래서 이 문제를 해결하기 위해 넌 블로킹 모드, 비동기 모드 등이 나옵니다. 기본적인 서버의 구조와 네트워크는 데이터를 주고 받을 때 무엇이든 확신할 수 없다는 것. 지금 받은 것이 다 받은 것인지 보낸 것이 전부 보내어졌는지를 어떻게 확인하고 처리하는지 이해하시기를 바랍니다.

그럼 이번에는 클라이언트를 보기로 하지요. 클라이언트는 서버보다 간단하니 한번 보시면서 분석해 보시기 바랍니다. 보낸 데이터를 에코 해서 받을 때 어떻게 받았는지 어떻게 다 받았는지 클라이언트는 자신이 서버에 보낸 문자의 길이를 알고 있기 때문에 이것을 활용했습니다. 한번 보시면 이해하시리라 생각합니다.

에코 클라이언트

그럼 이제 클라이언트 소스입니다.

클라이언트의 실행은 <실행파일명 서버IP 에코문자 포트> 입니다.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> // socket() bind() connect()
#include <arpa/inet.h>  // socketaddr_in,  inet_ntoa()
#include <netinet/in.h> // 만약 FreeBSD 라면 이 해더를 추가해야한다.
#include <string.h>
#include <unistd.h>
#include <stdlib.h>#define RCVBUFSIZE 128int main(int argc, char * argv[])
{
        int sock;
        struct sockaddr_in echoServAddr;
        unsigned short echoServPort;
        char * servIP;
        char * echoString;
        char echoBuffer[RCVBUFSIZE];
        unsigned int echoStringLen;
        int bytesRcvd, totalBytesRcvd;        if((argc < 3) || (argc  > 4))
        {
                printf("Usage: %s <Server IP> <Echo Word> [<Echo Port>]
", argv[0]);
                exit(1);
        }        servIP = argv[1];
        echoString = argv[2];        if(argc == 4)
                echoServPort = atoi(argv[3]);
        else
                echoServPort = 7; // 에코 서버의 well-know port입니다..        if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
                printf("socket() failed");
                exit(1);
        }        memset(&echoServAddr, 0, sizeof(echoServAddr));
        echoServAddr.sin_family = AF_INET;
        echoServAddr.sin_addr.s_addr = inet_addr(servIP);
        echoServAddr.sin_port = htons(echoServPort);        if(connect(sock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0)
        {
                printf("connect() failed");
                exit(1);
        }        echoStringLen = strlen(echoString);        if(send(sock, echoString, echoStringLen, 0) != echoStringLen)
        {
                printf("send() failed");
                exit(1);
        }        totalBytesRcvd = 0;
        printf("Received: ");        while(totalBytesRcvd < echoStringLen)
        {
                // 문자끝 NULL을 넣기 위해 RCVBUFSIZE-1
                if((bytesRcvd = recv(sock, echoBuffer, RCVBUFSIZE-1,
        0)) <= 0)
                {
                        // 리턴값이 0이면 서버와 연결이 끊어짐..
                        printf("recv() 실패 혹은 서버와 연결이 끊어졌다.");
                        exit(1);
                }                totalBytesRcvd += bytesRcvd;
                echoBuffer[bytesRcvd] = '';
                printf(echoBuffer);
        }
        printf("
");        close(sock);
        exit(0);
}

 

 

POSIX Thread

Thread vs Process

각 클라이언트를 다루기 위해 새로운 프로세스를 하나 만드는 것은 비용이 많이 듭니다. 그 내용을 살펴보면,

  • 프로세스가 하나 생성될 때마다 운영체제는 메모리, 스택, 파일/소켓 식별자들 및 기타를 포함한 부모 프로세스의 전체 상태를 복사
  • Thread들은 같은 프로세스 내의 멀티태스킹을 허용함으로써 이러한 비용을 감소. 새로 생성된 Thread는 부모와 같은 주소공간(코드 및 데이터)을 공유하고, 부모의 상태를 복제할 필요성 배제
  • 프로세스 복제이후 부모와 자식간에 정보를 주고받기 위해 프로세스간 통신(IPC) 필요 (자식으로부터 부모로 정보를 되돌리는 것은 더욱 많은 작업을 요구)
  • 프로세스 중의 모든 Thread가 공유하는 것
    • 프로세스 지시 사항
    • 대부분의 데이터
    • 공개된 파일들(Ex 지정 번호들)
    • 신호 처리기와 신호 배치들
    • 사용자와 그룹 ID
  • 각 Thread 자신만이 갖는 것
    • Thread ID
    • 프로그램 계수기와 스택 지시자를 포함한 레지스터의 조합
    • (지역변수와 반환 주소를 위한) stack
    • errno
    • 신호 선별
    • 우선순위

이와 같이 비교 될 수 있습니다.

Basic Thread Functions

거의 모든 pthread 함수는 성공하면 0을 리턴하고 실패하면 0이 아닌 값을 리턴 합니다. 그러나 errno 변수는 설정하지 않는 것이 특징입니다. 밑의 pthread 함수들은 모두 여기에 따른다고 생각하시면 됩니다.

pthread를 사용하기 위해서는 밑의 해더를 추가해야 합니다.

 

#include <pthread.h>

intpthread_create( pthread_t *threadID,        // Thread ID, (unsigned int)
                    const pthread_attr_t *attr, // Thread 속성, NULL Default
                    void *( * func )( void * ), // 입구함수
                    void *arg);                // 여러 인자를 전달할 때, structure 이용

 

 

스레드를 생성합니다. 첫 번째 인자는 스레드가 생성되면 그 스레드의 ID가 저장되는 변수이고, 두 번째 인자는 여러 가지 속성(우선순위나 스텍 사이즈 등을 말합니다.)을 나타냅니다. default로 하려면 NULL을 전달 하면 됩니다. 세 번째 인자는 스레드의 입구 함수(스레드가 시작되는 함수, 스레드가 할 역할을 기술해놓은 함수)입니다. 스레드가 실행되면 그 함수를 실행합니다. 입구함수의 형태는 반드시

 

void * ThreadMain( void * arg );

 

위와 같은 형태여야 합니다. 그리고 마지막은 스레드 입구함수의 인자로 전달될 변수입니다.

void pthread_exit ( void * status ); // 리턴할 값

Thread 중단합니다. 만일 Thread가 분리되지 않으면 Thread ID와 리턴 값은 종결 프로세스의 다른 Thread에 의하여 나중까지 pthread_join에 남겨집니다. Thread가 종결 될 때에는 객체가 사라지므로 status 는 호출 Thread에 지역적인 변수를 지정하면 안됩니다.

 

intpthread_join( pthread_t tid, void ** status );

 

tid가 가리키는 Thread가 종료할 때까지 위의 함수를 호출한 Thread의 수행을 멈춥니다. 만약 status가 NULL이 아니면 tid의 리턴 값은 status가 가리키는 영역에 저장됩니다.

 

pthread_tpthread_self( void );

 

Thread 자신의 Thread ID 리턴 합니다. 이 값은 pthread_create() 로 얻은 스레드 ID와 동일합니다.


intpthread_detach( pthread_t tid );

 

Thread 상태가 부모의 개입 없이도 종료 시 즉시 해제합니다. 주로 pthread_detach ( pthread_self() ); 로 사용합니다.

보다 더 자세한 사항은Joinc의 Pthread API Reference를 참고하세요.

 

TCP Echo Server를 위한 클라이언트당 Thread 멀티태스킹 Source

/****** TCPEchoServer_Thread.c *******/#include "TCPEchoServer.h"
#include <pthread.h> // for POSIX threadsvoid * ThreadMain(void * arg); // Main program of a thread// Structure of arguments to pass to client thread
struct ThreadArgs
{
        int clntSock;
};int main(int argc, char * argv[])
{
        int servSock;
        int clntSock;
        unsigned short echoServPort;
        pthread_t threadID;
        struct ThreadArgs * threadArgs;        if(argc != 2)
        {
                fprintf(stderr, "Usage: %s <Server Port>
", argv[0]);
                exit(1);
        }        echoServPort = atoi(argv[1]);        servSock = CreateTCPServerSocket(echoServPort);        for(;;)
        {
                clntSock = AcceptTCPConnection(servSock);                // Create memory for client argument
                if((threadArgs =
                   (struct ThreadArgs *) malloc(sizeof(struct ThreadArgs)))
                    == NULL)
                        DieWithError("malloc() failed");                threadArgs->clntSock = clntSock;
                // Create thread
                if(pthread_create(&threadID, NULL, ThreadMain,
                    (void *) threadArgs) != 0)
                        DieWithError("pthread_create() failed");
                printf("with thread %ld
", (long int) threadID);
        }
}void * ThreadMain(void * threadArgs)
{
        int clntSock;
        // Guarantees that thread resource are deallocated upon return
        pthread_detach(pthread_self());        clntSock = ((struct ThreadArgs *) threadArgs)->clntSock;
        free(threadArgs); // Deallocate memory for argument        HandleTCPClient(clntSock);        return (NULL);
}

TCP 관련 소스는 앞의 select() 예제 소스와 같으니 위의 소스를 보시면 되겠습니다. 소스코드의 컴파일은

 $ gcc -o TCPEchoServer-Thread TCPEchoServer_Thread.c -Wall -lpthread (FreeBSD 라면 -pthread)

로 하시면 됩니다.

소켓 부분은 에코 서버와 거의 같으니 스레드 부분만 보도록 하겠습니다.

pthread_create(&threadID, NULL, ThreadMain, (void *) threadArgs);

이 부분이 스레드를 생성하는 함수입니다. 입구함수 인자로 여기에서는 소켓번호 하나만 전달합니다. 그런데 전달되는 구조체를

struct ThreadArgs
{
    int clntSock;
};

이렇게 선언해 놓았습니다. 물론 소켓번호만 전달해도 상관없습니다. 그런데 만약 여러 가지 정보를 전달하려고 하면 인자가 하나뿐이니 하나만 전달할 수 있습니다. 아니면 전역 등의 방법을 사용해야 합니다. 그런데 스레드 입구함수의 인자가 void * 형이기 때문에 이런 구조체를 만들어서 구조체의 포인터를 전달하면 여러 인자를 전달 할 수 있게 됩니다.

그리고 소스를 보면 전달될 인자를 동적 메모리 할당을 하였는데 반드시 이렇게 해야 합니다. 만약 지역변수로 전달하게 되면 어떻게 될까요? 스레드는 서로 경쟁하며 실행됩니다. 그러니까 정확히 어느 것이 먼저 실행될지는 아무도 모르는 것입니다. 그래서 지역변수로 선언해 놓았다면 스레드가 실행되어 인자가 참조되기 전에 스레드 함수를 호출한 곳이 먼저 종료되었다면 인자의 변수는 잘못된 메모리를 가리키고 있는 것이 되어 버립니다. 그래서 잘못된 결과를 이르게 하는 것이죠. 그리고 스레드 입구함수에서 인자의 메모리를 해제했습니다. 이것도 위와 비슷합니다. 만약에 스레드 함수를 호출한 곳에서 메모리를 해제한다면 지역변수와 똑같은 결과를 낳게 됩니다.

위 소스는 부족한 소스입니다. 만약 서버가 종료된다면 생성했던 스레드가 확실히 종료되었는지 그런 것을 알 수 없기 때문이죠. 물론 소멸되겠지만요. 안전하게 스레드가 종료되었는지 알고 나서 서버를 닫는 것이 더욱 좋을 것입니다.

위의 스레드의 소스는 하나의 처리를 스레드에게 맡김으로써 여러 사용자를 받을 수 있게 하였습니다. 프로세스를 생성하는 것도 이와 비슷합니다. Pthread_create() 대신 fork()함수를 이용해서 프로세스를 만들면 됩니다. 앞에서 select()와 스레드에 대해서 알아 봤습니다. 그럼 이 두 개를 결합하여 만드는 것은 어떨까요? 그런 것은 채팅 서버를 한번 만들어 보시면서 하면 좋은 예가 될 것 같습니다.

마치며

초보적인 내용인 만큼 이 글이 네트워크를 공부를 시작하시는 분에게 조금이나마 도움이 되었으면 합니다. 그리고 함수 설명보다는 원리에 중점을 두어 설명을 하려고 했는데 잘 안된 것 같습니다. 아직 UDP, 멀티캐스트 같은 주제가 남아 있습니다. 꼭 보시고 가시길 바랍니다. 보통 멀티캐스트는 지원하는 라우터가 많이 없어서 하기 힘들다고 합니다. 소프트웨어로 처리하는 방법이 있기는 하지만 아직 부족한 점이 많이 있나 봅니다. 그러나 UPD는 반드시 공부해보셔야 할 듯합니다. TCP와 UDP의 각자의 장/단점이 서로 절충 될 수 있는 소지가 많이 있기 때문입니다. 많은 운영체제가 운영체제마다 다른 poller 를 제공하고 있는데 보통 성능상의 문제로 이런 것들을 이용해서 서버를 작성해나간다고 합니다. 각각마다 장단점이 있어 제가 무어라고 할 수 없지만, 그런 것들은 하나의 방법인 것 같습니다. 여러 가지 방법을 생각해보고 자신의 어플리케이션에 가장 효율적이고 가장 알맞은 그런 방법을 찾으면 되는 것 같습니다. 조금이라도 좋은 성능을 얻기 위해 서버를 어떻게 구성해야 하는지 패킷은 어떻게 구성하는지 등 여러 가지 방법들을 연구해보고 테스트 해보세요.

관련링크


 

 

 

Network의 기본

네트워크의 기본적인 사항에 대해 먼저 알아 보도록 하겠습니다.%%% 거의 인터넷 표준으로 자리잡은 TCP/IP에 대해서만 알아보도록 하겠습니다. 그러나 TCP/IP 주제만 가지고도 몇 개의 강좌를 해야 되므로, 자세한 내용은 다른 서적이나 강좌를 참고하세요.%%% 제가 추천하는 책은

  • TCP/IP Illustrated, Volume 1 (W. Richard Stevens 저)
  • TCP/IP Protocol Suite (Behrouz A, Forouzan 저)

를 추천합니다. 둘 다 원서입니다. 영어가 부담스러우신 분은 각 서적에 대한 번역서도 있으니 번역의 질 등을 잘 알아보고 선택하시기 바랍니다.%%% 다른 책을 참고하셔도 크게 상관은 없으나 간략한 소개만 되어 있는 것이 아닌 자세한 내용이 있는 것을 보세요. 왜냐하면 정확한 TCP/IP의 동작을 알아야지 네트워크에 오류가 있을 때 보다 쉽게 오류를 수정할 수 있기 때문입니다.

클라이언트와 서버구조

대부분 네트워크 프로그램은 서버와 클라이언트로 구분할 수 있습니다. 두 개를 구분하는 기준은 그 역할이 무엇이냐 입니다. 그 기준을 간단히 설명하면

  • 서버 : 클라이언트가 어떤 작업의 요청이 들어오면 요청을 처리하고 그 결과를 클라이언트에게 알려 주는 역할
  • 클라이언트 : 서버에게 작업을 요청하고 그 작업의 결과를 받아서 보여주는 역할

이렇게 설명할 수 있습니다.%%% 클라이언트/서버 구조는(이하 C/S) 우리 일상에도 많은 예가 있습니다. 가장 흔한 예가 웹 브라우저와 웹 서버입니다. 인터넷 익스플로러나 네스케이프가 클라이언트 역할을 하고 접속한 그리고 우리에게 서비스를 제공하는 컴퓨터가 서버라고 생각하시면 됩니다. 정확히는 아파치나 IIS 같은 http 서버를 말합니다.

여기서 클라이언트와 서버의 차이는 클라이언트는 서버 하나와 통신을 하지만, 서버는 여러 개의 클라이언트를 상대합니다. 그래야만 더 많은 사용자들에게 서비스를 제공할 수 있습니다.

그러나 모든 것이 C/S 구조로 된 것은 아닙니다. 클라이언트와 서버 역할을 모두 하는 것도 있습니다. P2P(Peer To Peer)가 그것입니다. 대표적인 소프트웨어로 소리바다나 구루구루 같은 것들이 여기에 속합니다. P2P는 클라이언트와 서버의 역할을 동시에 하고 있습니다.(여기에 대한 내용은http://extremendl.net에서 논의되고 있으니 참고하세요.)

TCP/IP

TCP/IP 를 알아보기 전에 간단히 프로토콜(Protocol)의 정의에 대해서 알아보도록 하겠습니다.

"둘 이상의 통신 개체 사이에 교환되는 메시지의 형태, 의미, 전송 순서, 그리고 메시지 송수신 및 기타 사전에 수행할 동작을 정의한 규약"

두 개 이상의 호스트(컴퓨터) 사이에서 데이터를 약속에 의한 방법으로 주고 받자는 것입니다. TCP/IP 도 이런 프로토콜의 한 종류입니다. 보통 TCP/IP 라 부르지만 정확히는 TCP/IP 프로토콜 그룹(패밀리)라고 부릅니다. 그룹이므로 TCP와 IP 프로토콜만 있는 것은 아닙니다. TCP, UDP, ARP, RARP, ICMP 등 여러 가지가 같이 있는 프로토콜입니다. 여기에서 대표적인 프로토콜?TCP와 IP 이기 때문에 TCP/IP 라고 부릅니다.

네트워크 프로토콜들은 대부분 계층(Layer)이라는 개념을 가지고 있습니다. 여기서 OSI 7 Layer 를 낯?드리고 싶지만, 이번 글은 소켓?련?글이므로 다른 서적이나 강좌를 참고하세요. 이것은 꼭 알아야 할 기본적인 네트워크 개념이니 꼭 익히셔야 합니다.%%% 계층의 개념을 간단히 말씀 드리면, 어떤 계층의 통신 상대의 같은 계층과 의미 있는 통신을 합니다. 그리고 각 계층들은 그 밑 계층이 제공하는 서비스를 이용하여 그 상위 계층에 서비스를 제공합니다. 만약 한 계층의 인터페이스가 변한다면, 그 바로 상위 계층에만 영향을 줍니다. 그리고 어느 한 계층에서 생성된 메시지들은 상대방의 같은 계층에서 분석되고 작동합니다. 무슨 말인지 조금 어렵게 느껴질 수 있습니다. 저도 계층 개념을 이해하는데 어려움을 느꼈습니다. 일단 OSI 7 Layer 를 보시면서 네트워크의 개념을 잡으시기 바랍니다. 간단히 그림을 보도록 하겠습니다.

사용자 삽입 이미지

%%%

<그림> 네트워크 계층과 흐름

그럼 TCP/IP 의 계층은 어떻게 되어 있는지 알아보도록 하겠습니다.

사용자 삽입 이미지

%%%

<그림> TCP/IP의 계층

TCP/IP는 위 그림과 같이 4계층으로 이루어져 있습니다. 혹은 링크 계층을 물리 계층과 데이터링크 계층으로 나누기도 하는데 크게 다르지 않습니다. OSI 7 Layer 를 보시면 큰 무리 없이 이해됩니다. 그럼 각 계층을 하나씩 살펴보도록 하겠습니다.

1) 링크 계층

물리적인 인터페이스와 관련된 하드웨어적인 부분을 제어합니다. 운영체제와 디바이스 드라이버나 그와 관련된 랜카드, 그와 연결된 케이블 같은 것을 제어하는 계층을 말합니다.

2) 네트워크 계층

네트워크상의 패킷의 이동을 제어하는 계층입니다. 패킷이라는 말이 처음 나왔는데, 패킷은 네트워크를 통해 데이터를 전달할 때 헤더와 데이터의 묶음을 말합니다. 정확히 이 계층에서는 IP 데이터그램이라고 합니다.(IP를 이용하여 신뢰성 없이 전달됩니다.) 이 패킷에는 송/수신지의 정보가 포함되어 있습니다. 이 계층의 역할은 한마디로 우편물의 주소와 같습니다. 어느 곳에 편지를 보낸다고 할 때, 여러 우체국을 거쳐 목적지의 우편함까지 옵니다. 우편물을 패킷이라면 우체국들은 라우터나, 게이트웨이라고 할 수 있습니다. 패킷이 가려고 하는 호스트(컴퓨터)까지의 이동을 담당하는 곳이 네트워크 계층입니다. 주로 IP가 이 역할을 하는데 IP는 신뢰성이 보장되지 않습니다. 확실히 갔는지 아닌지 알 수 없습니다. 또 다른 기능들이 많이 숨어 있는 계층이지만 여기까지 하도록 하겠습니다.

3) 트랜스포트 계층

상위 응용 층에 대해 두 호스트간의 데이터 흐름을 제공합니다. TCP/IP 에는 TCP 와 UDP라는 트랜스포트 프로토콜이 있습니다.

TCP는 위의 상위계층이 준 데이터를 목적지로 전달과 흐름제어의 기능을 제공합니다. 흐름의 제어란 데이터를 언제 보내야 하는지 얼마큼의 크기로 보내야 하는지 어떤 것을 보내야 하는지를 제어한다고 간단히 생각해도 될 것 같습니다. 위의 네트워크 계층에서 우편물과 같아 잘 보내어 졌는지 잘 받았는지 확인할 길이 없다고 했습니다. 그러나 TCP는 이러한 것까지 알아서 해줍니다. 즉 전화라고 보시면 됩니다. 우리가 타인에게 전화를 걸면, 신호음이 가고 상대편이 받을 때까지 기다립니다. 만약 상대방이 받지 않는다면, 우리는 다시 전화를 걸 수 있습니다. TCP도 마찬가지 입니다. 보내고 잘 받지 못했다면 다시 보내는 것이죠. TCP 는 네트워크 계층의 상위계층입니다. 이전 Layer 를 설명할 때, 하위계층의 인터페이스를 이용한다고 했습니다. 즉. TCP도 IP 데이터그램을 이용하여 정보가 전달 되는 것입니다. 그래서 패킷만 두고 본다면, TCP도 신뢰성이 없습니다. 그러나. IP의 상위 계층인 TCP는 이러한 점을 보안하여 서비스를 해줍니다. 즉, “시간이 얼마나 지났는데 와야 할 패킷이 안 온다 무언가 문제가 있다.” 이런 식으로 보안합니다. TCP는 연결지향 서비스이고 두 호스트간의 신뢰성 높은 데이터 흐름을 제공합니다. 연결지향이라는 말은 TCP는 데이터를 주고 받기 전에 클라이언트와 서버가 이제 연결해서 데이터를 주고 받겠다는 약속을 하는 것입니다. 그리고 데이터를 보내면 그 데이터가 반드시 목적지에 도착하고, (일정 시간 내에 받지 못하면 패킷이 손실 되었다고 보고, 다시 보내달라고 요청을 합니다.) 보낸 순서 또한 똑같다는 것입니다. TCP로 보내는 패킷을 TCP 세그먼트(segment)라고 보통 부릅니다.

UDP는 비연결형 서비스입니다. 즉, 클라이언트와 서버가 연결 약속은 하지 않고 바로 데이터를 주고 받는다는 것을 말합니다. 그리고 신뢰성이 없습니다. 즉, 데이터가 목적지에 반드시 도착하리라는 보장이 없습니다. 물론 보낸 순서도 마찬가지입니다. UDP로 보내는 패킷을 UDP 데이터그램(datagram)이라고 부릅니다.%%% 주로 TCP를 사용하기는 합니다만 UDP도 쓰이는 곳도 많습니다. UDP가 속도가 비교적 빠르기 때문에 패킷 하나 없어져도 크게 관계없는 실시간 방송이 라던지 그런 곳에 쓰입니다. 요즘 게임에도 TCP 와 UDP를 같이 섞어서 많이 사용한다고 합니다.

예를 들어 TCP와 UDP를 조금 더 알아 봅시다. 만약 서버에서 3개의 패킷을 보낸다고 가정을 한다면 여러 가지 라우터나 게이트웨이들은 패킷의 IP를 보고 이 패킷의 경로를 정해 목적지까지 보내 줍니다. 3개의 패킷은 가는 도중의 네트워크의 상태에 따라서 다른 경로를 통해 전달 될 수도 있습니다. 1번 패킷이 도중에 손실되고 3번이 2번 보다 먼저 목적지에 도착할 수도 있습니다. 만약 TCP 연결이라면 1번이 도착하지 않았으므로 다시 1번을 보내달라고 하고 서버는 다시 1번부터 3번까지 보내 줍니다. 여기서 1번만 보낼 수도 있지만 알고리즘이 복잡하고 네트워크가 충분히 빠르므로 1, 2, 3을 모두 보냅니다. 그런데 UDP는 3, 2번 패킷을 그대로 받습니다. 1번이 있는 지도 모릅니다. 그냥 받은 대로 쓰는 것입니다. 그런 특성들은 프로그래머들이 짜는 응용 계층에서 별도로 처리를 해주어야 합니다.

4) 응용 계층

간단히 우리가 쓰는 네트워크 응용 프로그램을 말합니다.

IP

IP는 인터넷상의 고유의 주소입니다. 전세계에서 유일합니다. 4바이트(32비트)의 숫자로 구성된 주소입니다. 랜카드와 1:1로 대응 됩니다. 예를 들면 104.245.123.24과 같은 식으로 되어있습니다. 이런 표시 방식을 dotted-decimal 방식이라고 합니다. 사람이 알기 쉽게 이런 식으로 쓰는 것입니다. 실제로는 11010100110... 이런 식으로 되어야 컴퓨터가 알아 볼 수 있습니다. IP는 클래스 A, 클래스 B, 클래스 C, 클래스 D, 클래스 E 가 있습니다.

클래스 | 범위 A 클래스 | 0.0.0.0 - 127.255.255.255 B 클래스 | 128.0.0.0 - 191.255.255.255 C 클래스 | 192.0.0.0 - 233.255.255.255 D 클래스 | 224.0.0.0 - 239.255.255.255 E 클래스 | 240.0.0.0 - 255.255.255.255

각 클래스들은 이런 범위를 가지고 있습니다. 클래스 E는 나중을 위해 예약되어 있는 클래스 입니다. 클래스 D는 멀티캐스트를 위한 IP입니다. 한마디로 우편물이 가기 위한 자신의 집의 주소라고 보시면 됩니다.

도메인 주소

컴퓨터는 IP를 인식하지만 사람이 외우기는 조금 불편합니다. 그래서 도메인 주소라는 것을 사용합니다. bgda.org 이런 식으로 쓰면 사람이 보다 알기겠지요. 컴퓨터는 도메인이 입력되어 들어오면, 그 도메인에 대항하는 IP로 변환해서 사용합니다. 이런 서비스를 DNS (Domain Name Service) 라고 합니다.

Port

포트(Port)는 하나의 컴퓨터에 실행 중인 여러 네트워크 프로그램을 구분하기 위해 부여된 번호입니다. 16비트로 구성된 번호입니다. 즉, 우편물이 집에 도착했는데 그 우편물이 누구의 것이냐는 것입니다. 여기서 집을 하나의 컴퓨터(호스트)라 하고, 주소를 컴퓨터의 IP, 우편물에 적힌 이름은 포트 번호라고 이해하시면 쉬울 것입니다. 즉, 컴퓨터까지는 왔는데 그 컴퓨터의 어느 프로그램이 패킷을 받을지를 알아야 하니 이런 번호가 부여됩니다.%%% 우리가 자주 쓰는 웹 서버나 ftp 서버 같은 것들도 전부 포트번호가 있습니다. 그런데 이런 것들은 자주 많이 쓰이기 때문에 포트번호를 지정해 놓았습니다. 그래서 우리들은 인터넷 주소만 입력하면 바로 웹 페이지를 열 수 있는 것입니다. 포트 번호를 따로 적지는 않습니다. 왜냐하면, 미리 이 포트번호는 http의 번호이다라고 정해놓았기 때문입니다. 그렇게 많이 쓰는 서버들의 포트들을 well-known 포트라고 합니다. 1 - 1024까지는 well-known 포트로 되어있습니다. 그래서 보통 새로운 서버를 만든다면 이 포트(well-know port)는 되도록 피하는 것이 좋습니다. 포트 번호가 16비트니 포트번호는 충분할 것입니다.

루프백(loopback)

클라이언트와 서버가 같은 호스트에서 TCP/IP를 이용하여 서로 통신할 수 있도록 하는 것입니다. 127.0.0.1 - 127.255.255.255 까지가 루프백 주소로써 localhost라는 이름으로도 할당하고 있습니다. 루프백으로 보내어진 데이터는 밖으로는 보내어지지 않습니다. 그러나 브로드캐스트나 멀티캐스트주소로 보내어진 것은 루프백에 복사된 다음 밖으로 나가게 됩니다. 그리고 자신의 IP로 보내어진 것도 루프백으로 보내어집니다.

MTU

MTU(Maximum Transmission Unit) 최대 전송단위라는 것인데 대부분의 네트워크는 패킷의 상한선이 정해져 있습니다. 그것보다 많은 양은 그보다 작게 쪼개어서 보냅니다. 이런 것을 단편화(Fragmentation)라고 합니다. 단편화된 패킷은 받을 때 합쳐지게 됩니다.

Path MTU

두 호스트의 네트워크는 다를 수 있습니다. 즉, 누구는 LAN이고 누구는 전화선일 수도 있는 겁니다. 그때 두 네트워크의 MTU는 다릅니다. 그리고 두 호스트 사이에 어떠한 네트워크도 있을 수 있습니다. 이 두 호스트 사이에 패킷을 전송하는 링크상의 최소 MTU 크기가 Path MTU라 합니다. 만약에 A 와 D가 통신을 한다고 하면 A와 D사이에는 B, C 라는 네트워크가 있다고 해봅시다.

A - B - C - D

A의 MTU가 100 B 200 C 70 D 80이라는 MTU를 가지고 있다면 A와 D의 Path MTU는 70이 되는 것입니다.

TTL

Time-to-live 라는 것으로 패킷이 통과할 수 있는 라우터의 수를 제한하기 위해 사용됩니다. 하나의 라우터를 거칠 때마다 TTL 값이 1씩 줄어들어 0이 되면 패킷은 버려지게(삭제) 됩니다. 라우터를 많이 안거치는 로컬에서는 작아도 상관없지만, 외국이나 그런 먼 곳(거쳐야 할 라우터가 많은) 곳에 보내려면 TTL값은 충분히 커야 합니다. 그렇지 않으면, 가는 도중에 TTL값이 0이 되어 패킷이 삭제될 수 있습니다. 라우터는 두 개의 같은 네트워크를 연결하는 중간 다리 역할을 하는 것이라고 보시면 됩니다. 게이트웨이란 것도 있는데, 이것은 서로 다은 네트워크를 연결하는 역할을 한다고 보시면 됩니다.

TCP 연결 (Three-way Handshake)

위에서 TCP는 연결지향 서비스라고 했습니다. TCP 연결 설정은 다음의 시나리오로 이루어 집니다.

  1. 서버는 들어오는 연결을 받을 준비가 되어 있도록 준비되어야 합니다.
  2. 클라이언트가 접속을 요청합니다. (클라이언트가 서버에게 SYN 세그먼트를 보냄)
  3. 서버는 클라이언트의 SYN 도착을 클라이언트에게 알립니다. (서버가 클라이언트에게 SYN을 보냄)
  4. 클라이언트는 서버에게 SYN도착을 알림(클라이언트가 서버에게 ack를 보냄)

이때 교환하는 패킷이 3개인데 그래서 Three-way Handshake 라 합니다. 쉽게 말해서 클라이언트가 서버에게 “접속한다.” 그러면 서버는 “그래 접속해라.” 합니다. 그리고 클라이언트는 “알았다.” 라고 하고 접속이 완료 되는 겁니다.

TCP 연결 종료

다음과 같은 시나리오 입니다.

  1. 클라이언트에서 close를 호출하면 데이터를 그만 보내겠다는 FIN 세그먼트를 서버에 보냅니다.
  2. 서버가 FIN을 받으면 FIN을 받았다는 ack를 클라이언트에게 보내고 close가 호출됩니다.
  3. 그러면 서버도 FIN을 클라이언트에게 보내게 됩니다.
  4. 그러면 마지막으로 클라이언트는 FIN을 받고 ack를 서버에게 보냅니다.

여기서는 클라이언트가 close를 먼저 했는데. 서버가 먼저 할 수도 있습니다.

소켓 API

이 글에 있는 소스는 TCP/IP 소켓프로그래밍 version C <사이텍미디어>를 참고 하였음을 알려드립니다.

소켓 API란?

네트워크 상에서 호스트간에 통신을 가능하게 해주는 일반적인 인터페이스 입니다. 응용층과 트랜스포트 계층 사이의 중간에 위치해있습니다.

소켓 주소 구조

소켓 API는 소켓과 관련된 주소를 지정하기 위해 일반적인 소켓 구조체를 정의해 놓았습니다.(소켓 주소에는 TCP/IP를 위한 주소만 있는 것이 아니라 다양하게 많이 존재합니다.) 그 형태는

struct sockaddr { unsigned short sa_family; /* Address family */ char sa_data[14]; /* Family-specific address */ };

이렇게 생겼습니다. 실제 TCP/IP 소켓 주소를 지정할 때는 이 구조체를 사용하지 않습니다. TCP/IP에 맞추어 사용합니다. TCP/IP 소켓 주소를 위해 사용하는 구조체를 보면,

struct in_addr { unsigned long s_addr; }; struct sockaddr_in { unsigned short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };

sockaddr_in의 sin_family 인자는 인터넷 주소 패밀리입니다. 이번 강좌에서는 AF_INET를 사용합니다. (일단 이렇게만 알아두세요.) 이건 IPv4 프로토콜이라는 것을 말합니다. sin_port 는 포트 번호를 지정하는 것입니다. short니 2바이트, 16비트 정수입니다. sin_addr은 IP주소가 들어 가는 부분입니다. 위에 struct in_addr의 정의가 나와있습니다. 정의에서 나오는 것과 같이 여기에는 IP의 32비트 주소가 들어 갑니다. 그러니 "128,23,23,14" 이런 문자열이 들어가진 않습니다. 이걸 110010101.. 이런 식으로 변환하여 넣어 주어야 합니다. 두 개를 변환하는 함수 물론 있습니다. 그리고 그 다음이 sin_zero[8]이 부분인데 여기에는 0값이 들어 갑니다. 즉, 사용하지 않습니다. 사용하지 않는데 왜 여기에 있는가 하면, 앞에 일반적인 구조체를 보여 드렸습니다. 그것의 크기에 맞추어서 8바이트 더미 값이 들어가 있습니다.

바이트 순서(Byte Ordering)

컴퓨터 메모리에 데이터가 저장되는 순서를 말합니다. 이것은 수행되는 기계에(CPU) 의존됩니다. 여기에는 Little-Endian 과 Big-Endian이 있습니다. Little-Endian 은 가장 낮은 바이트부터 저장되고 Big-Endian은 가장 높은 바이트부터 저장됩니다. 그림을 보면 쉽게 이해가 가실 겁니다. 4바이트 정수를 저장한다고 하면,

| 1byte | +-----------+-----------+-----------+-----------+ | 1 | 2 | 3 | 4 | +-----------+-----------+-----------+-----------+ | 1byte | +-----------+-----------+-----------+-----------+ | 4 | 3 | 2 | 1 | +-----------+-----------+-----------+-----------+ addr A addr A+1 addr A+2 addr A+3

위에 것이 Little-Endian 밑에 그림이 Big-Endian 입니다. Little-Endian 방식에 대표적인 것이 Intel계열의 CPU이고 Big-Endian은 Sparc 계열의 CPU입니다. 같은 기종의 통신이면 바이트 순서는 중요하지 않지만, 서로 다른 기종의 통신이라면 중요해 집니다. 통신을 할 때는 Big-Endian을 따릅니다. 그래서 네트워크로 보내기 전에 이 바이트 순서를 조정해 주어서 보내어야만 합니다.%%% 그럼 네트워크 순서와 호스트 순서를 바꾸는 함수에 대해서 알아 보도록 하겠습니다. 함수 이름에는 규칙이 있는데,

  • h - host
  • n - network
  • l - long
  • s - short

이렇습니다. 예를 하나 들면,

long int htonl (long int hostLong);

무슨 말일까요? 함수 이름이 htonl 입니다. 호스트에서 네트워크로 바꾸라는 얘기입니다. type은 long 형이고요. 이렇게 각각 long와 short에 대해 함수가 있습니다.

long int htonl (long int hostLong); long int ntohl (long int netLong); short int htons (short int hostShort); short int ntohs (short int netShort);

사용의 예를 들어 보죠. 포트번호가 2바이트입니다. 이것을 네트워크로 보내기 위해서는 바이트 순서를 조정해야 합니다. 소켓 주소 구조체에 대입은

SocketAddress.sin_port = htons(appPort);

이렇게 합니다.

소켓기술자란?

소켓 기술자는 유닉스의 파일 기술자와 동일합니다. 다만 그 I/O가 네트워크일 뿐입니다. 쉽게 말해 컴퓨터에서 네트워크로 나가는 문의 고유번호라고 생각하시면 됩니다. 네트워크로 데이터를 보내거나 받으려면 커널에게 소켓을 만들어 달라고 요청을 합니다. 그럼 커널은 소켓 하나를 열고 우리에게 그 소켓의 고유번호를 줍니다. 그것이 소켓 기술자입니다. 유닉스의 파일기술자에 대해 잘 아신다면, 똑같다고 보면 이해가 빠르실 겁니다. 이제부터 소켓번호라고 하면 이 소켓 기술자를 얘기하는 것으로 하겠습니다.

TCP 소켓

이제 TCP 서버와 클라이언트의 기본적인 함수 호출 구조를 살펴 보도록 하겠습니다.

서버 : socket() -> bind() -> listen() -> accept() -> send()/recv()

서버가 여기까지 호출을 하게 되면 클라이언트의 접속이 들어 오는지 기다립니다.

클라이언트 : socket() -> connect() -> send()/recv()

여기서 클라이언트와 서버는 three-way handshake를 하여 연결을 합니다. 그 후에 서버와 클라이언트는 send()와 recv() 를 이용해 데이터를 주고 받습니다. 그리고 close() 함수를 호출하여 접속을 끊게 되는 것입니다.

그럼 서버와 클라이언트에서 공통으로 사용하는 소켓 생성과 소멸에 관련된 함수에 대해서 알아 보도록 하겠습니다. 그 전에 여기서 설명하는 함수는 다른 말이 없으면 리턴 값이 에러면 -1을 리턴하고 errno이라는 전역변수에 에러 값을 넣어줍니다. 이 errno의 값을 보고 무슨 에러가 났는지 알 수 있습니다.

int socket( int protocolFamily, int type, int protocol );

socket 함수의 역할은 커널에 소켓을 열어 달라고 요청을 하여 그 소켓번호를 우리에게 넘겨 줍니다. protocolFamily는 AF_INET를 씁니다. IPv4 protocol 을 사용한다는 것을 알리는 것입니다. 물론 다른 것도 있지만 이 글에는 이것만 씁니다. type은 TCP를 사용할 땐 SOCK_STREAM, UDP를 사용할 땐 SOCK_DGRAM 을 넣어서 어떤 서비스를 사용하는지 커널에 알려 줍니다. protocol은 raw소켓을 쓸 때 말고는 0을 설정합니다. 그러니 여기서는 항상 0을 사용할 것입니다.(IPPROTO_TCP, IPPROTO_UDP를 이용하셔도 됩니다.) raw소켓은 IP계층의 서비스를 직접 이용할 때 쓰는 것입니다.

int close( int sockfd );

소켓을 닫고 통신을 종료합니다. sockfd는 닫을 소켓 번호입니다. 성공하면 0을 실패하면 -1을 반환합니다. 닫힌 소켓은 더 이상 사용할 수 없습니다. 여기선 TCP를 설명하니 TCP에 대해 조금 더 설명하겠습니다. 내부적으로 TCP는 send buffer 와 recv buffer가 있습니다. 만약에 close를 호출 하였는데 send buffer에 보낼 데이터가 남아 있으면 그것을 모두 보낸 후에 앞서 설명 드린 TCP 연결 종료 절차를 따릅니다.

위 두 함수에는 좀더 볼 것이 있는데. 그것은 참조 카운터입니다. socket로 소켓을 열면 참조 카운터가 1 증가 합니다. 그리고 다른 자식 프로세스로 복사될 때도 참조 카운터가 1증가 합니다. close는 참조 카운터를 1감소 시킵니다. 그러다가 참조카운터가 0이 되면 소켓을 닫습니다. 소켓 참조 카운터가 0이 아니라면 그것은 열린 상태가 되는 것입니다.

int shutdown( int sockfd, int howto );

이 함수도 네트워크 연결을 종료시키는 데 사용합니다. close()와 다른 점은 close는 참조 카운터를 1감소시키고 참조 카운터가 0이 되면 종료하는데 shutdown()은 참조 카운터와 상관없이 TCP의 연결 종료 절차를 시작합니다. 그런데 close()함수는 양방향(send recv) 둘 다 종료시키는데 반해 shutdown함수는 howto인자에 따라 동작이 달라집니다. 위에서 close함수를 설명할 때 약점이 하나 있었습니다. close()호출 후에 받을 데이터가 있다면 어떻게 할까요? 그건 받을 수 없습니다. 그러나 shutdown의 howto 인자를 설정하면 그것이 가능합니다. 그 값에는 다음과 같은 것이 있습니다.

  • SHUT_RD : 연결의 recv 한쪽만 닫습니다. 이제 이 소켓으로는 데이터를 받을 수 없습니다. 그리고 recv buffer도 폐기됩니다.
  • SHUT_WR : 연결의 send 한쪽만 닫습니다. 이제 이 소켓으로는 어떤 데이터도 보낼 수 없게 됩니다.send buffer에 남아 있는 데이터는 모두 보낸 뒤에 TCP 연결 종료 절차가 뒤따릅니다.
  • SHUT_RDWR : 연결의 양쪽 다 받습니다.

만약에 자신은 데이터를 다 보냈다 하면, SHUT_WR인자를 설정하여 shutdown()을 호출하면 다른 쪽이 보내는 데이터를 받을 수 있게 되는 것입니다.

이제는 TCP 서버의 기본적인 함수에 대해서 알아 보도록 하겠습니다.

TCP 서버는 통신의 종단에서 클라이언트의 연결요구를 수동적으로 기다리는 역할을 합니다. 그럼 그 과정을 요약하면,

  1. socket() 함수로 소켓을 생성
  2. bind() 함수로 생성된 소켓에 포트번호를 연결
  3. listen() 함수를 이용해 클라이언트의 연결요구를 받도록 함
  4. 클라이언트의 연결요청이 들어오면 accept() 함수로 새로운 소켓을 얻음
  5. send() recv()를 사용하여 클라이언트와 통신
  6. 서비스가 끝나면 close()함수를 이용하여 클라이언트의 연결을 닫음

이제 여기에 관련된 함수에 대해 알아 보겠습니다.

bind ()

int bind( int sockfd, struct sockaddr * localAddress, unsigned int addressLen );

bind() 함수의 원형입니다. bind함수는 localAddress 변수에 있는 IP 주소와 Port 번호를 연결시켜 주는 역할을 합니다. 만약, 클라이언트(ip 123.145.234.1 포트 6000)가 ip 203.229.234.13 port 5000 번으로 접속을 요청해 왔다고 합시다. 그럼 서버는 그 ip와 포트 변호를 보고 어느 프로그램의 패킷인지를 알 수 있어야 합니다. 바로 그것을 알려 주는 그리고 명시 하는 함수가 바로 bind입니다. 그런데 서버는 여러 개의 클라이언트를 처리 한다고 했습니다. 클라이언트는 똑같은 IP와 포트 번호를 이용해서 (사실 IP는 다를 수 있습니다.) 접속을 해올 것입니다. 그 예를 설명해 보죠.%%% A 라는 서버에 x y라는 두 개의 클라이언트가 접속을 해왔다고 합시다.

서버의 IP 는 111.222.333.44 Port 5000%%% x클라이언트 IP 222.222.222.22 Port 6000%%% y클라이언트 IP 222.222.222.33 Port 7000%%% 이라고 가정하면,

x가 먼저 클라이언트에게 접속을 하고 y가 접속을 합니다. 그러면 TCP는 연결을 위해 소켓이 새로이 생성되는데 그곳에 socket pair이라는 구조체에 서버의 IP와 Port 클라이언트의 IP와 Port를 같이 저장합니다. 그러면 두 개의 x, y클라이언트가 구분이 되겠죠?%%% 이렇게 클라이언트와 서버 IP, Port를 모두 사용하여 구분한다고 생각하시면 됩니다. 그리고 아까 서버의 IP가 다를 수 있다고 했는데 그것은 하나의 컴퓨터에 하나의 IP. 즉, 하나의 네트워크 인터페이스(랜카드) 만이 있는 것은 아닙니다. IP는 인터페이스당 유일하게 하나입니다. 그럼 만약에 서버가 2개의 인터페이스를 가지고 있다면 서버 프로그래밍의 설정에 따라서 동시에 두 IP에서 오는 패킷을 받을 수 있습니다.%%% 나중에 서버 프로그래밍 예제를 보시면 나옵니다. INADDR_ANY 를 서버설정에서 IP부분에 설정을 하면 모든 인터페이스로부터 패킷을 받는다는 의미입니다. 물론 특정 인터페이스에서 오는 것만 받을 수도 있습니다. 그때 서버를 bind할 때 그 받을 IP만 지정하면 되는 것입니다.

listen()

int listen ( int socket, int backlog );

첫 번째 인자는 소켓 번호입니다. 두 번째 인자가 설명이 조금 필요한데, 간단히 설명하면 연결요구 개수의 최대값을 나타냅니다. 무슨 말이신지 이해가 잘 안되실 수도 있는데 조금 더 설명을 드리면, TCP가 접속을 할 때 three-way handshake를 한다고 했습니다. 이 도중에 또 다른 클라이언트가 접속을 요구 할 수도 있습니다. 그럼 어떻게 해야 할까요? 일단 어디에다 저장해두어서 지금 하고 있는 연결설정을 끝내고 차례대로 들어온 순서대로 연결 설정을 해주면 되겠지요? 그 저장해둘 클라이언트 연결 요구의 수를 말하는 것입니다. 내부적으로는 연결이 완료된 것 대기중인 것 이렇게 두 개의 큐가 있습니다. 이 큐는 FIFO(First In First Out) 로 동작합니다. 연결완료 된 것과 대기중인 것 모두 합친 것의 수입니다.%%% 만약에 그 대기수도 다 차있는 상태에서 다시 연결 요구가 들어 오면 어떻게 할까요? TCP는 거기에 대해서 아무 것도 안 합니다. 그럼 클라이언트는 아무 응답이 없으므로 일정 시간 뒤에 다시 연결 요구를 합니다. 예전에는 이 수는 5 를 사용했던 것 같습니다. 많은 예제들이 아직도 5를 사용하고 있는데, 5 라고 해서 꼭 5개만 되는 것은 아닙니다. 운영체제나 네트워크 인터페이스 드라이브에 따라서 다를 수 있습니다. 어떤 것은 입력된 값 그대로 쓰고 어떤 건 여기에다가 1.5를 곱한 수를 사용하는 등 다양합니다. 그리고 이제 5 라는 제한도 없어 졌습니다. 더 큰 수도 지원합니다. 만약에 운영체제에서 지원하는 수보다 더 큰 수를 넣는다면 어떻게 될까요? 그렇게 해도 운영체제가 알아서 최대값을 안 넘게 해준다고 합니다.

accept()

int accept( int socket, struct socket * clientAddress, u nsigned int * addressLen );

이 함수의 기능은 listen 함수가 연결 요구의 개수를 지정하고 내부 큐에는 연결 설정이(three-way handshake) 완료된 큐와 대기중인 큐 두 개가 있다고 했습니다. 그 완료된 큐에서 순서대로(FIFO) 하나 가져와서 상대방과 연결된 하나의 소켓을 만드는 역할을 합니다. 만약에 완료된 큐에 아무것도 없다면 생길 때까지 블록 됩니다.%%% 함수가 성공하면 새롭게 생성된 소켓 번호를 리턴 합니다. 이 소켓을 통해 클라이언트와 통신을 합니다.

send()/recv()

int send( int socket, const void * msg, unsigned int msgLength, int flag ); int recv( int socket, void * recvBuffer, unsigned int bufferLength, int flag );

이 함수들은 이름 그대로 데이터를 주고 받는 함수입니다. send 함수의 socket 인자는 보낼 곳의 소켓번호이고, msg는 보낼 메시지의 시작 포인터입니다. msgLength 는 보낼 메시지의 길이입니다. recv 함수의 socket 인자는 send 함수와 동일합니다. recvBuffer은 받을 버퍼의 시작 포인터이고, bufferLength는 해당 버퍼의 크기입니다. Send 함수의 리턴 값은 보낸 데이터의 byte수입니다. 그리고 recv 함수의 리턴 값은 recvBuffer에 넣은 데이터의 수를 리턴 합니다. 만약 상대방이 접속을 끊으면 recv 함수의 리턴 값은 0이 됩니다.%%% TCP에서 데이터를 주고 받을 때 잊지 말아야 할 중요한 것이 있습니다. 예를 들어 설명을 하면, 데이터 100바이트를 보내겠다고 가정합시다.

send( socket, buffer, 100, 0 );

이런 식으로 보내게 됩니다. 그런데 send함수는 100바이트 전부를 보낼 때까지 블록 됩니다.(blocking mode 일 때) 그리고 send로 보낸 데이터를 받을 때는 recv함수를 사용합니다. 받을 버퍼로 char buffer[512]를 선언했다고 하지요.

recv( socket, buffer, 512, 0 );

그런데 이 recv함수는 우리가 원하는 데이터의 양만큼 데이터를 받지 못합니다. 내부적으로 TCP는 보내는 버퍼와 받는 버퍼 두 개가 있다고 했습니다. send 함수는 보내는 버퍼에 보낼 데이터를 옮기고 리턴 됩니다. recv 함수는 받는 버퍼에 1바이트라도 있으면 그것을 가져옵니다. 우리가 512 바이트를 선언하고 512를 인자로 넘겨 512바이트 이상은 가져 오지 않습니다. blocking mode일 때 recv 함수는 받는 버퍼가 비어 있으면 데이터가 들어올 때까지 기다렸다가 들어오면 그것을 받아서 리턴 합니다. 즉, 우리가 받기를 원하는 만큼 받을 수 없다는 것입니다. recv 해서 받을 데이터의 양은 아무도 알 수 없습니다. 그래서 항상 리턴 값을 체크해서 얼마나 받았는지 확인을 해야 하는 것입니다.%%% 그리고 TCP는 stream 방식입니다. 클라이언트에서 100바이트와 50바이트 250바이트를 이렇게 3번을 보냈다고 합시다. 그런데 이 데이터들을 구분할 수가 없다는 것입니다. 그것을 구분하는 것은 응용프로그램의 몫입니다. 데이터는 받는 버퍼에 구분 없이 연결되어 들어가 있는 것입니다. 이점을 항상 염두하고 프로그래밍을 해야 합니다. 즉, 서버에서 recv 를 했을 때 받은 크기는 100 이 아닐 수 있습니다. 100보다 클 수 있고 작을 수도 있습니다.

TCP 클라이언트 함수에 대해서 알아보도록 하겠습니다.

먼저 TCP 클라이언트 작성 순서를 알아보면,

  1. socket()함수로 소켓을 생성
  2. connect() 서버와의 연결
  3. send() recv() 사용하여 통신
  4. close()로 연결 닫음

이런 식으로 작성 합니다. connect()란 함수 말고는 나머지는 서버와 비슷합니다. 클라이언트는 bind() 함수를 사용하지 않는데 그것은 포트번호를 꼭 일정하게 묶을 필요가 없기 때문입니다. 커널이 알아서 적당하고 사용하지 않는 포트번호를 할당해 줍니다.

int connect( int socket, struct sockaddr * foreignAddress, unsigned int addressLength );

첫 번째 socket는 생성한 소켓의 번호입니다. 그리고 두 번째는 쉽게 말해 서버의 주소를 넣어서 보내는 것입니다. 그렇게 하면 그 쪽으로 연결을 요청해서 there-why handshake 를 하는 것입니다. addressLength 는 sockaddr 의 크기입니다.

소켓옵션

소켓옵션은 일반적으로 디폴트로 사용해도 문안하게 사용할 수가 있습니다. 그러나 보다 세밀한 설정을 하길 바란다면 이러한 옵션들을 설정하여 그 어플리케이션에 맞게 사용할 수 있습니다. 소켓 옵션에는 많은 것들이 있으니 Unix Network Programming vol 1 의 7장에 소켓 옵션에 대해서 잘 설명해 놓았습니다. 그것을 참고하시길 바랍니다. 여기서는 자주 쓰이는 옵션에 대해서 알아 보도록 하겠습니다. 그 전에 소켓을 옵션을 설정하고 설정된 것을 얻어 오는 함수를 알아보도록 하겠습니다.

int getsockopt( int socket, int level, int optname, void * optVal, unsigned int * optLen ); int setsockopt( int socket, int level, int optname, const void * optVal, unsigned int * optLen );

위의 두 개의 함수입니다. 하나는 얻어 오는 함수고 하나는 설정하는 함수입니다. 첫 번째 인자는 소켓 옵션을 얻어오거나 설정할 소켓의 번호입니다. 두 번째 인자는 소켓의 레벨인데, 어떤 것을 설정 혹은 얻을 것이냐 하는 겁니다. 일반적인 소켓의 옵션인가 IP에 관한 내용인가? TCP에 관한 내용인가? 아니면 IPv6의 내용인가? 하는 것을 나타냅니다. 여기에는 다음과 같은 것들이 있습니다.

  • SOL_SOCKET : 일반적인 소켓의 옵션들이 있습니다.
  • IPPROTO_IP : IP설정과 관계있는 곳: 주로 멀티캐스트와 관련된 것들이 있습니다.
  • IPPROTO_TCP : TCP와 관련있는 옵션들이 있습니다.

세 번째 인자는 그 레벨에서 어떤 것을 말하느냐입니다. 일반적인 소켓의 옵션에서도 그 중에 무엇을 변경 혹은 얻어 올 것인가 하는 것입니다. 버퍼의 크기를 변경할 건지 아니면 브로드캐스드인지 등 그런 세부적인 옵션을 말합니다.%%% 네 번째 인자는 setsockopt 함수에서는 설정될 값이 무엇이냐 하는 것이고, getsocketopt 함수에서는 얻은 값을 저장할 변수의 포인터입니다.%%% 다섯 번째는 네 번째 변수의 길이입니다. 변수라 말했지만 이것은 구조체로 된 것도 있습니다.%%% 네 번째 인자를 보면 void * 형 입니다. 이것은 무엇을 말할까요. 즉, 변수의 형이 정해지지 않았다는 것입니다. 각 옵션에 따라 int도 있고, unsigned char도 있고, 구조체도 있습니다.

이제 자주 사용하는 옵션들에 대해서 알아보도록 하겠습니다.

1) SOL_SOCKET Level

  • SO_RCVBUF, SO_SNDBUF

버퍼의 크기를 바꾸는 옵션입니다. 커널의 recv Buffer, send Buffer의 크기를 조절하는 데 사용합니다. 이것을 어떻게 잘 설정하느냐에 따라 성능이 향상된다고 합니다. 버터의 크기는 테스트와 네트워크의 상태에 따라서 달라진다고 합니다. 그런데 보통은 (대역폭 * 지연율) * 2 의 공식에 따라 버퍼의 크기를 설정한다고 합니다. recv Buffer 의 크기를 변경하는 코드를 보도록 하겠습니다.

int rcvBufferSize; int sockOptSize; . . . // 소켓 리시브 버퍼 크기 얻기 sockOptSize = sizeof( rcvBufferSize ); if( getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &rcvBufferSize, &sockOptSize ) < 0 ) { printf( "getsockopt() failed " ); exit( 1 ); } // 리시브 버퍼의 크기를 2배로 만든다. rcvBufferSize *= 2; if( setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &rcvBufferSize, sizeof( rcvBufferSize ) < 0 ) ) { printf( "setsockopt() failed " ); exit( 1 ); }

여기에서 보면 SOL_SOCKET, SO_RCVBUF 가 나옵니다. 즉, 일반적인 소켓의 옵션 중에 recv Buffer 의 크기를 말합니다. recv Buffer 와 마찬가지로 send Buffer 변경 옵션은 SOL_SOCKET level에 있습니다. 그리고 SO_RCVBUF 대신에 SO_SNDBUF라는 것을 넣어 주면 변경이 가능합니다.

그런데 하나 주의 하실 점이 있습니다. 바로 호출 순서입니다. 소켓의 옵션들의 대부분이 호출 순서가 중요합니다. 클라이언트의 경우 connect() 하기 전에 recv Buffer 를 변경해야 하는데, 왜냐하면 three-way handshake 할 때 MSS를 알려 주기 때문입니다. 그리고 서버의 경우에 listensocket (listen() 함수 호출 시 전달되는 소켓번호) 의 설정은 listen() 호출 전에 먼저 설정을 해주어야 합니다. 쉽게 설명해서 연결이 성립될 때 그러니까 three-way handshake할 때 한번에 보낼 수 있는 TCP 세그먼트(패킷)의 크기의 최대값을 알려 줍니다. 연결이 성립되면 최대 세그먼트의 크기(MSS)를 변경할 수 없어 연결하기 전에 미리 바꾸어 두는 것입니다.

  • SO_LINGER

SO_LINGER 옵션이 있습니다. 이것은 TCP 에서 적용되는 것인데 close함수의 행동을 지정하는 옵션입니다. close() 하면 recv Buffer 나 send Buffer 에 보내거나 받을 데이터가 있다면 전부 처리 후 close() 를 합니다. 그 방법을 바꾸는 것입니다. 먼저 전달되는 구조체에 대해서 알아 보도록 합시다.

struct linger { int l_onoff; int l_linger; }; setsockopt( sock, SOL_SOCKET, SO_LINGER, &linger 구조체 주소, sizeof( linger ) );

이런 식으로 호출하면 되겠죠. 그리고 세부적인 동작 설정은 linger구조체의 변수 설정에 있습니다.

  1. l_onoff가 0이면 기본적인 TCP동작이 적용됩니다.
  2. l_onoff가 0이 아니고(주로 1을 넣습니다.) l_linger가 0이면 연결이 닫힐 때 버퍼의 내용을 버리고 연결을 끊어 버립니다.
  3. l_onoff가 0이 아니고 l_linger도 0이 아니면 소켓이 닫힐 때 블럭 당한다고 합니다.

이 소켓옵션을 쓸 땐 2번을 주로 씁니다. 쓰는 이유는 만약 서버가 종료되고 다시 시작 할 때 입니다. 연결이 끊어지고 남은 데이터를 전송합니다. 그때 남은 데이터를 보낸다면 클라이언트에게 ack 메시지(받았다는 확인 메시지)를 받아야 완전한 종료가 이루어집니다. 그 메시지를 기다리는 시간이 있습니다. 만약 그것을 다 받지 못했다면 다시 보내야 하지요. 그런 상황에서 다시 서버를 시작하려고 하면 이미 사용 중인 포트라는 에러를 내게 됩니다. 그래서 이런 옵션을 사용하는 것입니다. 그런데 그것은 바람직한 해결 방법이 아니라고 합니다. 그래서 이런 옵션은 추천되고 있지 않습니다. 이에 대한 해결책은 따로 있습니다. 그것이 다음에 설명할 포트 재사용 옵션입니다.

  • SO_REUSEADDR

이 옵션을 선택하여 주면 위의 예에서 말한 서버 재 시작 시 다시 시작할 수 있습니다. 간단히 사용법을 알아보도록 하지요.

int nResue = 1; setsockopt( ListenSocket, SOL_SOCKET, SO_REUSEADDR, &nReuse, sizeof( nReuse ) );

이것도 호출 순서가 있는데 bind() 하기 전에 이 옵션을 설정해 놓아야 합니다. 이렇게 하면 소켓의 포트를 재 사용할 수 있습니다.

2) IPPROTO_IP Level

여기에는 주로 멀티캐스트와 관련된 옵션들이 있습니다. 다음에 멀티캐스트를 하실 때 그때 사용법을 참고하기면 됩니다.

3) IPPROTO_TCP Level

  • TCP_NODELAY

TCP에 보면 잔잔한 패킷들을 하나씩 다 보내는 것이 아니라 네트워크상에 작은 패킷들을 줄이기 위해 Nagle 알고리즘을 사용하여 어느 정도 묶어서 한꺼번에 보내는 것이 있습니다. 이것을 사용 할지 안 할지를 설정하는 옵션입니다. 이것은 주로 서버에서는 이 알고리즘을 사용 안 한다고 합니다. 왜냐하면 다른 일을 해야 하기 때문에 그냥 바로 보내버리는 것이 더욱 효과적이라는 것입니다. 패킷의 개수가 많아지기는 하지만 그런 알고리즘의 딜레이를 버림으로 보다 빠른 처리를 할 수 있다는 것입니다. 그러나 클라이언트의 경우는 Nagle알고리즘을 사용합니다. 작은 패킷을 묶어 보내 네트워크의 부하를 줄이자는 것입니다.

소켓 옵션은 다양하고 많은 것들이 있고 주의 사항들이 있습니다. 항상 자신의 프로그램에 맞게 올바른 설정을 하시고 사용하시기 전에 여러 가지로 알아보시고 하시기를 바랍니다. 그리고 옵션에서 인자 값을 넘길 때 인자의 형이 다릅니다. 위의 예제에서도 Linger옵션은 구조체를 사용하고 다른 것은 int 형이었습니다. 그리고 윈속(Winsock)도 다릅니다. 구조체로 된 부분은 거의 같지만 int로 된 부분은 BOOL로 사용하는 부분이 많이 있습니다. 잘 알아 보시고 사용하시기 바랍니다.

에코 프로그램

그럼 이제 소스를 보면서 이제까지의 내용들을 정리하도록 하겠습니다. 여기의 모든 소스는 유닉스나 리눅스 용입니다. 윈도우에서는 실행이 되지 않습니다.

구현할 것은 에코 서버와 클라이언트입니다. 에코 서버는 에코 클라이언트가 보낸 데이터를 받아서 그대로 다시 에코 클라이언트에게 보내는 것입니다. 그럼 에코 서버부터 보도록 하겠습니다.

에코 서버

서버의 실행은 <실행파일명 포트번호> 입니다.

#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> // socket() bind() connect() #include <arpa/inet.h> // socketaddr_in, inet_ntoa() #include <netinet/in.h> // 만약 FreeBSD 라면 이 해더를 추가해야 한다. #include <string.h> #include <unistd.h> #include <stdlib.h> #define RCVBUFSIZE 128 #define MAXPENDING 5 int main( int argc, char * argv[] ) { struct sockaddr_in echoServAddr, echoClntAddr; int servSock, clntSock; unsigned short echoServPort; unsigned int clntLen; char echoBuffer[BUF_LEN]; int recvMsgSize; if( argc != 2 ) { printf( "Usage : %s port ", argv[0] ); exit( 1 ); } echoServPort = atoi( argv[1] ); if( ( servSock = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ) ) < 0 ) { printf( "socket() failed " ); exit( 1 ); } memset( &echoServAddr, 0, sizeof( echoServAddr ) ); echoServAddr.sin_family = AF_INET; echoServAddr.sin_addr.s_addr = htonl( INADDR_ANY ); echoServAddr.sin_port = htons( echoServPort ); if(bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0) { printf("bind() failed "); exit(1); } if(listen(servSock, MAXPENDING) < 0) { printf("listen() failed "); exit(1); } for(;;) { clntLen = sizeof(echoClntAddr); if((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen)) < 0) { printf("accept() failed"); exit(1); } if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0) { printf("recv() failed"); exit(1); } while(recvMsgSize > 0) { if(send(clntSocket, echoBuffer, recvMsgSize, 0) != recvMsgSize) { printf("send() failed"); exit(1); } if((recvMsgSize = recv(clntSocket, echoBuffer, RECVBUFSIZE, 0)) < 0) { printf("recv() failed"); exit(1); } } close(clntSocket); } }

자, 이게 에코서버의 모습입니다. 우선 소스부터 분석하도록 합시다. 우선 서버의 소켓을 열고 서버의 주소 정보를 채우고 bind 시키고 클라이언트의 요구를 듣는 상태로 들어 갔습니다. 그리고 클라이언트의 연결요청이 들어오면 accept함수를 호출하여 클라이언트와 연결을 하고 그리고 에코 서비스를 해주는 과정으로 되어있습니다. 지금 제가 설명한 과정이 분석이 되실 겁니다. 그래도 좀더 자세히 설명에 들어가도록 합시다. 이 서버는 TCP 서버입니다. 그것을 알 수 있는 부분은 어디입니까? 소켓을 처음 생성하는 부분입니다.

servSock = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP );

여기서 SOCK_STREAM 으로 되어있습니다. 이것은 연결형 서버를 말하는 겁니다. 즉, TCP의 서비스를 이용하겠다는 것입니다. 여기서 마지막 인자를 IPPROTO_TCP 라고 되어있는데 꼭 이렇게 써주는 것은 아닙니다. 보통은 0 값을 전달합니다. 어느 것을 사용하나 상관은 없습니다. 명시적인 것이 좋으신 분은 IPPROTO_TCP라고 써주시면 되겠습니다. 여기서 PF_INET 를 사용했는데 AF_INET 와 무엇이 다를까요? PF 는 Protocol Family 의 약자이고 AF는 Address Family의 약자입니다. 글자는 달라고 내부적으로는 구분하지 않는다고 합니다. 그래서 여러 곳에서는 각각 다릅니다. PF_INET를 사용하는 것이 있고 AF_INET를 사용하는 곳도 있습니다. 여기에 접두어가 자신이 알기 쉽다고 생각하시는 것을 사용하시면 될 듯합니다. 중요한 것은 IPv4라는 겁니다. IPv6은 AF_INET6이라는 것을 사용합니다. 만약에 IP프로토콜 독립적으로 구성하고자 하시려면 이것을 고려 해주셔야 합니다. 다음으로 서버의 주소를 지정하고 bind 시켰습니다.

memset( &echoServAddr, 0, sizeof( echoServAddr ) ); echoServAddr.sin_family = AF_INET; echoServAddr.sin_addr.s_addr = htonl( INADDR_ANY ); echoServAddr.sin_port = htons( echoServPort ); if( bind( servSock, ( struct sockaddr * ) &echoServAddr, sizeof( echoServAddr ) ) < 0 ) { printf( "bind() failed " ); exit( 1 ); }

이 부분입니다. 여기서 3번째 줄에 INADDR_ANY 라는 단어가 들어 갑니다. 서버의 IP 주소를 넣어 주는 부분입니다. 직접 IP 주소를 넣어 주어도 상관은 없습니다. 그런데 이렇게 하면 만약에 서버의 IP가 바뀌거나, 다른 곳에서 서버를 가동시켜야 한다면, 이 부분도 바꾸어야 합니다. 그러면 위 소스에서 쓰는 것처럼 쓰는 것이 더욱 좋을 것입니다. 그런데 중요한 것은 그것이 아닙니다. 서버는 (꼭 서버만 아니고) IP가 여러 개인 서버도 있습니다. 그럴 경우 예를 들면, 여기서 bind 한 포트를 5000번이라고 합시다. 그리고 서버의 IP가 203.241.228.57 과 203.241.228.66 두 개의 IP를 가지고 있다고 하면, 클라이언트가 서버의 아무 IP를 가지고 포트 5000으로 들어오면 우리의 서버 어플리케이션에서 받겠다는 것입니다. 즉, IP : 203.241.228.57, Port 5000 ... IP : 203.241.140.66, Port 5000 으로 접속하는 모든 클라이언트의 요청을 받겠다는 것입니다. 내부적으로는 INADDR_ANY는 0의 값이 들어가 있다고 합니다. 위 서버 코드에서 서버의 IP를 출력해 보세요. 그럼 0.0.0.0 이 출력 될 겁니다.

간단히 실험하나 해보도록 하겠습니다. 도스 명령프롬프트 창을 열어서

>nslookup daum.net

라고 쳐봅니다. 물론 네트워크가 되는 컴퓨터에서요. daum의 IP주소가 나오는데 여러 개가 나옵니다. 위 명령은 네임서버에 daum의 정보를 얻어 오는 것입니다.

서버에서 에코 서비스를 처리하는 부분을 보도록 하겠습니다.

for(;;) { clntLen = sizeof(echoClntAddr); if((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen)) < 0) { printf("accept() failed"); exit(1); } if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0) { printf("recv() failed"); exit(1); } while(recvMsgSize > 0) { if(send(clntSocket, echoBuffer, recvMsgSize, 0) != recvMsgSize) { printf("send() failed"); exit(1); } if((recvMsgSize = recv(clntSocket, echoBuffer, RECVBUFSIZE, 0)) < 0) { printf("recv() failed"); exit(1); } } close(clntSocket); }

이 부분입니다. 클라이언트가 보낸 문자를 되돌려 보내는 부분입니다. 일단 무한 루프로 서버는 끝나지 않습니다. 여기에서 보면, 처음에 recv를 받고 send를 하고 다시 recv를 받습니다. 클라이언트는 처음 에코 요구를 하면 문자열을 한번만 보내는데 여기서는 한번 받고 루프를 돌면서 보내고 받고 그럽니다. 왜냐면 네트워크의 상태에 따라서 TCP는 얼마나 받을지 모른다는 것입니다. 그래서 루프를 돌면서 못 받은 데이터가 있으면 받아서 보내 줍니다.

여기서 에코 서버와 에코 클라이언트의 시나리오를 생각해 보도록 합시다. 서버가 일단 가동되고 있고, 그리고 클라이언트가 Hello라는 문자를 보내 서버에게 에코 요구를 보냅니다. 그런데 네트워크 상황이 안 좋아서 서버는 Hell 까지만 받고 말았습니다. 이것은 첫 번째의 recv 에서 받은 데이터입니다. 그러고 while로 들어가 send에서 서버는 Hell을 클라이언트에게 보냅니다. 그리고 다시 recv 로 들어갑니다. 그리고 o 라는 문자가 옵니다. 그리고 서버는 recv 로 받아서 다시 클라이언트에게 o를 보내어 줍니다. 그리고 다시 recv로 들어 갑니다. 그러면 여기서 서버는 recv 에서 블럭 됩니다. 네트워크에 어떤 데이터가 들어올 때까지 블록 되는 것입니다. 그럼 언제 recv가 리턴 되는가 하면 TCP는 연결을 끝내고 close를 할 때 recv에 0을 리턴 합니다. 설정될 때 three-way handshake하는 것처럼 연결을 끊을 때도 이와 비슷한 행동을 합니다. 그러니까 명시적으로 끊겠다고 하는 것이죠. 서버가 먼저 끊을 수 있고 클라이언트가 먼저 끊을 수도 있습니다. 어느 한쪽에서 close를 하면 “끊겠다”는 패킷을 다른 쪽에 보내면 다른 한쪽에서는 “알았다는 그리고 끊겠다”는 그런 종류의 단계를 취하는 것입니다. 그러면 여기서는 클라이언트가 먼저 끊었다고 합시다. 그럼 서버의 블록 된 recv는 0을 리턴 합니다. 그리고 while 루프를 끝냅니다. 그리고 마지막에 서버가 close를 해서 클라이언트와 연결을 닫습니다.

이제 서버의 구조에 대해 알아 봅시다. 위 서버는 클라이언트당 하나의 소켓이 열립니다. 처음 서버에서 소켓을 열어 listen() 함수에 전달하는 소켓을 보통 듣는 소켓(listen socket)이라고 합니다. 연결 요청은 이 소켓으로 들어 옵니다. 연결요청이 들어 오면 accept()로 다시 소켓 하나를 열어서 새로이 열린 소켓으로 클라이언트와 통신을 하는 것입니다. 그럼 이제 이 서버의 문제점을 알아 봅시다. recv에서 데이터를 받으면 블록 된다고 했습니다. 그런 클라이언트가 접속을 끝내지 않는다면, 다른 클라이언트의 서비스는 어려움이 많이 있습니다. 그리고 서버가 여러 가지 복잡한 일을 하는 것이라면, recv가 블록 되 프로세스가 놀고 있게 되어 효율적이지 못합니다. 그래서 이 문제를 해결하기 위해 넌 블로킹 모드, 비동기 모드 등이 나옵니다. 기본적인 서버의 구조와 네트워크는 데이터를 주고 받을 때 무엇이든 확신할 수 없다는 것. 지금 받은 것이 다 받은 것인지 보낸 것이 전부 보내어졌는지를 어떻게 확인하고 처리하는지 이해하시기를 바랍니다.

그럼 이번에는 클라이언트를 보기로 하지요. 클라이언트는 서버보다 간단하니 한번 보시면서 분석해 보시기 바랍니다. 보낸 데이터를 에코 해서 받을 때 어떻게 받았는지 어떻게 다 받았는지 클라이언트는 자신이 서버에 보낸 문자의 길이를 알고 있기 때문에 이것을 활용했습니다. 한번 보시면 이해하시리라 생각합니다.

에코 클라이언트

그럼 이제 클라이언트 소스입니다.

클라이언트의 실행은 <실행파일명 서버IP 에코문자 포트> 입니다.

#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> // socket() bind() connect() #include <arpa/inet.h> // socketaddr_in, inet_ntoa() #include <netinet/in.h> // 만약 FreeBSD 라면 이 해더를 추가해야한다. #include <string.h> #include <unistd.h> #include <stdlib.h> #define RCVBUFSIZE 128 int main(int argc, char * argv[]) { int sock; struct sockaddr_in echoServAddr; unsigned short echoServPort; char * servIP; char * echoString; char echoBuffer[RCVBUFSIZE]; unsigned int echoStringLen; int bytesRcvd, totalBytesRcvd; if((argc < 3) || (argc > 4)) { printf("Usage: %s <Server IP> <Echo Word> [<Echo Port>] ", argv[0]); exit(1); } servIP = argv[1]; echoString = argv[2]; if(argc == 4) echoServPort = atoi(argv[3]); else echoServPort = 7; // 에코 서버의 well-know port입니다.. if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { printf("socket() failed"); exit(1); } memset(&echoServAddr, 0, sizeof(echoServAddr)); echoServAddr.sin_family = AF_INET; echoServAddr.sin_addr.s_addr = inet_addr(servIP); echoServAddr.sin_port = htons(echoServPort); if(connect(sock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0) { printf("connect() failed"); exit(1); } echoStringLen = strlen(echoString); if(send(sock, echoString, echoStringLen, 0) != echoStringLen) { printf("send() failed"); exit(1); } totalBytesRcvd = 0; printf("Received: "); while(totalBytesRcvd < echoStringLen) { // 문자끝 NULL을 넣기 위해 RCVBUFSIZE-1 if((bytesRcvd = recv(sock, echoBuffer, RCVBUFSIZE-1, 0)) <= 0) { // 리턴값이 0이면 서버와 연결이 끊어짐.. printf("recv() 실패 혹은 서버와 연결이 끊어졌다."); exit(1); } totalBytesRcvd += bytesRcvd; echoBuffer[bytesRcvd] = ''; printf(echoBuffer); } printf(" "); close(sock); exit(0); }

I/O 모델

?Unix Network Programming6장에 보면 I/O 모델에 대한 이야기가 나옵니다. 우선 그 모델을 살펴 보도록 하겠습니다.

Blocking I/O

위의 에코서버가 Blocking mode 입니다. 소켓을 열면 기본적으로 Blocking mode가 되는 것이지요. 말 그대로 Blocking 당한다고 생각하시면 되겠습니다. 위의 에코 서버를 생각해 봅시다. 에코 서버는 클라이언트로부터 데이터를 받기 위해 recv() 함수를 호출합니다. 그리고 프로세스는 클라이언트로 데이터가 올 때까지 멈춰있습니다. 그것이 Blocking 입니다. 그 함수가 일을 마칠 때까지 기다리고 있는 것입니다.%%% 에코 서버처럼 간단하고 해야 할 일이 별로 없는 서버는 Blocking으로 만드는 것이 가능합니다. 그러나 게임같이 서버에서 많은 일을 하는 그리고 많은 사용자들을 처리하는 서버에는 알맞지 않습니다. 왜냐하면 한 클라이언트에게 데이터를 받기 위해 recv()에서 서버가 멈춰있다면 다른 클라이언트에게 피해가 있고, 다른 필요한 처리를 하는데 서버가 놀고 있게 되기 때문입니다. 그럼 그런 부분에서 함수를 바로 리턴하게 한다면 즉, 만약 리시브를 호출해서 받을 데이터가 있으면 받고 없다면 넘어가서 다른 일을 하면 될 것입니다.

Non-Blocking I/O

이렇게 나온 것이 Non-blocking 입니다. Non-blocking 은 요청한 I/O를 그 상황에서 할 수 있으면 하고 할 수 없다면 거기서 멈추지 말고 함수를 리턴하여 다른 작업을 할 수 있게 해주는 것입니다.%%% 여기서 리턴될 때(I/O를 할 수 없어 리턴될 때) 다른 오류 코드를 리턴 한다면 I/O가 이루어졌는지 안 이루어 졌는지를 알 수 있을 것입니다. 그렇게 동작하는 모드가 Non-Blocking입니다.%%% 그런데 여기서는 몇 가지 문제가 있는데 만약에 요청한 I/O를 할 수 없다면 클라이언트로부터 데이터는 받아야 하니 데이터를 받을 때까지 확인하는 작업이 필요해 지는 것입니다. 계속 반복문을 돌려 데이터를 다 받았는지를 확인 해야 하는 것입니다. 그것을 Polling 이라고 부릅니다. 이것은 CPU의 시간낭비인데 그것을 줄이는 방법은 어떤 것이 있을까요. 서버가 여러 가지 일을 하고 있는 상황에서 클라이언트가 어떤 데이터를 보내왔다고 하면, 그럼 여기서 누군가가 클라이언트가 데이터를 보내왔다는 것을 서버에게 알려 준다면, 그러면 폴링(polling)을 하는 것보다는 조금 더 좋은 성능을 보여 줄 수 있을 것입니다.

I/O Multiplexing

번역하자면 입출력 다중화라고 합니다. selcet()함수나 poll()함수를 이용하여 실제적으로 구현합니다. 이런 함수들을 이용하여 I/O를 호출하면 실제적으로는 시스템에서 blocking 됩니다. 어플리케이션에서는 blocking 당해 있지는 않습니다. 여기에 여러 개의 소켓들을 설정하여 그 소켓에 send, recv, error 등을 설정할 수 있습니다. 그러면 시스템에서 그런 설정된 사항에 맞는 상황이 일어나면 어플리케이션에게 그 사항을 알려줍니다. 그러면 어플리케이션에서 그것을 보고 알맞은 처리를 해주는 것이죠. 그러나 여기에도 단점이 있는데 한번에 select로 설정해 줄 수 있는 소켓의 개수가 제한이 다?것입니다.

Signal Driven I/O

이 방법은 인터럽트와 비슷하다고 생각하시면 됩니다. 이것은 만약에 어떤 I/O를 요청하고 그것이 준비가 되면 어플리케이션에게 신호를 보내어 준비되었다는 것을 알려 주는 것이지요. 만약에 이러한 방법을 쓴다고 한다면 클라이언트에게서 데이터가 들어 왔다면 어플리케이션에서 지정한 신호가 어플리케이션으로 온다는 겁니다. 어플리케이션에서는 그러한 신호를 받으면 그에 따른 적당한 처리를 해주면 됩니다. 그런데 여기에도 약간의 문제가 있습니다. 그 신호라는 것이 중복되어 들어 온다면 뒤에 온 신호는 무시됩니다. 그리고 이 방법은 TCP 에는 적당하지 않다고 하는데 왜냐하면 TCP 에서는 신호를 설정해두면 수많은 신호들이 어플리케이션에게 온다고 합니다. 그리고 신호가 발생되어도 어떤 일이 있었는지 알 수 없다고 합니다. 그래서 주로 UDP에서 사용한다고 합니다.

Asynchronaus I/O

Signal Driven I/O 에서는 I/O작업이 시작되는 순간에 신호를 보내어 알려 주는 것입니다. 비동기에서는 I/O작업이 완료되었을 때 이 사실을 알려주는 방식입니다.

대략적으로 개념은 이해하시리라 생각이 듭니다. 많은 방법들이 있습니다. Blocking 에서는 block 당하는 것을 해결하려고 non-blocking 이 나오고 non-blocking 의 폴링(polling)을 해결하려는 여러 가지 방법들이 나온 것 같습니다.

유닉스에서는 주로 I/O Multiplexing 을 많이 사용한다고 합니다. 그러나 실제 성능을 테스트해보면 non-blocking 이 가장 좋은 성능을 낸다고 합니다. 그런데 non-blocking 은 적성이 힘들고, 유지보수가 힘들다고 합니다. 그래서 I/O Multiplexing 을 사용하라고 권장하는 것 같습니다.

요즘에는 kqueue 가 BSD 계열 유닉스에서는 그것이 가장 좋은 성능을 낸다고 하고 윈도우에는 IOCP(I/O complete port)가 좋은 성능을 낸다고 합니다. 그러면 이런 좋은 성능을 내는 것만 사용하면 되지 않을까요? 그런 건 아닌 것 같습니다. 그 서버의 역할에 맞는 I/O 모델을 사용하는 것이 가장 효율적일 것입니다. Blocking 으로 충분히 감당할 수 있는 서버인데 무리해서 다른 모델을 도입하는 것은 개발 속도와 유지보수 면에서 불리한 면이 있을 수 있습니다. 그리고 서버의 여러 가지 설계, 그런 부분에서 해당 서버에 잘 맞는 I/O 모델을 선택해서 쓰면 되겠지요.

I/O Multiplexing 예제

이제 위의 에코 서버를 다시 작성해 봅시다.

위의 에코 서버는 한번에 한 사용자만을 처리할 수 있는 그런 서버였습니다. 그런데 서버에서 한 클라이언트가 아닌 여러 클라이언트를 처리 해주는 경우가 대부분입니다. 그럼 어떤 방법으로 여러 클라이언트를 처리 할까요? 일단 blocking으로 생각을 해보면 앞 강좌와 같은 코드가 나올 겁니다. (따로 스레드나 프로세스를 생성하지 않는다면 말이지요.) 이 방법은 한 클라이언트가 접속을 끝내지 않는다면 다른 사용자들은 끝날 때까지 가만히 기다리고 있는 그런 상황에 이르게 됩니다.

그럼 위에서 나온 다른 방법을 한번 살펴 보죠. 우리는 TCP이기 때문에 시그널방식은 접어두고, non-blocking 은 폴링(polling)을 사용해야 하고 성능은 좋지만 간단한 에코 서버이기 때문에 굳이 복잡하게 프로그래밍을 할 필요가 없을 것 같습니다. 비동기 모드는 조금 더 많은 공부를 해야 하니 I/O Multiplexing 으로 하겠습니다. I/O Multiplexing 방법 중 하나가 select()를 이용하는 방법입니다. 먼저 select() 함수에 대해 알아보도록 하겠습니다.

int select( int maxDescPlus1, fd_set * readDescs, fd_set * writeDescs, fd_set * exceptionDescs, struct timeval * timeout );

이것이 select함수의 원형입니다. 인자를 반대로 가며 설명을 하겠습니다. 먼저 timeval 입니다. 이 구조체는 몇 초인지, 그리고 몇 마이크로 초인지를 설정할 수 있습니다. 구조체를 살펴보면,

struct timeval { long tv_sec; // 초 long tv_usec; // 마이크로초 };

이 구조체의 필드를 채워서 보내면 어떤 입출력이 준비가 되거나 시간이 지나면 select()함수가 리턴 됩니다. 그러니까 지정된 시간 이상은 입출력의 준비를 받지 않겠다는 것입니다. 만약 이 필드에 전부 0으로 설정을 하면 지정된 입출력들을 점검한 뒤 바로 리턴 됩니다. 그리고 여기에 null값을 주면 무한히 기다리게 되는 것입니다.%%% 그 다음 인자를 보면 새로운 fd_set라는 것이 보입니다. 여기에 fd_set이라는 것으로 설정하여 시스템에게 어플리케이션에서 무엇을 해주길 바라는 지 알려 주는 것입니다. 변수 명을 보면 read, write, exception이라는 접두어가 있습니다. 말 그대로 recv에 필요한 것이면 설정하여 두 번째 인자에 넣어 주고 send가 필요하면 그 다음, exception 에 대한 처리가 필요하다면 그 다음의 인자에 설정하여 넣어 주면 되는 것입니다. 그러면 fd_set는 어떻게 생겨 먹었을까요. 여러 개의 나열된 비트 필드로 이루어져 있습니다.

소켓번호 0 1 2 3 4 5 6 .... ------------------------------------- | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | -------------------------------------

<그림> fd_set의 구조

만약 이렇게 하면 소켓번호 0번과 3번에 어떤 I/O가 일어 나면 알려 달라는 것입니다. 이것을 readDescs 인자에 넣으면 0번과 3번에 어떤 데이터가 들어오면 select문이 리턴 되는 것입니다. 똑같이 write와 exception인자도 동작이 똑같습니다. 그런데 0번은 표준 입력을 말합니다. 즉, 키보드를 말하지요. 1번은 표준출력을 2번은 표준에러를 말합니다. 표준에러와 출력은 주로 모니터를 가리킵니다. 소켓번호는 유닉스의 파일 디스크립터와 같다고 하였습니다. 유닉스는 전부 파일로 관리되니 0, 1, 2 는 표준 입력 출력 에러로 지정되어 있는 것입니다. 다시 위의 그림을 이야기 하면 키보드(표준입력)이나 소켓 번호 3번에 어떤 데이터가 들어오면 select가 리턴 되는 것입니다. 그런데 비트필드로 되어 있으니까 사용하기 불편합니다. 여기에는 매크로가 있습니다. 그 매크로를 이용해서 설정하고, 지우고, 확인합니다. 그 매크로를 알아보면,

매크로 명 |설명 FD_ZERO(fd_set *fdset) |*fdset의 모든 비트를 0으로 설정 FD_SET(int fd, fd_set *fdset) |*fdset 중 소켓 fd에 해당하는 비트를 1로 설정 FD_CLR(int fd, fd_set *fdset) |*fdset 중 소켓 fd에 해당하는 비트를 0으로 설정 FD_ISSET(int fd, fd_set *fdset) |*fdset 중 소켓 fd에 해당하는 비트가 1이고, 소켓에 I/O 변화가 생겼으면 true를 리턴

이렇게 있습니다. 처음에 FD_ZERO를 이용하여 초기화하고 확인할 소켓번호에 FD_SET로 설정한 뒤, select()를 호출합니다. 그리고 select가 리턴 되면, FD_ISSET로 어느 것이 입력 혹은 출력, 에러가 되었는지 확인하여 알아 내는 것입니다. 초기화는 중요합니다. 잘못된 값이 들어가는 것을 방지 하니깐요. 꼭 초기화를 해주시기 바랍니다.%%% 첫 번째 인자는 설정될 소켓번호의 최대값에 +1을 한 것입니다. +1을 한 이유는 배열과 비슷합니다. 위의 그림을 보면 0부터 시작합니다. 즉, 지정한 소켓의 최대값이 아니라 지정한 소켓의 개수를 나타내는 것이기 때문입니다.%%% select()함수의 리턴 값은 -1이면 에러를 나타내고 0이면 타임아웃을 나타냅니다. 그리고 양수이면 준비된 소켓번호의 카운터를 말합니다.

그럼 소스를 보도록 하겠습니다.

#include <stdio.h> #include <sys/socket.h> #include <unistd.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/time.h> #define MAXPENDING 5 #define RCVBUFSIZE 512 int CreateTCPServerSocket(unsigned short port); int AcceptTCPConnection(int servSock); void HandleTCPClient (int clntSocket); int main(int argc, char * argv[]) { int * servSock; int maxDescriptor; fd_set sockSet; long timeout; struct timeval selTimeout; int running = 1; int nPorts; int port; unsigned short portNo; if(argc < 3) { printf("Usage : %s <Timeout (secs.)> <Port 1> ... ", argv[0]); exit(1); } timeout = atol(argv[1]); noPorts = argc - 2; servSock = (int *) malloc(noPorts * sizeof(int)); maxDescriptor = -1; for(port = 0; port < noPorts; port++) { portNo = atoi(argv[port + 2]); servSock[port] = CreateTCPServerSocket(portNo); if(servSock[port] > maxDescriptor) maxDescriptor = servSock[port]; } printf("Starting server : Hit return to shutdown "); while(running) { FD_ZERO(&sockSet); FD_SET(STDIN_FILENO, &sockSet); for(port = 0; port < npPorts; port++) FD_SET(servSock[port], &sockSet); selTimeout.tv_sec = timeout; selTimeout.tv_usec = 0; if(select(maxDescriptor+1, &sockSet, NULL, NULL, &selTimeout) == 0) printf( "No echo requests for %ld secs... Server still alive ", timeout); else { if(FD_ISSET(STDIN_FILENO, &sockSet)) { printf("Shutting down server "); getchar(); running = 0; } for(port = 0; port < noPorts; port++) { if(FD_ISSET(servSock[port], &sockSet)) { printf("Request on port %d : ", port); HandleTCPClient( AcceptTCPConnection(servSock[port])); } } } } for(port = 0; port < noPorts; port++) close(servSock[port]); free(servSock); exit(0); } int CreateTCPServerSocket(unsigned short port) { int sock; struct sockaddr_in echoServAddr; if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { printf("socket() failed "); exit(1); } memset(&echoServAddr, 0, sizeof(echoServAddr)); echoServAddr.sin_family = AF_INET; echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY); echoServAddr.sin_port = htons(port); if(bind(sock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0) { printf("bind() failed "); exit(1); } if(listen(sock, MAXPENDING) < 0) { printf("listen() failed "); exit(1); } return sock; } void HandleTCPClient (int clntSocket) { char echoBuffer[RCVBUFSIZE]; int recvMsgSize; if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0) { printf("recv() failed "); exit(1); } while(recvMsgSize > 0) { if(send(clntSocket, echoBuffer, recvMsgSize, 0) != recvMsgSize) { printf("send() failed "); exit(1); } if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0) { printf("recv() failed "); exit(1); } } close(clntSocket); } int AcceptTCPConnection(int servSock) { int clntSock; struct sockaddr_in echoClntAddr; unsigned int clntLen; clntLen = sizeof(echoClntAddr); if((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen)) < 0) { printf("accept() failed "); exit(1); } printf("Handling client %s ", inet_ntoa(echoClntAddr.sin_addr)); return clntSock; }

전체적인 프로그램의 흐름을 보면, 사용자가 사용하겠다는 포트를 여러 개 열어서 각각 클라이언트의 요청이 들어오기를 기다리고 있습니다. 사용자가 정한 시간에 맞추어서 말이죠. 그리고 요청이 들어오면 에코 서비스를 하고 만약 사용자가 리턴 키를 누르면 서버가 종료되는 것입니다. (STDIN_FILENO가 리턴 키를 누르면 준비 됨)

그러나 이 예제도 부족합니다. 여기에서는 한 클라이언트를 받던걸 여러 사용자에게 받게 하였을 뿐입니다. 한 사용자를 전부 처리 할 때까지 다시 다른 클라이언트는 기다려야 하는 것은 아직 해결되지 못했습니다.%%% 그럼 어떻게 하면 공평하게 여러 사용자를 처리 할 수 있을까요? 만약 각 클라이언트마다 그 클라이언트를 전담하는 무언가를 만든다면, 어느 정도 공평하게 클라이언트를 처리할 수 있을 겁니다.%%% 그 방법에는 클라이언트당 하나씩 프로세스를 따로 만들 수도 있습니다. 그러나 프로세스를 하나 만든다는 것은 많은 비용이 들어 갑니다. 한마디로 메모리도 많이 먹고 새로운 프로세스를 만드는 데도 시간이 많이 걸린다는 것입니다. 또한 컨텍스트 스위칭(다른 프로세스로 CPU 타임을 넘기는 행동) 하는 그 비용도 많이 듭니다. 그리고 사용자가 아주 많다면 분명 서버에 무리가 갈 것입니다.%%% 프로세스보다 비용이 조금 드는 스레드를 한번 생각해봅시다. 분명 프로세스보다 비용이 덜 드니 프로세스보다는 성능이 좋을 것입니다. 그러나 사용자가 많아진다면, 이것 또한 해결책은 아닌 것 같습니다. 그러나 서버의 어떤 작업을 스레드로 분리하면 무언가 좋은 방법이 나올 것입니다. 그런 방법에 대해서는 다른 책이나 강좌를 참고하세요.

이번엔 프로세스가 아닌 스레드를 이용하여 에코 서버를 한번 만들어 보도록 합시다. 모든 문제를 해결할 수 있지는 않지만 공부하는 차원에서는 유용할 것입니다.%%% 유닉스에서는 POSIX라는 표준이 있습니다. 여기에는 스레드에 대한 내용이 있는데 그것이 pthread입니다.

POSIX Thread

Thread vs Process

각 클라이언트를 다루기 위해 새로운 프로세스를 하나 만드는 것은 비용이 많이 듭니다. 그 내용을 살펴보면,

  • 프로세스가 하나 생성될 때마다 운영체제는 메모리, 스택, 파일/소켓 식별자들 및 기타를 포함한 부모 프로세스의 전체 상태를 복사
  • Thread들은 같은 프로세스 내의 멀티태스킹을 허용함으로써 이러한 비용을 감소. 새로 생성된 Thread는 부모와 같은 주소공간(코드 및 데이터)을 공유하고, 부모의 상태를 복제할 필요성 배제
  • 프로세스 복제이후 부모와 자식간에 정보를 주고받기 위해 프로세스간 통신(IPC) 필요 (자식으로부터 부모로 정보를 되돌리는 것은 더욱 많은 작업을 요구)
  • 프로세스 중의 모든 Thread가 공유하는 것
    • 프로세스 지시 사항
    • 대부분의 데이터
    • 공개된 파일들(Ex 지정 번호들)
    • 신호 처리기와 신호 배치들
    • 사용자와 그룹 ID
  • 각 Thread 자신만이 갖는 것
    • Thread ID
    • 프로그램 계수기와 스택 지시자를 포함한 레지스터의 조합
    • (지역변수와 반환 주소를 위한) stack
    • errno
    • 신호 선별
    • 우선순위

이와 같이 비교 될 수 있습니다.

Basic Thread Functions

거의 모든 pthread 함수는 성공하면 0을 리턴하고 실패하면 0이 아닌 값을 리턴 합니다. 그러나 errno 변수는 설정하지 않는 것이 특징입니다. 밑의 pthread 함수들은 모두 여기에 따른다고 생각하시면 됩니다.

pthread를 사용하기 위해서는 밑의 해더를 추가해야 합니다.

#include <pthread.h> int pthread_create( pthread_t * threadID, // Thread ID, (unsigned int) const pthread_attr_t * attr, // Thread 속성, NULL Default void * ( * func )( void * ), // 입구함수 void * arg ); // 여러 인자를 전달할 때, structure 이용

스레드를 생성합니다. 첫 번째 인자는 스레드가 생성되면 그 스레드의 ID가 저장되는 변수이고, 두 번째 인자는 여러 가지 속성(우선순위나 스텍 사이즈 등을 말합니다.)을 나타냅니다. default로 하려면 NULL을 전달 하면 됩니다. 세 번째 인자는 스레드의 입구 함수(스레드가 시작되는 함수, 스레드가 할 역할을 기술해놓은 함수)입니다. 스레드가 실행되면 그 함수를 실행합니다. 입구함수의 형태는 반드시

void * ThreadMain( void * arg );

위와 같은 형태여야 합니다. 그리고 마지막은 스레드 입구함수의 인자로 전달될 변수입니다.

void pthread_exit ( void * status ); // 리턴할 값

Thread 중단합니다. 만일 Thread가 분리되지 않으면 Thread ID와 리턴 값은 종결 프로세스의 다른 Thread에 의하여 나중까지 pthread_join에 남겨집니다. Thread가 종결 될 때에는 객체가 사라지므로 status 는 호출 Thread에 지역적인 변수를 지정하면 안됩니다.

int pthread_join ( pthread_t tid, void ** status );

tid가 가리키는 Thread가 종료할 때까지 위의 함수를 호출한 Thread의 수행을 멈춥니다. 만약 status가 NULL이 아니면 tid의 리턴 값은 status가 가리키는 영역에 저장됩니다.

pthread_t pthread_self ( void );

Thread 자신의 Thread ID 리턴 합니다. 이 값은 pthread_create() 로 얻은 스레드 ID와 동일합니다.

int pthread_detach ( pthread_t tid );

Thread 상태가 부모의 개입 없이도 종료 시 즉시 해제합니다. 주로 pthread_detach ( pthread_self() ); 로 사용합니다.

보다 더 자세한 사항은Joinc의 Pthread API Reference를 참고하세요.

TCP Echo Server를 위한 클라이언트당 Thread 멀티태스킹 Source

/****** TCPEchoServer_Thread.c *******/ #include "TCPEchoServer.h" #include <pthread.h> // for POSIX threads void * ThreadMain(void * arg); // Main program of a thread // Structure of arguments to pass to client thread struct ThreadArgs { int clntSock; }; int main(int argc, char * argv[]) { int servSock; int clntSock; unsigned short echoServPort; pthread_t threadID; struct ThreadArgs * threadArgs; if(argc != 2) { fprintf(stderr, "Usage: %s <Server Port> ", argv[0]); exit(1); } echoServPort = atoi(argv[1]); servSock = CreateTCPServerSocket(echoServPort); for(;;) { clntSock = AcceptTCPConnection(servSock); // Create memory for client argument if((threadArgs = (struct ThreadArgs *) malloc(sizeof(struct ThreadArgs))) == NULL) DieWithError("malloc() failed"); threadArgs->clntSock = clntSock; // Create thread if(pthread_create(&threadID, NULL, ThreadMain, (void *) threadArgs) != 0) DieWithError("pthread_create() failed"); printf("with thread %ld ", (long int) threadID); } } void * ThreadMain(void * threadArgs) { int clntSock; // Guarantees that thread resource are deallocated upon return pthread_detach(pthread_self()); clntSock = ((struct ThreadArgs *) threadArgs)->clntSock; free(threadArgs); // Deallocate memory for argument HandleTCPClient(clntSock); return (NULL); }

TCP 관련 소스는 앞의 select() 예제 소스와 같으니 위의 소스를 보시면 되겠습니다. 소스코드의 컴파일은

$ gcc -o TCPEchoServer-Thread TCPEchoServer_Thread.c -Wall -lpthread (FreeBSD 라면 -pthread)

로 하시면 됩니다.

소켓 부분은 에코 서버와 거의 같으니 스레드 부분만 보도록 하겠습니다.

pthread_create(&threadID, NULL, ThreadMain, (void *) threadArgs);

이 부분이 스레드를 생성하는 함수입니다. 입구함수 인자로 여기에서는 소켓번호 하나만 전달합니다. 그런데 전달되는 구조체를

struct ThreadArgs { int clntSock; };

이렇게 선언해 놓았습니다. 물론 소켓번호만 전달해도 상관없습니다. 그런데 만약 여러 가지 정보를 전달하려고 하면 인자가 하나뿐이니 하나만 전달할 수 있습니다. 아니면 전역 등의 방법을 사용해야 합니다. 그런데 스레드 입구함수의 인자가 void * 형이기 때문에 이런 구조체를 만들어서 구조체의 포인터를 전달하면 여러 인자를 전달 할 수 있게 됩니다.

그리고 소스를 보면 전달될 인자를 동적 메모리 할당을 하였는데 반드시 이렇게 해야 합니다. 만약 지역변수로 전달하게 되면 어떻게 될까요? 스레드는 서로 경쟁하며 실행됩니다. 그러니까 정확히 어느 것이 먼저 실행될지는 아무도 모르는 것입니다. 그래서 지역변수로 선언해 놓았다면 스레드가 실행되어 인자가 참조되기 전에 스레드 함수를 호출한 곳이 먼저 종료되었다면 인자의 변수는 잘못된 메모리를 가리키고 있는 것이 되어 버립니다. 그래서 잘못된 결과를 이르게 하는 것이죠. 그리고 스레드 입구함수에서 인자의 메모리를 해제했습니다. 이것도 위와 비슷합니다. 만약에 스레드 함수를 호출한 곳에서 메모리를 해제한다면 지역변수와 똑같은 결과를 낳게 됩니다.

위 소스는 부족한 소스입니다. 만약 서버가 종료된다면 생성했던 스레드가 확실히 종료되었는지 그런 것을 알 수 없기 때문이죠. 물론 소멸되겠지만요. 안전하게 스레드가 종료되었는지 알고 나서 서버를 닫는 것이 더욱 좋을 것입니다.

위의 스레드의 소스는 하나의 처리를 스레드에게 맡김으로써 여러 사용자를 받을 수 있게 하였습니다. 프로세스를 생성하는 것도 이와 비슷합니다. Pthread_create() 대신 fork()함수를 이용해서 프로세스를 만들면 됩니다. 앞에서 select()와 스레드에 대해서 알아 봤습니다. 그럼 이 두 개를 결합하여 만드는 것은 어떨까요? 그런 것은 채팅 서버를 한번 만들어 보시면서 하면 좋은 예가 될 것 같습니다.

마치며

초보적인 내용인 만큼 이 글이 네트워크를 공부를 시작하시는 분에게 조금이나마 도움이 되었으면 합니다. 그리고 함수 설명보다는 원리에 중점을 두어 설명을 하려고 했는데 잘 안된 것 같습니다. 아직 UDP, 멀티캐스트 같은 주제가 남아 있습니다. 꼭 보시고 가시길 바랍니다. 보통 멀티캐스트는 지원하는 라우터가 많이 없어서 하기 힘들다고 합니다. 소프트웨어로 처리하는 방법이 있기는 하지만 아직 부족한 점이 많이 있나 봅니다. 그러나 UPD는 반드시 공부해보셔야 할 듯합니다. TCP와 UDP의 각자의 장/단점이 서로 절충 될 수 있는 소지가 많이 있기 때문입니다. 많은 운영체제가 운영체제마다 다른 poller 를 제공하고 있는데 보통 성능상의 문제로 이런 것들을 이용해서 서버를 작성해나간다고 합니다. 각각마다 장단점이 있어 제가 무어라고 할 수 없지만, 그런 것들은 하나의 방법인 것 같습니다. 여러 가지 방법을 생각해보고 자신의 어플리케이션에 가장 효율적이고 가장 알맞은 그런 방법을 찾으면 되는 것 같습니다. 조금이라도 좋은 성능을 얻기 위해 서버를 어떻게 구성해야 하는지 패킷은 어떻게 구성하는지 등 여러 가지 방법들을 연구해보고 테스트 해보세요.

크리에이티브 커먼즈 라이선스
Creative Commons License
사용자 삽입 이미지

사용자 삽입 이미지

사용자 삽입 이미지

개요
본 장은 Oracle Recovery Manager(RMAN)과 Backup Manager를 사용하여 물리적 백업을 수행하는 것에

초점을 두고 있습니다. RMAN 없이 백업을 수행하는 배경 지식이 본 장에 적용됩니다

(예를 들어 백업하는 동안 데이터베이스가 열려지거나 닫혀질 수 있습니다). 본 장에는 RMAN을 사용하기 위한 추가

백업 고려 사항, 요구 사항, 개념이 설명되어집니다.

Note: 오라클은 운영 체제 백업과 Recovery Manager를 사용한 백업을 포함하는 유연한 백업 전략을 권장합니다.

각 백업의 빈도는 다음에 따라 달라집니다:

  • 필요한 복구 방법
  • 변경 빈도와 변경되는 데이터량
사용자 삽입 이미지

닫힌 데이터베이스 백업(Closed DatabaseBackup)
데이터베이스가 닫혀 있는 동안(offline) 데이터베이스의 일부분을 백업하는 것을 말합니다.

닫힌 백업을 수행하려면 대상 데이터베이스가 열려지는 것이 아니라 마운트 되어야만 합니다.

복구카탈로그(RecoveryCatalog)를 사용한다면 복구카탈로그(RecoveryCatalog) 데이터베이스도

열려져 있어야 합니다.

 

오픈 데이터베이스 백업(Open Database Backup)
데이터베이스가 열려져 있는 동안(online) 데이터베이스의 일부분을 백업하는 것을 말합니다.

RMAN을 사용할 때는 테이블스페이스가 “alter tablespace begin backup” 명령을 사용하여

hot 백업 모드로 놓여져 있지 않아야 합니다. RMAN은 보다 적은 리두를 산출하는 다른 방법을 사용합니다.

 

Recovery Manager Backup의 유형
Recovery Manager는 다음과 같은 것들을 백업해 주는 기능을 제공합니다:

Note: RMAN을 사용하면온라인 리두 로그 파일은 백업되지 않습니다.

사용자 삽입 이미지

전체 백업
모든데이터 파일과 컨트롤 파일의 백업입니다.

완전 백업
증분 백업 전략에 요구되지 않는 하나, 또는 그 이상의 파일을 백업합니다. 지정된 파일에 대한 데이터를 포함하고 있는

모든 블록을 백업합니다.

증분 백업
마지막 증분 백업 이후 변경된 블록만을 포함하는 데이터파일의 백업입니다. 증분 백업은 지정된 파일에 대한 데이터를

포함하고 있는 모든 블록을 백업하는 기본 레벨(base-level) (또는 증분 레벨 0인) 백업을 필요로 합니다.

증분 레벨 0과 완전 백업은데이터 파일의 모든 블록을 복사합니다. 하지만, 완전 백업이 증분 백업 전략에 사용될 수는

없습니다.

 

운영 체제 백업
Recovery Manager가 아니라 운영 체제 유틸리티에 의한 오라클 파일 백업(예,데이터 파일의 사본)입니다.

사용자 삽입 이미지

지원 백업 유형
Recovery Manager 백업에는 두 가지 유형이 있습니다:

  • 이미지 복사: 운영 체제의 단일 파일 복사와 유사합니다만 오라클서버 프로세스에 의해 진행된다는 것이 다릅니다.
  • 백업 세트: 하나 이상의데이터 파일, 또는 특별한 오라클 형식으로 저장된아카이브된 로그를 포함합니다(오라클 익스포트 파일과 유사).

여러분은 이들 백업을 어떻게 혼용하는 것이 자신의 백업, 복구 전략에 가장 이로울지 결정해야 합니다.

이들 방식은 별개로 사용할 수도, 함께 사용할 수도 있습니다.

사용자 삽입 이미지

채널 할당
채널은 오라클 서버와 운영 체제 간의 통신을 위해 RMAN이 사용하는 방법으로 다음과 같은 특성을 가집니다:

  • Recovery Manager에서 실행되는 모든 백업이나 복원, 또는 복구 명령은 최소한 하나의 채널을 필요로 합니다.
     
  • 대상 데이터베이스에 대한 오라클 서버 프로세스는 할당된 모든 채널마다 생성됩니다.

  • 할당된 채널의 수는 백업이나 복원, 또는 복구하는 동안 사용되는 최대 병렬도와 같게 됩니다.

  • 필요한 매체 유형은 할당된 채널의 유형을 결정합니다.
  • 운영 체제나 매체 관리자를 위한 지원  장치 유형을 결정하려면 V$BACKUP_DEVICE를 질의하십시오.

  • 채널은 “allocate channel” 명령에 파라미터를 지정하여 “copy”와 “backup” 명령에 대한 제한을 가할 수 있습니다:
    -  Read rate: 과도한 디스크 IO로 인해 온라인 성능을 감소시키는 초당, 파일 당 읽는 버퍼의 수를 제한합니다.
       set limit channel <name> read rate = integer
    -  Kbytes: 채널에 의해 생성되는 백업 조각(backup piece) 파일 크기를 제한합니다.OS나 장치유형에 대한 최대 파일 크기가 존재할 때 유용합니다.
       set limit channel <name> kbytes = integer
    -  Maxopenfile: 큰 백업의 경우 동시에 열 수 있는 파일의 수를 제한합니다(기본값은 32). 이것은 운영 체제가 “too many files open” 에러를 리턴하는 것을 막기 위한 것입니다.
       set limit channel <name> maxopenfiles = integer
사용자 삽입 이미지

채널 할당의 예

  • 첫번째 예는 “change . . .delete” 명령으로 채널을 할당합니다. 파일은 OS에서 제거될 것입니다.
  • 두번째 예는 c1이라는 채널을 할당합니다. 이 채널에 의해 생성된 파일은 모두 '/disk1/backup/df3.bus' 형식을 가질 것입니다. 채널은 어느 순간 df3a.dbf와 df3b.dbf의 두 데이터 파일 중 오직 하나만을 열어 그데이터 파일을 백업하지만 다중화하지는 않을 것입니다.  이것은 동시에 한 파일밖에 읽지 못하기 때문에 tape streaming에는 이점이 없습니다.

채널 할당 구문
채널 할당 명령은 스크립트에 들어가거나 run 명령을 통합니다:
   allocate channel for delete <Device Type> <Options>;
   allocate channel <name> <Device Type> <Options>;
      단:     Device Type     TYPE=disk | <quoted_string>
                                       NAME=<quoted_string>

               Options             parms
                                        connect
                                        format

Note

  • 이것이 완벽한 구문은 아닙니다.
  • 구문의 첫번째 형태는 “change . . .delete” 명령에 대한 것입니다.
  • 두번째 형태는 백업, 복사, 복원, 그리고 복구에 대한 것입니다.
  • 순차적 IO 장치의 플랫폼 지정 유형을 식별하려면 “type=<quoted_string>”을 사용하십시오.
  • 디스크에 쓰거나 디스크로부터 읽어 들이려면 “type=disk”를 사용하십시오.
  • 포트에 특정한 I/O 장치를 지정하려면 “name=<quoted_string>”을 사용하십시오(일반적으로는 사용되지 않습니다).
  • “parms”는 포트에 특정한 문자열입니다. “type=disk”로 지정되어 있다면 사용하지 마십시오.
  • “connect”는 대상 데이터베이스에 대한 접속 문자열을 지정합니다(Oracle Parallel Server의 경우).
  • “format”은 백업 조각에 대한 기본 명명 규약을 지정합니다.
  • 할당된 각 채널은 각각 파라미터(db_block_size*db_file_direct_io_count)에 의해 각각 크기가 지정된 출력 버퍼를 네 개 생성합니다.

Backup Manager
Backup Manager를 사용하여 채널을 할당하려면:

  1. Backup Manager의 Oracle8 Recovery Manager subsystem을 사용하여 대상 데이터베이스에 접속
  2. Channel -> "Create…" 메뉴 항목 선택
  3. Create Channel 다이얼로그 박스에서는 반드시 채널 이름을 입력해야 합니다. 필요한 경우 다른 세부 사항(채널 제한 같은) 지정
  4. OK 클릭하여 채널 생성
  5. 이제 채널이 Backup Manager Navigator 창에 출력됩니다.
사용자 삽입 이미지

태그(Tag)
태그는 백업 세트나 파일 사본에 할당할 수 있는 의미있는 이름입니다. 사용자 태그의 장점은 다음과 같습니다:

  • 파일 사본, 또는 백업 세트 모음에 유용한 참조를 제공합니다.
  • 백업 파일을 쉽게 찾도록 “list” 명령에 사용할 수 있습니다.
  • “restore”와 “switch” 명령에 사용할 수 있습니다.
  • 다중 백업 세트나 파일 사본에 같은 태그를 사용할 수 있습니다.

유일하지 않은 태그가 하나 이상의데이터 파일을 참조할 경우 RMAN은 가장 현재의 사용 가능한 파일을 선택합니다.

예(슬라이드로부터)

  • 매달데이터 파일1, 2, 3, 4의 완전 백업이 수행됩니다. 만들어진 물리적 파일 이름이“df_DB00_863_1.dbf”일지라도 이 백업에 대한 컨트롤 파일 내의 태그는 “monthly_full_backup”입니다.
  • 매주데이터 파일3, 4의 완전 백업이 수행됩니다. 이 백업에 대한 태그는 “weekly_full_backup”입니다.
사용자 삽입 이미지

이미지 복사
단일데이터 파일,아카이브된 로그 파일, 그리고 컨트롤 파일을 디스크에 복사합니다.

이미지 복사 구문
복사 명령은 일반적으로 스크립트에 포함되거나 run 명령으로 실행됩니다.
   copy <Input File> to <Location> <Options>;
      단:     Input File      datafile
                                   datafilecopy [tag]
                                   backuppiece [tag]
                                   archivelog
                                   controlfilecopy [tag]
                                   current controlfile
               Options        tag=<name>
                                   level 0
                                   nochecksum

Note:이것이 완전한 구문인 것은 아닙니다.

사용자 삽입 이미지

이미지 사본의 특성
이미지 사본은 다음과 같은 특성을 갖습니다:

  • 디스크에만 쓰여질 수 있습니다. 파일이 디스크에 저장되면 즉시 사용할 수 있습니다(즉, 다른 매체로부터 복원할 필요가 없습니다). 이는 SQL 문 “alter database rename file”과 같은 RMAN 에서의 “switch” 명령을 사용하여 빠른 복구 방법을 제공합니다.

  • 이미지 사본은 단일 파일의 물리적 사본이므로데이터 파일이나아카이브된 로그, 또는 컨트롤 파일 중에서 복사할 것을 선택할 수 있습니다.

  • 운영 체제 백업과 유사하게 데이터가 들어 있는지의 여부와 상관없이 모든 블록이 복사된다는 점에서 비슷하긴 하지만, 오라클 서버 프로세스는 파일을 복사하고 훼손된 블록을 검사하고   컨트롤 파일 내에 사본을 등록하는 등의 추가 작업을 수행한다는 점에서 완전히 같지는 않습니다.

  • 파일 사본이 항상 모든 블록을 포함하고 있기 때문에 완전 백업, 또는 증분 레벨 0 백업의  일부일 수 있습니다. 사본이 증분 백업 세트와 연결지어 사용될 것이라면 레벨 0 옵션을 사용하십시오.
사용자 삽입 이미지


Copy 명령
파일의 이미지를 생성하려면 run 명령을 사용하십시오. 다음은 명령의 분해 조각입니다:

  • “Run”: copy 명령이 실행되는 방법입니다.
  • “Allocate channel”: 데이터를 출력 장치로 쓰는데 필요합니다.
  • “Copy”: 실제로 이미지 복사를 수행하는 명령입니다.
  • “Level 0”: 사본이 증분 백업 세트에 사용되도록 지정합니다.
  • 데이터 파일 구문은 어느 데이터 파일이 이미지 사본을 필요로 하는지를 나타냅니다. 이 경우데이터 파일3은 'file3.dbf'란 파일 이름으로 "/disk1/backup/" 위치로 복사됩니다(데이터 파일1 과 유사).
  • 더 이상 필요하지 않은 프로세스를 해제하려면 “release channel”를 수행해야 합니다. 이것을 수행하지 않으면 RMAN이 자동적으로 이것을 실행합니다.
  • 괄호는 “run” 명령의 끝을 나타냅니다.

Note:run 명령은 데이터베이스가 마운트되거나 데이터베이스가 열려져 archivelog 모드인 경우에만 작동합니다.

Backup Manager
Backup Manager를 사용하여 이미지 사본을 생성하려면 OEM 작업 스케줄러를 사용합니다. 이것은 대상 데이터베이스를 포함하고 있는 호스트 상에 intelligent agent를 필요로 합니다. 단계는 다음과 같습니다:

  1. Backup Manager의 Oracle8 Recovery Manager sub-system을 사용하여 대상 데이터베이스에 접속
  2. Navigator 창에서 복사할데이터 파일, 또는 컨트롤 파일 선택
  3. Backup -> "Image Copy …" 메뉴 항목 선택
  4. “ImageCopy” 다이얼로그 박스에서 복사할 대상과 원하는 채널의 이름을 지정합니다. 태그 이름이나 작업 스케줄링 옵션 같은 필요한 세부 사항을 지정합니다.
  5. 작업이 OEM 콘솔에 생성될 것입니다.
  6. 이제 파일의 사본이 Backup Manager Detail 창에 이미지 사본으로 나타날 것입니다.
사용자 삽입 이미지

이미지 사본 병렬화
기본으로 RMAN은 각 copy 명령을 직렬로 수행합니다. 하지만 다음과 같은 경우 병렬화가 달성될 수 있습니다:

  • 여러 파일에 한 copy 명령이 사용되는 경우
  • 많은 채널이 할당된 경우

예에서는 네 채널이 생성되었으나 세 채널만이 사용될 것입니다(채널 d4는 유휴(idle) 상태로 남아 있을 것입니다).

 다음은 명령이 실행되는 방식입니다:

  1. 디스크에 쓰기 위해 네 채널이 생성됩니다: d1, d2, d3, d4
  2. 첫번째 copy 명령이 세 채널을 사용합니다(서버 프로세스) - 각데이터 파일을 디스크로 쓰는데 사용됩니다.
  3. 두번째 copy 명령은 앞의 copy 명령이 실행을 끝낼 때까지 실행되지 않습니다 - 채널 하나가  사용됩니다.

Note:병렬도가 높을수록 더 많은 기계 자원을 사용합니다. 하지만 더 빠릅니다.

사용자 삽입 이미지

이미지 복사 프로세스
RMAN이 파일을 복사할 때는 다음 프로세스를 따릅니다:

  1. 서버 프로세스는 한 순간에 하나의 파일에 작동합니다.
  2. 블록을 건너 뛰지 않아 모든 블록이 포함됩니다.
  3. 훼손을 검사합니다.
    Note: 모든 이미지 복사가 완료될 때 V$COPY_CORRUPTION을 질의해야 합니다.
  4. 복사 무결성을 검증하기 위해체크섬(checksum)을 계산합니다.
  5. 파일의 새로운 사본에 대해 파일 헤더도 쓰여집니다.
  6. 사본에 대한 엔트리는 컨트롤 파일에 기록됩니다.
사용자 삽입 이미지

운영 체제 사본
RMAN에서 만들어지지 않은 운영 체제 파일 사본도 적합한 이미지 사본입니다만 RMAN에서 “catalog” 명령을

사용할 때까지 인식되지 않습니다. “hot”(open)과 “cold”(closed) 오라클 백업 모두 지원됩니다.

하드웨어 이중화(Hardware Mirrorring)
이미지 사본은데이터 파일이 이중화된 디스크 볼륨에 저장되는 시스템을 지원합니다.

이중화된 디스크에 문제가 발생하면 이중화는 깨어져 복원하지 못할 수도 있습니다.

이중화가 resilver되면 “change . . .delete” 명령을 사용하여 이미지 사본을 제거하십시오.

그러면 RMAN은 “catalog” 명령을 사용하여 새로운 이미지 사본을 인식할 수 있게 될 것입니다.

사용자 삽입 이미지

백업 세트
백업 세트는 디스크나 테이프 상에 오라클 독점 형식으로 저장된 하나 이상의 물리적 파일로 구성되어 있습니다.

각 물리적 파일은 하나 이상의 오라클 파일을 포함할 수 있습니다. 백업 세트에는 다음 두 가지 유형이 있습니다:

  • 데이터 파일: 데이터 파일과 컨트롤 파일을 포함할 수 있습니다. 하지만아카이브된 로그는 포함할 수 없습니다.
  • 아카이브된 로그: 아카이브된 로그를 포함합니다. 하지만데이터 파일이나 컨트롤 파일은 포함할 수 없습니다.

Note:백업 세트는 일반적으로 디스크 상에서 사용 가능한 이미지 사본과는 달리 복구를 수행하기 전에

RMAN에 의해 복원되어야 합니다.

데이터 파일 백업 세트의 컨트롤 파일
백업 세트의 각 파일은 동일한 오라클 블록 크기를 가져야 합니다(컨트롤 파일과데이터 파일은 동일한 블록 크기를

가지는 반면아카이브된 로그 블록은 기계에 종속적입니다). 컨트롤 파일이 포함되는 경우 컨트롤 파일은 마지막

데이터 파일백업 세트에 쓰여집니다. 컨트롤 파일도 다음 중 한 가지 방법으로 백업 세트에 포함될 수 있습니다:

  • “include control file” 구문을 사용하여 명시적으로
  • 파일 1을 백업하여 암시적으로(시스템데이터 파일)
사용자 삽입 이미지

백업 세트 구문
backup 명령은 일반적으로 스크립트나 run 명령을 통해 실행됩니다:

   backup <Level> ( <Backup Type> <Options> );
     
   단:             Level                    Full
                                                Incremental Level = [ 0 | 1 | 2 | 3 ]
                    Backup Type       datafile<quoted_string>
                                                datafilecopy [tag] <quoted_string>
                                                tablespace <quoted_string>
                                                database
                                                archivelog all
                                                current controlfile
                                                controlfilecopy <quoted_string>
                                                backupset <key>
                   Options                 tag=<quoted_string>
                                                format=<quoted_string>
                                                include current controlfile
                                                filesperset=<integer>
                                                channel <name>
                                                delete input
                                                skip [offline | readonly | inaccessible]

Note

  • 이것이 완전한 구문인 것은 아닙니다.
  • 완전 백업과 레벨 0 백업은 논리적으로는 아니지만 물리적으로는 똑같습니다. 완전 백업은 정상적인 증분 백업 전략에는 영향을 주지 않을 것입니다.
  • “Tag”는 각 백업 세트, 또는 조각에 이름을 주게 됩니다.
  • “Format”는 각 백업 세트에 위치와 고유 이름을 지정합니다. 값은 다음과 같습니다:
    -  %p : 백업 세트 내의 백업 조각의 번호. 이 값은 각 백업 세트에 대해 1부터 시작하여  생성된 각 조각마다 1씩 증가합니다.
    -  %s : 백업 세트 번호. 컨트롤 파일 내의 카운터이며 각 백업 세트에 대해 1씩 증가합니다. 카운터 값은 1에서부터 시작하며 컨트롤 파일의 수명 내내 유일합니다.
    -  %d : 대상 데이터베이스 이름
    -  %n : 부연된(padded) 대상 데이터베이스 이름
    -  %t : 백업 세트 스탬프.4 바이트 값으로 고정된 참조 날짜와 시간 이래 지난 초로  나타납니다.
    -  %u : 백업 세트 번호와 백업 세트가 생성된 시간을 압축하여 표현해 주는 8 문자의 이름입니다.
  • “Include current controlfile"은 백업 세트에 컨트롤 파일을 포함시킵니다.
  • “Filesperset”은 각 백업 세트에 포함된 오라클 파일 수를 나타냅니다.
  • “Channel”은 백업 세트에 대해 사용될 채널을 지정합니다.
  • “Delete input”은 백업된 후 OS, 컨트롤 파일, 그리고 복구카탈로그(RecoveryCatalog)로부터 제거될아카이브된 로그를 지정합니다.
  • “Skip”은 백업 세트에 포함시키지 않을 오라클 파일 유형을 결정합니다.
  •  
    사용자 삽입 이미지

백업 세트 특성
백업 세트는 다음과 같은 특성을 갖는 논리적 구조입니다:

  • 보통 하나의 물리적 파일(백업 조각)을 갖습니다만 여러 개 일수도 있습니다.

  • tape streaming을 돕기 위해 백업 명령에 의해 생성됩니다.   파라미터 “filesperset”은 백업 세트에  포함된데이터 파일의 수를 제어합니다.

  • 큰 데이터베이스에서는 백업 세트가 단일 테이프 릴이나 물리적 디스크, 또는 운영 체제  파일에 대한 최대 값을 초과할 수도 있습니다. 그러므로 각 백업 세트 조각의 크기는 제한될 수 있습니다(“set limit channel kbytes”).

  • 디스크나 테이프에 쓰여질 수 있습니다. 오라클은 media manager를 사용하여 테이프 장치에 쓰기를 하는 SBT_TAPE(System Backup to Tape) 이라 알려진, 대부분의 플랫폼에서 기본으로 하나의 테이프 출력을 제공합니다. 그렇지 않은 경우 오라클은 디스크에 순차적으로 쓰기를 함으로써 테이프 장치를 시뮬레이트합니다.

  • 복원 작업은 복구하기 전에 백업 세트로부터 파일을 발췌해야 합니다.

  • 아카이브된 로그 백업 세트는 증분(incremental)일 수 없습니다(기본값은 완전(full)입니다).

  • 데이터 파일 내에 빈 블록을 포함시키지 않음으로써 압축을 수행합니다.

Backup Manager
Backup Manager를 사용하여 백업을 생성하려면 OEM 작업 스케줄러가 사용됩니다. OEM 작업 스케줄러는 대상

데이터베이스를 포함하고 있는 호스트 상에 intelligent agent를 필요로 합니다. 단계는 다음과 같습니다:

  1. Backup Manager의 Oracle8 Recovery Manager sub-system을 사용하여 대상 데이터베이스에 접속
  2. Navigator 창에서 데이터베이스나 테이블스페이스, 데이터 파일, 또는아카이브된 로그 폴더 선택
  3. Backup -> "Backup Set…" 메뉴 항목 선택(아니면 좀 더 자동화된 접근 방법으로 "Backup   Wizard…" 항목을 사용할 수도 있습니다.)
  4. "BackupSet" 다이얼로그 박스에서 다음 지정:
    -  증분 레벨(기본값 0)
    -  백업에 컨트롤 파일 포함시킬 것인지 여부
    -  세트의 각 백업 조각에 대한 태그(tag) 이름
    -  백업 세트에 필요한 모든 채널(최소한 하나)
    -  필요한 파라미터, 또는 작업 스케줄러 정보
  5. OK 버튼 클릭
  6. OEM 콘솔에서 작업이 생성될 것입니다.
  7. 백업 세트에 포함된 모든데이터 파일이 Backup Manager Detail 창에 나타납니다. 이 백업 세트 정보는 데이터베이스나 테이블스페이스, 또는아카이브된 로그 레벨에서는 사용 가능하지 않습니다.
사용자 삽입 이미지

다중화된 백업 세트
하나 이상의 파일이 동일 백업 파일이나 조각에 쓰여질 경우 RMAN은 자동적으로 채널에 파일을 할당하고 파일을

다중화하며 사용되지 않은 블록은 건너 뜁니다. 동시에 백업할 충분한 수의 파일을 제공하여 고성능

순차 출력 장치(예, 빠른 테이프 장치)가 스트림(stream)될 수 있습니다. 이것은 다른 온라인 시스템 자원과 경쟁을

해야만 하는 백업에는 중요합니다. 테이프 장치가 위치한 대상 데이터베이스 상의 테이프를 변경하는 것은 운영자,

또는 스토리지 서브시스템의 책임입니다.
이 프로세스는 테이프에 쓰도록 설계되었으나 디스크에 쓰는데 사용될 수도 있습니다.

예(슬라이드로부터)
   RMAN> run { allocate channel c1 type ‘SBT_TAPE’;
      2>      backup (database filesperset = 3) ; }
데이터베이스는 하나의 물리적 파일 (세트)로 함께 다중화되어(filesperset = 3) 테이프에 저장될데이터 파일세 개를

포함하고 있습니다. 데이터 파일은데이터 파일1로부터 블록의 ‘n’ 번호를 씀으로써 다중화되고데이터 파일2,

데이터 파일3, 다시데이터 파일1 순으로 모든 파일이 백업될 때까지 진행합니다.

사용자 삽입 이미지

백업 세트 병렬화
다음을 통해 백업 세트가 병렬화 됩니다:

  • 다중 채널 할당
  • 백업할 많은 파일 지정
  • backup 명령에 filesperset 옵션 지정. filesperset이 지정되지 않으면 오직 한 채널만이 사용되어 모든 파일을 포함하는 백업 세트 한 조각을 생성합니다. 다른 채널은 유휴(idle) 상태로 남아 있게 됩니다.

Note: 여러 개의 테이프 장치(비슷한 구성을 가진)가 똑같이 일을 하고 거의 같은 시간에 일을 끝내도록 하려면 백업할데이터 파일이 각 백업 조각마다 비슷한 수의 데이터 블록을 가져야 합니다. 이 과정에 대한 도움을 받으려면 V$BACKUP_DATAFILE을 사용하십시오.

예(슬라이드로부터)

  • 백업해야 할 파일이 9개 있습니다(데이터 파일1부터 9까지).
  • 시스템에는 테이프 장치가 세 개 있습니다.
  • 데이터 파일은 세심하게 할당되어 각 세트는 (효율을 위해) 거의 같은 수의 백업해야 할 데이터 블록을 갖습니다.
    -  데이터 파일1, 4, 5는 백업 세트 1에 할당됩니다.
    -  데이터 파일2, 3, 9는 백업 세트 2에 할당됩니다.
    -  데이터 파일6, 7, 8는 백업 세트 3에 할당됩니다.
    -  세트 당 파일 3개씩을 그룹지었으므로 “filesperset” 파라미터를 사용할 필요가 없습니다. 세트 당 오라클 파일 3개씩이 쓰여진 물리적 파일 3개가  생길 것입니다. 테이프에 병렬 쓰기를  하려면 채널이 3개 필요합니다.

해결책
위의 요구 사항을 맞추려면 다음 구문을 사용하십시오:
  RMAN> run {
     2> allocate channel c1 type ‘SBT_TAPE’;
     3> allocate channel c2 type ‘SBT_TAPE’;
     4> allocate channel c3 type ‘SBT_TAPE’;
     5> backup
     6> incremental level = 0
     7> format ‘/disk1/backup/df_%d_%s_%p.bus’
     8> (datafile 1, 4, 5 channel c1 tag=DF1)
     9> (datafile 2, 3, 9 channel c2 tag=DF2)
    10> (datafile 6, 7, 8 channel c3 tag=DF3)
    11> (current controlfile);
    12> sql ‘alter systemarchivelog current’; }

Note: 이 명령은 데이터베이스가 마운트되었거나 archivelog 모드로 열려져 있을 때에만 작동합니다.

사용자 삽입 이미지

데이터 파일 백업 세트 프로세스
백업 세트를 생성할 때 RMAN은 다음 단계를 거칩니다:

  1. 세트의 각 파일에 네 개의 메모리 버퍼 할당. 각 버퍼의 크기는 db_block_size * db_file_direct_io_count 에 따릅니다.
     
  2. 백업할 파일을 채널에서 크기에 따라 내림차순으로 정렬

  3. 세트 내의 파일을 checkpoint하고 각 파일 헤더 블록을 복사

  4. 세트 당 하나 이상의 파일이 있는 경우 파일을 함께 다중화

  5. 모든 버퍼에서 블록 포함시킬 것인지 결정:
    -  증분이면 블록 내의 SCN 검사하여 포함할 것인지 확인
    -  완전, 또는 레벨 0이면 블록이 데이터를 포함한 적이 있는지 검사

  6. 훼손된 블록이 발견되며 컨트롤 파일에 저장되며 백업이 완료된 후 V$BACKUP_CORRUPTION을 사용하여 질의할 수 있습니다.

  7. 체크섬(checksum) 계산

  8. 출력 버퍼가 차면 출력 장치로 전송
사용자 삽입 이미지

아카이브된 로그 백업 세트
DBA가 경험하는 일반적인 문제는아카이브된 로그가 백업을 시도하기 전에아카이브로그 목적지로 완전히 복사되었는지 여부를 알지 못한다는 것입니다. Recovery Manager는 컨트롤 파일이나 복구카탈로그(RecoveryCatalog) 정보를 액세스하여 어떤 로그가아카이브되어 복구 도중 복원할 수 있는지를 알 수 있습니다.

아카이브된 로그 백업 세트의 특성

  • 데이터 파일이나 컨트롤 파일이 아닌아카이브된 로그만 포함할 수 있습니다.
  • 항상 완전 백업입니다(백업할아카이브된 로그의 범위를 지정할 수 있기 때문에 증분 백업을  수행하는 로직이 없습니다.)

아카이브된 로그 백업의 예(슬라이드로부터)
이 예는 로그 시퀀스 번호 1056부터 1058까지의아카이브된 로그를 각 백업 조각이아카이브된 로그를 세 개씩 포함하는 백업 세트로 백업합니다.아카이브된 로그는 복사된 후 디스크에서 삭제되며 V$ARCHIVED_LOG 뷰에도 삭제된 것으로 표시됩니다.

사용자 삽입 이미지

 아카이브된 로그 백업 프로세스

  1. 백업 세트의아카이브된 로그는 채널과  크기 순서대로 정렬됩니다.

  2. 데이터 파일 백업 프로세스와 유사합니다.

  3. 시퀀스와 스레드 번호는 참조가 용이하도록 조각에 대한 블록 번호로 대치됩니다.

  4. 블록을 건너 뛰지 않습니다.

  5. 훼손이 검출되면 설정은 종료됩니다.
사용자 삽입 이미지

시나리오

  • 데이터베이스에는 파일이 열 개 있고, 시스템에는 테이프 장치가 두 개 있습니다.
  • 컨트롤 파일을 포함하는 전체 데이터베이스 백업이 필요합니다.
  • streaming을 위해서는 백업 세트 당 파일 세 개가 최선이라고 결정되었습니다.
  • 유지가 쉽도록 백업 세트를 명명합니다.
  • 백업 세트 내의 파일을 나열합니다.
  • 훼손된 블록을 가진 파일이 없도록 검사합니다.

Note

  • DBMS_BACKUP_RESTORE로의 원격 프로시저 호출은 하나 이상의 서버 프로세스에서 병렬로 수행됩니다.
  • Recovery Manager는 각 명령을 수행하고 나서 다음 명령을 실행합니다(즉, 직렬 작업). 병렬화는 단일 백업(또는 복사) 명령에서만 이용됩니다
사용자 삽입 이미지

저장된 스크립트를 이용한 백업
“run” 명령에 전체 구문을 집어 넣는 대신 “create script” 명령을 사용하여 스크립트 파일을 생성하는 방법이 있습니다. 스크립트는 RMAN이 명령을 수행한 후 이 이름을 사용하여 복구카탈로그(RecoveryCatalog)에 저장됩니다.

스크립트 구문
자주 사용하는 운영 체제 명령, SQL 스크립트, “backup”, 그리고 “copy” 명령을 저장하려면 “create script” 명령을 사용하십시오;

   create script <name> {<commands> ;}
      단:            Commands        allocate
                                                  backup
                                                  copy
                                                  recover
                                                  sql
                                                  host

앞의 예를 사용하려면:

  1. “backup.rman”이라는 운영 체제 텍스트 파일을 생성하십시오. "create script" 명령으로 "run" 명령을 대체하고 스크립트 "Backup Example"라 부르십시오:
        create script BackupExample{
        allocate channel t1 type ‘SET_TAPE’;
        allocate channel t2 type ‘SET_TAPE’;
        backup
          filesperset 3
          format ‘/disk1/backup/df_%d_%s_%p’
          tag=SCEN
          (database include current controlfile); }

    Note:
    운영 체제 텍스트 파일을 생성하지 않아도 좋습니다만 오라클은 생성할 것을 권장하는  바입니다.

  2. 그리고 나서 텍스트를 잘라내어(cut) Recovery Manager로 붙여 넣어야(paste) 합니다:
        RMAN>create script BackupExample ( . . . )
        RMAN-03022: compiling command: create script
        RMAN-03023: executing command: create script
        RMAN-08085: created script BackupExample

    이 스크립트는 이제 복구카탈로그(RecoveryCatalog)에는 “Backup Example”로, 운영체제에는“backup.rman”으로 저장됩니다.

  3. 이러한 유형의 백업을 실행하고자 할 때는 다음 “run” 명령을 내리십시오:
        RMAN>run { execute script BackupExample; }    RMAN-03021: executing script: BackupExample
        RMAN-03022: compiling command: allocate
        RMAN-03023: executing command: allocate

    백업 세트 내의 모든 파일을 보려면 다음 명령을 사용하십시오:
        RMAN> list backupset of database tag=SCEN;
        RMAN-03022: compiling command: list
        RMAN-06230: List of Datafile Backups
        RMAN-06231: Key  File  Type         LV  Completion_time
        RMAN-06232: ---- ----  -----------  --  ---------------
        RMAN-06233: 1744    1  Incremental   0  25-NOV-97  ...
        RMAN-06233: 1744    2  Incremental   0  25-NOV-97  ...
        RMAN-06233: 1744    3  Incremental   0  25-NOV-97  ...
        RMAN-06233: 1744    4  Incremental   0  25-NOV-97  ...
        RMAN-06233: 1744    5  Incremental   0  25-NOV-97  ...
        RMAN-06233: 1744    6  Incremental   0  25-NOV-97  ...
        ...

    블록 레벨 훼손을 검사하려면 다음 명령을 사용하십시오:
        SQL>select * from v$backup_corruption where file# in (1,2, . . .);
        no rows selected.

결과

  • 각 세트 당 세 개의 다중 파일을 가진 세트가 네 개 생성됩니다.
  • 채널이 두 개 할당된 경우 이들 세트는 병렬로 생성됩니다. 키워드 filesperset이 포함되지 않으면 채널이 한 개만 사용되며 이 채널은 열 개의 파일 모두를 포함하는 조각 하나를 생성할 것입니다. 다른 채널은 유휴(idle) 상태로 있게 됩니다.

Backup Manager
백업 스크립트는 Backup Wizard를 통해서만 생성만 할 수 있습니다. Backup Manager로는 실행, 편집, 또는 삭제할 수는 없습니다.

사용자 삽입 이미지

백업 조각
“set limit” 구문을 사용하여 지정하지 않는 한 논리적 백업 세트는 보통 하나의 백업 조각을 가집니다. 백업 조각은 하나 이상의 오라클데이터 파일, 또는아카이브된 로그를 포함할 수 있는 하나의 물리적 파일입니다. 백업 세트는 완전 백업, 또는 증분 백업에 대한 백업 조각의 완벽한 세트입니다.

사용자 삽입 이미지

백업 조각 크기
백업 세트가 하나 이상의 조각을 갖는 경우는 다음 명령이 사용되었을 때 입니다:
“Set limit . . .kbytes”: 백업 조각의 최대 크기를 지정합니다.

Backup Manager
이 파라미터는 채널을 생성하거나 수정할 때 설정되어야 합니다.

예(슬라이드로부터)

시나리오
USER_DATA 테이블스페이스는 하나의 테이프 장치로 백업되어져야 합니다. 테이프 장치 당 최대 파일 크기는 4GB입니다.

결과
출력 파일이 4GB보다 작으면 백업 세트에 단 하나의 백업 조각만이 쓰여지게 될 것이며, 출력 파일이 4GB보다 크다면 백업 세트에 하나 이상의 백업 조각이 쓰여지게 될 것입니다. 각 백업 조각들은 산재된 세 파일로부터 함께 블록을 가지게 될 것입니다.

사용자 삽입 이미지

완전, 증분, 누적 백업
데이터 파일백업은 완전, 누적, 또는 증분으로 설정합니다.

완전 백업
모든 블록을 포함하는 하나 이상의데이터 파일을 백업합니다. 완전 백업은 데이터파일이나 이미지 사본,아카이브된 로그, 또는 컨트롤 파일을 포함할 수 있습니다.

증분 백업(레벨 >= 0)
이전 증분 백업 이후로 수정된 블록만을 포함하는 컨트롤 파일, 또는데이터 파일을 백업합니다.

누적 증분 백업(레벨 >= 0)
오라클은 누적 증분 백업을 옵션으로 제공합니다. 이 백업은 복원 도중 필요한 증분 백업의 횟수를 줄여주어 시간을 감소시켜 줍니다. 하지만 누적 백업은 같은 레벨에서 이전 증분 백업이 이미 기록한 정보를 백업하므로 더 많은 공간과 시간이 필요합니다.

사용자 삽입 이미지

증분 백업
증분 백업은 다음과 같은 특성을 가집니다:

  • n > 0 일 때, 증분 레벨 n 백업은 이전 증분(레벨 <= n) 백업 이후 변경된 모든 블록을 복사합니다.
  • 레벨 0 백업 세트, 또는 이미지 사본을 제일 먼저 생성해야 합니다. 증분 백업은 이들 레벨 0  백업에 가해진 변경에 기초합니다.
  • 레벨 0 백업을 수행할 때보다 더 적은 블록을 씁니다.
  • 증분 백업은 레벨 0 백업보다 빠릅니다.
  • 증분 백업의 기본 방식은 누적하지 않음(non-cumulative)입니다.

으뜸 법칙

  • 여러 블록에 많은 갱신이 가해진 경우라면 레벨 0 백업을 수행하십시오.
  • 비교적 적은 수의 블록에 많은 갱신이 가해진 경우라면 (레벨 > 0) 백업을 수행하십시오.
사용자 삽입 이미지

다중 레벨 증분 백업
다중 레벨 증분 백업 기능으로 여러 레벨의 증분 백업을 생성할 수 있습니다. 0부터 4까지의 다섯 레벨을 정의할 수 있습니다.
복원하는 동안에는 어떤 레벨이든지 오직 하나의 증분 백업만이 필요하기 때문에 다중 레벨 증분 백업은 복구 작업을 도와 줍니다.

3 레벨 백업 방식

  • 레벨 0 백업은 증분 백업에 대한 기초로서 수행됩니다.
  • 이 순간부터 앞으로 매달 레벨 1 증분 백업은 마지막 월별 백업 이후로 변경된 데이터 블록만 복사합니다.
  • 매주 레벨 2 백업은 마지막 주별 백업 이후로 변경된 블록만 복사합니다.
  • 매일 레벨 3 백업은 마지막 일별 백업 이후로 변경된 블록만 복사합니다.
  • 만약 세번째 달, 두번째 주, 4일째에 파일을 잃어 버렸다면 3월, 2주, 4일 째의 백업을 복원해야만 합니다.
사용자 삽입 이미지

백업의 예
여러분은 계속해서 자라나는 100GB 데이터베이스를 유지하고 있습니다. 기존 하드웨어에 기초하여 여러분은 전체 데이터베이스의 오픈 백업이 4 시간 걸린다고 판단합니다. 데이터베이스는 일주일에 7일 24시간 온라인 상태이므로 백업은 백업하는 동안 시스템 자원을 너무 많이 소비합니다. 그러므로, 레벨 0 백업은 일주일에 한번 이상 수행할 수 없습니다. 하지만 문제가 발생했을 경우에는 빨리 복구를 해야 합니다. 고로 여러분은 다음 백업, 복구 전략을 쓰기로 결정합니다:
레벨 0 백업은 작업이 가장 적은 날을 택해 매주 수행합니다. 여러분은 이 날을 일요일로 정합니다.

    RMAN>run { allocate channel c1 type disk
       2>    format = ‘/home/disk1/user4/BACKUP/sun_%s_%p.bus’;
       3>    backup incremental level = 0 (database); }

증분 레벨 2 백업은 격일간으로 수행합니다. 다만 수요일은 제외합니다. 이러한 백업은 이전 날부터 변경된 블록만 복사하기 때문에 빠릅니다:

    RMAN>run { allocate channel c1 type disk
       2>    format = ‘/home/disk1/user4/BACKUP/inc_%s_%p.bus’;       3>    backup incremental level = 2 (database); }

수요일은 데이터베이스 작업이 다소 적은 날이어서 복구 속도에 도움을 주기 위해 일요일 이후 변경된 모든 블록을 복사합니다. 예를 들어 금요일에 문제가 발생하면 복원하는데 일요일, 수요일, 그리고 목요일 백업만 필요하게 됩니다(월요일과 화요일 분은 필요 없습니다):
    RMAN>run {allocate channel c1 type disk
       2>    format = ‘/home/disk1/user4/BACKUP/wed_%s_%p.bus’;       3>    backup incremental level = 1 (database); }

목요일에는 완전 백업에 의해 증분이 대체됩니다. 이것이 백업 기본 레벨을 바꾸지는 않으므로 금요일의 백업은 수요일 이후의 변경 사항을 복사합니다. 그러므로 백업은 다음 레벨 0 전에  버려질 수 있습니다. 실수로 목요일의 백업이 레벨 0 이었다면 금요일의 백업은 목요일 이후의  모든 변경된 블록을 복사하며, 새로운 기본 레벨입니다. 이제 이 백업은 다음 레벨 0까지  유지되어야 합니다.

Note: 증분 레벨은 Backup Manager의 “Backup Set…”이나 “Backup Wizard…”를 사용하여 설정할 수 있습니다. 하지만 누적 레벨은 그렇지 않습니다.

사용자 삽입 이미지

누적 증분 백업
누적 증분은 다음과 같은 특성을 갖습니다:

  • 누적 증분 레벨 n 백업(단 n > 0)은 증분 레벨이 n보다 작은 이전 백업 이후 변경된 모든 블록을 복사합니다.
  • 누적 증분 백업은 같은 레벨에서 이전에 백업된 블록을 백업합니다. 그러므로 누적 증분 백업은 누적하지 않는(non-cumulative) 백업보다 더 오래 걸리고, 더 많은 블록을 쓰며 더 큰 백업 파일을 만들어 냅니다.
  • 복구할 때 각 레벨에서 더 적은 백업이 적용되므로 복구 속도를 향상시키기 위해 제공됩니다.

예(슬라이드로부터)
누적 백업은 동일 레벨에서 이전 증분 백업이 이미 복사한 변경 사항을 복제합니다. 그러므로 증분 레벨 2 백업이 수행되면 다음 누적 레벨 2는 새로 수정된 모든 블록과 증분 레벨 2에 의해 백업된 블록을 백업할 것입니다. 이것은 완벽한 복구를 하는데 동일 레벨의 증분 백업이 단 하나만 필요하다는 것을 의미합니다.
    RMAN>run {allocate channel c1 type disk
       2>  format = ‘/home/disk1/user4/BACKUP/cum_%s_%p.bus’;       3>  backup incremental level = 2 cumulative (database) ; }

사용자 삽입 이미지

백업 제약 조건
RMAN을 사용하여 백업을 수행하려면 다음을 알아야 합니다:

  • 대상데이터베이스 인스턴스는 RMAN이 접속할 수 있어야 합니다. 복구카탈로그(RecoveryCatalog)를 사용하지 않았다면 데이터베이스는 마운트 되거나("cold" 백업의 경우) 열려져야 ("hot" 백업의 경우) 합니다.

  • 온라인 리두 로그의 백업은 지원되지 않습니다. 리두 로그를 백업해야 한다면 데이터베이스는  archivelog 모드로 실행되어야 합니다.

  • 대상 데이터베이스가 noarchivelog 모드라면 “clean"테이블스페이스와 데이터 파일 백업을 수행할 수 있습니다(즉, "offline normal", 또는 "read only" 테이블스페이스의 백업).데이터베이스 백업은 데이터베이스가 처음으로 깨끗하게 종료되고 mount 모드로 재시작된 경우에만 수행할 수 있습니다.

  • 대상 데이터베이스가 archivelog 모드이면 "current"데이터 파일만을 백업할 수 있습니다  (복원된데이터 파일은 복구에 의해 current가 됩니다).

  • RMAN은 파라미터 파일이나 패스워드 파일, 또는 OS 파일을 백업하지 않습니다.

  • 복구카탈로그(RecoveryCatalog)가 사용되면 복구카탈로그(RecoveryCatalog) 인스턴스는 열려져야만 합니다.
사용자 삽입 이미지

Recovery Manager 작업 kill
Recovery Manager는 성공적으로 완료된 컨트롤 파일의 백업 세트만을 기록합니다. RMAN 작업이 비정상적으로 종료되면 불완전한 파일이 운영 체제에 존재하게 될 것이므로 이것을 제거해야 하며 RMAN은 이들을 사용하지 않을 것입니다. RMAN 작업을 종료하려면:

  • RMAN이 대화식으로 실행하고 있다면 <CTRL>-<C> 사용
  • RMAN이 일괄 처리 모드로 실행하고 있다면 RMAN 프로세스를 죽입니다(kill). 병렬로 종속성이 있을  수 있으므로 채널을 종료할 때는 조심하십시오.

컨트롤 파일을 자주 백업
데이터 파일을 백업할 때마다 항상 컨트롤 파일을 백업하십시오. 왜냐하면 컨트롤 파일을 복원하여야 하는데 증분 백업이 수행되었다면, 컨트롤 파일을 복구하기 위해서는아카이브된 로그를 복원해야만 하기 때문입니다. 이런 경우 복구 속도를 향상시키는 증분 백업 수행의 이점이 많은 수의아카이브로그를 이용하는 경우 보다도 더 사라지게 됩니다.

Note
:  데이터 파일1로 백업 세트를 생성하는 것은 컨트롤 파일을 포함합니다.

훼손

  • 오라클8은 백업 파일을 사용할 수 없게 만들거나 복원된데이터 파일을 훼손하게 되는 작업을 수행하려는 시도를 검출해 내며 금지합니다.
  • 백업 도중 발생하는 훼손된데이터 파일블록에 대한 정보는 컨트롤 파일과 경고 파일(alert file)에 기록됩니다. 훼손된데이터 파일블록은 여전히 백업에 포함되어 있긴 하지만서버 프로세스에 의해 훼손된 것으로 식별됩니다. 오라클은  훼손된 블록의 주소와 훼손 유형을 컨트롤 파일에 기록합니다. 컨트롤 파일로부터  훼손된 블록을 보려면, 백업세트에 대해서는 V$BACKUP_CORRUPTION을, 이미지 사본에 대해서는 V$COPY_CORRUPTION 을 보십시오.

    데이터 파일백업에 허용되는, 이전에 검출되지 않은 블록 훼손 수를 제한하려면 “set  maxcorrupt” 구문을 사용하십시오:
        RMAN> run {
           2> allocate channel cl type disk;
           3> set maxcorrupt for datafile ‘/disk1/data/oem_01.dbf’ to 1;
           4> copy level 0 datafile ‘/disk/data/oem_01.dbf’
           5>    to ‘/disk1/backup/oem_01.dbf’;
           6> release channel c1; }
  • 어긋난(fractured)블록은 쓰여지고 있는 동안 백업 유틸리티가 읽었는데일관성이 결여된  블록입니다.체크섬(checksum)은 백업 유틸리티가 어긋난 블록을 검출하여 다시 읽어일관성에 부합되는 것을 읽을 수 있도록 합니다. 이것은 기본 작업입니다.
사용자 삽입 이미지

Recovery Manager의 메모리 사용

COPY 또는 BACKUP 명령을 실행할 때 메모리 버퍼가 필요합니다. 다음 가정을 기반으로 하여 메모리를 구성할 수 있습니다.

  • 각 데이터 파일은 네 개의 입력 버퍼를 갖고 각 채널은 네 개의 출력 버퍼를 갖습니다(크기는db_block_size*db_file_direct_io_count에 의해 결정됨).
  • I/O 슬래이브가 사용되지 않는 경우 PGA에서 메모리가 할당됩니다. 그렇지 않으면 대용량 풀에서 메모리를 얻습니다. LARGE_POOL_SIZE 매개변수를 사용하여 대용량 풀을 구성합니다.
  • 운영 체제에서 AIO(비동기 I/O)를 지원하는 경우 다음 매개변수를 사용하여 I/O 슬래이브를 설정합니다.
    - DISK_ASYNCH_IO = TRUE
    - TAPE_ASYNCH_IO = TRUE
    - BACKUP_TAPE_IO_SLAVES = TRUE
  • 테이프 장치 쓰기 전용 슬래이브를 설정하려면 다음 매개변수를 설정합니다. 비동기 I/O를 지원하지 않는 플랫폼에서는 슬래이브 프로세스가 비동기 I/O를 시뮬레이트합니다.
    - BACKUP_TAPE_IO_SLAVES = TRUE
  • I/O 슬래이브를 설정하는 데 추가 프로세스가 필요하므로 이에 따라 자원을 할당합니다.

메모리 사용 보기

적절한 디스크 I/O 슬래이브 수는 2 또는 3입니다. 보다 많은 I/O 슬래이브 수가 필요한 경우 다른 채널을 할당하는 것이 효율적입니다. 메모리 사용은 V$SGASTAT(name = 'KSFQ buffers')를 사용하여 볼 수 있습니다.

대용량 풀에 충분한 공간이 없는 경우 이미 시작된 I/O 슬래이브는 계속되고 나머지 프로세스는 동기적으로 실행됩니다. 대용량 풀에서 공간을 얻을 수 없는 프로세스는 이러한 사항을 대상 데이터베이스의 경고 로그에 보고합니다.

사용자 삽입 이미지

문제 해결
백업 진행 상황을 확인하려면 V$SESSION_LONGOPS를 사용하십시오. 예를 들면:
  SQL>select sid, serial#, context    2  round (sofar/totalwork*100, 2) “% Complete”,
    3  substr (to_char (sysdate, ‘yymmdd hh24:mi:ss’), 1, 15) “Time”
    4  from v$session_longops    5  where compnam = ‘dbms_backup_restore’;

   SID  SERIAL#   CONTEXT   % Complete  Time
  ----  -------   -------   ----------  ---------------
    19       86        46          100  971124 05:13:40
    19       86      1060          100  971124 05:13:40
    29      181        46        10.56  971124 05:13:40

% Complete 가 계속 증가하고 있지 않다면 문제가 될 수 있습니다.
어떤 이벤트가 대기하고 있는지 알아 보려면 V$SESSION_WAIT를 검사해 보십시오:
  SQL> select event, p1text, seconds_in_wait
    2  from v$session_wait
    3  where wait_time = 0;

RPC 호출 시작을 실패
RMAN으로부터의 다음 RPC 에러 메시지는 문제가 아닙니다:
  RMAN-08010: channel c8: including datafile number 6 in backupset
  RMAN-10030: RPC call . . . failed to start on channel c9

이것은 보통 대상데이터베이스 인스턴스가 느리다는 것을 나타냅니다. 다음   “RPC call OK” 메시지가 나오면 백업이 성공적으로 계속 진행됩니다:
  RMAN-10036: RPC call ok on channel c9
  RMAN-08010: channel c3: including datafile number 18 in backupset

시작부터 정지된(hang) 백업
테이프 백업이 시작된 후, RMAN이 새로운 정보를 출력하지 않으면 V$SESSION_WAIT는 “compnam = ‘dbms_backup_restore’” 문에 대한 어떠한 정보도 보여주지 않을 것이며 백업은 “hung”인 것처럼 보일 것입니다. 이런 상황이라면 media manager를 확인하여 hung 되어 있거나 비정상적 으로 종료되지 않도록 하십시오.
대부분의 media manager는 sbtio.log 파일에 정보를 쓰므로 더 이상의 정보를 얻고자 하면 이 파일을 검사하십시오. 이 파일은 $ORACLE_HOME/rdbms/log 디렉토리에 위치하고 있습니다(background_dump_dest나 user_dump_dest가 아닙니다).

테이프 장치가 발견되지 않을 때
다음 에러가 검출되었을 경우 7004 에러는 보통 테이프 장치가 발견되지 않았다는 것을 나타냅니다.
  RMAN-03007: exception occurred ... error is retryable
  RMAN-07004: unhandled exception ... on channel c4
  RMAN-10032: unhandled exception ... step 4: ORA-06512: at line 158
  RMAN-10035: exception raised ... operation failed, retry possible
  ORA-19506: failed to create sequential file, name=“f_98_1”,    parms=“ “
  ORA-27007: failed to open file

사용자 삽입 이미지

데이터 딕셔너리
컨트롤 파일을 질의하는데 사용되는 RMAN데이터 딕셔너리뷰는 다음과 같습니다:

  • V$ARCHIVED_LOG: 데이터베이스에 생성되고, 백업되고, 지워진아카이브를 보여 줍니다.

  • V$BACKUP_CORRUPTION: 백업 세트를 백업하는 동안 훼손이 발견된 블록을 보여 줍니다.

  • V$COPY_CORRUPTION: 이미지 복사 도중 훼손이 발견된 블록을 보여 줍니다.

  • V$BACKUP_DATAFILE: 각데이터 파일내의 블록 수를 결정하여 동일한 크기의 백업 세트를 생성하는데 유용합니다.데이터 파일에 대해 훼손된 블록 수를 찾아 줄 수도 있습니다.

  • V$BACKUP_REDOLOG: 백업 세트에 저장된아카이브된 로그를 보여 줍니다.

  • V$BACKUP_SET: 생성되었던 백업 세트를 보여 줍니다.

  • V$BACKUP_PIECE: 백업 세트에 생성된 백업 조각을 보여 줍니다.
사용자 삽입 이미지

 

  관련내용

  참조

  파라미터

 BACKUP_TAPE_IO_SLAVES
 BACKUP_DISK_IO_SLAVES
 DB_BLOCK_SIZE
 DB_FILE_DIRECT_IO_COUNT
 DISK_ASYNCH_IO
 LARGE_POOL_SIZE
 SESSIONS
 TAPE_ASYNCH_IO

  동적 성능 뷰

 V$ARCHIVED_LOG
 V$BACKUP_CORRUPTION
 V$COPY_CORRUPTION
 V$BACKUP_DATAFILE
 V$BACKUP_REDOLOG
 V$BACKUP_SET
 V$BACKUP_PIECE
 V$BACKUP_DEVICE
 V$SESSION_LONGOPS
 V$SESSION_WAIT
 V$SGASTAT

  데이터 딕셔너리 뷰

 None

 명령어

 RUN { <commands>; }
 ALLOCATE CHANNEL <name> TYPE DISK;
 COPY <datafile> TO <location>;
 BACKUP <level> ( <Backup Type> <Options>  )
 CREATE SCRIPT { <commands>; }
 RUN { EXECUTE SCRIPT <name>; }


크리에이티브 커먼즈 라이선스
Creative Commons License

오라클과 NLS(2)

Dev 2006/04/21 01:22 |
이번 회는 캐릭터셋을 변경하는 작업에 대한 준비와 그 방법에 대한 것을 알아보고자 한다. 캐릭터셋 변경은 미래의 시스템 확장과 개발의 용이함을 위해 권장되는 작업이지만, 실제 위험성이 크기 때문에 매우 조심해야 한다. 그래서인지 OTN의 NLS 포럼에서도 빈번히 등장하는 질문이기도 한 "캐릭터셋 변경"에 대해 이번 회에서 집중적으로 다루어보기로 하겠다.

  1. 잘못된 캐릭터셋을 사용해온 시스템, 치료해야 하나?
    1. 캐릭터셋 오용의 예
    2. 권장 사항
      1. 1. 바로잡기의 필요성을 인식하라
      2. 2. 절대 함부로 변경하지 말라
  2. 캐릭터셋을 변경 방식과 위험성
    1. 캐릭터셋 변경이란?
      1. 변경 케이스 1) 캐릭터셋 딕셔너리 정보 변경 + 데이터 불변
      2. 변경 케이스 2) 캐릭터셋  딕셔너리 정보 변경 + 데이터 변경
    2. 캐릭터셋 변경의 위험성
      1. 1) 데이터 절삭
      2. 2) 데이터 깨짐
        1. US7ASCII 데이터베이스의 한글 데이터를 exp/imp를 이용하여 KO16MSWIN949 또는 UTF8로 마이그레이션
        2. KO16KSC5601 데이터베이스의 한글 데이터를 exp/imp를 이용하여 KO16MSWIN949 또는 UTF8로 마이그레이션
      3. 3) 애플리케이션 오동작
  3. 캐릭터셋 변경의 실제
    1. ALTER DATABASE 명령을 이용한 딕셔너리 변경
      1. 절차
    2. 데이터 마이그레이션
      1. 준비작업
      2. CSSCAN 실행
        1. 1) CSSCAN 설치
        2. 2) CSSCAN 구동
      3. exp/imp를 이용한 변경
      4. CSALTER를 이용한 변경
  4. 글을 마치며


연.재.순.서.

1회 : 오라클과 NLS의 찰떡궁합 들여다보기(1)
2회 : 오라클과 NLS의 찰떡궁합 들여다보기(2)
3회 : 오라클 GDK를 사용하여 깔끔한 다국어 개발 유틸리티를 만들자
4회 : 한글화된 오라클 제품, 그 이면의 비밀.

잘못된 캐릭터셋을 사용해온 시스템, 치료해야 하나?

캐릭터셋 오용의 예

캐릭터셋과 전혀 맞지 않는 데이터를 저장하고 검색할 수 있을까? 정답은 당연히 없다. 그렇지만, 현실적으로 많은 시스템의 데이터베이스에 그런 데이터가 저장되어 있을 것이라고 믿고 있다.
  1. 오용 1) US7ASCII 데이터베이스에 한글을 저장한 경우
  2. 오용 2) US7ASCII 데이터베이스에 한글, 중국어, 일본어 등 다양한 언어를 저장한 경우
  3. 오용 3) KO16KSC5601 데이터베이스에 확장 한글(비완성형)을 저장한 경우
이 세 가지 경우는 잘못된 캐릭터셋을 사용하고 있는 경우 중 문제가 발생할 만한 대표적인 경우들이다. 다음 표는 이 경우들에 대해 발생할 수 있는 문제점과 취할 수 있는 치료방법을 요약하고 있다.


문 제 감시 시점
지 속적 사용 가능 여부
해 결책
오용1
1) 다른 정상적인 데이터베이스와 DBLINK로 연결시 한글 전달 실패
2) 캐릭터 관련 함수들의 예측할 수 없는 결과
3) 다른 캐릭터셋으로 변경 시도시 데이터 마이그레이션 실패
4) exp/imp를 사용해 다른 데이터베이스로 데이터 마이그레이션 실패
1) 똑같이 잘못된 구성을 가진 데이터베이스 집합 시스템에서는 지속 운영 가능

2) 정상적인 데이터베이스와 정상적인 통신은 불가능함

3) 정상적은 데이터베이스로 업그레이드시 난관 봉착
Full Backup이 필요없다고 절대 말하지 말라!

오라클 엔지니어로 하여금 캐릭터셋을 강제로 KO16MSWIN949로 변경하게 함으로써 해결할 수 있다.

단, 변경 전에 정말 데이터들이 순수하게 KO16MSWIN949라는 한 캐릭터셋으로 커버되는 것이 맞는지 확인해야 한다. 그렇지 않으면 "예 2"의 경우가 된다.

이런 캐릭터셋 변경은 오라클 엔지니어나, 지원 계약을 맺은 고객이 충분히 엔지니어로부터 위험성에 대해 설명을 들은 후에야 시도할 수 있다는 사실을 잊지 말기 바란다. 그렇지 않은 시도는 누구도 책임져 주지 않는다.

캐릭터셋 변경 후 애플리케이션의 디버깅은 필수이다.
오용2
1) 다른 데이터베이스와 DBLINK로 연결시 한글 전달 실패
2) 캐릭터 관련 함수들의 예측할 수 없는 결과
3) 다른 캐릭터셋으로 변경 시도시 실패
4) exp/imp를 사용해 다른 데이터베이스로 데이터 마이그레이션 실패
1) 똑같이 잘못된 구성을 가진 데이터베이스 집합 시스템에서는 지속 운영 가능

2) 정상적인 데이터베이스와 정상적인 통신은 불가능함

3) 정상적은 데이터베이스로 업그레이드시 난관 봉착
최악의 케이스. 다양한 캐릭터셋이 한자리에 모인 잔치집으로, 어떤 한 캐릭터셋으로의 강제 변환이 의미없다.

그나마 각 언어별로 데이터를 구분하여 조회할 수 있다면, 한국어는 한국어별로, 중국어는 중국어별로 physical OS file로 덤프를 떠서 해결하기를 권장한다.

각 언어별로 Manual하게 고쳐나가는 것 밖에는 도리가 없다.

Physical dump file은 SQL Loader를 통해 새로운 UTF8 데이터베이스에 안전하게 로딩할 수 있다. 물론 지난 호에 나온 대로 각 언어별로 NLS_LANG변수값은 확실히 설정해주어야 한다.

SQL Loader를 사용할 때 NLS_LANG값은:
  • 한국어 파일 업로드시 : .KO16MSWIN949
  • 중국어간체 : .GB2312
  • 중국어번체 : .ZHT16MSWIN950 또는 .BIG5

이런 내용은 자신이 없거나, 책임질 수 있는 위치가 아니면, 절대 함부로 시도해서는 안된다.

데이터베이스를 제대로 고쳤을 경우에는 애플리케이션도 재개발 수준의 디버깅을 해야 한다.
오용3
1) exp/imp를 사용해 다른 데이터베이스로 데이터 마이그레이션 실패
2) 가끔 이런 데이터베이스를 기반으로 한 애플리케이션에서 깨진 글자들을 발견하게 됨(종종 우리나라 게시판들에서 볼 수 있는 현상)
3) 운영자들이 자신의 시스템은 정상이라고 생각하는 경우가 많이 감지가 어려움
4) KO16KSC5601(완성형)이 우리나라 한글을 대표하는 캐릭터셋이라는 인식이 문제
1) 일부 비완성형 한글 데이터들의 처리를 주의하면 지속 사용할 수 있고, ,다른 KO16MSWIN949, UTF8기반의 정상적인 한글 데이터베이스와도 어느 정도 통신이 가능하다

KO16KSC5601에서 KO16MSWIN949로 캐릭터셋을 변경한다. KO16MSWIN949는 KO16KSC5601의 수퍼셋이므로 이러한 변경은 자연스럽다. 가장 쉽게 해결될 수 있는 케이스라고 할 수 있다.

그렇지만, 여전히 캐릭터셋 변경이라는 것은 위험하므로, 반드시 지원을 받아야 하는 것은 필수이다.


권장 사항

1. 바로잡기의 필요성을 인식하라

잘못된 캐릭터셋을 가진 데이터베이스를 계속 사용하는 것은 결국 잘못된 애플리케이션 개발을 유도해 전체적으로 잘못된 요소들만으로 이루어진 시스템이 되고 만다. 초기 구성의 부적절함으로 인해 특히 개발자가 고생할 수 있다. 개발자의 입장에서는 KO16MSWIN949나 UTF8기반이 아닌, US7ASCII 데이터베이스를 기반으로 한글을 입출력하는 프로그램을 개발하도록 강요받는다면 결국 억울하게 부가적인 업무를 전가받는 것이다.

캐릭터셋 변경은 비단 데이터베이스 자체 뿐 아니라, 애플리케이션을 비롯한 전체 시스템에 영향을 주기 때문에 경우에 따라 단순한 작업이 아니며, 짜투리 시간을 활용하여 감행할 수 있는 사항이 아니다. 적절한 시기를 선택해야 한다.

여러분이 자바 기반의 웹 개발자라면 다음과 같은 코드를 사용하여 개발한 적이 있는가?

저장:
String p_UserComment = new String(request.getParameter("comment").getBytes("KSC5601"),"8859_1");
pstmt.setString(1,p_UserComment);
pstmt.executeUpdate();

조회:
if(resultSet.next())
{
    String v_UserComment = new String(resultSet.getString("comment").getBytes("8859_1"),"KSC5601");
..
{

이런 억지스런 인코딩의 변환이야말로 잘못된 구성으로 인해 개발자들이 부가적으로 고생하는 단적인 예이다.  한글을 쓰든, 중국어든, 베트남어 기반의 애플리케이션이든, 올바른 구성의 시스템상에서는 저런 식의 코드 변환이 필요할 리가 없다.

꼬여진 개발은 결국 가까운 미래에 "확장 불가", "마이그레이션 불가"라는 벽에 부닥칠 수 있다.

2. 절대 함부로 변경하지 말라

 여러분들 중에 함부로 Production(Live) Database를 변경할 만한 경솔함과 만용을 가진 사람이 있을 거라고 생각해서 이를 언급하는 것은 아니다. 하지만, 제아무리 실력이 뛰어난 DBA라고 할 지라도, 데이터 전체를 무력화 시킬 수 있는 작업을 홀로 감행하지 않기를 거듭 강조하고 싶다. 반드시 적어도 다음 사람들이 연관되고 협력해야 한다
  • 해당 사용업체의 DBA
  • 의사결정 권한을 가지고, 시스템 전체의 운영을 책임지는 책임자
  • 오라클의 지원 엔지니어

캐릭터셋을 변경 방식과 위험성

캐릭터셋 변경이란?

일반적으로 우리가 "캐릭터셋을 변경한다"고 표현하는 작업은 사실 다음 두 가지 작업을 동시에 의미한다. 따라서, 이 말이 사용될 때에는 다음 두 가지 중 어떤 의미를 가리키는지 확실히 짚고 넘어갈 필요가 있다.

변경 케이스 1) 캐릭터셋 딕셔너리 정보 변경 + 데이터 불변

캐릭터셋 변경 시도 시점을 기준으로, 데이터베이스에 저장되어 있는 데이터 자체는 전혀 변경하지 않은 채,  딕셔너리에 있는 캐릭터셋 정보만 변경하는 작업이다.  일반적으로 캐릭터셋에 맞게 데이터를 잘 저장시켜 온 데이터베이스에는 사용할 수 없는 방법이다. 예를 들어 KO16MSWIN949 캐릭터셋을 가진 데이터베이스에 한글을 Windows-949의 코드페이지에 맞게 잘 저장하고 사용해 왔다면 이 데이터베이스의 딕셔너리 정보만을 UTF8으로 바꾸어서는 전혀 엉뚱한 결과를 얻게 되는 것이다. 주로 다음과 같은 상황에서 사용할 수 있다.
  • 비어있는(empty) 데이터베이스 인스턴스 : DBCA(Database Configuration Assistants)를 이용해 데이터베이스 인스턴스를 생성할 때, 캐릭터셋을 잘못 선택했다면, 도중에 작업을 중단하는 방법도 있지만(사실 중단하기를 권장한다),  생성을 마친 후 캐릭터셋 정보를 변환함으로써 문제를 해결할 수 있는 경우가 있다. KO16MSWIN949로 생성해야 할 데이터베이스를 KO16KSC5601로 설정하여 생성했다면, 생성을 마친 후 간단한 절차를 거쳐 KO16MSWIN949로 변경할 수 있는 것이다.

  • 캐릭터셋과는 전혀 엉뚱하고도 다른 캐릭터셋으로 인코딩된 정보를 강제 저장해 온 인스턴스 : 이 경우가 상당히 많은데, 데이터베이스 캐릭터셋은 US7ASCII밖에 없는 것으로 생각을 했던 것일까? US7ASCII은 그 이름만 보아도 절대 한글을 저장할 수 없을 것 같은 캐릭터셋인데, 여기에 한글 데이터를 버젓이 저장해온 시스템들이 상당히 많다. 왜 이런 일이 발생할 수 있는가에 대해서는 지난 연재의 NLS_LANG 변수 코너를 읽어보고 생각해 보기 바란다. 캐릭터셋은 US7ASCII이지만, 실제로는 완성형 + 한글 윈도우즈 코드 페이지(KO16MSWIN949)의 데이터를 저장해 온 경우, 데이터는 현재 상태에서 가공하지 않은 채, 딕셔너리에 있는 캐릭터셋 정보만을 강제로 KO16MSWIN949로 변환하는 방식으로 문제를 해결할 수 있다.
"ALTER DATABASE CHARACTER SET" 명령어가 이에 해당하며, 여기에 대해서는 다음 섹션에서 자세한 절차를 다루도록 하겠다.

변경 케이스 2) 캐릭터셋  딕셔너리 정보 변경 + 데이터 변경

캐릭터셋의 변경과 함께 데이터베이스가 가지고 있는 데이터의 인코딩까지도 변경하는 마이그레이션 작업을 포함하는 경우로, "변경 케이스 1"에 비해 상당한 시간이 소요되는 방대한 작업이며 그에 따라 위험성이 크다. 기존 데이터베이스의 백업은 물론 당연하며, 새로운 캐릭터셋을 가진 새 데이터베이스 인스턴스를 생성하여 그곳에 현재 데이터베이스로부터 데이터를 마이그레이션을 하는 것이 가장 안전하다. 이 경우 보통 exp/imp를 사용하여 해결할 수 있다. 현존하는 데이터베이스의 데이터를 직접 변경하려고 할 경우를 대비해 오라클에서는 CSSCAN이라는 툴을 제공하고 있다.

캐릭터셋 변경의 위험성

"내가 책임지고 변경하겠소!"

일반적으로 존재하는 데이터베이스의 캐릭터셋을 변경하는 작업은 상당히 위험하다. 그래서, 현업에서는 누구도 쉽사리 총대를 매고 이 명령어를 수행하려고 들지 않는다. 괜한 용기를 부렸다가는 돌이킬 수 없는, 또는 돌이키기가 너무나 고통스러운 상황을 맞이할 수 있는 게 이런 작업이다.

실제로 데이터베이스 캐릭터셋을 변경할 때 대략 다음과 같은 문제가 발생할 수 있다.

1) 데이터 절삭

 KO16KSC5601이나 KO16MSWIN949 캐릭터셋을 가진 데이터베이스에서는 한글 한 글자당 2바이트의 물리적 공간이 소모된다.  그래서, 대부분의 데이터베이스 스키마 설계시 한글 한 글자를 2바이트로 감안하는 경우가 많다. 예를 들어, 한글 및 영문 50자까지 허용할 수 있는 컬럼의 경우, 대략 컬럼 길이를 설정할 때 100바이트로 설정하게 된다. 이런 상황에서, 다국어 지원의 필요성이 있어, UTF8로 마이그레이션해야 하는 상황을 생각해 보자. UTF8 데이터베이스에서는 한글 한 글자당 3바이트가 소모되므로 당장 100바이트의 컬럼은 50자가 아닌 33자밖에 저장하지 못하는 컬럼으로 전락한다.

다음과 같은 상황을 생각해 보자. KSC5601데이터베이스에 길이 10바이트의 컬럼을 가진 테이블이 있다고 가정하고, 여기에 5자의 한글을 꽉 채워보자.

SQL> CREATE TABLE t (sval VARCHAR2(10), svalchar VARCHAR2(5 CHAR));
SQL> INSERT INTO t VALUES('한국오라클','한국오라클');

C:\Documents and Settings\jwryoo>set NLS_LANG=.KO16KSC5601
C:\Documents and Settings\jwryoo>exp scott/tiger@KSC5601
...
. . exporting table                              T          1 rows exported
Table(T) or Partition(T:P) to be exported: (RETURN to quit) >
...

C:\Documents and Settings\jwryoo>imp scott/tiger@AL32UTF8
. importing SCOTT's objects into SCOTT
. . importing table                            "T"
IMP-00019: row rejected due to ORACLE error 12899
IMP-00003: ORACLE error 12899 encountered
ORA-12899: value too large for column "SCOTT"."T"."SVAL" (actual: 15, maximum: 10)
Column 1 한국오라클
Column 2 한국오라클          0 rows imported
Import terminated successfully with warnings.

마이그레이션하기 전에 반드시 테이블의 컬럼 길이는 비율에 맞게 확장해야 한다.

-- 10 * 3 / 2 = 15
SQL> alter table t
  2  modify sval varchar2(15);

Table altered.

SQL> alter table t
  2  modify svalchar varchar2(15);

Table altered.

..
C:\Documents and Settings\jwryoo>set NLS_LANG=.KO16KSC5601
C:\Documents and Settings\jwryoo>exp scott/tiger@KSC5601
....
Export terminated successfully without warnings.
:\Documents and Settings\jwryoo>imp scott/tiger@AL32UTF8
Enter table(T) or partition(T:P) names. Null list means all tables for user
Enter table(T) or partition(T:P) name or . if done: T

Enter table(T) or partition(T:P) name or . if done: .

. importing SCOTT's objects into SCOTT
. . importing table                            "T"          1 rows imported
Import terminated successfully without warnings.

C:\Documents and Settings\jwryoo>sqlplus scott/tiger@AL32UTF8
SQL> SELECT * FROM t;

SVAL                           SVALCHAR
------------------------------ -----------
한국오라클                      한국오라클

US7ASCII 데이터베이스에 한글을 저장하게 될 경우, 한 글자당 거의 100% 2바이트로 저장된다. 사용자들이 일반적으로 사용하는 한글 클라이언트는 대부분 Windows-949(한글 Windows OS)나 KSC5601 완성형(UNIX계열)이고 이들은 모두 2바이트이기 때문이다. 따라서, 여기에 한글을 저장하므로 마찬가지로 "한글최대길이 * 2"의 길이만큼 컬럼을 지정하게 된다. 하지만, US7ASCII 데이터베이스를 바로잡을 때에는 KO16MSWIN949 캐릭터셋으로 강제변환하게 되므로 데이터에 대한 변경은 없다. 따라서, 이 부분만큼은 크게 문제가 없다고 하겠다. 하지만 역시 USASCII에서 UTF8으로 갈 경우에는, KO16MSWIN949로 강제 변경 후, 데이터 마이그레이션을 해야 하므로, 반드시 스키마 점검을 해야 할 것이다.

2) 데이터 깨짐

데이터 마이그레이션 시 일부 데이터 혹은 전체 데이터가 손상되는 경우가 발생한다. 그래서 특히 타겟 데이터베이스와 동일한 조건의 데이터베이스를 이용해 테스트를 충분히 하는 것은 필수 과정이다. 특히 exp/imp를 이용하여 데이터 마이그레이션을 할 때 이런 현상이 나타난다.
US7ASCII 데이터베이스의 한글 데이터를 exp/imp를 이용하여 KO16MSWIN949 또는 UTF8로 마이그레이션
데이터 전체가 손상된다. 불가능하며 시간낭비이다. 애초에 시도하지 않는 것이 좋다. 일부 DBA들이 꼼수를 사용하고 있으나, 그 위험은 시도하는 본인의 부담이라는 것을 잘 인식해야 한다(at their own risk).
KO16KSC5601 데이터베이스의 한글 데이터를 exp/imp를 이용하여 KO16MSWIN949 또는 UTF8로 마이그레이션
이 경우가 데이터 일부 깨짐 현상이 발생할 수 있는 대표적인 케이스이다. KO16KSC5601은 만능 한글 캐릭터셋이 아니라 완성형 2350자만을 지원한다는 사실은 이미 누누히 강조해 왔을 것이다. 하지만 NLS_LANG=.KO16KSC5601으로 설정함으로써, 많은 데이터베이스가 비완성형 한글 8822자들의 일부를 저장하고 사용해 왔을 것으로 믿는다. 이와 같은 데이터는 정상적인 절차를 거쳐 exp/imp를 이용해 다른 캐릭터셋의 데이터베이스로 마이그레이션이 불가능하다.

C:\Documents and Settings\jwryoo>set NLS_LANG=.KO16KSC5601

C:\Documents and Settings\jwryoo>sqlplus scott/tiger@KSC5601
SQL> select sval from t;

SVAL
---------------
커피숖

C:\Documents and Settings\jwryoo>exp scott/tiger@KSC5601
....

C:\Documents and Settings\jwryoo>imp scott/tiger@AL32UTF8
..
Enter table(T) or partition(T:P) name or . if done: .

. importing SCOTT's objects into SCOTT
. . importing table                            "T"          1 rows imported
Import terminated successfully without warnings.

SQL> sqlplus scott/tiger@AL32UTF8
SQL> select sval from t;

SVAL
------------------------------
커피?

SQL> select dump(sval) from t;

DUMP(SVAL)
-------------------------------------------------

Typ=1 Len=12: 236,187,164,237,148,188,239,191,189

-- 231 191 189 : Corrupted character  (ㅤ숖 :236136150)

이런 경우는 "ALTER DATABASE" 명령어로 KO16KSC5601에서 KO16MSWIN949로 딕셔너리 변경(변경 케이스 1)만을 한 후 작업하면 문제를 피할 수 있다.

3) 애플리케이션 오동작

US7ASCII 데이터베이스에 한글을 저장하고 그것을 기반으로 만들어진 애플리케이션은 과연 캐릭터셋을 올바르게 변경한 후에 안전할 수 있을까? 정답은 물론 "없다"이다. 문자열을 처리하는 연산에서 대혼란이 올 수 있다. US7ASCII 데이터베이스에 저장된 한글의 모습을 들여다보자.

US7ASCII:



SQL> select sval from t;

SVAL
-------------------------------------

한국오라클

SQL> select length(sval) from t;

LENGTH(SVAL)
------------
          10

SQL> select instr(sval,'오') from t;

INSTR(SVAL,'오')
----------------
               5

SQL> select substr(sval,3,2) from t;

SU
--


SQL> select concat(substr(sval,3,3),'좋아') from t;

CONCAT(
-------
국옥종

KO16KSC5601:
KO16MSWIN949:
UTF8, AL32UTF8:


SQL> select sval from t;

SVAL
-------------------------------------

한국오라클

SQL> select length(sval) from t;

LENGTH(SVAL)
------------
           5

SQL> select instr(sval,'오') from t;

INSTR(SVAL,'오')
----------------
               3

SQL> select substr(sval,3,2) from t;

SUBSTR(SVAL,3,2)
----------------
오라

SQL> select concat(substr(sval,3,3),'좋아') from t;

CONCAT(SUBSTR(SVAL,3,3),'좋아')
------------------------------------
오라클좋아

예에서 보는 바와 같이 스트링 연산들이 US7ASCII에서는 왜곡될 수 밖에 없다. 따라서, 애플리케이션 개발자들도 이 왜곡된 결과를 바로잡는 코드를 부가적으로 애플리케이션에 넣도록 강요받게 된다.

US7ASCII:
왜곡된 애플리케이션 코드를 이용한 결과 바로 잡기


SQL> select sval from t;

SVAL
-------------------------------------

한국오라클

SQL> selectlength(sval)/2from t;

LENGTH(SVAL)/2
------------
          5

SQL> select instr(sval,'오') from t;    -- 왜곡된 결과 그대로 사용

INSTR(SVAL,'오')
----------------
               5

SQL> selectsubstr(sval,5,4)from t;

SU
--


SQL> selectconcat(substr(sval,5,6), '좋아')from t;

CONCAT(SUB
----------
오라클좋아

KO16KSC5601:
KO16MSWIN949:
UTF8, AL32UTF8:


SQL> select sval from t;

SVAL
-------------------------------------

한국오라클

SQL> select length(sval) from t;

LENGTH(SVAL)
------------
           5

SQL> select instr(sval,'오') from t;

INSTR(SVAL,'오')
----------------
               3

SQL> select substr(sval,3,2) from t;

SUBSTR(SVAL,3,2)
----------------
오라

SQL> select concat(substr(sval,3,3),'좋아') from t;

CONCAT(SUBSTR(SVAL,3,3),'좋아')
------------------------------------
오라클좋아

보는 바와 같이 원하는 결과를 얻기 위해서는 개발자가 일일이 "한글은 2바이트"라는 가정 하에 바이트 단위의 연산을 해 가며 프로그래밍을 해야 한다.

오라클을 이용해 바이트단위의 프로그래밍을 하는 것은:
- 최신식 오디오 시스템을 구축해 놓고 라디오 가능만 사용하는 것
- 전자동 세탁기를 사 놓고, 손으로 통을 돌려 세탁하는 것

- 가스렌지를 구비해 놓고  성냥으로 불을 켜는 것

대조표의 오른쪽에 있는 것처럼, 한글을 지원하는 데이터베이스의 경우 한글을 2바이트로 저장하든, 3바이트로 저장하든 스트링 연산 함수는 기본적으로 캐릭터 단위로 동작하므로 애플리케이션의 코드가 동일함을 알 수 있다. 잘못된 데이터베이스 캐릭터셋은 잘못된 애플리케이션 코딩을 유도하고, 이는 나중에 제대로 된 데이터베이스 캐릭터셋으로 마이그레이션시 심각한 휴유증을 양산할 소지가 있다. 따라서, 캐릭터셋 변경시 애플리케이션의 스트링 연산 부분을 손보는 것은 필수작업이다.

캐릭터셋 변경의 실제

ALTER DATABASE 명령을 이용한 딕셔너리 변경

앞서 살펴본 "변경 케이스 1"에 해당하는 것으로 데이터에 대한 어떠한 검토나 수정은 없이 딕셔너리의 캐릭터셋 정보만을 교체하는 작업이다. KO16KSC5601을 KO16MSWIN949로 변경할 때 유용하다. 그리고 한글만을 저장해 온 US7ASCII 데이터베이스 또한 이 방식으로 KO16MSWIN949로 변경할 수 있다.

절차

 SQL> SHUTDOWN IMMEDIATE;
<do a full backup>
SQL> STARTUP MOUNT;
SQL> ALTER SYSTEM ENABLE RESTRICTED SESSION;
SQL> ALTER SYSTEM SET JOB_QUEUE_PROCESSES=0;
SQL> ALTER SYSTEM SET AQ_TM_PROCESSES=0;
SQL> ALTER DATABASE OPEN;
SQL> ALTER DATABASE CHARACTER SET KO16MSWIN949;
SQL> SHUTDOWN IMMEDIATE;
SQL> STARTUP;

SQL> SHUTDOWN IMMEDIATE;
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup mount
ORACLE instance started.

Total System Global Area  171966464 bytes
Fixed Size                   777956 bytes
Variable Size             145760540 bytes
Database Buffers           25165824 bytes
Redo Buffers                 262144 bytes
Database mounted.
SQL> ALTER SYSTEM ENABLE RESTRICTED SESSION;

System altered.

SQL> ALTER SYSTEM SET JOB_QUEUE_PROCESSES=0
  2  ;

System altered.

SQL> ALTER SYSTEM SET AQ_TM_PROCESSES=0;

System altered.

SQL> ALTER DATABASE OPEN
  2  ;

Database altered.

SQL> ALTER DATABASE CHARACTER SET KO16MSWIN949;

Database altered.

-- 한편 현재 캐릭터셋의 Superset이 아닌 캐릭터셋으로는 변경이 불가능하다.

SQL> ALTER DATABASE CHARACTER SET WE8DEC;
ALTER DATABASE CHARACTER SET WE8DEC
*
ERROR at line 1:
ORA-12712: new character set must be a superset of old character set

-- 다시 데이터베이스를 재구동하면 이제 어엿한 KO16MSWIN949 데이터베이스가 되어 있다.

C:\Documents and Settings\jwryoo>set NLS_LANG=.KO16MSWIN949

C:\Documents and Settings\jwryoo>sqlplus scott/tiger@KSC5601

SQL> select sval from t;

SVAL
---------------
커피숖

변경 후에는 이제 exp/imp를 이용하여 UTF8 데이터베이스로 마이그레이션하는 것도 가능해진다.

이 방식을 사용하려면 반드시 새로운 캐릭터셋은 기존 캐릭터셋의 Superset이어야 한다. 그렇지 않으면 기존 데이터의 안전이 보장될 수 없기 때문이다.

기존 캐릭터셋
새로운 캐릭터셋
변경 가능 여부
US7ASCII
KO16KSC5601/KO16MSWIN949/UTF8/AL32UTF8
가능
KO16KSC5601
KO16MSWIN949
가능
KO16MSWIN949
UTF8
불가능
UTF8
AL32UTF8
가능

강제로 캐릭터셋을 변경하는 옵션 또한 알려져 있으나, 이 지면에 소개할 수 없는 점 양해 바란다. 간단하지만 극히 위험한 작업이다. 이미 강조했지만, 반드시 오라클 엔지니어나 오라클과 지원계약을 맺은 고객의 담당자가 충분히 그 위험성과 방법에 대해 숙지한 후에 작업할 수 있따는 것을 명심하기 바란다.

데이터 마이그레이션

준비작업

  • 데이터베이스 컬럼 길이 점검 : 데이터 자체가 변경되므로 데이터의 길이 또한 변경될 수 있다.
  • 애플리케이션 점검 : 애플리케이션 또한 스키마 및 캐릭터셋 변경으로 인해 영향을 받지 않는지 점검한다. 특히 UTF8으로 갈 경우, 한글을 2바이트로 가정하고 작성한 코드가 없는지 점검한다.

CSSCAN 실행

CSSCAN은 캐릭터셋 변경시 발생할 수 있는 문제점을 미리 감지하고 보고서를 생성해준다는 점에서 매우 유용하다. 비록 이것을 실행하는 것이 완전히 필수적인 요소라고 할 수는 없어도(CSALTER 방식에서는 필수, 지난 일주일 내에 CSSCAN을 수행한 결과가 있어야 함) 지금 보유하고 있는 데이터가 소중하다고 생각된다면 반드시 돌려보는 것이 좋을 것이다.

1) CSSCAN 설치
SQL> connect / as sysdba
Connected.
SQL> @/home/oracle/oracle/rdbms/admin/csminst.sql
2) CSSCAN 구동
[oracle@krrnddel oracle]$ csscan system/..

Connected to:
Oracle Database 10g Enterprise Edition Release 10.1.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

(1)Full database, (2)User, (3)Table: 1 > 2

Current database character set is KO16KSC5601.

Enter new database character set name: > KO16MSWIN949

Enter array fetch buffer size: 102400 >

Enter number of scan processes to utilize(1..32): 1 >

Enter user name to scan: > SCOTT

Enumerating tables to scan...

. process 1 scanning SCOTT.DEPT[AAAL+oAAEAAAAAJAAA]
. process 1 scanning SCOTT.EMP[AAAL+qAAEAAAAAZAAA]
. process 1 scanning SCOTT.BONUS[AAAL+sAAEAAAAApAAA]
. process 1 scanning SCOTT.TESTTBL[AAAME1AAEAAAAA5AAA]
. process 1 scanning SCOTT.KANGYS[AAAME5AAEAAAABBAAA]
. process 1 scanning SCOTT.DEPTENTITY_GRADUATE_DEPTCOMP[AAAMI8AAEAAAABJAAA]
. process 1 scanning SCOTT.DEPTENTITY_DEPT_DEPTCOMP[AAAMJAAAEAAAAB5AAA]
. process 1 scanning SCOTT.NLSTECH_SAMPLE_SORT_KOREAN_M[AAAMm8AAEAAAABZAAA]
. process 1 scanning SCOTT.NLSTECH_SAMPLE_SORT_KSC5601[AAAMm9AAEAAAACBAAA]
. process 1 scanning SCOTT.CHARSET_TEST[AAAMwLAAEAAAACJAAA]
. process 1 scanning SCOTT.T[AAAM9IAAEAAAACRAAA]

Creating Database Scan Summary Report...

Creating Individual Exception Report...

Scanner terminated successfully.

$ ls scan*
scan.err  scan.out  scan.txt

  • scan.out : STDOUT에 출력된 결과를 저장해 놓은 파일
  • scan.err : 캐릭터셋 변경시 손실되는 데이터

    scan.err의 내용물 예:

    [Application data individual exceptions]

    User  : SCOTT
    Table : TESTTBL
    Column: VAL
    Type  : VARCHAR2(100)
    Number of Exceptions         : 1
    Max Post Conversion Data Size: 6

    ROWID              Exception Type      Size Cell Data(first 30 bytes)
    ------------------ ------------------ ----- ------------------------------
    AAAME1AAEAAAAA9AAA lossy conversion         <8c>c
    ------------------ ------------------ ----- ------------------------------
    ...

  • scan.txt : 종합 보고서. 다음 예에 나온 섹션들이 가장 핵심적인 부분이라 하겠다. Data Dictionary 분석 테이블이 비어 있는 것은위의 CSCSAN 실행을 User단위로(SCOTT 사용자에 한해) 테스트를 했기 때문이다. 새로운 캐릭터셋에서도 변경이 없는 데이터(Changeless, 주로 아스키 영문 데이터)와 변경 가능한(Convertible) 데이터들의 합이 정상적으로 마이그레이션되는 데이터의 비중을 보여준다.
    • Truncation : 변경 후 Truncate될 데이터
      Lossy : 변경 후 손상될(깨질) 데이터

    ...
    [Data Dictionary Conversion Summary]

    Datatype                    Changeless      Convertible       Truncation            Lossy
    --------------------- ---------------- ---------------- ---------------- ----------------
    VARCHAR2                             0                0                0                0
    CHAR                                 0                0                0                0
    LONG                                 0                0                0                0
    CLOB                                 0                0                0                0
    VARRAY                               0                0                0                0
    --------------------- ---------------- ---------------- ---------------- ----------------
    Total                                0                0                0                0
    Total in percentage              0.000%           0.000%           0.000%           0.000%


    [Application Data Conversion Summary]

    Datatype                    Changeless      Convertible       Truncation            Lossy
    --------------------- ---------------- ---------------- ---------------- ----------------
    VARCHAR2                            37               20                1                5
    CHAR                                 0                0                0                0
    LONG                                 0                0                0                0
    CLOB                                 4                0                0                0
    VARRAY                               0                0                0                0
    --------------------- ---------------- ---------------- ---------------- ----------------
    Total                               41               20                1                5
    Total in percentage             61.194%          29.851%           1.493%           7.463%

    [Distribution of Convertible, Truncated and Lossy Data by Table]

    USER.TABLE                                              Convertible       Truncation            Lossy
    -------------------------------------------------- ---------------- ---------------- ----------------
    SCOTT.CHARSET_TEST                                                0                0                3
    SCOTT.DEPTENTITY_DEPT_DEPTCOMP                                    2                1                0
    SCOTT.NLSTECH_SAMPLE_SORT_KOREAN_M                                5                0                0
    SCOTT.NLSTECH_SAMPLE_SORT_KSC5601                                 5                0                0
    SCOTT.T                                                           0                0                1
    SCOTT.TESTTBL                                                     8                0                1
    -------------------------------------------------- ---------------- ---------------- ----------------

    [Distribution of Convertible, Truncated and Lossy Data by Column]

    USER.TABLE|COLUMN                                       Convertible       Truncation            Lossy
    -------------------------------------------------- ---------------- ---------------- ----------------
    SCOTT.CHARSET_TEST|CHARCOL                                        0                0                3
    SCOTT.DEPTENTITY_DEPT_DEPTCOMP|DNAME                              2                1                0
    SCOTT.NLSTECH_SAMPLE_SORT_KOREAN_M|TEXT                           5                0                0
    SCOTT.NLSTECH_SAMPLE_SORT_KSC5601|TEXT                            5                0                0
    SCOTT.T|SVAL                                                      0                0                1
    SCOTT.TESTTBL|VAL                                                 8                0                1
    -------------------------------------------------- ---------------- ---------------- ----------------

    [Indexes to be Rebuilt] : 캐릭터셋 변경 후 재생성되어야 하는 인덱스들의 리스트

    USER.INDEX on USER.TABLE(COLUMN)                                                        
    -----------------------------------------------------------------------------------------
    -----------------------------------------------------------------------------------------

 이 결과를 보면 어떤 테이블의 어떤 행에서 어떤 컬럼에 어떤 데이터가 손상이 될 수 있는지 살펴볼 수 있다. 단 US7ASCII 데이터베이스에 한글을 저장해 놓고 KO16MSWIN949 또는 UTF8로 데이터 마이그레이션 및 캐릭터셋 변환이 가능한지 CSSCAN을 돌려보고 싶다면 그러지 않기를 권장한다. 그것은 불가능하다는 것은 명백하고 잘못하면 엄청난 크기의 리포트만을 하염없이 기다려야 할 지도 모른다.

그런데 데이터 딕셔너리에는 Changeless만 있는 것이 사실 가장 안전하다. 변경 가능한(Convertible) 데이터가 존재하는 것이 100% 나쁘다고 할 수는 없겠지만, 딕셔너리가 수정되었을 때의 파장은 알 수 없으므로 이는 조심해야 한다.
  • CLOB에 저장된 데이터 : US7ASCII에서는 CLOB에 있는 데이터 역시 US7ASCII 형식으로 저장되지만, KO16MSWIn949나 UTF8같은 멀티바이트 캐릭터셋에서는 데이터가 UCS-2와 호환되는 고정 바이트로 CLOB에 저장된다. 따라서, US7ASCII에서 다른 멀티바이트 캐릭터셋으로 마이그레이션하려고 CSSCAN을 구동했을 때에는 CLOB내의 데이터는 모두 Changeless가 아니라 Convertible로 처리될 것이다.
  • 당연한 이야기이지만, 캐릭터들은 기존 캐릭터셋과 새로운 캐릭터셋에 모두 존재하지만 실제 바이너리값에서는 차이가 있다. KO16MSWIN949에서의 "가"(176 160)와 UTF8에서의 "가"(234 176 128)는 엄연히 바이너리 값이 다르다. 따라서, 멀티바이트(한글)로 테이블명이나 인덱스명을 생성했을 때 CSSCAN은 이들을 Convertible로 처리할 것이다.
exp/imp를 사용하여 새로운 데이터베이스로 데이터와 딕셔너리를 마이그레이션할 것이 아니라면(CSALTER를 사용할 것이라면), 오라클은 안전을 위해 CLOB에 들어있지 않은 딕셔너리 Convertible 데이터들을 마이그레이션 전에 처리할 것을 권장한다
  • 한글 이름으로 된 테이블 이름 변경
  • 한글 이름으로 된 테이블 및 객체들은 exp후 drop시키고, 마이그레이션 후 다시 imp한다
기타 딕셔너리에서 Lossy나 Corrupted된 데이터가 발생한다면 이는 심각하며 반드시 전문가나 오라클 기술 지원 인력과 상의해야 할 문제이다.

exp/imp를 이용한 변경

전통적인 백업 방식을 이용하여 마이그레이션하는 방법으로 다른 버전의 데이터베이스들끼리의 데이터 마이그레이션 작업을 상당히 깔끔하게 할 수 있다는 장점이 있다. 하지만, exp가 잘되었다거나 imp가 잘 되었다는 것이 데이터가 안전하게 저장되었다는 것을 검증해 주지는 못한다. 예를 들어 KO16KSC5601 데이터베이스에서 exp한 파일에 잘못된 데이터(숖)이 있다고 하더라도 이것을 다시 UTF8 데이터베이스에 imp했을 때 그것을 발견하지 못한다. exp/imp에 대해서는 앞서 충분히 보여주었으므로 따로 예제를 들지 않겠다. NLS_LANG 변수값의 설정이 매우 중요하다는 사실은 확실히 기억하기 바란다.

CSALTER를 이용한 변경

실제 CSALTER를 수행하기 전에 반드시 Full Database Scan이 이루어져야 한다. 그렇지 않으면 다음과 같은 메시지를 만나게 될 것이다.

SQL> @@/home/oracle/oracle/rdbms/admin/csalter.plb normal

0 rows created.


Function created.


Function created.


Procedure created.

This script will update the content of the Oracle Data Dictionary.
Please ensure you have a full backup before initiating this procedure.
Would you like to proceed ?(Y/N)?y
..
Full database scan is required

csalter.plb는 SYS사용자만이 수행할 수 있고, 수행 시점에서 이 데이터베이스에 접속된 유일한 세션이어야 한다. 그리고 반드시 새로운 캐릭터셋은 기존 캐릭터셋의 Superset이어야 한다. 한글 지원 캐릭터셋과 US7ASCII 캐릭터셋들의 Subset-Superset 관계는 앞서 ALTER DATABASE 방식을 설명하는 중 소개된테이블을 참조하기 바란다.

글을 마치며

캐릭터셋 변경은 필요하고도 위험하다. 캐릭터셋을 잘못 사용해 왔다는 사실을 알게 되는 시점부터 운영자는 고민에 빠진다. 캐릭터셋을 잘못 사용해 왔다는 것은 그 상단의 애플리케이션조차도 잘못되어 있을 가능성이 높다는 의미이다. 따라서, 데이터를 잘 보존하면서 캐릭터셋을 옮기는 것도 리스크가 크지만, 그 상단의 애플리케이션 또한 다시 디버깅하고 테스팅해야 한다는 사실 또한 운영자에게 두려움으로 다가올 수 있다.
  • 캐릭터셋 변경에 대한 방식을 충분히 습득하자
  • 캐릭터셋 변경에 따른 위험성을 충분히 이해하자
  • 캐릭터셋 변경에 대한 관계자들의 폭넓은 동의가 확보하자
  • 캐릭터셋 변경 전에 반드시 변경을 되돌릴 수 있는 장치를 마련하자
  • 캐릭터셋 변경 전에 충분히 테스트 DB로 테스트하자
크리에이티브 커먼즈 라이선스
Creative Commons License

오라클과 NLS (1)

Dev 2006/04/21 01:21 |
사용자 삽입 이미지

오라클과 NLS의 찰떡궁합 들여다보기

류정우, 한국오라클 WPTG팀




  • 오라클의 NLS 지원 특성
    1. 영역(Territory)별 지원
    2. 언어(Language)적 지원
  • 오라클 제품 처음부터 올바르게 설치하자
  • 올바른 캐릭터셋을 선택하자
    1. KO16KSC5601
    2. KO16MSWIN949
    3. UTF8/AL32UTF8
    4. National Characterset
    5. 캐릭터셋 선택의 원칙
  • 올바른 NLS 환경변수값 설정하기
    1. NLS_LANG
      1. 1) NLS_LANG 변수의 구성
      2. 2) NLS_LANG 변수값 설정의 기본 원칙
      3. 3) 데이터베이스의 캐릭터셋과 동일한 값으로 캐릭터셋을 설정하는 경우
  • KO16KSC5601에 서 지원되지 않는 글자들을 KO16KSC5601 데이터베이스에 입출력하기
  • 오라클 데이터베이스에서의 한글 정렬
    1. KO16KSC5601 데이터베이스
    2. UTF8/AL32UTF8 데이터베이스
      1. NLS_SORT=’KOREAN_M’ 이용
    3. KO16MSWIN949 데이터베이스
      1. 방법 1) NLS_SORT=’UNICODE_BINARY’ 이용
      2. 방법 2) NLS_SORT=’KOREAN_M’ 이용
    4. 인덱스를 이용하여 성능 향상시키기
  • 오라클 데이터베이스에서 한글 비교하기
    1. NLS_COMP
  • 글을 마치며


    Oracle Forms를 이용하여 기업용 어플리케이션을 개발할 때 요구되는 화면의 유형은 크게 두 가지로 나눌 수 있다.
    하나는 입력용 화면이고 또 다른 유형은 조회용 화면이다. 입력용 화면은 데이터의 정합성 에 초점을 두어 개발이 이루어지며,
    조회용 화면은 다양한 조건으로 데이터를 조회할 수 있는 기능에 초점을 맞추고 있다.

    "왜 우리만 한글이 제대로 안 보이는 거야. 이거 비싼 돈 주고 도입했더니 완전 엉터리 아냐?
    아니면 오라클 개발자들이 꼬부랑말 쓰는 녀석들이라는데...영 아니올시다인가?"

    "그러니까 외국 제품들 쓰면 다 이 모양이라니깐.. 대한민국에 관심이나 있겠어?"

    어떻게든 되게 만들어야 합니다. 어김없이 사건 현장에는 혈투를 벌이는 병사가 구원을 요청한다. "한글, 한글이 왜 깨지냐고요?
    업그레이드하기 전까지는 잘 돌아갔는데 이게 무슨 경우입니까? 고객에게 어떤 답변을 해 주어야 합니까?"
    제 아무리 자식을 좋은 학교에 보내어도 노심초사 자식을 걱정하는 부모의 마음처럼, 천하를 호령하고 다닌다는 오라클 데이터베이스에
    데이터를 저장하고 있으면서도 고객 또한 자식같은 데이터를 잃을까 노심초사하기 마련이다.  오라클의 최전방의 병사들은
    이런 고객의 마음을 헤아리고자 백방으로 문제를 해결하려고 뛰어다니면서도 머릿속에 이런 생각이 든다.
    "아, 왜 세상엔 다양한 언어들이 있어서..."

    최전방 공격수가 아니라 다국적 소프트웨어로서의 오라클 제품의 완벽성을 위해 일하고 있는 본인의 입장에서는 처음부터
    병사들의 절규들을 이해할 수 있었던 것은 아니었다. 그도 그럴 것이 많은 문제들이 소프트웨어 자체의 결함보다는 잘못된
    설정과 이해에서 비롯된 것이기 때문이었다.  하지만, "소프트웨어의 글로벌화"라는 슬로건이 제대로 등장하기 시작한지는
    실질적으로 아직 10년도 되지 않았다. 게다가 다국어 지원의 개념이 직접 소프트웨어의 설계에 적용되고 개발, 출시되고
    사용되기까지는 그 10년 중 상당 부분이 소모되어야 했다. 그러므로 아직 시스템의 "한글화", "다국어화"라는 것이 100%
    이해되기에는 무리가 있는 것이다.

    "지금부터라도 제대로 "글로벌화"된 시스템을 이해하고 적용해 보는 거야!"

    오라클 데이터베이스가 속편하게 입력되는 데이터를 마치 바이너리 스트림을 저장하듯이 텍스트 데이터를 언어의 특성에 맞는지
    검사하거나 변환하는 작없없이 저장한다는 오히려 겉으로 드러나는 문제점이 더 적게 보이는 착시 현상이 발생할 지도 모른다.
    또한 오라클 데이터베이스가 현재 사용자에 대해, 어느 나라에 있는지 고려하지 않고 단순히 날짜나 숫자 데이터를 저장만 해주고,
    어떤 형식으로 날짜가 출력되어야 사용자가 이해할 수 있는지, 어떤 통화기호를 사용해야 사용자가 제대로 된 값을 이해할 수 있는지
    고려하지 않는다면 오라클 데이터베이스 입장에서도 개발의 부담이 훨씬 적었을 것이다. 그 모든 것은 상위 시스템 개발자의 책임이니까......

    "오라클 데이터베이스 한국어판 나왔습니까?"
    오라클의 제품들은 현재 세계 140개 이상의 다양한 로케일을 지원하고 있다. 물론 한국어도 포함한다. 단순히 한국어를 포함하는 게
    아니라 한국어는 "가장 중요한 10개 언어들 중 하나"의 위치를 차지하고 있다. 그런데도 "오라클 데이터베이스의 한국어판 출시 임박"
    이런 광고를 본 적이 없을 것이다. 한국어 뿐 아니라 저 100개가 넘는 모든 로케일에 대해 "~~판"이라는 것은 애시당초 존재하지 않는다.
    왜? 오라클 제품 자체가 "글로벌판"이기 때문이다. 출시 자체를 모든 로케일에 대한 지원이 끝난 이후에야 하게 되며, 출시된 제품에는
    이미 모든 로케일에 대한 지원이 포함되어 있는 것이다. 많은 제품들이 "영어 버전"의 출시 후 수 주, 혹은 수 달 후에 "한국어판"이라는
    것을 출시하는 것과는 다른 전략이다. 소프트웨어의 하부 설계 자체가 이미 다국어 지원을 고려하여 만들어졌기 때문에 가능한 것이다.

    SQL> alter session set NLS_CALENDAR = 'ROC Official';

    세션이 변경되었습니다.

    SQL> SELECT sysdate from dual;

    SYSDATE
    ------------------------------------------
    中華民國94年07月21日
    [오라클은 추가적으로 6개의 달력 시스템을 지원한다]

    위에서 보듯, 여러분이 설치한 오라클 데이터베이스는 일반적으로 사용하는 Gregorian 달력 이외에 6가지 달력을 더 지원하고 있다.
    이런 것이 오라클 소프트웨어만의 특징은 아닐 지라도 멋있지 않은가? "글로벌화"된 소프트웨어라는 것.

    "그래서 어렵다"
    이것은 오라클 제품의 강점이기도 하지만, 그래서 사람들에게 "어렵다"는 느낌을 심어준다. 왜냐하면 오라클 제품을 사용하는 사람은
    결국 입맛에 맞게 설정이 끝나 있는 "한국어판"을 사용하는 것이 아니라, "글로벌판"을 입맛에 맞게 설정하여 사용해야 하기 때문이다.
    하지만, 그건 단순한 이유일 뿐이다. 사실은 그 "입맛에 맞는 설정"이라는 것을 제대로 할 수 있도록 해 주는 "교본"이 없다는 것이
    문제이다. 그리하여 많은 DBA들조차 초기 설정을 제대로 하지 않은 채 시스템을 도입하게 되고, 무엇이든 해 낸다(자랑스럽기도 하고
    개발자로서 슬프기도 한)는 실력을 갖추고 있는 우리의 개발자들이 잘못된 설정을 가진 시스템을 기반으로 하여, 어떤 수를 써서라도
    문제가 발생하지 않도록 밤을 새는 것이다. 그리 어렵지 않은 "다국어 지원"을 상식화하면, 개발자들도 억울하게 고생하지 않고
    작업을 빨리 마친 후 집에 가서 재미있는 드라마도 볼 수 있는 것이다.

    "알아보세. 느껴보세. 기뻐해보세"
    누구나 잘 알고 있는 것 같으면서도, 누구나 잘못 사용할 수도 있는 오라클 데이터베이스의 견고한 NLS(National Language Support)
    아키텍처를 어떻게 하면 문제없이 잘 사용할 수 있는지 일단 한 번 그 세계로 빠져 보기로 하자.  약 보름 간격으로 네 번에 걸쳐
    오라클의 NLS 세계를 관광하고, 관광 다녀와서 이제는 개발자도 삼순이 같은 재미있는 드라마 보러 자신있게 퇴근하는 데 조금이라도
    도움이 될 만한 지식 혹은 상식을 가져 보기로 하자.

      연재 순서
      1회 : 오라클과 NLS의 찰떡궁합 들여다보기(1)
      2회 : 오라클과 NLS의 찰떡궁합 들여다보기(2)
      3회 : 오라클 GDK를 사용하여 깔끔한 다국어 개발 유틸리티를 만들자
      4회 : 한글화된 오라클 제품, 그 이면의 비밀.

    PartⅠ. 오라클의 NLS 지원 특성.

    1. 영역(Territory)별 지원

      영국과 미국은 "영어"를 모국어로 사용하는 나라들이지만, 이들 나라에서 사용되는 날짜 표기 방법은 각각 다르다. 영국에서는 "일/월/연도"로 표기하는 반면, 미국에서는 "월/일/연도"로 표기한다. 물론 사용하는 통화기호 또한 "파운드"와 "달러"로 각각 다르다. 이렇듯, 같은 언어를 사용하는 지역이라고 해도 서로 다른 지리적, 사회적 특성으로 말미암아 서로 차이점을 가지게 마련이다.  영역 정보들이야말로, 여러분들이 NLS에 대해 "캐릭터셋 지원"이나 "번역"이상의 그 무엇으로 인식하는 데 큰 도움을 줄  것이다. 한 영역은 다음 표와 같은 고유 정보들을 포함할 수 있다.

      • 달력 설정 방법 : 어떤 나라는 한 주의 첫 번 째 요일을 일요일로 생각하고(한국), 다른 나라들 중에서는 월요일(체코)로 생각할 수 있다. 또한 한 달의 첫 번 째 주를 생각할 때, 어떤 나라(체코)는 그 달의 날짜들이 해당 주의 과반수 이상을 차지하고 있을 때 그 주를 첫 번 째 주로 생각하고, 어떤 나라(한국)에서는 최초의 완전한 한 주를 그 달의 첫 번 째 달로 생각한다.
      • 날짜 포맷 : 같은 날짜를 표기하는 데 각 지역마다 고유의 방식이 있음. 각 지역마다 "짧은 형식"과 "긴 날짜 형식"을 지정할 수 있다. 2005년 8월 10일이라는 날짜 데이터를, 한국에서는 "05/08/10 오후 07:28:03"로, 체코에서는 "10.08.05 19:28:03"로 제공할 수 있다.
      • 통화 기호 : 각 지역마다 통화기호와 금액 표기 방식이 다르다.  또한 통화의 변경(EURO화 변경과 같이)으로 인해 두 개의 통화가 사용될 수 있고(DUAL CURRENCY), 국제적으로 통용되는 ISO 통화기호도 있다. 한국은 ₩, 체코는 Kč를 사용한다.
      • 숫자 그룹 : 소수점 기호나 숫자를 그룹핑하는 방법이 지역마다 다르다. 기타 측정방식(미터방식 등)이나 반올림 방식, 음수기호의 위치 등이 지역마다 다르다. 한국에서는 소수점기호는 dot, 그룹기호는 comma이지만, 체코에서는 소수점기호가 comma, 그룹기호는 dot이다.

    2. 언어(Language)적 지원

      언어별로 달리 지원을 하는 특성은 다음과 같은 것이 있다.

      • 캐릭터셋 : 각 언어가 저장될 수 있는 캐릭터셋을 대부분 지원한다. 한국어의 경우 KO16MSWIN949와 KO16KSC5601이 있다.
      • 정렬 방식 : 각 언어별로 정렬하는 규칙이 다르다. 기본적으로는 이진 바이너리 코드값을 이용해 데이터를 정렬할 수 있지만,
        때에 따라서는 각 언어가 가진 글자들의 고유한 특성에 맞게 정렬을 해 줄 필요성이 있다.  이런 정렬을 Linguistic Sorting이라고 한다. 한국어에 대해서는 KOREAN_M이라는 정렬 방식을 지원한다.
      • 날짜 표기에 사용되는 기호 : 날짜를 표시할 때 사용하는 month, day, day of week, year같은 정보를 그 나라에 맞게 번역하여 제공한다.
      • 에러메시지 및 UI 번역 : 사용자들의 불편을 최소화하기 위해 각 언어별로 번역된 에러 메시지와 사용자 인터페이스를
        제공한다.

    PartⅡ. 오라클 제품 처음부터 올바르게 설치하자.

      한국어 환경을 제대로 지원하려면 그림과 같이 반드시"한국어""실행 언어"에 포함시켜야 한다. 사실 많은 DBA들이 이 부분을
      간과하고 있다. 아무 생각 없이 계속"다음(N)"버튼만 누르다가는 돌이킬 수 없는 결과를 얻게 될 수 있으므로 이 과정은 정말
      중요하다고 하겠다.

      사용자 삽입 이미지

      언어 선택의 의미.

      "한국어"를 선택하지 않는다고 해서 한국어를 데이터베이스에 저장할 수 없다는 것은 아니다.
      먼저 이 "언어 선택"에서 한국어를 선택한다는 것의 의미를 정확히 알 필요가 있다. 위에 강조한 대로 한국어를 비록 선택하지
      않았다고 해서 데이터베이스에 한글 데이터를 넣을 수 없다는 것은 결코 아니다.

      "한국어 저장 여부는 오로지 캐릭터셋이 무엇인가에 달렸다"

      한국어를 제대로 저장하는가 여부는 오로지 다음에 언급할 캐릭터셋 설정에만 의존할 뿐, 실상"언어 선택"에서의 한국어 선택
      여부는 관계없다는 점을 잘 구분해야 한다 위의 그림에서 묻는 바와 같이 이 화면은 "실행 환경"에서 어떤 언어를 지원할 것인가를
      묻고 있다. 즉 여기에서 "한국어"를 선택하면 다음과 같은 리소스가 설치된다.

      • 번역된 메시지 : 오라클 데이터베이스와 함께 제공되는 애플리케이션 중, iSQL*plus나 자바나 ADF 기반의 웹 애플리케이션의 경우에는 "언어 선택"과 관계없이 번역된 작업 환경이 제공된다. 하지만, SQL*Plus와 같은 기존 애플리케이션은 오라클의 번역 메시지 리소스에 의존하며 이들 파일은 각 언어별로 따로 제공된다. 만일 "한국어"를 선택하지 않으면 한국어 메시지 파일은 설치되지 않게 된다.
      • 폰 트 : 오라클 ADF(UIX 혹은 CABO) 기반의 애플리케이션의 경우, "확인", "취소" 등의 버튼이 이미지로 제공되는 경우가 많다. 이런 이미지들은 번역된 메시지를 바탕으로 UIX 엔진에 의해 동적으로 생성된다. 이 이미지가 제대로 생성되기 위해서는 반드시 특정 폰트를 필요로 하게 되는데, 이 폰트는 "한국어"를 선택하지 않을 경우 설치되지 않는다. 이 특정 폰트는 한국어, 중국어(간체, 번체) 그리고 일본어에 대해 각각 제공되므로,  만일 한국어 이외에 이들 언어도 지원해야 할 경우 필수적으로 그 언어들을 선택해야 할 것이다.

      • 로케일 정보 : 번역 뿐만 아니라 오라클 데이터베이스가 다양한 로케일 정보를 가진 클라이언트들과 제대로 통신하기 위해서는 각 국가별, 언어별로 특색있는 로케일 정보를 지니고 있어야 한다. 이들은 날짜 형식("2050-04-14" "Jul 9, 2005" 등), 통화 코드($) 등의 정보를 포함하고 있다. 한국어에 관한 로케일 정보를 위해 반드시 "한국어"를 선택해야 한다.


      한글 Windows 혹은 LANG=ko로 설정한 유닉스 환경
      이 환경에서는 그림과 같이 OUI가 한글로 뜨게 된다.

      기타 유닉스 환경
      DBA 중에서는 "한글" 환경에서 오라클 제품을 설치하면 오동작하거나 설치되지 않는 경우가 많다고 믿는 사람들이 종종 있다. 하지만 이것은 틀린 이야기다. 오히려 한글 데이터나 한글 기반의 애플리케이션을 위해서는 한글 환경에서 오라클 제품을 설치하는 것이 실수를 줄이는 길이다.

      "저희는 LANG=ko로 하면 깨진 화면이 떠서요"

      하지만, 아직 리눅스 등 많은 OS에서 오라클이 제공하는 번역된 인스톨 환경을 사용할 수 없다. 오라클의 인스톨러인 OUI(Oracle Universal Installer)는 자바 기반의 애플리케이션이며, 한글 출력 가능 여부는 JRE(Java Runtime Engine)와 OS에 의존한다. Sun에서 제공하는 JRE의 Linux 버전은 현 시점까지는 오로지 일본어 폰트 정보(font.properties.ja)만 제공하고 있으며 한국어에 대해서는 제공하고 있지 않다. 따라서, 부득이하게 한글이 깨져서 나오게 되는 것이다. 이 경우에는 할 수 없이 LANG=C로 설정하고 영문 환경에서 오라클 제품을 설치해야 한다. 독자들은 이제 이런 상황에서 무엇을 조심해야 하는지를 깨달았을 것이다. 이 경우 "언 어 선택"에서는 오로지 "영어"만이 선택되어 있으므로, 반드시 "한국어"를 선택하고 넘어가야 할 것이다.

    PartⅢ. 올바른 캐릭터 셋을 선택하자.

      "올바른 캐릭터셋이라 함은 한글을 저장할 수 있는 캐릭터셋을 말한다."
      자, 설치에서 한숨을 돌렸다면, 이제 실제 데이터베이스 인스턴스가 생성될 때, 올바른 캐릭터셋을 선택하는 것이 중요하다.
      물론 이 사항은 오라클 많은 소프트웨어 제품들 중 데이터베이스를 설치할 때에만 해당되는 사항이다.
      캐릭터셋은 잘못 설치되었을 경우에는 그야말로 치명적이다. 이런 데이터베이스에 어떤 잘못된 방식으로든 한글 바이트 코드를
      저장하고 사용하게 된다면, 돌이킬 수 없는 결과를 낳게 된다.

      "정해진 캐릭터셋을 가지고 있지 않은 데이터베이스에 결코 한글 데이터를 저장할 수 없다."
      독자 중에는 이 말을 믿으려고 하지 않는 사람들이 있을 것이다. "설마? 내가 해 봤는데, 되던데요? 이런 생각을 가진 사람들이
      있을 것이라고 믿는다. 하지만, 여러분들이 저장한 것은 결코 한글이 아니다(한글을 저장하고 사용해 왔다고 믿고 싶을 테지만).
      여러분들이 저장해 온 것은 그저 한글을 가장한 이진 코드의 덩어리일 뿐이다. 데이터베이스가 여러분들이 던져주는 코드를
      올바른 텍스트로 인지하는 능력을 억제시킨 채, 강제로 데이터를 저장하며 그것을 가지고 한글을 제대로 저장했다고 우길 수는
      없는 것이다. 다 같이 믿자. 저 말은 부정할 수 없는 사실이며 결코 부정될 수 없다.

      "왜 유독 한국 사람들은 그동안 US7ASCII 캐릭터셋을 사랑해 왔나? 이제 헤어질 때가 왔다"
      현재 한글을 지원하는 캐릭터셋으로는 다음 네 가지가 있다.오직 이 네 가지이다. 각각의 특색이 다르므로 유의해야 한다.

      • KO16KSC5601
      • KO16MSWIN949
      • UTF8
      • AL32UTF8

    1. KO16KSC5601

    이름에서 알 수 있는 바와 같이 이 캐릭터셋은 한글 완성형 코드와 일치한다. 완성형은 일반적으로 많이 사용되는 2350자의 한글을 25*94 매트릭스에 배열한 문자셋이며, 4888자의 한자와 히라카나, 카타카나, 그리고 영문 및 각종 기호들을 포함하고 있다. 유닉스 환경에서는 LANG=ko로 하여 DBCA(Database Configuration Assistant)를 실행할 경우, 자동으로 캐릭터셋을 KO16KSC5601로 지정한다. 물론 변경할 수도 있다.

    "KO16KSC5601 캐릭터셋을 사용하기 전에 그 특성을 제대로 알고 사용하자"

    햏햏

    모두에게하게 되었소. 솔가 예약했던 커피이 배신을 때리는 바람에 그만 낙동강 오리알이 되었지요. 정말하오.


    방각하

    불행하게도 여러분의 KO16KSC5601 데이터베이스는 이 글을 제대로 저장할 수 없다. 굵게 표시된 글자는 완성형에 포함되지 않은 글자들이다. 그래서 이 글을 저장한 후 다시 읽으려고 하면 다음과 같은 결과를 보게 될 뿐이다.

    ? ?

    모두에게?하게 되었소. 솔?가 예약했던 커피?이 배신을 때리는 바람에 그만 낙동강 오리알이 되었지요. 정말?하오.

    ?방각하

    비록 이런 글을 올리는 게 올바른 한국어 문화에 이바지하는 길이 아니라는 것을 잘 알고 있지만, 시스템을 개발하는 사람이 사용자에게 문화 계몽을 시킬 수는 없는 노릇이다. 또한 사람의 이름을"솔믜"라고 짓지 말라고 요구할 수도 없다. 실제로 이쁜 이름이 아닌가?

    "캐릭터셋 지정 전에 반드시 사용할 시스템이 어느 정도 범위의 한글 지원을 원하는가를 확실히 검사할 필요가 있다"

    2. KO16MSWIN949

    Windows-949 캐릭터셋은 마이크로소프트사의 Windows Codepage 949번, 즉 한글 코드 페이지를 따른 코드셋이다.
    이는 완성형(KO16KSC5601)을 그대로 포함하고 있으며, 추가로 현대 한글 조합으로 표현할 수 있는 모든 가짓수에 해당하는 8822자의 한글을 추가해 포함하고 있다. 그러니까 "Windows-949 캐릭터셋은 KSC5601의 수퍼셋(Superset)"이 되며, 따라서"KO16MSWIN949 또한 KO16KSC5601의 수퍼셋"이 된다.

    "다른 운영 체제에서도 사용할 수 있다!"
    운영 체제가 Windows 949 코드 페이지를 지원하지 않는다고 해서, KO16MSWIN949 캐릭터셋을 가진 데이터베이스 인스턴스를 생성할 수 없다는 것은 아니다. 데이터베이스 캐릭터셋과 운영체제의 캐릭터셋은 전혀 별개라고 인식해야 한다. 비록 Windows-949는 특정 업체의 문자셋이기는 하지만, 이를 기반으로 한 KO16MSWIN949 캐릭터셋은 한글 2350자의 한계를 가진 KO16KSC5601의 대안으로 용이하게 이용될 수 있다. 기억하자.
    Unix에서든 Linux에서든, KO16MSWIN949 캐릭터셋을 가진 데이터베이스 인스턴스를 생성할 수 있다.

    3. UTF8/AL32UTF8

    UTF8은 유니코드를 구현한 캐릭터셋 중에 가변길이 인코딩 방식을 택하고 있는 캐릭터셋이다.
    자세한 인코딩 방식은 여기에서 논할 필요가 없지만, 가변 길이를 위해 일종의 플래그 비트를 각 바이트마다
    포함시켜야 하다보니, 한 글자를 표한하는데 필요한 바이트의 길이가 최대 3바이트(AL32UTF8의 경우 6바이트)까지 늘어날 수 있다.

    유니코드는 잘 알려진 바와 같이 현대 한글 11172자를 모두 가나다 순으로 잘 정렬된 상태로 포함하고 있다.
    그래도 한글 한 자가 3바이트의 물리적 공간을 차지하므로, 오로지 모든 한글을 지원한다는 이유만으로 사용하는 것은 곤란하다. 하지만, 한글 이외에도 다른 언어들을 함께 데이터베이스에 저장해야 한다면 다른 선택의 여지가 없는 유일한 선택이 된다.

    한글을 지원하는 캐릭터셋 비교 테이블

    KO16KSC5601
    KO16MSWIN949
    UTF8
    AL32UTF8
    한글 지원상태
    한글 2350자 KO16KSC5601 + 확장 8822자(총 11172자)
    한글 11172자
    한글 11172자
    캐릭터셋/인코딩 버전
    한글완성형
    완성형코드포함
    확장된 8822자는 MS Windows Codepage 949에 따라 배열
    8.1.6 이전 : Unicode 2.1
    8.1.7 이후: Unicode 3.0
    9i Rel1: Unicode 3.0
    9i Rel2 : Unicode 3.1
    10g Rel1 : Unicode 3.2
    1/0g Rel2 : Unicode 4.0
    한글바이트
    2바이트
    2바이트
    3바이트
    3바이트
    지원버전
    7.x
    8.0.6 이상
    8.0 이후
    9i Release 1 이상
    Database Characterset으로 설정 가능 여부
    가능
    가능
    가능
    가능
    National Characterset으로 설정 가능 여부
    불가능
    불가능
    가능
    불가능
    한글정렬
    단순 바이너리 정렬로  구현 가능
    KOREAN_M 또는 UNICODE_BINARY 등 특수한 옵션 필요
    (한글 정렬에 관한 설명 참조)
    한글 정렬은 단순 바이너리 정렬로 가능. 한자 정렬은 KOREAN_M 옵션 필요

    장점
    - 특별한 장점이 없음. 완성형 코드만을 입출력하는 것이 확실할 경우에는 높은 성능
    - 2바이트로 모든 한글 저장/입출력 가능. 공간의 소모가 적으면서도 모든 한글을 입출력할 수 있다


    - 현대 한글 11172자가 정확한 순서로 배열되어 정렬이 효과적
    - 다른 언어들(중국어 태국어 등) 또한 같은 데이터베이스 인스턴스에 저장되어야 할 경우 UTF8 등의 유니코드 캐릭터셋 이외에 다른 대안이 있을 수 없음
    - 한글 지원은 UTF8과 동일
    단점
    - 한글을 2350자밖에 지원하지 못한다는 치명적인 단점이 있어 미래에는 사용이 자제되어야 할 캐릭터셋
    - 완성형과 호환을 하려다보니, 글자배열순서와 정렬 순서가 다르게 됨. 단순한 "ORDER BY" 절로는 제대로 한글 정렬을 할 수 없음
    한글 한 캐릭터가 3바이트를 소모하게 되어 공간의 소모가 크고, 유니코드 인코딩/디코딩에 성능을 소모해야 한다


    4. National Characterset

    네셔널 캐릭터셋은 유니코드를 지원하지 않는 캐릭터셋을 가진 데이터베이스에서 유니코드를 지원하기 위해 부가적으로 설정할 수 있는 캐릭터셋이다. 즉, 하나의 데이터베이스 인스턴스는 "캐릭터셋"과 "네셔널 캐릭터셋"을 가진다. 처음 시스템 구축 당시와는 달리, 한글 이외의 다른 언어를 급히 저장해야 할 필요성이 있는 경우 네셔널 캐릭터셋을 적절히 활용할 수 있다.

    네셔널 캐릭터셋을 가능한 캐릭터셋은 단 두 가지이다. UTF8과 AL16UTF16(기본값).
    네셔널 캐릭터셋을 사용하기 위해서는 특정 타입으로 테이블의 컬럼 또는 PL/SQL 변수를 선언해야 한다. CHAR와 VARCHAR2,CLOB에 대응되는 네셔널 캐릭터셋 기반의 타입으로는 NCHAR, NVARCHAR2,NCLOB이 있다.

    즉, KO16MSWIN949 데이터베이스에서 다음과 같이 테이블을 생성할 경우,

      CREATE TABLE test_table
      ( varchar_value VARCHAR2(2000),
      nvarchar_value NVARCHAR2(2000)
      );
    "varchar_value" 컬럼에는 KO16MSWIN949에 속하는 글자들만 저장할 수 있는 반면, nvarchar_value 컬럼에는 유니코드에 속한 모든 글자들을 저장할 수 있다. 약간의 부가적인 코드가 필요할 뿐, 실제 프로그래밍 방식은 거의 같다.

    5. 캐릭터셋 선택의 원칙

    많은 원칙이 필요없다. 다음 몇가지만 기억하자.

    • 한글 지원을 위해서는 반드시 위의 네 가지 캐릭터셋 중에 하나를 선택해야 한다
    • 한국에서만 사용하는 시스템이라면 KO16MSWIN949를 선택한다
    • 한국어 뿐 아니라 중국어, 일본어, 러시아어 등 다양한 언어로 된 데이터를 저장해야 한다면 UTF8, AL32UTF8을 선택한다. 인코딩 변환으로 한국어 기반의 캐릭터셋에 비해 속도의 저하가 있다고 알려져 있다.
    • 대부분이 한글이며, 일부 외국어가 필요하다면, 한국어 기반의 캐릭터셋(KO16MSWIN949)을 사용하되, National Characterset을 이용한 컬럼에 외국어를 저장한다.

    PartⅣ. 올바른 NLS 환경변수값 설정하기.

      "모로 가도 서울만 가면 된다"고 자동차에 대한 지식없이 어떻게 하다보니까 차를 뒤로 움직이게 되어, 후진으로만 목적지에 도달한다면 과연 목적지에 잘 도달했다고 칭찬받아야 할까? 그 답은 "예"일 수도, "아니오"일 수도 있지만, "모로 가도 한글만 나오면 된다"는 식으로 구축된 시스템에 대해서는 무조건 "아니오"가 답이다. 많은 시스템들이 제대로 운전에 대한 지식과 준비 없이 시스템을 목적지로 운전시키고 있다.

      오라클 데이터베이스는 무려 20개의 다양한 NLS 환경변수를 제공한다. 하지만 염려는 붙들어 매길...... 그렇다고 해서 20개 모두의 씀씀이를 다 알아야 한국어데이터를 제대로 다룰 수 있다는 의미는 아니다. "손가락만 까딱해도 된다"는 요즘 자동차들이 제공하는 수많은 기능들 중에 핵심적인 기능만 알아도 안전운전을 할 수 있듯이, 몇 가지 핵심적인 변수의 의미만 제대로 파악하고 제대로 사용한다면 그야말로 "안전한" 한국어 환경을 구축할 수 있는 것이다.

      1. NLS_LANG

      1) NLS_LANG 변수의 구성
      NLS_LANG 변수는 단순히 하나의 변수가 아니라 실질적으로 NLS 연산의 모든 것을 결정한다고 해도 틀리지 않은 세 가지 정보를 포함하고 있는 중요한 변수이다.

         NLS_LANG = [언어]_[영역].[캐릭터셋]


      정 의
      가능한 값
      언어
      현재 사용자가 사용하는 언어적 특성을 결정짓는 값 SQL> select parameter,value from V$NLS_VALID_VALUES where parameter like '%LANG%' ORDER BY value;
      ...

      KOREAN
      LATIN AMERICAN SPANISH
      ...
      TRADITIONAL CHINESE
      ..
      VIETNAMESE
      영역
      현재 사용자가 위치한 영역의 특성을 결정짓는 값 SQL> select parameter,value from V$NLS_VALID_VALUES where parameter like '%TERR%' ORDER BY value;
      ...

      KOREA
      ...
      SAUDI ARABIA
      ...
      YUGOSLAVIA
      캐릭 터셋
      현재 사용자의 시스템이 인식할 수 있는 캐릭터셋의 값 SQL> select parameter,value from V$NLS_VALID_VALUES where parameter like '%CHARACTERSET%' ORDER BY value;

      ..
      KO16KSC5601
      ..
      KO16MSWIN949
      ..
      WE8DEC

      ..
      ZHT16MSWIN950

      KOREAN_KOREA.KO16KSC5601
      KOREAN_KOREA.KO16MSWIN949
      AMERICAN_AMERICA.US7ASCII
      AMERICAN_AMERICA.WE8ISO8859P1
      JAPANESE_JAPAN.JA16SJIS
      .KO16MSWIN949
      .UTF8

      2) NLS_LANG 변수값 설정의 기본 원칙
      "제 데이터베이스가 UTF8인데, NLS_LANG도 .UTF8로 해야 하는 거 아닌가요?"
      땡, 틀렸다. NLS_LANG 변수가 데이터베이스 캐릭터셋과 값이 항상 같아야 한다면 무엇하러 설정하겠는가?

      "NLS_LANG 변수는 데이터베이스에게 사용자의 환경을 알려주는 인식표 역할을 한다"
      그렇다. NLS_LANG 변수의 값은 멀리 있는 데이터베이스의 환경이 아니라, 사용자 자신이 속해 있는 환경을 도리어 데이터베이스에 알려주는 역할을 하는 변수이다.

      NLS_LANG을 다음과 같이 설정해 보자.

      > set NLS_LANG=AMERICAN_AMERICA.KO16MSWIN949

      이는 사용자 자신이 미국의 영어를 쓰고, 아메리카 영역 내에 있으며, 가진 컴퓨터가 사용하는 캐릭터셋은 KO16MSWIN949라는 의미이다.

      > sqlplus scott/tiger
      SQL> select x from y;
      select x from y
      *
      ERROR at line 1:
      ORA-00942: table or view does not exist


      SQL> select hiredate from emp;

      HIREDATE
      ------------
      17-DEC-80
      ...

      18 rows selected.

      이와 같이 모든 메시지나 날짜 형식을 미국에 맞게 표현해 준다.

      SQL> exit

      이제 NLS_LANG을 다음과 같이 변경해 보자.

      > set NLS_LANG=KOREAN_KOREA.KO16MSWIN949

      이는 사용자 자신이 한국어를 쓰고, 한국 영역 내에 있으며, 가진 컴퓨터가 사용하는 캐릭터셋은 KO16MSWIN949라는 의미이다.

      SQL> select hiredate from emp;

      HIREDATE
      --------

      80/12/17
      ..

      18 개의 행이 선택되었습니다.


      이제 한국의 날짜 형식과 함께 한국어로 메시지를 보여주게 된다.


      이에 따라, Windows 운영체제에서 한국어 환경을 사용하는 사용자들은 다음과 같이 NLS_LANG 값을 설정할 수 있다.
      KOREAN_KOREA.KO16MSWIN949

      그리고, 유닉스 운영체제에서 한국어를 입출력한다면 다음과 같이 NLS_LANG을 설정할 수 있다.
      KOREAN_KOREA.KO16KO16KSC5601

      데이터베이스가 UTF8이든 KO16MSWIN949이든 상관없이 한글을 지원하는 데이터베이스와 통신한다면 반드시 위와 같이 NLS_LANG값을 설정해야 한다.


      3) 데이터베이스의 캐릭터셋과 동일한 값으로 캐릭터셋을 설정하는 경우
      데이터베이스의 데이터는 사용자로 전달될 때, NLS_LANG에 설정된 캐릭터셋에 기반해 적절히 변환되어 전달된다.
      즉 내부적으로는 한글이 UTF8로 저장되어 있다 할 지라도, NLS_LANG의 값에 따라 UTF8로 인코딩된 문자열을 Windows 949 코드 페이지로 변환하여 사용자에게 전달하게 되는 것이다.

      NLS_LANG 값에 있는 캐릭터셋을 UTF8로 설정하는 경우가 꼭 없는 것은 아니다. 만일 데이터베이스의 캐릭터셋이 UTF8인 상태에서, 질의를 요청한 사용자의 NLS_LANG의 값도 .UTF8이라면, 데이터베이스에서는 단순히 UTF8 문자열을 사용자에게 전달하게 되며 이를 어떻게 사용할 지는 사용자의 몫에 달려 있다.

      NLS_LANG 값을 데이터베이스 캐릭터셋에 맞추는 경우는 대략 다음과 같은 경우가 있다.

      • 데이터베이스로부터 데이터를 export받을 때
      • export 받은 데이터베이스와 같은 캐릭터셋을 가진 데이터베이스로 export된 파일을 import할 때
      • 기타 다국어 지원 애플리케이션에서 목적에 따라 사용할 수 있다.

      [주요 NLS 변수 요약]

      변수명
      정의
      기본값 설정
      설정방법
      NLS_TERRITORY
      영역 설정
      NLS_LANG 변수값에 의해 자동 설정
      초기화변수
      ALTER SESSION SET NLS_TERRITORY = 'KOREA'
      NLS_LANGUAGE
      언어 설정
      NLS_LANG 변수값에 의해 자동 설정 초기화변수
      ALTER SESSION SET NLS_TERRITORY = 'KOREAN'
      NLS_LANG
      언어,영역, 캐릭터셋
      설정
      AMERICAN_AMERICA.US7ASCII
      OS 환경변수
      NLS_COMP
      SQL에서의 비교 방식(<,>,=)
      설정
      BINARY값으로 비교
      초기화변수,OS환경변수
      ALTER SESSION SET NLS_COMP = ''
      NLS_SORT
      문자열의 정렬방법 설정
      NLS_LANGUAGE값에 따라 결정
      초기화변수,OS환경변수
      ALTER SESSION SET NLS_SORT = 'KOREAN_M'
      NLS_TERRITORY 변수에 따라 그 값이 결정되는 변수:
      • NLS_CREDIT(대차대조표 '대변'항목의 금액표기를 위한 기호. 보통 '공백'문자)
      • NLS_CURRENCY
      • NLS_DATE_FORMAT
      • NLS_DEBIT(대차대조표 '차변'항목의 금액표기를 위한 기호. 보통 '마이너스'문자)
      • NLS_ISO_CURRENCY
      • NLS_LIST_SEPARATOR(숫자를 가로로 나열할 때 각 숫자를 구분하는 기호로, 우리나라의 경우 콤마이다)
      • NLS_MOMETARY_CHARACTERS(금액 표기시 금액을 읽기 쉽게 나누는 문자로 우리나라에서는 3자리마다 ","를 추가한다)
      • NLS_NUMERIC_CHARACTERS(소수점기호와 숫자 그룹핑을 위한 문자 설정. 우리나라에서는 '.,'이다(dot와 comma)
      • NLS_TIMESTAMP_FORMAT
      • NLS_TIMESTAMP_TZ_FORMAT
      • NLS_DUAL_CURRENCY(유로화 변경 기간동안의 혼란을 막기 위해 만들어진 매개변수. 9i Release 2과 그 이후로는 EU의 유로화 변경이 완료된 상태로 NLS_CURRENCY와 값이 동일하다. 다만 미래에 다른 지역에서도 통화기호의 변경이 일어나면 사용될 수 있다)

      NLS_LANGUAGE 변수에 따라 그 값이 결정되는 변수:
      • NLS_DATE_LANGUAGE
      • NLS_SORT

      *초기화변수 : init.ora또는 spinit.ora에 설정되어 데이터베이스 구동시에 자동 설정될 수 있는 변수를 의미함
      *OS환경변수 : OS에서 초기화 방식(setenv, export 등)으로 값을 할당,변경할 수 있는 변수
    PartⅤ. KO16KSC5601에 서 지원되지 않는 글자들을 KO16KSC5601 데이터베이스에 입출력하기.

    이제는 KO16KSC5601 캐릭터셋은 “ㅤㅅㅛㅍ”, “ㅤㅃㅙㄼ”과 같은 글자를 지원하지 않는다는 것을 알게 되었을 것이다.
    그저 한글 하면 “KSC5601”을 떠 올리는 것이 보통이니 이런 사실을 아는 것만으로 획기적인 사고의 전환이 가능하다
    .

    “우리 시스템은 일반 사용자의 다양한 의견을 게시판을 통해 수렴하고 있다. 어떤 한글이 입력될 지 모르는 상황이다. KO16KSC5601”은 한글을 2350자밖에 지원하지 않으므로 사용하지 않아야겠다."

    이런 매끄러운 사고가 가능한 것이다. 그러면 어떻게 KO16KSC5601에서 지원되지 않는 글자들을 KO16KSC5601” 데이터베이스에 입출력할 수 있는가? 결론은 이렇다.

    “그런 일은 있어서도 안 되고, 가능하지도 않다”
    실망스러운가? 하지만 엄연한 사실이며, 여러분들은 이 말을 아주 심각하게 받아들여야 한다. 하지만 여전히 여러분들보다
    생각이 덜 하는 사람들, 심각하게 받아들이지 않는 사람들이 있다.

    “KO16KSC5601”에 얼마든지 그런 글자들을 삽입할 수 있던데요?
    그래서 그런 사람들에게 직접 예를 보여줄 수 있다. 한글 윈도우 상에서 다음과 같은 질의를 보여주자.
    한글 Windows 운영체제이므로 암시적으로 NLS_LANG의 값은 KOREAN_KOREA.KO16MSWIN949라고 생각하면 된다.

    DOS> sqlplusscott/tiger@KSC5601DB

    SQL> create table charset_test(charcol VARCHAR2(100), ncharcol NVARCHAR2(100));

    SQL> insert into charset_test(charcol) values('ㅤㅅㅛㅍ');

     

    1 개의 행이 만들어졌습니다.

     

    SQL> select charcol from charset_test;

     

    CHARCOL

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


    “ㅤㅅㅛㅍ”이라는 글자는 INSERT 문장 실행시 에러가 발생되지는 않았지만 결국 제대로 된 값이 삽입되지 않았다.
    여기서 다시 “삽입할 수 있다”파는 다음과 같은 주장을 펼칠 수 있다.

    “NLS_LANG =.KO16KSC5601로 설정하면 결국 캐릭터셋과 NLS_LANG 값이 일치하여 제대로 삽입할 수 있을 것이다.”

    그래서 이렇게 시도를 할 수 있다. 어떤 결과가 나오는지 살펴보자.

    DOS> set NLS_LANG=.KO16KSC5601

    DOS> sqlplusscott/tiger@KSC5601DB

    SQL> insert into charset_test(charcol) values('ㅤㅅㅛㅍ');

    ERROR:

    ORA-01756: quoted string not properly terminated


    이번에는 오히려 INSERT 문장 자체가 실패한다. 어떻게 이런 결과가 나올까? 앞서 NLS_LANG에 대해 배운 바로는
    분명히 NLS_LANG 변수와 데이터베이스의 캐릭터셋이 일치할 경우 데이터의 변환 없이 데이터가 그대로 삽입되게 된다.
    그런데 삽입 문장 자체가 실패하다니? 하지만 놀랄 필요없다. 그저 이것은 당연한 결과이다. 개발자 여러분은 최대한
    단순해질 필요가 있다. 개발자들은 가끔 너무 잘 하려고 하는 경향이 있으며, 안 되는 것은 없다고 생각한다.
    하지만 모든 꼼수는 결국 가까운 미래에 자기 자신에게 부메랑이 되어 격무의 부담으로 돌아온다는 사실을 꼭 기억하자.

    “지원되지 않는 글자를 데이터베이스에 삽입하지 말자”

    인정하건대 글자에 따라, 프로그래밍 방식에 따라 지원되지 않는 글자들로 연산이 가능한 때가 있다.
    단순히 NLS_LANG과 데이터베이스 캐릭터셋이 일치하는 바람에 들어가지 못할 데이터가 들어가게 되는 경우가 있다.
    예를 들어 위의 “ㅤ숖”자 대신에 역시 완성형에서 지원되지 않는 글자인 “똠”을 테스트하면 엉뚱하게 들어가게 된다.
    그렇지만 이것은 단순히 “우연의 일치”일 뿐이다. 1바이트와 2바이트가 섞여 있는 캐릭터셋에서 한 글자의 끝을 인식하는 과정에서 우연히도 “ㅤ똠”이 그 과정을 무사히 통과했을 뿐이다. 그렇지 않으면 대부분 “ㅤ숖”과 같이 엉뚱하게 “문 자열이 제대로 끝나지 않았다”는 에러를 만나게 된다. 확인할 겸 몇 글자 더 시도해 보기로 하자.

    DOS> set NLS_LANG=.KO16KSC5601

    DOS> sqlplusscott/tiger@KSC5601DB

    SQL> insert into charset_test(charcol) values('숖');

    ERROR:

    ORA-01756: quoted string not properly terminated

     

     

    SQL> insert into charset_test(charcol) values('똠');

     

    1 row created.

     

    SQL> insert into charset_test(charcol) values('믜');

    ERROR:

    ORA-01756: quoted string not properly terminated

     

     

    SQL> insert into charset_test(charcol) values('뾃');

     

    1 row created.

     

    SQL> insert into charset_test(charcol) values('햏');

     

    1 row created.

     

    SQL> select charcol from charset_test;

     

    CHARCOL

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

     

    햏/p>


    일부 OCI나 MS 기반의 애플리케이션에서 NLS_LANG과 캐릭터셋이 일치하는 바람에 지원되지 않는 글자들을 삽입하고 조회할 수 있기도 한다. 지금 얼마나 많은 오라클 데이터베이스 인스턴스에서 이런 현상이 벌어지고 있는지 상상하기조차 힘들다. 하지만 미래에는 그런 일이 일어나지 않아야겠다. 왜냐하면 그런 잘못된 구성으로 인해 발생하는 힘든 일은 모두 개발자가 감당하고 있기 때문이다.

    PartⅥ. 오라클 데이터베이스에서의 한글 정렬.

    1. KO16KSC5601 데이터베이스

    KO16KSC5601에서는 한글 2350자의 바이너리 정렬 순서가 한글의 언어적 정렬 방식과 동일하다. 따라서, 단순한 ORDER BY 명령어만으로 정렬의 효과를 거둘 수 있다. 그리고, 한자의 경우 한글 뒤에 한자의 음에 맞게 정렬이 된다. 예에 나와 있는 한자들은 "가구","류","애"로 모두 잘 정렬되어 있는 것을 볼 수 있다. 하지만 명심하라, 단지 한글 2350자들과 한자 4888자의 정렬일 뿐이다. 나머지 글자들에 대해서는 입출력도 불가능하거니와, 입출력을 무슨 수를 써서 했다고 하더라도 정렬이 제대로 될 리가 없다.

    SQL> SELECT text FROM nlstech_sample_sort_ksc5601 ORDER BY text;

     

    TEXT

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

    가나다라마바사

    라디오를켜라

    可口

     

    // 한글, 한자의 정렬이 올바르다

    실제로 잘 만들어진 프레임워크의 내부를 보면 좋다고 알려진 패턴을 구현한 것인 경우가 많고 구조는 잘 정의된 패턴에 기반하고 있는 경우가 일반적이다. 프레임워크는 디자인 패턴들을 실제로 구현한 결과이고 프레임워크를 문서화할 때는 패턴을 통해서 기술하는 것이 효과적이다.

    2. UTF8/AL32UTF8 데이터베이스
    UTF8 데이터베이스의 경우, 한글만을 고려하면 별다른 정렬 옵션이 필요없다. 왜냐하면 한글 11172자의 정렬 순서와 바이트 코드 정렬 순서가 일치하기 때문이다. 한글만의 정렬에 관해서는 정확성과 성능을 모두 만족한다고 볼 수 있다.

    데이터베이스에서 예제 실행:

    SQL> SELECT * FROM nlstech_sample_sort_ko ORDER BY text;

    TEXT

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

    가나다라마바사

    똠각하

    라디오를켜라

    먄해

    햏햏

     

    // 정렬 결과가 올바르다


    하지만, 한자까지 고려한다면 이것도 역시 완벽한 방법은 아니다. 다음 예제를 보자.

    SQL> SELECT text FROM nlstech_sample_sort_unicode_b ORDER BY text;

     

    TEXT

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

    可口

    가나다라마바사

    똠방각하

    라디오를켜라

    먄해

    햏햏


    한자의 경우도 단순히 유니코드 내의 바이트 코드값에 의존하게 되면 별다른 의미를 찾을 수 없는 배열만을 얻게 된다. 이 경우에는 특정 NLS_SORT 매개변수 값을 지정함으로써 좀 더 의미있는 결과를 얻을 수 있다.

    NLS_SORT=’KOREAN_M’ 이용

    NLS_SORT 값을 설정할 수 있는 방법에 대해서는 이미 앞에서 설명한 바가 있다.

    9i부터 지원되는 ‘KOREAN_M’ 정렬 방식을 이용하면 한자에 관련되어 몇 가지 원칙이 적용된다.
    • 한글은 단순히 유니코드 바이트 정렬에 의존한다
    • 모든 한글은 한자에 우선한다
    • 한자는 발음 순서대로 정렬된다

    한 마디로 KO16KSC5601에서 사용되던 정렬 방식으로 모든 한글과 한자를 정렬하겠다는 방법이다.

    KOREAN_M 사용:

    SQL> SELECT text FROM nlstech_sample_sort_unicode_b ORDER BY NLSSORT(text,'NLS_SORT=KOREAN_M');
     

    TEXT

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

    가나다라마바사

    똠방각하

    라디오를켜라

    먄해

    햏햏

    可口

    // 한자가 발음 순서대로 정렬되어 있다(가애)

    3. KO16MSWIN949 데이터베이스

    KO16MSWIN949는 KO16KSC5601에서 지원되지 않는 8822자의 한글을 추가적으로 지원한다는 점에서 KO16KSC5601의 대안으로 자주 이용되는 캐릭터셋이다. 하지만, 기존 KO16KSC5601의 수퍼셋으로 군림하려다 보니 총 11172자의 한글의 바이트 코드가 한글의 언어적 정렬 순서와 불일치할 수 밖에 없다.

    KO16MSWIN949 데이터베이스에서 실행:

     

    SQL> SELECT * FROM nlstech_sample_sort_unicode_b ORDER BY text;

     

    TEXT

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

    똠방각하

    먄해

    가나다라마바사

    라디오를켜라

    햏햏

     

    // 정렬 결과가 엉뚱하다


    이를 극복하기 위해서는 단순한 바이트코드 정렬방식이 아니라 다른 정렬방식을 적용해야 한다.

    방법 1) NLS_SORT=’UNICODE_BINARY’ 이용

    현재 사용자 세션의 기본 정렬 방식을 변경하여 해결:

     

    SQL> ALTER SESSION set NLS_SORT = 'UNICODE_BINARY';

     

    세션이 변경되었습니다.

     

    SQL> SELECT * FROM nlstech_sample_sort_unicode_b ORDER BY text;

     

    TEXT

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

    가나다라마바사

    똠방각하

    라디오를켜라

    먄해

    햏햏



    질의 자체에 NLS_SORT 값을 지정해 주는 방법:

     

    SQL> SELECT text FROM nlstech_sample_sort_ko ORDER BY NLSSORT(text,'NLS_SORT=UNICODE_BINARY');

     

    TEXT

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

    가나다라마바사

    똠각하

    라디오를켜라

    먄해

    햏햏



    방법 2) NLS_SORT=’KOREAN_M’ 이용

    한자 데이터까지 고려하면 ‘KOREAN_M’을 사용하는 것이 좋다.

    UNICODE_BINARY 사용:

     

    SQL> SELECT text FROM nlstech_sample_sort_korean_m ORDER BY NLSSORT(text,'NLS_SORT=UNICODE_BINARY');

     

    TEXT

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

    可口

    가나다라마바사

    똠방각하

    라디오를켜라

    먄해

    햏햏

     

    // 한자의 정렬 또한 단순히 유니코드 바이트 코드에 의존한다.



    KOREAN_M 사용:

    SQL> SELECT text FROM nlstech_sample_sort_korean_m ORDER BY NLSSORT(text,'NLS_SORT=KOREAN_M');

     

    TEXT

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

    가나다라마바사

    똠방각하

    라디오를켜라

    먄해

    햏햏

    可口

     

    // 한자가 발음 순서대로 정렬되어 있다(가애)

    4. 인덱스를 이용하여 성능 향상시키기.

      미리 NLSSORT를 이용한 인덱스를 생성하면 비록 디스크 공간이 더 필요하기는 하지만, 편리하고 성능도 좋아진다.
      인덱스는 각 NLS_SORT 값별로 여러 개를 생성할 수 있다. 어느 인덱스를 사용할 것인지는 NLS_SORT 변수의 값에 달려 있다.

      KO16MSWIN949 데이터베이스에서 실행:

       

      SQL> CREATE INDEX nlstech_sample_sort_ub ON nlstech_sample_sort_ms949 (NLSSORT(text, 'NLS_SORT = UNICODE_BINARY'));

       

      인덱스가 생성되었습니다.

       

      SQL> CREATE INDEX nlstech_sample_sort_km ON nlstech_sample_sort_ms949 (NLSSORT(text, 'NLS_SORT = KOREAN_M'));

       

      인덱스가 생성되었습니다.

       

      SQL> ALTER SESSION set NLS_SORT = 'UNICODE_BINARY';

       

      세션이 변경되었습니다.

       

      // UNICODE_BINARY 인덱스가 사용된다

      SQL> SELECT text FROM nlstech_sample_sort_ms949 ORDER BY text;

       

      TEXT

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

      可口

      가나다라마바사

      똠방각하

      라디오를켜라

      먄해

      햏햏

       

      8 개의 행이 선택되었습니다.

       

      SQL>

      SQL> ALTER SESSION set NLS_SORT = 'KOREAN_M';

       

      세션이 변경되었습니다.

       

      // KOREAN_M 인덱스가 사용된다

      SQL> SELECT text FROM nlstech_sample_sort_ms949 ORDER BY text;

       

      TEXT

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

      가나다라마바사

      똠방각하

      라디오를켜라

      먄해

      햏햏

      可口

       

      8 개의 행이 선택되었습니다.



      NLS_SORT의 값으로 설정할 수 있는 것은 버전 별로 다를 수 있다. 이는 각 버전마다 존재하는 Globalization Support Guide(8.1.7 이전에는 Nationa Language Support Guide)를 참조하면 부록에서 발견할 수 있다. 하지만 본인이 운영하는 데이터베이스라면 대략 다음과 같은 질의로 확인할 수 있다.

      SQL> select value from V$NLS_VALID_VALUES where parameter = 'SORT' ORDER BY value
      ..
      BINARY
      ...
      KOREAN_M
      ...
      UNICODE_BINARY
      ..

    PartⅦ. 오라클 데이터베이스에서 한글 비교하기.

    앞선 팁에서 짐작할 수 있듯이, 정렬이 원하는 대로 되지 않는다면, 비교 또한 제대로 될 리가 없다. 두꺼운 책의 마지막에 있는 인덱스(찾아보기)나 사전처럼, 한글 키워드를 ‘가나다’ 그룹으로 만들기 위해서는 정렬과 비교가 필수적인 기능이다.

    "오라클의 기본 비교 방식은 BINARY"

    정렬도 그렇지만, 비교할 경우에도 역시 오라클 데이터베이스는 기본적으로 BINARY 방식을 사용한다. 같은 글자라 해도 이진 코드는 각 캐릭터셋마다 다르므로 어떤 캐릭터셋에서는 “>”의 결과가 나왔던 비교문도 다른 캐릭터셋에서는 “<”의 결과가 나올 수도 있다. 우리는 이미 어떤 정렬 방식을 지정하면 제대로 정렬이 되는지 앞선 팁에서 익혔으므로, 이제 여기서는 오라클이 비교를 수행할 경우에도 내부적으로 같은 방식을 사용할 것을 지시하기만 하면 된다.

    1.NLS_COMP.

    NLS_COMP 변수는 오라클이 비교 연산들을 수행할 시 어떤 방식을 사용할 지를 지정하는 변수이다. 복잡한 NLS_SORT와는 달리 이 변수의 값으로는 오로지 BINARY 또는 ANSI 두 값만이 허용된다. BINARY는 기본으로 설정되어 있는 값으로 비교 연산은 이진 바이트 코드의 비교로 이루어진다는 것을 의미한다. 하지만, ANSI라고 설정하면, 같은 비교 연산자의 수행 방식이 이진 비교에서 언어적 비교(Linguistic Comparison)으로 전환된다. 즉, 우리가 NLS_SORT 변수에 설정한 정렬 방식이 비교 연산자(>, <, BETWEEN 등)들에게 약발이 먹힌다는 의미이다.

    NLS_SORT와는 달리 이 값은 NLSSORT 함수의 내부 패러미터가 아니므로, SQL 질의문 내에 사용될 수는 없다.
    데이터베이스 초기화 패러미터(init.ora)로 지정되거나 환경 변수, 세션 변수로 지정되어야 한다.

    ALTER SESSION SET NLS_COMP=ANSI;
    또는
    ALTER SESSION SET NLS_COMP=BINARY;


    그렇다면 다음을 살펴보자.

    KO16MSWIN949 데이터베이스에서:

    SQL> SELECT text FROM nlstech_sample_comp_ms949;

    TEXT

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

    루비

    다대기

    두상이크다

    병원

    도망

    나름대로

    도도하다

    똠방각하

    거위

    도시남녀

    너도밤나무

     

    TEXT

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

    구씨가족

    라면

     

    13 개의 행이 선택되었습니다.


    정렬되지 않은 이 문자열들의 집합에서 ‘ㄷ’ 에 속한 문자열들만을 추출하기를 원한다고 치자.
    그렇다면 먼저 다음과 같이 시도해 볼 수 있다.

    SQL> SELECT text FROM nlstech_sample_comp_ms949 WHERE text >= '다' AND text < '라';

     

    TEXT

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

    다대기

    두상이크다

    도망

    도도하다

    도시남녀


    제대로 된 것 같지만,똠방각하가 누락되었다. 그 이유는 짐작할 수 있을 것이다.
    KO16KSC5601에 포함되어 있지 않은이라는 글자는 아쉽게도 이진 정렬에서는 제대로 된 결과를 보여주지 못한다.
    그래서 이번에는 다음과 같이 시도해 보기로 한다.

    SQL> ALTER SESSION SET NLS_SORT='KOREAN_M';

    세션이 변경되었습니다.

    SQL> SELECT text FROM nlstech_sample_comp_ms949 WHERE text >= '다' AND text < '라';

     

    TEXT

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

    다대기

    두상이크다

    도망

    도도하다

    도시남녀


    애석하게도 결과는 똑같다. 분명히 NLS_SORT 값을 ‘KOREAN_M’으로 설정하여 정렬 시 유니코드 이진 값을 기준으로 정렬할 것을 명시했지만, 비교 연산자의 수행 시, 이 원칙이 지켜지지 않았다. 그래서 이번에는 비교 연산을 수행할 때, NLS_SORT에서 명시한 언어별 정렬 방식을 사용할 것을 명령해 보자.

    SQL> ALTER SESSION SET NLS_SORT='KOREAN_M';

     

    세션이 변경되었습니다.

     

    SQL> ALTER SESSION SET NLS_COMP=ANSI;

     

    세션이 변경되었습니다.

     

    SQL> SELECT text FROM nlstech_sample_comp_ms949 WHERE text >= '다' AND text < '라';

     

    TEXT

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

    다대기

    두상이크다

    도망

    도도하다

    똠방각하

    도시남녀

     

    6 개의 행이 선택되었습니다.


    이제야 비로소 제대로 된 결과가 나오는 것을 알 수 있다.
    PartⅧ. 글을 마치며.

    이상으로 오라클이 어떤 방식으로 NLS를 지원하는가를 한국어를 중심으로 알아보았다. 쉬운 내용이지만, 그만큼 많은 사람들이 간과하기도 쉬운 것으로, 아무리"나는 한국에 존재하는 시스템만을 위한 소프트웨어 개발자로소이다"라고 해도 NLS에 대한 개념을 가진 상태에서 개발을 진행하는 것은 매우 중요하다. 오라클이라는 소프트웨어는 기본적인 설계부터 NLS를 고려해 만들어졌다. 시스템을 구축할 때 첫 출발을 잘 한다면, 그 어떤 소프트웨어보다도 강력하게 지원되는 NLS 기능을 마음껏 누릴 수 있을 것이다. 다음 회에는 조금 더 실용적인 사례를 살펴보기로 하겠다.

  • 크리에이티브 커먼즈 라이선스
    Creative Commons License