본문 바로가기

난 iOS개발자/iOS

지극히 주관적인 리팩토링 (이벤트로그는 어디에 넣어야하는가?)

728x90

회사 프로젝트를 진행하다보면 다양한 코드를 만나게 된다.
작성자의 의도가 분명한 코드도 있는 반면 어느정도 편리함과 타협하여 쉽게 작성한 코드도 보인다.
물론 지극히 내 주관적인 기준으로 판단했을때의 코드이다. 
작성자의 분명한 의도를 내가 파악하지 못했을 수도 있다. 
 
요즘 맞닥들인 문제는 내 스타일과 조직내 코딩컨벤션의 갭이 꽤 크다는 것이다.
내가 정답이라는건 아니지만 내가 보고 고쳐야겠다고 생각이 든 코드도 그리 좋아보이지는 않았다.
 
구성원이 여러명인 조직에 들어온 것은 또 처음이라 내가 적응을 못하고 있는것은 아닐런지, 
스스로 생각해보기도 했다. 처음엔 꽤 혼란스러웠지만 이제는 적응이 되었고 어느정도 분위기도 파악되었다.
 
아래 부터는 내가 생각한 재구성이 필요한 코드가 어떤것이었는지 설명하면서 어떤 방향으로 바꾸고자 생각했었는지 써내려 가보겠다.
 


어느정도 규모가 있는 이 프로젝트에는 유입된 사용자들의 액션을 실시간으로 수집하고 통계를 볼 수 있는 로직이 들어가있다.
사용자가 어떤 버튼을 눌렀는지, 어떤 화면에 진입하고 어떤 동작을 하고 있는지 매우 상세하게 정보를 수집하고 있는데 
내 눈에 들어온 것은 수집 로그를 서버로 보내는 코드였다. 
 
여기서 서버란, 각 통계 솔루션 서버를 뜻한다. 수집/통계 솔루션을 iOS프로젝트에 import하고 제공하는 API를 사용하는데,
예를 들면 아래와 같은 코드가 최종적인 전송 코드이다. 그리고 실제로 이렇게 쓰고 있었다.

@IBAction func loginButtonTapped() {
    Analytics.logEvent("BTN02_LOGIN", parameters: ...)
    login()
}

func login() {
    Service.userLogin {
        Analytics.logEvent("MM01_COMPLETE_LOGIN",parameter...)
    }
}

이러한 코드가 ViewController 클래스 내 산발적으로 삽입되어 있다. 
logEvent는 사용자액션(버튼 터치, 화면 이동)을 수집하기도 하지만 로그인 성공 시 임의의 정보를 수집하기도 한다.
오로지 사용자 액션으로만 발생하는 이벤트가 아니라는 뜻이었다.
 
프로젝트는 MVVM패턴이 적용된 상태였으나, 해당 이벤트 코드들은 ViewController에 작성되어있었고, 
ViewModel에서 관리해야될것 같은 정보도 로그이벤트에 담아서 전송하고 있는 부분도 있었다.
 
잠시 고민이 들었다. 로그를 전송하는것이 ViewController에서 이뤄져야 할 작업인가? 
아니라는 생각이 우선 들었지만, 점진적으로 천천히 리팩토링을 진행해보는걸로 진로를 잡고 작업을 시작했다.
 
 
첫번째 리팩토링 요소는 하드코딩으로 작성된 이벤트로그의 Name이었다.
위 예시코드처럼 "BTN02_LOGIN" 같은 하드코딩 스타일의 작성이 여기저기 분포되어 있다보니 프로젝트에 투입된지 얼마 안 된
내가 봤을때 무척이나 불편했다.
해당 화면에서 수집하는 로그가 어떤것들이 있는지 알기 위해서는 각 코드라인의 상수들을 직접 눈으로 확인해야하는 불편함이 있었는데,
이를 열거형으로 관리하면 괜찮을것 같다는 생각이 들어서 정리를 했다.
 

enum AnalyticsEventName {
    case BTN01_KAKAO_LOGIN
    case BTN02_FACEBOOK_LOGIN
    case MMS01_COMPLETE_LOGIN
}

 
두번째 리팩토링 요소는 Analytics의 타입메서드로 다이렉트 호출하는 부분을 한 메서드로 묶는것이었다.
원래 코드는  Analytics.logEvent 호출을 필요한 시점에 편하게 호출을 하고 있었으나,
주관적인 코딩 스타일은 class에서 "logEvent를 호출" 이라는 기능(메서드)를 만들어서 클래스 외부? 로 나가는 호출을 한 군데에서 파악하는것을 좋아한다.
 

func sendAnalyticsEvent(keyName: AnalyticsEventName, parameters: [String: Any]? = nil) {
    Analytics.logEvent(keyName.rawValue, parameters: parameters)
}

이렇게 작성된 메서드를 원래 호출코드에서 

sendAnalyticesEvent(.BTN01_KAKAO_LOGIN)

과 같이 사용한다면 코드의 흐름을 더욱 추적하기 쉽다고 판단하여 변경했다.
 
이렇게 하니 한 클래스 내에서 로그이벤트를 전송하는 구조는 잡힌것 같다. 
이제 다음 문제는 역할과 책임을 고려해 볼 차례였다.
 
지금까지 변경된 코드들은 아직도 ViewController에 존재한다.
MVVM의 ViewController가 이벤트로그를 전송하는 역할을 한다? 아니라고 생각된다.
위에서 언급했든 단순히 Action에 대한 로그를 전송하는게 아닌 임의의 비즈니스 로직의 수행결과 등도 로그전송을 하고 있기 때문이다.
 
로그인 결과에 대한 정보를 로그이벤트로 전송하고도 있었는데,  로그인 결과를 왜 ViewController가 받고 있었는지 좀 의문이었다... 이러한 UI와 관련이 없는 비즈니스 로직이 ViewModel로 우선적으로 옮겨져야 겠지만 이 문제는 여기서 다루지 않겠다. 이런 문제점들을 보고 있었을때 이미 난 머릿속으로 대공사가 될 것이라고 생각하고 있었다. 로그인처리뿐 아니라 다른 문제들도 있었기 때문에...
 
아무튼, 이미 손을 대고 당장 고칠 수 있는 부분부터 고쳐보기 시작했다.
 
세번째 리팩토링은 Analytics 코드를 ViewModel로 이동하는것이었다.
ViewController는 Analytics이벤트에 전송해야할 정보들을 알 필요가 없었다. 로그인 타입이라던지, 또 다른 정보라던지...
모두 ViewController에 종속되지 않은 정보라 할 수 있었다. 그리하여 난 위 코드처럼 변경된 코드들을 ViewModel로 옮기고 
ViewController에서 발생하는 이벤트(액션) 들을 ViewModel이 받고, ViewModel에서 이벤트 로그를 전송할 수 있게 했다.
 
그러나 내 계획은 여기서 그만두는게 아니었다.
(여기서 멈췄어야했나...! PR피드백으로 받은 허용된 내용은 여기까지였다.)
 
난 ViewModel도 Analytics이벤트들을 관리할 필요가 없다고 생각했다.
그래서 이벤트 로그를 전송하는 어느기반 클래스or구조체를 만들고 
각 화면(또는 로직)에서 필요한 이벤트를 따로 구현하여 사용했으면 좋겠다..고했는데 말이 어렵다. 
코드로 설명 하는게 빠르겠다.

import 대충RxSwift

final class LoginViewModel {
    private let analyticsEventSender: LoginEventType = LoginViewAnalyticsEventSender()
    
    struct Input {
    	var kakaoLoginEventFromView: AnyObserver<...>
    }

    var input: Input

    private let kakaoLoginEventSubject = PublishSubject<...>()
    
    init() {
    	input = Input( ..fromView: kakaoLoginEventSubject.asObserver)
        
    	kakaoLoginEventSubject.subscribe {
            analyticsEventSender.sendKakaoLoginEvent()
        }
    }
}

kakaoLoginEve...from..이런 변수명은 중요한게 아니니 대충 흐름만 본다면
아직 LoginViewAnalyticsEventSender가 무엇인지, SomeSenderType이 무엇인지 설명하지 않았지만
analyticsEventSender 개체를 통해 kakaoLogin 이라는 이벤트를 전송한다는 것을 알 수 있다.
 
또 ViewModel이 BTN01_KAKAO_LOGIN과 같은 이벤트 코드를 직접 알 필요가 없어졌다.
단순히 eventSender에게 kakaoLogin이라는 이벤트를 전송하라고 말하고 있다.
지극히 주관적인 내 관점에선 이러한 구조가 더욱 유연한 코드라고 생각한다.
 
물론 이 뒤에 나올 코드들을 본다면 굳이..? 라는 생각이 드는 사람도 있겠지만 ㅎㅎ
 
네번째 리팩토링, 앱 내 사용중인 이벤트 키네임을 한 파일에서 모두 관리하게 했다.

enum AnalyticsEventName {
    case BTN01_KAKAO_LOGIN
    case BTN02_FACEBOOK_LOGIN
    case MMS01_COMPLETE_LOGIN
}

->
protocol SendableAnalyticsEvent {
    func toString() -> String
}

enum AnalyticsEventName {
    enum LoginVC: String, SendableAnalyticsEvent {
        case BTN01_KAKAO_LOGIN
        case BTN02_FACEBOOK_LOGIN
        case MMS01_COMPLETE_LOGIN   
        
        func toString() -> String {
            return self.rawValue
        }
    }
}

이렇게 한 이유는 말 그대로 한 파일 내에서 각 화면들에서 사용중인 이벤트들을 한 눈에 보기 위함이다.
여기에 다른화면이 추가되었을때 중복되는 키네임이 존재할 수 도 있다.
LoginVC가 SendableAnalyticsEvent를 채택하고 있고 toString()을 구현했는데 
String이면서 self.rawValue를 바로 사용않고 왜 toString이 있느냐? 는 이 뒤에서 설명한다.
 
다섯번째 리팩토링, 각 화면마다 로그이벤트를 관리하는 객체를 만들었다.

import 대충RxSwift

final class LoginViewModel {
    /// 여기
    private let analyticsEventSender: LoginEventType = LoginViewAnalyticsEventSender()
  
  ...
  
    	kakaoLoginEventSubject.subscribe {
            analyticsEventSender.sendKakaoLoginEvent()
        }
    }
}

analyticsEventSender의 구현체인, LoginViewAnalyticsEventSender가 바로 그거다.
 
아래는 세부 내용이다.

protocol LoginEventType {
    typealias Sender = AnalyticsEventName.LoginVC
    func sendKakaoLoginEvent()
    func sendFacebookLoginEvent()
    func completeLoginEvent()
}

struct LoaginViewAnalyticsEventSender: LoginEventType {
    var kakaoLogin = AnalyticsEventSender<Sender>(event: .BTN01_KAKAO_LOGIN)
    var facebookLogin = AnalyticsEventSender<Sender>(event: .BTN02_FACEBOOK_LOGIN)
    var loginComplete = AnalyticsEventSender<Sender>(event: .MMS01_COMPLETE_LOGIN)
    
    func sendKakaoLoginEvent(){ some() ... }
    func sendFacebookLoginEvent() { ... }
    func completeLoginEvent(){ ... }
    
    private func some() {}
}
struct AnalyticsEventSender<E: SendableAnalyticsEvent> {
    let event: E
    let needParameters: Bool

    init(event: E, needParameters: Bool = false) {
        self.event = event
        self.needParameters = needParameters
    }

    func send(parameters: [String: Any]? = nil) {
        if needParameters, parameters == nil {
            assert(parameters != nil, "this Event need Parameters")
        }

        print("send! \(event.toString())")
    }
}

네이밍은 무척이나 어렵다... 
어쨌거나, AnalyticsEventSender부터 설명해보면,
 
제네릭을 사용하고 있다. SendableAnalyticsEvent를 받아서 초기화를 진행한다.
SendableAnalyticsEvent는 열거형으로 정의된 로그이벤트의 전송 키네임이다. 
단순히 AnalyticsEventName.LoginVC을 받으면 될 것 같지만, 그렇게 하면 SendableEvent를 범용적으로 사용할수 없게 된다.
또 열거형을 프로토콜로 감싸지 않는다면 제네릭으로 받아올수 없기 때문에 SendableAnalyticsEvent를 채택하여 toString이라는 메서드를 통해 rawValue를 받아오게 했다. 
 
나중에 다른화면에서도

enum AnalyticsEventName {
    enum LoginVC: String, SendableAnalyticsEvent {
        ...
    }
    
    enum OtherVC: String, SendableAnalyticsEvent {
    	case BTN01_KAKAO_LOGIN
        ///...
        func toString() -> String {
            return self.rawValue
        }
    }
}

과 같이 이벤트들을 열거형으로 정의하고 사용하면 된다.
 
LoginViewAnalyticsEventSender와 LoginEventType 프로토콜을 보자.

protocol LoginEventType {
    typealias Sender = AnalyticsEventName.LoginVC
    func sendKakaoLoginEvent()
    func sendFacebookLoginEvent()
    func completeLoginEvent()
}

struct LoginViewAnalyticsEventSender: LoginEventType {
    var kakaoLogin = AnalyticsEventSender<Sender>(event: .BTN01_KAKAO_LOGIN)
    var facebookLogin = AnalyticsEventSender<Sender>(event: .BTN02_FACEBOOK_LOGIN)
    var loginComplete = AnalyticsEventSender<Sender>(event: .MMS01_COMPLETE_LOGIN)
    
    func sendKakaoLoginEvent(){ some() ... }
    func sendFacebookLoginEvent() { ... }
    func completeLoginEvent(){ ... }
    
    private func some() {}
}

LoginEventType프로토콜은 LoginView에서 사용중인 이벤트들이 무엇인지 사용자에게 노출시키고 사용할수 있게 한다.
EventSender의 사용자는 구현체인 LoginViewAnalyticsEventSender가 무엇을 구현했는지 알 필요 없이,
LoginEventType라는 프토콜에 의존하게 되며 실제 구현체의 내부 로직이 변경되는것으로부터 파생되는 이슈들을 피할수 있다.
 
구체적인 이벤트 키네임을 공개하지 않고 send...Event()로 행위로서만 외부에 노출된다.
(네이밍에서부터 내부로직유추를 막기란 아주 으류운것 같다...)
 
또 LoginEventType에서 typealias를 AnalyticeEventName.LoginVC 설정하고 있다.
밑에 구현체에서 사용하는것을 보면 각 AnalyticsEventSender<Sender>처럼 제네릭으로 사용하고 있는데,
제네릭으로 받을 타입을 AnalyticsEventName.LoginVC로 한정 짓기 위해 typealias를 설정한 것이었다.
 
 


 
 

여기까지 내 리팩토링의 완료 단계였다.

지금까지 생각해본 결과가 이정도 이지만, 더 생각을 하다보면 더 개선될 수도.
 

이 코드들은 실제 제품 코드로는 들어가지 못했다. PR에서 반려 당한 코드들이기 때문이다.

첫번째 이유는 복잡성 이었다. 누구라도 한 눈에 보기 쉬운 코드가 되어야 하는데 그러지 못하다는것이었다. 또 제네릭을 지양하는 팀원은 이런 구조가 생소하고 이해하기 어렵다고 했다. 내 의도는 이러한 구조가 약속처럼 사용된다면 이해하기 어렵고 따라가기 귀찮다는 불만을 해소할수 있을거라 생각했지만, 변론을 펼쳤음에도 동료개발자님께 납득을 시켜드리지 못했다.
 
두번째 이유는 번거로움.
난 프로젝트에 이제 막 투입되어 화면 일부분을 리팩토링하려 한것이다. 그러나 이런 코드가 모든 화면에 퍼져야 한다면 나뿐만 아니라
이 팀원 역시 이 코드를 이해하고 적용해야한다. 그러면서 발생하는 코드 작성의 번거로움이 문제였다.
단순히 Analytics의 타입메서드를 사용하면 매우 간편하니 따로 교육이나 설명조차 필요 없었겠지만 내가 만든 구조를 사용하려면 
일단, AnalyticsEventName 열거형 내부에 각 화면에서 사용하는 EventName을 또 열거해야한다.
그리고 이를 기반으로 LoginEventType같은 protocol을 만들어주고, 이 프로토콜을 채택한 구현체를 또 만들어줘야한다.
여기까지만 해도 굉장히 번거로워 보일 수 있다. 이런 의견에 나는... 번거로울지라도 이게 맞다는 의견을 내비췄지만 코딩엔 정답이 없으니까... 번거롭다는 의견을 묵살할 수 없었다.
 
세번째 이유는 이런 사소한것까지 볼륨을 키울 필요가 없어보인다. 였다.
AnalyticsEvent말고도 다른 통계솔루션들이 import되어있고 , 사용비중은 AnalyticsEvent가 가장 저조한 상태였다.
굳이 사용빈도가 많지 않은 이 이벤트들을 위해 이렇게까지 할 필요가 있냐는 의견에 크게 반문을 하지않았다.
이런저런 이유를 가져다 대기 시작하면 결국 쉽게 쉽게 가는 코드가 나올수 밖에 없을테고 나중에 리팩토링을 하려면 구조때문에 골치가 아파올거라 생각한다. 지금 당장 내가 느꼈던것 처럼. 역할에 맞는 책임을 확실히 구분해야 리팩토링을 할 때도 더 쉬운길을 갈 수 있을거라는 생각을 하고 있지만, 결국 팀원과의 화합이 더 중요하기 때문에 최대한 의견을 받아들였다.
 
사실 피드백들이 나에겐 크게 납득이 되지않았지만, 뒤늦게 프로젝트에 합류한 내가 작은 불씨를 지펴서 프로젝트 모든 구석구석을 다 살피고 최적의 코드를 만들순 없다고 생각한다. 고로 팀원의 의견을 최대한 수렴하고 기존의 방식을 채택하는것이 지금은 우선이라고 생각이 들었다. 내게 피드백을 준 동료는 함께 일하는 팀원간의 코드로하는 소통에서, 불필요한 리소스 소모를 줄이고자 하는 생각이 컸으므로 기존의 편리한 사용방식을 쉽게 버릴수 없다는걸 알고 있다. 코드리뷰때마다 좋은 지적을 많이 해주셔서 많이 배운다.
 
내 의도는 로그를 전송하는 코드도 사용자 입장(ViewModel)에서 최대한 간편하게 사용하게끔 하고 싶었던 거였다.
이러한 나의 의도가 코드에 고스란히 녹아들었는지는 제3자가 관찰하고 파악하는게 더 맞는걸수도 있다. 내가 그랬던 것처럼 말이다.
결국 이런저런 이유로 내 코드는 반려당했지만, 이것도 하나의 연습이라고 생각하려 한다.
 
곧 이런 주제로 또 하나의 포스팅을 할 것 같다. 
새로운 화면을 만드는데, 그 화면도 ... 이런 사단이 날 것 같단 말이지.

728x90