자바 computeIfAbsent vs getOrDefault 차이 — Map 값 없을 때 처리
getOrDefault와 computeIfAbsent의 차이는 한 줄이면 끝납니다: getOrDefault는 맵을 건드리지 않고 값만 꺼내 보고, computeIfAbsent는 키가 없으면 값을 만들어 맵에 넣습니다. 그래서 Map<K, List<V>> 같은 곳에 값을 누적할 때 getOrDefault(key, new ArrayList<>()).add(x)라고 쓰면 리스트가 맵에 저장되지 않아 데이터가 사라집니다. 이럴 땐 computeIfAbsent를 써야 합니다. (Java 8+ 공통, 아래 예제는 JDK 21에서 직접 실행해 확인)
getOrDefault vs computeIfAbsent vs putIfAbsent 한눈 비교
| 메서드 | 키 없을 때 | 맵에 넣나? | 값 계산 시점 |
|---|---|---|---|
getOrDefault(k, d) |
기본값 d 반환 |
❌ 안 넣음 | 호출 시 이미 만들어진 값 |
computeIfAbsent(k, f) |
f.apply(k)로 계산 |
✅ 넣음(null 아니면) | 키 없을 때만 지연 계산 |
putIfAbsent(k, v) |
값 v를 넣음 |
✅ 넣음 | 항상 즉시 계산(인자로 평가됨) |
getOrDefault는 맵에 넣지 않는다 — 흔한 누적 버그
getOrDefault(key, defaultValue)는 공식 문서상 "키에 매핑된 값이 있으면 그 값을, 없으면 defaultValue를 반환"할 뿐입니다. 맵에는 아무것도 쓰지 않습니다. 그래서 반환된 기본값에 add()를 해도 그 객체는 맵과 무관한 임시 객체라 그대로 버려집니다.
Map<String, List<Integer>> g1 = new HashMap<>();
g1.getOrDefault("a", new ArrayList<>()).add(10); // ❌ 새 리스트에 넣고 버림
System.out.println(g1); // {} ← 맵은 그대로 비어 있음
{}. 값을 누적하려면 getOrDefault가 아니라 computeIfAbsent가 필요합니다.
computeIfAbsent — 없으면 만들어 넣고 그 값을 돌려준다
computeIfAbsent(key, mappingFunction)는 "키에 값이 없거나(또는 null이면) 매핑 함수로 값을 계산해 맵에 넣고, 그 값을 반환"합니다. 반환된 것이 맵 안에 실제로 들어간 그 객체라서, 거기에 add()하면 맵에 반영됩니다. 그룹핑·누적의 정석 패턴입니다.
Map<String, List<Integer>> g2 = new HashMap<>();
g2.computeIfAbsent("a", k -> new ArrayList<>()).add(10);
g2.computeIfAbsent("a", k -> new ArrayList<>()).add(20); // 두 번째는 함수 호출 안 함
System.out.println(g2); // {a=[10, 20]}
두 번째 computeIfAbsent는 이미 "a"가 있으므로 람다를 실행하지 않고 기존 리스트를 반환합니다. 그래서 새 ArrayList가 낭비되지 않고, [10, 20]이 한 리스트에 쌓입니다.
computeIfAbsent vs putIfAbsent — 지연 계산이 핵심 차이
둘 다 "없을 때만 넣는다"지만 결정적 차이가 있습니다. putIfAbsent(k, v)는 v가 메서드 인자라 키가 이미 있어도 항상 먼저 평가됩니다. 반면 computeIfAbsent의 k -> ...는 키가 없을 때만 실행됩니다.
Map<String, String> p = new HashMap<>();
p.put("k", "old");
p.putIfAbsent("k", expensive()); // 🔴 "k" 있어도 expensive() 호출됨(인자 평가)
p.computeIfAbsent("k", kk -> expensive()); // 🟢 "k" 있어서 expensive() 호출 안 됨
값을 만드는 비용이 크거나(new, DB 조회, 계산) 부작용이 있으면 putIfAbsent는 헛일을 하게 됩니다. 값 생성이 비싸면 computeIfAbsent를 씁니다. 이 "인자는 항상 평가된다 vs 필요할 때만 평가된다"는 함정은 자바 Optional의 orElse vs orElseGet 차이와 완전히 같은 구조입니다(orElse는 항상 평가, orElseGet은 지연).
주의 — null 값 처리가 다르다
키가 없는 것과 값이 null인 것을 헷갈리면 버그가 됩니다.
getOrDefault: 키가 null 값으로 매핑돼 있으면 기본값이 아니라 그null을 반환합니다(키가 존재하므로).Map<String, Integer> n = new HashMap<>(); n.put("z", null); n.getOrDefault("z", 99); // 99가 아니라 nullcomputeIfAbsent: 매핑 함수가null을 반환하면 매핑을 기록하지 않습니다(키가 맵에 안 들어감). 또 값이null인 키는 "없는 것"으로 보고 함수를 실행합니다.
getOrDefault, 없으면 만들어 누적하면 computeIfAbsent, 이미 있는 상수 값을 넣기만 하면 putIfAbsent.
자주 묻는 질문 (FAQ)
Q. 카운팅(빈도수)에는 뭘 쓰나요?
개수 세기는 값이 Integer라 merge가 더 깔끔합니다: map.merge(key, 1, Integer::sum). computeIfAbsent는 List·Set처럼 가변 컨테이너를 만들어 누적할 때 쓰세요.
Q. computeIfAbsent 안에서 같은 맵을 또 수정하면?
매핑 함수 안에서 그 맵 자체를 변경하면 안 됩니다(ConcurrentModificationException이나 잘못된 결과 가능). 함수는 값만 계산하게 두세요.
Q. Java 몇 버전부터 되나요?
셋 다 Map의 default 메서드로 Java 8부터 있습니다. 위 예제는 JDK 21에서 확인했고 동작은 8과 동일합니다.
마무리
getOrDefault는 읽기 전용, computeIfAbsent는 없으면 만들어 넣기, putIfAbsent는 이미 가진 값을 즉시 넣기. 이 셋만 구분하면 "맵에 값이 안 들어간다", "매번 새 객체가 생긴다" 같은 버그가 사라집니다. 값 생성 비용이 크면 지연 계산되는 computeIfAbsent를 기본으로 잡으세요.
📚 참고 출처 (2026년 7월 20일 확인)
· Java SE 21 API — Map (getOrDefault · computeIfAbsent · putIfAbsent)
· 동작은 로컬 JDK 21.0.1로 직접 실행해 확인

COMMENTS