본문으로 건너뛰기
김신건의 로그

[Spring] CompositeMap & CompositeIterator

· 수정 · 📖 약 1분 · 543자/단어 #spring #util #map #iterator #composite #layered
CompositeMap, CompositeIterator, org.springframework.util.CompositeMap, composite map, Spring CompositeMap, 합성 맵

정의

org.springframework.util.CompositeMap<K,V> 는 두 개의 Map 을 하나의 view 로 합쳐서 보여주는 read-mostly 자료구조. 첫 번째 Map 이 우선, 없으면 두 번째 Map 을 본다.

org.springframework.util.CompositeIterator<E> 는 여러 Iterator 를 순차적으로 이어 붙여 하나의 iterator 처럼 제공.

둘 다 “여러 소스의 데이터를 하나로 본다” 는 Composite 패턴의 도우미. 데이터를 실제로 병합(merge) 하지 않고 뷰(view) 만 제공하므로 메모리 절약.

사용 상황

상황설명
환경별 설정기본값 Map + 환경 오버라이드 Map 을 하나로
L1/L2 캐시빠른 캐시(primary) 에 없으면 느린 캐시(secondary) 조회
부모-자식 BeanDefinition부모 정의 위에 자식 정의 overlay
다중 properties 소스.properties, 환경변수, Vault 를 하나처럼 조회

레이어드 조회 흐름

flowchart LR
    Client -->|"get(key)"| CM["CompositeMap"]
    CM -->|"1. primary 조회"| PM["Primary Map\n(override / 우선)"]
    PM -->|"key 존재"| Client
    PM -->|"key 없음"| SM["Secondary Map\n(default / fallback)"]
    SM -->|"key 존재"| Client
    SM -->|"key 없음"| Null["null 반환"]
flowchart TB
    subgraph "레이어드 설정 예"
        ENV["환경변수\nDATABASE_URL=prod-db"]
        DEF["기본값\nDATABASE_URL=localhost\nTIMEOUT=30s"]
        CM2["CompositeMap\n(ENV primary)"]
        ENV --> CM2
        DEF --> CM2
        CM2 -->|"DATABASE_URL"| ENV
        CM2 -->|"TIMEOUT"| DEF
    end

CompositeMap

import org.springframework.util.CompositeMap;

Map<String, Integer> primary = Map.of("a", 1, "b", 2);
Map<String, Integer> secondary = Map.of("b", 99, "c", 3);

CompositeMap<String, Integer> composite = new CompositeMap<>(primary, secondary);

composite.get("a");   // 1 (primary)
composite.get("b");   // 2 (primary 우선, secondary 의 99 무시)
composite.get("c");   // 3 (secondary fallback)
composite.get("d");   // null

composite.size();     // 3 (a, b, c - 중복 key 는 1개로 카운트)
composite.keySet();   // [a, b, c]
composite.entrySet(); // [{a=1}, {b=2}, {c=3}]

put 은 primary 에만

// put 은 primary 맵에만 들어감 (secondary 는 불변 취급)
Map<String, Integer> primary = new HashMap<>(Map.of("a", 1));
Map<String, Integer> secondary = Map.of("b", 99, "c", 3);

CompositeMap<String, Integer> composite = new CompositeMap<>(primary, secondary);
composite.put("d", 4);   // primary 에 추가

primary.get("d");        // 4
secondary.get("d");      // null (secondary 건드리지 않음)

불변 primary 로 사용 (read-only view)

// primary, secondary 모두 불변 Map 이면 CompositeMap 도 사실상 read-only
Map<String, String> defaults = Map.of("timeout", "30s", "retries", "3");
Map<String, String> overrides = Map.of("timeout", "60s");

CompositeMap<String, String> config = new CompositeMap<>(overrides, defaults);
config.get("timeout");    // "60s" (override)
config.get("retries");    // "3" (default)

CompositeIterator

여러 컬렉션을 실제로 합치지 않고 순차 순회:

import org.springframework.util.CompositeIterator;

List<Integer> a = List.of(1, 2, 3);
List<Integer> b = List.of(4, 5);
List<Integer> c = List.of(6);

CompositeIterator<Integer> it = new CompositeIterator<>();
it.add(a.iterator());
it.add(b.iterator());
it.add(c.iterator());

while (it.hasNext()) {
    System.out.print(it.next() + " ");
}
// 출력: 1 2 3 4 5 6
// Stream 으로 변환
Iterable<Integer> iterable = () -> {
    CompositeIterator<Integer> ci = new CompositeIterator<>();
    ci.add(a.iterator());
    ci.add(b.iterator());
    return ci;
};

StreamSupport.stream(iterable.spliterator(), false)
    .filter(n -> n % 2 == 0)
    .forEach(System.out::println);   // 2 4 6

remove 지원

CompositeIteratorremove() 를 지원한다. 현재 순회 중인 원본 컬렉션(mutable) 에서 항목 제거.

List<String> list1 = new ArrayList<>(List.of("a", "b", "c"));
List<String> list2 = new ArrayList<>(List.of("d", "e"));

CompositeIterator<String> it = new CompositeIterator<>();
it.add(list1.iterator());
it.add(list2.iterator());

while (it.hasNext()) {
    String v = it.next();
    if (v.equals("b") || v.equals("d")) {
        it.remove();     // list1 또는 list2 에서 제거
    }
}
// list1: [a, c], list2: [e]

실전 패턴

환경별 설정 레이어

@Configuration
public class ConfigLayerConfig {

    @Bean
    public Map<String, String> appConfig(
        @Value("#{systemEnvironment}") Map<String, String> envVars,
        @Value("#{applicationProperties}") Map<String, String> appProps
    ) {
        // 환경변수가 우선, 없으면 application.properties
        return new CompositeMap<>(envVars, appProps);
    }
}

다중 캐시 계층 (L1/L2)

public class LayeredCache<K, V> {
    private final Map<K, V> l1;   // 빠른 로컬 캐시 (small)
    private final Map<K, V> l2;   // 느린 원격 캐시 (large)

    private final CompositeMap<K, V> composite;

    public LayeredCache(int l1Size, Map<K, V> l2) {
        this.l1 = new LinkedHashMap<>(l1Size, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K,V> e) {
                return size() > l1Size;
            }
        };
        this.l2 = l2;
        this.composite = new CompositeMap<>(l1, l2);
    }

    public V get(K key) {
        return composite.get(key);    // L1 먼저, 없으면 L2
    }

    public void put(K key, V value) {
        l1.put(key, value);           // L1 에 넣음
    }
}

Spring PropertySource 스타일

public class LayeredPropertySource {
    private final List<Map<String, String>> sources = new ArrayList<>();

    public void addFirst(Map<String, String> source) {
        sources.add(0, source);
    }

    public void addLast(Map<String, String> source) {
        sources.add(source);
    }

    public String getProperty(String key) {
        // 앞에서부터 찾기
        return sources.stream()
            .filter(m -> m.containsKey(key))
            .map(m -> m.get(key))
            .findFirst()
            .orElse(null);
    }
}

실제 Spring PropertySources 는 더 복잡하지만 동일한 철학.

한계 및 함정

WARNING

Thread-safe 가 아니다. CompositeMapCompositeIterator 모두 단일 스레드 전용. 멀티 스레드에서 공유하면 외부 동기화 필요.

IMPORTANT

size()entrySet() 은 중복 key 를 한 번만 카운트. primary 에 "a"=1, secondary 에 "a"=99 가 있으면 size() 에서 "a" 는 1회만 카운트되고, get("a")1 을 반환한다.

CAUTION

secondary 수정 시 동작: put() 은 primary 에만 쓴다. secondary 를 직접 수정하면 CompositeMap 에 반영되지만, secondary 불변 Map(Map.of(...)) 을 직접 수정하려 하면 UnsupportedOperationException.

WARNING

entrySet().iterator().remove() 지원 여부: 구현에 따라 다름. primary Map 이 mutable 이어야 한다.

CompositeIterator 함정

// 동일 iterator 를 두 번 add 하면 순서 꼬임
List<Integer> list = List.of(1, 2, 3);
CompositeIterator<Integer> ci = new CompositeIterator<>();
ci.add(list.iterator());
ci.add(list.iterator());    // 같은 소스를 두 번 순회 (1 2 3 1 2 3 출력)

관련 위키

이 글의 용어 (6개)
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Spring] Bean Lifecycle: 초기화와 소멸 콜백spring
정의 가 Bean 등록과 주입 을 다룬다면, 이 페이지는 등록 이후 생성 → 사용 → 소멸 사이에 끼어드는 콜백을 다룬다. Spring 컨테이너가 단순히 객체를 만들고 끝내는 게…
[Spring] ConcurrentLruCachespring
정의 는 Spring 5.3+ 의 thread-safe LRU (Least Recently Used) 캐시. 크기 제한이 있고, 가장 오래 사용하지 않은 항목부터 제거. 내부적으…
[Spring] LinkedCaseInsensitiveMapspring
정의 는 을 기반으로 key 비교를 대소문자 무시 로 하는 Map. 단, 원본 key (대소문자 보존) 도 유지. HTTP 헤더 같은 도메인에서 과 을 같은 key 로 다뤄야 할…
[Spring] MultiValueMap & LinkedMultiValueMapspring
정의 는 하나의 key 에 여러 value 가 매핑되는 Map 인터페이스. 형식적으로는 의 sugared 형태. 는 그 기본 구현체, 내부적으로 를 보관 (삽입 순서 유지). S…
집합과 맵 (Set and Map)algorithm
정의 집합 (Set) 은 고유한 원소들의 모임을 나타내는 추상 자료구조로, 멤버십 검사, 삽입, 삭제를 지원한다. 맵 (Map, Dictionary) 은 키-값 쌍을 저장하며, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기