네이버 블로그와 지식인 서비스는 국내 검색엔진의 성장과 함께 해왔습니다. 최근 네이버 블로그는 서비스를 시작한 지 15년이 되었는데요. 그동안 정말 많은 변화들이 있었죠. 시간이 가면서 카카오톡, 카카오스토리, 페이스북, 트위터, 인스타그램 그리고 유튜브에 이르기까지 다양한 플랫폼이 등장했고 그에 따라 네이버와 다음 검색엔진도 변화를 겪었습니다.


#검색엔진 점유율과 유튜브의 영향력

최근 검색엔진 점유율을 보면,


1. 네이버
2. 구글
3. 다음
4. 네이트
5. 줌
네이트는 실질적으로 다음 검색결과를 따르고 있고 줌은 사용자가 많지 않기 때문에 네이버, 구글, 다음의 3파전이라고 해도 과언이 아닙니다. 특히 주목할 만한 점은 3위였던 구글이 2위로 올라섰다는 사실이죠.

안드로이드 모바일 기기의 확대로 인한 점유율과 구글 크롬 브라우저의 확대로 그런 영향이 생기지 않았나 생각되는데요. 현재, 유튜브의 급속한 성장을 고려해 볼 때 구글 검색과 유튜브 플랫폼과의 연결에 대한 부분을 생각하지 않을 수 없습니다.

SNS 트렌드는 너무도 빨리 변해서 굳건할 것 같았던 '카카오스토리'나 '페이스북'의 하락세는 앞으로 트위터나 인스타그램도 극복해야 할 한 가지 과제가 될 것으로 보입니다. 결국, 시간이 지나면 '독점'보다는 '마니아'층이 형성되어 플랫폼이 유지될 가능성이 높겠죠.

그런 면에서,

네이버라는 거대한 국내 플랫폼이 생존하기 위해서는 '차별화' 요소가 매우 중요합니다. 최근 많은 업데이트를 하면서 '기대'와 '우려'가 뒤섞여 있는데요. 각종 모바일 서비스로 사업을 확대하는 다음카카오의 비즈니스 전략을 고려해 볼 때 다른 분야, 이를테면 '해외사업'과 같은 부분들이 네이버의 '성장'을 뒷받침할 것으로 보입니다.

동영상 서비스는 넷플릭스와 유튜브의 영향을 무시할 수 없기에 국내에서는 '전문 콘텐츠'로 승부를 봐야 하고 어떻게 시청자 층을 끌어 모을 것인가 하는 점은 숙제로 남았습니다. 최근 SBS와 아프리카TV가 e-스포츠 동영상 플랫폼을 합작하면서 변화의 바람이 서서히 불고 있는데요. 이에 다른 국내 플랫폼들이 어떤 아이디어로 그 바람을 이어갈지 궁금합니다.




출처 : 링크클릭

네이버 검색 시대가 끝나간다

[조중혁 칼럼] 다시 미디어포털시대로…

인터넷 역사는 미디어 포털과 검색 포털의 왕좌 쟁탈전으로 요약할 수 있다. 

1990년대 중반부터 2000년대 초반까지는 미디어 포털이 인터넷을 지배했다. 국내외 모두 상황이 비슷했다.

하지만 2000년도 초반부터 검색 포털쪽으로 주도권이 넘어갔다. 검색 포털은 최근까지도 인터넷 황제로 군림했다.

이 흐름이 최근 다시 미디어 포털로 넘어가고 있다. 검색 포털인 네이버의 시대가 끝나가고 있다. 실제로 지난 달 발표된 모바일 앱 분석업체 와이즈앱 자료에 따르면 유튜브 이용시간은 294억분으로 1위를 차지했으며 계속적으로 상승 중이다. 반면 네이버 119억분이나 지속적으로 하락하고 있다.

포털 서비스란 개념을 처음 만든 곳은 야후였다. 야후는 인터넷을 미디어로 정의했다. 지금은 야후도 구글이나 네이버처럼 검색 창에 단어를 입력하고 그 단어가 들어간 문서를 찾는 검색 서비스를 제공하고 있다. 하지만 초기에 야후는 운영자들이 좋은 사이트와 정보를 발견해 이를 잘 편집해 나열해 주는 서비스였다.

■ '검색→동영상' 구글, 야후와 경쟁서 승리

야후는 2000년도 초반까지만 해도 기계적 검색의 중요성을 낮게 평가했다. 창업자이자 최고경영자(CEO)였던 제리양은 검색 서비스를 개발할 필요성을 못 느낀다고 여러 차례 언론을 통해 말하기도 했다. 검색은 보조 기능으로 외부에서 프로그램을 도입해 붙이는 방법으로도 충분하단 게 당시 야후의 입장이었다.

야후에 검색 기술을 제공하던 업체 중 한곳이 구글이었다. 구글은 야후의 보조 서비스였지만 높은 완성도로 많은 사랑을 받았다. 결국 구글은 야후의 우산을 벗어나 검색 서비스로 인터넷 왕좌를 차지했다.

그렇다면 당시 야후는 검색 대신 무엇이 중요하다고 생각했던 걸까? 바로 동영상이었다. 야후가 동영상에 얼마나 미래를 걸었는지 알 수 있는 사건은 브로드캐스트 (Broadcast.com) 인수 건이다. 야후는 1999년에 57억 달러(약 5조 6천억원) 라는 천문학적인 금액에 동영상 사이트 브로드캐스트를 인수했다.

이는 7년 뒤인 2006년 구글의 유튜브 인수와 비교할 수 있다. 초대형 인수 합병으로 많은 관심을 모았던 유튜브 인수 때 구글이 사용한 금액은 16억5천만 달러였다. 야후가 브로드캐스트를 인수 한 금액의 3분의 1에도 못 미친다.

이 금액도 시장에서는 비싸게 구입했다고 비판이 많았던 것을 생각해 보면 야후가 브로드캐스트 인수를 위해 얼마나 많은 돈을 지불했는지 알 수 있다. 그만큼 야후는 동영상에 미래를 걸었단 얘기다.

하지만 누구나 아는 것처럼 야후의 도전은 실패로 끝났다. 야후는 너무 시대를 앞서 갔고 빠른 성과가 나오지 않자 브로드캐스트을 점점 축소하기 시작했다. 하지만, 구글은 검색 시대에서 동영상 시대로의 전환을 슬기롭게 이어 나갔다. 검색에서 번 돈을 동영상에 투자하며 미디어 시대를 차근차근 준비해 현재의 유튜브를 만들었다.

■ 미디어 포털로 이동하는 인터넷 왕좌, 네이버의 대응은

국내도 흐름은 비슷했다. 초기 인터넷은 미디어를 지향한 다음의 일방적 승리였다. 많은 사람들이 포털다음(Daum)의 뜻을 넥스트(Next)로 알고 있지만 원래는 ‘여러 사람의 소리’라는 뜻인 다음(多音)이다. Next라는 의미는 추후 의미를 확장한 뜻이었다. 즉, 다음은 창업때부터 미디어를 지향했다.

야후와 구글과의 관계처럼 네이버는 다음의 검색 기술을 제공하던 업체에 불과했다. 하지만, 네이버는 독립했으며 한겨레신문이 DBDic을 참고해 지식인을 만들었다. 검색과 연동해 정보로 활용하며 미디어 포탈인 다음을 추월해 1등 자리에 올라 갈 수 있었다.

미디어를 추구했던 야후가 동영상 서비스를 먼저 시작한 것처럼 다음도 네이버에 비해 동영상 서비스인 ‘TV팟’을 먼저 시작했다. 하지만, 카카오에 인수되면서 동력을 잃었다. 네이버는 일반인이 자유롭게 올릴 수 있는 동영상 서비스를 시작했지만 얼마 서비스를 하지 않고 종료했고, 대신 방송국 등 전문 업체가 만든 동영상 서비스인 ‘네이버TV’에 집중했지만 콘텐츠의 다양성이 부족하고 긴 광고 시간으로 외면을 받아 구글과 다르게 미디어 시대에 적응하지 못했다.

미디어 포털에서 검색 포털로 옮겨 갔던 왕권이 다시 미디어 포털로 옮겨가고 있다. 네이버는 이런 흐름을 어떻게 극복할 수 있는지 궁금하다.

출처 : 링크클릭

* 업무의 준비

업무 시작전 간밤에 발생한 시스템별 특이사항들을 점검합니다. 이를 통해 서비스에 영향을 미칠 수 있는 사항의 경우 업무 담당자와 협의를 하여 사전 조치가 될 수 있도록 합니다

주 1회는 파트원들이 모여 현재 진행되는 업무에 대한 진척도, 이슈사항등을 공유하여 해결방안을 함께 모색합니다.


* 본격적인 업무 시작

시스템별 점검이 완료되면 사내메일 확인, 전자결재 확인을 필두로 업무를 시작합니다. 요청받은 작업 내역을 검토하고 시뮬레이션 하여 문제 발생율을 최소화 합니다. 

개선이 필요한 부분에 대해서는 내부적으로 검토후 가이드라인을 제시하여 업무 담당자가 개선할 수 있도록 지원합니다.


* 인프라 협의

인프라 신규 구성이나 변경 발생시 검토 및 협의 진행을 합니다. 업무 특성, 필요요건, 중요도 등에 따라 적절한 H/W, S/W적 구성을 하며 개선점을 제시합니다. 

물론 충분한 보안사항을 준수하여 인프라를 설계합니다. 구축 후에는 인프라에 대한 24시간 모니터링 체계를 구축하여 장애 발생시 신속한 인지/복구를 수행하고 있습니다. 

정기적으로 장비 점검 및 fail-over test를 실시하여 장애 예방을 하고 있습니다.


* 주간 실적 및 계획 리뷰

인프라 업무 보고를 통해 DB 및 서버와 네트워크에 대한 주간 실적 및 계획을 리뷰하고 계획하여 백화점 그룹사 전체의 인프라를 운영해 나갑니다. 

또한 인프라적 취약점, 이슈사항 공유를 통하여 지속적인 장애 예방활동 및 보안강화 작업으로 안정적인 서비스를 제공하고 있습니다. 

야간에 진행되는 작업에 대한 검토는 퇴근전에 수행하여 필요시 원격을 통해 지원하며 작업 완료 확인을 합니다.



- 참고 : (링크)


OAuth     Open standard for Authorization

HMAC     Hash-based Message Authentication Code

SHA     Secure Hash Algorithm



참고링크 : (링크)

개발자를 위한 인프라 기초 총정리



IT Infrastructure

개발자가 왜 인프라를 알아야 할까?

인프라 구성 요소


- 글 출처 : (링크)

'IT' 카테고리의 다른 글

기사 | '조직구조의 혁신으로 포털사이트의 1위가 된 네이버'  (0) 2019.03.13
기사분석 | '네이버 검색 시대가 끝나간다.'  (0) 2019.03.13
인프라 운영 직무  (0) 2019.03.01
OAuth  (0) 2019.02.27
OAuth 2.0  (0) 2019.02.20

기본적인 OAuth의 1.0과 2.0에 대한 정보

+) NAVER에서 사용하고 있는 내용



참고 문서(링크)

(1) NAVER D2 사이트 내의 OAuth에 대한 탄생과 사용 및 1.0과 2.0 소개

: https://d2.naver.com/helloworld/24942


(2) NAVER CLOUD PALTFORM 사이트 내의 OAuth 이용 가이드.

: https://docs.ncloud.com/ko/storage/storage-2-3.html



- 상세 내용

(1) 출처 : https://d2.naver.com/helloworld/24942

개요

네이버 클라우드 플랫폼과 네이버 클라우드 플랫폼 API를 정의하고, 네이버 클라우드 플랫폼 API 인증 방식인 OAuth 인증 방식을 소개합니다.

네이버 클라우드 플랫폼과 네이버 클라우드 플랫폼 API

네이버 클라우드 플랫폼은 NAVER의 최신 컴퓨팅 기술과 운영 노하우가 축적된 클라우드 서비스입니다. 네이버 클라우드 플랫폼에서 제공하는 여러 가지 상품군 중에 솔루션 상품을 이용할 수 있도록 제공하는 응용 프로그램 인터페이스(API)를 네이버 클라우드 플랫폼 API라고 합니다.

네이버 클라우드 플랫폼 API는 표준화된 OAuth 인증을 이용합니다. OAuth 인증 방식은 별도의 시스템이 로그인 정보를 저장하지 않도록 하고 암호화된 인증 토큰을 이용해야만 리소스에 접근할 수 있도록 허용하므로 안전합니다. 또한 다양한 플랫폼의 오픈 라이브러리(OAuth Client Library)를 사용할 수 있도록 지원합니다. 네이버 클라우드 플랫폼에서 제공하는 API는 이러한 OAuth 인증 방식의 통일된 체계를 통해 인증을 처리합니다.

OAuth 인증 방식

기존의 애플리케이션이나 웹 서비스에서는 아이디와 비밀번호를 사용하여 사용자 인증을 수행합니다. 이러한 인증 방식은 애플리케이션을 제작하거나 웹 서비스를 운영하는 회사마다 서로 다른 방법으로 사용자를 확인합니다. 하지만 표준화된 OAuth 인증 방식을 이용하면 해당 인증을 공유하는 애플리케이션이나 웹 서비스 사이에 별도의 인증이 필요 없어 개발자 입장에서는 더 간편하게 인증 절차를 구현할 수 있습니다.

3-legged OAuth 인증 방식

OAuth 인증 방식은 전형적으로 3 party system (User, Consumer, Service Provider) 형태로 고안되어 있습니다. 이러한 방식을 3-legged 인증 방식이라고 합니다.

  • User: Service Provider에 계정을 가진 사용자
  • Consumer: User를 대신하여 Service Provider에 접근하기 위해 OAuth를 사용하는 웹 서비스나 애플리케이션
  • Service Provider: OAuth를 통해 접근을 허용하는 웹 애플리케이션

2-legged OAuth 인증 방식

네이버 클라우드 플랫폼 API에서는 Consumer와 Service Provider가 동일하므로, 3-legged 인증 방식보다 간소화된 2-legged 인증 방식을 지원하고 있습니다.

즉, 3 party system 구조에서 Consumer가 생략되었다고 보면 됩니다. 2-legged OAuth 인증 방식의 흐름은 다음과 같습니다.

네이버 클라우드 플랫폼 API에서 사용하는 OAuth 버전은 OAuth 1.0a(http://tools.ietf.org/html/rfc5849)를 기반으로 합니다.

네이버 클라우드 플랫폼 API 인증 절차

OAuth 인증 방식을 적용해 네이버 클라우드 플랫폼 API를 사용하는 방법을 설명합니다.

네이버 클라우드 플랫폼 API 사용 절차

네이버 클라우드 플랫폼 API를 사용하려면 다음과 같은 과정을 거쳐야 합니다.

  • API 인증키 발급(생략 가능)
  • API URL 검증(생략 가능)
  • OAuth 인증 클라이언트 구현

API 인증키는 기본으로 발급되며 API URL 검증도 편의를 위해 제공되는 부분이므로 생략할 수 있습니다.

API 인증키 발급

네이버 클라우드 플랫폼 계정이 생성되면 기본적으로 네이버 클라우드 플랫폼 API 인증키가 하나 발급됩니다. 발급된 인증키는 네이버 클라우드 플랫폼 포털 웹사이트 로그인 후 마이페이지 > API 인증키 관리에서 확인할 수 있습니다.

인증키는 계정 생성 시 자동으로 발급되는 것 외에 사용자가 하나 더 생성할 수 있어서 두 개까지 발급받을 수 있습니다. 인증키를 사용 안 함으로 설정하거나 삭제하면 유효하지 않은 키로 인식됩니다.

API 인증키는 Access Key와 Secret Key 한 쌍으로 구성되어 있습니다. Access Key는 API를 인증할 때 파라미터로 직접 전달되며, Secret Key는 OAuth_signature 파라미터를 서명할 때 사용합니다.

API URL 검증

OAuth 인증 시 OAuth 파라미터를 전달하는 방식은 2가지입니다. HTTP 호출 시 GET 방식으로 URL 자체에 OAuth 인증에 필요한 파라미터를 붙여서 전달하는 방식과 HTTP 헤더에 넣어서 보내는 방식이 있습니다.

구분설명
목적API를 사용하기 위해 네이버 클라우드 플랫폼 사이트에서 얻어야 할 정보를 확인할 수 있습니다. 
API 인증키 값의 유효성을 확인할 수 있습니다. 
API 동작 여부와 결과를 확인할 수 있습니다.
UI 확인값URL 기능 동작 대상 URL(예: 컨테이너명/파일명) 
API URL (GET) 
http://restapi.fs.ncloud.com{URL}? 
&oauth_consumer_key={Access Key} 
&oauth_timestamp={timestamp} 
&oauth_signature={oauth signature} 
&oauth_nonce={nonce 값} 
&oauth_signature_method=HMAC-SHA1 
&oauth_version=1.0 
예) 
http://restapi.fs.ncloud.com/{컨테이너명}? 
{해당기능} 
&oauth_consumer_key={Access Key} 
&oauth_timestamp={timestamp} 
&oauth_signature={oauth signature} 
&oauth_nonce={nonce 값} 
&oauth_signature_method=HMAC-SHA1 
&oauth_version=1.0
입력값인증 Access Key 선택(예: xoiOTgN2sgjDb5oRzp8q).
인증 Secret Key(인증 Access Key 선택에 따라 자동으로 선택됨).
(예: SMSyGbEi7ucZpPnRGlomftWRNENz0TV1mMOI7MiA)
* 실제 API 사용 시 인증 Access Key와 인증 Secret Key를 사용해 인증 파라미터('oauth_'를 prefix로 사용하는 파라미터)를 생성하여 request에 추가하는데, URL 검증하기 기능을 통해 그 정보들의 구성 예를 확인할 수 있습니다.

서비스 서버에 적용되는 URL과 파라미터 관련 내용은 다음 절에서 설명합니다.

OAuth 인증 클라이언트 구현

네이버 클라우드 플랫폼 API를 사용하려면 아래 표에 명시된 URL과 필수 파라미터를 생성하여 요청해야 합니다.

구분필요 여부인자 설명
호출 URLY호출 대상 기능 URL. 형식은 http://restapi.fs.ncloud.com/{기능요청URL}?{파라미터}이다. 
예) http://restapi.fs.ncloud.com/{test}?list...
인증 파라미터YOAuth 인증을 위한 파라미터이며, 이 정보들은 header에 구성할 수 있습니다. 
oauth_consumer_key 
예) oauth_consumer_key=xoiOTgN2sgjDb5oRzp8q 
oauth_signature_method 
예) oauth_signature_method=HMAC-SHA1 
oauth_version 
예) oauth_version=1.0 
oauth_timestamp 
예) oauth_timestamp=1335419436 
oauth_nonce 
예) oauth_nonce=W4SkWT 
oauth_signature 
예) oauth_signature=qVsXa0Pf913BIhv55f06ont3aIE%3D

아래 3가지로 분류하여 소개합니다.

  • 인증 파라미터 생성 방법
  • 서명서(signature) 생성 절차 및 예제(자바)
  • 오픈 라이브러리를 이용한 인증 파라미터 생성

인증 파라미터 생성 방법

네이버 클라우드 플랫폼 API는 표준 OAuth를 지원하므로 다양한 오픈 Client Library를 사용하여 인증 파라미터를 생성할 수 있습니다.
생성된 인증 파라미터는 앞 절의 URL 예시처럼 HTTP Request에 파라미터로 추가하거나 헤더에 추가하여 사용할 수 있습니다.

구분설명
oauth_consumer_key마이페이지 > API 인증키 관리에서 생성한 Access Key 값. 
예) oauth_consumer_key=xoiOTgN2sgjDb5oRzp8q
oauth_signature_methodOAuth 서명 방식. 고정값으로 HMAC-SHA1를 사용합니다. 
예) oauth_signature_method=HMAC-SHA1
oauth_versionOAuth 버전. 고정값으로 1.0을 사용합니다. 
예) oauth_version=1.0
oauth_timestamptimestamp 값. 오픈 라이브러리를 사용할 수 있으며, 사용 예제는 "오픈 라이브러리를 이용한 인증 파라미터 생성“ 절을 참조합니다. 
예) oauth_timestamp=1335419436
oauth_nonce재전송 공격(replay attack)에 대한 예방으로 재사용 방지를 위한 일회성 파라미터입니다. 오픈 라이브러리를 사용할 수 있으며, 사용 예제는 "오픈 라이브러리를 이용한 인증 파라미터 생성“ 절을 참조합니다. 
예) oauth_nonce=W4SkWT
oauth_signature마이페이지 > API 인증키 관리에서 생성한 Secret Key를 사용한 서명이다(일회성). oauth_consumer_key와 쌍을 이룬 Secret Key여야 합니다. 
오픈 라이브러리를 사용할 수 있으며, 사용 예제는 "오픈 라이브러리를 이용한 인증 파라미터 생성" 절을 참고합니다. 
예) oauth_signature=qVsXa0Pf913BIhv55f06ont3aIE%3D

서명서(signature) 생성 절차 및 예제(자바)

서명서(signature)생성을 위한 절차를 기술합니다.

  • baseString 생성
  • signature 생성
  • 요청 URL 생성(queryString 방식 또는 Authorization Header 방식)

baseString 생성

  1. 요청 파라미터에 baseString 생성에 필요한 파라미터를 추가합니다.
  2. 요청 파라미터와 value 값을 알파벳순으로 정렬합니다.
  3. baseString = RequestMethod + '&' + oauthEncode(requestUrl) + '&' + oauthEncode(queryString)

java 예제)

  • 요청 파라미터에 baseString 생성에 필요한 파라미터를 추가합니다.
  • 요청 파라미터와 value 값을 알파벳순으로 정렬합니다.
  • 파라미터 넣는 부분은 API에서 받는 파라미터가 존재할 때만 넣습니다.

  • 예제 코드에서 사용자가 값을 변경해야 할 부분은 아래와 같습니다.

    • String consumerKey = "consumer-k1";
    • String consumerSecret = "consumer-secret1";
    • String requestUrl = " http://restapi.fs.ncloud.com/container/resource ";
    • String requestMethod = "GET";
    • requestParameters.put("list", null);
    • requestParameters.put("test_param1", Arrays.asList("a"));
    • requestParameters.put("test_param2", Arrays.asList("b2", "b1"));
    • requestParameters.put("test_param3", Arrays.asList("한글"));
    /**
     * signature base string을 만들기 위한 significantParameter 세팅
     * @param requestParameters
     * @param consumerKey
     * @return
     */
    private SortedMap<String, SortedSet<String>> getSignificantParametersForSignaturBaseString(Map<String, List<String>> requestParameters, String consumerKey) {
        SortedMap<String, SortedSet<String>> significantParameters = convertTypeToSortedMap(requestParameters);

        SortedSet<String> consumerKeySet = new TreeSet<String>();
        consumerKeySet.add(consumerKey);
        significantParameters.put("oauth_consumer_key", consumerKeySet);

        SortedSet<String> nonceSet = new TreeSet<String>();
        nonceSet.add(generateNonce());
        significantParameters.put("oauth_nonce", nonceSet);

        SortedSet<String> signatureMethodSet = new TreeSet<String>();
        signatureMethodSet.add("HMAC-SHA1");
        significantParameters.put("oauth_signature_method", signatureMethodSet);

        SortedSet<String> timestampSet = new TreeSet<String>();
        timestampSet.add(generateTimestamp());
        significantParameters.put("oauth_timestamp", timestampSet);

        SortedSet<String> versionSet = new TreeSet<String>();
        versionSet.add("1.0");
        significantParameters.put("oauth_version", versionSet);

        System.out.println("significantParameters : " + significantParameters);

        return significantParameters;
    }

    /**
     *
     * @param requestParameters
     * @return
     */
    private SortedMap<String, SortedSet<String>> convertTypeToSortedMap(Map<String, List<String>> requestParameters) {
        SortedMap<String, SortedSet<String>> significantParameters = new TreeMap<String, SortedSet<String>>();

        Iterator<String> parameterNames = requestParameters.keySet().iterator();
        while (parameterNames.hasNext()) {
            String parameterName = parameterNames.next();

            if (requestParameters.get(parameterName) == null || requestParameters.get(parameterName).size() == 0) {
                SortedSet<String> significantValues = new TreeSet<String>();
                significantValues.add("");
                significantParameters.put(parameterName, significantValues);
            } else {
                for (String parameterValue : requestParameters.get(parameterName)) {
                    if (parameterValue == null) {
                        parameterValue = "";
                    }

                    SortedSet<String> significantValues = significantParameters.get(parameterName);
                    if (significantValues == null) {
                        significantValues = new TreeSet<String>();
                        significantParameters.put(parameterName, significantValues);
                    }
                    significantValues.add(parameterValue);
                }
            }
        }
        return significantParameters;
    }

    private String generateTimestamp() {
        return Long.toString(System.currentTimeMillis() / 1000L);
    }

    private String generateNonce() {
        return Long.toString((new Random()).nextLong());
    }

a. baseString = RequestMethod + '&' + oauthEncode(requestUrl) + '&' + oauthEncode(queryString)

    /**
     * signature base string 생성을 위한 query string 생성
     * @param significantParameters
     * @return
     */
    private StringBuilder getQueryStringForBaseString(SortedMap<String, SortedSet<String>> significantParameters) {
        StringBuilder queryString = new StringBuilder();
        Iterator<Map.Entry<String, SortedSet<String>>> paramIt = significantParameters.entrySet().iterator();
        while (paramIt.hasNext()) {
            Map.Entry<String, SortedSet<String>> sortedParameter = paramIt.next();
            Iterator<String> valueIt = sortedParameter.getValue().iterator();
            while (valueIt.hasNext()) {
                String parameterValue = valueIt.next();
                queryString.append(OAuthCodec.oauthEncode(sortedParameter.getKey())).append('=').append(OAuthCodec.oauthEncode(parameterValue));
                if (paramIt.hasNext() || valueIt.hasNext()) {
                    queryString.append('&');
                }
            }
        }
        return queryString;
    }

    /**
     * signature base string 생성
     * @param requestMethod
     * @param requestUrl
     * @param significantParameters
     * @return
     */
    private String makeSignatureBaseString(String requestMethod, String requestUrl, SortedMap<String, SortedSet<String>> significantParameters) {
        StringBuilder queryString = getQueryStringForBaseString(significantParameters);

        requestUrl = normalizeUrl(requestUrl);
        requestUrl = OAuthCodec.oauthEncode(requestUrl);

        return new StringBuilder(requestMethod.toUpperCase()).append('&').append(requestUrl).append('&').append(OAuthCodec.oauthEncode(queryString.toString())).toString();
    }

    /**
     * @param url
     * @return
     */
    private String normalizeUrl(String url) {
        try {
            URL requestURL = new URL(url);
            StringBuilder normalized = new StringBuilder(requestURL.getProtocol().toLowerCase()).append("://").append(requestURL.getHost().toLowerCase());
            if ((requestURL.getPort() >= 0) && (requestURL.getPort() != requestURL.getDefaultPort())) {
                normalized.append(":").append(requestURL.getPort());
            }
            normalized.append(requestURL.getPath());
            return normalized.toString();
        } catch (MalformedURLException e) {
            throw new IllegalStateException("Illegal URL for calculating the OAuth signature.", e);
        }
    }

    class OAuthCodec extends URLCodec {

        private OAuthCodec() {
        }

        public static String oauthEncode(String value) {
            if (value == null)
                return "";
            try {
                return new String(URLCodec.encodeUrl(SAFE_CHARACTERS, value.getBytes("UTF-8")), "US-ASCII");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        public static String oauthDecode(String value) throws DecoderException {
            if (value == null)
                return "";
            try {
                return new String(URLCodec.decodeUrl(value.getBytes("US-ASCII")), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        protected static final BitSet SAFE_CHARACTERS;

        static {
            SAFE_CHARACTERS = (BitSet)URLCodec.WWW_FORM_URL.clone();
            SAFE_CHARACTERS.clear(42);
            SAFE_CHARACTERS.clear(32);
            SAFE_CHARACTERS.set(126);
        }
    }

signature 생성

Secret Key와 위에서 생성한 baseString을 가지고 HMAC SHA-1 해시알고리즘을 이용하여 서명서를 생성합니다.

java 예제)

    /**
     * base string과 consumer secret key를 가지고 signature 생성
     * @param signatureBaseString
     * @param consumerSecret
     * @return
     * @throws NoSuchAlgorithmException
     * @throws UnsupportedEncodingException
     * @throws InvalidKeyException
     */
    private String sign(String signatureBaseString, String consumerSecret) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA1");
        SecretKeySpec spec = new SecretKeySpec(new String(consumerSecret + "&").getBytes("UTF-8"), "HmacSHA1");
        mac.init(spec);
        byte text[] = signatureBaseString.getBytes("UTF-8");
        byte signatureBytes[] = mac.doFinal(text);
        signatureBytes = Base64.encodeBase64(signatureBytes);
        String signature = new String(signatureBytes, "UTF-8");
        System.out.println("signature : " + signature);
        return signature;
    }

요청 URL 생성(queryString 방식 또는 Authorization Header 방식)

java 예제)

  • queryString 방식
    /**
     * request URL 생성을 위한 query string 생성
     * @param significantParameters
     * @return
     */
    private StringBuilder getQueryStringForRequest(SortedMap<String, SortedSet<String>> significantParameters) {
        StringBuilder queryString = new StringBuilder();
        Iterator<Map.Entry<String, SortedSet<String>>> paramIt = significantParameters.entrySet().iterator();
        while (paramIt.hasNext()) {
            Map.Entry<String, SortedSet<String>> sortedParameter = paramIt.next();
            Iterator<String> valueIt = sortedParameter.getValue().iterator();
            while (valueIt.hasNext()) {
                String parameterValue = valueIt.next();
                if (StringUtils.isEmpty(parameterValue)) {
                    queryString.append(OAuthCodec.oauthEncode(sortedParameter.getKey()));
                } else {
                    queryString.append(OAuthCodec.oauthEncode(sortedParameter.getKey())).append('=').append(OAuthCodec.oauthEncode(parameterValue));
                }

                if (paramIt.hasNext() || valueIt.hasNext()) {
                    queryString.append('&');
                }
            }
        }
        return queryString;
    }

    /**
     * request URL 생성
     * @param requestUrl
     * @param significantParameters
     * @param signature
     * @return
     * @throws UnsupportedEncodingException
     */
    private String makeRequestUrl(String requestUrl, SortedMap<String, SortedSet<String>> significantParameters, String signature) throws UnsupportedEncodingException {
        StringBuilder queryString = getQueryStringForRequest(significantParameters);
        queryString.append('&').append("oauth_signature").append("=").append(URLEncoder.encode(signature, "UTF-8"));
        return new StringBuffer(requestUrl).append("?").append(queryString.toString()).toString();
    }

    /**
     * queryString 방식 요청 수행
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    public void request() throws IOException, NoSuchAlgorithmException, InvalidKeyException {

        String consumerKey = "consumer-k1";
        String consumerSecret = "consumer-secret1";
        String requestUrl = " http://restapi.fs.ncloud.com/container/resource";
        String requestMethod = "GET";
        Map<String, List<String>> requestParameters = new HashMap<String, List<String>>();
        requestParameters.put("list", null);
        requestParameters.put("test_param1", Arrays.asList("a"));
        requestParameters.put("test_param2", Arrays.asList("b2", "b1"));
        requestParameters.put("test_param3", Arrays.asList("한글"));

        SortedMap<String, SortedSet<String>> significantParameters = getSignificantParametersForSignaturBaseString(requestParameters, consumerKey);
        String baseString = makeSignatureBaseString(requestMethod, requestUrl, significantParameters);
        String signature = sign(baseString, consumerSecret);
        String signedUrl = makeRequestUrl(requestUrl, significantParameters, signature);
        System.out.println("signedUrl : " + signedUrl);

        URL obj = new URL(signedUrl);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();
        con.setRequestMethod(requestMethod);

        int responseCode = con.getResponseCode();
        System.out.println("Response Code : " + responseCode);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        System.out.println(response.toString());
    }
  • Authorization Header 방식
    /**
     * @param significantParameters
     * @param signature
     * @return
     * @throws UnsupportedEncodingException
     */
    private String makeAuthorizationHeader(SortedMap<String, SortedSet<String>> significantParameters, String signature) throws UnsupportedEncodingException {
        StringBuilder authorizationHeaderString = new StringBuilder();

        Iterator<Map.Entry<String, SortedSet<String>>> paramIt = significantParameters.entrySet().iterator();
        while (paramIt.hasNext()) {
            Map.Entry<String, SortedSet<String>> sortedParameter = paramIt.next();
            int valueSize = sortedParameter.getValue().size();
            Iterator<String> valueIt = sortedParameter.getValue().iterator();

            if ("oauth_consumer_key".equals(sortedParameter.getKey())) {
                if (valueSize != 1) {
                    throw new IllegalArgumentException("oauth_consumer_key is empty or one more value.");
                }
                authorizationHeaderString.append("oauth_consumer_key").append('=').append('\"').append(valueIt.next()).append('\"');
                authorizationHeaderString.append(", ");
            }

            if ("oauth_nonce".equals(sortedParameter.getKey())) {
                if (valueSize != 1) {
                    throw new IllegalArgumentException("oauth_nonce is empty or one more value.");
                }
                authorizationHeaderString.append("oauth_nonce").append('=').append('\"').append(valueIt.next()).append('\"');
                authorizationHeaderString.append(", ");
            }

            if ("oauth_signature_method".equals(sortedParameter.getKey())) {
                if (valueSize != 1) {
                    throw new IllegalArgumentException("oauth_signature_method is empty or one more value.");
                }
                authorizationHeaderString.append("oauth_signature_method").append('=').append('\"').append(valueIt.next()).append('\"');
                authorizationHeaderString.append(", ");
            }

            if ("oauth_timestamp".equals(sortedParameter.getKey())) {
                if (valueSize != 1) {
                    throw new IllegalArgumentException("oauth_timestamp is empty or one more value.");
                }
                authorizationHeaderString.append("oauth_timestamp").append('=').append('\"').append(valueIt.next()).append('\"');
                authorizationHeaderString.append(", ");
            }

            if ("oauth_version".equals(sortedParameter.getKey())) {
                if (valueSize != 1) {
                    throw new IllegalArgumentException("oauth_version is empty or one more value.");
                }
                authorizationHeaderString.append("oauth_version").append('=').append('\"').append(valueIt.next()).append('\"');
                authorizationHeaderString.append(", ");
            }
        }

        return new StringBuffer().append("OAuth ").append(authorizationHeaderString.toString()).append("oauth_signature").append('=').append('\"').append(URLEncoder.encode(signature, "UTF-8")).append('\"').toString();
    }

    /**
     * Authorization Header 방식 요청 수행
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    public void request2() throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        String consumerKey = "consumer-k1";
        String consumerSecret = "consumer-secret1";
        String requestUrl = " http://restapi.fs.ncloud.com/container/resource";
        String requestMethod = "GET";
        Map<String, List<String>> requestParameters = new HashMap<String, List<String>>();
        requestParameters.put("list", null);
        requestParameters.put("test_param1", Arrays.asList("a"));
        requestParameters.put("test_param2", Arrays.asList("b2", "b1"));
        requestParameters.put("test_param3", Arrays.asList("한글"));

        SortedMap<String, SortedSet<String>> significantParameters = getSignificantParametersForSignaturBaseString(requestParameters, consumerKey);
        String baseString = makeSignatureBaseString(requestMethod, requestUrl, significantParameters);
        String signature = sign(baseString, consumerSecret);

        StringBuilder queryString = getQueryStringForRequest(convertTypeToSortedMap(requestParameters));
        requestUrl += "?" + queryString.toString();
        System.out.println("requestUrl : " + requestUrl);

        String authorizationHeaderValue = makeAuthorizationHeader(significantParameters, signature);
        System.out.println("authorizationHeaderValue : " + authorizationHeaderValue);

        URL obj = new URL(requestUrl);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();
        con.setRequestMethod(requestMethod);
        con.setRequestProperty("Authorization", authorizationHeaderValue);

        int responseCode = con.getResponseCode();
        System.out.println("response Code : " + responseCode);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        System.out.println(response.toString());
    }

오픈 라이브러리를 이용한 인증 파라미터 생성

인증 파라미터를 생성할 때 오픈 라이브러리를 활용할 수 있습니다. 
이 예제에서는 signpost-core 라이브러리를 사용합니다. 
이 라이브러리를 사용하게 되면 헤더에 인증 파라미터가 붙어서 전달됩니다.

signpost-core 라이브러리를 직접 다운로드하거나, Maven을 사용한다면 pom.xml에 아래와 같이 설정합니다.

<dependency>
   <groupId>oauth.signpost</groupId>
   <artifactId>signpost-core</artifactId>
   <version>1.2</version>
   <scope>compile</scope>
</dependency>

다음의 예제 코드를 참고하여 OAuth 인증 파라미터를 포함한 HTTP 요청을 생성합니다.

  • 예제 코드에서 사용자가 값을 변경해야 할 부분은 아래와 같습니다.
    • String consumerKey = "consumer-k1";
    • String consumerSecret = "consumer-secret1";
    • String requestUrl = " http://restapi.fs.ncloud.com/container/resource";
    • String requestMethod = "GET";
    • requestParameters.put("list", null);
    • requestParameters.put("test_param1", "a");
    • requestParameters.put("test_param2", Arrays.asList("b2", "b1"));
    /**
     * 라이브러리 요청 수행
     * @throws IOException
     * @throws OAuthMessageSignerException
     * @throws OAuthExpectationFailedException
     * @throws OAuthCommunicationException
     */
    public void request() throws IOException, OAuthMessageSignerException, OAuthExpectationFailedException, OAuthCommunicationException {

        String consumerKey = "consumer-k1";
        String consumerSecret = "consumer-secret1";
        String requestUrl = " http://restapi.fs.ncloud.com/container/resource";
        String requestMethod = "GET";

        Map<String, Object> requestParameters = new HashMap<String, Object>();
        requestParameters.put("list", null);
        requestParameters.put("test_param1", "a");
        requestParameters.put("test_param2", Arrays.asList("b2", "b1"));

        StringBuffer queryString = new StringBuffer();
        int index= 1;
        int requestParamSize = requestParameters.size();
        for (String key : requestParameters.keySet()) {
            if (index == 1) {
                queryString.append("?");
            }
            queryString.append(key);

            if (requestParameters.get(key) != null) {
                queryString.append('=');
                queryString.append(URLEncoder.encode(requestParameters.get(key).toString(), "UTF-8"));
            }
            if (index!= requestParamSize) {
                queryString.append("&");
                index ++;
            }
        }

        OAuthConsumer consumer = new DefaultOAuthConsumer(consumerKey, consumerSecret);

        URL url = new URL(requestUrl + queryString.toString());
        HttpURLConnection con = (HttpURLConnection)url.openConnection();

        con.setRequestMethod(requestMethod);
        con.setConnectTimeout(500000);
        con.setUseCaches(false);
        con.setDefaultUseCaches(false);

        consumer.sign(con);

        con.connect();

        int responseCode = con.getResponseCode();
        System.out.println("ResponseCode : " + responseCode);

        if (responseCode == HttpURLConnection.HTTP_OK) {
            BufferedReader resultReader = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));
            StringBuffer requestResult = new StringBuffer();
            String readString = null;
            while ((readString = resultReader.readLine()) != null) {
                requestResult.append(readString);
            }

            System.out.println(requestResult.toString());
        }
    }

부록. 약어/용어

약어 정리

OAuth     Open standard for Authorization

HMAC     Hash-based Message Authentication Code

SHA     Secure Hash Algorithm

연관 정보 바로가기

아래 가이드에서 연관 정보를 확인할 수 있습니다.


현대 오토에버 2019 상반기 신입사원 수시채용 글(링크)을 보다가

'ICT - 어플리케이션 개발/운영'의 모집분야에 SAP CO 모듈 개발/운영이라는 업무를 담당하는 직무를 모집한다기에 SAP CO 모듈이라는 것에 생소한 나는 궁금한 건 바로바로 찾아봐야 하기에, 검색해봤다.

그 중 이해하기 쉽고, 기억하고 싶었던 글의 내용을 발췌해왔다. 내 스토리에도 간직하여 두고두고 읽고 싶어서.


하단의 글 출처 - https://krksap.tistory.com/307


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


SAP CO에 대해 알아보자 - 제0편 Prologue



1.SAP CO란?

CO는 Controlling에서 온 말입니다. 한국말로 번역하면 '관리회계'라는 뜻이죠. 위에 짤에도 써있듯이 이번에 살펴볼 공식 교재인 TFIN20_1의 교재 이름도 'Management Accounting I'이라고 되어 있어요. 말 그대로 '관리회계'입니다.


그럼 '관리회계'는 무엇을 하는 걸까요?


관리회계는 SAP의 Core중 Core인 것 같아요. '의사결정'을 내리기 위한 레포트 작성이 '관리회계'입니다. 그리고 이런 '레포트'를 작성하기 위한 데이터를 체계적으로 분류하고 관리하기 위한 모듈이 CO라고 할 수 있습니다.


여기에서 말하는 레포트는 '원가 분석', '생산 관리', '매출 이익률 분석' 등등 여러가지 회사의 사업이나 활동 등이 회사가 돈을 얼마나 썼고 얼마나 벌었는지 이런 정보를 분석한게 레포트라고 할 수 있겠지요.



CO에 대해서 안다는 것은 아래 3가지를 알고 있으면 CO에 대해 알고 있다고 할 수 있습니다.

1.CO 마스터 데이터의 생김새와 활용 방법

2.기업 활동에 대한 데이터의 흐름

3.결산


이렇게 3가지 인데 포함하는 범위가 너무 넓어서 한번에 알기는 힘들 것 같네요.


그러면 다음편부터 하나씩 알아보도록 할게요.


SAP CO에 대해 알아보자 - 제1편 Organization Unit




이번 포스트는 제목과 마찬가지로 CO를 구성하는 Organization Unit(조직 구성 요소)에 대해 알아볼거에요. SAP FI, SD, CO 등 대부분의 모듈의 교재(공식교재 포함)에서 가장 처음에 나오는게 'Organization Unit'이에요.


인간을 구성하는 요소인 인간의 장기(위장, 심장, 대장, 소장, 비장 등)도 Organization이라고 표현 하듯이 CO를 구성 하는 요소도 Organization이라고 표현을 해요.


인체의 장기 중에 '위장', '심장' 같은게 없으면 인간으로서 역할을 제대로 할 수가 없듯이 CO도 CO를 구성하는 Organization Unit들을 잘 설정 해주고 관계를 맺어 주어야 제 역할을 할 수 있습니다. 그렇기 때문에 각각의 요소들에 대해 하나씩 알아보는게 중요하겠지요?


그래서 제1장은 항상 Organization Unit인 것 같아요. 그러면 CO의 구성 요소들에 대해 하나씩 알아볼게요.




1.Overhead Cost(경상비)

CO를 시작하면 가장 먼저 접하는 개념이 'Overhead Cost'일거에요. Overhead Cost라는 말을 살펴보면 'Over'라는 단어가 들어가 있어서 넘친다거나 '부담'을 준다는 느낌이 들어요. 그리고 'Cost'는 '비용'이지요.


회계는 궁극적으로 얼마를 썼는지(비용)와 얼마를 벌었는지(수익)를 돈으로 나타낸거라고 할 수 있죠? 이를테면 아이폰 1대 만드는데 원가가 5만원인데 판매가는 80만원이라고 하면 비용은 5만원인거고 수익은 80만원 벌은거고 순수익은 75만원이고 이런 것들을 기록한 것이 회계이지요.


'Overhead'는 사용한다는 뜻이 있어요. IT쪽을 하다보면 'CPU 오버헤드를 준다, 오버헤드가 걸렸다'이런 말들을 들어볼 수 있는데 말 그대로 '사용'한다는 뜻이에요.


'OverheadeCost'(경상비)는 말 그대로 '기업이 항상 사용하는 비용'이라는 뜻이에요.


'비용'이라는건 돈을 쓰는거지요?


기업이 돈을 쓰는데가 어디겠어요? 물건 만들어 파는 기업인 자동차 회사 같은 경우는 재료 사오는 비용인 '원가 재료비', 자동차 신제품 출시하려면 연구 해야되지요. '연구비', 사람들 쓰는데 들어가는 비용 '인건비' 그리고 영업하는데 들어가는 영업소 유지 비용 같은 '판매 관리비', TV에 CF 광고 내보내는데 드는 비용인 '광고 홍보비' 이런거지요?


그런데 이런 돈은 자동차 회사가 자동차를 만들어내고 판매 하는데 '계속 들어가는 돈'입니다. 그래서 '경상비'라는 어려운 말은 살짝 풀어보면 '계속 들어가는 돈'입니다.


CO는 이러한 '경상비'를 관리(Controlling)하는 '관리회계' 모듈이에요. 다들 잘 알겠지만 '관리'는 뭐냐면 보통 우리들이 많이 쓰는 말 중에 '돈 관리', '시간 관리'라는 말이 있지요. '관리'는 '쓸때 쓰고 아낄건 아낀다'라는 뜻이에요. 시간관리는 '시간을 써야할 때 쓰고 아껴야 할 때 아낀다'이런 뜻입니다. '경상비 관리'는 '돈을 써야 할 곳에 쓰고 아껴야 할 곳에 아낀다'라는거겠지요.


'연구비'에 돈을 더 쓸지, '홍보비'에 돈을 더 쓸지 어디에 돈을 쓰는게 더 효율적인지를 관리하는게 관리회계 모듈인 CO가 하는 일이기 때문에 Overhead Cost(경상비)라는 말이 가장 먼저 나오는 거에요.




2.CO와 FI의 관계

인터넷을 검색해 보면 보통 CO는 '관리회계'이고 Internal Reporting 목적으로 사용하고, FI는 '재무회계'라고 하고 External한 목적으로 사용한다고나와요.


FI는 기업의 성적표인 '재무제표'를 작성하기 위한 모듈 이라고 보면 됩니다. 학점이 4.5 만점에 4.0인데 중급회계는 A+고 상법은 A0이고 교양과목은 B+가 나왔다는걸 보여주는게 FI에요.


CO는 4.0이라는 점수를 받기 위해서 내가 들인 노력과 시간 이를테면 중급회계 공부하는데는 1주일에 6시간, 상법을 공부하는데는 3시간 운동하는데는 2시간인데 '상법'을 공부 할 때는 학교에서 들은 수업 말고 인터넷 강의도 들었다고 하면 3시간 중에 인터넷 강의 들은 시간과 그에 따른 비용을 얼마 썼는지에 대한 내용이에요.


이런 것들을 잘 관리 해서 내가 인터넷 강의를 1시간 더 들을건지 학원을 다닐건지 혼자 2시간을 더 공부할건지 이런 것들을 결정해서 더 좋은 학점을 받기 위한 '전략'을 세우기 위한 모듈이 CO입니다.


+ Recent posts