스토리 홈

인터뷰

피드

뉴스

조회수 1954

Interview - Android App Developer 박형일님

크래커나인 팀에서는 사용자의 의견을 적극 반영하기 위해서 안드로이드 개발자 박형일님의 크래커나인 사용기 인터뷰를 해보았습니다.개발자 박형일님본인 소개를 부탁 드려요~저는 에이치나인에서 안드로이드 개발 파트를 담당하고 있는 박형일 입니다. 개발 경력은 12년 정도 됐구요.에이치나인에 입사한지는 4년 정도 됐습니다. 주로 외주 안드로이드 앱 개발 업무를 하고 있습니다.개발일을 꽤 오래 하셨네요. 시니어 개발자들은 코드를 직접 작성하는 것을 선호 하시는 것 같던데, 형일님은 어떠신가요?저도 코드를 직접 작성하는 것을 선호 하는 편입니다. 툴에서 생성되는 코드를 별로 신뢰하지 않아요. 제가 원하는 대로 안 나온다는 느낌을 많이 받거든요.그럼 크래커나인의 첫인상은 어떠셨나요? GUI 로 부터 코드를 생성해 주는 툴인데..처음엔 아이디어는 괜찮은데, 이게 과연 제대로 된 코드를 생성 해 줄까? 라는 의심이 들었죠. 꼭 사용 해 보고 싶은 프로그램은 아니었어요.크래커나인을 사용하신지는 얼마나 되셨죠?올해 6월쯤에 처음 접하여 2달 정도 된 같아요.어떤 프로젝트에 적용 해 보셨나요?회사에서 회의실 예약 시스템이 필요하다고 해서 회의실 예약 앱을 만들었어요.그 때 처음 사용 했죠. 공식 프로젝트가 아니라 디자이너 없이 웹에서 무료로 사용할 수 있는 Sketch 파일로 된 디자인 샘플을 받아서 혼자서 만들었어요.앱을 구성하는 화면은 대략 5~6개 정도로 간단한 프로젝트여서 가능 했죠. 지금은 외주 과제를 하고 있는데, 크래커나인을 사용 해서 하고 있어요.외주 같으면 주로 파워포인트로 작성된 GUI 가이드라인 문서를 보고 개발 하잖아요. 크래커나인을 사용하기 전에는 디자이너와 무엇을 통해서 개발에 필요한 디자인 정보를 얻으셨어요? 에이치나인에 있으면서 거의 대부분 GUI 가이드라인 문서를 보고 개발 했죠. 최근에 다른 프로젝트 팀에서 GUI 가이드라인 문서 없이 제플린을 쓰더라구요. 그래서 그것도 조금 써봤어요.제플린과 같은 기존의 유사 서비스와 비교해서 크래커나인은 어땠나요?GUI 가이드라인 문서를 보면서 개발을 하다 제플린을 써보니까 너무 편리하더라구요. 문서에서 원하는 정보 찾는게 불편했거든요.그래서 사실 처음 크래커나인을 사용 했을 때는 제플린과 큰 차이를 못 느꼈어요.근데 프로젝트를 진행 하다 보니 크래커나인의 코드 생성 기능이 있어서 좀 더 편리하다고 느껴지는 순간이 오더라구요. 제플린을 보고 개발 할 때는 코드나 수치를 직접 입력하다 보니 실수를 할 때도 있었고, 여전히 XML 작성하는 수고로움이 남아 있었거든요.근데 크래커나인의 레이아웃 코드 생성 기능을 사용해서 나온 XML 코드가 100% 는 아니지만 70~80%는 작업을 안해도 될 수준이더라구요. 그래서 XML 작성하는데 들이는 시간이 많이 줄어 들었어요.크래커나인을 익히는데 어렵지는 않으셨어요?타 서비스를 사용한 경험이 있어서 많은 부분 기능을 따로 매뉴얼로 보지 않아도 파악이 됐는데요.사용하는데 크게 문제는 없었던 거 같아요. 직관적으로 파악이 안 되는 기능은 사용하지 못한 것 같지만 따로 매뉴얼은 찾아보지 않았습니다.Cracker9의 어떤 기능이 가장 편리하셨나요?당연히 레이아웃 코드 자동 생성 기능이었습니다.손으로 직접 코딩 하지 않고 View들의 관계를 맺으면 자동으로 View의 관계와 속성을 xml 코드로 생성해주는 기능이 개발하는 데에 상당히 많은 도움이 되었습니다.Cracker9을 사용하면서 불편했던 점이나 개선했으면 하는 부분이 있었나요?Asset 이름이나 리소스(문자열, drawable) 이름이 해쉬값이나 text1, text2 이런 식으로 되어 있어 나중에 다 변경을 해 주어야 되었습니다. 이 부분은 개선이 되었으면 좋겠습니다.※ 위의 내용은 Cracker9 0.9.5 에서 개선되었습니다.Cracker9의 정식 버전으로 출시가 되면 사용할 의향이 있으신가요?네, 사용할 것 같아요. 가격만 너무 비싸지 않으면 전 무조건 사용하겠습니다.App을 만들거나 디자이너와 소통해야 하는 다른 개발자들에게 알려주고 싶은 Cracker9 사용 Tip이 있다면 알려주세요~디자이너가 텍스트나 이미지 단위로 View를 만드는데요. 개발자가 원하는 모든 View를 만들진 않기 때문에 Custom Layout 활용은 필수입니다.Custom Layout을 잘 활용하시면 원하시는 레이아웃 작업이 모두 가능합니다. 그리고, 오른쪽에 Tree Structure 패널이 있는데요.View의 트리 구조 순서로 레이아웃 xml 코드가 생성되기 때문에 어떻게 코드가 만들어질지 예상할 수 있어요. 물론 View의 순서나 부모/자식 관계는 마우스 드래그로 편집할 수 있습니다.마지막으로 Constraint Layout의 관계를 맺을 때, View가 작으면 정확하게 클릭하여 작업하기 어려울 수 있는데요. 당연하지만 View를 확대해서 연결 지으면 쉽게 작업할 수 있습니다.마지막으로 Cracker9에게 한 말씀해주세요~저는 안드로이드 App을 개발하면서 레이아웃 작업은 초반에 하는 시간 잡아먹는 노가다 작업이라고 생각을 했는데요.레이아웃을 자동으로 생성해주는 Cracker9을 사용하면서 더 이상 노가다라는 생각을 하지 않게 되었습니다. 아직은 Beta 버전이라 부족한 부분이 보이지만, 개선될 것이라고 믿고 계속 사용할 거 같아요.앞으로 발전하는 모습 기대하겠습니다 파이팅!#에이치나인 #디자이너 #개발자 #협업툴 #크래커나인 #솔루션기업 #팀원인터뷰 #기업문화 #조직문화 #팀원자랑
조회수 1226

EOS Token 생성과 발행, 전송

이번시간에는 배포한 Contract를 통해 Token 발행과 전송을 해보겠습니다. 이를 위한 준비는 아래 2미디엄 글을 참조해주세요EOS Smart Contract 를 위한 준비EOS Smart Contract 배포먼저 저번 시간에 배포한 token 발행 abi 를 확인해 보겠습니다.$ cleos get abi hexlanthenryget abiabi를 확인하다보면 actions 라는 항목에 총 3개의 action이 있음을 확인할 수 있습니다. 이 3개의 name이 실행할 수 있는 action입니다. token발행은 create action을 통해 진행할 수 있습니다.Token 생성$ cleos push action hexlanthenry create '["hexlanthenry", "10000000000.0000 HEX"]' -p hexlanthenrycreate action 실행 결과create action 을 통해 ‘HEX’ 토큰을 100억개 생성했습니다. create 라는 action의 인자는 account_name(hexlanthenry), maximum_supply(10000000000.0000 HEX) 입니다. 즉 첫번째 인자는 토큰의 발행자를 나타내며, 두번째 인자는 토큰의 최대 수량을 나타냅니다.이 인자가 어떻게 들어가는지는 abi 의 struct 를 확인하면 알 수 있습니다.abi의 create structparameter 1 : account_name type— issuerparameter 2 : asset type — maximum_supply+ 저번 강의에서 공지한데로 다음 포스팅에서는 abi가 무엇을 뜻하는지, 이를 통해 어떻게 action을 실행할 수 있는지 알아보도록 하겠습니다.Token 발행생성과 발행 이 2개의 개념이 헷갈릴 수 있습니다. create action을 통한 생성은 최대 발행량을 결정 하는 것이며, issue action 은 토큰을 유통 시키는 것입니다.create : token 생성과 동시에 최대 발행량 결정issue : token 의 유통따라서 issue action을 통해 이전에 생성한 HEX token을 발행해보겠습니다.$ cleos push action hexlanthenry issue '["hexlanthenry", "10000.0000 HEX", "initial issue"]' -p hexlanthenryissue contract 실행 결과issue action 역시 data로 어떤 인자가 들어가는지는 abi를 통해 확인 가능합니다.abi의 issue structparameter 1 : account_name type — toparameter 2 : asset type — quantityparameter 3 : string type — memomemo 는 transfer 가 어떤 목적인지에 대해 설명해주는 인자 입니다. 생략해도 되는 값으로, 원하시면 parameter 개수를 유지하는 선에서 empty string을 넣으시면 됩니다. memo를 어떻게 쓰면 유용한지에 대해서도 다른 포스팅에 담도록 하겠습니다.issue가 잘 실행 되었는지 확인해 보겠습니다.$ cleos get currency balance hexlanthenry hexlanthenry저는 issue 를 4번 수행한 후 balance 를 체크 했기 때문에 총 40000개의 HEX token이 존재하는 것을 확인 할 수 있습니다.hexlanthenry 의 HEX token개수예외사항1create 하지 않은 token을 issue 할 경우해당 symbol 이 존재하지 않음예외사항2생성한 token 수보다 많은 양을 issue 할 경우maximum supply를 초과함Token transfer마지막으로 token을 다른 계정에 전송 해보도록 하겠습니다. 다른계정에 token을 보내야 하기 때문에 계정을 생성하거나 존재하고 있는 계정을 사용하시면 됩니다.아래 명령으로 hexlanthenry 계정이 babylion1234 계정으로 10000개의 HEX 토큰을 보냅니다.$ cleos push action hexlanthenry transfer '["hexlanthenry", "babylion1234", "10000.0000 HEX", "first"]' -p hexlanthenrytransfer 실행결과transfer 시 들어가는 data에 대해서도 abi를 확인해보겠습니다. 다른 action보다 많은 인자를 필요로 합니다. [“hexlanthenry”, “babylion1234”, “10000.0000 HEX”, “first”]abi의 transfer structparameter 1 : account_name type — fromparameter 2 : account_name type — toparameter 3 : asset type — quantityparameter 4 : string type — memo실제로 babylion1234 계정을 확인해 보면, 방금 배포한 HEX token을 보유하고있는 것을 확인할 수 있습니다.babylion1234의 HEX 보유이번 포스팅에서는 token을 생성과 발행 그리고 전송을 다뤄봤습니다. EOS는 Ethereum 과 달리 토큰 발행을 매우 쉽게 진행할 수 있습니다. 이 두 dapp의 차이에 대해서도 포스팅을 하고 싶으나 우선 다음 포스팅에서는 contract 개발의 기초를 다루도록 하겠습니다.감사합니다.#헥슬란트 #HEXLANT #블록체인 #개발자 #개발팀 #기술기업 #기술중심
조회수 1078

테이블이냐, 컬렉션이냐, 그것이 문제로다!(KOR)

편집자 주 외래어 표기법에 따르면 ‘원어에서 띄어 쓴 말은 띄어 쓴 대로 한글 표기를 하되, 붙여 쓸 수도 있다.’고 규정하고 있다.(제3장 제1절 영어의 표기, 제10항과, 컴퓨터 전문어, 전기 전문어 등) 즉 ‘원칙’과 ‘허용’이 모두 가능하다는 의미다. 이를 바탕으로 여러 표기 용례를 참고한 결과, TableView는 ‘테이블뷰(원칙)’로 표기해야 하나, 본문에서는 독자의 가독성을 높이기 위해 ‘테이블 뷰(허용)’로 표기한다. 응용하여, CollectionView는 ‘컬렉션 뷰’로, TableViewCell은 ‘테이블 뷰 셀’ 등으로 띄어 쓴다. Overview앱에서 데이터를 사용자에게 보여줄 땐 여러 가지의 모습으로 나타납니다. 설정 앱처럼 목록으로 보여줄 때도 있고, 사진 앱처럼 그리드(grid) 형식으로 보여줄 때도 있습니다. 이처럼 데이터를 보여줄 때 많이 사용되는 뷰는 테이블 뷰(UITableView) 또는 컬렉션 뷰(UICollectionView)입니다. 각자 특징이 있기 때문에 앱의 성격에 따라 적절한 뷰를 사용해야 합니다. 왜냐하면 목록을 보여주는 디자인을 바꿀 때, 다시 개발해야 하는 수고를 덜 수 있기 때문입니다. 이번 글에선 각각의 뷰를 간략하게 알아보겠습니다. 목록 형식의 설정 앱과 그리드 형식의 사진 앱 스크린샷테이블 뷰(UITableView)단일 열에 배열된 행을 사용해 데이터를 표시하는 뷰입니다. 수직 스크롤만 가능하며, 테이블의 개별 항목을 구성하는 셀은 테이블 뷰 셀(UITableViewCell) 객체입니다. 테이블 뷰는 이 객체들을 이용해 테이블에 표시되는 행을 그립니다. 여러 행은 하나의 섹션 안에 구성될 수 있으며, 각 섹션은 헤더(header)와 푸터(footer)를 가질 수 있습니다. 섹션과 행은 인덱스 번호로 구별하는데, 번호는 0부터 시작합니다. 테이블 뷰는 plain과 grouped 스타일 중 한 가지의 스타일을 가질 수 있습니다. Plain 스타일은 보통 목록 스타일입니다. 섹션의 헤더와 푸터는 섹션 분리기(inline separators)로 표시되고 스크롤을 할 때 해당 섹션 안에 있는 콘텐츠 위에 나타납니다. Grouped 스타일은 시각적으로 뚜렷한 행 그룹을 표시하는 섹션이 있습니다. 섹션의 헤더와 푸터는 콘텐츠 위에 나타나지 않습니다. 아래와 같은 사진을 보시면 확연히 차이를 볼 수 있습니다. plain 스타일의 연락처 앱과 grouped 스타일의 설정 앱테이블 뷰의 많은 메소드들은 인덱스패스(NSIndexPath) 객체를 매개변수 또는 리턴 값으로 사용합니다. 테이블 뷰는 해당하는 행의 색인 인덱스와 섹션 인덱스 값을 가져올 수 있게 인덱스패스의 범주를 선언합니다. 또한 색인 인덱스와 섹션 인덱스 값을 가지고 인덱스패스를 만들 수 있습니다. 특히 여러 섹션이 있는 테이블 뷰는 섹션 인덱스 값이 반드시 있어야 행의 인덱스 번호로 구별할 수 있습니다.override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> AttractionTableViewCell {         // Table view cells are reused and should be dequeued using a cell identifier.         let cellIdentifier = "AttractionTableViewCell"              guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? AttractionTableViewCell else {             fatalError("The dequeued cell is not an instance of AttractionTableViewCell.")         }                 let attraction = attractions[indexPath.row]                 cell.attractionLabel.text = "\(indexPath.row). \(attraction.nameWithDescription)"         cell.attractionImage.image = attraction.photo                 cell.attractionImage.tag = indexPath.row                 attraction.indexPath = indexPath                 ...                 return cell     } 위의 코드는 데이터 소스(data source) 메소드로, 테이블 뷰의 특정한 위치에 셀을 추가합니다. 다시 말해, 이 메소드는 테이블 뷰가 ‘표시할 새로운 셀이 필요할 때마다’ 특정 행에 노출할 정보가 있는 셀을 만들고 리턴하는 걸 말합니다. 매개변수로 필요한 셀 객체의 행을 가리키는 indexPath 값을 전달합니다. 그리고 indexPath의 row 값을 이용해서 attraction이라는 배열 인덱스로 활용하고, 셀에 표시할 정보들을 설정합니다. 여기서 attraction 배열은 관광 명소들의 정보들이 담고 있는 배열인데, 1행은 첫 번째로 저장한 관광 명소, 2행은 두 번째로 저장한 관광 명소 등 순서대로 설정하도록 indexPath.row 값을 이용하는 것입니다. indexPath의 row 값과 배열의 인덱스 값은 0부터 시작하기 때문입니다. 해당 예제는 섹션이 1인 경우이기 때문에 섹션 인덱스 값이 없지만, 섹션이 여러 개 있다면 반드시 섹션 인덱스 값을 이용해서 설정해야 합니다.테이블 뷰 객체는 데이터 소스(data source)와 델리게이트(delegate)가 필요합니다. 데이터 소스는 UITableViewDataSource 프로토콜을 구현해야 하고, 델리게이트는 UITableViewDelegate 프로토콜을 구현해야합니다. 데이터 소스는 테이블 뷰가 테이블을 만들 때 필요한 정보를 제공하고 테이블의 행이 추가, 삭제 또는 재정렬할 때 데이터 모델을 관리합니다. 델리게이트는 화면에 보이는 모습과 행동을 담당합니다. 예를 들어 표시할 행의 수, 사용자가 특정 행을 터치했을 때, 행의 재정렬 등과 같은 것입니다.override func numberOfSections(in tableView: UITableView) -> Int {         // #warning Incomplete implementation, return the number of sections         return 1     }      override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {         // #warning Incomplete implementation, return the number of rows         return attractions.count     } 위의 두 소스는 데이터 소스가 필수적으로 구현해야 하는 메소드입니다. 하나는 섹션의 개수를 리턴하고, 또 하나는 한 섹션 안에 있는 행의 개수를 리턴합니다.테이블 뷰는 수정 모드에서 행을 추가, 삭제, 재정렬할 수 있습니다. 각 행은 테이블 뷰 셀에 연관된 editingStyle에 따라서 추가, 삭제, 재정렬을 할 수 있는데, 예를 들어 editingStyle이 insert라면 추가하는 메소드를 실행하고, delete면 삭제하는 메소드를 실행합니다. 행의 showsReorderControl 속성이 true라면, 재정렬하는 메소드를 실행할 수 있습니다.// Override to support editing the table view.     override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {         if editingStyle == .delete {             // Delete the row from the data source             ...                 // delete rows and attractions and reload datas             attractions.remove(at: indexPath.row)             tableView.deleteRows(at: [indexPath], with: .middle)             tableView.reloadData()         } else if editingStyle == .insert {             // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view         }     } 위 소스는 editingStyle이 delete일 때 셀을 삭제하고 테이블 뷰를 다시 로드하는 기능을 구현한 것입니다.테이블 뷰를 만드는 가장 쉽고 권장하는 방법은 바로 스토리보드에서 테이블뷰컨트롤러(UITableViewController)를 이용해서 만드는 겁니다. 런타임에 테이블뷰컨트롤러는 테이블 뷰를 만들고 델리게이트와 데이터 소스를 자기 자신으로 할당합니다.컬렉션 뷰(UICollectionView)컬렉션 뷰는 테이블 뷰에서 할 수 있는 모든 것을 할 수 있습니다. 섹션을 가질 수 있고, 인덱스패스 값을 이용해서 셀을 구별합니다. 이 셀들은 컬렉션 뷰 셀(UICollectionViewCell)의 서브 클래스이며 데이터 소스(UICollectionViewDataSource)와 델리게이트(UICollectionViewDelegate)가 필요합니다. 셀을 추가, 삭제, 재정렬하는 기능도 구현할 수 있습니다. 그렇다면 컬렉션 뷰와 테이블 뷰를 구분하는 특징은 무엇일까요? 바로 레이아웃입니다. 컬렉션 뷰는 여러 개의 열과 행으로 셀을 표현할 수 있습니다. 예를 들어, 그리드(grid) 형태로 아이템의 목록을 보여줄 수 있습니다. 그래서 수직 스크롤뿐만 아니라 수평 스크롤도 할 수 있습니다.스토리보드에서 디자인한 테이블 뷰 셀과 컬렉션 뷰 셀위 스크린샷에서 테이블 뷰와 컬렉션 뷰의 가장 큰 차이는 바로 셀입니다. 테이블 뷰에서는 하나의 열에 여러 행을 표시하는 형식이기 때문에, 셀의 모습을 행에 맞춰서 디자인합니다. 하지만 컬렉션 뷰는 열과 행을 만들 수 있기 때문에, 꼭 행의 모습이 아니더라도 다양한 모습으로 셀을 디자인할 수 있습니다. 컬렉션 뷰 셀의 가장 큰 특징이기도 하죠. 위처럼 셀을 디자인하고 앱을 실행하면 아래의 화면이 나타납니다.테이블 뷰와 컬렉션 뷰의 앱 화면 차이또한 컬렉션 뷰는 레이아웃 객체가 있습니다. 기존에 제공하는 flow layout을 사용해도 괜찮지만, 본인이 원하는 레이아웃 모양을 custom layout을 만들어서 사용합니다. 이를 담당하는 프로토콜은 UICollectionViewDelegateFlowLayout 입니다.func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {         let fullWidth = collectionView.frame.size.width - (self.CGFLOAT_INSET_WIDTH * 3) - (self.CGFLOAT_ITEMSPACING * 3)         let width = fullWidth/3         return CGSize(width: width, height: width + self.CGFLOAT_HEIGHT_ATTRACTIONCELL_DEFAULT)     }         func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {         return UIEdgeInsetsMake(self.CGFLOAT_LINESPACING_VERTICAL, self.CGFLOAT_INSET_WIDTH, self.CGFLOAT_LINESPACING_VERTICAL, self.CGFLOAT_INSET_WIDTH)     } 위 소스에서 collectionView(:layout:sizeForItemAt:) 메소드는 해당하는 셀의 사이즈를 설정하고, collectionView(:layout:insetForSectionAt:) 메소드는 섹션 안에 margin을 설정합니다.여러 모양의 셀을 이루어 하나의 뷰 화면을 구현할 수도 있습니다. 섹션마다 셀을 만들어 각각 다른 모습의 셀을 설정하고, 한 화면에 다양한 모습의 셀을 가진 뷰를 만드는 것입니다. 예를 들어, 헤더, 메뉴, 본문, 푸터 각각 셀을 만들어서 원하는 모양으로 만들고, 하나의 뷰 컨트롤러에 셀을 조합해서 한 화면에 나타나게 할 수 있습니다. 이 방법을 사용하면 자주 사용하는 셀을 재활용할 수 있습니다. 똑같은 헤더와 푸터 셀을 여러 번 만들지 않고 기존의 셀을 재활용하면 시간도 절약하고, 훨씬 깔끔한 소스를 만들 수 있을 겁니다.브랜디 앱 스크린샷 일부위의 스크린샷처럼 여러 화면에서 보여줘야 할 똑같은 뷰가 있을 때, 셀 xib 파일을 만들고 컬렉션 뷰에서 셀을 섹션별로 설정 및 사용하면 재활용하기 좋습니다.Conclusion지금까지 테이블 뷰와 컬렉션 뷰의 특징들을 살펴봤습니다. 한마디로 정리하면 테이블 뷰는 가장 간단한 목록을 만들 수 있습니다. 컬렉션 뷰는 다양한 모습의 목록으로 커스터마이징(Customizing)할 수 있습니다.그렇다면 우리는 어떤 것을 선택해야 할까요? 구현할 목록이 얼마나 복잡한지에 따라 선택은 달라집니다. 테이블 뷰는 간단하고 보편적인 목록을 만듭니다. 반면에 컬렉션 뷰는 특정한 모습의 목록을 만들 수 있습니다. 그래서 테이블 뷰는 목록이 간단하고 디자인 변경이 없을 때만 사용하길 권장합니다. 하지만 나중에 디자인이 바뀔 수도 있다면 컬렉션 뷰를 사용하는게 더 좋겠죠.Simple is the best! 간단하게 구현할 수 있는 건 테이블 뷰를 사용합시다. 테이블 뷰에서 구현하기 힘들다면 컬렉션 뷰를 이용해 개성 있는 목록을 마음껏 만들어봅시다!참고UITableView - UIKit | Apple Developer DocumentationUICollectionView - UIKit | Apple Developer Documentation 글김주희 사원 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 2818

DevOps 팀을 위한 모니터링 팁

다음 중 몇 개나 해당하시나요?1~5명 규모의 작은 개발팀에서 일한다.DevOps 조직이다.우여곡절 끝에 서비스는 런칭했지만, 개발과 동시에 운영을 해야하는 상황이다.서버 인프라 지식이 별로 없다.무중단 서비스 운영 경험이 별로 없다.팀 내에 시스템 엔지니어(SE)와 데이터베이스 전문가(DBA)가 없다.하나라도 해당한다면 이 글이 도움이 될 지도 모릅니다.누구나 쉽고 빠르게 앱을 만들고 서비스를 런칭할 수 있는 시대가 되었지만 문제는 런칭 이후입니다. 런칭 이후에는 고객이 100명이라도 안정적인(High Availability) 서비스를 운영해야 하는 것이 백엔드 개발자의 임무이기 때문입니다.안정적인 서비스를 운영하기 위해서는 체계적인 모니터링 필수라고 하는데 그마저도 쉽지 않습니다. 가장 큰 문제는 (장애가 터지기 전까지) 무엇을 모니터링 해야 하는지조차 모른다는 것이고, 당장 개발해야할 것들이 산더미처럼 쌓여있는데 사람도 부족한 것도 문제입니다.그렇지만 누군가는 해야하는 일입니다. 리디북스 역시 모니터링이 전혀 없던 시절이 있었으나, 크고 작은 실패와 좌절을 겪으며 조금씩 경험을 쌓아가고 있습니다. 이번 글에서는 우리가 모니터링과 관련하여 고민해 온 내용들을 소개해볼까 합니다.어떻게 모니터링할 것인가시스템의 안정성을 높이기 위해 투입해야 하는 노력은 지수적으로 증가합니다. 아래 표에서 보듯이 SLA 를 99.999% 에서 99.9999% 로 높이려고 한다면 1년에 약 5분의 가용시간을 얻을 뿐이지만 이를 위해 수백시간 이상의 노력을 들여야 합니다.가용성연간 장애 시간주간 장애 시간99.995%26.28 분30.24 초99.999%5.26 분6.05 초99.9999%0.525 분0.6048 초완벽함을 추구하면 할 수록 얻을 수 있는 고객 만족은 미미한 것에 비해 이를 위한 개발자의 노력은 기하급수적으로 증가합니다. 따라서, 먼저 대응의 적정선을 찾고 효율적으로 움직이기 위한 계획을 세워야 합니다.리디북스에서는 해야할 일을 4가지로 분류하여 중요한 일부터 처리하는 아이젠하워 매트릭스에서 그 대응 원칙을 차용하였는데, 그 이유는 시사하는 바가 동일하기 때문이었습니다. 즉, 중요한 것은 대부분 긴급하지 않고, 긴급한 것은 대체로 중요하지 않다는 점입니다. 그리고 매트릭스의 두 축은 아래와 같습니다.얼마나 급한가?사무실의 무선 인터넷이 안된다면 서비스에 큰 문제는 아니지만, 당장 해결해야 하는 급한 일입니다. 반대로 백업 스크립트가 며칠째 동작하지 않아서 최근 데이터의 스냅샷이 없다면, 이는 당장 해결할 필요는 없겠지만 매우 중요한 일입니다.그리고 장애란, 단순히 “고장”을 의미하는 것이 아니라 서비스 이용에 지장이 없더라도 어떤 수치나 결과가 예상과 다른 상황을 의미해야 합니다. 예를 들어, 웹서버의 평균 CPU 사용률이 70%가 넘는다거나 네트워크 대역폭을 90% 이상 사용하는 상황은 정상이 아닙니다. 조금만 트래픽이 몰려도 문제가 발생할 가능성이 매우 높기 때문에 잠재적인 장애로 간주해야 합니다.우리는 급한 문제를 우선적으로 처리하는 경향이 있어서, 덜 급하지만 더 중요한 일을 놓치는 경우가 많습니다. 이를 피하려면 장애의 그 심각도에 따라서도 구분해야 합니다.얼마나 심각한가?심각도를 처음부터 너무 상세하게 구분할 필요는 없으며, 크게 서비스 이용에 치명적인 것과 그렇지 않은 것으로 나누어 생각하면 됩니다. “치명적”의 의미는 서비스마다 다를 수 있지만 대개 아래에 해당합니다.사업에 지장을 초래한다.고객을 잃는다.만약 웹페이지의 로딩 속도가 매우 느려서 나쁜 이미지를 준다면 이 역시 치명적일 수 있습니다. 실제로 아마존에서는 로딩 속도가 100ms 지연될 때마다 눈에 띄는 매출 하락이 발생했다는 테스트 결과가 있습니다. 따라서 속도에 대한 매트릭을 모니터링 지표에 추가하는 것은 좋은 선택입니다.이상을 토대로 장애 종류에 따른 대응 원칙을 정리하면 아래와 같습니다. 급함안급함심각함➀ 즉각 대응, 즉각 인지➁ 평소 보완, 항상 경계안심각함➂ 빨리 대응, 최소 대응➃ 대응하지 않기이 중에서 항상 의식하고 놓치지 말아야 하는 것은 안급하지만 잠재적으로 심각한 장애(➁)입니다. 그리고 모니터링은 한 번 시작하게 되면 관리를 위한 비용이 꾸준히 투입되어야 하기 때문에 사소한 문제(➂, ➃)를 굳이 파헤치는 것은 오히려 독이 될 수도 있습니다.모니터링 측면에서 본다면 발생중인 장애는 최대한 빨리 발견하는 것이 중요하며, 잠재적인 장애는 상태의 변화를 최대한 빨리 감지하는 것이 중요합니다. 예를 들어, 디스크의 여유공간은 완전히 바닥나기 전까지 어떠한 경고도 나타나지 않지만 부족한 상황이 발생하면 어떤 부작용이 생길지 예측할 수 없습니다.필수 모니터링 갖추기모니터링을 해야할 대상은 기술 스택과 코드 구현에 따라 달라지겠지만, 빼놓을 수 없는 것들이 몇 가지 있습니다. 리디북스에서는 서버의 프로비저닝과 동시에 아래 내용들을 함께 준비하고 있습니다.1. 리소스 및 시스템 모니터링각종 시스템 리소스 및 하드웨어 상태는 필수 모니터링 대상입니다. 모니터링 툴을 설치해보면 측정해주는 항목들이 너무 많아서 당황스러운 경험을 하게 되는데요. 그 중에서도 우리가 주목하고 있는 항목들은 아래와 같습니다.CPU UsageLoad AverageDisk UsageDisk Utilization (iowait, IOPS)Swap Memory Usage (사용시)Temperature (인프라 직접 구축시)RAID Status (인프라 직접 구축시)S.M.A.R.T Errors (인프라 직접 구축시)이 중 몇가지는 New Relic 에서 무료로 지원하므로 당장 여력이 없다면 이를 이용하는 것도 좋은 방법입니다.클라우드 환경이 아닌 데이터센터에서 인프라를 직접 구축하여 운영하고 있다면 좀 더 많은 노력이 필요합니다. 하드웨어적인 장애를 직접 신경써야 하기 때문입니다. 실제로 팬(fan)이 고장나거나 케이블이 환풍구를 막아서 서버의 온도가 비정상적으로 높아지다가 기기가 오동작하는 어처구니없는 상황도 발생합니다.Disk서버 환경에서 SSD 사용이 점점 대세가 되어가고 있는데, 최근 구글이 공개한 정보에 따르면 SSD에서 배드블럭이 발생하는 일은 매우 흔하며, 시간이 오래될 수록 안정성이 떨어진다고 합니다.따라서 디스크와 관련된 RAID나 S.M.A.R.T 오류는 가능한 빨리 대응해야 합니다. 특히 RAID 장비를 구성할 때에는 같은 공정에서 출하된 같은 벤더의 제품을 일괄적으로 구매해서 사용하기 때문에, 동일한 하드웨어 결함을 지니고 있거나 평균 수명도 비슷하므로 결코 안이하게 대응해서는 안됩니다.리디북스에서는 전자책 원본을 보관하는 스토리지에서 4개의 사본(replica) 중 3개가 연달아 깨지는 끔찍한 사고를 경험한 이후로, 디스크 오류는 1순위로 대응하고 있습니다. 참고로 스토리지 서버를 구축한지 3년째가 되는 해였고, 모두 S사의 제품이었습니다.iowait 은 CPU가 유휴(idle) 상태로 I/O를 대기하는 시간을 나타낸 수치입니다. 이를 통해 현재 시스템이 I/O 병목을 겪고 있는지 판단할 수 있기 때문에 중요합니다. 이 수치가 너무 높다면 블록 디바이스나 네트워크가 너무 느린 상황이거나 포화 상태일 수 있으므로, 더 높은 IOPS 장비로 업그레이드하거나 부하를 분산해야 합니다.단, CPU 성능에 영향을 받는 수치이므로 고성능 CPU를 사용할수록 평균 iowait이 높게 측정됩니다. (따라서 성능을 평가하기 위한 지표로는 IOPS도 함께 분석해야 합니다.)Load AverageLoad Average(평균 부하)는 마치 서버의 종합 성적표 같아서, 이 역시 주목할 필요가 있습니다. Load Average에 변동이 생긴다면 평소와는 다른 처리량(throughput)을 내고 있다는 뜻입니다. 요청량이 증가하여 수치가 올라갔다면 서버 증설과 튜닝에 대비해야 하지만, 그렇지 않다면 어딘가 병목이 발생하여 처리 효율이 낮아졌다는 신호입니다.아직 Load Average를 모니터링하고 있지 않다면 주요 서버군부터 아래 규칙을 참고하여 초기 기준치를 설정하기를 권장합니다. 물론 어디까지나 초기 설정 값이며, 실제 상황에 적합하지 않을 수 있습니다.Warning Level : 0.7 * number of cores Critical Level : 1.0 * number of cores간혹 커널 자체에 문제가 있거나, 커널 모드에서 예외가 발생하는 경우에는 syslogd 데몬이 남기는 로그를 파악해야 합니다. Papertrail, Splunk, Loggly 등의 서비스는 크리티컬 수준 이상의 syslog 에 대해 알림을 설정할 수 있을 뿐 아니라, 텍스트 형태로 남겨지는 모든 로그에 대한 관리를 쉽게 도와줍니다. 비록 유료지만 커널 모니터링 용으로만 사용한다면 비용이 많이 들지 않습니다.2. 응용프로그램 모니터링앱이나 서버에서 발생하는 크래시와 예외를 수집하는 도구 역시 장애 예방에 필수입니다. 해당 기능을 실시간으로 제공하는 다양한 서비스들이 존재하는데 많이 쓰이는 것으로는 Sentry, Rollbar, Airbrake, NewRelic APM 등이 있습니다. 대부분 5분만에 설정이 가능한데다 어느것을 선택하더라도 핵심 기능에는 부족함이 없습니다.단, 현재까지 가성비로는 Sentry가 제일 뛰어납니다. Python의 Flask와 Jinja의 개발자로 유명한 Armin Ronacher가 팀에 합류했기에 발전가능성 측면에서도 많은 기대가 됩니다.Sentry의 실시간 에러 대시보드3. 데이터베이스 모니터링팀에 DBA가 있나요? 모든 서버 개발자들이 인덱스와 스토리지 엔진의 특징에 대해 잘 이해하고, DB를 능숙하게 다루나요? 그것도 아니라면 개발자들이 작성한 모든 스키마와 쿼리에 대한 검증 과정을 거치고 있나요? 만약 그렇지 않다면 슬로우쿼리 모니터링은 필수입니다.우리가 서비스 초기에 겪은 문제의 대부분은 인덱스를 잘 다루지 못하거나 새로 도입한 ORM에 대한 이해도가 낮아서 발생한 문제였습니다. 그 중에서도 특정 쿼리가 너무 많은 I/O를 유발하던 것이 주된 원인이었으며, 작고 가벼운 쿼리가 너무 많이 호출되어 문제가 된 경우는 거의 없었습니다.잘못 설계된 스키마나 쿼리는 평소에는 드러나지 않다가 사용자가 몰리기 시작하면 큰 부하를 발생시켜서 기어이 서비스를 마비시키곤 합니다. 문제가 커지기 전에 그 조짐을 감지할 수는 없을까, 고민 끝에 우리가 시도한 방법은 “2초 이상 수행되는 쿼리에 대해서 로그를 남기고, 초당 3개 이상 로그가 발생할 경우 알림”을 받도록 하는 것이었습니다.MySQL에서는 아래 설정으로 로그를 활성화시킬 수 있습니다.[mysqld] long_query_time=2 # 2초 이상 수행되는 쿼리에 대해서 slow_query_log=1 # 로그를 남겨주세요 쿼리 분석에는 Percona의 pt-query-digest 를 추천합니다. VividCortext 혹은 MONyog 등의 솔루션은 시각적으로 화려하고 실제로도 강력한 기능을 갖추고 있지만, 유료라는 큰 단점이 있습니다.모니터링을 통해 알림을 받게 되면 문제가 더 커지기 전에 해당 기능을 수정하거나 중단시킬 기회가 생깁니다. 특히 새롭게 추가한 기능을 배포할 때 서비스가 불안해 질 수 있는데, 퍼포먼스 문제를 미리 발견하고 롤백을 서두를 수 있다는 것도 장점입니다.물론 가장 이상적인 상황은 n초 이상 수행되는 쿼리를 모두 없애는 것입니다. 하지만 현실은 튜닝을 포기하고 테이블을 풀스캔하도록 두는게 나은 선택일 수 있으며, OLAP/ETL 인프라가 별도로 구축되어 있지 않은 상황에서는 어쩔 수 없이 슬로우쿼리가 발생하게 됩니다. 우리가 초당 로그 갯수로 판단을 하게된 것도 이러한 이유 때문이었습니다.자동으로 슬로우 쿼리를 받아보면 문제해결에 도움이 됩니다.4. 배치 작업(scheduled task) 모니터링매일 백업 스크립트를 돌리고는 있는데, 백업이 정상적으로 완료가 되었는지는 어떻게 판단하면 될까요? 에러는 위에서 설명한 도구들로 확인이 가능하겠지만 스크립트가 수행도중 멈춰버렸거나, 서버의 전원이 꺼졌다면? 게다가 크론 작업(crontab)이 수십개가 넘어가면 이를 수동으로 체크하는 것도 일이므로, 반드시 자동화해야 합니다.이러한 상황에서 활용할 수 있는 유용한 도구가 PushMon 입니다. PushMon은 정해진 시간에 ping을 보내지 않으면 이메일이나 SMS로 알림을 주는 서비스로, 원리는 매우 단순하나 없어서는 안될 기능을 “무료”로 제공합니다.모니터링에 대응하기모니터링을 효율적으로 하기 위한, 즉 서비스 안정성을 높이기 위한 핵심 원칙은 “필요한 인원이 필요한 알림만 받는것”입니다.알림이 너무 많이 와서 음소거(Mute)를 하고 싶은 생각이 든다면 모니터링 체계에 문제가 있다는 신호입니다. 불필요하게 많은 경고는 안전 불감증을 낳을 뿐더러 정작 중요한 경고를 놓칠 확률을 높이기 때문입니다. 치명적인 알림은 모든 채널로 즉각 수신하고, 경고성 알림은 메일로 수신하되 정기 리포트나 메일함 자동분류 기능을 이용하여 중요한 정보를 놓치지 않는 습관이 중요합니다.불필요하게 많은 인원이 알림을 받는 상황도 문제입니다. 알림 수신자를 늘리면 모니터링의 퀄리티가 높아질 것이라고 생각하지만 절대 그렇지 않습니다. 오히려 방관자 효과가 발생하여 아무도 알림에 대응하지 않는 상황이 발생하게 됩니다. 따라서 알림이 발생했을 때에는 1차, 2차 담당자를 사전에 지정하고 운영할 필요가 있습니다.방관자 효과의 적절한 예팀에서 Slack을 사용한다면 기능 연동을 통해 실시간으로 이슈를 파악할 수 있고, 담당자 지정을 보다 쉽고 명확하게 할 수 있습니다. 특히, 별것 아닌 이모티콘(emoji) 만으로도 방관자 효과를 크게 줄일 수 있는데, 예를 들면 아래와 같습니다.👀 - 확인중 ✅ - 확인 완료 😱 - 확인은 하였으나 나는 해결을 못하겠음Sentry를 Slack에 연동한 모습또한, 모니터링 시스템에 대한 모니터링도 중요합니다. SaaS를 이용하는 경우에는 최악의 경우 해당 서비스의 점검기간에 대비할 수 없으며, 심지어는 점검중이라는 사실 조차 인지하지 못할 수 있습니다. 이에 대비하기 위해 리디북스에서는 Server Density로 모니터링을 모니터링하고 있습니다.맺음말장애를 얼마나 꼼꼼하게 예방하는지, 그리고 얼마나 즉각적으로 반응하는지는 팀 구성원의 실력으로 정해지는것이 아니라 팀의 문화와 원칙에 따라 정해집니다. 아직 팀에 뚜렷한 대응 원칙이 없다면 먼저 상황에 맞는 기준과 척도를 결정하고 공유해볼 것을 추천합니다.무엇보다 DevOps를 수행하는 것은 사람임을 잊지 말아야 합니다. 인간은 99.99% 가용성이나 24/7 을 보장하지 못하며, Uptime은 하루도 되지 않습니다. 최근 DevOps가 대세가 되어가지만 Ops에서의 인간적인 측면은 진지하게 고려되지 않고 있습니다. 이러한 환경을 개선하기 위한 HumanOps에 대한 소개와 함께 글을 마칩니다.     HumanOps 계명시스템을 만들고 고치는 것은 인간이다.인간은 지치고 스트레스를 받으며, 행복과 슬픔을 느낀다.시스템은 아직 감정이 없다. 오로지 SLA만 있다.인간은 스위치 온/오프 상태를 반복해야 한다.시스템을 운영하는 인간의 행복이 시스템의 안정성에 영향을 준다.빈번한 알림 == 인간의 피로최대한 자동화하고, 최후의 수단으로 인간에게 이관하라.문서화하고, 훈련하고, 시간을 아껴라.창피 주지 마라.인간의 문제는 시스템의 문제다.인간의 건강은 사업의 건강에 영향을 준다.인간 > 시스템#리디북스 #개발 #DevOPS #모니터링 #인사이트 #서버개발 #운영 #꿀팁
조회수 370

금요일의 해커톤

안녕하세요. 엘리스입니다!지난 8월 말, 엘리스의 야심 찬 첫 해커톤이 있었습니다. 이번 해커톤은 매주 금요일 찾아가는 문제 ‘금요일에 코딩하는 토끼’에 대한 수강생 여러분의 성원에 힘입어 개최되었습니다.주제는 ‘코딩 문제의 A에서부터 Z까지 직접 설계하고 제작한다.’ 해커톤에서는 아이데이션 단계에서부터 문제 기획과 코딩, 채점을 위한 그레이더 제작까지 코딩 문제의 모든 것을 다루었습니다. 물론 실제 문제 동작을 위해 실행과 채점을 반복하며 디버깅하여 완벽한 실습 문제를 만드는 것 역시 이번 경연의 핵심이었는데요.이를 통해 모든 참가자 여러분들은 일일 엘리스 아카데미 실습 문제의 출제자가 되었습니다. 어떤 과정을 거친 어떤 결과물들이 있었을까요?해커톤 현장 스케치해커톤의 소개를 경청 중이신 참가자 여러분.지금까지 프로그래밍 문제를 많이 풀어보셨을 여러분이, 반대로 문제의 출제자가 되어 문제를 구성하는 관점에서 생각해보고 채점 방식까지 고민해본다면 프로그래밍에 대한 이해도를 더 높일 수 있을 것이라는 기대로 이와 같은 해커톤이 기획되었습니다. 교육자로서 엘리스 플랫폼의 다양한 기능을 직접 이용해볼 수 있는 것은 일석이조의 이점이었죠!경직된 분위기를 깨고 뇌를 말랑말랑하게 만들기 위한 아이스 브레이킹 시간은 팀 대항전으로 진행되었습니다.간단한 코딩 문제를 가장 먼저 맞히는 팀이 점수를 얻는 스피드 코딩 게임을 통해서 순발력을 높이고, 잠시 후 해커톤에서 본격적으로 사용하게 될 엘리스 플랫폼과 친해질 시간도 가질 수 있었습니다.'그림 그리기 게임'에서는 각 팀 디자이너들의 창의력이 폭발! 개발과 관련된 온갖 단어들을 1초 만에 그림으로 표현해야 하는 설명자의 재치와 크로키 실력(?)이 강조되었던 순간이었는데요. 승자는 '오즈'팀! 모두 오즈 팀 디자이너의 그림 실력에 입을 다물지 못했다고 합니다.게임을 하는 동안 어느새 어색했던 처음의 분위기가 파괴되었습니다. ^^ 1시간 동안 문제의 초안을 기획하는 시간이 주어지고, 이어 각 팀의 아이디어 발표 시간이 있었습니다.해커톤의 룰은 아래와 같았는데요.실행 가능한 프로그래밍 문제 1개 출제.동화를 모티브로 한 문제 스토리를 기획.채점 가능한 그레이더 제작.모든 팀들이 알고리즘 문제를 기획해주셨습니다. 동화의 서사구조를 논리적으로 단순화하거나 변형하여 알고리즘 문제에 녹여낸 과정이 인상적이었습니다.아이데이션 단계에서는 문제의 완성된 모습이 전부 그려지지는 않았지만 많은 고민의 흔적과 창의적인 생각들을 엿볼 수 있어 이로부터 탄생될 프로그래밍 실습을 기대할 수 있었습니다.밤샘 코딩 중...우승 문제 소개기획하고 코딩하고 디자인을 하다 보니(!) 어느새 날이 밝아왔습니다. 이제 남은 것은 팀별 결과물 발표와 우승팀 시상 뿐!'금코토'를 패러디하여 팀 명을 지어주신 어린 왕자 팀. /* prince */로고까지 깨알 섬세!모든 팀이 각기 다방면에서 강점을 부각하는 문제를 출제해주셨기 때문에 우열을 가리기 어려웠는데요. ‘금코토’배 해커톤이라는 이름에 걸맞게 금코토 과목의 취지와 가장 부합하는 문제를 출제한 팀에게 가산점을 주어 우승팀을 선발하였습니다. 그 결과 대망의 우승 문제는...거울나라의 앨리스팀의 ‘케이크와 병’ 단순한 명료한 문제 구성과 초등학생도 이해할 수 있는 쉽고 친절한 프레젠테이션으로 인상 깊었던 문제였습니다. 완성도, 문제 활용도 면에서 금코토 문제를 능가하며 단순하면서도 재미있게 풀 수 있는 문제라는 심사위원들의 평가가 있었습니다. 우승팀인 거울나라의 앨리스 팀 전원에게는 엘리스 굿즈를 선물로 보내드립니다. :)이밖에 겁쟁이 사자를 동물의 왕으로 만들기 위해 용기의 성을 짓는 알고리즘 문제를 낸 오즈의 마법사 팀의 문제는 스토리에 착안하여 자칫 복잡해질 수 있는 내용을 세세한 문제 설계로 극복하려 했던 점이 우수하게 평가받았습니다. 술주정뱅이 별에 사는 만취한 아저씨를 옮기는 알고리즘 문제를 낸 ‘목요일에 코딩하는 어린 왕자’ 팀은 참신성과 '넓이 우선 탐색', '깊이 우선 탐색', '다익스트라 알고리즘'을 모두 공부해볼 수 있도록 한 문제 구성 면에서 높은 평을 받았습니다.큰 상품도 내걸지 않았던 첫 해커톤이었는데도 참가자분들 모두가 열과 성을 다해 밤을 새워 문제를 만들어 주셨습니다. 모든 참가자 여러분들께 감사의 말씀 전합니다. :) 해커톤 이후 진행한 설문 조사에서 100%의 확률로 모든 분들이 다음 해커톤에 재참가 의사를 밝히셨는데요. 모두 첫 해커톤을 즐겨주셨던 것 같네요. 엘리스에서는 앞으로도 해커톤을 지속적으로 개최할 예정입니다. 코끝 시려질 때쯤 더욱 풍성하고 유익한 기획의 해커톤으로 찾아뵐 예정이니 많은 관심 가져주세요!*금코토 — ‘금요일에 코딩하는 토끼’라는 엘리스 아카데미 과목의 줄임말. 매주 금요일 저녁때쯤 업로드되는 문제로, 특정 루트로 토끼가 움직이도록 코딩해야 하는 콘셉트와 귀여운 휴보 래빗이 특징입니다. >>문제 풀어보기(무료)

기업문화 엿볼 때, 더팀스

로그인

/