🏢 네이버 기술 스택 분석

34 min read
naverjavaspringhadoophbaseelasticsearchkubernetesarcuspinpointdeviewsearch-enginebig-data

네이버가 대한민국 최대 검색 포털에서 글로벌 기술 기업으로 성장한 기술 스택 심층 분석 - Java, Spring, Hadoop으로 구축한 초대규모 플랫폼

🏢 네이버 기술 스택 분석

한국 최대 검색 포털에서 글로벌 기술 플랫폼으로 진화한 네이버의 기술 스택을 심층 분석합니다.
"기술 플랫폼의 경계를 넘어" - 검색부터 AI까지 모든 것을 아우르는 기술력


📊 회사 개요

서비스 규모

  • 일일 활성 사용자: 3,000만+ 명
  • 월간 검색: 20억+ 건
  • 서비스: 50+ 개 (검색, 쇼핑, 페이, 웹툰, 클라우드 등)
  • 데이터 저장량: 100PB+
  • 분당 요청: 1,000만+ 건

엔지니어링 조직

  • 개발자: 3,000명+ (전체 직원 4,500+)
  • 문화: "기술이 미래를 만든다"
  • R&D 센터: 춘천, 대전, 일본, 베트남
  • 오픈소스: 200+ 프로젝트 공개

기술적 도전과제

  1. 초대규모 검색: 수십억 웹 페이지 인덱싱
  2. 실시간 처리: 뉴스, 실시간 검색어
  3. AI/ML: 한국어 자연어 처리
  4. 글로벌 확장: 일본, 동남아 서비스

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Search    │────►│    Query    │────►│   Index     │
│   Frontend  │     │  Processor  │     │   Cluster   │
└─────────────┘     └─────────────┘     └─────────────┘
       │                    │                    │
       │              ┌─────▼─────┐       ┌─────▼─────┐
       │              │   Cache    │       │   Hadoop   │
       │              │  (Arcus)   │       │   Cluster  │
       │              └───────────┘       └───────────┘
       │                    │                    │
       └────────────────────┤                    │
                      ┌─────▼─────┐       ┌─────▼─────┐
                      │   API      │       │   ML       │
                      │  Gateway   │       │  Platform  │
                      └───────────┘       └───────────┘

핵심 설계 원칙

  1. Scalability First: 무한 확장 가능
  2. High Availability: 24/7 무중단
  3. Data-Driven: 모든 결정은 데이터로
  4. Open Innovation: 오픈소스 우선
  5. Global Standard: 글로벌 수준 기술

기술 진화

  • 1999-2005: 검색 엔진 개발
  • 2006-2010: 플랫폼화, 오픈 API
  • 2011-2015: 빅데이터, 클라우드
  • 2016-2020: AI/ML, 글로벌 확장
  • 2021-현재: 초거대 AI, 메타버스

🧩 기술 스택 상세

검색 엔진 기술

대규모 검색 시스템

// 네이버 검색 엔진 핵심 (Java)
@Component
public class NaverSearchEngine {

    private final IndexManager indexManager;
    private final QueryProcessor queryProcessor;
    private final RankingEngine rankingEngine;
    private final CacheManager cacheManager;

    @GetMapping("/search")
    public SearchResult search(
            @RequestParam String query,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "unified") String collection) {

        // 1. 쿼리 정규화 및 분석
        NormalizedQuery normalized = queryProcessor.normalize(query);

        // 캐시 확인
        String cacheKey = generateCacheKey(normalized, page, collection);
        SearchResult cached = cacheManager.get(cacheKey);
        if (cached != null && !isRealTimeQuery(normalized)) {
            return cached;
        }

        // 2. 쿼리 분석
        QueryAnalysis analysis = analyzeQuery(normalized);

        // 3. 컬렉션별 검색
        List<CollectionResult> results = new ArrayList<>();

        if (collection.equals("unified")) {
            // 통합검색
            results = searchAllCollections(analysis);
        } else {
            // 특정 컬렉션 검색
            results.add(searchCollection(collection, analysis));
        }

        // 4. 랭킹 및 정렬
        SearchResult finalResult = rankingEngine.rank(results, analysis);

        // 5. 캐시 저장
        if (!isRealTimeQuery(normalized)) {
            cacheManager.put(cacheKey, finalResult, getTTL(collection));
        }

        return finalResult;
    }

    private QueryAnalysis analyzeQuery(NormalizedQuery query) {
        QueryAnalysis analysis = new QueryAnalysis();

        // 형태소 분석
        List<Token> tokens = morphAnalyzer.analyze(query.getText());
        analysis.setTokens(tokens);

        // 의도 분석
        Intent intent = intentClassifier.classify(query, tokens);
        analysis.setIntent(intent);

        // 엔티티 추출
        List<Entity> entities = entityExtractor.extract(tokens);
        analysis.setEntities(entities);

        // 쿼리 카테고리 분류
        Category category = categoryClassifier.classify(query, entities);
        analysis.setCategory(category);

        return analysis;
    }

    private List<CollectionResult> searchAllCollections(QueryAnalysis analysis) {
        // 병렬 검색 실행
        CompletableFuture<CollectionResult> webFuture =
            CompletableFuture.supplyAsync(() -> searchWeb(analysis));
        CompletableFuture<CollectionResult> newsFuture =
            CompletableFuture.supplyAsync(() -> searchNews(analysis));
        CompletableFuture<CollectionResult> blogFuture =
            CompletableFuture.supplyAsync(() -> searchBlog(analysis));
        CompletableFuture<CollectionResult> shoppingFuture =
            CompletableFuture.supplyAsync(() -> searchShopping(analysis));

        // 결과 수집
        return Stream.of(webFuture, newsFuture, blogFuture, shoppingFuture)
            .map(CompletableFuture::join)
            .filter(result -> result.getCount() > 0)
            .collect(Collectors.toList());
    }
}

// 분산 인덱싱 시스템
@Service
public class DistributedIndexer {

    private final HBaseClient hbaseClient;
    private final ElasticsearchClient esClient;
    private final KafkaProducer<String, IndexDocument> producer;

    public void indexDocument(Document document) {
        // 1. 문서 전처리
        ProcessedDocument processed = preprocessDocument(document);

        // 2. 분산 저장
        // HBase에 원본 저장
        hbaseClient.put(
            "documents",
            document.getId(),
            processed.toHBaseRow()
        );

        // 3. 역인덱스 생성
        InvertedIndex invertedIndex = createInvertedIndex(processed);

        // 4. Elasticsearch 인덱싱
        esClient.index(
            "web_index",
            document.getId(),
            invertedIndex.toElasticsearchDocument()
        );

        // 5. 실시간 업데이트 전파
        producer.send(new ProducerRecord<>(
            "index-updates",
            document.getId(),
            new IndexDocument(document.getId(), processed, invertedIndex)
        ));
    }

    private InvertedIndex createInvertedIndex(ProcessedDocument doc) {
        InvertedIndex index = new InvertedIndex();

        // 토큰화 및 위치 정보 저장
        Map<String, List<Position>> tokenPositions = new HashMap<>();

        List<Token> tokens = tokenizer.tokenize(doc.getContent());
        for (int i = 0; i < tokens.size(); i++) {
            Token token = tokens.get(i);
            tokenPositions.computeIfAbsent(token.getValue(), k -> new ArrayList<>())
                .add(new Position(i, token.getOffset(), token.getLength()));
        }

        // TF-IDF 계산
        for (Map.Entry<String, List<Position>> entry : tokenPositions.entrySet()) {
            String term = entry.getKey();
            List<Position> positions = entry.getValue();

            double tf = (double) positions.size() / tokens.size();
            double idf = Math.log(totalDocuments / getDocumentFrequency(term));

            index.addTerm(term, positions, tf * idf);
        }

        return index;
    }
}

실시간 검색어 처리

// 실시간 급상승 검색어 시스템
@Component
public class RealtimeSearchTrendAnalyzer {

    private final RedisTemplate<String, SearchQuery> redisTemplate;
    private final TimeSeriesDB tsdb;

    // 검색 쿼리 수집
    @EventListener
    public void collectSearchQuery(SearchEvent event) {
        String query = event.getQuery();
        long timestamp = event.getTimestamp();

        // 시간대별 집계
        String minuteKey = getMinuteKey(timestamp);
        redisTemplate.opsForZSet().incrementScore(
            minuteKey,
            query,
            1.0
        );

        // 시계열 데이터 저장
        tsdb.addPoint(
            "search.queries",
            timestamp,
            1.0,
            Tags.of("query", query)
        );
    }

    // 급상승 검색어 계산
    @Scheduled(fixedDelay = 60000) // 1분마다
    public void calculateTrends() {
        long now = System.currentTimeMillis();

        // 최근 10분 vs 이전 10분 비교
        Map<String, Double> recent = getQueryCounts(now - 10 * MINUTE, now);
        Map<String, Double> previous = getQueryCounts(now - 20 * MINUTE, now - 10 * MINUTE);

        List<TrendingQuery> trends = new ArrayList<>();

        for (Map.Entry<String, Double> entry : recent.entrySet()) {
            String query = entry.getKey();
            double recentCount = entry.getValue();
            double previousCount = previous.getOrDefault(query, 1.0);

            // 증가율 계산
            double growthRate = (recentCount - previousCount) / previousCount;

            // 급상승 조건: 200% 이상 증가 & 최소 검색수 100회
            if (growthRate > 2.0 && recentCount > 100) {
                trends.add(new TrendingQuery(
                    query,
                    recentCount,
                    growthRate,
                    calculateHotScore(recentCount, growthRate, now)
                ));
            }
        }

        // 상위 20개 선정
        trends.sort((a, b) -> Double.compare(b.getHotScore(), a.getHotScore()));
        List<TrendingQuery> top20 = trends.stream()
            .limit(20)
            .collect(Collectors.toList());

        // 결과 저장 및 전파
        publishTrends(top20);
    }

    private double calculateHotScore(double count, double growth, long timestamp) {
        // Newton's law of cooling 응용
        long age = System.currentTimeMillis() - timestamp;
        double timeFactor = Math.exp(-age / (5.0 * MINUTE));

        return count * growth * timeFactor;
    }
}

빅데이터 플랫폼

Hadoop 기반 데이터 처리

// 네이버 C3 (Cloud Computing Center) 플랫폼
public class NaverBigDataPlatform {

    // MapReduce Job for 로그 분석
    public static class LogAnalysisMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

        private final static IntWritable one = new IntWritable(1);
        private Text word = new Text();

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {

            // 로그 파싱
            LogEntry entry = LogParser.parse(value.toString());

            // 검색 쿼리 추출
            if (entry.getType() == LogType.SEARCH) {
                String query = entry.getQuery();

                // 형태소 분석
                List<String> tokens = MorphAnalyzer.analyze(query);

                for (String token : tokens) {
                    word.set(token);
                    context.write(word, one);
                }
            }

            // 사용자 행동 분석
            String userId = entry.getUserId();
            String action = entry.getAction();

            context.write(new Text(userId + ":" + action), one);
        }
    }

    public static class LogAnalysisReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context)
                throws IOException, InterruptedException {

            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }

            context.write(key, new IntWritable(sum));
        }
    }

    // Spark Streaming for 실시간 분석
    public void runRealtimeAnalysis() {
        SparkConf conf = new SparkConf()
            .setAppName("NaverRealtimeAnalysis")
            .set("spark.streaming.kafka.maxRatePerPartition", "10000");

        JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(10));

        // Kafka 스트림 생성
        JavaPairInputDStream<String, String> messages = KafkaUtils.createDirectStream(
            jssc,
            String.class,
            String.class,
            StringDecoder.class,
            StringDecoder.class,
            kafkaParams,
            topicsSet
        );

        // 실시간 로그 처리
        JavaDStream<LogEntry> logs = messages
            .map(tuple -> LogParser.parse(tuple._2()));

        // 윈도우 기반 집계
        JavaPairDStream<String, Integer> searchCounts = logs
            .filter(log -> log.getType() == LogType.SEARCH)
            .mapToPair(log -> new Tuple2<>(log.getQuery(), 1))
            .reduceByKeyAndWindow(
                (a, b) -> a + b,
                Durations.minutes(10),
                Durations.minutes(1)
            );

        // HBase에 저장
        searchCounts.foreachRDD(rdd -> {
            rdd.foreachPartition(partition -> {
                HBaseConnection conn = HBaseConnectionPool.getConnection();
                Table table = conn.getTable(TableName.valueOf("search_trends"));

                partition.forEachRemaining(record -> {
                    Put put = new Put(Bytes.toBytes(record._1()));
                    put.addColumn(
                        Bytes.toBytes("counts"),
                        Bytes.toBytes("value"),
                        Bytes.toBytes(record._2())
                    );
                    table.put(put);
                });

                table.close();
            });
        });

        jssc.start();
        jssc.awaitTermination();
    }
}

캐싱 시스템 (Arcus)

분산 캐시 구현

// 네이버가 개발한 Arcus (memcached 클러스터)
@Component
public class ArcusCache {

    private final ArcusClient arcusClient;

    @PostConstruct
    public void init() {
        // Arcus 클라이언트 초기화
        ConnectionFactoryBuilder cfb = new ConnectionFactoryBuilder();
        cfb.setProtocol(Protocol.TEXT);
        cfb.setFailureMode(FailureMode.Cancel);

        arcusClient = ArcusClient.createArcusClient(
            "naver-search-cache",
            new ZKConnectionFactory(zkAddress),
            cfb
        );
    }

    public <T> T get(String key, Class<T> type) {
        try {
            Future<Object> future = arcusClient.asyncGet(key);
            Object value = future.get(100, TimeUnit.MILLISECONDS);

            if (value != null) {
                return objectMapper.convertValue(value, type);
            }
        } catch (TimeoutException e) {
            // 캐시 타임아웃 - 통과
            log.debug("Cache timeout for key: {}", key);
        } catch (Exception e) {
            log.error("Cache error for key: {}", key, e);
        }

        return null;
    }

    public void set(String key, Object value, int ttl) {
        try {
            String json = objectMapper.writeValueAsString(value);
            arcusClient.set(key, ttl, json);
        } catch (Exception e) {
            log.error("Failed to set cache for key: {}", key, e);
        }
    }

    // Collection 타입 지원
    public void addToList(String key, Object value) {
        CollectionAttributes attrs = new CollectionAttributes();
        attrs.setMaxCount(1000);
        attrs.setExpireTime(3600);

        arcusClient.asyncLopInsert(
            key,
            -1, // tail insert
            objectMapper.writeValueAsString(value),
            attrs
        );
    }

    public List<String> getList(String key, int count) {
        CollectionFuture<List<Object>> future =
            arcusClient.asyncLopGet(key, 0, count, false, false);

        try {
            List<Object> result = future.get(100, TimeUnit.MILLISECONDS);
            return result.stream()
                .map(Object::toString)
                .collect(Collectors.toList());
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }
}

AI/ML 플랫폼

CLOVA AI 시스템

# 네이버 CLOVA 자연어 처리
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer

class ClovaNLPEngine:
    def __init__(self):
        # 한국어 특화 BERT 모델
        self.tokenizer = BertTokenizer.from_pretrained('naver/kobert')
        self.model = KoBertForSequenceClassification.from_pretrained('naver/kobert')

        # GPU 사용
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

    def analyze_intent(self, query: str) -> IntentResult:
        # 토큰화
        inputs = self.tokenizer(
            query,
            return_tensors='pt',
            padding=True,
            truncation=True,
            max_length=128
        )

        # 의도 분류
        with torch.no_grad():
            outputs = self.model(**inputs.to(self.device))
            predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)

        # 상위 의도 추출
        top_intents = []
        for idx, prob in enumerate(predictions[0]):
            if prob > 0.1:  # 10% 이상 확률
                top_intents.append({
                    'intent': self.intent_labels[idx],
                    'confidence': float(prob)
                })

        return IntentResult(
            query=query,
            intents=sorted(top_intents, key=lambda x: x['confidence'], reverse=True)
        )

    def extract_entities(self, text: str) -> List[Entity]:
        # NER (Named Entity Recognition)
        tokens = self.tokenizer.tokenize(text)
        inputs = self.tokenizer(text, return_tensors='pt')

        with torch.no_grad():
            outputs = self.ner_model(**inputs.to(self.device))
            predictions = torch.argmax(outputs.logits, dim=2)

        # BIO 태깅 디코딩
        entities = []
        current_entity = None

        for idx, (token, label_id) in enumerate(zip(tokens, predictions[0])):
            label = self.ner_labels[label_id]

            if label.startswith('B-'):
                if current_entity:
                    entities.append(current_entity)
                current_entity = {
                    'text': token.replace('##', ''),
                    'type': label[2:],
                    'start': idx,
                    'end': idx
                }
            elif label.startswith('I-') and current_entity:
                current_entity['text'] += token.replace('##', '')
                current_entity['end'] = idx
            else:
                if current_entity:
                    entities.append(current_entity)
                    current_entity = None

        return entities

# Papago 번역 엔진
class PapagoTranslator:
    def __init__(self):
        self.models = {}
        self.load_models()

    def translate(self, text: str, source: str, target: str) -> str:
        # 언어 쌍별 특화 모델 선택
        model_key = f"{source}_{target}"
        if model_key not in self.models:
            model_key = "multilingual"

        model = self.models[model_key]

        # 전처리
        preprocessed = self.preprocess(text, source)

        # 번역
        translated = model.translate(preprocessed)

        # 후처리
        postprocessed = self.postprocess(translated, target)

        return postprocessed

    def preprocess(self, text: str, lang: str) -> str:
        # 언어별 전처리
        if lang == 'ko':
            # 한국어 특수 처리
            text = self.normalize_korean(text)
        elif lang == 'ja':
            # 일본어 특수 처리
            text = self.normalize_japanese(text)

        return text

모니터링 시스템 (Pinpoint)

APM (Application Performance Management)

// 네이버가 개발한 Pinpoint APM
@Aspect
@Component
public class PinpointTracer {

    private final TraceContext traceContext;

    @Around("@annotation(Traced)")
    public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
        // 트레이스 시작
        Trace trace = traceContext.currentTraceObject();
        if (trace == null) {
            trace = traceContext.newTraceObject();
        }

        SpanEventRecorder recorder = trace.traceBlockBegin();

        try {
            // 메서드 정보 기록
            recorder.recordServiceType(ServiceType.SPRING);
            recorder.recordApi(getMethodDescriptor(joinPoint));

            // 실행
            Object result = joinPoint.proceed();

            // 성공 기록
            recorder.recordException(false);

            return result;

        } catch (Throwable t) {
            // 예외 기록
            recorder.recordException(t);
            throw t;

        } finally {
            // 트레이스 종료
            trace.traceBlockEnd();

            // 성능 메트릭 수집
            long elapsed = trace.getElapsedTime();
            if (elapsed > SLOW_THRESHOLD) {
                recordSlowTrace(joinPoint, elapsed);
            }
        }
    }

    // 분산 트레이싱
    @Component
    public class DistributedTracer {

        public void propagateTrace(HttpRequest request) {
            Trace trace = traceContext.currentTraceObject();
            if (trace != null) {
                // 트레이스 ID 전파
                request.addHeader("Pinpoint-TraceID", trace.getTraceId().toString());
                request.addHeader("Pinpoint-SpanID", trace.getSpanId());
                request.addHeader("Pinpoint-pSpanID", trace.getParentSpanId());
                request.addHeader("Pinpoint-Sampled", String.valueOf(trace.canSampled()));
                request.addHeader("Pinpoint-Flags", String.valueOf(trace.getFlags()));
            }
        }

        public void continueTrace(HttpServletRequest request) {
            String traceId = request.getHeader("Pinpoint-TraceID");
            if (traceId != null) {
                // 전파된 트레이스 계속
                TraceId remoteTraceId = TraceId.parse(traceId);
                long parentSpanId = Long.parseLong(request.getHeader("Pinpoint-pSpanID"));

                Trace trace = traceContext.continueTraceObject(remoteTraceId, parentSpanId);
                trace.recordAcceptorHost(request.getRemoteAddr());
                trace.recordRpc(request.getRequestURI());
            }
        }
    }
}

기술 스택 요약:

  • Backend: Java, Spring, Node.js
  • 검색: Elasticsearch, Lucene, 자체 엔진
  • 빅데이터: Hadoop, Spark, HBase
  • 캐시: Arcus (자체 개발), Redis
  • 데이터베이스: MySQL, MongoDB, Cassandra
  • 인프라: 자체 데이터센터, NBP (Naver Business Platform)
  • 모니터링: Pinpoint (자체 개발)
  • AI/ML: TensorFlow, PyTorch, CLOVA

💡 핵심 기술 인사이트

1. 검색 기술의 정수

  • 한국어 특화 형태소 분석
  • 실시간 인덱싱
  • 의도 기반 검색

2. 오픈소스 기여

  • Pinpoint APM
  • Arcus Cache Cluster
  • nGrinder 성능 테스트

3. AI 기술 선도

  • CLOVA AI 플랫폼
  • Papago 번역
  • 초거대 AI HyperCLOVA

📈 성과 & 지표

기술적 성과

  • 검색 응답시간: < 50ms
  • 시스템 가용성: 99.95%+
  • 일일 처리 데이터: 10TB+
  • 동시 접속자: 500만+ 명
  • API 호출: 분당 1,000만+ 건

비즈니스 영향

  • 국내 검색 점유율: 60%+
  • 글로벌 진출: 일본 LINE, 동남아
  • AI 서비스: 10+ 개 출시
  • 개발자 생태계: 10만+ 개발자

🎓 네이버에서 배울 점

✅ 적용 가능한 패턴

  1. 대규모 검색 시스템: 분산 인덱싱
  2. 실시간 처리: 스트리밍 아키텍처
  3. 캐싱 전략: 다층 캐시 구조
  4. 오픈소스화: 핵심 기술 공개
  5. AI 통합: 검색과 AI의 융합

❌ 주의사항

  1. 운영 복잡도: 수많은 서비스 관리
  2. 레거시 부담: 오래된 시스템 유지
  3. 규모의 저주: 작은 변경도 큰 영향
  4. 경쟁 압박: 글로벌 기업과 경쟁

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. 초거대 AI: HyperCLOVA X
  2. 로봇공학: 서비스 로봇
  3. 디지털 트윈: 제2사옥 활용
  4. 클라우드: NBP 글로벌 확장
  5. 메타버스: 제페토, 아크버스

기술 투자 영역

  • 양자 컴퓨팅: 미래 검색 기술
  • 블록체인: LINE 블록체인
  • 자율주행: 네이버랩스
  • 5G/6G: 초연결 서비스
  • 그린 IT: 친환경 데이터센터

"기술이 사람을 더 가깝게, 정보를 더 유용하게, 세상을 더 편리하게 만듭니다."

  • 네이버 기술 조직

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

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 카카오 기술 스택 분석

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

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

🏢 Airbnb 기술 스택 분석

에어비앤비가 전 세계 400만 개의 숙소를 연결하는 글로벌 마켓플레이스 기술 스택 심층 분석 - Ruby on Rails에서 Service-Oriented Architecture로의 진화

24 min read
airbnb, ruby +12
Read more
🏢 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

🏢 Spotify 기술 스택 분석

스포티파이가 전 세계 5억 명에게 8,300만 곡을 스트리밍하는 기술 스택 심층 분석 - 마이크로서비스, 머신러닝, 그리고 음악 추천의 비밀

25 min read
spotify, java +12
Read more

Found this helpful?

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