스토리 홈

인터뷰

피드

뉴스

개발에 관심있다면 꼭 읽어야하는 글
조회수 5348

REST 아키텍처를 훌륭하게 적용하기 위한 몇 가지 디자인 팁

최근의 서비스/애플리케이션의 개발 흐름은 멀티 플랫폼, 멀티 디바이스 시대로 넘어와 있습니다. 단순히 하나의 브라우저만 지원하면 되었던 이전과는 달리, 최근의 서버 프로그램은 여러 웹 브라우저는 물론이며, 아이폰, 안드로이드 애플리케이션과의 통신에 대응해야 합니다. 그렇기 때문에 매번 서버를 새로 만드는 수고를 들이지 않기 위해선 범용적인 사용성을 보장하는 서버 디자인이 필요합니다.REST 아키텍처는 Hypermedia API의 기본을 충실히 이행하여 만들고자 하는 시스템의 디자인 기준을 명확히 확립하고 범용성을 보장하게 해줍니다. 이번 글에선 현대 서비스 디자인을 RESTful하게 설계하는 기초적인 내용에 대해 정리하려고 합니다.REST란 무엇인가?REST는 Representational state transfer의 약자로, 월드와이드웹과 같은 분산 하이퍼미디어 시스템에서 운영되는 소프트웨어 아키텍처스타일입니다. 2000년에 Roy Fielding에 의해 처음 용어가 사용되었는데, 이 분은 HTTP/1.0, 1.1 스펙 작성에 참여했었고 아파치 HTTP 서버 프로젝트의 공동설립자이기도 합니다.REST는 HTTP/1.1 스펙과 동시에 만들어졌는데, HTTP 프로토콜을 정확히 의도에 맞게 활용하여 디자인하게 유도하고 있기 때문에 디자인 기준이 명확해지며, 의미적인 범용성을 지니므로 중간 계층의 컴포넌트들이 서비스를 최적화하는 데 도움이 됩니다. REST의 기본 원칙을 성실히 지킨 서비스 디자인은 “RESTful 하다.” 라고 흔히 표현합니다.무엇보다 이렇게 잘 디자인된 API는 서비스가 여러 플랫폼을 지원해야 할 때, 혹은 API로서 공개되어야 할 때, 설명을 간결하게 해주며 여러 가지 문제 상황을 지혜롭게 해결하기 때문에 (버전, 포맷/언어 선택과 같은) REST는 최근의 모바일, 웹 서비스 아키텍처로서 아주 중요한 역할을 하고 있습니다.중심 규칙REST에서 가장 중요하며 기본적인 규칙은 아래 두 가지입니다.URI는 정보의 자원을 표현해야 한다.자원에 대한 행위는 HTTP Method(GET, POST, PUT, DELETE 등)으로 표현한다.1번 사용자에 대해 정보를 받아야 할 때를 예를 들면, 아래와 같은 방법은 좋지 않습니다.GET /users/show/1 이와 같은 URI 방식은 REST를 제대로 적용하지 않은 구 버전의 Rails에서 흔히 볼 수 있는 URL입니다. 이 URI은 자원을 표현해야 하는 URI에 /show/ 같은 불필요한 표현이 들어가 있기 때문에 적절하지 않습니다. 본다는 것은 GET이라는 HTTP Method로 충분히 표현할 수 있기 때문이죠. 최근의 Rails는 아래와 같이 변경되었습니다.GET /users/1 자원은 크게 Collection과 Element로 나누어 표현할 수 있으며, 아래 테이블에 기초한다면 서버 대부분과의 통신 행태를 표현할 수 있습니다.ResourceGETPUTPOSTDELETERESTful Web Service HTTP methodsCollection URI, such as http://example.com/resources/컬렉션에 속한 자원들의 URI나 그 상세사항의 목록을 보여준다.전체 컬렉션은 다른 컬렉션으로 교체한다.해당 컬렉션에 속하는 새로운 자원을 생성한다. 자원의 URI는 시스템에 의해 할당된다.전체 컬렉션을 삭제한다.Element URI, such as http://example.com/resources/item17요청한 컬렉션 내 자원을 반환한다.해당 자원을 수정한다.해당 자원에 귀속되는 새로운 자원을 생성한다.해당 컬렉션내 자원을 삭제한다.이 외에도 PATCH 라는 HTTP Method에도 주목하시기 바랍니다. PUT이 해당 자원의 전체를 교체하는 의미를 지니는 대신, PATCH는 일부를 변경한다는 의미를 지니기 때문에 최근 update 이벤트에서 PUT보다 더 의미적으로 적합하다고 평가받고 있습니다. Rails도 4.0부터 PATCH가 update 이벤트의 기본 Method로 사용될 것이라 예고하고 있습니다.입력 Form은 어떻게 받아오게 하지?위의 예시를 통해 많은 행태를 표현할 수 있습니다만 새로운 아이템을 작성하거나 기존의 아이템을 수정할 때 작성/수정 Form은 어떻게 제공할지에 대한 의문을 초기에 많이 가집니다.정답은 Form 자체도 정보로 취급해야 한다는 것입니다. 서버로부터 “새로운 아이템을 작성하기 위한 Form을 GET한다”고 생각하시면 됩니다. Rails 에선 기본적인 CRUD를 제공할 때 아래와 같은 REST 인터페이스를 구성해줍니다.HTTPVerbPathactionused forGET/photosindexdisplay a list of all photosGET/photos/newnewreturn an HTML form for creating a new photoPOST/photoscreatecreate a new photoGET/photos/:idshowdisplay a specific photoGET/photos/:id/editeditreturn an HTML form for editing a photoPUT/photos/:idupdateupdate a specific photoDELETE/photos/:iddestroydelete a specific photo모바일 환경에 따라 다른 정보를 보여줘야 한다면?접속하는 환경에 따라 다른 정보를 보여줘야 할 때가 있습니다. 가령 모바일 디바이스에서 볼 때 다른 사용자 인터페이스를 제공한다든지 하는 경우인데요. 일부 애플리케이션은 독립적인 모바일 웹서비스를 개발한 후 단지 이를 이동시켜주기만 할 때가 있는데, 이는 어떤 경우에 좋지 못한 사용성을 보여줍니다. 모바일 뷰와 일반 웹페이지 뷰의 URI가 달라서 같은 정보를 공유할 때 각 환경에 적절한 디자인과 인터페이스로 보이지 않기 때문입니다.모바일에서 블로그를 구경하던 도중, 컴퓨터를 이용하고 있는 친구에게 자신이 보고 있는 내용을 보내주고 싶을 때가 있습니다. 티스토리 블로그는 모바일 뷰의 URI가 기존 URI와 달라서, 친구가 해당 URI를 데스크탑에서 열어도 모바일에 최적화된 정보를 받을 수밖에 없게 됩니다. 이 URI를 데스크탑에서 열어보시기 바랍니다.REST 하게 만든다면 URI는 플랫폼 중립적이어야 하며, 정보를 보여줄 때 여러 플랫폼을 구별해야 한다면 Request Header의 User-Agent 값을 참조하는 것이 좋습니다. 예를 들어 iPhone에서 보내주는 User-Agent 값은 아래와 같습니다.Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3 대부분 브라우저, OS 플랫폼은 HTTP Request를 보낼 시 보내는 주체에 대한 설명을 User-Agent에 상세하게 포함하여 통신하고 있기 때문에 요청자의 환경을 정확히 알 수 있습니다.버전과 정보 포맷을 지정할 수 있게 해야 한다면?오픈 API를 제공하거나, 클라이언트가 항상 최신 버전을 유지할 수 없는 환경이라면 서버에서 버전 협상을 지원해야 합니다. 서버가 버전 협상을 지원한다면 최신 버전으로 업데이트가 되더라도 구 버전의 정보 요청에 하위 호환하게 하여 서비스 적용범위를 넓게 유지할 수 있습니다. 이와 함께 클라이언트가 html로 정보를 받을지, json으로 받을지, xml로 받을지 선택할 수 있다면 더욱 좋을 것입니다.Header의 Accept 헤더를 이용해서 요청 환경에서 정보의 버전과 포맷을 지정할 수 있게 합니다. 아래는 Github API에 요청 시 쓰는 Accept 헤더입니다.application/vnd.github+json vnd.는 Vendor MIME Type으로, 서비스 개발자가 자신의 독자적인 포맷을 규정할 수 있게 HTTP 표준에서 제공하는 접두어입니다. vnd. 이후에 서비스 제공자 이름을 쓴 후, +로 문서의 기본 포맷을 표현해줍니다.이에 더해, Accept 헤더는 파라미터를 받을 수 있습니다. 많은 REST 지지자들은 이 파라미터를 이용해 버전 명을 지정하는 것을 권장합니다.vnd.example-com.foo+json; version=1.0 Ajax와 REST최근 빠른 속도의 웹서비스를 구현하기 위해 서비스 전체를 Ajax 통신으로 구동되게끔 HTML5 애플리케이션을 만드는 일이 많습니다. 서비스 전체를 Ajax 기반으로 구동되게 개발한다면 중복된 콘텐츠를 여러 번 전달하지 않아도 되고, 브라우저 렌더링 과정이 간소화되므로 더욱 빠른 서비스를 구축할 수 있습니다. 하지만 Ajax 기반의 서비스는 초기에 URL에 관련된 문제가 있어 REST한 서비스를 만들 때 애로사항이 있었습니다. 콘텐츠가 바뀌어도 URL은 그대로여서 친구에게 내가 보고 있는 콘텐츠를 보여줄 방법이 불편했기 때문이죠.최근엔 두 가지 방법으로 이를 보완할 수 있습니다. 첫 번째는 #! 기법으로, 구형 브라우저에서도 # 이하의 URL을 Javascript로 자유자재로 변경할 수 있다는 점을 이용한 방법입니다. 방법은 아래와 같습니다.Ajax 통신을 통해 이동되는 페이지의 URI는 현재 URI의 #! 이후에 붙인다.페이지가 처음 열릴 때, #! 이후로 URI가 붙어있다면 해당 URI로 redirect를 해준다.이와 같은 방법으로 Ajax 서비스를 만들면, 페이지를 이동한 이후에 URL을 친구에게 복사해서 전달해주어도 친구가 내가 보고 있는 콘텐츠를 볼 수 있으며, 구글에서 수집할 때 해당 #! 이하의 URL을 판별해서 제대로 수집해주기 때문에 검색엔진에도 성공적으로 노출될 수 있습니다.하지만 위 방법의 단점은 1. 상대방이 Javascript를 지원하지 않는 브라우저를 이용하거나 Javascript 기능을 꺼 놓았을 때 제대로 된 콘텐츠를 볼 수 없다는 것이며, 2. URI가 몹시 보기 지저분해진다는 것입니다. 두 번째 방법은 pushState라는 새로운 표준을 이용한 방법으로, javascript의 pushState를 통해 Ajax 통신 후에 변경된 컨텐츠의 URI을 제대로 바꿔줄 수 있습니다. 하지만 최신 표준을 지원하는 브라우저에서만 정상적으로 구동되기 때문에, 하위 호환에 신경을 써야 한다는 단점이 있습니다. pjax같은 프로젝트들이 하위 호환을 포함하여 이런 구현을 쉽게 하도록 도와주고 있습니다.언어언어별로 다른 URI의 서비스를 가지는 서비스들을 종종 볼 수 있습니다. 역시 좋지 못한 설계입니다. 한국어로 작성된 컨텐츠를 보고 있는 중 해당 콘텐츠를 미국인 친구에게 보여줄 일이 생겼다고 가정해봅시다. 단순히 URI를 복사해서 주는 것으로는 미국인 친구에게 내가 보고 있는 정보를 제대로 전달해줄 수 없다면 아주 불편할 것입니다.Request Header의 Accept-Language는 받고자 하는 언어를 명시하고 있습니다. 대부분 브라우저, OS 플랫폼은 사용자가 즐겨 쓰는 언어를 이 Header에 포함하여 요청을 만들고 있기 때문에, 해당 Header를 참조하여 그에 걸맞은 언어를 제공해주는 것이 가장 정확한 서비스를 제공해줄 수 있습니다.물론 이 방법만으로 부족한 점이 있습니다. 자신의 주 언어와 다른 언어의 세팅을 가지고 서비스를 이용하는 사용자도 있으며, 몇 가지 이유 때문에 해당 사이트만, 해당 순간에만 다른 언어로 정해서 보고 싶을 수 있기 때문입니다. 아쉽게도 일반적인 브라우저에서 언어 변경을 하는 인터페이스는 매우 불편하고 찾기 어렵게 되어있기 때문에, 서비스에서 이에 대한 추가 인터페이스를 제공해주지 않으면 일부 사용자를 잃거나 불편하게 할 수 있습니다. 보통은 Accept-Language보다 우선해서 적용하는 언어 옵션을 세션에 저장할 수 있게 하고, 이에 대한 변경 인터페이스를 서비스에서 제공해주는 식으로 문제를 해결할 수 있습니다.마치며REST는 여러 가지 서비스 디자인에서 생길 수 있는 문제를 최소화해주고, HTTP 프로토콜의 표준을 최대한 활용하여 여러 추가적인 장점을 함께 가져갈 수 있게 해줍니다. 이번 글에서는 REST하지 않은 서버 설계를 통해 생길 수 있는 실질적인 문제들을 제시하고 REST 아키텍처가 이를 어떻게 해결해주는지 함께 보았습니다.‘REST가 완전한 정답이냐?’라고 한다면 이에 대해서는 아직 논의가 남아있습니다. 구형 브라우저가 아직 제대로 지원해주지 못하는 부분이 분명히 있으며 (PUT, DELETE를 사용하지 못하는 점, pushState를 지원하지 않는 점) 브라우저를 통해 테스트할 일이 많은 서비스라면 쉽게 고칠 수 있는 URL보다 Header 값은 왠지 더 어렵게 느껴지기도 합니다.만일, 만들고 있는 서비스의 API가 널리 쓰여야 한다면 REST를 완전하게 적용한 디자인이 더 독이 될 수 있습니다. 많은 개발자는 별로 똑똑하지 못하며, HTTP 프로토콜에 대한 이해가 부족하여서 API가 어렵게 느껴질 수 있기 때문입니다. 그러므로 Google을 포함한 많은 기업의 서비스 API가 REST 스타일을 완전히 따르고 있진 않습니다.하지만 그럼에도 REST가 중요한 점은, 이를 제대로 구현하는 것이 서비스 디자인에 큰 부가이익을 가져다 줄 수 있으며, 많은 현대의 API들이 REST를 어느 정도로 충실하게 반영하느냐를 고민할 뿐이지 REST를 중심으로 디자인되고 있다는 점은 분명하기 때문입니다. REST를 얼마나 반영할 지는 API가 어떤 개발자를 범위에 두는지, 개발 기간이 얼마나 되는지, 함께 하는 동료의 역량은 어떠한지 등을 고려해서 집단마다 다르게 반영하게 될 것입니다.#스포카 #개발 #개발자 #개발팀 #꿀팁 #인사이트 #REST
조회수 11088

Next.js 튜토리얼 6편: 서버 사이드

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지 5편: 라우트 마스킹6편: 서버 사이드 - 현재 글7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요이전 편에서는 깔끔한 URL를 생성하는 방법에 대해 배웠습니다. 기본적으로 다음과 같이 생긴 URL를 가질 수 있습니다: http://localhost:3000/p/my-blog-post하지만 이 URL은 클라이언트 사이드 이동 시에만 동작합니다. 페이지를 새로고침하면 404 페이지가 표시됩니다.페이지 디렉토리에 p/my-blog-post를 부르는 실제 페이지가 없기 때문입니다.Next.js 커스텀 서버 API를 이용하여 쉽게 해결할 수 있습니다. 어떻게 구현할 수 있는지 살펴봅시다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.커스텀 서버 생성하기Express를 사용하여 애플리케이션의 커스텀 서버를 생성할 예정입니다. 간단합니다.먼저 애플리케이션에 Express를 추가해주세요:npm install —save express애플리케이션에 server.js 파일을 생성하고 다음과 같이 작성해주세요:npm dev 스크립트를 수정해주세요:이제 npm run dev 명령어로 애플리케이션을 다시 실행시켜주세요.어떤 일이 일어날까요?- 깔끔한 URL을 지원하는 서버 사이드를 추가할 것이다.- 애플리케이션이 동작하지만 서버 사이드의 깔끔한 URL은 동작하지 않는다.- "Express와 Next.js은 함께 동작할 수 없습니다"라는 에러가 발생할 것이다.- "Next.js 커스텀 서버는 프로덕션에서만 동작합니다"라는 에러가 발생할 것이다.커스텀 라우트 생성하기경험했다시피 구현한 커스텀 서버가 "next" 바이너리 명령어와 비슷하기 때문에 이전과 비슷하게 동작합니다.블로그 포스트 URL과 매치되는 커스텀 라우트를 추가해봅시다.새로운 라우트가 있는 server.js는 다음과 같습니다:다음의 코드를 살펴봅시다:단순히 기존 "/post" 페이지에 커스텀 라우트를 매핑했습니다. 또한 쿼리 매개 변수도 매핑했습니다.이게 끝입니다.애플리케이션을 다시 실행시키고 다음 페이지로 이동해주세요:http://localhost:3000/p/hello-nextjs더이상 404 페이지가 보이지 않습니다. 이제 실제 페이지를 볼 수 있습니다.하지만 작은 문제가 있습니다. 뭔지 아시나요?- 아무런 문제가 없다.- 클라이언트 사이드에서 랜더링된 제목과 서버 사이드에서 랜더링된 제목이 다르다.- 서버 사이드에서 랜더링된 페이지는 콘솔에 에러를 발생시킨다.- 클라이언트 사이드에서 랜더링된 페이지는 콘솔에 에러를 발생시킨다.URL에 있는 정보/post 페이지는 쿼리 문자열 파라미터 title을 통해 제목을 가져옵니다. 클라이언트 사이드 라우팅에서는 쉽게 URL 마스킹을 통해 적당한 값을 전달할 수 있습니다. (Link의 as prop을 통해)서버 라우트에서는 URL에 있는 블로그 포스트 ID만을 가지기 때문에 제목이 없습니다. 이 경우 ID를 서버 사이드 쿼리 문자열 파라미터로 설정합니다.다음과 같은 라우트 정의를 볼 수 있습니다:문제가 발생하지만 실제로는 ID를 사용하여 클라이언트와 서버 모두 서버에서 데이터를 가져오므로 이는 별로 문제가 되지 않습니다.그래서 ID만 필요합니다.마무리Next.js의 커스텀 서버 API를 사용한 라우트를 간단히 구현해보았습니다. 깔끔한 URL을 지원하는 서버 사이드를 추가했습니다. 원하는 대로 여러 라우트를 구현할 수 있습니다.Express를 사용하는 것에 국한되지 않습니다. 원하는 Node.js 서버 프레임워크를 사용할 수 있습니다. 커스텀 서버 API에 대한 Next.js 문서를 볼 수 있습니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 1285

[인공지능 in IT] 올림픽의 주인공은 여전히 인간이다

2011년 7월 7일 새벽 오전0시 18분, 남아프리카 공화국 더반 컨벤션센터에서 평창이라는 단어가 적힌 흰색 쪽지가 뽑힐 당시, 대한민국 국민 모두의 염원이 현실로 이뤄졌다. 88 서울 올림픽, 2002 한일 월드컵 이후 우리나라에서 열린 전세계 규모의 가장 큰 축제가 평창에서 개최됨을 알리는 순간이었다. 동계올림픽은 인종과 국가, 정치 및 이념을 초월하고 전인류의 평화와 화합을 증진시키기 위한 글로벌 겨울 축제이자 세계 젊은이들의 힘과 기록의 제전이라 할 수 있다. 1924년 프랑스 샤모니 대회를 시작으로 2018년까지 총 22회 개최, 고대 그리스의 올림피아 경기부터 시작된 '올림픽'보다 그 역사가 현저히 짧지만, 올림픽이라는 의미만은 분명하다.< 2018>올림픽에 참가하는 선수들은 그들의 육체적, 정신적인 능력의 최대치를 겨룬다. 올림픽은 인간이 가진 힘을 시험하는 장이고, 이에 관중들은 우월한 선수들의 능력에 환호성을 자아낸다. 물론, 기술이 발달하며 선수들이 입는 유니폼이나 여러 도구들이 진화를 거듭했지만, 올림픽이라는 것은 인간 본연의 모습에 큰 초점을 맞춘 시험대다. 이렇듯 'Raw'하다고 볼 수 있는 선수들의 경쟁을 비 선수 입장에서 지켜볼 수 있는 것만으로도 엄청난 기회라고 생각한다.이번 평창 동계올림픽에서 발견한 재미있는 요소 중 하나는 관중을 위한 기술의 접목이라 할 수 있다. 과학기술정보통신부는 동계올림픽과 패럴림픽 진행 중 다양한 기술을 누릴 수 있도록 '평창 ICT 올림픽 가이드북'을 발간했다. 해당 가이드북을 살펴보면, ICT 올림픽은 이번 평창 올림픽의 5대 목표 중 하나다. 인공지능, 5G, UHD, IoT, 가상현실(VR) 등을 어떻게 이용할 수 있는지 국문과 영문으로 된 가이드북을 제작하고, 평창 ICT 체험관과 인천공항 등 오프라인을 포함한 온라인 상에도 게시한다.< 2018>5대 ICT 서비스 중 가장 관심이 가는 영역은 인공지능이다. 한국을 찾는 외국인들이 느낄 수 있는 언어장벽을 완화시키기 위해 인공지능 기반 통번역서비스를 제공하고, 올림픽이 열리는 동안 경기 정보 및 교통상황을 알아보기 위해 인공지능 콜센터를 통해 24시간 문의할 수 있다. 혹자는 단순히 편의성을 위해 간단한 기술을 도입했다 할 수 있을 것이다. 하지만, 인공지능 기술은 그 자체로도 어려운 과제임과 동시에, 선수가 아닌 관람객을 위해 도입했다는 것에서 의미를 부여하고 싶다.다시 한번 언급하지만, 올림픽은 오랜 훈련을 통해 능력을 입증하고 싶은 선수들은 물론, 경기를 지켜보고 이들을 응원하는 관람객들까지 모두 '참여'하는 축제다. 한국어를 전혀 모르는 핀란드의 방송사 관계자가 통번역서비스를 활용해 아무 문제없이 스피드 스케이팅 경기를 중계할 수 있을 것이고, 대구에서 평창까지 봅슬레이 경기를 보러 가는 가족들이 편안한 운전을 위해 새벽 1시에라도 교통 정보를 얻을 수 있는 것이다.최근 인공지능을 활용한 통번역 서비스는 날이 갈수록 진화를 거듭하고 있다. 대부분의 기업에서 'NMT(Neural Machine Translation)'라고 부르는 인공신경망 기계번역을 활용하는데, NMT는 인공지능 기술을 적용해 어마어마한 양의 번역 결과 데이터를 스스로 학습하고, 다른 번역 케이스에도 적용하는 기술이다. 우리에게 많이 알려진 NMT 탑재 번역 서비스는 구글 번역과 네이버 파파고가 대표적이다. 이번 평창올림픽에서 사용되는 통번역서비스는 한글과컴퓨터가 한국전자통신연구원(ETRI)과 공동 개발한 '말랑말랑 지니톡'이다. 실제로 지니톡에는 올림픽 종목, 강원도 지역 관광지, 음식 등 동계올림픽에 관련된 데이터베이스 10만 건을 적용, 상당한 정확도를 보인다고 한다.한글과컴퓨터가 한국전자통신연구원(ETRI)과 공동 개발한 말랑말랑 지니톡, 출처: 한컴24시간 상담할 수 있는 콜센터도 인공지능은 핵심적인 역할을 담당한다. 여기서의 인공지능은 대량의 텍스트 기반 데이터를 학습하는 것이 중요하다. (1) 인식률이 높은 'DNN(Deep Neural Network)'을 토대로, (2) 한국어를 음성으로 인식할 수 있는 'STT(Speech To Text)'를 구축한 뒤(굉장히 어려운 과제다), (3) 콜센터로 들어오는 상담 내용을 분석하고, (4) 이를 제대로 학습할 수 있는 인공지능 기술을 접목해야 한다. 이 모든 것을 만족해야 콜센터에 상담을 요청하는 고객들이 인간 상담원과의 기계간의 괴리감을 적게 느낄 수 있기 때문이다.인공지능 상담 영역은 CS가 가장 활발히 이루어지는 업계 중 하나인 금융, 특히 보험 업계에서 많이 적용 중이다. AIA생명 한국지점은 인공지능을 도입해 고객과 상담하는 챗봇과 전화로 응답하는 로보텔러 서비스를 실시한다. 고객이 자주하는 문의에 대해서는 채팅 형태로 24시간 365일 인공지능 챗봇이 1차 상담을 진행하고, 대기시간 없이 바로 연결할 수 있어 생산성과 효율성, 정확도 등을 높일 수 있다. 또한, 고객에게 판매된 보험계약에 대해 로보텔러가 직접 전화를 걸어 완전 판매를 모니터링하는 업무도 진행한다. 여기는 인공지능 상담사가 학습한 대화를 기반으로 고객과 대화를 진행해 계약정보를 확인하고 계약을 확정하는 음성서비스를 적용했다.이번 평창 동계올림픽을 통해 인공지능은 그 범위를 가리지 않고 곳곳에 적용될 수 있다는 새로운 잠재력을 평가 받고 있다. 사실 최근 몇 년 사이에 IT 뿐만이 아니라 유통, 제조 등을 막론하고 기업들은 사활을 걸고 인공지능에 투자 중이다. 고도의 기술을 축적하고, 이를 적용해 새로운 사업 모델로 확장하거나, 단순반복적인 일을 대체해 비용을 줄일 수 있다는 점은, 인공지능을 하나의 패러다임으로 이끌고 있다. 물론, 이번 평창 동계올림픽에서 선보인 인공지능 기술들은 여러 기업들이 사업적인 측면을 고려한 사안일 것이다. 하지만, 인간이 주체가 되어 열리는 올림픽이라는 축제 속에서 인공지능이 한자리를 차지한다는 것은, 이제 더이상 놀랄 일은 아닐지 모른다. 결국, 인공지능도 인간의 삶을 이롭게 하기 위해 탄생된 기술이라는 것을 잊지 말자.이호진, 스켈터랩스 마케팅 매니저조원규 전 구글코리아 R&D총괄 사장을 주축으로 구글, 삼성, 카이스트 AI 랩 출신들로 구성된 인공지능 기술 기업 스켈터랩스에서 마케팅을 담당하고 있다#스켈터랩스 #기업문화 #인사이트 #경험공유 #조직문화 #인공지능기업 #기술기업
조회수 3198

Apache Spark에서 컬럼 기반 저장 포맷 Parquet(파케이) 제대로 활용하기 - VCNC Engineering Blog

VCNC에서는 데이터 분석을 위해 다양한 로그를 수집, 처리하는데 대부분은 JSON 형식의 로그 파일을 그대로 압축하여 저장해두고 Apache Spark으로 처리하고 있었습니다. 이렇게 Raw data를 바로 처리하는 방식은 ETL을 통해 데이터를 전처리하여 두는 방식과 비교하면 데이터 관리비용에서 큰 장점이 있지만, 매번 불필요하게 많은 양의 데이터를 읽어들여 처리해야 하는 아쉬움도 있었습니다.이러한 아쉬움을 해결하기 위해 여러 논의 중 데이터 저장 포맷을 Parquet로 바꿔보면 여러가지 장점이 있겠다는 의견이 나왔고, 마침 Spark에서 Parquet를 잘 지원하기 때문에 저장 포맷 변경 작업을 하게 되었습니다. 결론부터 말하자면 74%의 저장 용량 이득, 10~30배의 처리 성능 이득을 얻었고 성공적인 작업이라고 평가하지만 그 과정은 간단하지만은 않았습니다. 그 과정과 이를 통해 깨달은 점을 이 글을 통해 공유해 봅니다.Parquet(파케이)에 대해Parquet(파케이)는 나무조각을 붙여넣은 마룻바닥이라는 뜻을 가지고 있습니다. 데이터를 나무조각처럼 차곡차곡 정리해서 저장한다는 의도로 지은 이름이 아닐까 생각합니다.Parquet을 구글에서 검색하면 이와 같은 마룻바닥 사진들이 많이 나옵니다.빅데이터 처리는 보통 많은 시간과 비용이 들어가므로 압축률을 높이거나, 데이터를 효율적으로 정리해서 처리하는 데이터의 크기를 1/2 혹은 1/3로 줄일 수 있다면 이는 매우 큰 이득입니다. 데이터를 이렇게 극적으로 줄일 수 있는 아이디어 중 하나가 컬럼 기반 포맷입니다. 컬럼 기반 포맷은 같은 종류의 데이터가 모여있으므로 압축률이 더 높고, 일부 컬럼만 읽어 들일 수 있어 처리량을 줄일 수 있습니다.https://www.slideshare.net/larsgeorge/parquet-data-io-philadelphia-2013Parquet(파케이)는 하둡 생태계의 어느 프로젝트에서나 사용할 수 있는 효율적인 컬럼 기반 스토리지를 표방하고 있습니다. Twitter의 “Julien Le Dem” 와 Impala 프로젝트 Lead였던 Cloudera의 “Nong Li”가 힘을 합쳐 개발한 프로젝트로 현재는 많은 프로젝트에서 Parquet를 지원하고 컬럼 기반 포맷의 업계 표준에 가깝습니다.Parquet를 적용해보니 Apache Spark에서는, 그리고 수많은 하둡 생태계의 프로젝트들에서는 Parquet를 잘 지원합니다.val data = spark.read.parquet("PATH") data.write.parquet("PATH") Spark에서는 이런 식으로 손쉽게 parquet 파일을 읽고, 쓸 수가 있습니다. 데이터를 분석하기 전에 원본이라고 할 수 있는 gzipped text json을 읽어서 Parquet 로 저장해두고 (gzipped json은 S3에서 glacier로 이동시켜버리고), 이후에는 Parquet에서 데이터를 읽어서 처리하는 것 만으로도 저장용량과 I/O 면에서 어느 정도의 이득을 얻을 수 있었습니다. 하지만 테스트 결과 저장용량에서의 이득이 gz 23 GB 에서 Parquet 18GB 로 1/3 정도의 저장용량을 기대했던 만큼의 개선이 이루어지지는 않았습니다.Parquet Deep Dive상황을 파악하기 위해 조금 더 조사를 해보기로 하였습니다. Parquet의 포맷 스팩은 Parquet 프로젝트에서 관리되고 있고, 이의 구체적인 구현체로 parquet-mr 이나 parquet-cpp 프로젝트 등에서 스펙을 구현하고 있습니다. 그리고 특별한 경우에는 Spark에서는 Spark 내부에 구현된 VectorizedParquetRecordReader 에서 Parquet 파일을 처리하기도 합니다.파일 포맷이 바뀌거나 기능이 추가되는 경우에는 쿼리엔진에서도 이를 잘 적용해주어야 합니다. 하지만 안타깝게도 Spark은 parquet-mr 1.10 버전이 나온 시점에도 1.8 버전의 오래된 버전의 parquet-mr 코드를 사용하고 있습니다. (아마 다음 릴리즈(2.4.0)에는 1.10 버전이 적용될 것으로 보이지만)Parquet 의 메인 개발자 중에는 Impala 프로젝트의 lead도 있기 때문에, Impala에는 비교적 빠르게 변경사항이 반영되는 것에 비하면 대조적입니다. 모든 프로젝트들이 실시간적으로 유기적으로 업데이트되는 것은 힘든 일이기 때문에 어느 정도는 받아들여야겠지만, 우리가 원하는 Parquet의 장점을 취하기 위해서는 여러 가지 옵션을 조정하거나 직접 수정을 해야 합니다.VCNC 데이터팀에서는 저장 용량과 I/O 성능을 최적화하기 위하여 Parquet의Dictionary encoding (String들을 압축할 때 dictionary를 만들어서 압축하는 방식, 길고 반복되는 String이 많다면 좋은 압축률을 기대할 수 있습니다)Column pruning (필요한 컬럼만을 읽어 들이는 기법)Predicate pushdown, row group skipping (predicate, 즉 필터를 데이터를 읽어 들인 후 적용하는 것이 아니라 저장소 레벨에서 적용하는 기법)과 같은 기능들을 사용하기를 원했고, 이를 위해 여러 조사를 진행하였습니다.저장용량 줄이기102GB의 JSON 포맷 로그를 text그대로 gzip으로 압축하면 23GB가 됩니다. dictionary encoding이 잘 적용되도록 적절한 옵션 설정을 통해 Parquet로 저장하면 6GB로, 기존 압축방식보다 저장 용량을 74%나 줄일 수 있었습니다.val ndjsonDF = spark.read.schema(_schema).json("s3a://ndjson-bucket/2018/04/05") ndjsonDF. sort("userId", "objectType", "action"). coalesce(20). write. options(Map( ("compression", "gzip"), ("parquet.enable.dictionary", "true"), ("parquet.block.size", s"${32 * 1024 * 1024}"), ("parquet.page.size", s"${2 * 1024 * 1024}"), ("parquet.dictionary.page.size", s"${8 * 1024 * 1024}"), )). parquet("s3a://parquet-bucket/2018/04/05") 비트윈의 로그 데이터는 ID가 노출되지 않도록 익명화하면서 8ptza2HqTs6ZSpvmcR7Jww 와 같이 길어지기에 이러한 항목들이 dictionary encoding을 통해 효과적으로 압축되리라 기대할 수 있었고, Parquet에서는 dictionary encoding이 기본이기에 압축률 개선에 상당히 기대하고 있었습니다.하지만 parquet-mr 의 구현에서는 dictionary의 크기가 어느 정도 커지면 그 순간부터 dictionary encoding을 쓰지 않고 plain encoding으로 fallback하게 되어 있었습니다. 비트윈에서 나온 로그들은 수많은 동시접속 사용자들의 ID 갯수가 많기 때문에 dictionary의 크기가 상당히 커지는 상태였고, 결국 dictionary encoding을 사용하지 못해 압축 효율이 좋지 못한 상태였습니다.이를 해결하기 위해, parquet.block.size를 default 값인 128MB에서 32MB로 줄이고 parquet.dictionary.page.size를 default 값 1MB에서 8MB 로 늘려서 ID가 dictionary encoding으로만 잘 저장될 수 있도록 만들었습니다.처리속도 올리기저장용량이 줄어든 것으로도 네트워크 I/O가 줄어들기 때문에 처리 속도가 상당히 올라갑니다. 하지만 컬럼 기반 저장소의 장점을 온전하게 활용하기 위해서 column pruning, predicate pushdown들이 제대로 작동하는지 점검하고 싶었습니다.소스코드를 확인하고 몇 가지 테스트를 해 본 결과, Spark에서는 Parquet의 top level field에서의 column pruning은 지원하지만 nested field들에 대해서는 column pruning을 지원하지 않았습니다. 비트윈 로그에서는 nested field들을 많이 사용하고 있었기에 약간 아쉬웠으나 top level field에서의 column pruning 만으로도 어느 정도 만족스러웠고 로그의 구조도 그대로 사용할 예정입니다.Predicate pushdown도 실행시간에 크게 영향을 줄 거라 예상했습니다. 그런데 Spark 2.2.1기준으로 column pruning의 경우와 비슷하게, top level field에 대해서만 predicate pushdown이 작동하는 것을 확인할 수 있었습니다. 이는 성능에 큰 영향을 미치기에, predicate 로 자주 사용하는 column들을 top level 로 끌어올려 저장하는 작업을 하게 되었습니다. 여기에 추가로 parquet.string.min-max-statistics 옵션을 손보고 나서야 드디어 10~30배 정도의 성능 향상을 볼 수 있었습니다.매일 15분 정도 걸리던 "의심스러운 로그인 사용자" 탐지 쿼리가 30여초만에 끝나고, cs처리를 위해 한 사람의 로그만 볼 때 5분 정도 걸리던 쿼리가 30여초만에 처리되게 되었습니다.못다 한 이야기parquet.string.min-max-statistics 옵션과 row group skipping에 관해.Parquet 같은 포맷 입장에서 string 혹은 binary 필드의 순서를 판단하기는 어렵습니다. 예를 들어 글자 á 와 e 가 있을 때 어느 쪽이 더 작다고 할까요? 사전 편찬자라면 á가 더 작다고 볼 것이고, byte 표현을 보면 á는 162이고 e는 101이라 e가 더 작습니다. Parquet 같은 저장 포맷 입장에서는 binary 필드가 있다는 사실만 알고 있고, 그 필드에 무엇이 저장될지, 예를 들어 á와 e가 저장되는지, 이미지의 blob가 저장되는지는 알 수 없습니다. 그러니 순서를 어떻게 정해야 할지는 더더구나 알 수 없습니다.그래서 Parquet 내부적으로 컬럼의 min-max 값을 저장해 둘 때, 1.x 버전에서는 임의로 byte sequence를 UNSINGED 숫자로 해석해 그 컬럼의 min-max 값을 정해 저장했습니다. 이후에 이를 개선하기 위해 Ryan Blue가 PARQUET-686에서 parquet-format에 SORT_ORDER를 저장할 수 있도록 했습니다.여기에서 문제는 이전 버전과의 호환성입니다. SORT_ORDER가 없던 시절의 Parquet 파일을 읽으려 할 때, min-max 값을 사용해 row group skipping이 일어나면 query 엔진에서 올바르지 않은 결과가 나올 수 있으니, binary 필드의 min-max 값을 parquet-mr 에서 아예 반환하지 않게 되어있습니다.하지만 이는 우리가 원하는 동작이 아닙니다. 여기에 parquet.string.min-max-statistics option을 true로 설정하면, 이전처럼 binary필드의 min-max값을 리턴하게 되고 rowgroup skipping이 작동하여 쿼리 성능을 크게 올릴 수 있습니다.마치며Spark과 Parquet 모두 많은 사람이 사랑하는 훌륭한 오픈소스 프로젝트입니다. 또한 별다른 설정이나 튜닝 없이 기본 설정만으로도 잘 돌아가는 편이기 때문에 더더욱 많은 사람이 애용하는 프로젝트이기도 합니다.하지만 오픈소스는 완전하지 않습니다. 좋은 엔지니어링 팀이라면 단지 남들이 많이 쓰는 오픈소스 프로젝트들을 조합해서 사용하는 것에서 그치지 않고 핵심 원리와 내부 구조를 연구해가며 올바르게 활용해야 한다고 생각합니다. 기술의 올바른 활용을 위해 비트윈 데이터팀은 오늘도 노력하고 있습니다.

기업문화 엿볼 때, 더팀스

로그인

/