ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드에 여러 제조사의 영수증 프린터 연동할때 어려웠던 점
    Java-Kotlin/Android 2026. 5. 6. 13:32
    반응형

    회사에서 POS/KIOSK로 나가는 안드로이드 기기의 기본적인 테스트를 하기 위한 앱이 필요할 것으로 예상되어 개인적으로 DeviceTestApplication 프로젝트를 파서 진행했다. 해당 프로젝트에서는 다양한 영수증 프린터(Epson, Sewoo 등)의 출력 테스트, 디스크 읽기/쓰기 속도 테스트, RFID Reader의 읽기 테스트, 네트워크 테스트 등을 구현할 계획이었다. 오늘은 그중에 다양한 영수증 프린터의 연동 및 출력 테스트를 제작한 것에 대하여 글을 남긴다.

     


    나는 처음에 프린터 제조사의 안드로이드 연동 SDK 대로만 연동하면 될 것으로 예상했다. 하지만 처음에 생각했던것보다 난관이 의외로 많았다. 

     

    어려웠던 점

    지금 기억나는 연동시에 어려웠던 점들은 하드웨어 제어 흐름의 파편화와 영수증 출력 레이아웃을 그리는 레이아웃 코드의 가독성 붕괴였다.

    1. 비즈니스 로직과 어울리지 않는 어지러운 콜백 함수 

      프린터 모듈에서 영수증을 출력하는 과정은 연결 -> 상태 확인 -> 출력 -> 커팅 이라는 지극히 동기적이고 순차적인 흐름이다. 하지만 코드는 연결 성공 콜백 안에서 상태 확인을 호출하고, 그 콜백 안에서 다시 출력을 지시하는 식으로 깊어지며 콜백이 반복되어 나중에는 알아보고 수정하기도 힘든 스파게티 코드가 되어갔다.

    2. 명령형으로 나열된 하드코딩 레이아웃

      영수증 레이아웃을 출력할 때마다 printer.printText(), printer.setAlignment(), printer.setTextStyle() 같은 함수들을 수십 번씩 호출했다. 코드가 길어지니 결과물(영수증)이 어떻게 생겼을지 직관적으로 그려지지가 않았고, 조그마한 추가사항이나 수정이라도 직접 수정하고 뽑아보는 과정을 반복했다. 그래서 유지보수가 너무나 힘들었다.

     

    원인

    이러한 문제점들이 발생한 원인은, 제조사 SDK의 설계 패턴을 우리 프로젝트의 비즈니스 로직에 그대로 노출시킨 데에 있었다.

     

     

    첫번째 문제점의 원인은 이렇다.

     

    대부분의 안드로이드 하드웨어 SDK는 C언어나 C++의 네이티브 so파일을 JNI로 연결해놓은 형태이다. 그래서 어쩔수 없이 비동기형태의 콜백 패턴을 강제한다. 이러한 SDK를 우리가 연동시에 적절하게 동기화 하여 사용하도록 래핑해서 사용하는게 가장 이상적이지만 그렇게 진행하지 못했던게 원인.

     

    두번째 문제점의 원인은 이렇다.

     

    영수증이라는 하나의 UI를 그리는 작업임에도 이 작업에 대한 구현 방식은 선언적으로 추상화하지 않고, 하드웨어에 직접 명령을 내리는 방식에 매몰되어 구현했던게 원인.

     

    문제의 원인

    • 콜백 안에 콜백을 중첩하여 순차적 비즈니스 로직을 처리
    • 함수 호출을 한 줄씩 나열하는 명령형 영수증 템플릿 작성

    이상적 해결

    • 비동기 콜백을 동기적(순차적) 흐름으로 묶어내어 가독성 확보
    • HTML이나 Compose처럼 직관적이고 선언적인 코드로 영수증 레이아웃 작성

     

    해결

    이 문제는 하드웨어를 다루는 Wrapper 레이어의 접근 방식만 고쳐도 생산성에 엄청난 향상을 가져올 수 있을 것으로 보였다. 즉시 비동기 콜백을 동기로 묶어내는 작업과, 영수증 템플릿을 위한 Kotlin DSL 모듈 구축을 진행했다.

     

     

    1. CountDownLatch를 활용한 Blocking 처리 먼저 CB125BlockingPrinter 클래스를 만들어 비동기 콜백의 사슬을 끊었다. CountDownLatch를 사용해 콜백 응답이 올 때까지 스레드를 대기시키고, 결과를 반환하도록 만들었다.

    // CB125BlockingPrinter.kt 
    fun getStatusDetail(): PrinterStatusType {
        var status: PrinterStatusType = PrinterStatusType.UNKNOWN
        val latch = CountDownLatch(1)
    
        // 비동기 콜백 호출
        printer?.printerStatus { result ->
            status = when (result) {
                POSConst.STS_NORMAL -> PrinterStatusType.NORMAL
                // ...
                else -> PrinterStatusType.UNKNOWN
            }
            latch.countDown() // 결과를 받았으므로 래치 해제
        }
    
        // 최대 2초 동안 콜백 대기, 이후 순차적 로직 진행
        latch.await(2, TimeUnit.SECONDS)
        return status
    }

     

    2. Kotlin DSL(Type-Safe Builders) 도입 영수증 템플릿 작성은 Kotlin의 + 연산자 오버로딩과 확장 함수를 활용해 완전히 새롭게 디자인했다.

    // PrinterTestUtil.kt
    private val testComp = styledComponent<TestSheetProps> {
        val (printerType) = props
        +HorizontalLine(HorizontalLineProps())
    
        +"영수증 프린터 출력 테스트 용지 입니다."
        +HorizontalLine(HorizontalLineProps())
        
        +("문자열 속성 테스트 : 굵게" format TextComponent.Format(style = setOf(TextStyle.BOLD)))
        +("문자열 속성 테스트 : 사이즈 4배" format TextComponent.Format(fontHeight = 4.0, fontWidth = 4.0))
        +("문자열 속성 테스트 : 우측 정렬, 밑줄" format TextComponent.Format(
            alignment = Alignment.RIGHT,
            style = setOf(TextStyle.UNDERLINE)
        ))
        
        +HorizontalLine(HorizontalLineProps())
        +"출력시간: ${LocalDateTime.now()}"
        +cut
    }

    styledComponent는 내부의 프린터 영수증 템플릿 라이브러리에서 정의한 함수이다.

     

    결과는 좋았다. 기존에 수십 줄에 달하던 알 수 없는 명령어들의 나열이, 실제 영수증 형태를 꼭 닮은 몇 줄의 코드로 압축되었다. 새로운 텍스트 포맷이나 바코드를 추가할 때도 기존 로직을 헤치지 않고 블록을 끼워 넣듯 손쉽게 작업할 수 있었다. 게다가 Epson이든 Sewoo든 하위 제조사 구현체만 갈아 끼우면 동일한 DSL 템플릿을 재사용할 수 있는 구조가 완성되었다.

     

    결론

    나는 이렇게 마주한 문제를 언어의 기능을 활용한 적극적인 구조화를 통해 해결했다.

    처음에 작성했던 단순 SDK 함수 나열은 하드웨어 통신의 본질적인 복잡성을 전혀 캡슐화하지 않은 채 날것 그대로 비즈니스 로직에 노출시켰고, 그렇기에 실패할 수밖에 없었다. 하지만 CountDownLatch를 통해 동기적 흐름을 되찾고, Kotlin DSL을 통해 선언적 레이아웃을 구성하고 나니, 복잡했던 하드웨어 연동이 프론트엔드 UI를 짜는 것만큼 즐거운 경험으로 바뀌었다.

    물론 언어의 고급 기능을 섞어 구조를 재설계하고, 제조사마다 다른 명령어 규격(Epson의 ESC/POS 등)을 하나의 인터페이스로 추상화하는 일은 생각보다 훨씬 피곤하고 고생스러운 과정이다. 하지만 미래에 이 결제 모듈을 유지보수할 누군가(아마도 높은 확률로 나 자신)를 위해서는 결국 낡은 패턴을 직시하고 부숴내야만 한다.

    댓글

Designed by Tistory.