PHpullh

LANGUAGE COMPARISON

Map를 언어별로 비교하기

같은 개념을 실제 예제로 나란히 확인하고, 각 언어의 개별 설명으로 이어갈 수 있습니다.

키-값 저장소에서 언어마다 갈리는 세 가지

맵, 딕셔너리, 연관 배열, 해시맵. 부르는 이름은 달라도 하는 일은 같지만, 실무에서 사고를 만드는 지점은 세 군데로 좁혀집니다. 없는 키를 조회했을 때 무엇이 돌아오는가, 순회 순서가 보장되는가, 그리고 무엇을 같은 키로 볼 것인가입니다. 아래 열 개 언어의 코드는 문법만 놓고 보면 대동소이하지만, 이 세 질문에 대한 답은 서로 꽤 다릅니다.

없는 키에 대한 태도부터 보겠습니다. Python은 KeyError를 던져 실수를 즉시 드러냅니다. Go는 제로값과 ok 불리언을 함께 돌려주어, 값을 받는 쪽에서 존재 여부를 판단하게 합니다. Java와 JavaScript는 각각 null과 undefined를 조용히 돌려주고, Rust는 Option으로 감싸 컴파일러가 처리를 강제합니다. C++의 operator[]는 한 걸음 더 나가서, 없는 키를 조회하면 그 키를 기본값으로 만들어 넣습니다. 읽기만 했다고 생각한 코드가 맵을 키우는 셈이라, 조회에는 find나 at을 쓰는 것이 원칙입니다.

Kotlin

List, Set, Map 생성과 기초

불변/가변 컬렉션 생성 함수와 기본 조작 방법을 익힙니다.

// 불변 컬렉션
val list = listOf(1, 2, 3, 4, 5)
val set  = setOf("a", "b", "c", "b")    // 중복 제거
val map  = mapOf("one" to 1, "two" to 2)

// 가변 컬렉션
val mList = mutableListOf(1, 2, 3)
mList.add(4)
mList.removeAt(0)
mList[0] = 99

val mMap = mutableMapOf("a" to 1)
mMap["b"] = 2
mMap.remove("a")

// 읽기 안전
println(map["one"])          // 1 (Int?)
println(map.getOrDefault("three", 0))  // 0
println(map.getOrElse("three") { -1 }) // -1

// 컬렉션 + 연산
val combined = list + listOf(6, 7)     // 새 리스트 반환
val filtered = list - listOf(2, 4)     // [1, 3, 5]

// 배열
val arr = arrayOf(1, 2, 3)
val intArr = intArrayOf(1, 2, 3)       // primitive 배열
println(intArr.sum())                   // 6

Python

딕셔너리 — 완전 정복

Python에서 가장 많이 쓰는 자료구조. 생성, 조작, 컴프리헨션, 병합까지.

# 딕셔너리 생성
d1 = {"name": "Alice", "age": 30}
d2 = dict(name="Bob", age=25)
d3 = dict.fromkeys(["a", "b", "c"], 0)  # {'a':0,'b':0,'c':0}

# 접근 & 안전한 읽기
print(d1["name"])              # Alice
print(d1.get("email", "N/A")) # N/A (KeyError 없음)

# 수정
d1["email"] = "alice@test.com"
d1.update({"age": 31, "city": "Seoul"})

# setdefault — 없으면 설정하고 반환
d1.setdefault("score", 100)

# 순회
for key in d1:          print(key)
for val in d1.values(): print(val)
for k, v in d1.items(): print(f"{k}: {v}")

# 딕셔너리 병합 (Python 3.9+)
merged = d1 | d2      # 새 딕셔너리
d1 |= {"extra": True} # 인플레이스 병합

# 딕셔너리 컴프리헨션
squares = {x: x**2 for x in range(1, 6)}
inverted = {v: k for k, v in squares.items()}

# 중첩 딕셔너리 안전 접근
from collections import defaultdict
word_count = defaultdict(int)
for word in "python is great and python is easy".split():
    word_count[word] += 1
print(dict(sorted(word_count.items(), key=lambda x: -x[1])))

Go

맵 — map[K]V

Go의 해시맵. 키-값 저장소로 동시성에서는 주의가 필요합니다.

package main

import (
	"fmt"
	"sort"
)

func main() {
	// 맵 생성
	m1 := map[string]int{
		"apple":  5,
		"banana": 3,
		"cherry": 8,
	}
	m2 := make(map[string]int)       // 빈 맵
	m2["key"] = 100

	// 읽기 & 존재 확인 (comma-ok 패턴)
	val, ok := m1["apple"]
	if ok {
		fmt.Println("apple:", val)
	}

	val2 := m1["durian"] // 없으면 zero value (0)
	fmt.Println("durian:", val2)

	// 삭제
	delete(m1, "banana")

	// 순회 (순서 보장 안 됨)
	keys := make([]string, 0, len(m1))
	for k := range m1 { keys = append(keys, k) }
	sort.Strings(keys) // 정렬
	for _, k := range keys {
		fmt.Printf("%s: %d
", k, m1[k])
	}

	// 중첩 맵
	graph := map[string][]string{
		"A": {"B", "C"},
		"B": {"D"},
		"C": {"D", "E"},
	}
	fmt.Println(graph["A"])

	// 빈도 카운팅 패턴
	words := []string{"go", "is", "great", "go", "is"}
	freq := make(map[string]int)
	for _, w := range words { freq[w]++ }
	fmt.Println(freq)

	// 구조체를 값으로
	type Point struct{ X, Y int }
	points := map[string]Point{
		"origin": {0, 0},
		"unit":   {1, 1},
	}
	fmt.Println(points["unit"])
}

Java

ConcurrentHashMap

멀티스레드 환경에서 안전한 ConcurrentHashMap의 원리와 사용법입니다.

import java.util.concurrent.*;
import java.util.*;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) throws Exception {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 원자적 연산 (스레드 안전)
        map.put("counter", 0);
        map.compute("counter", (k, v) -> v + 1); // 원자적 갱신
        map.merge("counter", 1, Integer::sum);    // 원자적 병합
        map.putIfAbsent("new", 100);

        // 병렬 벌크 연산 (Java 8+)
        // parallelismThreshold: 이 크기 이상이면 병렬 처리
        map.put("Alice", 90);
        map.put("Bob", 85);
        map.put("Charlie", 95);

        // forEach 병렬
        map.forEach(1, (key, val) ->
            System.out.println(key + "=" + val));

        // search 병렬 — null이 아닌 첫 결과 반환
        String found = map.search(1, (key, val) ->
            val > 90 ? key : null);
        System.out.println("90점 초과: " + found);

        // reduce 병렬
        int total = map.reduce(1,
            (key, val) -> val,
            Integer::sum);
        System.out.println("총점: " + total);

        // 주의: size()는 근사값!
        // mappingCount()가 더 정확
        System.out.println("크기: " + map.mappingCount());
    }
}

JavaScript

맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

// 맵 또는 객체 생성
const user = { name: "user16", level: 16 };
console.log(user.name, user.level);

TypeScript

맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

// 맵 또는 객체 생성
const user = { name: "user16", level: 16 };
console.log(user.name, user.level);

C#

맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

// 맵 또는 객체 생성
using System;
using System.Linq;

var values = new[] { 3, 1, 4, 16 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));

C++

맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

// 맵 또는 객체 생성
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> values = {3, 1, 4, 16};
    std::sort(values.begin(), values.end());
    std::cout << values.front() << "\n";
}

Rust

맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

// 맵 또는 객체 생성
fn main() {
    let mut values = vec![3, 1, 4, 16];
    values.sort();
    println!("{:?}", values);
}

순회 순서를 믿어도 되는 경우

이 부분이 언어 간 이식에서 가장 자주 깨집니다. Go는 맵 순회 순서를 의도적으로 무작위화합니다. 순서에 의존하는 코드가 우연히 동작하다가 나중에 터지는 일을 막으려는 설계이므로, 정렬된 출력이 필요하면 키를 따로 모아 정렬해야 합니다. Java의 HashMap은 순서를 명세하지 않으며, 삽입 순서가 필요하면 LinkedHashMap, 키 정렬이 필요하면 TreeMap을 골라야 합니다. Kotlin의 mapOf는 JVM에서 삽입 순서를 유지하는 구현을 씁니다.

Python의 딕셔너리는 삽입 순서 유지가 언어 명세에 포함되어 있고, JavaScript의 Map도 삽입 순서를 보장합니다. 반면 JavaScript 일반 객체는 규칙이 더 복잡해서, 정수처럼 보이는 키가 먼저 오름차순으로 나오고 나머지 문자열 키가 삽입 순서로 뒤따릅니다. ID를 키로 쓰는 객체에서 순서가 뒤집혀 보이는 원인이 대개 이것입니다. PHP 배열은 삽입 순서를 유지하지만, 숫자 문자열 키가 정수로 자동 변환되는 별도의 함정이 있습니다.

정리하면 세 무리로 나뉩니다. 순서를 보장하지 않는다고 명시한 쪽(Go, Java HashMap), 삽입 순서를 보장하는 쪽(Python, JavaScript Map, PHP, Kotlin), 그리고 키 순서로 항상 정렬해 주는 쪽(TreeMap, C++ std::map)입니다. C++은 특히 std::map이 정렬 기반 트리이고 std::unordered_map이 해시 기반이라, 이름만 보고 고르면 성능 특성이 정반대인 자료구조를 쓰게 됩니다.

같은 키란 무엇인가

키 동등성은 자동으로 되는 것이 아닙니다. Java와 Kotlin에서 직접 만든 클래스를 키로 쓰려면 equals와 hashCode를 짝으로 재정의해야 하며, 하나만 고치면 조회가 실패합니다. Kotlin의 data class와 Java의 record는 이 둘을 자동 생성해 주므로 키 타입으로 안전합니다. Python은 키가 해시 가능해야 하고, 그래서 리스트는 키가 될 수 없지만 튜플은 됩니다. JavaScript 일반 객체는 키를 전부 문자열로 강제 변환하므로 숫자 1과 문자열 "1"이 같은 슬롯을 씁니다. Map은 키 타입을 그대로 유지하지만 객체 키는 참조 동일성으로 비교하니, 내용이 같은 두 객체는 서로 다른 키입니다.

동시성에서 특히 주의할 점이 있습니다. Go의 맵은 동시 읽기·쓰기에서 런타임이 프로그램을 강제 종료시킵니다. sync.Mutex로 감싸거나 sync.Map을 써야 합니다. Java의 HashMap도 스레드 안전하지 않아 ConcurrentHashMap이 필요합니다. Python은 GIL 덕분에 단일 연산은 깨지지 않지만, "읽고 판단하고 쓰기"처럼 여러 연산을 묶은 흐름은 여전히 경쟁 상태가 생깁니다.

기본값 처리 관용구도 미리 익혀 두면 코드가 짧아집니다. Python의 dict.get과 collections.defaultdict, Java의 getOrDefault·computeIfAbsent·merge, Kotlin의 getOrElse, Rust의 entry API가 같은 문제를 각자의 방식으로 풉니다. 특히 카운팅이나 그룹핑에서는 이 API를 쓰는 쪽이 조건문을 직접 쓰는 것보다 경쟁 상태와 중복 조회를 함께 줄여 줍니다. 자세한 예제는 심층 가이드의 각 언어 컬렉션 문서에 있습니다.

맵을 언제 쓰지 말아야 하는가

키-값 조회가 필요하다고 해서 항상 해시맵이 정답은 아닙니다. 원소가 열 개 안팎이라면 배열을 선형 탐색하는 쪽이 해시 계산과 포인터 추적을 건너뛰기 때문에 실제로 더 빠른 경우가 흔합니다. 키가 작은 정수 범위에 몰려 있다면 배열 인덱스를 그대로 쓰는 편이 낫고, 키 집합이 컴파일 시점에 고정되어 있다면 열거형이나 구조체 필드로 표현하는 쪽이 오타를 컴파일러가 잡아 줍니다. 설정 값을 문자열 키 맵으로 들고 다니는 코드가 나중에 "이 키가 어디서 쓰이는지 찾을 수 없는" 상태가 되는 것도 같은 맥락입니다.

반대로 맵이 확실히 맞는 자리는 키 집합이 실행 중에 정해지고, 조회가 삽입보다 훨씬 잦으며, 순서가 의미 없는 경우입니다. 자료구조 선택은 이름이 아니라 이 세 조건으로 판단하시는 편이 좋습니다.