🏢 토스 기술 스택 분석
토스가 한국 금융을 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Node.js, React Native, Kotlin으로 만든 모바일 금융 플랫폼
🏢 토스 기술 스택 분석
간편송금으로 시작해 종합 금융 플랫폼으로 성장한 토스의 기술 스택을 심층 분석합니다.
"금융의 모든 것, 토스에서" - 기술로 금융을 재정의하는 혁신
📊 회사 개요
서비스 규모
- 월간 활성 사용자: 1,400만+ 명
- 월간 거래: 2억+ 건
- 금융 서비스: 40+ 개
- 연간 거래액: 70조원+
- 직원: 1,800명+
엔지니어링 조직
- 개발자: 600명+ (전체의 35%+)
- 문화: "Move Fast, Think Deep"
- 조직: 목적 조직 (Purpose Organization)
- 기술 철학: 사용자 중심, 데이터 기반
기술적 도전과제
- 금융 규제 준수: 엄격한 보안/컴플라이언스
- 대규모 트래픽: 초당 수만 건 거래
- 무중단 서비스: 24/7 가용성
- 빠른 성장: 매년 2배 이상 성장
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │Mobile Client│────►│ API │────►│ Service │ │(React Native) │ Gateway │ │ Mesh │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────▼─────┐ ┌─────▼─────┐ │ │ Auth │ │ Payment │ │ │ Service │ │ Core │ │ └───────────┘ └───────────┘ │ │ │ └────────────────────┤ │ ┌─────▼─────┐ ┌─────▼─────┐ │ Event │ │ Data │ │ Stream │ │ Platform │ └───────────┘ └───────────┘ 핵심 설계 원칙
- Security First: 보안이 최우선
- Mobile First: 모바일 중심 설계
- Microservices: 서비스별 독립 배포
- Event Driven: 이벤트 기반 아키텍처
- Data Driven: 모든 결정은 데이터로
기술 진화
- 2015-2016: 간편송금 MVP
- 2017-2018: 마이크로서비스 전환
- 2019-2020: 토스뱅크, 토스증권
- 2021-2022: 토스페이먼츠 분사
- 2023-현재: 슈퍼앱화, 해외 진출
🧩 기술 스택 상세
Backend 기술
Node.js 기반 마이크로서비스
// 토스의 결제 처리 서비스 import { Injectable } from "@nestjs/common"; import { EventEmitter2 } from "@nestjs/event-emitter"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource, EntityManager } from "typeorm"; @Injectable() export class PaymentService { constructor( @InjectRepository(Payment) private paymentRepository: Repository<Payment>, private eventEmitter: EventEmitter2, private dataSource: DataSource, private fraudDetectionService: FraudDetectionService, private notificationService: NotificationService, ) {} async processPayment(request: PaymentRequest): Promise<PaymentResult> { // 분산 트랜잭션 시작 const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction("SERIALIZABLE"); try { // 1. 멱등성 체크 const existingPayment = await this.checkIdempotency( request.idempotencyKey, ); if (existingPayment) { return existingPayment; } // 2. 사기 탐지 const fraudCheck = await this.fraudDetectionService.analyze({ userId: request.userId, amount: request.amount, recipient: request.recipient, deviceInfo: request.deviceInfo, behaviorPattern: await this.getUserBehaviorPattern(request.userId), }); if (fraudCheck.isHighRisk) { await this.handleHighRiskTransaction(request, fraudCheck); throw new PaymentSecurityException("Security check required"); } // 3. 계좌 잔액 확인 및 보류 const account = await queryRunner.manager.findOne(Account, { where: { userId: request.userId }, lock: { mode: "pessimistic_write" }, }); if (account.balance.lessThan(request.amount)) { throw new InsufficientBalanceException(); } // 4. 결제 생성 const payment = await queryRunner.manager.save(Payment, { id: generateUUID(), userId: request.userId, amount: request.amount, currency: request.currency, recipient: request.recipient, status: PaymentStatus.PENDING, idempotencyKey: request.idempotencyKey, metadata: request.metadata, }); // 5. 잔액 차감 account.balance = account.balance.minus(request.amount); await queryRunner.manager.save(account); // 6. 외부 결제 처리 const externalResult = await this.processExternalPayment(payment); // 7. 결제 완료 처리 payment.status = PaymentStatus.COMPLETED; payment.externalId = externalResult.transactionId; payment.completedAt = new Date(); await queryRunner.manager.save(payment); // 8. 트랜잭션 커밋 await queryRunner.commitTransaction(); // 9. 이벤트 발행 await this.eventEmitter.emitAsync("payment.completed", { paymentId: payment.id, userId: payment.userId, amount: payment.amount, timestamp: payment.completedAt, }); // 10. 알림 발송 await this.notificationService.sendPaymentNotification(payment); return { paymentId: payment.id, status: payment.status, amount: payment.amount, completedAt: payment.completedAt, }; } catch (error) { await queryRunner.rollbackTransaction(); // 보상 트랜잭션 await this.compensateFailedPayment(request, error); throw error; } finally { await queryRunner.release(); } } // 실시간 사기 탐지 private async getUserBehaviorPattern( userId: string, ): Promise<BehaviorPattern> { const recentTransactions = await this.paymentRepository.find({ where: { userId }, order: { createdAt: "DESC" }, take: 100, }); return { averageAmount: this.calculateAverage(recentTransactions), frequency: this.calculateFrequency(recentTransactions), commonRecipients: this.extractCommonRecipients(recentTransactions), unusualPatterns: await this.detectAnomalies(recentTransactions), }; } // 분산 락을 이용한 동시성 제어 async transferMoney( fromUserId: string, toUserId: string, amount: Money, ): Promise<TransferResult> { // Redis 분산 락 획득 const lockKeys = [`account:${fromUserId}`, `account:${toUserId}`].sort(); // 데드락 방지를 위한 정렬 const locks = await Promise.all( lockKeys.map((key) => this.redisLock.acquire(key, 5000)), ); try { // 트랜잭션 실행 return await this.executeTransfer(fromUserId, toUserId, amount); } finally { // 락 해제 await Promise.all(locks.map((lock) => lock.release())); } } } // 이벤트 기반 데이터 동기화 @Injectable() export class DataSyncService { constructor( private readonly kafkaProducer: KafkaProducer, private readonly elasticsearchService: ElasticsearchService, ) {} @OnEvent("payment.completed") async handlePaymentCompleted(event: PaymentCompletedEvent) { // 1. 데이터 웨어하우스 동기화 await this.kafkaProducer.send({ topic: "payment-events", messages: [ { key: event.paymentId, value: JSON.stringify({ eventType: "PAYMENT_COMPLETED", timestamp: event.timestamp, data: event, }), }, ], }); // 2. 검색 인덱스 업데이트 await this.elasticsearchService.index({ index: "payments", id: event.paymentId, body: { userId: event.userId, amount: event.amount, timestamp: event.timestamp, searchableText: this.generateSearchableText(event), }, }); // 3. 실시간 대시보드 업데이트 await this.updateRealtimeDashboard(event); } } Kotlin 기반 금융 코어
// 토스뱅크 계좌 시스템 @Service @Transactional class AccountService( private val accountRepository: AccountRepository, private val transactionRepository: TransactionRepository, private val eventPublisher: ApplicationEventPublisher, private val auditService: AuditService ) { suspend fun createAccount(request: CreateAccountRequest): Account { // 본인 인증 확인 val identity = verifyIdentity(request.userId) require(identity.isVerified) { "Identity verification required" } // 중복 계좌 체크 val existingAccount = accountRepository.findByUserId(request.userId) if (existingAccount != null) { throw DuplicateAccountException("Account already exists") } // 계좌 생성 val account = Account( id = generateAccountId(), userId = request.userId, accountNumber = generateAccountNumber(), balance = Money.ZERO, currency = Currency.KRW, status = AccountStatus.ACTIVE, type = request.accountType, createdAt = Instant.now() ) // 저장 val savedAccount = accountRepository.save(account) // 이벤트 발행 eventPublisher.publishEvent( AccountCreatedEvent( accountId = savedAccount.id, userId = savedAccount.userId, accountNumber = savedAccount.accountNumber ) ) // 감사 로그 auditService.log( AuditLog( action = "ACCOUNT_CREATED", entityId = savedAccount.id, userId = request.userId, details = mapOf( "accountType" to request.accountType, "accountNumber" to savedAccount.accountNumber ) ) ) return savedAccount } suspend fun deposit(accountId: String, amount: Money): Transaction { return withTimeout(5000) { // 5초 타임아웃 val account = accountRepository.findByIdWithLock(accountId) ?: throw AccountNotFoundException(accountId) // 입금 한도 체크 val dailyDeposit = transactionRepository .sumDailyDeposits(accountId, LocalDate.now()) if (dailyDeposit + amount > DAILY_DEPOSIT_LIMIT) { throw DepositLimitExceededException( "Daily deposit limit exceeded" ) } // 잔액 업데이트 val previousBalance = account.balance account.balance = account.balance + amount accountRepository.save(account) // 거래 내역 생성 val transaction = Transaction( id = generateTransactionId(), accountId = accountId, type = TransactionType.DEPOSIT, amount = amount, balanceBefore = previousBalance, balanceAfter = account.balance, description = "Deposit", status = TransactionStatus.COMPLETED, createdAt = Instant.now() ) transactionRepository.save(transaction) // 실시간 알림 sendDepositNotification(account.userId, amount) transaction } } // 이체 트랜잭션 @Transactional(isolation = Isolation.SERIALIZABLE) suspend fun transfer( fromAccountId: String, toAccountId: String, amount: Money, description: String? = null ): TransferResult = coroutineScope { // 계좌 조회 (순서 정렬로 데드락 방지) val accountIds = listOf(fromAccountId, toAccountId).sorted() val accounts = accountRepository.findByIdsWithLock(accountIds) val fromAccount = accounts.find { it.id == fromAccountId } ?: throw AccountNotFoundException(fromAccountId) val toAccount = accounts.find { it.id == toAccountId } ?: throw AccountNotFoundException(toAccountId) // 잔액 확인 if (fromAccount.balance < amount) { throw InsufficientBalanceException( "Insufficient balance: ${fromAccount.balance}" ) } // 이체 한도 확인 checkTransferLimit(fromAccount, amount) // 잔액 이동 fromAccount.balance -= amount toAccount.balance += amount // 저장 accountRepository.saveAll(listOf(fromAccount, toAccount)) // 거래 내역 생성 (병렬 처리) val (withdrawalTx, depositTx) = awaitAll( async { createTransaction( fromAccount, TransactionType.WITHDRAWAL, -amount, description ?: "Transfer to ${toAccount.accountNumber}" ) }, async { createTransaction( toAccount, TransactionType.DEPOSIT, amount, description ?: "Transfer from ${fromAccount.accountNumber}" ) } ) TransferResult( transferId = generateTransferId(), fromTransaction = withdrawalTx, toTransaction = depositTx, amount = amount, completedAt = Instant.now() ) } } // 실시간 거래 모니터링 @Component class TransactionMonitor( private val redisTemplate: RedisTemplate<String, String>, private val alertService: AlertService ) { private val suspiciousPatterns = listOf( // 연속된 소액 이체 Pattern { transactions -> transactions.size >= 10 && transactions.all { it.amount < Money(10000, Currency.KRW) } && transactions.distinctBy { it.toAccountId }.size == 1 }, // 새벽 시간 대량 이체 Pattern { transactions -> val nightTransactions = transactions.filter { val hour = it.createdAt.atZone(ZoneId.of("Asia/Seoul")).hour hour in 0..6 } nightTransactions.sumOf { it.amount } > Money(10000000, Currency.KRW) }, // 급격한 패턴 변화 Pattern { transactions -> val recent = transactions.take(10) val historical = transactions.drop(10).take(90) val recentAvg = recent.map { it.amount }.average() val historicalAvg = historical.map { it.amount }.average() recentAvg > historicalAvg * 10 } ) @EventListener fun onTransaction(event: TransactionEvent) { runBlocking { // 최근 거래 내역 조회 val recentTransactions = getRecentTransactions(event.userId, 100) // 패턴 분석 val detectedPatterns = suspiciousPatterns .filter { pattern -> pattern.matches(recentTransactions) } if (detectedPatterns.isNotEmpty()) { // 위험도 계산 val riskScore = calculateRiskScore(detectedPatterns, recentTransactions) if (riskScore > 0.7) { // 거래 차단 blockUserTransactions(event.userId) // 알림 발송 alertService.sendHighRiskAlert( userId = event.userId, riskScore = riskScore, patterns = detectedPatterns ) } } } } } Mobile 기술 (React Native)
크로스 플랫폼 앱
// 토스 React Native 앱 import React, { useEffect, useState } from "react"; import { View, Text, TouchableOpacity, Animated, PanResponder, } from "react-native"; import BiometricAuth from "react-native-biometrics"; import { useRecoilState } from "recoil"; // 생체 인증 Hook export const useBiometricAuth = () => { const [isAuthenticated, setIsAuthenticated] = useRecoilState(authState); const authenticate = async (): Promise<boolean> => { try { const { available, biometryType } = await BiometricAuth.isSensorAvailable(); if (!available) { throw new Error("Biometric authentication not available"); } const { success, signature } = await BiometricAuth.createSignature({ promptMessage: "토스 앱 잠금해제", cancelButtonText: "취소", }); if (success) { // 서버 검증 const verified = await verifyBiometricSignature(signature); setIsAuthenticated(verified); return verified; } return false; } catch (error) { console.error("Biometric auth failed:", error); return false; } }; return { authenticate, isAuthenticated }; }; // 송금 화면 컴포넌트 export const TransferScreen: React.FC = () => { const [amount, setAmount] = useState(""); const [recipient, setRecipient] = useState<User | null>(null); const slideAnim = useRef(new Animated.Value(0)).current; const { authenticate } = useBiometricAuth(); // 스와이프하여 송금 제스처 const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (_, gestureState) => { if (gestureState.dx > 0) { slideAnim.setValue(gestureState.dx); } }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dx > 200) { // 송금 실행 executeTransfer(); } else { // 원위치로 Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, }).start(); } }, }) ).current; const executeTransfer = async () => { try { // 생체 인증 const authenticated = await authenticate(); if (!authenticated) { showError("인증에 실패했습니다"); return; } // 송금 API 호출 const result = await transferAPI.send({ toUserId: recipient.id, amount: parseAmount(amount), currency: "KRW", }); // 성공 애니메이션 await showSuccessAnimation(); // 결과 화면으로 이동 navigation.navigate("TransferComplete", { result }); } catch (error) { handleTransferError(error); } }; return ( <SafeAreaView style={styles.container}> <TransferHeader recipient={recipient} /> <AmountInput value={amount} onChange={setAmount} currency="KRW" /> <View style={styles.swipeContainer}> <Animated.View {...panResponder.panHandlers} style={[ styles.swipeButton, { transform: [{ translateX: slideAnim }], }, ]} > <Text style={styles.swipeText}>밀어서 송금</Text> </Animated.View> </View> <RecentTransfers onSelectRecipient={setRecipient} /> </SafeAreaView> ); }; // 실시간 자산 현황 export const AssetDashboard: React.FC = () => { const [assets, setAssets] = useState<Asset[]>([]); const [totalValue, setTotalValue] = useState(0); useEffect(() => { // WebSocket 연결로 실시간 업데이트 const ws = new WebSocket("wss://api.toss.im/assets/stream"); ws.onmessage = (event) => { const update = JSON.parse(event.data); switch (update.type) { case "BALANCE_CHANGE": updateAssetBalance(update.assetId, update.newBalance); break; case "PRICE_CHANGE": updateAssetPrice(update.assetId, update.newPrice); break; case "NEW_TRANSACTION": addTransaction(update.transaction); break; } }; return () => ws.close(); }, []); return ( <ScrollView style={styles.dashboard}> <TotalAssetCard value={totalValue} /> <SectionTitle>내 자산</SectionTitle> {assets.map((asset) => ( <AssetCard key={asset.id} asset={asset} onPress={() => navigateToAssetDetail(asset)} /> ))} <SectionTitle>투자 상품</SectionTitle> <InvestmentProducts /> <InsightCard title="이번 달 소비 분석" insight={generateSpendingInsight()} /> </ScrollView> ); }; // 보안 모듈 class SecurityModule { private static instance: SecurityModule; private jailbreakDetected = false; private appTampered = false; static getInstance(): SecurityModule { if (!this.instance) { this.instance = new SecurityModule(); } return this.instance; } async initialize() { // 탈옥/루팅 탐지 this.jailbreakDetected = await this.detectJailbreak(); // 앱 무결성 검증 this.appTampered = await this.verifyAppIntegrity(); // 보안 위협 감지 시 종료 if (this.jailbreakDetected || this.appTampered) { Alert.alert("보안 경고", "보안 위험이 감지되어 앱을 종료합니다.", [ { text: "확인", onPress: () => RNExitApp.exitApp() }, ]); } // SSL Pinning 설정 await this.setupSSLPinning(); // 화면 캡처 방지 this.preventScreenCapture(); } private async detectJailbreak(): Promise<boolean> { // iOS if (Platform.OS === "ios") { const suspiciousPaths = [ "/Applications/Cydia.app", "/Library/MobileSubstrate/MobileSubstrate.dylib", "/bin/bash", "/usr/sbin/sshd", "/etc/apt", ]; for (const path of suspiciousPaths) { if (await RNFS.exists(path)) { return true; } } } // Android if (Platform.OS === "android") { const rootBinaries = [ "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", ]; for (const binary of rootBinaries) { if (await RNFS.exists(binary)) { return true; } } } return false; } } 데이터 플랫폼
실시간 분석 시스템
# 토스 데이터 파이프라인 from pyspark.sql import SparkSession from pyspark.sql.functions import * from pyspark.sql.types import * import mlflow class TossDataPipeline: def __init__(self): self.spark = SparkSession.builder \ .appName("TossRealtimeAnalytics") \ .config("spark.sql.adaptive.enabled", "true") \ .config("spark.sql.adaptive.coalescePartitions.enabled", "true") \ .getOrCreate() def process_transaction_stream(self): # Kafka 스트림 읽기 transaction_stream = self.spark \ .readStream \ .format("kafka") \ .option("kafka.bootstrap.servers", "kafka-cluster:9092") \ .option("subscribe", "transactions") \ .option("startingOffsets", "latest") \ .load() # 스키마 정의 transaction_schema = StructType([ StructField("transaction_id", StringType()), StructField("user_id", StringType()), StructField("amount", DoubleType()), StructField("currency", StringType()), StructField("type", StringType()), StructField("timestamp", TimestampType()), StructField("merchant_id", StringType()), StructField("category", StringType()) ]) # 파싱 및 변환 parsed_stream = transaction_stream \ .select(from_json(col("value").cast("string"), transaction_schema).alias("data")) \ .select("data.*") \ .withWatermark("timestamp", "10 minutes") # 실시간 집계 # 1. 사용자별 지출 패턴 user_spending = parsed_stream \ .groupBy( window("timestamp", "1 hour"), "user_id", "category" ) \ .agg( sum("amount").alias("total_spent"), count("transaction_id").alias("transaction_count"), avg("amount").alias("avg_transaction") ) # 2. 이상 거래 탐지 anomaly_detection = parsed_stream \ .groupBy( window("timestamp", "5 minutes"), "user_id" ) \ .agg( sum("amount").alias("total_amount"), count("transaction_id").alias("count"), stddev("amount").alias("amount_stddev") ) \ .filter( (col("total_amount") > 10000000) | # 1천만원 이상 (col("count") > 50) | # 5분간 50건 이상 (col("amount_stddev") > 1000000) # 표준편차 100만원 이상 ) # 결과 저장 # 1. 실시간 대시보드용 Redis user_spending.writeStream \ .foreachBatch(lambda df, epoch_id: self.write_to_redis(df, "user_spending")) \ .outputMode("update") \ .start() # 2. 이상 거래 알림 anomaly_detection.writeStream \ .foreachBatch(lambda df, epoch_id: self.send_anomaly_alerts(df)) \ .outputMode("update") \ .start() # 3. 데이터 웨어하우스 저장 parsed_stream.writeStream \ .format("delta") \ .option("checkpointLocation", "/checkpoints/transactions") \ .option("path", "/data/warehouse/transactions") \ .partitionBy("timestamp") \ .start() self.spark.streams.awaitAnyTermination() def train_fraud_detection_model(self): # 학습 데이터 로드 training_data = self.spark.read \ .format("delta") \ .load("/data/warehouse/transactions") \ .filter(col("label").isNotNull()) # 특징 엔지니어링 feature_data = training_data \ .withColumn("hour_of_day", hour("timestamp")) \ .withColumn("day_of_week", dayofweek("timestamp")) \ .withColumn("amount_log", log(col("amount") + 1)) \ .withColumn("user_avg_amount", avg("amount").over(Window.partitionBy("user_id"))) \ .withColumn("amount_deviation", col("amount") - col("user_avg_amount")) # ML 파이프라인 from pyspark.ml import Pipeline from pyspark.ml.feature import VectorAssembler, StandardScaler from pyspark.ml.classification import GBTClassifier assembler = VectorAssembler( inputCols=[ "amount_log", "hour_of_day", "day_of_week", "amount_deviation", "transaction_count_1h" ], outputCol="features" ) scaler = StandardScaler( inputCol="features", outputCol="scaled_features" ) gbt = GBTClassifier( featuresCol="scaled_features", labelCol="is_fraud", maxDepth=5, maxIter=100 ) pipeline = Pipeline(stages=[assembler, scaler, gbt]) # 모델 학습 model = pipeline.fit(feature_data) # MLflow로 모델 저장 with mlflow.start_run(): mlflow.spark.log_model(model, "fraud_detection_model") mlflow.log_metric("auc", 0.95) mlflow.log_metric("precision", 0.92) mlflow.log_metric("recall", 0.89) return model 기술 스택 요약:
- Backend: Node.js (TypeScript), Kotlin, Python
- Frontend: React Native, React
- 데이터베이스: PostgreSQL, MongoDB, Redis
- 메시지큐: Kafka, RabbitMQ
- 인프라: AWS, Kubernetes
- 데이터: Spark, Airflow, Delta Lake
- 모니터링: Datadog, Sentry
- 보안: Vault, AWS KMS
💡 핵심 기술 인사이트
1. 모바일 퍼스트 전략
- React Native로 빠른 개발
- 네이티브 수준 성능
- 일관된 사용자 경험
2. 마이크로서비스 아키텍처
- 서비스별 독립 배포
- 장애 격리
- 빠른 개발 속도
3. 실시간 데이터 처리
- 스트리밍 아키텍처
- 실시간 이상 탐지
- 즉각적인 인사이트
📈 성과 & 지표
기술적 성과
- 앱 구동 시간: < 1초
- API 응답시간: P50 < 100ms
- 시스템 가용성: 99.99%
- 일일 배포: 50+ 회
- 장애 복구시간: < 5분
비즈니스 영향
- MAU: 1,400만+ (한국 경제활동인구 50%+)
- 누적 송금액: 180조원+
- 기업 가치: 8조원+ (유니콘)
- 금융상품 가입자: 500만+
🎓 토스에서 배울 점
✅ 적용 가능한 패턴
- 간단한 UX: 복잡한 금융을 쉽게
- 빠른 반복: 주간 단위 릴리즈
- 데이터 기반: A/B 테스트 문화
- 보안 우선: 제로 트러스트
- 목적 조직: 자율과 책임
❌ 주의사항
- 규제 대응: 금융 규제 변화
- 보안 요구사항: 극도로 높은 기준
- 확장성: 급성장에 따른 부담
- 기술 부채: 빠른 성장의 부작용
📚 추천 리소스
🔮 미래 전망
현재 집중 분야
- 토스뱅크: 인터넷 전문은행
- 토스증권: 모바일 투자
- 토스페이: B2B 결제
- 해외 진출: 베트남, 일본
- Web3: 블록체인, 암호화폐
기술 투자 영역
- AI/ML: 개인화 금융 서비스
- 블록체인: DeFi, CBDC
- 오픈뱅킹: API 플랫폼
- 빅데이터: 신용평가 고도화
- 클라우드: 멀티 클라우드
"누구나 쉽게 금융 서비스를 이용할 수 있는 세상을 만들어갑니다."
- 토스 기술 조직
마지막 업데이트: 2025-01-28