본문 바로가기
작디 작은 나만의 라이브러리/EPSON 써멀 프린터 라이브러리

[신입 개발자의 '0' 번째 라이브러리] Dialect와 Adapter 패턴의 활용 - EPSON 써멀 프린터 라이브러리 제작기(1)

by 시니성 2024. 12. 15.

들어가며

여태까지 제가 만들었던 세 가지 라이브러리 제작기를 다루었는데, 생각해보니 너무 옛날이라 깜빡 잊고 있던 라이브러리가 있더라구요 ㅎㅎ;
바로 입사 2개월 차였던 2023년 11월, 주방 주문서 출력을 위한 EPSON 써멀 프린터 라이브러리 입니다.
이 라이브러리의 주요 과제는 다음과 같았습니다:

  1. Epson 프린터의 명령어 셋(Command Set)을 체계적으로 관리
  2. 네트워크 소켓, 시리얼 연결, 안드로이드용 Epson SDK 등 다양한 연결 방식 지원
  3. 확장 가능하고 유지보수가 용이한 구조 설계

사실, 처음에는 네트워크 연결만 구현하면 되는, 나름대로 단순한(?) 요구사항이었습니다.
하지만 저는 조금 다르게 생각했습니다. 프린터와의 통신은 결국 동일한 명령어 셋을 전달하는 것이고, 단지 전달 방식만 다를 뿐이었죠.
그렇다면 추상화를 통해 다양한 연결 방식에 대응할 수 있는 코드를 짜 두는 것이 좋겠다고 판단했습니다.
그리고 이러한 판단은 실제로 큰 도움이 되었습니다:

  • 네트워크 연결은 즉시 프로덕션에 투입
  • 시리얼 연결은 로컬 테스트 환경에서 활용
  • 최근에는 안드로이드 POS용 USB 연결도 쉽게 추가

특히 마지막 안드로이드 USB 연결 지원 코드를 작성할때가 제겐 인상깊었습니다.
비록 2개월 차의 부족한 코드였지만, 초기에 설계한 추상화 덕분에 어댑터 코드만 추가하는 것으로 쉽게 지원할 수 있었죠 ㅎㅎ
(추상화의 힘을 피부로 체감하게 해준 라이브러리를 포스팅 하는걸 잊고있었다니..!)

무튼 이번 글에서는 이 라이브러리의 전체적인 설계와 핵심 컴포넌트인 PrinterDialect와 Adapter 패턴의 활용에 대해 살펴보겠습니다.
(지금 보면 PrinterDialect가 너무 많은 책임을 가지고 있어, 단일 책임 원칙을 위반했다는 것을 알 수 있는데요. 1년차가 된 지금 이 라이브러리의 코드를 톺아봤을 때, 아쉬운 점과 개선 가능한 점은 마지막 글에서 다룰 예정입니다.)

PrinterDialect: 프린터 명령어의 중앙화

PrinterDialect 클래스 개요

프린터 제어를 위한 모든 명령어는 PrinterDialect 객체에 중앙화하여 관리하도록 설계했습니다.
이는 다음과 같은 이점을 제공합니다:

  1. 명령어 중복 제거
  2. 명령어 수정/추가의 용이성
  3. 문서화와 유지보수 편의성
object PrinterDialect {
    // 기본 제어 문자
    private const val NUL: Byte = 0
    private const val STX: Byte = 2
    private const val ETX: Byte = 3
    // ... 기타 제어 문자들

    // 프린터 초기화 명령어
    internal val INIT_PRINTER: ByteArray = byteArrayOf(ESC, '@'.code.toByte())

    // 줄바꿈 명령어
    internal val NEW_LINE: ByteArray = byteArrayOf(CR, LF)

    // 용지 절단 명령어
    internal val CUT: ByteArray = byteArrayOf(ESC, 'i'.code.toByte())

    // ... 기타 명령어들
}

각 명령어는 ByteArray 형태로 정의되어 있으며, 필요한 경우 파라미터를 받아 동적으로 명령어를 생성하는 메서드도 제공합니다.

명령어 생성 메서드

PrinterDialect는 다양한 프린터 기능을 위한 명령어 생성 메서드를 제공합니다:

object PrinterDialect {
    // 텍스트 정렬 설정
    internal fun createAlignmentCode(alignment: Int = 0): ByteArray =
        ALIGNMENT_CMD + byteArrayOf(alignment.toByte())

    // 폰트 크기 설정
    internal fun createFontSizeCode(width: Int = 0, height: Int = 0): ByteArray =
        FONT_SIZE_CMD + byteArrayOf((height + width).toByte())

    // QR 코드 생성
    internal fun createQrCodeSizeCode(size: Int = 5): ByteArray {
        val cn: Byte = 49
        val parameter = byteArrayOf(cn, QR_SIZE_FN, size.toByte())
        val plPh = parameter.plPh
        return QR_CODE_CMD + plPh + parameter
    }

    // ... 기타 명령어 생성 메서드들
}

Adapter 패턴을 활용한 연결 방식 추상화

기본 인터페이스 설계

프린터 연결 방식의 추상화를 위해 다음과 같은 계층 구조를 설계했습니다:

// 최상위 인터페이스
interface EpsonPrinterAdaptor

// 공통 기능을 구현한 추상 클래스
abstract class T83PrinterAdaptor : EpsonPrinterAdaptor {
    abstract var outputStream: OutputStream?
    internal abstract fun connect()
    internal abstract fun disconnect()
    internal abstract fun outputStream(): OutputStream
    internal abstract fun validationCheck(): Result<Unit>
    internal abstract fun output(data: ByteArray)

    // 공통 출력 기능 구현
    internal fun printText(data: String, alignment: Alignment, ...) { ... }
    internal fun printBarcode(data: String, symbology: BarcodeType, ...) { ... }
    internal fun printQrCode(data: String, alignment: Alignment, ...) { ... }
    // ... 기타 공통 메서드들
}

구체적인 Adapter 구현

각 연결 방식별로 concrete adapter를 구현했습니다:

// 네트워크 연결용 어댑터
internal object NetworkPrinterAdaptor : T83PrinterAdaptor() {
    internal var tcpIp: String = ""
    internal var port: Int = 9100
    internal var timeout: Int = 1000

    override fun connect() {
        disconnect()
        try {
            if (socket?.isConnected == true) return
            val newSocket = Socket()
            val address = InetSocketAddress(tcpIp, port)
            newSocket.connect(address, timeout)
            socket = newSocket
            outputStream = socket!!.getOutputStream()
        } catch (e: Exception) {
            logger.error("connect error", e)
            disconnect()
            throw e
        }
    }
    // ... 기타 구현
}

// 안드로이드 SDK용 어댑터
class AndroidSerialPrinterApiAdapter(
    private val printer: Printer,
) {
    internal var connectionType: AndroidSerialConnectionType = AndroidSerialConnectionType.USB
    internal var address: String = ""
    // ... 기타 구현
}

설계의 장점

  1. 확장성: 새로운 프린터 연결 방식 추가가 용이합니다. 새로운 Adapter를 구현하기만 하면 됩니다.
  2. 유지보수성: 프린터 명령어가 중앙화되어 있어 명령어 수정이 용이하고, 각 연결 방식별 코드가 분리되어 있어 유지보수가 쉽습니다.
  3. 재사용성: 공통 기능이 추상 클래스에 구현되어 있어 코드 재사용성이 높습니다.

마치며

이러한 설계를 통해 다양한 연결 방식을 지원하면서도 유지보수가 용이한 프린터 라이브러리를 구현할 수 있었습니다.
다음 글에서는 각각의 연결 방식별 구현에 대해 더 자세히 살펴보도록 하겠습니다.

프로젝트 초기 단계에서 이러한 설계 결정을 내린 것이 이후 개발 과정에서 큰 도움이 되었습니다.
특히 명령어의 중앙화와 Adapter 패턴의 활용은 코드의 확장성과 유지보수성을 크게 향상시켰습니다.

728x90