스토리 홈

인터뷰

피드

개발에 관심있다면 꼭 읽어야하는 글
2019. 01. 31. 조회수 1466

iOS에서 간결한 API 클라이언트 구현하기 (like Retrofit+GSON)

이 글은 안드로이드 개발에서 웹 서버 API 클라이언트를 간결하게 구현할 수 있도록 도와주는 강력한 오픈소스 라이브러리인 Retrofit과 GSON의 조합을 iOS 개발에서도 따라해보고 싶은 분들을 위해 작성되었습니다. Retrofit+GSON를 실제로 사용하는 좋은 예제는 다른 블로그 글에서도 찾아볼 수 있습니다.배경리디북스 서비스가 발전하면서 점점 복잡해지고, 자연히 앱의 기능도 다양해지기 시작했습니다. 기능이 다양해지면서 웹 서버와의 연동을 위한 API 종류도 늘어났고 앱 내에서 API 호출이 필요한 부분도 다양해지면서 관련된 중복 코드가 이곳 저곳에 산재하게 되었고 전체적인 코드 퀄리티 향상을 위해 이를 최소화하고 모듈화 할 필요성이 생겼습니다.안드로이드에서는 Pure Java로 작성되어 어노테이션을 통한 간결한 코드를 사용할 수 있게 해주는 Retrofit을 GSON과 연동하여 JSON 응답을 손쉽게 객체에 맵핑 하여 사용함으로써 이러한 문제를 성공적으로 해결할 수 있었습니다. 이후 iOS 개발을 진행하면서 비슷한 역할을 할 수 있는 도구가 있을까 찾아봤지만 마땅하지 않아 결국 사용 가능한 도구들을 이용해 비슷하게 따라해보기로 했습니다.목표Retrofit+GSON 조합을 최대한 따라해서 iOS 앱의 코드 퀄리티를 높이기 위한 작업을 진행하기는 하지만 모방하는 것 자체가 목적이 될 수는 없으므로, 구체적인 목적은 다음과 같은 것들로 상정해보았습니다.API 통신 부분을 모듈화하여 관련 중복 코드를 최소화하기NSArray, NSDictionary를 직접 사용하여 제어 했던 JSON 처리 부분을 추상화하여 모델 클래스를 정의, JSON 응답을 자동으로 객체에 맵핑 해서 사용할 수 있도록 하기필요한 것Retrofit과 GSON의 동작에 대한 이해AFNetworking비동기 HTTP 요청 처리에 용이하므로 기존에도 이미 API 호출을 위해서도 사용하고 있었습니다.이 글의 내용은 버전 2.6.3 기준입니다.Swift 언어와 그에 대한 이해사실 Objective-C를 사용해도 무방하지만, 작업 당시 Swift가 발표된 지 얼마 되지 않은 시점 이었기 때문에 시험 삼아 선택 되었으며 실제로 Swift가 Objective-C 대비 가진 장점들이 적지 않게 활용되었습니다.이 글의 내용은 버전 2.0 기준입니다.구조와 동작클래스 이름 앞에 붙어 있는 RB는 리디북스에서 사용하는 클래스 접두어 입니다.RBApiServiceAPI 통신을 담당하는 부분의 핵심은 중앙의 RBApiService 클래스를 포함한 상속 구조라고 할 수 있으며 상술하면 다음과 같습니다.AFNetworking에서, HTTP 요청 작업의 큐잉부터 시작과 종료까지 라이프 사이클 전반을 관리하는 역할을 하는 AFHTTPRequestOperationManager를 상속받는 RBApiService 클래스를 정의각 API들은 역할군에 따라 RBBookService(책 정보 관련 API), RBAccountService(사용자 계정/인증 관련 API) 등과 같은 RBApiService의 하위 클래스들의 메소드로 정의됨이 하위 클래스들이 AFHTTPRequestOperationManager의 역할을 그대로 이어받아 자신을 통해 이루어지는 API HTTP 요청 작업들을 관리이 설명에 따르면 웹 서버의 /api/foo/bar API를 요청하는 메소드는 RBFooService 클래스에 다음과 같이 정의될 것입니다.func bar(param1: String, param2: String, success: RBApiSuccessCallback, failure: RBApiFailureCallback) -> AFHTTPRequestOperation! { let paramters = ["param1": param1, "param2": param2] responseSerializer = RBJSONResponseSerializer(responseClass: RBFooBarResponse.class) return GET("/api/foo/bar", parameters: parameters, success: success, failure: failure) }RBApiSuccessCallback과 RBApiFailureCallback은 요청과 응답이 완료되고 각각 성공, 실패일 때 호출되는 람다 함수(Objective-C의 block에 대응되는 개념) 타입으로 다음과 같이 typealias를 통해 선언되어 있습니다. typealias RBApiSuccessCallback = ((operation: AFHTTPRequestOperation, responseObject: AnyObject) -> Void)? typealias RBApiFailureCallback = ((operation: AFHTTPRequestOperation?, error: NSError) -> Void)?GET 메소드는 AFHTTPRequestOperationManager의 메소드로 새로운 HTTP GET 요청 작업을 생성하고 큐에 넣은 뒤 그 인스턴스를 반환합니다. bar 메소드는 이렇게 반환된 인스턴스를 다시 그대로 반환하는데 API 호출을 의도한 측에서는 이 인스턴스를 통해 필요한 경우 요청 처리를 취소할 수 있습니다. API에 따라 GET 이외의 다른 방식의 요청이 필요하다면 POST, PUT, DELETE등의 메소드들 또한 사용할 수 있습니다.RBFooBarResponse 클래스는 이 API 호출의 JSON 응답을 맵핑하기 위한 모델 클래스입니다. 이 API 요청의 응답은 RBJSONResponseSerializer 클래스를 통해 사전에 정의된 규칙에 따라 적절히 RBFooBarResponse 인스턴스로 변환되고 이 모든 과정이 성공적으로 진행되면 RBApiSuccessCallback의 responseObject 인자로 전달됩니다.모델 클래스와 RBJSONResponseSerializer앞서 이야기했듯이 RBJSONResponseSerializer는 JSON 형태로 온 응답을 특정 모델 클래스의 인스턴스로 맵핑시키는 작업을 수행합니다(Retrofit+GSON 조합에서 GsonConverter의 역할에 대응한다고 볼 수 있습니다).iOS 개발에서 전통적으로 JSON을 다루는 방식은 Cocoa 프레임워크에서 기본적으로 제공하는 NSJSONSerialization 클래스를 이용하여 JSON Array->NSArray로, 그 외의 JSON Object는 NSDictionary로 변환하여 사용하는 방식입니다. 이러한 방식을 사용할 경우 별다른 가공이 필요 없다는 장점이 있는 대신 다음과 같은 문제들에 직면할 수 있습니다.데이터가 명시적으로 정의된 프로퍼티로 접근되지 않고 문자열 키 기반의 키-밸류 형태로만 접근되므로 데이터의 타입이 명시적이지 않아 타입 검사와 캐스팅이 난무하게 되어 가독성을 해침오타와 같은 개발자의 단순 실수로 인한 버그를 유발할 가능성도 커짐특히 오타로 인한 버그의 경우 명시적인 모델 클래스의 프로퍼티로 맵핑 해서 사용한다면 IDE가 에러를 검출해주거나 최소한 빌드 타임 에러가 발생할테니 미연에 방지할 수 있습니다. 이러한 문제는 사소한 실수로 인해 찾기 힘든 버그가 발생한다는 점과 코드 리뷰를 통해서도 발견하기가 힘들다는 점에서 지속적으로 개발자를 괴롭힐 수 있습니다.RBJSONResponseSerializer를 통한 인스턴스로의 변환은 이런 문제 의식에서 출발했고 Retrofit에 GSON을 연계하여 사용하기 위한 GsonConverter가 해결을 위한 힌트를 제공한 셈입니다.// AFJsonResponseSerializer는 NSJSONSerializer를 이용해 NSArray/NSDictionary로 변환하는 기본적인 작업을 해줌 class RBJSONResponseSerializer: AFJSONResponseSerializer { var responseClass: NSObject.Type! override init() { super.init() } required init(responseClass: NSObject.Type!) { self.responseClass = responseClass super.init() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { // 파서를 직접 구현하는 건 노력이 많이 필요하므로 우선 AFJSONResponseSerializer를 이용해 NSArray/NSDictionary로 변환 let responseObject: AnyObject! = super.responseObjectForResponse(response, data: data, error: error) if let dictionary = responseObject as? NSDictionary where responseClass != nil { // 변환 결과가 NSDictionary이면서 responseClass가 정의되어 있다면 변환 작업 시작 return responseClass.fromDictionary(dictionary, keyTranslator: PropertyKeyTranslator) } // NSArray라면 JSON이 top level array로 이루어졌다는 뜻이므로 변환 불가로 보고 그대로 반환 // 혹은 responseClass가 정의되어 있지 않아도 그대로 반환 return responseObject } }Key translatorfromDictionary 메소드 호출 시 함께 인자로 전달되는 keyTraslator는 JSON에서 사용되는 키로부터 모델 클래스의 프로퍼티 이름으로의 변환을 나타내는 람다 함수로 개발자가 원하는 규칙에 따라 정의하면 됩니다. 위의 코드에서 사용 중인 PropertyKeyTranslator는 리디북스 API에서 사용 중인 규칙 및 Swift의 네이밍 컨벤션에 따라 다음과 같이 언더스코어(_) 케이스로 된 이름을 카멜 케이스로 바꾸는 형태로 정의되었으며 이는 GSON의 FieldNamingPolicy 중 LOWERCASE_WITH_UNDERSCORES와 유사합니다.let PropertyKeyTranslator = { (keyName: String) -> String in let words = keyName.characters.split { $0 == "_" }.map { String($0) } var translation: String = words[0] for i in 1..NSObject.fromDictionary 메소드fromDictionary 메소드는 NSDictionary로 표현된 데이터를 실제 모델 클래스의 인스턴스로 변환하는 작업을 수행하며 NSObject의 extension(Objective-C의 category 개념과 유사합니다)으로 정의하여 원하는 모델 클래스가 어떤 것이든지 간에 공통적인 방법을 사용할 수 있게끔 했습니다.extension NSObject { class func fromDictionary(dictionary: NSDictionary) -> Self { // keyTranslator가 주어지지 않으면 디폴트 translator 사용 return fromDictionary(dictionary, keyTranslator: { $0 }) } class func fromDictionary(dictionary: NSDictionary, keyTranslator: (String) -> String) -> Self { let object = self.init() (object as NSObject).loadDictionary(dictionary, keyTranslator: keyTranslator) return object } func loadDictionary(dictionary: NSDictionary, keyTranslator: (String) -> String) { // 주어진 dictionary에 포함된 모든 키-밸류 쌍에 대해 작업 수행 for (key, value) in (dictionary as? [String: AnyObject]) ?? [:] { // keyTranslator를 이용해 키를 프로퍼티 이름으로 변환 let keyName = keyTranslator(key) // 프로퍼티 이름을 사용할 수 있는지 검사 if respondsToSelector(NSSelectorFromString(keyName)) { if let dictionary = value as? NSDictionary { // 밸류가 NSDictionary면 해당 프로퍼티의 타입에 대해 fromDictionary 메소드 호출 if let ecls = object_getElementTypeOfProperty(self, propertyName: keyName) as? NSObject.Type { setValue(ecls.fromDictionary(dictionary, keyTranslator: keyTranslator), forKey: keyName) } else { NSLog("NSObject.loadDictionary error: not found element type of property. (key: \(keyName), value: \(dictionary))") } continue } else if let array = value as? NSArray { var newArray = [NSObject]() // 밸류가 배열이면 각 요소별로 작업 수행 for object in array { if let dictionary = object as? NSDictionary { // 배열 요소가 NSDictionary면 프로퍼티의 배열 요소 타입에 대해 fromDictionary 메소드 호출한 뒤 배열에 추가 if let ecls = object_getElementTypeOfProperty(self, propertyName: keyName) as? NSObject.Type { newArray.append(ecls.fromDictionary(dictionary, keyTranslator: keyTranslator)) } else { NSLog("NSObject.loadDictionary error: not found element type of property. (key: \(keyName), value: \(dictionary))") } } else if let object = object as? NSObject { // NSDictionary가 아니면 그대로 배열에 추가 newArray.append(object) } else { NSLog("NSObject.loadDictionary error: can't cast element. (key: \(keyName), value: \(object))") } } setValue(newArray, forKey: keyName) continue } else if value is NSNull { continue } // NSDictionary, NSArray가 아니면서 null도 아니면 그대로 사용 setValue(value, forKey: keyName) } } } }주어진 dictionary에 존재하는 모든 키-밸류 쌍에 대해 밸류가 가진 타입과 이에 대응하는 프로퍼티의 타입에 따라 적절히 프로퍼티에 대응될 객체를 구한 다음 Cocoa 프레임워크에서 제공하는 KVC를 이용해 채워넣습니다.프로퍼티 타입 정보 가져오기모델 클래스가 반드시 Int, String, Float과 같은 기본적인 타입들로만 이루어져 있을 필요는 없고 다른 모델 클래스의 인스턴스나 배열을 포함하고 있어도 타입 정보를 런타임에 가져와 재귀적으로 데이터를 채워나가는 것이 가능합니다. 프로퍼티의 타입을 알아내는 과정은 다음과 같이 Swift에서 제공하는 Mirror 구조체를 통해 이루어지는데 이는 마치 (이름에서도 느낄 수 있듯이) Java의 리플렉션을 떠올리게 합니다.// 타입 이름에서 특정 접두어("Optional", "Array", "Dictionary" 등)를 찾아 제거 func encodeType_getUnwrappingType(encodeType: String, keyword: String) -> String { if encodeType.hasPrefix(keyword) { let removeRange = Range(start: encodeType.startIndex.advancedBy(keyword.length + 1), end: encodeType.endIndex.advancedBy(-1)) return encodeType.substringWithRange(removeRange) } else { return encodeType } } // object의 타입에서 propertyName의 이름을 갖는 프로퍼티의 타입 이름을 반환 func object_getEncodeType(object: AnyObject, propertyName name: String) -> String? { let mirror = Mirror(reflecting: object) let mirrorChildrenCollection = AnyRandomAccessCollection(mirror.children)! // object의 타입 구조 children 중에서 propertyName을 찾음 for (label, value) in mirrorChildrenCollection { if label == name { // Optional 타입인 경우 "Optional" 접두어를 제외 return encodeType_getUnwrappingType("\(value.dynamicType)", keyword: "Optional") } } return nil } // object의 타입에서 propertyName의 이름을 갖는 프로퍼티의 타입 인스턴스를 반환 func object_getElementTypeOfProperty(object: AnyObject, propertyName name: String) -> AnyClass? { // 타입의 이름을 가져옴 if var encodeType = object_getEncodeType(object, propertyName: name) { let array = "Array" // "Array" 접두어로 시작할 경우 (배열인 경우) if encodeType.hasPrefix(array) { // "Array" 에서 "Array" 제외하고 T를 반환 return NSClassFromString(encodeType_getUnwrappingType(encodeType, keyword: array)) } let dictionary = "Dictionary" if encodeType.hasPrefix(dictionary) { // "Dictionary" 에서 "Dictionary", "K"를 제외하고 V를 반환 encodeType = encodeType_getUnwrappingType(encodeType, keyword: dictionary) encodeType = encodeType.substringWithRange(Range(start: encodeType.rangeOfString(", ")!.endIndex.advancedBy(1), end: encodeType.endIndex)) return NSClassFromString(encodeType) } // 커스텀 클래스 접두어를 가지고 있다면 그 타입 그대로 반환 if encodeType.hasPrefix(RidibooksClassPrefix) { return NSClassFromString(encodeType) } } return nil }RidibooksClassPrefix는 커스텀 클래스들의 접두어를 나타내는 상수이며(리디북스의 경우 앞서 이야기했듯 “RB”), 이 접두어가 붙어있는 경우에만 모델 클래스로 간주해 해당 타입 인스턴스가 반환됩니다.예시앞서 정의한 PropertyKeyTranslator를 사용했을 때, 위에 예시로 사용했던 /foo/bar API 요청의 JSON 응답과 모델 클래스 및 생성되는 인스턴스 형태의 예를 들면 다음과 같을 것입니다.(Int, Bool, Float과 같은 기존 NSNumber 기반의 타입을 가지는 프로퍼티들은 아직 정확한 원인은 알 수 없으나 nil 이외의 값으로 초기화 해주지 않으면 프로퍼티가 존재하는지 확인하기 위해 사용하는 respondsToSelector 메소드가 false를 뱉게 되어 사용할 수 없으므로 클래스 선언시 적절한 초기값을 주어야 합니다.{ "success": true, "int_value": 1, "string_value": "Hello!", "float_value": null, "baz_qux": { "array_value": [1, 2, 3] } }class RBFooBarResponse : NSObject { var success = false // true var intValue = 0 // 1 var stringValue: String! // "Hello!" var floatValue: Float! = 0.0 // nil var bazQux: RBBazQux! } class RBBazQux : NSObject { var arrayValue: [Int]! // [1, 2, 3] }맺음말이런 작업들을 통해 당초 목표했던 두 가지, API 통신 관련 중복 코드를 최소화 하면서 JSON 응답을 가독성이 더 좋고 실수할 확률이 적은 모델 클래스의 인스턴스로 자동 변환 하도록 하는 것 모두 달성하는 데에 성공했습니다.다만 모든 것이 뜻대로 될 수는 없었는데 Retrofit+GSON과 비교했을 때 플랫폼 혹은 언어의 특성에 기인하는 다음과 같은 한계들 또한 존재했습니다.Retrofit에서는 Java 어노테이션을 이용해 API 메소드의 인터페이스만 정의하면 됐지만 iOS 구현에서는 GET, POST 등의 실제 요청 생성 메소드를 호출 하는 것 까지는 직접 구현해줘야 함키->프로퍼티 이름 변환 규칙에 예외 사항이 필요할 때 GSON에서는 @SerializedName 어노테이션을 통해 손쉽게 지정할 수 있지만 iOS 구현에서는 예외 허용을 위한 깔끔한 방법을 찾기가 힘듬 (다만, 예외가 필요한 경우가 특별히 많지는 않기 때문에 큰 문제는 되지 않음)향후에는 HTTP 통신을 위해 사용 중인 AFNetworking(Objective-C로 작성됨)을 온전히 Swift로만 작성된 Alamofire로 교체하는 것을 검토 중이며 기존에 비해 좀 더 간결한 코드를 사용할 수 있을 것으로 기대하고 있습니다. 다만 Alamofire의 최신 버전이 iOS 8 이상을 지원하고 있어 iOS 7을 아직 지원 중인 리디북스인 관계로 언제 적용할 수 있을지는 아직 미지수입니다.#리디북스 #개발 #개발자 #iOS #iOS개발 #API #API클라이언트 #GSON #Retrofit #중복코드 #최소화 #API통신 #웹서버 
2019. 04. 11. 조회수 312

[인터뷰] Clara의 인턴 직무 인터뷰 제3화 _iOS developer 민트를 만나다

안녕하세요:)인턴들의 하루하루를 전해드리는 클라라입니다오늘은 저번 시간에 말씀드렸던 Tech unit의  미녀 인턴과의 인터뷰를 진행했습니다!그녀의 이름은 상쾌한 Mint!본명에 '박하'가 들어가서 민트라는 이름을 지었다고 하네요~센스 만점이죠?이름처럼 상큼한 민트와의 인터뷰바로 만나보시죠!고고고☞Q. 안녕하세요 민트, 간단한 자기소개와 요즘 어떤 일을 하시는지 소개해주세요~M.네! 안녕하세요~ 저는 iOS 개발을 하고 있는 개발자입니다. 많은 분이 개발자가 코딩을 하고 이런 것들은 어렴풋이 알고 계실 텐데, 지금 저는 iOS 앱에서 개선할 부분을 조사하고 더 잘 구현하고자 열심히 개발하고 있습니다. 아직은 주로 UX/UI 의 개선에 집중하고 있고, 하는 일보다 배우는 일이 더 많은 것 같네요!M.네! 안녕하세요~ 저는 iOS 개발을 하고 있는 개발자입니다. 많은 분이 개발자가 코딩을 하고 이런 것들은 어렴풋이 알고 계실 텐데, 지금 저는 iOS 앱에서 개선할 부분을 조사하고 더 잘 구현하고자 열심히 개발하고 있습니다. 아직은 주로 UX/UI 의 개선에 집중하고 있고, 하는 일보다 배우는 일이 더 많은 것 같네요!Q. 개발자는 그 안에서도 하는 일이 다양하다고 들었어요. 요즘 민트의 주 업무에 대해 더 자세하게 설명해주실 수 있을까요?M.그럼요~지금 저는 아이폰의 OS인 iOS에 특화된 방식으로 개발하는 네이티브 방식을 활용하고 있어요. 네이티브 방식이란 안드로이드나 iOS와 같은 특정 OS에 최적화된 방식으로 앱을 개발하고 있다는 뜻입니다. 그렇지 않은 개발 방식도 있거든요! 모바일 웹페이지를 앱처럼 꾸며서 보여주는 등 여러 방식이 있습니다.M.그럼요~지금 저는 아이폰의 OS인 iOS에 특화된 방식으로 개발하는 네이티브 방식을 활용하고 있어요. 네이티브 방식이란 안드로이드나 iOS와 같은 특정 OS에 최적화된 방식으로 앱을 개발하고 있다는 뜻입니다. 그렇지 않은 개발 방식도 있거든요! 모바일 웹페이지를 앱처럼 꾸며서 보여주는 등 여러 방식이 있습니다.iOS개발은 안드로이드 앱 개발과 비교했을 때 제약 조건도 많고, 생소한 스타일의 개발 언어를 써야 하는 게 어려워요. 하지만 동시에 iOS 특유의 사용감과 안정성이 매력이에요. 그리고 아까 UX/UI라는 용어를 사용했는데 이는 User Experience와 User interface의 약자, 즉 사용자 경험을 의미합니다. 저희는 사용자 경험을 더욱 편리하게 하는 쪽으로 앱을 유지 보수하는 일을 하고 있는 거예요. 미미박스는 고객을 소중히 여기기 때문에 이런 UX/UI에 있어서도 많은 신경 쓰고 있습니다.Q. 그럼 개발자로서 미미박스는 어떤 장점을 가지고 있나요? 저희 회사 자랑 좀 해주세요!!!M. Q. 그럼 개발자로서 미미박스는 어떤 장점을 가지고 있나요? 저희 회사 자랑 좀 해주세요!!!M. 음, 저는 미미박스가 개발자의 의견을 듣고 반영하고자 하는 회사임을 가장 말씀드리고 싶어요! 미미박스 개발팀에서는 디자인팀+앱 개발팀+PM 팀, 세 팀이 모여서 정기적으로 회의를 하고 있습니다. 이 회의를 스크럼이라고 하는데, 프로젝트와 관련된 모든 사람들이 모여서 계획하고 피드백하는 것이죠.이걸 하면 좋은 이유는 개발을 담당하는 사람이 직접 기획에도 참여할 수 있다는 거예요. 보통 한국에서 개발 직무는 보통 상명하달식으로 이루어진다고 해요. 위에서 개발이라는 직무를 이해하지 않고 일방적으로 일정을 정해서 던져주는 거죠. 그런데 미미박스는 그렇지 않고 자신의 생각을 내고 반영할 수 있어서 좋아요.   Q. 오오오~ 그렇군요! 민트와 저는 자리가 멀잖아요. 업무적인 것과 별개로, Tech 유닛의 분위기는 어떤가요??? M.저희 유닛 분위기 완전 좋아요! 그리고 저는 사수 분들이 똑똑하셔서 배울 점이 많다는 생각으로 회사를 다니고 있어요. 서로 돕고 정보를 공유하는 분위기여서 무려 시니어 분들이 제게 본인의 코드를 다 오픈해주세요. 근데 그 코드가 다 샘플 코드의 수준이고요!(샘플 코드란 일종의 '교과서'같은 존재로, 코딩의 수준이 아주 높다는 뜻입니다.)iOS 직무는 신입의 진입장벽이 높거든요. 사전 지식 없이는 독학으로 따라잡을 수 없는 부분이 많기 때문에 코드와 그에 대한 설명을 들을 수 있다는 건 엄청난 거죠. 마치 최고의 영업사원이 자신의 영업 비밀을 공개해주는 그런 경우라고 할까요? 애플 워치의 코드까지 알려주는 회사, 흔치 않습니다! (엄지 척)  민트에게 몰려든 고양이들~Q. 와우! 애플 워치도 코딩을 하는 거군요. 제겐 너무나 신세계인데요...!  이제 마지막 질문입니다. 여성 개발자로서 강점은 무엇일까요?M.저는 사실 특정 산업 군이나 성별에 구애받지 않는 작업을 한다고 생각해요. 그럼에도 화장품을 온라인으로 사 본 개발자과 그렇지 않은 개발자는 차이가 있다고 생각해요. 여성이 주 고객층인 뷰티 쇼핑몰에 대한 경험이 쌓이면 새롭고 좋은 UX에 대한 아이디어도 더 잘 나오지 않을까 싶네요.  민트와의 인터뷰 어떠셨나요?저 클라라처럼 컴알못이거나개발자의 하루가 궁금하셨던 분들은 이번 인터뷰가 큰 도움이 되셨으면 좋겠습니다.민트를 마지막으로 인턴의 생활을 엿볼 수 있는 클라라의 인터뷰가 마무리 되는데요 :)미미박서의 일과 삶에 대해서 조금이나마 더 알아가셨다면,그래서 '미미박스에서 일해보고 싶다'는 마음이 스멀스멀 생기셨다면!클라라는 그것만으로도 보람찰 것 같습니다.그럼 또 미미박스의 소식으로 찾아올게요~
2018. 07. 10. 조회수 665

Humans of TODAIT : 전설의 전성윤을 만나다

‘Humans of TODAIT’의 두번째 주인공, 투데잇 안드로이드 개발자 전성윤을 만나봤습니다. 투데잇의 전설(★)이 된 그의 이야기를 함께 들어볼까요?(2016.08)✓ 전설윤, 그는 누구인가Q. 자기소개 부탁드려요.안녕하세요! 투데잇에서 안드로이드 개발자를 맡았던 전성윤입니다. 퇴사자 인터뷰를 하려니까 투데잇을 떠나는 게 정말 실감나네요. 작년 2015년 10월 경, ‘SW 마에스트로’ 과정에서 만난 분께서 대표님을 소개해주셨고, 좋은 인연으로 연결되어 투데잇과 함께 일하게 되었어요. 처음엔 안드로이드 개발자로 들어왔다가 지금은 안드로이드 개발 팀장직까지 맡고 있습니다. (당시 2016.08)Q. 투데잇을 떠나는 이유는 무엇인가요?사실 처음 입사할 때도 1년 정도 생각하고 있었어요. 일하다보니 투데잇이 너무 좋아져서 나가고 싶지 않았지만, 아무래도 군문제와 학교 복학 문제가 마음에 걸리더라고요. 그래서 여러 고민 끝에 할 수 없이 나가게 된 케이스예요. 결국 아쉽지만 이번 8월을 마지막으로 투데잇과 헤어지게 되었죠.Q. 별명이 전설윤이라고 하던데, 왜 전설이라고 불리는 건가요?저도 이번 인터뷰를 통해서 생각해보게 된 건데요. ‘전설’이라는 칭호가 지금은 한물 간 사람에게 붙는 것이 아닌가 싶어요. 다른 팀원분들이 들어오시게 되면서 자연스럽게 리즈시절이 지나게 되었죠. (웃음)다른 이유가 아니라, 혼자만 잘하는 것이 아닌 다 함께 잘하기 위해 애썼기 때문인 것 같아요. 업무에 있어 버려야 할 습관 같은 작업 처리 팁에 대해 새 팀원분들께 적극적으로 공유했고 적당한 선에서 체크해드렸어요. 7–8개월의 짬 덕분인지 기획 단계에서부터 개발 단계 때 준비할 것들이나 구현 방법들이 곧장 떠오르는 경지에 이르더라고요. 이런 점들이 다른 팀원분들이 좋은 퍼포먼스를 낼 수 있게 이끌어주는 역할을 했던 것 같아요. 또 그 분들도 잘 하는 방법에 대해 치열하게 고민했고 실제로도 다들 너무 잘해주셔서 결론적으로 이제는 혼자가 아닌 다 함께 잘하게 된 거죠.✓ 보안 전문가에서 투데잇 안드로이드 팀장으로 레벨 업!Q. 투데잇 안드로이드 팀장까지, 성윤님의 입사 초반부터 지금까지 업무 성장과정이 듣고 싶어요!입사 초기에는 솔직히 맘고생 많이 했어요. 내가 지금 당장 회사에 기여할 수 있는 점이 무엇일까 의욕적으로 많이 고민했지만, 그에 비해 개발 능력은 많이 떨어졌죠. 초기에는 무엇보다 제 개발 능력 수준에 대해 정확히 몰랐고 커뮤니케이션하는 방법이 미숙했던 것 같아요. 그 때마다 대표님, 개발팀장님께서 진심어린 피드백과 따끔한 조언을 계속 해주셔서 해결할 수 있었죠. 두 분의 노력덕분에 중반부터는 업무 처리 능력도 커뮤니케이션 능력도 많이 향상됐어요,이후에 본격적으로 개발자분들이 더 들어오고 협업이 시작되면서 1인 개발자에서 2인 개발자 체제로 바뀌게 되었고, 자연스럽게 그에 따른 새로운 이슈들이 생길 수 밖에 없었어요. 다른 개발자분들에게 업무 분담하고 리드하는 부분에서 어려움을 느꼈지만, 함께 잘 해결해나갈 수 있었죠 지금은 기획적인 틀을 잡고 누군가에게 일을 맡기고 내 일을 해내고 하는 여러 부분에서 팀장의 위치에서 역할을 잘 해내는 것 같아요. (웃음)Q. 그렇다면 특별히 힘들거나 어려웠던 일정은 무엇이 있었나요?되게 좋은 질문이에요! 전 개발자다보니까 이런 질문이 너무 좋네요. (웃음)구매페이지와 그룹 기능 작업이 좀 힘들었어요. 그 중에서도 프로버전 구매페이지 작업이 가장 힘들었는데요. 작업 자체가 힘들다기보다는 구매페이지에서 에러가 나면 유저의 신뢰도에 큰 영향을 주기 때문에 한치의 실수도 용납되지 않는 부분이라는 점이 부담이 됐죠. 처음엔 실수도 많았는데, 그 이후에 치밀하게 설계한 덕분에 두 번째부턴 버그가 터져도 바로 대처할 수 있었어요. 개인적으로 개발적으로 많이 성장한 느낌을 받았죠.그룹 기능 개발은 클라이언트 쪽에서 해결해야 하는 부분이 많아서 힘들었어요. 하지만 릴리즈 이후 유저분들이 격렬하게 환호해주신 덕분에 뿌듯하게 잘 마무리할 수 있었죠. 두 작업 다 힘들었지만, 굉장히 보람됐던 작업으로 기억에 남아요.Q. 유저들의 피드백을 보면서 얻은 게 많다고 하던데, 좀 더 말해주세요!“실제 유저와 소통하고 친근한 서비스를 제공하려고 노력하다보니 유저들의 피드백이 얼마나 소중한 건지 알게되더라구요.”투데잇을 다니기 전까지만 해도 저는 별 5개 짜리 리뷰가 당연하게 받아야할 칭찬이라고 생각했어요. 그래서 사실 크게 반응하지 않았었는데, 실제 유저와 소통하고 친근한 서비스를 제공하려고 노력하다보니 유저들의 피드백이 얼마나 소중한 건지 알게되더라구요. 당연히 좋은 피드백은 정말 힘이 많이 되었고, 좋지 않은 피드백도 굉장히 감사했어요. 사실 유저 입장에서 그냥 지워버리면 그만인건데, 우리 앱의 장점을 알아봐주시고, 개선할 점을 말해주시고 또 기다려주시는 거잖아요. 그런 유저분들 보면서 빨리 개선해드리고 싶단 생각이 들죠. 그 어떤 피드백보다 더 큰 동기부여가 되더라고요.✓ 투데잇 TALK“투데잇팀은 서로 부담없이 정말 효율적으로 커뮤니케이션을 해서 어떻게 하면 서로의 업무 컨텍스트를 빠르게 마무리할지 고민해요.”Q. 투데잇의 힘은 이거다!음 투데잇의 힘은 서로를 존중하고 커뮤니케이션을 중시하는 데에서 나오는 것 같아요. 결국 모든 문제의 결착점은 커뮤니케이션이거든요. 사소한 거라도 시기에 맞춰서 커뮤니케이션이 되어야 하는데, 개발팀과 비개발팀간의 커뮤니케이션이 사실 어렵잖아요. 하지만 투데잇팀은 서로 부담없이 정말 효율적으로 커뮤니케이션을 해서 어떻게 하면 서로의 업무 컨텍스트를 빠르게 마무리할지 고민해요. 누군가 못한 부분이 있더라도 절대 무시하지 않고, 서로의 업무를 존중하고 정식적으로 피드백을 공유하면서 문제를 잘 해결하기 위한 고민을 하죠.Q. 투데잇에서 제일 기억에 남는 순간이 있다면?투데잇에서는 많이 힘들었을 때부터 행복했을 때 그리고 소소한 일상들까지 전부 다 기억에 남아요. 워크샵이라고 가평에 가서 일한 적이 있었는데, 정말 놀지도 못하고 거의 일만했거든요. 근데 밖에 나와서 그런지 그 자체가 너무 재밌었어요. 일하면서도 되게 색다르고 즐거웠죠. (웃음) 제주도로 워크샵 갔을 때도 너무 재미있었고, 정말 매일 매순간이 기억에 남아요. 팀 분위기가 너무 좋아서 그런지 특별한 일들 뿐만 아니라 개발팀 회의할 때나 회사 메신저에서 웃고 떠들고 했던 것들처럼 아주 소소한 일상들까지 모두 에피소드였던 것 같아요. 깜짝 생일파티도 그렇고 되게 예정없이 나온 에피소드가 많았거 든요. 그런게 진짜 참된 에피소드 아닐까요?✓ 마지막으로…Q. 애정이 컸던 만큼 투데잇을 떠나기 많이 아쉬울 것 같아요.네 많이 아쉽죠. 전 투데잇에서 10개월 동안 정말 하루종일 개발만 했어요. 일 하는 게 너무 좋아서 거의 자취하다시피 야근도 매일 했었거든요. 좋아하는 사람과 좋은 분위기에서 재밌게 일하기도 했고, 또 어떤 목표를 이루기 위해 정말 치열하게 고민하고 일할 수 있었는데 이 부분들을 더이상 느끼지 못한다는 점이 많이 아쉬워요.음 아쉬운 사건을 말하라면, 맨 처음으로 앱 안에 코틀린을 적용해볼 때 너무 시간을 오래 끌었던 일이 있었어요. 잘 적용시킬 방법에 대해 혼자 고민하고 정리해보다가 늦어졌었는데, 다른 분들 일정에 피해를 준데다가 실수도 한두개가 아니었거든요. 그 때 제가 계획대로 잘 처리했으면 마케팅적으로나 여러 시도들을 해볼 수 있지 않았을까?하는 아쉬움이 들죠. 결국 개발자가 세운 일정에서 해내는 여부에 따라 회사에 큰 영향이 가고, 다른 팀에도 막대한 영향이 갈 수 있다는 걸 뼈저리게 깨달았어요. 그래도 이 사건 덕분에 업무적으로도 많이 성장할 수 있었던 것 같아요. 몸소 당해봤으니 그럴 수 밖에 없죠. (웃음)Q. 투데잇을 떠나며 마지막으로 하고 싶은 말이 있다면?“충분히 치열하게 일했고 다함께 즐겁게 소통하면서 일했기 때문에 언젠가 또 다시 만나지 않을까 싶어요.”개인적으로 아쉬운 점은 있지만, 충분히 치열하게 일했고 다함께 즐겁게 소통하면서 일했기 때문에 언젠가 또 다시 만나지 않을까 싶어요. 모든 투데잇 사람들과 연을 이어가고 싶기도 하고 특히 대표님께 보답하고 싶다는 마음이 크거든요. 처음 들어갔을 때, 아무 준비 안 돼있는 상태였던 절 믿어주시고 함께 일할 수 있는 기회를 주셨어요. 앞서 말한 것처럼 일하면서 초반에 실수도 많이 했는데 꾸준히 믿고 지금의 포지션까지 저를 밀어주셨다는 점이 너무 감사하거든요. 대표님께 보답하자! 투데잇에 보답하자!가 마지막으로 하고 싶은 말이에요. 저 나가더라도 아는 척해주셔야 해요..!Q. 투데잇을 꿈꾸는 개발자에게 한마디!음 투데잇에 들어오는 건 어쩌면 쉬울 수도 있어요. 저 같은 경우 30분 정도 면접을 보고 당일에 바로 함께하기로 했거든요. 하지만 중요한 건 본인이 ‘함께 성장하고 싶은 사람’인가?에 대해 생각해보셔야 해요. 또 투데잇과 방향성이 맞는지 스스로 생각해보고, 성장하기 위해선 어떤 태세를 취해야 하는지 고민해보아야 하죠. 만약 이런 성향이 맞지 않는 사람이라면 아마 들어오시더라도 투데잇과 잘 맞지 않아서 힘들 수도 있거든요. 투데잇에 들어오고 싶은 분들은 이런 부분들에 대해 한 번 깊게 고민해보셨으면 좋겠어요.#투데잇 #팀원소개 #팀원인터뷰 #팀원자랑 #기업문화 #조직문화 #개발자 #개발팀
2017. 08. 07. 조회수 795

CTE for postgresql and sqlalchemy

저희 서비스는 가게마다 웹에서 접속할 수 있는 어드민을 제공하는데, 프렌차이즈가 아닌 하나의 독립적인 가게들일 경우 정보를 가져와 나타내는 데는 굳이 CTE 를 쓸 필요가 없지만 프렌차이즈일 경우 본사와 지점들로 나누어져 있어서 본사와 지점들 정보를 다 가져오기 위해서 CTE 를 사용하게 되었습니다.그럼 postgresql 의 CTEReadme 에 나와 있는 예제와 sqlalchemy core 로 변환하는 것까지 살펴보겠습니다.CTE란?Common table expression 의 약자로 ‘공통 테이블 식’입니다.CTE 특징WITH절 같은 SELECT 문에서 효과적으로 테이블 식을 정의 할 수 있습니다.CTE는 VIEW의 사용방법과 비슷하지만, VIEW보다 편리합니다.VIEW와 달리 사전에 CTE를 정의할 필요가 없습니다.개체로 저장되지 않고, 쿼리 지속시간에만 존재합니다.CTE는 재귀 쿼리를 사용할 수 있습니다.재귀 CTE는 여러행을 반환 가능합니다.동일 문에서 결과 테이블을 여러번 참조 가능합니다.재귀 CTE 예제아래 예제는 ‘A’부서 하위에 있는 부서만 추출하는 예제입니다.일단 재귀 CTE를 이용한 쿼리를 사용하려면 ‘WITH RECURSIVE’ 키워드를 추가해야 합니다.Table ‘department’ 인접 리스트로 조직 구조를 나타냅니다.CREATE TABLE department ( id INTEGER PRIMARY KEY, -- department ID parent_department INTEGER REFERENCES department, -- upper department ID name TEXT -- department name ); INSERT INTO department (id, parent_department, "name") VALUES (0, NULL, 'ROOT'), (1, 0, 'A'), (2, 1, 'B'), (3, 2, 'C'), (4, 2, 'D'), (5, 0, 'E'), (6, 4, 'F'), (7, 5, 'G');부서 구조:ROOT-+->A-+->B-+->C | | | +->D-+->F +->E-+->G A의 하위 부서를 추출, 다음과 같은 재귀 쿼리를 사용할 수 있습니다.WITH RECURSIVE subdepartment AS ( -- non-recursive term SELECT * FROM department WHERE name = 'A' UNION ALL -- recursive term SELECT d.* FROM department AS d JOIN subdepartment AS sd ON (d.parent_department = sd.id) ) SELECT * FROM subdepartment ORDER BY name;위의 쿼리는 다음과 같이 설명할 수 있습니다.중간 테이블(Intermediate table), 작업 테이블(work table), 결과 테이블(result table)이 있습니다.초기화비재귀 구간을 실행 (SELECT * FROM department WHERE name = ‘A’)ResultTable = WorkTable = (‘A’) 결과 테이블과 작업 테이블에 결과를 배치합니다.IntermediateTable = () 중간 테이블을 비웁니다.재귀 쿼리 실행(SELECT d.* FROM WT AS d JOIN subdepartment AS sd ON d.parent_department = sd.id) 하위 부서와 작업 테이블을 바꾸고, 재귀 구간을 실행합니다.중간 테이블에 쿼리 결과를 할당합니다.결과 테이블 및 작업 테이블에 중간테이블 추가합니다.중간 테이블을 비웁니다.재귀가 끝났는지 확인2번 과정의 중간테이블이 비어 있으면 재귀의 실행이 종료되고, 결과 테이블은 반환됩니다.중간테이블이 비어 있지 않으면 다시 2번의 과정으로 돌아갑니다.“subdepartment”는 재귀 표현을 포함하고 있는 CTE입니다. 먼저 비재귀항이 평가되고, 다음 재귀항이 평가됩니다. 재귀항은 평가하고 처리하는 데이터가 없을 때까지 결과가 반복적으로 이전 결과에 추가됩니다. 끝으로 마지막 SELECT가 실행되고 데이터는 결과 집합에서 추출됩니다.CTE의 한계점SEARCH 및 CYCLE 절은 구현되지 않습니다.상호 재귀는 허용되지 않습니다.UNION ALL의 마지막 SELECT만 재귀 이름을 포함할 수 있습니다.재귀와 재귀스캔(RecursiveScan) 계획의 비용은 항상 0입니다sqlalchemy 로 변환sqlalchemy 에서 필요한 모듈들을 불러옵니다.from sqlalchemy import Table, Column, Text, Integer, MetaData, select metadata = MetaData() department 테이블을 정의합니다.department = Table('department', metadata, Column('id',Integer), Column('parent_department',Integer), Column('name',Text)) WITH 절부터 시작되는 CTE 부분의 비재귀항을 subdepartment로 만듭니다. 재귀 사용을 위해 .cte( recursive=True) 부분을 붙여줍니다.subdepartment = select([ department.c.id, department.c.parent_department, department.c.name]).where(department.c.name == 'A') \ .cte(recursive=True) department 와 subdepartment 에 각각 alias를 붙여줍니다.subd_alias = subdepartment.alias() department_alias = department.alias() CTE 부분의 재귀항과 비재귀 항을 union all 해주는 subdepartment를 만듭니다. (이 부분이 postgresql 예제 쿼리에서 봤던 WITH RECURSIVE subdepartment 전체를 나타내는 부분이라 할 수 있습니다.)subdepartment = subdepartment.union_all( select([ department_alias.c.id, department_alias.c.parent_department, department_alias.c.name]) \ .where(department_alias.c.parent_department == subd_alias.c.id)) 마지막으로 결과 쿼리를 출력하기 위한 statement를 만듭니다.statement = select([ subdepartment.c.id, subdepartment.c.parent_department, subdepartment.c.name]).order_by(subdepartment.c.name) 원문: CTEReadme참조: 공통 테이블 식 사용 ,공통 테이블 식을 사용하는 재귀 쿼리#스포카 #개발 #개발자 #서버개발 #개발팀 #꿀팁 #인사이트 #조언

기업문화 엿볼 때, 더팀스

로그인

/