본문 바로가기

모바일(Mobile)/안드로이드(Android)

[Android] 파이프라인 디자인 설계와 고찰

최근에 파일의 전송과 관련된 기능을 구현하기 위해 고민을 했던 적이 있는데,
이때 적용한 아이디어가 꽤 유용한 것 같아서 기록 삼아 글을 쓰게 되었습니다.

 

먼저, 문제 해결 방법에 관한 이야기를 공유하기 전에 문제 상황에 대해서 간략히 정리해 보도록 하겠습니다.

 

이번에 만들고자 했던 기능은 다음과 같이 세 가지로 구분할 수 있다고 봤습니다.

  1. 데이터 불러오기 (File Read)
  2. 데이터 변환하기 (Data Conversion)
  3. 데이터 전송하기 (Data Transmit)

 

위의 요구 사항을 만족하기 위해 구현할 함수 및 워크 플로우는 단일 함수 또는 클래스로도 구현할 수 있겠지만,
이번에는 조금 더 고차원적인 설계를 해보고 싶었습니다.

 


 

그 이유는 각 단계를 분리하면 나중에 부품처럼 조립해서 쓰기 좋겠다고 생각 때문이었습니다.

 

앞서 정리한 세 가지 단계 중에서 특히 데이터 불러오기와 데이터 전송하기 작업은 쓰이는 빈도수도 많고,
추상화된 클래스를 상속하여 다형성을 추가할 여지가 충분한 부분도 많아 보였습니다.

 

그래서, 어쩌면 오버 엔지니어링이 될지도 모르겠지만 에라 모르겠다 하고
작업을 분리하되 연결하여 하나로 합칠 수 있는 “파이프라인” 디자인을 채택해 보았습니다.

 

 

<pipeline description>

 

 

파이프 라인 의사 코드

# 파일 → 파싱 → 전송 파이프라인

function pipeline(file_path):

    # 1단계: 파일 읽기

    raw_data = read_file(file_path)

    # 2단계: 파싱

    parsed_data = parse(raw_data)

    # 3단계: 전송

    send(parsed_data)

    return "Pipeline Completed"

 

# 실행 예시

pipeline("input.txt")

 

 

위 구조를 보았을 때, 전체적인 파이프라인은 설계 및 구현에 난항이 없으리라 예견했습니다.

그런데, 이 생각이 징크스였던 걸까요? 실제 클래스 및 메서드를 구현하던 중 각각의 단계에서 필요한 파라미터와 반환 값이 다르다는 것을 고려하지 못한 걸 알아차려 버렸습니다.

 

저의 경우 1단계에서 필요한 파라미터가 3단계에서는 전혀 필요 없는데도,

1단계를 위해 3단계까지 함수의 인자로 끌려다녀야 하는 다소 괴이한 구조가 매우 눈에 거슬리고 말았습니다. 🤣

 


 

 

안드로이드에서 사용할 모듈로서 해당 기능을 만들 예정이었기 때문에,

제네릭을 통해 추상화를 열심히 적용해 봐도 근본적으로 함수 인자로 끌려다니는 깍두기 파라미터를 어찌할 방도를 몰라서 굉장히 답답했습니다.

 

의사 코드로 표현한 기능의 각 단계를 담당할 클래스를 A, B, C라고 했을 때,

만약 C에서 꼭 필요한데 A에서 필요 없는 파라미터 y가 있다면 이 y는 A → B → C를 거쳐 C에서 '소비(Consume)' 될 때까지 파라미터가 전달되는 구조로 가기 쉽고,

 

A에서 전혀 안 쓰는 파라미터인데 C에서 필요하니까 억지로 끌고 가는 구조로 가다 보니, 결과적으로 A → C까지 흐름에서 A의 초기 파라미터가 과도하게 비대해진 걸 고치고 싶었습니다.

왜냐하면 나중에 유지 보수 측면에서도 코드를 난해하게 볼 수밖에 없는 구조가 되기 너무 쉬워 보였기 때문입니다.

그래서 이러한 기술 부채를 해결하려면 어떤 방향으로 개선해 가는 것이 좋을지 고민했는데, 제가 겪은 이 문제가 바로 파이프라인 설계에서 자주 마주하는 콘텍스트 전파 문제(context propagation problem)라고 한다고 합니다.

 

 

<context reference image>

 

 

위 그림처럼 앞으로 구현할 기능을 다시 한번 재정의해 봤을 때,

제가 구현할 클래스 또는 함수는 파일 → 파싱 → 전송이라는 비교적 선형적인 파이프라인을 가지는 것은 명확했습니다.

 

그래서, 근본적으로 스스로 지적한 문제를 해결하는 건 아닐 수도 있지만,

코드상에서 관심사를 분리하고 주된 기능에 집중할 수 있도록

“전역 Pipeline Context + Stage 별 input/output 분리”라는 구조를 채택하기로 했습니다.

 

이는 거시적으로 해석한다면, 안드로이드의 Context 관리 정책과 비슷해 보였고

특히 액티비티와 같은 인터페이스 전문 클래스에서 Context에 접근해야 할 경우가 생기면 필요에 따라 요청하고 받는 식의 구조가 되어서 안드로이드와 결을 같이 하는 것 같아 잘 어울릴 것 같았기 때문입니다.

 

물론, 단순한 클래스 하나 만드는데 이렇게 깊이 고민해야 하나 생각이 들기도 했지만,

무언가 이번에는 해야 하니까 코드를 작성하기보다 앞으로 당분간 코드를 굳이 안 고쳐도 될 정도로 견고한 구조를 만들어 보고 싶었습니다.

 

이러한 계획이 성공적이라면, 객체 지향을 한층 더 제대로 이해하고 좋은 코드, 좋은 기능을 한 줄 더 적어낼 수 있다는 기대감에 고취되었기 때문이었죠,

 


 

<pipeline example>

 

 

다행히 이번에 적용한 클래스 설계는 꽤 유용했습니다.

원래는 ‘파일 → 파싱 → 전송’이라는 단일 파이프라인을 위해 설계했지만,

이를 응용한 프로그램(=앱)에서 ‘파일 → 판독’과 같은 다른 파이프라인을 구축하는 것도 가능했고,
유효성 검사와 같은 추가적인 기능 구현도 훨씬 쉬워져서 재미를 충분히 봤던 것 같습니다.

 

이번 포스팅에서 다룬 기능을 구현할 때 원래는 단일 또는 두 가지 정도의 클래스로 충분히 만들 수 있다는 것은 알았지만,
특정 클래스에 기능이 종속되고 기능을 분리하여 재조립하는 것에 한계가 있다는 점이 아쉬웠었습니다.

 

그렇기에 설계에만 꽤 오랜 시간을 투자했지만, 다행히도 다양한 사례에 응용하고 확장할 때 빠르게 적용 및 검증할 수 있어서 좋았습니다. 😊