[TroubleShooting] 디자인 패턴 적용기 - 1

Larry·2025년 7월 4일

TroubleShooting

목록 보기
1/2

현재 개발을 맡고있는 Android VPN 서비스인 Kraton VPN을 구현하던 중, 여러 디자인 패턴을 활용하며 겪은 시행착오에 대해 소개하는 글입니다.

🧠 배경: VPN 앱에서의 위임이 필요한 이유

VPN 앱을 개발하다 보면, 사용자에게는 하나의 단일 버튼(연결/해제)만 제공하지만, 실제로는 여러 가지 VPN 프로토콜(WireGuard, IKEv2 등) 중 하나를 선택해 실제 네트워크 연결을 수행해야 합니다.

이때 VPNController는 직접 각 프로토콜의 로직을 구현하기보다는, 그 작업을 Protocol이라는 인터페이스로 정의하고, 각 구현체(IKEv2, WireGuard 등)에 위임(delegate) 하도록 설계하면 다음과 같은 이점을 가질 수 있습니다.

  • VPNController는 프로토콜 구현 세부사항을 몰라도 되므로, 새로운 프로토콜을 쉽게 추가할 수 있음
  • SOLID 원칙 (특히 SRP, OCP, LSP)을 자연스럽게 따르게 됨

인터페이스 설계: Protocol

interface Protocol {
    fun connect()
    fun disconnect()
    fun status(): String
}

실제 구현체: IKEv2, WireGuard

class IKEv2 : Protocol {
    override fun connect() = println("IKEv2: Establishing secure tunnel")
    override fun disconnect() = println("IKEv2: Tearing down connection")
    override fun status() = "IKEv2 Connected"
}

class WireGuard : Protocol {
    override fun connect() = println("WireGuard: Setting up fast, lightweight tunnel")
    override fun disconnect() = println("WireGuard: Disconnecting tunnel")
    override fun status() = "WireGuard Connected"
}

잘못된 설계1: OCP(개방-폐쇄 원칙) 위반 사례

아래 예시는 VPNController 클래스 내부에 프로토콜별 분기 로직을 직접 작성한 형태입니다

class VPNController(private val type: String) {
    fun connect() {
        if (type == "IKEv2") {
            println("IKEv2: Establishing secure tunnel")
        } else if (type == "WireGuard") {
            println("WireGuard: Setting up fast, lightweight tunnel")
        }
    }

    fun disconnect() {
        if (type == "IKEv2") {
            println("IKEv2: Tearing down connection")
        } else if (type == "WireGuard") {
            println("WireGuard: Disconnecting tunnel")
        }
    }
}

사실 보자마자 가장 먼저 떠오른 형태의 코드입니다. 하지만 이 구현은 다음과 같은 이유로 OCP를 위배합니다.

  • 기능을 확장(새로운 프로토콜 추가 등)하기 위해 반드시 기존 클래스 코드를 수정해야 합니다.
  • 분기 조건이 많아지면서 코드가 점점 복잡해지고 가독성이 떨어짐.
  • 확장에 열려 있어야 할 VPNController가 변경에 자주 노출되어, 버그 발생 가능성이 커집니다.
  • 예를 들어 새로운 프로토콜 OpenVPN을 추가하고 싶다면 다음과 같이 클래스 내부를 수정해야 합니다
if (type == "OpenVPN") {
    println("OpenVPN: Starting session")
}

클래스는 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 하기 때문에, 이는 명백히 개방-폐쇄 원칙 위반입니다.

잘못된 설계2: 상속을 통한 구현

class VPNController : WireGuard()

이렇게 상속을 사용할 경우:

VPNController는 WireGuard에 갇혀버려 다른 프로토콜을 사용할 수 없습니다

타입이 고정되기 때문에 val protocol: WireGuard = VPNController() 와 같은 의도치 않은 타입 캐스팅이 가능해집니다 (리스코프 치환 원칙 위배)

이렇게 실수하기 쉬운 잘못된 설계 케이스 두 가지를 알아보았는데요, 이러한 문제들을 예방하기 위해 위임을 활용할 수 있습니다.

by 키워드 위임을 통한 구현

반면, 위임을 사용하는 구조에서는 다음과 같이 새로운 프로토콜을 추가해도 기존 코드에 영향을 주지 않습니다.
인자를 통해 넘어온 Protocol 인터페이스 기반 위임을 통해 동작을 분리하고, 기능 추가 시에도 기존 코드 변경 없이 확장 가능합니다.

class VPNController(private val protocol: Protocol) : Protocol by protocol {
    fun logStatus() {
        println("Controller: Delegating to ${protocol.javaClass.simpleName}")
    }
}

이제 VPNController는 어떤 Protocol이든 자유롭게 사용할 수 있으며, 실제 연결 로직은 Protocol 구현체에 위임됩니다.

실행 예시

fun main() {
    val ikeController = VPNController(IKEv2())
    ikeController.connect()             // IKEv2: Establishing secure tunnel
    ikeController.logStatus()          // Controller: Delegating to IKEv2

    val wgController = VPNController(WireGuard())
    wgController.connect()             // WireGuard: Setting up fast, lightweight tunnel
    println(wgController.status())     // WireGuard Connected
}

주의해야 할 점

1. 위임 대상은 변경 불가 (by는 val 파라미터 기준으로 위임 수행)

class VPNController(var protocol: Protocol) : Protocol by protocol

위임 대상을 동적으로 바꾸려고 이렇게 var로 선언해보아도, 위임된 대상은 최초 전달된 인스턴스로 고정됩니다. 이후 protocol = WireGuard() 로 바꿔도 실제 델리게이션은 여전히 초기 객체에 위임합니다.
따라서 GC 수집 대상이 되지 않고 메모리 누수가 발생할 수 있습니다.

by 키워드는 파라미터가 아닌 프로퍼티가 될 수 없습니다. 위임 대상이 동적으로 바뀌길 원하면 delegation pattern 대신 strategy pattern을 고려해야 합니다.

2. 함수 오버라이딩 우선순위 (중복 메소드 대응)

class VPNController(private val protocol: Protocol) : Protocol by protocol {
    override fun disconnect() = println("Controller: Custom disconnect logic")
}

이처럼 델리게이션 대상과 동일한 이름의 메소드를 직접 구현하면, 코틀린은 내부 구현이 우선되며, 자동 위임 메소드는 생성되지 않습니다. 따라서 정확한 동작 흐름 제어가 가능합니다.

결론

VPNController는 여러 프로토콜 중 하나에게 실행을 위임하는 역할만 담당합니다. 어떤 프로토콜인지에 따라 실제 동작은 달라지지만, VPNController 자체는 그 사실을 알 필요도 없고 알지도 못합니다. 이것이 바로 위임의 본질적인 이점입니다.
프로토콜을 바꾸고 싶을 땐 VPNController를 바꿀 필요 없이, 전달하는 Protocol 구현체만 바꾸면 됩니다.

객체 간의 공통점이 있다고 해서 반드시 상속을 사용해야 하는 것은 아닙니다. 구조를 설계할 때에는, 우선 연관 짓고자 하는 각 객체의 책임을 명확히 하고 객체 간의 관계를 정의해야 설계 실수를 줄일 수 있습니다.

profile
안드로이드 개발자 Larry입니다.

0개의 댓글