스토리 홈

인터뷰

피드

뉴스

조회수 3221

야놀자 앱은 왜 자동실행 되나요?

pluu 04 JUL 2018저는 야놀자 CX서비스실의 Android 파트에서 레이아웃 깎기와 Kotlin과 새로운 Android 기술을 전파하는 노현석입니다. 야놀자에 합류하고서 경험한 가장 독특한 케이스에 대해서 이야기해 보려고 합니다.시작은 물음표부터언제부터인가 야놀자앱을 설치하거나 업데이트하면 앱이 자동으로 실행된다는 리뷰가 들어오기 시작했습니다.네?! 그게 무슨 말이에요?안드로이드 개발을 시작한 이래로 처음 들어보는 내용이라, 원인도 정확한 해결책도 떠오르지 않는 그런 리뷰였습니다. 그래서 자연스럽게 브라우저를 켜서 구글에 검색을 먼저 해봤습니다. Android, Auto Start, Install 등 다양한 검색 결과로 일정한 패턴의 내용을 확인할 수 있습니다.  Intent Action 관련 내용android.intent.action.PACKAGE_ADDEDandroid.intent.action.PACKAGE_CHANGEDetc.Broadcast Receiveretc.일반적으로 안드로이드 앱이 설치 및 업데이트될 때 발생하는 이벤트(이하 Broadcast)를 받는 방법에 대한 설명이 많습니다. Broadcast는 배터리 변화, 전화 여부, 와이파이 등 시스템의 상태 변화를 감지하거나 서비스 내부적에서 이벤트를 전달하기 위해 사용합니다. ???? 실질적인 해결책은 되지 않지만, 범위를 좁혀서 찾아볼 포인트로 Intent 의PACKAGE관련 액션을 포커스로 잡았습니다. 하지만, 야놀자앱에서는 마케팅 성과 측정을 위해com.android.vending.INSTALL_REFERRER를 광고 트래킹 SDK에서 사용하는 것 이외에는 별도의 작업을 하지는 않습니다. 그러나, 이를 알 리가 없는 사용자는야놀자 앱이 일으키는 문제라고 인지하기 쉽습니다.  일차적으로, 어느 경로를 통해서인지는 모르지만 누군가가 야놀자 앱을 실행하는 것이라고 생각했습니다.야놀자 앱 사용자의 기기에 설치된 모든 앱 리스트를 받아올 수도 있고, 리퍼럴에 따른 앱 실행경로를 모두 수집할 수도 있지만, 단순히 버그를 찾기 위해 사용자의 동의 없이 정보를 수집할 수는 없기 때문에 장기전으로 돌입하게 되었습니다. 하지만 동일한 리뷰는 계속되었고 여전히 뚜렷한 해결책이 없는 채로 시간이 흘러갔습니다.  저 재현되는데요증상이 나타나지만 재현은 되지 않고, 재현 경로를 단기간에 파악하기는 어려운 과제였습니다. 한두 명에 불과하던 제보가 시간이 지날수록 Android 파트의 목을 조르듯이 점점 유입되는 횟수가 늘어만 갔습니다. 그런데 어느 날, 다른 팀의 분께서저 재현되는데요라는 한 줄기의 빛과 같은 언급을 해주셨습니다.믿고 싶지 않은 일이 현실이 되었다네? 그게 … 정말로 일어났습니다.이제부터가진짜시작역시버그는재현이되어야제대로잡을수있겠죠! 저에게는재현되는 단말이 있어요!Android에서 디버깅을 할 수 있는 다양한 수단이 있습니다. 이번 사례의 경우는Log혹은Dump를 확인해보는 선택지가 있습니다.Log민감한 정보라고 판단되는 부분은 모자이크했습니다.앱 설치 후 광고 SDK가 수집하는 것으로 보이는 Log에는 다양한 항목들이 나열되는 것을 볼 수 있습니다. 이때 설치한 앱의 정보가 SDK를 통해 특정 API로 전송되는 것도 확인할 수 있습니다. 하지만 Log는 Log일 뿐입니다.  Dumpsys이렇게 Log만으로 추적이 어려울 때, 추가적으로 시스템의 상태를 얻어내 디버깅 할 수 있는 방법이 있는데 바로dumpsys입니다. dumpsys는 Android 단말에서 실행되며 시스템 서비스에 대한 다양한 정보를 제공하는 도구입니다. ADB(Android Debug Bridge)를 사용하여 dumpsys를 호출 시 해당 단말에서 실행 중인 모든 시스템 서비스에 대한 정보를 가져올 수 있습니다. 간단하게 말하면 배터리의 잔량, 메모리 소비량, 네트워크 통신 상태 등을 명령어로 확인할 수 있습니다. dumpsys의 기능에 대해서는 방대한 설명이 필요하므로, 자세한 내용은 아래 링크로 대체합니다.  Android Developers ~ dumpsyshttps://android.googlesource.com/platform/frameworks/native/+/master/cmds/dumpsys/dumpsys.cppActivity DumpDumpsys 에서 좀 더 Activity 와 관련된 정보를 얻기 위해서는 아래의 명령어를 적용해볼 수 있습니다.// Activity Log Dump adb shell dumpsys activity activities 결과를 확인해봅니다. 아래와 같은 Activity 의 활동 이력을 얻을 수 있습니다.Activity Dump에 나타난mCallingPackage값으로 야놀자 앱을 시작시킨 앱의 패키지를 확인할 수 있습니다. 해당 패키지를 실제 Play Store에서 확인해본 결과, 사진 보정 필터앱으로 유명한카메라 앱중 하나였습니다.???? 야놀자와는 전혀 연관성이 없는 앱인데, 호출하고 있네요… ????Process ID// 애플리케이션의 Process ID 취득 adb shell ps Activity Dump에서 확인한mCallingUid는u0a423였는데, 이는 Activity를 호출한 uid 값을 가리킵니다. 실제로 Process 가 호출되는 Application ID도 카메라 앱에서 호출한 ID 정보와 일치합니다.대상 앱 자료 분석단순하게는 APK 를 분석하여 추측하는 방법이 있습니다. Android Studio 에서 제공되는Analyze APK기능을 이용하여 해당 앱에서 사용되는 서비스의 정보를 파악할 수 있습니다. 이 방법을 이용하여 문제의 앱이 사용하는 광고 SDK 서비스에서 패키지 설치/제거 관련 Broadcast Receiver를 수집하는 것을 확인 할 수 있습니다.패키지 관련 Broadcast인android.intent.action.PACKAGE_ADDED, android.intent.action.PACKAGE_REMOVED를 앱이 사용하는 것은 잘못된 것이 아닙니다. 예를 들어 런처 앱의 경우 단말기 내부의 앱 정보가 변경되었다는 이벤트를 이용하여 화면 렌더링 및 동작을 변경하는 처리를 할 수 있습니다 해당 광고 SDK의 경우에는 앱을 설치 및 실행하는 것으로 사용자에게 포인트 및 여러 혜택을 제공할 것이라고 예상할 수 있습니다.개인적인 의견으로는 사용자의 액션과 상관없이 동작하는 부분에 대해서는 분명히 Android 의 개선도 필요하다고 생각됩니다. 이런 정상 동작과 어뷰징은 아슬아슬한 경계에 있지만, 자칫 어뷰징으로 이어지는 경우 서비스의 품질이 떨어지게 되면서 사용자와 개발사 모두에게 좋지 않은 경험을 줄 뿐입니다.설마 이것도 되려나?동일 패키지명이번 포스팅을 작성하게 된 카메라 앱과 야놀자 서비스 사이에 특별한 관계가 없다면, 왜 이런 현상이 발생하는지 고민해봤습니다. SDK도 연결하지 않았다면, 앱을 추적할 수 있는 유일한 키는패키지명이지 않을까라는 생각으로 패키지명만 야놀자 앱과 동일한 샘플 앱으로 테스트해봤습니다.동일 재현 성공!!그럼… 해결… 끝?많은 사람들에게 이름이 널리 알려진 여러 서비스에서조차 이번 포스팅에서 다룬 내용과 같은 현상이 발생하고 있습니다. 발생 유무에 따른 차이점이나 현상의 인과 관계를 명확히 판단하기엔 아직 정보가 많이 부족합니다. 그리고 이번 분석에서 발견한 문제의 앱을 비롯하여 또 다른 제2, 제3의 앱들이 등장할 거란 가능성도 배제할 수 없는것이 현재 상황입니다. 슬프게도 아직 이 현상은 지금도 계속되고 있으며, 불편을 호소하는 리뷰가 등록되어 서비스 전체의 이미지와 평점을 갉아먹고 있습니다. 안드로이드 생태계가 사용자 및 서비스 제공자에게 더 유익한 방향으로 나아갔으면 하는 바람을 담아 작성했습니다.도움 주신 분동일 증상을 발견하고, 단말을 빌려주신 R&D SF팀 전호숙님같이 추적해주신 R&D CX 서비스실 유관종님Dump/Log 관련 조언을 주신 Wind River의 차영호님 (????????????)국어가 많이 부족한 저를 도와주신 리뷰어 ???????????? R&D CX 서비스실 강미경님, 송요창님, 유관종님, 유용우님, 이미혜님이번 현상 추적에 도움을 주신 분들에게 감사함을 전합니다.#야놀자 #개발자 #개발팀 #문제해결 #버그수정 #안드로이드 #인사이트 #경험공유
조회수 4080

[Tech Blog] Go 서버 개발하기

Go 서버 개발을 시작하며   특정 API만 다른 언어로 구현해서 최대의 성능을 내보자! 저희 서버는 대부분 Django framework 위에서 구현된 광고 할당 / 컨텐츠 할당 / 허니스크린 앱 서비스 이렇게 나눌 수 있는데 Python 이라는 언어 특성상 높은 성능을 기대하기가 어려웠습니다. 하지만 세가지 서비스에서 락스크린에서 어떤 컨텐츠나 광고를 보여줄지 결정하는 Allocation(할당) API 가 가장 많이 호출되고 있었는데 빈도로 보면 80% 정도로 높은 비중을 차지하고 있어서 이 Allocation API 들을 성능이 좋은 다른 언어로 구현하면 어떨까 하는 팀내 의견이 있었습니다. Why Go? 저는 예전부터 Java,  C# 등의 컴파일 언어에 익숙해서 기존 Java 와 C, 그리고 Go 라는 최근에 새로 나온 언어 중에서 아래 블로그글과 같이 여러 reference 들을 통해 성능이 좋다는 Go 로 이 API 들을 포팅하는 작업을 시작하게 되었습니다. Go 에 대한 첫 인상은 Java, C계열 언어보다 덜 verbose 보였고 python 보다는 strongly-typed, encapsulated 하다보니 자유도를 제한해서 코드를 보기 쉽게 하는 것을 선호하는 저의 성격과도 잘 맞는 언어였습니다.     출처: Carles Mateo, Performance of several languages서버 개발 환경   Server design How to import libraries  GVT (https://github.com/FiloSottile/gvt) – Go 는 vendering tool 을 통해 dependency 를 관리할 수 있습니다. GVT 의 경우 처음 도입했을 때 별로 유명하지 않았는데 사용법이 간단해서 도입하게 되었습니다. 아래와 같이 참조하고 있는 revision 을 관리해주며 update 통해서 최신 소스를 받아 올수 있습니다.   { "version": 0, "dependencies": [ { "importpath": "github.com/Buzzvil/go-env", "repository": "https://github.com/Buzzvil/go-env", "vcs": "git", "revision": "2d8489d40184a12c4d09d09ce1ff717e5dbb0745", "branch": "master", "notests": true }, ....  Design pattern  Go 언어에서는 package level cycling dependency 를 허용하지 않아서 좀더 명확한 구조를 만들기 좋았습니다. 예를들어 Service 에서는 Controller 를 참조할수 없고 Model 에서는 Controller / Service / DTO 등을 참조할수 없도록 강제했습니다. 모든 API 요청은 Route 를 통해 Controller 에게 전달되고 이 때 생성된 DTO (Data transfer object) 들을 Controller 가 직접 혹은 Service layer 에서 처리하도록 하였고 DB 에 접근할 때는 모델을 통해 혹은 직접 접근하도록 했지만 추후 구조가 복잡해지면 DB 쿼리 등을 담당하는 DAO (Data access object) 를 도입할 계획입니다   Libraries                  요소이름선택 이유NetworkGinWeb 서버이다 보니 네트워크 성능을 최우선으로 고려, 벤치마크 표를 보고 이 라이브러리를 선택Redis & cachego-redis역시 성능을 가장 중요한 지표로 보고 이 라이브러리 선택MysqlGormORM 없이는 개발하기 힘든 시대이죠. 여러 Database를 지원하고 ORM 중에서도 method chaining 을 사용하는 Gorm 을 선택Dynamoguregu dynamoAWS에서 제공하는 Dynamo 패키지를 그대로 사용하면 코드 양이 너무 많아지고 역시 method chaining 을 지원해서 선택Environment variablescaarlos0 envGo 에서는 tag 를 이용하면 좀더 코드를 간결하고 읽기 쉽게 사용할수 있는데 이 라이브러리가 환경변수를 읽어오기 쉽도록 해줌   Redis cache  func SetCache(key string, obj interface{}, expiration time.Duration) error { err := getCodec().Set(&cache.Item{ Key: key, Object: obj, Expiration: expiration, }) return err } func GetCache(key string, obj interface{}) error { return getCodec().Get(key, obj) }  Mysql  var config model.DeviceContentConfig env.GetDatabase().Where(&model.DeviceContentConfig{DeviceId: deviceId}).FirstOrInit(&config)  Dynamo if err := env.GetDynamoDb().Table(env.Config.DynamoTableProfile).Get(keyId, deviceId).All(&profiles); err == nil && len(profiles) > 0 { ... }  Environment variables  var ( Config = ServerConfigStruct{} onceConfig sync.Once ) type ( ServerConfigStruct struct { ServerEnv string `env:"SERVER_ENV"` LogLevel string .... } ) func LoadServerConfig(configDir string) { onceConfig.Do(func() {//최초 한번반 호출되도록 env.Parse(&Config) } }    Unit test   환경 구성 Test 환경에는 Redis / Mysql / Elastic search 등에 대한 independent / isolated 된 환경이 필요해서 이를 위해 docker 환경을 따로 구성하였습니다. Test case 작성은 아래와 같이 package 를 분리해서 작성했습니다.  package buzzscreen_test var ts *httptest.Server func TestMain(m *testing.M) { ts = tests.GetTestServer(m) // 환경 시작 tearDownElasticSearch := tests.SetupElasticSearch() tearDownDatabase := tests.SetupDatabase() code := m.Run() // 여기서 작성한 TestCase 들 실행 // 환경 종료 tearDownDatabase() tearDownElasticSearch() ts.Close() os.Exit(code) }  Mock server는 은 http.RoundTripper interface 를 구현해서 http.Client 의 Transport 멤버로 설정해서 구현했습니다. 아래는 Test case 작성 예제입니다.  httpClient := network.DefaultHttpClient mockServer := mock.NewTargetServer(network.GetHost(MockServerUrl)) .AddResponseHandler(&mock.ResponseHandler{ WriteToBody: func() []byte { return []byte(mockRes) }, Path: "/path", Method: http.MethodGet, }) clientPatcher := mock.PatchClient(httpClient, mockServer) defer clientPatcher.RemovePatch()  Unit test 관련해서는 내용이 방대해서 추후 다른 포스트를 통해 자세히 소개하도록 하겠습니다.  Infra API 요청 분할 AWS Application load balancer 여러 API 중에서 할당 API 를 제외한 요청은 기존의 Django 서버로 요청을 보내고 할당요청에 대해서만 Go서버로 요청을 보내도록 구현하기 위해 먼저 시도 했던 것은 AWS Application load balancer (이후 ALB) 였습니다. ALB 의 특징이 path 로 요청을 구별해서 처리할수 있었기 때문에 Allocation API 만 Go 서버 로 요청이 가도록 구현했습니다.  출처: Amazon Devops Blog, Introducing Application Load Balancer   하지만 이렇게 오랫동안 서비스 하지 못했는데 그 이유는 서버 구성이 하나 더 늘어나고 앞단에 ALB 까지 추가되다 보니 이를 관리하는데 추가 리소스가 들어가게 되어서 어떻게 하면 이러한 비용을 줄일수 있을까 고민하게 되었습니다.   Using docker & nginx  Go로 작성된 서버가 독립적인 Micro service 냐 아니면 Django 서버에서 특정 API 를 독립시켜 성능을 강화한 모듈이냐 의 정체성을 두고 생각해봤을때 후자가 조금더 적합하다보니 Go / Django 서버는 한 묶음으로 관리하는 것이 명확했습니다. Docker 를 도입하면서 nginx container 가 proxy 역할을 하고 path를 보고 Go container / Django container 로 요청을 보내는 구성을 가지게 되었습니다.  글을 마치며   시작은 미약하였으나 끝은 창대하리라 하나의 API를 이전했음에도 불구하고 Allocation API 에 대해서는 약 1/3, 서버 Instance 비용은 1/2.5 수준으로 감소했습니다.   설명: 기존 4개의 Django 인스턴스의 CPU 사용률이 모두 13% 정도 감소, Go 인스턴스의 CPU 사용율은 17% 정도   17 / (13 * 4)  ≒ 1 / 3  충분히 만족할만한 성과가 나와서 그 뒤로 몇가지 API도 Go 로 옮겼고 새로 작성하는 API 는 Go 환경 안에서 직접 구현하는 중입니다. 처음에는 호출이 많은 하나의 API 를 다른 언어로 포팅하기 위해 시작한 작업이었는데 Container 기술을 도입하는 등 서버 Infra 까지 변경하면서 상당히 큰 작업이 뒤따르게 되었습니다. 하지만 이 작업을 하면서 많은 동료들의 도움과 조언이 있었고 결국 완성할수 있었습니다. 이렇게 실험적인 도전을 성공 할수 있는 환경에 여러분을 초대하고 싶습니다! Go언어에 대한 문의나 좋은 의견도 환영합니다.
조회수 2634

Next.js 튜토리얼 5편: 라우트 마스킹

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지5편: 라우트 마스킹 - 현재 글6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요이전 편에서는 쿼리 문자열을 이용하여 동적 페이지를 생성하는 법을 배웠습니다. 생성한 블로그 게시물 중 하나에 대한 링크는 다음과 같습니다:http://localhost:3000/post?title=Hello Next.js하지만 이 URL은 구립니다.다음과 같은 URL를 가지면 어떨까요? http://localhost:3000/p/hello-nextjs더 낫지 않나요?이번 편에서 이것을 구현할 예정입니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.라우트 마스킹라우트 마스킹이라 불리는 Next.js의 특별한 기능을 사용할 예정입니다.기본적으로 애플리케이션에서 표시되는 실제 URL와 다른 URL이 브라우저에 표시됩니다.블로그 포스트 URL에 라우트 마스크를 추가해봅시다.pages/index.js에 다음과 같은 코드를 작성해주세요:다음의 코드 블럭을 살펴봅시다:<Link> 엘리먼트에서 "as"라는 또다른 prop를 사용하였습니다. 이는 브라우저에서 보여질 URL입니다. 애플리케이션에 표시되는 URL은 "href" prop에 지정되어 있습니다.첫 번째 블로그 포스트를 클릭하면 블로그 포스트로 이동할 것입니다.그 다음에 뒤로가기 버튼을 클릭하고 앞으로가기 버튼을 클릭해보세요. 무슨 일이 일어날까요?- 에러가 발생할 것이다- 인덱스 페이지로 돌아가고 포스트 페이지로 다시 이동할 것이다- 인덱스 페이지로 이동하지만 그 후에는 아무런 일도 일어나지 않을 것이다- 인덱스 페이지로 돌아가고 에러가 발생할 것이다히스토리 인식본 것처럼 라우트 마스킹은 브라우저 히스토리를 활용하여 잘 작동합니다. 해야 할 일은 링크에 "as" prop를 추가하는 것뿐입니다.새로고침하기home 페이지로 돌아가세요: http://localhost:3000/첫 번째 포스트 제목을 클릭하면 post 페이지로 이동합니다.브라우저를 새로고침하면 무슨 일이 일어날까요?- 예상대로 페이지가 첫 번째 포스트를 랜더링 할것이다- 페이지가 로드되지 않고 계속 로딩 중일 것이다- 500 에러가 발생할 것이다- 404 에러가 발생할 것이다 404서버에 불러올 페이지가 없기 때문에 404가 에러가 발생합니다. 서버는 p/hello-nextjs 페이지를 불러오려고 시도하지만 우리는 index.js와 post.js 두 개의 페이지밖에 없습니다.이 방법으로는 프로덕션으로 이 애플리케이션을 실행할 수 없습니다. 이 문제를 고쳐야 합니다.Next.js의 커스텀 서버 API는 이 문제를 해결할 수 있는 방법입니다.다음 편에서 이것을 사용하는 방법을 배울 예정입니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 1672

PyCon2017 첫번째날 후기

아침에 느지막이 일어났다. 어제 회사일로 피곤하기도 했지만 왠지 컨디션이 좋은 상태로 발표를 하러 가야지!라는 생각 때문에 깼던 잠을 다시 청했던것 같다. 일어나 아침식사를 하고 아이 둘과 와이프를 두고 집을 나섰다. 작년 파이콘에는 참가해서 티셔츠만 받고 아이들과 함께 그 옆에 있는 유아교육전을 갔었기에 이번에는 한참 전부터 와이프에게 양해를 구해둔 터였다.코엑스에 도착해서 파이콘 행사장으로 가까이 가면 갈수록 백팩을 메고, 면바지를 입고, 영어 글자가 쓰인 티셔츠를 입은 사람의 비율이 높아지는 것으로 보아 내가 제대로 찾아가고 있구나 라는 생각이 들었다.늦게 왔더니 한산하다.지난번에는 입구에서 에코백과 가방을 나눠줬던 것 같은데 이번에는 2층에서 나눠준다고 한다. 1층이 아무래도 복잡해지니 그런 것 같기도 하고, 2층에서 열리는 이벤트들에도 좀 더 관심을 가져줬으면 하는 것 같기도 하다. 우선 스피커 옷을 받고 싶어서 (솔직히 입고 다니고 싶어서) 2층에 있는 스피커방에 들어갔다.허락 받지 않고 사진찍기가 좀 그래서 옆방을 찍었다첫 번째 키노트는 놓쳤지만 두 번째 키노트는 꼭 듣고 싶었기에 간단히 인사만 하고 티셔츠를 들고 나왔다. (외국에서 오신 연사분과 영어로 대화를 나누고 있어서 자리를 피한것은 아니다.) 나가는 길에 보니 영코더(초등학교 5학년 부터 고등학생 까지 파이썬 교육을 하는 프로그램)을 진행하고 있었다. 의미있는 시도를 하고 있다는 생각이 들었다.이 친구들 2년 뒤에 나보다 잘할지도 모른다.키노트 발표장에 갔더니 아웃사이더님이 뒤에 서 게셨다. 지난 파이콘 때 뵙고 이번에 다시 뵈었으니 파이콘이 사람들을 이어주는 역할을 하는구나 싶었다.키노트에서는 현우 님의 노잼, 빅잼 발표 분석 이야기를 들을 수 있었다. 그리고 발표를 통해 괜히 이것저것 알려줘야만 할 것 같아 발표가 부담스러워지는 것 같다는 이야기를 들었다. 나 또한 뭔가 하나라도 지식을 전달해야 한다는 압박감을 느끼고 있었던 터라 현우 님의 키노트 발표를 듣고 나니 좀 더 오늘을 즐겨야겠다는 생각이 들었다.오늘은 재미있었습니다!현우님 키노트를 듣고 같은 시간(1시)에 발표를 하시는 경업님과 이한님 그리고 내일 발표이신 대명님, 파이콘 준비위원회를 하고 계신 연태님과 함께 식사를 하러 갔다. 가는 길에 두숟갈 스터디를 함께 하고 계신 현주님과 희진 님도 함께했다. 사실 이번에는 발표자도 티켓을 사야 한다고 해서 조금 삐져 있었는데 양일 점심 쿠폰을 주신다고 해서 삐진 마음이 눈 녹듯이 사라졌다.부담 부담식사를 하고 발표를 할 101방으로 들어가 봤다. 아직 아무도 없는 방이라 그런지 괜히 긴장감이 더 생기는 느낌이다. 발표 자료를 열어 처음부터 끝까지를 한번 넘겨 보고 다시 닫았다. 처음에는 가장 첫 발표라 불만이었는데 생각해보니 발표를 빨리 마치고 즐기는 게 훨씬 좋겠다는 생각이 들었다. 발표 자료를 다듬을까 하다가 집중이 되지 않아 밖으로 나갔다. “열린 공간” 현황판에 충동적으로 포스트잇을 하나 붙이고 왔다. 어차피 발표는 나중에 온라인으로도 볼 수 있으니까 사람들과 이야기를 나눠 봐야 겠다 싶었다. (내 발표에는 사람이 많이 왔으면 하면서도, 다른 사람의 발표는 온라인으로 보겠다는 이기적인 생각이라니..)진짜 궁금하긴 합니다다시 발표장으로 돌아왔다. 왠지 모르는 분들은 괜찮은데 아는 분들이 발표장에 와 계시니 괜히 더 불안하다. 다른 분들은 발표자료에 짤방도 많이 넣으셨던데.. 나는 짤방도 없는 노잼 발표인데.. 어찌해야 하나. 하지만 시간은 다가오고 발표를 시작했다.얼굴이 반짝 반짝리허설을 할 때 22분 정도 시간이 걸렸던 터라 조금 당겨서 진행을 했더니 발표를 거의 20분에 맞춰서 끝냈다. 그 뒤에 몇몇 분이 오셔서 질문을 해주셨다. 어리버리 대답을 한 것 같다. 여하튼 내 발표를 찾아오신 분들께 도움이 되었기를. 그리고 앞으로 좀 더 정확한 계산을 하시기를.대단히 발표 준비를 많이 하지도 못하면서 마음에 부담만 쌓아두고 있는 상황이었는데, 발표가 끝나니 아주 홀가분한 마음이 되었다. 발표장을 나가서 이제 부스를 돌아보기 시작했다. 매해 참여해 주고 계신 스마트스터디도 보이고 (정말 안 받고 싶은 ‘기술부채’도 받고 말았다.) 쿠팡, 레진 등 친숙한 회사들이 많이 보였다. 내년에는 우리 회사도 돈을 많이 벌어 여기에 부스를 내고 재미있는 이벤트를 하면 좋겠다는 생각이 들었다.부스를 돌아다니다가 이제 파이콘의 명물이 된 내 이름 찾기를 시작했다. 이름을 찾기가 쉽지가 않다. 매년 참여자가 늘어나서 올해는 거의 2000명에 다다른다고 하니 파이썬 커뮤니티의 성장이 놀랍다. 10년 전에 파이썬을 쓸 때에는 그리고 첫 번째 한국 파이콘이 열릴 때만 해도 꽤 마이너 한 느낌이었는데, 이제 주류가 된 것 같아 내 마음이 다 뿌듯하다. (그리고 내 밥줄이 이어질 수 있는 것 같아 역시 기쁘다)어디 한번 찾아보시라다음으로는 박영우님의 "Django admin site를 커스텀하여 적극적으로 활용하기” 발표를 들으러 갔다. (짧은 발표를 좋아한다.) 알고 있었던 것도 있었지만 커스텀이 가능한지 몰랐던 것들도 있어서 몇 개의 기능들을 킵해 두었다. 역시 컨퍼런스에 오면 내게 필요한 ‘새로운 것’에 대한 실마리를 주워가는 재미가 있다.익숙하다고 생각했지만 모르는것이 많다4시가 되어 OST(Open Space Talk)를 하기로 한 208B 방으로 조금 일찍 갔다. 주제가 뭐였는지는 잘 모르겠는데 주식 투자, Tensor Flow, 비트코인, 머신러닝 등등의 이야기들이 오가고 있었다. 4시가 되어 내가 정한 주제에 대해 관심 있는 사람들이 모였다. 괜히 모일 사람도 없는데 큰방을 잡은 것이 아닐까 하고 생각하고 있었는데, 생각보다 많은 분들이 오셨다.각 회사들이 어떤 도구를 사용하는지 설문조사도 해보고, 또 어떤 개발 방법론을 사용하는지, 코드 리뷰, QA는 어떻게 하고 있는지에 대한 이야기를 나눴다. 다양한 회사에서 다양한 일을 하는 사람들이 모여 있다 보니 생각보다 꽤 재미있게 논의가 진행되었다. 사실 내가 뭔가 말을 많이 해야 할 줄 알았는데, 이야기하고 싶은 분들이 많이 있어서 진행을 하는 역할만 하면 되었다. 마지막으로는 “우리 회사에서 잘 사용하고 있어서 다른 회사에도 추천해 주고 싶은 것”을 주제로 몇 가지 추천을 받은 것도 재미가 있었다.열심히 오간 대화를 적어두긴 했다5시에 OST를 마치고는 바로 집으로 돌아왔다. 오늘 저녁에 아이들을 잘 돌보고 집 청소도 열심히 해두어야 내일 파이콘에 참여할 수 있기 때문이다. 기대된다. 내일의 파이콘도.그리고 정말 감사드린다. 파이콘을 준비해주시고 운영해주고 계신 많은 분들께.#8퍼센트 #에잇퍼센트 #개발자 #개발 #파이썬 #Python #파이콘 #Pycon #이벤트참여 #참여후기 #후기
조회수 818

순서대로 척척, ORDER BY

ORDER BY 는 원하는 순서대로 자료를 출력하고 싶을 때 사용합니다. 편의를 위해 이전 글의 예제에서 MBR_NM 의 INDEX 인 IX_MBR_BAS_02 를 제거하고 진행하겠습니다. 이번 글에서는 이해-적용-출력-활용의 순서로 살펴볼게요. 지난 글 보기: 단일 TABLE을 SELECT하자!이해: ORDER BY의 오름차순과 내림차순SELECT     MBR_NM FROM test.TB_MBR_BAS ORDER BY     MBR_NM  ; 기본적인 ORDER BY는 위와 같이 사용합니다. 오름차순과 내림차순으로도 정렬할 수 있습니다. 오름차순일 때는 컬럼 뒤에 옵션을 넣지 않거나 ASC를 사용하고, 반대로 내림차순일 때는 DESC를 사용하면 됩니다.[오름차순]ORDER BY      MBR_NM ORDER BY      MBR_NM ASC [내림차순]ORDER BY      MBR_NM DESC 위의 Query(오름차순) 의 실행계획을 보면 아래와 같이 표시됩니다.결과는 다음과 같습니다. (수행시간 3초)내림차순 Query의 실행 계획을 보면 아래와 같이 표시됩니다.결과는 다음과 같습니다. (수행시간 3초)오름차순과 내림차순 정렬 Query를 보면 실행계획은 같고 결과는 다르게 나타납니다.실행계획을 보면 이렇게 표시됩니다.- table : TB_MBR_BAS - type : ALL - Extra : using filesort Extra의 using filesort는 DBMS에서 정렬을 한다는 의미로 퀵소트 알고리즘을 사용합니다. 실행계획의 내용을 풀어보면 “TB_MBR_BAS 을 전부(ALL) 읽은 후 정렬한다(using filesort)” 정도로 보면 됩니다.적용: INDEX와 정렬의 관계이번에는 삭제했던 MBR_NM의 INDEX인 IX_MBR_BAS_02를 다시 생성하고 수행해보겠습니다.CREATE INDEX IX_MBR_BAS_02 ON test.TB_MBR_BAS (MBR_NM); SELECT     MBR_NM FROM test.TB_MBR_BAS ORDER BY     MBR_NM  ; INDEX를 생성하고 실행계획을 보면 아래와 같이 표시됩니다.실행계획을 보면 몇 가지 달라진 게 눈에 띕니다.1. type : ALL -> index 2. key : 없음 -> IX_MBR_BAS_02 3. Extra : using filesort -> Using index 특히 Extra는 using filesort에서 Using index 로 바뀐 것을 알 수 있습니다. using filesort가 정렬을 한다는 것인데, 정렬을 하지 않고 어떻게 정렬해서 보여준다는 것일까요? INDEX를 이해하면 바로 알 수 있습니다. 일반적인 INDEX는 기본이 BTree INDEX 입니다. MySQL의 BTree INDEX는 오름차순 정렬 상태로 저장되어 있습니다. 이미 정렬한 상태로 저장되어 있는 INDEX를 사용하기 때문에 Query를 수행할 때 다시 정렬할 필요가 없죠. 그래서 using filesort가 나타나지 않는 겁니다.출력: Query 실행다음으로 성이 김 씨인 사람들의 이름을 순서대로 출력해보겠습니다. 여기서는 두 가지 Query를 이용해 비교해보겠습니다.예시 1)SELECT     MBR_NM FROM test.TB_MBR_BAS WHERE MBR_NM LIKE '김%' ORDER BY     MBR_NM  ; 예시 2)SELECT     MBR_NM FROM test.TB_MBR_BAS WHERE SUBSTR(MBR_NM,1,1) = '김' ORDER BY     MBR_NM  ; 예시를 보면 WHERE 절이 다릅니다. 예시1은 “MBR_NM이 ‘김’으로 시작하는 것을 오름차순 정렬해 보여주라는 것”이고, 예시2는 “MBR_NM의 첫 번째 글자가 ‘김’인 것을 오름차순 정렬해 보여주라는 것”입니다.이제 두 개의 Query 실행계획을 비교해보겠습니다.예시 1)예시 2)여기서 주의 깊게 봐야 할 컬럼은 type입니다. 다른 컬럼들은 TB_MBR_BAS의 테이블을 조회하면서 IX_MBR_BAS_02 INDEX만을 사용해 보여주겠다는 내용을 갖고 있습니다. IX_MBR_MAS_02 INDEX가 MBR_NM으로 정렬되어 있기 때문에 using filesort가 나타나지 않은 것입니다. 그렇다면 type에 range와 index는 어떤 차이가 있는 것일까요?range : where 조건에 조회하는 범위가 지정된 경우 나타납니다.예시1은 TB_MBR_BAS를 조회하는데 IX_MBR_BAS_02 INDEX의 MBR_NM에서 ‘김’이 시작되는 위치부터 끝나는 위치까지 조회해 보여주라는 의미입니다. IX_MBR_BAS_02 INDEX를 이용해 ‘김’이 시작되는 위치로 바로 접근할 수 있는 것이 핵심입니다.index : index를 처음부터 끝까지 읽는다는 의미입니다.예시2는 TB_MBR_BAS를 조회하는데 IX_MBR_BAS_02 INDEX를 순서대로 읽어서 MBR_NM의 첫 글자가 ‘김’인 것을 보여주라는 의미입니다.두 개의 차이점을 꼽자면, range는 원하는 범위로 바로 접근해 값을 가져올 수 있는 것이고, index는 처음부터 끝까지 읽어서 그 값이 조건에 맞을 경우 가져오라는 것입니다. 따라서 예시1이 휠씬 성능이 뛰어난 Query라고 볼 수 있습니다. 결과는 모두 아래와 같이 출력됩니다.수행시간은 차이를 보였습니다. 예시1은 0.0041초, 예시2는 0.5초였는데요. 예시에서는 건수가 적기 때문에 큰 차이가 없는 것처럼 보이지만, 자료가 10배 또는 100배 많아진다고 생각해보세요. 엄청난 차이겠죠.활용: Query를 만들고 DISTINCT !마지막으로 Query 하나를 만들어보겠습니다. 1) MBR_NM의 중복을 제거하고2) 김 씨이면서3) 이름이 ‘혜’로 시작하는 사람을 먼저 출력하고4) 이외의 사람은 그 다음부터 오름차순으로 출력하려면 어떻게 만들어야 할까요?중복을 제거할 때는 일반적으로 DISTINCT 와 GROUP BY 두 가지를 사용합니다. 이번 글에서는 DISTINCT를 사용하겠습니다. 다음으로는 오름차순 정렬할 때 김 씨를 먼저 출력하는 것인데 조건문을 사용하여 김 씨인 것과 아닌 것을 구별해 우선순위를 주겠습니다. 다른 것은 위의 Query를 이행하면 됩니다. 먼저 DISTINCT를 넣고 수행해 보겠습니다.SELECT     DISTINCT     MBR_NM FROM test.TB_MBR_BAS ORDER BY     MBR_NM  ; 실행계획은 다음과 같습니다.DISTINCT를 수행하면 Extra가 나타나며 group by로 표시됩니다. 여기서는 IX_MBR_BAS_02를 이용하여 gorup by(중복제거)하여 보여준다는 의미입니다. 수행하면 다음과 같은 값이 나옵니다.다음으로는 MBR_NM이 ‘김혜’로 시작하는 것을 먼저 보여주기 위해 ORDER BY 절에 CASE WHEN문을 사용하겠습니다.SELECT     DISTINCT     MBR_NM FROM test.TB_MBR_BAS ORDER BY     CASE         WHEN MBR_NM LIKE '김혜%'    THEN 0         ELSE 1     END     ,MBR_NM  ; 실행계획은 다음과 같습니다.ORDER BY에 조건이 들어가면서 INDEX의 순서대로 정렬한 것을 그대로 보여줄 수 없기 때문에 Extra에 Using temporary, Using filesort가 나타납니다. Using temporary는 가상 테이블을 만들어 사용하는 것인데, 다시 말해 가상 테이블을 만들어 다시 정렬하는 것입니다. 이에 대한 출력값은 다음과 같습니다.‘김혜’로 시작하는 사람이 먼저 나왔군요.글을 마치며지금까지 ORDER BY와 연관된 조건 처리를 알아봤습니다. 데이터를 더욱 체계적으로 나타내고 싶으신가요? ORDER BY를 이용해서 원하는 목적을 달성해보세요.글한석종 부장 | R&D 데이터팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발자 #개발팀 #인사이트 #경험공유
조회수 878

로봇 택시가 산업 지형을 바꾼다

로봇 택시가 산업 지형을 바꾼다"자율 주행 전기차의 등장으로 유가가 배럴당 25달러까지 떨어지고, 새로운 세계 질서가 도래할 것이다."'에너지 혁명 2030' 의 저자로 2015년 한국을 방문한 적 있는 토니 세바 스탠포드대 교수(RethinkX의 공동 설립자)가 최근 미국 CNBC와 인터뷰에서 "기름 수요가 2020년~2021년 사이 1백만 배럴로 꼭지를 찍은 뒤 10년 내(2030년경)로 70만 배럴까지 줄어들면서 유가가 하락세를 면치 못할 것”이라며 이같이 예견했다.점차 많은 사람들이 공유경제의 축으로 자율 주행 전기차를 사용하게 되면서 ▽오일 회사는 생존이 어렵게 되고 ▽심해, 세일가스업체와 정유 파이프라인 회사들도 문을 닫게 될 것이라는 게 그의 예측이다.차량도 소유의 대상에서 서비스의 대상, 이른바 TAAS (transport as a service) 로 바뀐다. 차량을 더 이상 구입하지 않고, 편리하게 앱으로 로봇 택시를 호출해 이용하면 그만이다. 자율 주행 전기차의 가격도 싸지면서, 전기차 이용이 소유에 비해 10배 정도 저렴해질 것이라는 것.  경제학자인 그의 섬뜩한 전망은 계속된다. 급속한 기술의 진보로 인해 값비싼 정유회사나, 적응력이 떨어지는 카 메이커(의 주식)가 매력이 없어지며, 차량과 관련된 직업 중에서 차 딜러도 2024년까지 사라질 것이라는 예상이다.뿐만 아니라, 주차공간의 80% 이상이 쓸모없게 되고, (자동차)보험의 비용도 급격하게 떨어지기 때문에 보험회사도 설 땅이 좁아진다는 것.이 같은 그의 전망이 혼자만의 견해는 아니지만,  다른 사람들은 이런 변화에 훨씬 많은 시간이 걸리고, 그렇게 극적이지도 않을 거라고 내다본다.반대로 그는 차량 관련 지출이 줄면서 가계 소비가 늘어나 경제성장을 촉진할 것이라고 예상했다. 아울러 차세대 차의 개발과 관련된 차 운영체제, 컴퓨터 플랫폼, 배터리, 지도 소프트웨어 회사들을 눈여겨 보라고 조언했다.세바 교수의  유가 하락 전망이  결코 급진적인 것은 아니다.  유가는 WTI(서부 텍사스중질유)기준으로 2016년 2월에  배럴당 20달러대 중반까지 급락한 바 있다.  6월 14일 현재는  40달러대 중반에 머물러 있다.다만, 차량이 소유의 대상에서 이용 서비스의 대상으로 바뀐다든지, 차 딜러가 사라진다든지 하는 등의 혁신적이거나 과격한 전망이 실현될 지는 두고 볼 일이다.참조 : 다음은 토니 세바 교수가 참여한 화제의 보고서 ‘Rethinking transportation 2020-2030’ 원문 링크https://static1.squarespace.com/static/585c3439be65942f022bbf9b/t/591a2e4be6f2e1c13df930c5/1494888038959/RethinkX+Report_051517.pdf
조회수 2356

CloudWatch에 대하여

OverviewAmazon Web Services(AWS)는 많은 고객들이 이용하고 있습니다. AWS를 이용하여 프로젝트를 운영하고 있다면 각종 서비스의 리소스를 모니터링 하는 게 귀찮게 느껴질 수 있습니다. 이번 글에서는 AWS 리소스를 효과적으로 모니터링할 수 있는 Cloudwatch 서비스를 소개하겠습니다.Cloudwatch는 통합 뷰를 확보하는데 필요한 데이터를 제공합니다. 뿐만 아니라 이벤트 및 리소스를 이용해 경보를 생성할 수도 있습니다.1. Events2. Logs3. Custom Metrics(맞춤형 지표) 생성하기4. Alarm 생성5. Dashboards쉬어가기: Query 언어가 지원하는 여섯 가지 명령 유형1. EventsCloudWatch Events는 정기적인 일정에서 트리거(trigger)되는 규칙을 생성할 수 있습니다.1.규칙 생성을 클릭합니다.2.대상을 호출할 일정을 설정합니다.호출 방식에는 이벤트 패턴과 일정 두 가지가 있습니다. 이벤트 패턴은 json 구조로 표현됩니다. AWS 서비스에서 발생하는 패턴과 일치하면 트리거가 동작합니다. 일정은 지정한 시간과 일치하면 트리거가 동작합니다.cron 또는 rate 표현식을 사용해 예약된 모든 이벤트는 UTC+09:00 시간대를 사용합니다. 최초 단위는 1분입니다.아래는 각각의 필드에 대한 일정 cron식 설명입니다.이번 예제에서는 특정 시간에 트리거되는 일정으로 설정하겠습니다.매일 4시에 동작하도록 설정19 + 9(UTC) - 24(하루) = 새벽 4시3.대상 추가를 선택해 호출할 대상을 지정합니다.Lambda 함수 외에 여러 서비스를 선택할 수 있지만 이번 예제에서는 Lambda 함수를 지정하여 구성하겠습니다.4.규칙의 이름과 설명을 등록하고 규칙 생성을 클릭합니다.5.규칙이 생성된 것을 볼 수 있습니다.2. LogsCloudWatch Logs는 운영 중인 애플리케이션 리소스를 기록하고 액세스할 수 있으며, 관련된 로그 데이터를 검색할 수도 있습니다.1.생성된 규칙이 지정된 시간에 동작하면 CloudWatch Logs에 로그 그룹이 생성된 걸 확인할 수 있습니다.2.Lambda 함수에서 실행된 로그 메시지를 확인할 수 있으며 필터링도 가능합니다.3.로그 그룹에 이벤트 만료 시점을 설정해 오래된 데이터는 모두 자동으로 삭제되도록 설정할 수 있습니다.3. Custom Metrics(맞춤형 지표) 생성하기모니터링하고자 하는 통계치를 직접 선정하고, CloudWatch로 보내 관리하는 지표를 생성해보겠습니다.1.Log Groups에 대한 지표를 생성하겠습니다. 해당 Log Groups에 ‘Filters’를 클릭합니다.2.’Add Metric Filter’를 클릭합니다.3.로그 지표에 대한 필터 패턴을 정의합니다.Filter Pattern* “INFO Success 200” → 세 단어를 모두 포함하는 로그 이벤트 메시지와 일치* “INFO - Start - End” → ‘INFO’ 포함된 메시지 중에 ‘Start’, ‘End’ 제외된 필터 로그 이벤트 메시지와 일치4.필터 및 지표 정보를 입력한 후 ‘Create Filter’를 클릭합니다.Metric Details* Metric Namespace → CloudWatch 지표에 대한 대상 네임 스페이스* Metric Name → 모니터링된 로그 정보가 게시되는 CloudWatch 지표의 이름* Metric Value → 일치하는 로그가 발견될 때마다 지표에 게시하는 숫자 값* Default Value → 일치하는 로그가 발견되지 않은 기간 동안 지표 필터에 보고되는 값5.두 가지 케이스의 필터를 생성했습니다.4. Alarm 생성단일 CloudWatch 지표를 감시하거나 CloudWatch 측정치를 기반으로 하는 수학 표현식의 결과를 감시하는 CloudWatch 경보를 생성할 수 있습니다. 지표가 지정된 임계값에 도달하면 자동으로 이메일을 보내는 Alarm을 만들어보겠습니다.1.추가된 지표 필터에 ‘Create Alarm’ 버튼을 클릭해 경보를 추가합니다.2.경보 세부 정보 및 수행할 작업을 정의합니다.경보 평가경보를 생성할 때, CloudWatch가 경보 상태를 변경하는 조건 세 가지에 대한 설정을 지정할 수 있습니다.기간은 경보에 대해 개별 데이터 포인트를 생성하기 위해 지표 또는 표현식을 평가하는 기간입니다. 초로 표시됩니다. 1분을 기간으로 선택하면 1분마다 하나의 데이터 포인트가 생성됩니다.Evaluation Period(평가 기간)는 경보 상태를 결정할 때 평가할 가장 최근의 기간 또는 데이터 포인트의 수입니다.Datapoints to Alarm(경보에 대한 데이터포인트)는 평가 기간에 경보가 ALARM상태에 도달하게 만드는 위반 데이터 포인트의 수입니다. 위반 데이터 포인트가 연속적일 필요는 없습니다. Evaluation Period(평가 기간)와 동일한 마지막 데이터 포인트의 수 이내면 됩니다.3.경보가 발생할 Alarm 상태와 알림 받을 이메일을 등록합니다.경보 상태/OK/ 지표 또는 표현식이 정의된 임계값 내에 있습니다./ALARM/ 지표 또는 표현식이 정의된 임계값을 벗어났습니다./INSUFFICIENT_DATA/ 경보가 방금 시작되었거나, 측정치를 사용할 수 없거나, 또는 측정치를 통해 경보 상태를 결정하는데 사용할 충분한 데이터가 없습니다.4.이메일 수신함에서 ‘AWS 알림 - 구독 확인’이라는 제목의 메일을 클릭합니다. 내용에 포함된 링크를 클릭해 알림을 수신할 것을 확인합니다. (AWS는 확인된 주소로만 알림을 전송할 수 있습니다.)5.이메일 수신함을 확인해 ‘Confirm subscription’을 클릭합니다.6.등록한 이메일이 확인되었습니다.7.AWS에 이메일이 정상적으로 등록되었는지 SNS Subscriptions 메뉴에서 확인합니다.8.Lambda를 실행해 Alarm 상태를 변경해보겠습니다.9.등록한 이메일 주소로 Alarm 메일이 도착했습니다.5. DashboardsCloudWatch를 통해 리소스를 손쉽게 모니터링할 수 있는 맞춤형 통계 기능입니다.1.Metric Filter에서 추가된 Custom Namespaces를 클릭합니다.2.생성된 Metrics를 선택한 후, Graphed metrics Tab을 클릭합니다.3.Metrics에 표시될 그래프를 설정합니다.1)그래프 제목 : testLambda12)그래프 표시 : 숫자3)그래프 라벨 : testMetrics-400, testMetrics-2004)통계 : 합계5)기간 : 1 Day4.수식을 응용하여 여러 형식의 Metrics 표현식을 추가하겠습니다.지표 수식 함수* METRICS() : 요청에 모든 지표를 반환* SUM(METRICS()) : 모든 지표의 합계* AVG(METRICS()) : 모든 지표의 평균* MIN(METRICS()) : 모든 지표의 최소값* MAX(METRICS()) : 모든 지표의 최대값* ABS(METRICS()) : 각 요소의 절대값* RATE(METRICS()) : 각 요소의 초당 변경 비율5.완성된 지표 Source를 복사합니다.{ "metrics": [ [ { "expression": "SUM(METRICS())", "label": "합계", "id": "e1", "stat": "Sum", "period": 86400 } ], [ { "expression": "AVG(METRICS())", "label": "평균", "id": "e2", "stat": "Sum", "period": 86400 } ], [ { "expression": "MIN(METRICS())", "label": "최소값", "id": "e3", "stat": "Sum", "period": 86400 } ], [ { "expression": "MAX(METRICS())", "label": "최대값", "id": "e4", "stat": "Sum", "period": 86400 } ], [ { "expression": "SUM(METRICS())/SUM(m1)", "label": "SUM(METRICS())/SUM(m1)", "id": "e5", "stat": "Sum", "period": 86400 } ], [ { "expression": "SUM(100/[m1, m2])", "label": "SUM(100/[m1, m2])", "id": "e6", "stat": "Sum", "period": 86400 } ], [ "testMetrics", "testMetrics1", { "id": "m1", "stat": "Sum", "period": 86400, "label": "testMetrics-400" } ], [ ".", "testMetrics2", { "id": "m2", "stat": "Sum", "period": 86400, "label": "testMetrics-200" } ] ], "view": "singleValue", "stacked": false, "region": "ap-northeast-1", "title": "testLambda1", "period": 300 } 6.Dashboard name을 입력한 후 ‘Create dashboard’를 클릭합니다.7.’Add widget’을 클릭해 Number 유형을 선택합니다.8.Source Tab에서 복사해 둔 지표 Source를 붙여 넣고, ‘Create widget’을 클릭합니다.9.위젯이 추가되었습니다. 추가된 위젯은 ‘Save dashboard’ 버튼을 클릭해야 최종 저장됩니다.10.이번에는 로그 메시지 결과를 확인할 수 있는 Query result 유형을 추가해보겠습니다. 먼저 Query result 유형을 선택합니다.11.로그 메시지에 조건을 추가해 필터링합니다.잠시 쉬어가기!: Query 언어가 지원하는 여섯 가지 명령 유형fields : 지정한 필드를 검색합니다. 필드 명령 내에서 함수 및 연산을 사용할 수 있습니다. 만약 @ 기호, 마침표(.) 및 영숫자 문자 이외의 문자가 포함된 로그 필드가 쿼리에 명명되어 있으면 해당 필드 이름은 억음 기호로 둘러싸야 합니다.filter : 하나 이상의 조건으로 필터링합니다. filter statusCode like /2\d\d/ → 필드 statusCode의 값이 200~299인 로그 이벤트를 반환합니다.stats : 로그 필드에 대한 지정된 시간 간격의 집계 통계를 계산합니다.sort : 검색된 로그 이벤트를 정렬합니다.limit : 쿼리에서 반환되는 로그 이벤트 수를 제한합니다.parse : 로그 필드에서 데이터를 추출하고 쿼리로 추가 처리할 수 있는 임시 필드가 하나 이상 생성됩니다.12.추가된 위젯은 이름과 사이즈를 조절한 후, ‘Save dashboard’ 버튼을 클릭해 최종 저장합니다.13.생성한 Alarm을 Dashboard에 추가하겠습니다.14.Dashboard가 완성되었습니다!Conclusion지금까지 CloudWatch 서비스를 소개했습니다. 이 서비스를 이용하면 로그와 지표를 쉽게 시각화할 수 있고, 작업을 자동화할 수도 있는 것을 확인했습니다. CloudWatch를 이용해 애플리케이션을 최적화하고, 원활하게 실행해보는 건 어떨까요. 분명 리소스를 효과적으로 다룰 수 있을 겁니다.글곽정섭 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만
조회수 1458

A씨의 일주일

스포카 개발팀 문성원입니다. 오늘은 스포카 개발팀의 가공의 개발자 A씨의 일주일을 통해, 스포카 개발팀에서는 일주일간의 개발 일정을 어떻게 진행하는지 살펴보겠습니다. 평소 스타트업(Startup) 개발팀의 문화에 관심이 있으셨던 분들에게 도움이 되길 바랍니다.월요일오전 10시, A씨는 평소보다 조금 일찍 사무실에 도착했습니다. 매주 월요일은 스포카 전체 미팅이 있는 날이기 때문이죠. 한 주간 각자 진행한 것을 토대로 이번 주에 진행할 일을 대외적으로 공표하는 이 회의에 앞서, 스포카 개발팀은 따로 미팅을 잠깐 가집니다. 그동안 지난 주 개발 사항, 이번 주 구현 목록 등을 트렐로(Trello)를 통해 정리한 뒤, 이를 전체 미팅에서 공유합니다. A씨는 지난 주에 미쳐 다 구현하지 못했던 서버의 몇 가지 기능과 클라이언트 신버전 배포 준비를 하게 되었습니다.정신없이 회의 하고 났더니 벌써 점심시간입니다. 늘 가던 근처 식당에서 즐겁게 점심을 먹고 사무실로 올라온 A씨는 막간을 이용해 간밤에 올라온 스포카 개발 블로그의 원고를 검토합니다. 몇 가지 오탈자와 맞춤법을 지적한 뒤 모두가 지루해할 월요일 오후 1~2시경에 공개하는 것이 목표입니다.올라간 블로그 글을 확인한 뒤에, A씨는 구현해야 할 서버 기능을 살펴보기로 했습니다. 생각보단 많긴 하지만 일주일 안에는 어떻게든 끝낼 수도 있을 것 같은 분량이네요. 우선 트렐로에 올라온 카드의 명세를 토대로 작업해야 할 내용을 체크리스트(Check List)로 정리합니다. 그다음은 모두가 짐작하시듯이 열심히 일합니다. A씨는 프로니까요.어느덧 저녁 시간이 다 되었습니다. 특별히 일이 없는 이상 야근은 하지 않는 주의인 A씨지만, 오늘만큼은 저녁을 먹고 조금 더 남아있기로 합니다. 팀 내에서 진행하고 있는 스터디 때문이죠. 혼자서 읽기는 까다로웠던 책을 다 같이 읽어보니 조금은 이해가 더 되는 느낌이 드네요.화요일A씨는 오전에 작업하던 중 이상한 점을 발견합니다. 구현하기로 한 기능이 기존 기능과 모순이 되기 때문이죠. 이걸 어떻게 해결할까 고민하던 A씨는 다행히 사무실에 남아있던 엔에이블러(Enabler)팀원들과 간단하게 미팅을 합니다. 문제를 설명하고 명세를 다시 확인한 A씨는 작성한 회의록과 함께 배포합니다. 트렐로의 해당 카드에도 첨부하여 나중에 다시 볼 수 있게 하는 것은 기본입니다.뜻하지 않은 문제 때문에 오전을 날려서 기분이 나빠진 A씨지만, 다행히 좋아하는 스파게티를 먹고 기운을 내기로 했습니다. 사무실에 올라와 인터넷 뉴스와 페이스북을 잠시 보던 A씨는 암묵적으로만 정해진 점심시간이 끝나자 바로 작업을 시작합니다. A씨는 프로니까요.그런데 문제가 있습니다. 오전에 배포한 회의록을 읽어 본 다른 팀원들이 이건 다른 문제의 원인이 될 수 있다고 합니다. A씨는 새 기능 추가가 단순히 로직이 아니라 클라이언트 UI를 포함한 대규모 변경이 필요하다는 것을 깨닫습니다. A씨는 새 기능에 대한 대략적인 스케치를 발사믹 목업(Balsamiq Mockup)으로 마친 뒤 이를 다시 배포합니다. 또한, 관련된 카드에 설명도 잊지 않습니다.수요일매주 수요일 오전에 스포카 개발팀은 짧은 미팅을 합니다. 금주의 진행사항 중 변경사항이나 도움이 필요한 내용을 공유하는 자리인데요. 여기서 A씨는 어제 일을 다시 정리해서 이야기하고, 일정이 지연될 수 있음을 전달합니다. A씨에게 할당된 카드 일부를 다음 주로 미루거나, 좀 한가한 사람에게 나눠주는 형식으로 짐을 던 A씨였지만, 여전히 큰일이 되어버린 기능 변경은 무거운 짐입니다.이런 대량의 작업 때문에 고민하던 A씨에게 같은 팀 B씨가 어떤 라이브러리를 소개해줍니다. A씨는 처음 보는 라이브러리인지라 B씨가 전담해서 가르쳐주는 모양이 되었지만, 생각보다 문제 해결에 큰 도움이 될 것 같습니다. 마침 다음 주에 개발 블로그에 글을 써야 할 당번이 된 A씨는 그 라이브러리에 대해 좀 더 공부해서 쓰기로 정합니다.B씨의 도움 덕에 진행 속도가 붙은 A씨는, 금주 업무 중 하나였던 클라이언트 새 버전의 테스트를 내일부터 진행하기 위해 페이스북이나 인터넷 뉴스도 보지 않은 채 열심히 일합니다. 이런 A씨의 프로다운 모습에 하늘도 감탄했는지, 퇴근 시간인 7시 전에 작업을 끝낸 A씨는 구현된 기능들을 테스트 해 보고 팀의 다른 개발자와 공유하기 위해 github에 만들어진 스포카 서버 코드 저장소에 푸시(Push)합니다.목요일구글 플레이(Google Play)는 하루 정도면 배포가 되니 다행이지만 애플 앱스토어(Apple App Store)는 일주일 정도의 심사기간이 있기 때문에 거절(Reject)당하지 않게 철저히 준비해야 합니다. 그래서 어느 때보다 A씨는 날카로운 눈매로 클라이언트를 점검합니다. 아니나 다를까 메뉴를 이동하다 보니 화면 구성이 흐트러지는 버그가 발견되었습니다. 하지만 프로답게 A씨는 당황하지 않고 재현 조건을 확인한 뒤, 클라이언트 담당자인 C씨에게 알려줍니다. “클라이언트 관련한 버그를 찾았는데, 트렐로를 확인해보세요.”라구요. QA(Quality Assurance) 업무 역시 스포카 개발팀은 직접 처리합니다.밖에 비가 오는지라 피자를 시켜먹은 뒤, 자리에 앉아 잠깐 쉬고 있던 A씨에게 D씨가 다가와서는, “어제 푸시한 소스를 내려받다(Pull)가 충돌(Conflict)이 났는데, 어떻게 병합(Merge)해야 할지 모르겠네요.” 라며 묻습니다. A씨는 D씨와 충돌이 난 소스를 함께 검토하고 문제가 발생하지 않게끔 조정한 뒤 이를 다시 푸시해서 상황을 종료합니다.이러는 사이 C씨가 A씨가 말한 버그를 고쳤다며 다시 확인해보라고 트렐로의 관련 카드를 “테스트” 리스트로 옮깁니다. A씨는 재현된 상황에서 문제 없이 동작하는 것을 확인하고 카드를 “완료” 리스트로 다시 옮깁니다. 이제 클라이언트 앱을 심의 신청하고 어제 구현한 서버 쪽 코드의 개선사항이 있는지 살펴봅니다. 서버는 클라이언트가 앱스토어나 플레이에 준비되는 것을 확인한 뒤, DotCloud에서 제공하는 배포 스크립트를 통해 손쉽게 버전업할 수 있기 때문에 시간이 좀 남아 있습니다. 현재로선 특별히 더 손댈 부분이 없다는 걸 확인한 A씨는 오늘도 즐겁게 퇴근합니다.금요일월요일과 마찬가지로 오늘도 A씨는 평소보다 조금 서둘러서 사무실에 도착했습니다. 오늘은 사내 전체적으로 한 주간 있었던 업무 내용을 간략하게 보고하는 자리가 있습니다. A씨는 이번 주에 맡은 서버 개발이 이러저러해서 이렇고 저렇게 바뀌었다고 설명한 뒤, 앱스토어에 신청되었다는 사실을 공지합니다. 전체 보고가 끝난 뒤엔 개발팀은 따로 남아서 약간 자세하게 금주 작업을 공유하면서 트렐로의 “완료” 상태에 있는 카드들을 정리하는 시간을 갖습니다.점심을 먹고 나서, 이번 주에 더는 급한 일정이 없다는 것을 확인한 A씨는 개발 블로그에 쓸 글을 정리하기 시작합니다. 수요일에 B씨가 알려 준 라이브러리의 사용 방법은 대강 배웠지만, 그것을 남에게 설명할 수 있을 만큼 자세히 알지는 못했기 때문에 A씨는 한동안 공식 문서와 예제 코드들과 씨름합니다. 그래도 어느새 옆에서 거들기 시작한 B씨 덕에 글은 생각보다 순조롭게 마무리되었습니다. 이제 다음 주 월요일까지 퇴고해서 블로그에 공개하기만 하면 되죠.생각보다 오늘 업무를 끝낸 A씨는 친구들과 약속이 있는 홍대로 가기 위해, 7시 정시에 사무실을 떠납니다.#스포카 #개발 #개발자 #개발팀 #개발자의일주일 #개발자의일상 #인사이트 #경험공유
조회수 1804

잔디 팀에서 가장 자유로운 영혼을 가진 그녀! 고객 경험(CX)팀의 Soo를 만나다

맛있는 인터뷰: 고객 경험(Customer Experience) 매니저 Soo ▲ 점심엔 역시 맥주 한 잔이죠? 알코올과 함께 하는 맛있는 인터뷰 먼저 인터뷰를 제안해 온 사람은 처음이다. 본인 소개를 부탁한다Soo(이하 ‘S’): 반갑다! 잔디 팀에서 고객 경험: CX(Customer Experience) 업무를 담당하고 있는 Soo라고 한다. 고객 응대뿐 만 아니라 서비스 번역이나 비즈니스 팀에서 사용되는 제품 메뉴얼 작성, 영상 작업 등 고객 경험에 연관된 다양한 업무를 수행하고 있다. 하는 일이 꽤 많은 것 같은데?S: 잔디 팀원이라면 당연히 이 정도는! 타이 음식은 오랜만이다. 이 곳을 오게 된 이유가 있다면?S: 우리가 온 곳은 망고플레이트에서도 평이 좋은 태국 음식점 ‘알로이 타이(Aloy Thai)‘다. 개인적으로 동남아 음식을 너무 좋아한다. 미국에 있을 때 먹었던 쌀국수 맛이 늘 그리웠는데.. 수소문 끝에 알아낸 인생 맛집이다. 선릉역 2번 출구에서 도보 5분 거리에 있다. 정확한 주소는 서울시 강남구 대치동 8… 잠깐! 광고비를 받은 건가? 맛있는 인터뷰는 원칙적으로 협찬을 금지하고 있다S: 무슨 소리. 인생 맛집이라 이렇게라도 알리고 싶었다. 아님 말고..S: ..^^ 음식과 함께 술을 주문한 인터뷰이는 Soo가 처음이다S: 평소 술을 즐기는 편이다. 하지만 오해하지 않았으면 좋겠다. 술을 좋아하는 거지 잘 마시는 건 아니다. 가끔 집에서 혼술하는 것도 좋아한다. 술 말고 좋아하는 건?S: 게임을 좋아한다. 미국에 있을 때는 집에서 혼자 농구게임을 엄청 많이 했고, 친구들과 철권을 즐겼다. 한국에서는 롤을 무척이나 많이 했다. 아침부터 새벽까지 랭겜을 돌리곤 했다. 티어가…?S: 그것은 비밀이다. (웃음) 술, 게임, 쌀국수까지. Soo의 미국 생활이 진심 궁금하다S: 남들과 크게 다르지 않다. 중학교를 제외한 학창 시절을 모두 미국에서 보냈다. 한국에서 이렇게 오래 지내보는 건 처음이다. 잔디 팀에 조인하면서 한국 생활을 시작한 격인데 처음엔 무척 낯설었다. 2년 지난 지금은 꽤 괜찮아졌다. ▲ 미국에 있을 당시의 Soo 모습. 왼쪽에서 화사하게 웃고 있는 사람이 Soo다.어떻게 잔디 팀을 알고 지원했는지 궁금하다S: 대기업에서 인턴을 해보니 수직적인 기업 문화가 맞지 않았다. 때마침 지인에게 잔디 팀을 추천 받게 되어 입사하게 되었다. 스타트업은 뭔가 열정이 넘치다 못해 폭발하는 사람만 가는 곳이라 생각했는데, 지금은 그 ‘스타트업’ 중 한 곳에서 일하고 있다. 묘한 감정이 든다. (웃음) 잔디 팀의 업무 문화는 마음에 드는가?S: 잔디 팀에서 일하면서 가장 좋은 점은 내 직무에서 풀어야 할 숙제를 스스로 한다는 점이다. 개인적으로 가장 재미있고 신나는 경험이다. 너무 교과서적인 대답이다. 신박한 답변을 원한다S: 역으로 질문하고 싶다. 잔디 팀의 업무 문화가 마음에 드는가? 소중한 말씀 감사합니다..S: ^^ 주말에는 무엇을 하고 지내는가?S: 보통 술을 마신다. (웃음) 아니면 집에서 영화를 본다. 뭔가 #술 #알코올 #혼술 #집스타그램 해시태그를 붙여야 할 것 같은 인터뷰다. 다른 이야기를 해보자!S: 언제든지! 잔디 표지모델은 어떻게 하게 되었는지?S: Product 팀의 DL이 부탁해서 촬영하게 되었다. 사진을 본 내 친구들이 이게 뭐냐며 비웃었던 게 가장 기억에 남는다. DL이 보정을 해준다고 했는데 실제로는 목주름만 보정해줬다. 뭔가 슬펐다. ▲ 잔디 홍보 자료에 자주 등장한 Soo 일하는 자리를 보면 아기자기한 물건들이 많다. 애착이 가는 물건이 있다면?S: 내가 기르고 있는 식물이다. 귀엽기도 하고, 물만 줘도 조용히 잘 자라는 녀석들이 기특하다. 펫을 기른다는 기분으로 정성스레 기르고 있다. 이름도 지어주었다. 이름이?S: 밝힐 수 없다. 맛있는 인터뷰를 통해 공개하기엔 부적절한 이름이다. (웃음) 대학교에서 신문방송학을 전공했다고 들었다. 전공과 무관한 고객 경험 업무를 하게 된 계기가?S: 고객 응대만을 하는 CS(Customer Service)가 아니라 총체적인 ‘고객 경험’에 참여하는 CX 라는 점이 끌렸다. 제품과 고객을 잇는 브릿지 역할을 한다는 점이 매력적이었고, 잔디를 이용할 때 퍼널(Funnel) 최전방에서 가장 먼저 접하는 사람이 나라는 점도 흥미로웠다. 그리고 주 전공인 영상 제작 업무도 CX 일을 하며 할 수 있어 좋았다. 업무를 하다 보면 재미있는 에피소드가 있을 것 같다S: 연령대가 높은 사용자 중 생각보다 컴퓨터 사용법을 잘 모르는 경우를 종종 볼 수 있다. 그럼에도 불구하고 최근 많은 이슈가 되고 있는 협업 트렌드를 배우고자 열심히 노력하는 모습이 너무 인상적이었다. 더욱 더 도와주고 싶다는 생각이 자연스레 들 정도다. 협업툴에 대한 요구가 많아졌음을 직감하는지?S: 협업툴에 대한 요구도 많아졌지만 그보다 더 피부에 와닿는 변화는 고객의 인식이 확연히 바뀌었다는 점이다. 처음 CX 업무를 시작했을 때 접한 잔디 사용자들은 돈을 주고 서비스를 사용한다는 개념을 생소하게 여겼다. 반면 지금은 다르다. 최근 잔디 도입을 문의하는 고객 대다수는 서비스 요금부터 문의한다. 잔디 도입 문의 어디에 하는 게 효과적인가?S: 잔디 웹사이트 우측 하단에 있는 파란색 버튼을 클릭하거나 도입 문의 폼을 남기면 CX팀과 세일즈 팀이 바로 도움을 드린다. ▲ 인형과 식물이 가득한 Soo의 업무 공간잔디 팀에서 배운 점이 있다면?S: 사람과 소통하는 방법을 가장 많이 배웠다. 아무래도 한국 문화에 익숙하지 않아 ‘한국식 소통 방법’이 낯설었는데 사회 생활을 통해 자연스레 학습할 수 있어 좋았다. 잔디 팀에서의 경험 덕분에 자신감이 생겼다. 다른 곳에 간다고 해도 잘 할 수 있을 것 같다. 첫 직장으로서 잔디 팀의 생활이 만족스럽다는 걸로 들린다S: 물론이다. (웃음) 정말인가?S: 물론이다. 건배나 하자. 태국 음식엔 역시 맥주가 짱이다. (웃음) 어떤 꿈을 가졌는지 궁금하다S: 사실 무엇을 해야할 지 정한 건 없다. 막연하지만 나만의 것을 해보고 싶다. 사무실에 앉아서 일하는 것보단 무언가 발로 뛰며 성취하는 경험을 해보고 싶다. 이전 인터뷰이였던 잔디 HR 담당자 Amy의 질문이다. 자신의 인생에서 가장 행복했던 순간은?S: 행복했던 순간이 너무 많아 한 가지만 고르기 힘들다. 뭔가 성취감을 느꼈을 때 행복을 느끼는 것 같다. 그 외에는 맥주 한잔하면서 집에서 뒹굴뒹굴할 때가 행복하다. 일상의 소소한 것에서 느끼는 즐거움이 진짜 행복은 아닐지 생각해본다. 다음 인터뷰이를 위한 질문을 부탁한다S: 올해 꼭 이루고 싶은 목표는? ▲ 술과 음식으로 점철된 맛있는 인터뷰가 열린 선릉역 맛집 ‘알로이 타이’마지막 질문이다. 왜 맛있는 인터뷰가 하고 싶었는지?S: 잔디 팀과 함께 한 시간이 어언 2년이다. 팀의 일원으로서 잔디 이름을 가진 어딘가에 내 흔적을 남기고 싶었다. 맛있는 인터뷰가 그 흔적으로 적합하다고 생각하는가?S: 물론이다. 맛있는 인터뷰를 보면 그간 잔디 팀과 함께 했던, 그리고 함께 한 멤버들의 모습을 꺼내볼 수 있다. 일종의 추억 보관함이라고 해야할까? 내 이야기도 잔디 팀의 누군가에게 추억이 될 거라 생각해 내 이름을 꼭 남기고 싶었다. 인터뷰 해줘서 너무 고맙다. (웃음) #토스랩 #잔디 #JANDI #팀원소개 #인터뷰 #기업문화 #조직문화 #팀원자랑
조회수 3241

Attention is all you need paper 뽀개기

이번 포스팅에서는 포자랩스에서 핵심적으로 쓰고 있는 모델인 transformer의 논문을 요약하면서 추가적인 기법들도 설명드리겠습니다.Why?Long-term dependency problemsequence data를 처리하기 위해 이전까지 많이 쓰이던 model은 recurrent model이었습니다. recurrent model은 t번째에 대한 output을 만들기 위해, t번째 input과 t-1번째 hidden state를 이용했습니다. 이렇게 한다면 자연스럽게 문장의 순차적인 특성이 유지됩니다. 문장을 쓸 때 뒤의 단어부터 쓰지 않고 처음부터 차례차례 쓰는 것과 마찬가지인것입니다.하지만 recurrent model의 경우 많은 개선점이 있었음에도 long-term dependency에 취약하다는 단점이 있었습니다. 예를 들어, “저는 언어학을 좋아하고, 인공지능중에서도 딥러닝을 배우고 있고 자연어 처리에 관심이 많습니다.”라는 문장을 만드는 게 model의 task라고 해봅시다. 이때 ‘자연어’라는 단어를 만드는데 ‘언어학’이라는 단어는 중요한 단서입니다.그러나, 두 단어 사이의 거리가 가깝지 않으므로 model은 앞의 ‘언어학’이라는 단어를 이용해 자연어’라는 단어를 만들지 못하고, 언어학 보다 가까운 단어인 ‘딥러닝’을 보고 ‘이미지’를 만들 수도 있는 거죠. 이처럼, 어떤 정보와 다른 정보 사이의 거리가 멀 때 해당 정보를 이용하지 못하는 것이 long-term dependency problem입니다.recurrent model은 순차적인 특성이 유지되는 뛰어난 장점이 있었음에도, long-term dependency problem이라는 단점을 가지고 있었습니다.이와 달리 transformer는 recurrence를 사용하지 않고 대신 attention mechanism만을 사용해 input과 output의 dependency를 포착해냈습니다.Parallelizationrecurrent model은 학습 시, t번째 hidden state를 얻기 위해서 t-1번째 hidden state가 필요했습니다. 즉, 순서대로 계산될 필요가 있었습니다. 그래서 병렬 처리를 할 수 없었고 계산 속도가 느렸습니다.하지만 transformer에서는 학습 시 encoder에서는 각각의 position에 대해, 즉 각각의 단어에 대해 attention을 해주기만 하고, decoder에서는 masking 기법을 이용해 병렬 처리가 가능하게 됩니다. (masking이 어떤 것인지는 이후에 설명해 드리겠습니다)Model ArchitectureEncoder and Decoder structureencoder는 input sequence (x1,...,xn)<math>(x1,...,xn)</math>에 대해 다른 representation인 z=(z1,...,zn)<math>z=(z1,...,zn)</math>으로 바꿔줍니다.decoder는 z를 받아, output sequence (y1,...,yn)<math>(y1,...,yn)</math>를 하나씩 만들어냅니다.각각의 step에서 다음 symbol을 만들 때 이전에 만들어진 output(symbol)을 이용합니다. 예를 들어, “저는 사람입니다.”라는 문장에서 ‘사람입니다’를 만들 때, ‘저는’이라는 symbol을 이용하는 거죠. 이런 특성을 auto-regressive 하다고 합니다.Encoder and Decoder stacksEncoderN개의 동일한 layer로 구성돼 있습니다. input $x$가 첫 번째 layer에 들어가게 되고, layer(x)<math>layer(x)</math>가 다시 layer에 들어가는 식입니다.그리고 각각의 layer는 두 개의 sub-layer, multi-head self-attention mechanism과 position-wise fully connected feed-forward network를 가지고 있습니다.이때 두 개의 sub-layer에 residual connection을 이용합니다. residual connection은 input을 output으로 그대로 전달하는 것을 말합니다. 이때 sub-layer의 output dimension을 embedding dimension과 맞춰줍니다. x+Sublayer(x)<math>x+Sublayer(x)</math>를 하기 위해서, 즉 residual connection을 하기 위해서는 두 값의 차원을 맞춰줄 필요가 있습니다. 그 후에 layer normalization을 적용합니다.Decoder역시 N개의 동일한 layer로 이루어져 있습니다.encoder와 달리 encoder의 결과에 multi-head attention을 수행할 sub-layer를 추가합니다.마찬가지로 sub-layer에 residual connection을 사용한 뒤, layer normalization을 해줍니다.decoder에서는 encoder와 달리 순차적으로 결과를 만들어내야 하기 때문에, self-attention을 변형합니다. 바로 masking을 해주는 것이죠. masking을 통해, position i<math>i</math> 보다 이후에 있는 position에 attention을 주지 못하게 합니다. 즉, position i<math>i</math>에 대한 예측은 미리 알고 있는 output들에만 의존을 하는 것입니다.위의 예시를 보면, a를 예측할 때는 a이후에 있는 b,c에는 attention이 주어지지 않는 것입니다. 그리고 b를 예측할 때는 b이전에 있는 a만 attention이 주어질 수 있고 이후에 있는 c는 attention이 주어지지 않는 것이죠.Embeddings and Softmaxembedding 값을 고정시키지 않고, 학습을 하면서 embedding값이 변경되는 learned embedding을 사용했습니다. 이때 input과 output은 같은 embedding layer를 사용합니다.또한 decoder output을 다음 token의 확률로 바꾸기 위해 learned linear transformation과 softmax function을 사용했습니다. learned linear transformation을 사용했다는 것은 decoder output에 weight matrix W<math>W</math>를 곱해주는데, 이때 W<math>W</math>가 학습된다는 것입니다.Attentionattention은 단어의 의미처럼 특정 정보에 좀 더 주의를 기울이는 것입니다.예를 들어 model이 수행해야 하는 task가 번역이라고 해봅시다. source는 영어이고 target은 한국어입니다. “Hi, my name is poza.”라는 문장과 대응되는 “안녕, 내 이름은 포자야.”라는 문장이 있습니다. model이 이름은이라는 token을 decode할 때, source에서 가장 중요한 것은 name입니다.그렇다면, source의 모든 token이 비슷한 중요도를 갖기 보다는 name이 더 큰 중요도를 가지면 되겠죠. 이때, 더 큰 중요도를 갖게 만드는 방법이 바로 attention입니다.Scaled Dot-Product Attention해당 논문의 attention을 Scaled Dot-Product Attention이라고 부릅니다. 수식을 살펴보면 이렇게 부르는 이유를 알 수 있습니다.Attention(Q,K,V)=softmax(QKT√dk)V<math>Attention(Q,K,V)=softmax(QKTdk)V</math>먼저 input은 dk<math>dk</math> dimension의 query와 key들, dv<math>dv</math> dimension의 value들로 이루어져 있습니다.이때 모든 query와 key에 대한 dot-product를 계산하고 각각을 √dk<math>dk</math>로 나누어줍니다. dot-product를 하고 √dk<math>dk</math>로 scaling을 해주기 때문에 Scaled Dot-Product Attention인 것입니다. 그리고 여기에 softmax를 적용해 value들에 대한 weights를 얻어냅니다.key와 value는 attention이 이루어지는 위치에 상관없이 같은 값을 갖게 됩니다. 이때 query와 key에 대한 dot-product를 계산하면 각각의 query와 key 사이의 유사도를 구할 수 있게 됩니다. 흔히 들어본 cosine similarity는 dot-product에서 vector의 magnitude로 나눈 것입니다. √dk<math>dk</math>로 scaling을 해주는 이유는 dot-products의 값이 커질수록 softmax 함수에서 기울기의 변화가 거의 없는 부분으로 가기 때문입니다.softmax를 거친 값을 value에 곱해준다면, query와 유사한 value일수록, 즉 중요한 value일수록 더 높은 값을 가지게 됩니다. 중요한 정보에 더 관심을 둔다는 attention의 원리에 알맞은 것입니다.Multi-Head Attention위의 그림을 수식으로 나타내면 다음과 같습니다.MultiHead(Q,K,V)=Concat(head1,...,headh)WO<math>MultiHead(Q,K,V)=Concat(head1,...,headh)WO</math>where headi=Attention(QWQi,KWKi,VWVi)dmodel<math>dmodel</math> dimension의 key, value, query들로 하나의 attention을 수행하는 대신 key, value, query들에 각각 다른 학습된 linear projection을 h번 수행하는 게 더 좋다고 합니다. 즉, 동일한 Q,K,V<math>Q,K,V</math>에 각각 다른 weight matrix W<math>W</math>를 곱해주는 것이죠. 이때 parameter matrix는 WQi∈Rdmodelxdk,WKi∈Rdmodelxdk,WVi∈Rdmodelxdv,WOi∈Rhdvxdmodel<math>WiQ∈Rdmodelxdk,WiK∈Rdmodelxdk,WiV∈Rdmodelxdv,WiO∈Rhdvxdmodel</math>입니다.순서대로 query, key, value, output에 대한 parameter matrix입니다. projection이라고 하는 이유는 각각의 값들이 parameter matrix와 곱해졌을 때 dk,dv,dmodel<math>dk,dv,dmodel</math>차원으로 project되기 때문입니다. 논문에서는 dk=dv=dmodel/h<math>dk=dv=dmodel/h</math>를 사용했는데 꼭 dk<math>dk</math>와 dv<math>dv</math>가 같을 필요는 없습니다.이렇게 project된 key, value, query들은 병렬적으로 attention function을 거쳐 dv<math>dv</math>dimension output 값으로 나오게 됩니다.그 다음 여러 개의 head<math>head</math>를 concatenate하고 다시 projection을 수행합니다. 그래서 최종적인 dmodel<math>dmodel</math> dimension output 값이 나오게 되는거죠.각각의 과정에서 dimension을 표현하면 아래와 같습니다.*dQ,dK,dV<math>dQ,dK,dV</math>는 각각 query, key, value 개수Self-Attentionencoder self-attention layerkey, value, query들은 모두 encoder의 이전 layer의 output에서 옵니다. 따라서 이전 layer의 모든 position에 attention을 줄 수 있습니다. 만약 첫번째 layer라면 positional encoding이 더해진 input embedding이 됩니다.decoder self-attention layerencoder와 비슷하게 decoder에서도 self-attention을 줄 수 있습니다. 하지만 i<math>i</math>번째 output을 다시 i+1<math>i+1</math>번째 input으로 사용하는 auto-regressive한 특성을 유지하기 위해 , masking out된 scaled dot-product attention을 적용했습니다.masking out이 됐다는 것은 i<math>i</math>번째 position에 대한 attention을 얻을 때, i<math>i</math>번째 이후에 있는 모든 position은 Attention(Q,K,V)=softmax(QKT√dk)V<math>Attention(Q,K,V)=softmax(QKTdk)V</math>에서 softmax의 input 값을 −∞<math>−∞</math>로 설정한 것입니다. 이렇게 한다면, i<math>i</math>번째 이후에 있는 position에 attention을 주는 경우가 없겠죠.Encoder-Decoder Attention Layerquery들은 이전 decoder layer에서 오고 key와 value들은 encoder의 output에서 오게 됩니다. 그래서 decoder의 모든 position에서 input sequence 즉, encoder output의 모든 position에 attention을 줄 수 있게 됩니다.query가 decoder layer의 output인 이유는 query라는 것이 조건에 해당하기 때문입니다. 좀 더 풀어서 설명하면, ‘지금 decoder에서 이런 값이 나왔는데 무엇이 output이 돼야 할까?’가 query인 것이죠.이때 query는 이미 이전 layer에서 masking out됐으므로, i번째 position까지만 attention을 얻게 됩니다.이 같은 과정은 sequence-to-sequence의 전형적인 encoder-decoder mechanisms를 따라한 것입니다.*모든 position에서 attention을 줄 수 있다는 게 이해가 안되면 링크를 참고하시기 바랍니다.Position-wise Feed-Forward Networksencoder와 decoder의 각각의 layer는 아래와 같은 fully connected feed-forward network를 포함하고 있습니다.position 마다, 즉 개별 단어마다 적용되기 때문에 position-wise입니다. network는 두 번의 linear transformation과 activation function ReLU로 이루어져 있습니다.FFN(x)=max(0,xW1+b1)W2+b2x<math>x</math>에 linear transformation을 적용한 뒤, ReLU(max(0,z))<math>ReLU(max(0,z))</math>를 거쳐 다시 한번 linear transformation을 적용합니다.이때 각각의 position마다 같은 parameter W,b<math>W,b</math>를 사용하지만, layer가 달라지면 다른 parameter를 사용합니다.kernel size가 1이고 channel이 layer인 convolution을 두 번 수행한 것으로도 위 과정을 이해할 수 있습니다.Positional Encodingtransfomer는 recurrence도 아니고 convolution도 아니기 때문에, 단어의sequence를 이용하기 위해서는 단어의 position에 대한 정보를 추가해줄 필요가 있었습니다.그래서 encoder와 decoder의 input embedding에 positional encoding을 더해줬습니다.positional encoding은 dmodel<math>dmodel</math>(embedding 차원)과 같은 차원을 갖기 때문에 positional encoding vector와 embedding vector는 더해질 수 있습니다.논문에서는 다른 *frequency를 가지는 sine과 cosine 함수를 이용했습니다.*주어진 구간내에서 완료되는 cycle의 개수PE(pos,2i)=sin(pos/100002i/dmodel)<math>PE(pos,2i)=sin(pos/100002i/dmodel)</math>PE(pos,2i+1)=cos(pos/100002i/dmodel)<math>PE(pos,2i+1)=cos(pos/100002i/dmodel)</math>pos<math>pos</math>는 position ,i<math>i</math>는 dimension 이고 주기가 100002i/dmodel⋅2π<math>100002i/dmodel⋅2π</math>인 삼각 함수입니다. 즉, pos<math>pos</math>는 sequence에서 단어의 위치이고 해당 단어는 i<math>i</math>에 0부터 dmodel2<math>dmodel2</math>까지를 대입해 dmodel<math>dmodel</math>차원의 positional encoding vector를 얻게 됩니다. k=2i+1<math>k=2i+1</math>일 때는 cosine 함수를, k=2i<math>k=2i</math>일 때는 sine 함수를 이용합니다. 이렇게 positional encoding vector를 pos<math>pos</math>마다 구한다면 비록 같은 column이라고 할지라도 pos<math>pos</math>가 다르다면 다른 값을 가지게 됩니다. 즉, pos<math>pos</math>마다 다른 pos<math>pos</math>와 구분되는 positional encoding 값을 얻게 되는 것입니다.PEpos=[cos(pos/1),sin(pos/100002/dmodel),cos(pos/10000)2/dmodel,...,sin(pos/10000)]<math>PEpos=[cos(pos/1),sin(pos/100002/dmodel),cos(pos/10000)2/dmodel,...,sin(pos/10000)]</math>이때 PEpos+k<math>PEpos+k</math>는 PEpos<math>PEpos</math>의 linear function으로 나타낼 수 있습니다. 표기를 간단히 하기 위해 c=100002idmodel<math>c=100002idmodel</math>라고 해봅시다. sin(a+b)=sin(a)cos(b)+cos(a)sin(b)<math>sin(a+b)=sin(a)cos(b)+cos(a)sin(b)</math>이고 cos(a+b)=cos(a)cos(b)−sin(a)sin(b)<math>cos(a+b)=cos(a)cos(b)−sin(a)sin(b)</math> 이므로 다음이 성립합니다.PE(pos,2i)=sin(posc)<math>PE(pos,2i)=sin(posc)</math>PE(pos,2i+1)=cos(posc)<math>PE(pos,2i+1)=cos(posc)</math>PE(pos+k,2i)=sin(pos+kc)=sin(posc)cos(kc)+cos(posc)sin(kc)=PE(pos,2i)cos(kc)+cos(posc)sin(kc)<math>PE(pos+k,2i)=sin(pos+kc)=sin(posc)cos(kc)+cos(posc)sin(kc)=PE(pos,2i)cos(kc)+cos(posc)sin(kc)</math>PE(pos+k,2i+1)=cos(pos+kc)=cos(posc)cos(kc)−sin(posc)sin(kc)=PE(pos,2i+1)cos(kc)−sin(posc)sin(kc)<math>PE(pos+k,2i+1)=cos(pos+kc)=cos(posc)cos(kc)−sin(posc)sin(kc)=PE(pos,2i+1)cos(kc)−sin(posc)sin(kc)</math>이런 성질 때문에 model이 relative position에 의해 attention하는 것을 더 쉽게 배울 수 있습니다.논문에서는 학습된 positional embedding 대신 sinusoidal version을 선택했습니다. 만약 학습된 positional embedding을 사용할 경우 training보다 더 긴 sequence가 inference시에 입력으로 들어온다면 문제가 되지만 sinusoidal의 경우 constant하기 때문에 문제가 되지 않습니다. 그냥 좀 더 많은 값을 계산하기만 하면 되는거죠.Trainingtraining에 사용된 기법들을 알아보겠습니다.Optimizer많이 쓰이는 Adam optimizer를 사용했습니다.특이한 점은 learning rate를 training동안 고정시키지 않고 다음 식에 따라 변화시켰다는 것입니다.lrate=d−0.5model⋅min(step_num−0.5,step_num⋅warmup_steps−1.5)warmup_step<math>warmup_step</math>까지는 linear하게 learning rate를 증가시키다가, warmup_step<math>warmup_step</math> 이후에는 step_num<math>step_num</math>의 inverse square root에 비례하도록 감소시킵니다.이렇게 하는 이유는 처음에는 학습이 잘 되지 않은 상태이므로 learning rate를 빠르게 증가시켜 변화를 크게 주다가, 학습이 꽤 됐을 시점에 learning rate를 천천히 감소시켜 변화를 작게 주기 위해서입니다.RegularizationResidual ConnectionIdentity Mappings in Deep Residual Networks라는 논문에서 제시된 방법이고, 아래의 수식이 residual connection을 나타낸 것입니다.yl=h(xl)+F(xl,Wl)<math>yl=h(xl)+F(xl,Wl)</math>xl+1=f(yl)<math>xl+1=f(yl)</math>이때 h(xl)=xl<math>h(xl)=xl</math>입니다. 논문 제목에서 나온 것처럼 identity mapping을 해주는 것이죠.특정한 위치에서의 xL<math>xL</math>을 다음과 같이 xl<math>xl</math>과 residual 함수의 합으로 표시할 수 있습니다.x2=x1+F(x1,W1)<math>x2=x1+F(x1,W1)</math>x3=x2+F(x2,W2)=x1+F(x1,W1)+F(x2,W2)<math>x3=x2+F(x2,W2)=x1+F(x1,W1)+F(x2,W2)</math>xL=xl+L−1∑i=1F(xi,Wi)<math>xL=xl+∑i=1L−1F(xi,Wi)</math>그리고 미분을 한다면 다음과 같이 됩니다.σϵσxl=σϵσxLσxLσxl=σϵσxL(1+σσxlL−1∑i=1F(xi,Wi))<math>σϵσxl=σϵσxLσxLσxl=σϵσxL(1+σσxl∑i=1L−1F(xi,Wi))</math>이때, σϵσxL<math>σϵσxL</math>은 상위 layer의 gradient 값이 변하지 않고 그대로 하위 layer에 전달되는 것을 보여줍니다. 즉, layer를 거칠수록 gradient가 사라지는 vanishing gradient 문제를 완화해주는 것입니다.또한 forward path나 backward path를 간단하게 표현할 수 있게 됩니다.Layer NormalizationLayer Normalization이라는 논문에서 제시된 방법입니다.μl=1HH∑i=1ali<math>μl=1H∑i=1Hail</math>σl= ⎷1HH∑i=1(ali−μl)2<math>σl=1H∑i=1H(ail−μl)2</math>같은 layer에 있는 모든 hidden unit은 동일한 μ<math>μ</math>와 σ<math>σ</math>를 공유합니다.그리고 현재 input xt<math>xt</math>, 이전의 hidden state ht−1<math>ht−1</math>, at=Whhht−1+Wxhxt<math>at=Whhht−1+Wxhxt</math>, parameter g,b<math>g,b</math>가 있을 때 다음과 같이 normalization을 해줍니다.ht=f[gσt⊙(at−μt)+b]<math>ht=f[gσt⊙(at−μt)+b]</math>이렇게 한다면, gradient가 exploding하거나 vanishing하는 문제를 완화시키고 gradient 값이 안정적인 값을 가짐로 더 빨리 학습을 시킬 수 있습니다.(논문에서 recurrent를 기준으로 설명했으므로 이에 따랐습니다.)DropoutDropout: a simple way to prevent neural networks from overfitting라는 논문에서 제시된 방법입니다.dropout이라는 용어는 neural network에서 unit들을 dropout하는 것을 가리킵니다. 즉, 해당 unit을 network에서 일시적으로 제거하는 것입니다. 그래서 다른 unit과의 모든 connection이 사라지게 됩니다. 어떤 unit을 dropout할지는 random하게 정합니다.dropout은 training data에 overfitting되는 문제를 어느정도 막아줍니다. dropout된 unit들은 training되지 않는 것이니 training data에 값이 조정되지 않기 때문입니다.Label SmoothingRethinking the inception architecture for computer vision라는 논문에서 제시된 방법입니다.training동안 실제 정답인 label의 logit은 다른 logit보다 훨씬 큰 값을 갖게 됩니다. 이렇게 해서 model이 주어진 input x<math>x</math>에 대한 label y<math>y</math>를 맞추는 것이죠.하지만 이렇게 된다면 문제가 발생합니다. overfitting될 수도 있고 가장 큰 logit을 가지는 것과 나머지 사이의 차이를 점점 크게 만들어버립니다. 결국 model이 다른 data에 적응하는 능력을 감소시킵니다.model이 덜 confident하게 만들기 위해, label distribution q(k∣x)=δk,y<math>q(k∣x)=δk,y</math>를 (k가 y일 경우 1, 나머지는 0) 다음과 같이 대체할 수 있습니다.q′(k|x)=(1−ϵ)δk,y+ϵu(k)<math>q′(k|x)=(1−ϵ)δk,y+ϵu(k)</math>각각 label에 대한 분포 u(k)<math>u(k)</math>, smooting parameter ϵ<math>ϵ</math>입니다. 위와 같다면, k=y인 경우에도 model은 p(y∣x)=1<math>p(y∣x)=1</math>이 아니라 p(y∣x)=(1−ϵ)<math>p(y∣x)=(1−ϵ)</math>이 되겠죠. 100%의 확신이 아닌 그보다 덜한 확신을 하게 되는 것입니다.Conclusiontransformer는 recurrence를 이용하지 않고도 빠르고 정확하게 sequential data를 처리할 수 있는 model로 제시되었습니다.여러가지 기법이 사용됐지만, 가장 핵심적인 것은 encoder와 decoder에서 attention을 통해 query와 가장 밀접한 연관성을 가지는 value를 강조할 수 있고 병렬화가 가능해진 것입니다.Referencehttp://www.whydsp.org/280http://mlexplained.com/2017/12/29/attention-is-all-you-need-explained/http://openresearch.ai/t/identity-mappings-in-deep-residual-networks/47https://m.blog.naver.com/PostView.nhn?blogId=laonple&logNo=220793640991&proxyReferer=https://www.google.co.kr/https://www.researchgate.net/figure/Sample-of-a-feed-forward-neural-network_fig1_234055177https://arxiv.org/abs/1603.05027https://arxiv.org/abs/1607.06450http://jmlr.org/papers/volume15/srivastava14a.old/srivastava14a.pdfhttps://arxiv.org/pdf/1512.00567.pdf
조회수 1160

레진 기술 블로그 - AWS Auto Scalinging Group 을 이용한 배포

레진코믹스의 서버 시스템은 잘 알려진대로 Google AppEngine에서 서비스되고 있지만, 이런저런 이유로 인해 최근에는 일부 컴포넌트가 Amazon Web Service에서 서비스되고 있습니다. AWS 에 새로운 시스템을 셋업하면서, 기존에 사용하던 PaaS인 GAE에서는 전혀 고민할 필요 없었던, 배포시스템에 대한 고민이 필요했습니다. 좋은 배포전략과 시스템은 안정적으로 서비스를 개발하고 운영하는데 있어서 필수적이죠.초기에는 Beanstalk을 이용한 운영에서, Fabric 을 이용한 배포 등의 시행착오 과정을 거쳤으나, 현재는 (스케일링을 위해 어차피 사용할 수밖에 없는) Auto Scaling Group을 이용해서 Blue-green deployment로 운영 중입니다. ASG는 여러 특징 덕분에 배포에도 유용하게 사용할 수 있습니다.ASG를 이용한 가장 간단한 배포는, Instance termination policy 를 응용할 수 있습니다. 기본적으로 ASG가 어떤 인스턴스를 종료할지는 AWS Documentation 에 정리되어 있으며, 추가적으로 다음과 같은 방식을 선택할 수 있습니다.OldestInstanceNewestInstanceOldestLaunchConfigurationClosestToNextInstanceHour여기서 주목할 건 OldestInstance 입니다. ASG가 항상 최신 버전의 어플리케이션으로 스케일아웃되게 구성되어 있다면, 단순히 인스턴스의 수를 두배로 늘린 뒤 Termination policy 를 OldestInstance 로 바꾸고 원래대로 돌리면 구버전 인스턴스들부터 종료되면서 배포가 끝납니다. 그러나 이 경우, 배포 직후 모니터링 과정에서 문제가 발생할 경우 기존의 인스턴스들이 이미 종료된 상태이기 때문에 롤백을 위해서는 (인스턴스를 다시 생성하면서) 배포를 다시 한번 해야 하는 반큼 빠른 롤백이 어렵습니다.Auto scaling lifecycle 을 이용하면, 이를 해결하기 위한 다른 방법도 있습니다. Lifecycle 은 다음과 같은 상태 변화를 가집니다.기본적으로,ASG의 인스턴스는 InService 상태로 진입하면서 (설정이 되어 있다면) ELB에 추가됩니다.ASG의 인스턴스는 InService 상태에서 빠져나오면서 (설정이 되어 있다면) ELB에서 제거됩니다.이를 이용하면, 다음과 같은 시나리오로 배포를 할 수 있습니다.똑같은 ASG 두 개를 구성(Group B / Group G)하고, 그 중 하나의 그룹으로만 서비스를 운영합니다.Group B가 라이브 중이면 Group G의 인스턴스는 0개입니다.새로운 버전을 배포한다면, Group G의 인스턴스 숫자를 Group B와 동일하게 맞춰줍니다.Group G가 InService로 들어가고 ELB healthy 상태가 되면, Group B의 인스턴스를 전부 Standby로 전환합니다.롤백이 필요하면 Standby 상태인 Group B를 InService 로 전환하고 Group G의 인스턴스를 종료하거나 Standby로 전환합니다.문제가 없다면 Standby 상태인 Group B의 인스턴스를 종료합니다.이제 훨씬 빠르고 안전하게 배포 및 롤백이 가능합니다. 물론 실제로는 생각보다 손이 많이 가는 관계로(특히 PaaS인 GAE에 비하면), 이를 한번에 해주는 스크립트를 작성해서 사용중입니다. 대략 간략하게는 다음과 같습니다. 실제 사용중인 스크립트에는 dry run 등의 잡다한 기능이 많이 들어가 있어서 걷어낸 pseudo code 입니다. 스크립트는 사내 PyPI 저장소를 통해 공유해서 사용 중입니다.def deploy(prefix, image_name, image_version): '''Deploy specified Docker image name and version into Auto Scaling Group''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) # Find deployment target set future_set = set(map(lambda g: g['AutoScalingGroupName'].split('-')[-1], filter(lambda g: not g['DesiredCapacity'], groups))) if len(future_set) != 1: raise ValueError('Cannot specify target auto scaling group') future_set = next(iter(future_set)) if future_set == 'green': current_set = 'blue' elif future_set == 'blue': current_set = 'green' else: raise ValueError('Set name shoud be green or blue') # Deploy to future group future_groups = filter(lambda g: g['AutoScalingGroupName'].endswith(future_set), groups) for group in future_groups: asg_client.create_or_update_tags(Tags=[ { 'ResourceId': group['AutoScalingGroupName'], 'ResourceType': 'auto-scaling-group', 'PropagateAtLaunch': True, 'Key': 'docker:image:version', 'Value': image_version, } ]) # Set capacity, scaling policy, scheduled actions same as current group set_desired_capacity_from(current_set, group) move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) # Await ELB healthy of instances in group await_elb_healthy(future_groups) # Entering standby for current group for group in filter(lambda g: g['AutoScalingGroupName'].endswith(current_set), groups): asg_client.enter_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])), ShouldDecrementDesiredCapacity=True ) def rollback(prefix, image_name, image_version): '''Rollback standby Auto Scaling Group to service''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) def filter_group_by_instance_state(groups, state): return filter( lambda g: len(filter(lambda i: i['LifecycleState'] == state, g['Instances'])) == g['DesiredCapacity'] and g['DesiredCapacity'], groups ) standby_groups = filter_group_by_instance_state(groups, 'Standby') inservice_groups = filter_group_by_instance_state(groups, 'InService') # Entering in-service for standby group for group in standby_groups: asg_client.exit_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])) ) # Await ELB healthy of instances in standby group await_elb_healthy(standby_groups) # Terminate instances to rollback for group in inservice_groups: asg_client.set_desired_capacity(AutoScalingGroupName=group['AutoScalingGroupName'], DesiredCapacity=0) current_set = group['AutoScalingGroupName'].split('-')[-1] move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) 몇 가지 더…Standby 로 돌리는 것 이외에 Detached 상태로 바꾸는 것도 방법입니다만, 인스턴스가 ASG에서 제거될 경우, 자신이 소속된 ASG를 알려주는 값인 aws:autoscaling:groupName 태그가 제거되므로 인스턴스나 ASG가 많아질 경우 번거롭습니다.cloud-init 를 어느 정도 최적화해두고 ELB healthcheck 를 좀 더 민감하게 설정하면, ELB 에 투입될 때까지 걸리는 시간을 상당히 줄일 수 있긴 하므로, 단일 ASG로 배포를 하더라도 롤백에 걸리는 시간을 줄일 수 있습니다. 저희는 scaleout 시작부터 ELB에서 healthy 로 찍힐 때까지 70초 가량 걸리는데, 그럼에도 불구하고 아래의 이유 때문에 현재의 방식으로 운영중입니다.같은 방식으로 단일 ASG로 배포를 할 수도 있지만, 배포중에 혹은 롤백 중에 scaleout이 돌면서 구버전 혹은 롤백 버전의 인스턴스가 투입되어버리면 매우 귀찮아집니다. 이를 방지하기 위해서라도 (Blue-green 방식의) ASG 두 개를 운영하는게 안전합니다.같은 이유로, 배포 대상의 버전을 S3나 github 등에 기록하는 대신 ASG의 태그에 버전을 써 두고 cloud-init 의 user-data에서 그 버전으로 어플리케이션을 띄우게 구성해 두었습니다. 이 경우 인스턴스의 태그만 확인해도 현재 어떤 버전이 서비스되고 있는지 확인할 수 있다는 장점도 있습니다.다만 ASG의 태그에 Tag on instance 를 체크해 두더라도, cloud-init 안에서 이를 조회하는 경우는 주의해야 합니다. ASG의 태그가 인스턴스로 복사되는 시점은 명확하지 않습니다. 스크립트 실행 중에 인스턴스에는 ASG의 태그가 있을 수도, 없을 수도 있습니다.굳이 인스턴스의 Lifecycle 을 Standby / InService 로 전환하지 않고도 ELB 를 두 개 운영하고 route 53 에서의 CNAME/ALIAS swap 도 방법이지만, DNS TTL은 아무리 짧아도 60초는 걸리고, JVM처럼 골치아픈 동작 사례도 있는만큼 선택하지 않았습니다.물론 이 방법이 최선은 절대 아니며(심지어 배포할때마다 돈이 들어갑니다!), 현재는 자원의 활용 등 다른 측면에서의 고민 때문에 새로운 구성을 고민하고 있습니다. 이건 언젠가 나중에 다시 공유하겠습니다. :)

기업문화 엿볼 때, 더팀스

로그인

/