ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Project Review] 성능 최적화를 위한 백엔드 분투기
    면접 준비/포트폴리오 2026. 2. 22. 13:47

    프로젝트에서 주식 데이터와 AI를 결합하는 과정에서 발생한 성능 병목을 해결하기 위해 다양한 기술적 시도를 했습니다. 그 중심에 있었던 코드들을 공유합니다.


    1. Spring WebFlux & WebClient: 병렬 데이터 처리 (Parallel Processing)

    "왜 굳이 WebFlux와 WebClient를 썼나요?"

    주식 상세 페이지 하나를 보려면 주가, 뉴스, 기술 지표 API를 각각 호출해야 합니다. 이를 순차적으로 호출하면 사용자는 너무 오래 기다려야 하죠. Mono.zip을 활용해 이를 한 번에 병렬로 처리했습니다.

     
    // StockService.java 중 일부
    public Mono<StockDetailResponse> getStockDetail(String stockCode) {
        // 1. 주가 정보 호출 (비동기)
        Mono<PriceInfo> priceMono = webClient.get()
                .uri("/api/v1/price/{code}", stockCode)
                .retrieve()
                .bodyToMono(PriceInfo.class);
    
        // 2. 뉴스 데이터 호출 (비동기)
        Mono<NewsInfo> newsMono = webClient.get()
                .uri("/api/v1/news/{code}", stockCode)
                .retrieve()
                .bodyToMono(NewsInfo.class);
    
        // 3. 기술 지표 연산 결과 호출 (비동기)
        Mono<IndicatorInfo> indicatorMono = webClient.get()
                .uri("/api/v1/indicators/{code}", stockCode)
                .retrieve()
                .bodyToMono(IndicatorInfo.class);
    
        // 4. Mono.zip을 사용하여 세 작업을 병렬로 실행하고 결과를 합침
        return Mono.zip(priceMono, newsMono, indicatorMono)
                .map(tuple -> new StockDetailResponse(
                        tuple.getT1(), // price
                        tuple.getT2(), // news
                        tuple.getT3()  // indicators
                ));
    }
    
    • I/O와 스레드 점유: RestTemplate은 응답이 올 때까지 스레드가 '대기(Blocking)'하지만, WebClient는 요청만 던져두고 스레드는 다른 일을 하러 떠납니다(Non-blocking). 덕분에 적은 자원으로도 빠르게 데이터를 모을 수 있었습니다.

    2. 동시성 제어: Race Condition 방지

    "사용자가 관심 종목 버튼을 광클한다면?"

    동일한 종목이 DB에 중복 저장되는 것을 막기 위해 DB 레벨의 제약 조건과 예외 처리를 결합했습니다.

    // WatchlistService.java 중 일부
    @Transactional
    public void addWatchlist(WatchlistRequest request) {
        try {
            // 관심 종목 저장 시도
            watchlistRepository.save(new Watchlist(request.getUserId(), request.getStockCode()));
        } catch (DataIntegrityViolationException e) {
            // DB 유니크 제약 조건 위반 시(중복 데이터) 발생하는 예외 처리
            log.error("중복 관심 종목 추가 발생: {}", request.getStockCode());
            throw new AlreadyExistsException("이미 관심 종목에 등록되어 있습니다.");
        }
    }
    
    • Race Condition(경쟁 상태): 여러 요청이 동시에 들어올 때 데이터 정합성이 깨지는 현상입니다. 저는 DB의 Unique Index를 활용해 원천적으로 중복을 차단하고, 서버에서 예외를 잡아 사용자에게 친절한 피드백을 주도록 설계했습니다.

    3. JPA Batch Insert: 대량 데이터 적재 최적화

    2,500개의 종목 정보를 하나씩 insert하면 네트워크 오버헤드가 엄청납니다. 이를 500개씩 묶어서 처리하도록 설정했습니다.

     

    application.properties 설정

    # 한 번에 묶어서 보낼 쿼리 개수 설정
    spring.jpa.properties.hibernate.jdbc.batch_size=500
    # 정렬을 통해 배치를 더 효율적으로 실행
    spring.jpa.properties.hibernate.order_inserts=true
    spring.jpa.properties.hibernate.order_updates=true
    

     

     

    Bulk Insert 로직 예시

    // StockDataService.java 중 일부
    @Transactional
    public void saveAllStocks(List<StockEntity> stocks) {
        // 500개씩 끊어서 저장하여 배치 처리 유도 (Chunk Processing)
        for (int i = 0; i < stocks.size(); i += 500) {
            List<StockEntity> batch = stocks.subList(i, Math.min(i + 500, stocks.size()));
            stockRepository.saveAll(batch);
        }
    }
    

    4. 핵심 기술 개념 (Study Note)

    • WebClient: 비동기 논블로킹 방식으로 동작하는 HTTP 클라이언트. 리소스 점유율이 낮아 고성능 처리에 적합함.
    • Mono.zip: 여러 개의 비동기 작업을 병렬로 실행하고 그 결과를 하나로 묶어주는 연산자.
    • DataIntegrityViolationException: DB의 제약 조건(Unique, Not Null 등)을 어겼을 때 발생하는 Spring 예외.
    • Batch Insert: 여러 SQL 문을 하나의 네트워크 패킷에 담아 보내 통신 횟수를 줄이는 기법.
Designed by Tistory.