Informer를 쓰기 시작하면 이런 코드가 자연스럽게 나온다.
Lister<V1Pod> podLister = new Lister<>(podInformer.getIndexer());
그러면 곧 두 가지가 헷갈린다.
Lister로 조회할 수 있는데 굳이Indexer를 직접 써야 할 때가 있을까?- Indexer가 namespace와 name으로 객체를 찾는다면, namespace 인덱스는 정확히 무엇일까?
결론부터 말하면 Lister와 Indexer는 같은 Informer 캐시를 읽는다. Lister는 namespace와 name으로 객체를 쉽게 찾도록 돕고, Indexer는 사용자가 추가한 기준으로 객체 묶음을 찾을 때 쓴다.
이 글은 이전 글인 Informer, Indexer, Lister 전체 구조에서 한 단계 더 들어가, 두 조회 API를 실제로 언제 나눠 쓰는지에 집중한다.
캐시에는 두 가지 조회 방법이 있다
“Indexer는 namespace와 객체 이름으로 인덱싱한다”는 설명은 반만 맞다. Java client의 기본 Cache에는 서로 다른 역할의 두 구조가 있다.
- 기본 저장소(primary store):
namespace/name을 key로 객체 하나를 저장한다. - 보조 인덱스(secondary index): namespace 같은 값을 기준으로 관련 객체의 key를 묶어 둔다.
즉 payments/checkout-7f8d라는 Pod를 하나 찾을 때는 namespace 인덱스를 훑는 것이 아니다. items.get("payments/checkout-7f8d")에 가까운 primary key 조회다. 반대로 payments namespace의 Pod 전체를 찾을 때는 namespace 보조 인덱스가 쓰인다.
Cache의 실제 구현은 이 개념을 items와 indices라는 Map으로 나눠 갖고 있다.
items
"payments/checkout-7f8d" → V1Pod
"payments/worker-6b9c" → V1Pod
indices
"namespace" → "payments" → { "payments/checkout-7f8d", "payments/worker-6b9c" }
"byNodeName" → "worker-a" → { "payments/checkout-7f8d" }
이 구조 덕분에 한 Pod는 items에 한 번만 저장되고, 여러 인덱스는 그 Pod의 primary key만 들고 있다. Cache의 update()는 이전 객체의 인덱스 엔트리를 지운 뒤 새 객체에서 계산한 index value를 다시 넣는다. delete()도 primary store와 모든 인덱스에서 함께 제거한다.
Lister로 이름과 namespace 조회하기
Lister의 구현은 매우 작다. Indexer 하나와 선택적인 namespace를 보관하고, 아래 두 형태의 조회를 제공한다.
Lister<V1Pod> pods = new Lister<>(podInformer.getIndexer());
V1Pod pod = pods.namespace("payments").get("checkout-7f8d");
List<V1Pod> paymentPods = pods.namespace("payments").list();
List<V1Pod> allPods = pods.list();
내부 동작은 다음처럼 다르다.
| Lister 호출 | 내부에서 하는 일 | 잘 맞는 질문 |
|---|---|---|
namespace("payments").get("checkout-7f8d") | getByKey("payments/checkout-7f8d") | 이 namespace의 이 이름을 가진 Pod가 현재 있는가? |
namespace("payments").list() | byIndex("namespace", "payments") | 이 namespace의 Pod를 모두 보고 싶다. |
list() | indexer.list() | 현재 캐시 전체를 보고 싶다. |
여기서 중요한 포인트는 get(name)이다. Lister는 name만 따로 인덱싱하지 않는다. namespace가 있으면 namespace/name 문자열을 만들어 primary store에 바로 조회한다.
그래서 reconcile 코드에서 “이 키의 현재 객체를 읽는다”는 목적이라면 Lister가 가장 의도를 잘 드러낸다.
V1Pod current = podLister.namespace(namespace).get(name);
if (current == null) {
// 삭제됐거나, 아직 cache에 없는 상태를 처리한다.
return;
}
Lister의 반환값도 API 서버에서 새로 받은 응답이 아니라 Informer가 관리하는 로컬 캐시의 객체다. 반환 객체를 직접 수정하면 같은 캐시를 읽는 다른 코드에도 영향을 줄 수 있으므로 읽기 전용으로 다뤄야 한다.
Indexer로 원하는 기준의 Pod 묶기
모든 조회가 namespace/name으로 끝나지는 않는다. 예를 들어 Node drain이나 장애 분석 로직에서 “worker-a에 올라간 Pod를 자주 찾는다”면 매번 전체 Pod를 list()한 뒤 Java stream으로 거르는 것은 아쉽다.
이때 Pod의 spec.nodeName을 기준으로 사용자 정의 인덱스를 추가한다.
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.openapi.models.V1Pod;
import java.util.List;
import java.util.Map;
public void addPodIndexes(SharedIndexInformer<V1Pod> podInformer) {
podInformer.addIndexers(
Map.of(
"byNodeName",
pod -> {
String nodeName = pod.getSpec() == null
? null
: pod.getSpec().getNodeName();
return nodeName == null || nodeName.isBlank()
? List.of()
: List.of(nodeName);
}));
}
이 함수는 Informer를 생성한 직후, 시작하기 전에 호출해야 한다.
SharedIndexInformer<V1Pod> podInformer = createPodInformer(...);
addPodIndexes(podInformer); // factory.startAllRegisteredInformers() 이전
factory.startAllRegisteredInformers();
이후에는 Indexer의 byIndex()로 바로 조회한다.
List<V1Pod> podsOnWorkerA =
podInformer.getIndexer().byIndex("byNodeName", "worker-a");
Function<ApiType, List<String>>가 List<String>을 반환한다는 점도 눈여겨볼 만하다. 한 객체가 index value를 여러 개 가질 수 있기 때문이다. 예를 들어 Pod를 모든 owner reference의 kind/name으로 묶고 싶다면 owner마다 하나씩 key를 반환할 수 있다. 값이 없을 때는 빈 리스트를 반환하면 그 Pod는 해당 인덱스에 넣지 않는다.
사용자 정의 인덱스는 왜 Indexer로 읽을까?
Lister에는 indexName을 받는 생성자가 있지만, 일반적인 사용 목적은 namespace와 name 조회다. namespace("payments") 메서드도 항상 기본 namespace 인덱스를 사용한다.
더 중요한 것은 Lister.get(name)이 사용자 정의 인덱스를 사용하지 않는다는 점이다. Lister는 get()에서 기본 키 형식의 문자열을 만들기 때문이다. byNodeName처럼 하나의 값으로 여러 Pod를 찾는 인덱스에는 Indexer.byIndex()를 사용해야 한다.
정리하면 다음처럼 나누면 된다.
| 필요한 조회 | 권장 API | 이유 |
|---|---|---|
| namespace와 name으로 Pod 하나 조회 | Lister.namespace(ns).get(name) | Kubernetes 리소스 키 조회라는 의도가 분명하다. |
| 특정 namespace의 Pod 목록 | Lister.namespace(ns).list() | 기본 namespace index를 감싼 편의 API다. |
| 모든 Pod 목록 | Lister.list() 또는 Indexer.list() | 단순 전체 조회다. 일반 코드에서는 Lister가 읽기 의도를 잘 드러낸다. |
| Node, label 값, owner처럼 원하는 기준으로 묶어 조회 | Indexer.byIndex() | 사용자 정의 인덱스 이름과 조회 값을 명시해야 한다. |
| index 등록·점검 | Indexer.addIndexers(), getIndexers() | 캐시 구조 자체를 다루는 작업이다. |
Informer를 시작하기 전에 인덱스 등록하기
이 부분은 실제 구현을 알고 있으면 이유가 명확하다.
DefaultSharedIndexInformer.addIndexers()는 informer가 이미 시작됐다면 예외를 던진다. 그 아래 Cache.addIndexers()도 cache에 객체가 하나라도 있으면 예외를 던진다. 실행 중인 cache에 인덱스를 추가하려면 기존 객체를 전부 다시 색인해야 하는데, Java client는 이 동작을 자동으로 해 주지 않는다.
따라서 Bean을 구성할 때 다음 순서를 지켜야 한다.
SharedIndexInformer 생성
→ 사용자 정의 인덱스 등록
→ event handler 등록
→ factory.startAllRegisteredInformers()
→ hasSynced() 확인 후 worker 시작
인덱스 함수는 Informer가 객체를 추가·수정·삭제할 때마다 호출된다. 이 함수에서 외부 시스템을 호출하거나 무거운 계산을 하면 캐시 갱신이 느려질 수 있다. metadata나 spec처럼 객체 안에서 빠르게 읽을 수 있고 쉽게 바뀌지 않는 문자열을 조회 값으로 쓰는 것이 좋다.
Lister와 Indexer 고르는 기준
Lister와 Indexer는 난이도에 따라 나눈 API가 아니다. 필요한 조회 방식에 따라 고르면 된다.
Lister는 namespace/name이라는 Kubernetes 리소스 식별 방식으로 현재 cache를 읽는다.Indexer는 기본 저장소와 보조 인덱스를 제공한다. 사용자 정의 조회 기준이 필요할 때 직접 사용한다.- 기본 키
namespace/name과 기본 namespace 인덱스는 서로 다르다. 전자는 객체 하나를 찾고, 후자는 namespace 안의 객체 묶음을 찾는다. - 사용자 정의 인덱스는 시작 전에 등록하고, 캐시에서 받은 객체는 읽기 전용으로 사용한다.
이 기준을 잡아 두면 일반적인 reconcile은 Lister로 단순하게 유지하면서, Node·owner·라벨처럼 반복 조회가 많은 운영 요구만 Indexer로 명시적으로 최적화할 수 있다.