외부 연동 서비스 리소스 제한 이슈

박화랑·2025년 7월 1일
1

Spring_6기

목록 보기
28/32

Brave Search & YouTube API Key 순환 구조 설계

문제 상황

많은 무료 API는 무료 플랜에 대해 요청 횟수 제한(Rate Limit)을 둔다. 대표적으로 YouTube Data API, Brave Search API 등은 하루, 혹은 초당 요청 횟수를 제한 하는 경우가 생긴다. 무료 유저로서는 슬픈 상황이다. 물론 지원 예산이 있긴 하지만 가능하면 이런 비용 절약 측면에서 설계를 하는 것도 좋다는 생각이 들어서 이번에 실험해보게 되었다.

  • 기존에는 .envapplication.yml 같은 설정 파일에 하나의 API 키만 고정해 사용하는 방식으로 인해 키가 만료되거나 한도에 도달하면 서버를 닫아야 한다는 점이다.
  • 문제는 요청 한도를 초과하면 429 Too Many Requests 에러가 발생하고, 서버를 재시작하지 않으면 해결할 방법이 없다.

예시 오류 로그:

{"code":"RATE_LIMITED","rate_limit":1,"rate_current":1,"quota_limit":2000,"quota_current":111}

이는 서비스 중단을 유발하며, 테스트나 운영 환경에서는 큰 문제가 될 것이다.


키 순환(Key Rotation) 전략

  • YouTube 및 Brave Search API에는 하루 또는 초당 요청 한도가 존재
    RateLimit으로 제한을 할수도 있지만 1초에 한 번 사용할 수 있으면 테스트나 배포 상황에서 요청에 대한 처리 속도가 너무 느리다. 따라서 key swapping을 통해 하나의 키를 사용할 때 생기는 제약을 넘는 기능을 만들 수 있다. 물론 유료로 결제하면 훨씬 좋겠지만 비용을 최대한 절약할 수 있으면 절약해서 프로젝트를 진행하는 것도 좋은 경험일 것 같다. 따라서 이번 프로젝트에서는 하나의 키가 아닌 여러 키를 순환하면서 사용하는 기능을 추가해볼 것이다.

API 키가 여러 개 있을 때 이를 자동으로 순환해서 사용하는 방식이 필요하다. 가장 단순한 방법은 큐(Queue) 자료구조를 사용할 것이다

하지만 단순 JSON 파일이나 메모리 기반 큐는 서버 재시작 시 초기화되고, 설정 변경 시 반영이 어려우며, 운영 중 키를 수정하기 어려운 문제를 해결해 줄 좋은 툴이 있다.


Apache Zookeeper란?

간단히 요약하자면 Zookeeper는 Apache 재단에서 제공하는 분산 환경을 위한 설정 관리 시스템이라고 한다.

아파치 공식 사이트

특징

  • Znode라 불리는 트리 기반 Key-Value 저장소 제공
  • 클러스터 기반으로 높은 가용성 보장
  • 분산 락, 설정 동기화, Watcher(감시) 기능 제공

그래서 왜 씀?

  • 서버 재시작 없이 설정값 변경 가능 (중요!)
  • 여러 인스턴스 간에 설정을 공유할 수 있지만 지금 프로젝트에서 쓰이는 기능은 아니다
  • 실시간 반영 및 감지 기능 제공 (Watcher) -> 편리하다

Spring Cloud Zookeeper 키 순환 구조 상세 구현

build.gradle 설정

ext {
    springCloudVersion = "2025.0.0"
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    // Zookeeper + Curator
    implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-config'
    implementation 'org.apache.curator:curator-framework:5.5.0'
    implementation 'org.apache.curator:curator-recipes:5.5.0'
}
  • 이 설정이면 가능하다. 이전에 쓰던 버전이랑 호환성은 잘 체크해야 한다. 이 곳에서 내 프로젝트의 springboot version과 맞는 springcloud version을 찾을 수 있다.
    Spring Cloud 2025.0.0

docker-compose 설정


  zookeeper:
    image: zookeeper:3.8
    ports:
        - "2181:2181"
    networks:
      - devmountain-net

Zookeeper 설정 구성 (ZookeeperConfig.java)

Zookeeper와의 연결을 설정하고, 설정된 키의 변경 사항을 실시간으로 감지하기 위한 Watcher를 등록하는 구성 파일

@Configuration
public class ZookeeperConfig {

    @Value("${spring.cloud.zookeeper.connect-string}")
    private String connectString;

    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(connectString)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        client.start();
        return client;
    }

    @Bean(initMethod = "registerWatcher")
    public BraveKeyWatcher braveKeyWatcher(CuratorFramework client, BraveSearchProperties properties) {
        return new BraveKeyWatcher(client, properties);
    }

    @Bean(initMethod = "registerWatcher")
    public McpKeyWatcher mcpKeyWatcher(CuratorFramework client) {
        return new McpKeyWatcher(client);
    }
}
  • CuratorFramework: Zookeeper 클라이언트를 생성하고 연결한다
  • BraveKeyWatcher: 설정 변경을 감지하고 키 값을 실시간으로 갱신한다.
  • McpKeyWatcher: MCP 서버 설정 변경을 감지하고 환경 변수를 갱신한 뒤 MCP 서버를 재시작

API 키 속성 관리 (BraveSearchProperties.java)

Zookeeper로부터 Brave API 키를 가져와 관리하는 클래스. 키 값이 변경될 때 실시간으로 반영된다

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "brave.search.api")
public class BraveSearchProperties {

    private String key;
}
  • Zookeeper의 /config/devmountain/brave.search.api.key에 연결된 값을 실시간으로 갱신하여 애플리케이션에 제공하여 처리하도록 한다.

Watcher를 통한 실시간 갱신 (BraveKeyWatcher.java, McpKeyWatcher.java)

Zookeeper의 키 변경 이벤트를 감지하는 Watcher 클래스

Brave Search API 키 갱신:

@Slf4j
@RequiredArgsConstructor
public class BraveKeyWatcher implements CuratorWatcher {

    private final CuratorFramework client;
    private final BraveSearchProperties properties;

    public void registerWatcher() {
        try {
            client.getData().usingWatcher(this).forPath("/config/devmountain/brave.search.api.key");
        } catch (Exception e) {
            log.error("Failed to register watcher", e);
        }
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDataChanged) {
            try {
                byte[] newData = client.getData().usingWatcher(this).forPath(event.getPath());
                String newKey = new String(newData);
                properties.setKey(newKey);
                log.info("Brave Search API key updated: {}", newKey);
            } catch (Exception e) {
                log.error("Failed to update Brave API key", e);
            }
        }
    }
}

MCP 서버 환경 변수 갱신 및 재시작:

@Slf4j
@RequiredArgsConstructor
public class McpKeyWatcher implements CuratorWatcher {

    private final CuratorFramework client;

    public void registerWatcher() {
        try {
            client.getData().usingWatcher(this).forPath("/config/devmountain/mcp.server.env");
        } catch (Exception e) {
            log.error("Failed to register watcher", e);
        }
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDataChanged) {
            try {
                byte[] newData = client.getData().usingWatcher(this).forPath(event.getPath());
                String newEnv = new String(newData);
                System.setProperty("MCP_SERVER_ENV", newEnv);
                restartMcpServer();
                log.info("MCP 서버 환경 변수 업데이트 및 서버 재시작: {}", newEnv);
            } catch (Exception e) {
                log.error("Failed to update MCP server environment", e);
            }
        }
    }

    private void restartMcpServer() {
        // MCP 서버 재시작 로직 구현
    }
}
  • Brave Search API 키는 실시간으로 업데이트한다.
  • MCP 서버는 환경 변수가 변경될 때마다 자동으로 재시작된다

정리 및 주요 포인트

정리 및 주요 포인트 비교

비교 항목이전 방식변경된 방식
API 키 관리 위치.env 또는 application.yml에 단일 키 고정Zookeeper의 Znode(/config/devmountain/...)에 다중 키 저장 및 관리
키 수하나Queue 자료구조로 순환
키 변경 반영키 변경 시 서버 재시작 필요Watcher 감지 시 실시간 반영 (서버 재시작 불필요)
Rate Limit 초과 시 대응429 Too Many Requests 발생 → 서버 재시작 후 복구현재 키 사용 한도 초과 시 즉각적으로 대응하여 키에서 제거
MCP 서버 환경 변수 갱신환경 변수 수정 → 수동 재배포/재시작Zookeeper Watcher 감지 → System.setProperty 후 자동 재시작 로직 실행
가용성 및 확장성낮음 (단일 포인트 장애, 수동 관리)높음 (분산 설정 관리, 자동화된 키 순환 및 감시)
장애 대응 속도느림 (수동 확인 및 재시작)빠름 (Watcher 이벤트 기반 자동 처리)
  • 실시간 키 갱신: Watcher가 Zookeeper 설정 변경을 즉각 감지하여 서버 재시작 없이 키 변경 가능

  • MCP 서버 자동 관리: MCP 서버 환경 설정 변경 시 서버 자동 재시작

  • Zookeeper 사용 이유:

    • 고가용성 및 높은 신뢰성
    • 서버 재시작 없이 설정 변경 반영 가능
    • Watcher 기능으로 즉시 갱신

이 구조는 API Rate Limit 관리 및 환경 변수 관리에 매우 효과적이며, 운영 중 무중단 키 관리 및 설정 변경에 최적화할 수 있었다.

소감

생각보다 Spring zookeeper 구현 자료들이 없어서 공식 자료를 참고를 많이 했다. 확실히 Zookeeper는 배포 단계에서 매우 유용한 설정이 될 것이다. 무엇보다 여러 인스턴스를 띄우면 이 진가가 잘 발휘될 것 같다. 확실히 이건 테스트를 해 볼 필요도 없는 명백한 기능 향상 방향이라 이전이랑 비교하는게 맞을지는 모르겠지만 정량적 지표를 사용하는게 좋다고 생각하니 나중에 표로 정리할 예정이다

profile
개발자 희망생

0개의 댓글