쓰기 쉬운 Toss Front SDK
안녕하세요, 토스플레이스 Frontend Developer 이주함입니다.
저는 토스플레이스에서 자체 개발한 결제 단말기인 Toss Front(이하 프론트)의 외부 연동 SDK(Software Development Kit)를 개발하고 있습니다. 이 SDK를 활용하면 토스 서비스의 데이터를 연동해 내가 원하는 플러그인 앱을 개발하고, 프론트에서 동작하도록 연동할 수 있어요. 즉, 3rd-party의 연동을 통해 내부 개발이 아닌, 외부 연동사의 개발로 무한히 확장할 수 있는 구조입니다.
이 글에서는 초기 단계의 SDK를 쓰기 쉽게 만들기 위해 어떤 고민을 했는지 소개하려 해요. 연동사가 개발을 할 때 어떤 플로우로 SDK를 사용하는지 확인하고, 더 안정적으로 사용할 수 있게끔 제공하여 휴먼 에러를 구조적으로 방지하는 방법을 사례와 함께 소개합니다.
들어가며: 쓰기 쉬운 SDK
외부 연동사의 다양한 연동된 앱과 함께 프론트의 생태계를 성장시키기 위해서는, 무엇보다 사용 경험이 좋은 SDK 인터페이스와 기능 설계가 필수적입니다.
일반적으로 외부 SDK를 사용할 때에는 “쓰기 쉽다” 혹은 “어렵다”의 평가로 SDK의 첫인상이 결정되죠. 이 첫인상은 당장의 도입 여부에 영향을 끼치고, 이 경험은 이후의 확장성과 장기적인 유지 사용에까지 영향을 미칩니다.
서버를 열고 닫는 SDK 예시를 하나 살펴보겠습니다.
const { serverId } = await sdk.server.open();
sdk.server.onConnect(`connection:${serverId}`, (connection) => {
sdk.server.onMessage(`message:${connection.id}`, onMessage);
sdk.server.onError(`error:${connection.id}`, onError);
});
// Clean-up
await sdk.server.disconnectMessages(serverId); // 핸들러 부착 제거
await sdk.server.close(serverId); // 서버 닫기인터페이스의 네이밍이 너무 추상적이지도 않고, 각각의 메서드들의 역할이 명확하며, 예시 코드 작성 가이드대로 작성하는 것이 크게 어렵지 않습니다.
최근 AI 도구의 발전으로 이러한 예시 코드를 더 쉽게 작성할 수도 있고요.
하지만 플랫폼의 관점에서 위와 같이 가이드를 주는 것은 굉장히 위험합니다.
Connect 이후에 Message 콜백 이벤트를 부착하지 않도록 코드가 작성될 수 있고, 핸들러 부착 제거가 안 된 상태로 서버가 닫히고 메모리 누수(Memory Leak)가 일어날 수 있습니다.
이로 인해 하나의 문의가 더 들어오는 장애 상황이 일어날 수 있고, 이는 곧 SDK 안정성의 문제로 다가옵니다. 즉, 연동사의 휴먼 에러가 SDK 플랫폼의 안정성과 별개의 일이 될 수는 없다는 것이죠.
SDK를 사용하는 인터페이스 자체에 간편성이 녹아져야, 하나의 안전 장치가 될 수 있습니다. 다시 말해, SDK 내부에서 동작하는 불필요한 것들을 은닉화하며 보여주는 사용 코드의 간편성을 높여야 합니다.
잘 설계된 인터페이스는 사용자가 올바른 방법으로만 SDK를 사용하도록 유도하며, 잘못된 사용 패턴을 원천적으로 방지할 수 있어요.
아래의 예시 인터페이스를 위 예시 인터페이스를 한 번 비교해 보면, 어떤 의미인지 잘 와닿을 것 같습니다.
const server = await sdk.start({
onConnection,
onMessage,
});
// Clean-up
await server.stop();Facade 패턴 다시 정의하기
앞선 예시에서 살펴본 것처럼, SDK가 제공하는 원자적인 기능 자체보다 더 중요한 것은 “사용자가 그 기능을 어떤 방식으로 사용하게 되는가”입니다.
서버를 열고 닫는 기능은 하나였지만, 그 기능을 사용하기 위해 사용자가 감당해야 했던 절차 순서, 정리 책임이 숨어있었던 어려움이었습니다. 이는 일종의 암묵적인 의존성이에요.
이 지점에서 자연스럽게 등장하는 설계 패턴이 바로 퍼사드(Facade) 패턴입니다.
1. Facade 패턴의 본질은 뭘까
Facade 패턴은 보통 ‘복잡한 서브시스템을 단순한 인터페이스로 감싸는 패턴’으로 정의됩니다.
하지만 SDK 설계 관점에서 Facade의 본질은 기능을 숨기는 것 자체가 아닙니다. “복잡한 내부 구현을 ‘사용자의 의도(Intent)’ 기준으로 다시 구성하는 것” 이에요.
즉, 다양한 경우에서 SDK 메서드 내부에는 사용자의 인증, 실패 시 재시도 로직, 상태 관리, 클린업(Cleanup) 로직과 같은 복잡한 요소들이 존재하지만, 사용자는 “서버를 시작한다”, “파일을 업로드한다”, “결제를 요청한다”와 같은 자연스러운 목적만 표현하도록 만드는 것입니다.
예시로, AWS CDK(Cloud Development Kit)의 특히 L2에서 Facade 개념이 잘 드러납니다.
AWS CDK 에서는 구문의 레벨에 따라 L1~L3을 나누어 제공합니다.
L1 구문은 low-level의 AWS 리소스 속성 정의를 완전히 제어해야 할 때 사용하도록 제공하고, L2 구문은 일반적으로 가장 널리 사용되는 구문 유형으로 제공합니다. 좀 더 직관적인 “의도 기반 API”으로써 상위 추상화를 제공하죠. 사용자는 속성, 권한, 리소스 간 이벤트 상호 작용 등을 더 간단하고 빠르게 정의할 수 있게 됩니다.
아래는 S3 Bucket을 만드는 코드 예시입니다.
// L1: CfnBucket은 CloudFormation의 AWS::S3::Bucket 리소스를 그대로 표현하는 L1 Construct입니다.
// - CloudFormation이 노출하는 속성을 거진 모두 드러내서 무엇이든 설정할 수 있다는 장점이 있습니다. 하지만 그만큼 직접 챙길 것도 많습니다.
new CfnBucket(this, "MyBucket", {
bucketName: "my-bucket",
versioningConfiguration: {
status: "Enabled",
},
publicAccessBlockConfiguration: {
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
},
});
// L2: Bucket은 S3 버킷을 “의도 기반”으로 만들 수 있게 감싼 L2 Construct(고수준 추상화)입니다.
// - 자주 쓰는 옵션을 간단한 프로퍼티(versioned 등)로 제공하고, 내부적으로 필요한 CloudFormation 설정으로 변환해줍니다.
// - 권한/알림/정책 같은 주변 구성도 더 자연스러운 API로 이어 붙일 수 있습니다(ex, bucket.grantReadWrite()).
new Bucket(this, "MyBucket", {
versioned: true,
});SDK를 Facade 패턴으로 설계하는 것 역시 같은 방향을 지향합니다. 기능 은닉이 목적이 아니라, 인지 부하를 줄이고 결합도를 낮추는 것이 목적입니다.
2. low-level vs high-level 인터페이스의 공존
여기서 한 가지 오해를 짚고 넘어가야 합니다.
Facade 패턴이 high-level API만 제공하는 것은 아니라는 것입니다. 좋은 SDK일수록, 저수준과 고수준 인터페이스가 함께 존재해요.
실무에서의 SDK 사용 패턴에서 파레토 법칙을 적용해볼 수 있습니다. *전체 사용 사례의 80%는 반복적인 공통 유즈케이스와 나머지 20%만이 특수한 요구사항
Facade는 이 80%를 책임지는 계층입니다. 80% 유즈케이스를 고수준 Facade로 간단하게 제공합니다. 20% 특수 케이스는 저수준 API로 탈출 가능하게끔 제공합니다.
이 구조는 단기적인 개발자 경험(DX, Developer Experience) 뿐 아니라 SDK의 장기적인 호환성과 확장성을 지켜줍니다.
3. 편의성과 유연성 사이 균형
물론 Facade 패턴이 모든 문제의 정답은 아닙니다. 추상화의 수준이 높아질수록 필연적으로 발생하는 트레이드오프가 존재합니다.
첫 번째로는 20% 이상의 저수준 API를 원할 때에 문제가 생깁니다. 세밀한 제어를 의도적으로 방지하였기 때문에, 특정 연결만 유지하고 나머지를 끊어야 하는 특수한 경우가 생길 경우에 제약이 크게 다가올 수 있습니다.
두 번째로는 저에게 발생하는 유지 비용입니다. 내부 로직이 복잡해질수록 SDK 내부에서는 오케스트레이션 로직을 더 정교하게 관리해야 합니다. SDK의 성숙도가 높아질수록 내부의 복잡도는 개발자가 떠안게 되는 것은 자연스러운 이치죠.
이러한 단점을 보완하기 위해 앞서 언급한 탈출구(Escape Hatch)가 존재해야 합니다. Facade가 제공하는 편리함에 안주하지 않고, 언제든 저수준 인터페이스로 내려가 세밀한 조작이 가능하도록 설계의 균형을 잡는 것이 필요합니다.
실제 코드로 보는 사례 분석
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3rd-party Plugin App │
│ │
│ const server = await sdk.start({ onConnection, onMessage }); │
│ ... │
│ await server.stop(); │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ [ High-level Interface ] : Facade Layer │
│ │
│ start() │
│ • 의도 기반 API: 80%의 공통 유즈케이스를 한 번에 처리 │
│ • 자동화된 오케스트레이션: 서버 오픈, 연결별 리스너 등록/해제를 내부에서 수행 │
│ • 안정성 보장: Clean-up 누락 등의 휴먼 에러를 방지하는 캡슐화 로직 작동 │
│ • 추상화: 내부의 원자적인 절차를 숨기고 통합된 핸들 반환 │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ [ Low-level ] │ │ [ Low-level ] │ │ [ Low-level ] │
│ Server │ │ Messaging │ │ Events │
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
│ • open(params) │ │ • send(params) │ │ • listenConnection │
│ • close(params) │ │ • disconnect(params)│ │ • listenMessage │
│ │ │ │ │ • listenError │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘1.
high-level의 메서드(start)와 low-level의 메서드(open, send, listen, close)를 동시에 제공합니다.
high-level 메서드인 start 메서드를 위한 개선을 계속 하더라도, low-level의 메서드는 안정적으로 유지가 가능하여 호환성 유지에 도움이 됩니다. (Escape Hatch)
2.
cleanup의 책임이 명확합니다.
connection 별 unlisten 동작을 Map으로 관리하고, disconnection 시 cleanup 수행 + map에서 제거하고, stop 메서드를 통해 전체 cleanup을 제어하고, 이는 unlistenConnection 메서드를 호출하고, 곧 close 메서드로 제어합니다.
3.
리소스 관리의 책임을 가져옵니다.
“리소스를 만든 곳에서 닫는다”는 단일 책임 원칙(SRP, Single Responsibility Principle)을 잘 보여줌으로써 SDK 안정성을 지켜냅니다.
이를 통해, 이벤트/리스너 누수를 방지하는 구조로 SDK를 설계할 수 있고, 사용자 실수로부터 일어나는 비효율적인 리소스 관리를 구조적으로 줄일 수 있습니다.
마치며
결국 SDK의 품질은 어떤 기능들을 제공하는가보다, 사용자가 그 기능을 어떤 형태로 쓰게 되느냐에 따라 갈립니다. 사용 형태를 고민하기 위해서, Facade 패턴에 기반하여 쓰기 쉽게 SDK 메서드와 인터페이스를 구성하는 것이 중요한 방법론이 되죠.
첫 번째로 DX를 개선하고, 두 번째로 성능과 안정성을 확보하며, 세 번째로 내부 구현이 바뀌어도 사용자 코드를 보호하는(Breaking Change를 막을 수 있는) 플랫폼의 발전을 가능하게 만듭니다.
설정의 조합인지, 80%의 유즈케이스인지를 사려 깊게 살펴보며 Facade에게 책임을 부여할지, low-level의 메서드를 유지할지 고민을 지속적으로 이어가야 합니다.
Toss Front의 외부 연동 생태계 역시 이런 신뢰 위에서 성장한다고 생각합니다.
앞으로도 사용자가 무엇을 하려는지 고민하며 그 의도에 맞는 안전한 SDK를 제공하도록 계속 다듬어가겠습니다.
감사합니다.
