🏢 토스 기술 스택 분석

39 min read
tossnodejsreact-nativekotlintypescriptawskubernetesfintechmobile-firstmicroservices

토스가 한국 금융을 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Node.js, React Native, Kotlin으로 만든 모바일 금융 플랫폼

🏢 토스 기술 스택 분석

간편송금으로 시작해 종합 금융 플랫폼으로 성장한 토스의 기술 스택을 심층 분석합니다.
"금융의 모든 것, 토스에서" - 기술로 금융을 재정의하는 혁신


📊 회사 개요

서비스 규모

  • 월간 활성 사용자: 1,400만+ 명
  • 월간 거래: 2억+ 건
  • 금융 서비스: 40+ 개
  • 연간 거래액: 70조원+
  • 직원: 1,800명+

엔지니어링 조직

  • 개발자: 600명+ (전체의 35%+)
  • 문화: "Move Fast, Think Deep"
  • 조직: 목적 조직 (Purpose Organization)
  • 기술 철학: 사용자 중심, 데이터 기반

기술적 도전과제

  1. 금융 규제 준수: 엄격한 보안/컴플라이언스
  2. 대규모 트래픽: 초당 수만 건 거래
  3. 무중단 서비스: 24/7 가용성
  4. 빠른 성장: 매년 2배 이상 성장

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│Mobile Client│────►│   API       │────►│  Service    │
│(React Native)     │  Gateway    │     │   Mesh      │
└─────────────┘     └─────────────┘     └─────────────┘
       │                    │                    │
       │              ┌─────▼─────┐       ┌─────▼─────┐
       │              │   Auth     │       │  Payment   │
       │              │  Service   │       │   Core     │
       │              └───────────┘       └───────────┘
       │                    │                    │
       └────────────────────┤                    │
                      ┌─────▼─────┐       ┌─────▼─────┐
                      │   Event    │       │    Data    │
                      │   Stream   │       │  Platform  │
                      └───────────┘       └───────────┘

핵심 설계 원칙

  1. Security First: 보안이 최우선
  2. Mobile First: 모바일 중심 설계
  3. Microservices: 서비스별 독립 배포
  4. Event Driven: 이벤트 기반 아키텍처
  5. 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만+

🎓 토스에서 배울 점

✅ 적용 가능한 패턴

  1. 간단한 UX: 복잡한 금융을 쉽게
  2. 빠른 반복: 주간 단위 릴리즈
  3. 데이터 기반: A/B 테스트 문화
  4. 보안 우선: 제로 트러스트
  5. 목적 조직: 자율과 책임

❌ 주의사항

  1. 규제 대응: 금융 규제 변화
  2. 보안 요구사항: 극도로 높은 기준
  3. 확장성: 급성장에 따른 부담
  4. 기술 부채: 빠른 성장의 부작용

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. 토스뱅크: 인터넷 전문은행
  2. 토스증권: 모바일 투자
  3. 토스페이: B2B 결제
  4. 해외 진출: 베트남, 일본
  5. Web3: 블록체인, 암호화폐

기술 투자 영역

  • AI/ML: 개인화 금융 서비스
  • 블록체인: DeFi, CBDC
  • 오픈뱅킹: API 플랫폼
  • 빅데이터: 신용평가 고도화
  • 클라우드: 멀티 클라우드

"누구나 쉽게 금융 서비스를 이용할 수 있는 세상을 만들어갑니다."

  • 토스 기술 조직

마지막 업데이트: 2025-01-28

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 배달의민족 기술 스택 분석

배달의민족이 한국 배달 문화를 혁신하며 동남아로 확장한 기술 스택 심층 분석 - Java, Spring, Kotlin으로 구축한 O2O 플랫폼

51 min read
baemin, woowa +10
Read more
🏢 company

🏢 Canva 기술 스택 분석

캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼

32 min read
canva, java +10
Read more
🏢 company

🏢 카카오 기술 스택 분석

카카오가 5천만 한국인의 일상을 연결하는 기술 스택 심층 분석 - Java, Kotlin, MySQL로 구축한 국민 메신저부터 슈퍼앱까지

32 min read
kakao, java +9
Read more
🏢 company

🏢 당근마켓 기술 스택 분석

당근마켓이 동네 커뮤니티를 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Ruby on Rails, Kotlin, Swift로 만든 하이퍼로컬 플랫폼

48 min read
karrot, daangn +9
Read more
🏢 company

🏢 Netflix 기술 스택 분석

넷플릭스가 전 세계 2.4억 사용자에게 스트리밍 서비스를 제공하는 기술 스택 심층 분석 - 마이크로서비스, AWS, Chaos Engineering의 교과서

9 min read
netflix, microservices +11
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.