소개
다른 언어들과 달리 Go 언어에서 딕셔너리 (맵) 는 순서가 없는 컬렉션입니다. 이번 실습에서는 딕셔너리를 정렬하는 방법과 이를 더욱 유연하게 사용하는 방법을 배워보겠습니다.
핵심 개념:
- 딕셔너리 정렬
- 딕셔너리의 키와 값 교체하기
- 딕셔너리 슬라이스
- 슬라이스를 값으로 가지는 딕셔너리
- 딕셔너리의 참조 타입 특성
다른 언어들과 달리 Go 언어에서 딕셔너리 (맵) 는 순서가 없는 컬렉션입니다. 이번 실습에서는 딕셔너리를 정렬하는 방법과 이를 더욱 유연하게 사용하는 방법을 배워보겠습니다.
핵심 개념:
~/project 디렉토리에 map.go 파일을 생성합니다.
touch ~/project/map.go
map.go 파일에 다음 코드를 작성합니다.
package main
import (
"fmt"
)
func main() {
// 문자열 키와 정수 값을 가지는 맵을 선언하고 초기화합니다.
// 이 맵은 학생 이름과 점수를 저장합니다.
m := map[string]int{
"Alice": 99, // 각 키 - 값 쌍은 학생과 해당 점수를 나타냅니다.
"Bob": 38,
"Charlie": 84,
}
// for-range 루프를 사용하여 맵을 순회합니다.
// 'key'는 학생 이름, 'value'는 점수를 나타냅니다.
for key, value := range m {
fmt.Println(key, value)
}
fmt.Println("\nInsert Data")
// 맵에 새로운 키 - 값 쌍을 추가하는 방법을 보여줍니다.
// 문법: map[key] = value
m["David"] = 25
// 업데이트된 맵 내용을 보여주기 위해 다시 순회합니다.
// 실행할 때마다 출력 순서가 달라질 수 있음에 유의하세요.
for key, value := range m {
fmt.Println(key, value)
}
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 비슷할 것입니다.
Charlie 84
Bob 38
Alice 99
Insert Data
David 25
Charlie 84
Bob 38
Alice 99
데이터가 삽입된 순서가 고정되어 있지 않기 때문에 출력 결과는 달라질 수 있습니다. 이는 Go 맵의 핵심적인 특징으로, 맵을 순회할 때 요소의 특정 순서를 보장하지 않습니다.
프로그램을 여러 번 실행해 보면 데이터의 출력 순서가 매번 바뀔 수 있음을 확인할 수 있습니다. 이는 맵의 요소 순서에 의존해서는 안 된다는 점을 잘 보여줍니다.
하지만 데이터를 삽입한 후 딕셔너리를 정렬해야 할 때가 있습니다. 어떻게 하면 될까요?
맵 자체는 정렬할 수 없으므로, 맵을 슬라이스로 변환한 다음 해당 슬라이스를 정렬하는 방식을 사용합니다.
먼저, 딕셔너리를 키 (Key) 를 기준으로 정렬하는 방법을 배워보겠습니다.
단계는 다음과 같습니다.
sort 패키지를 사용하여 슬라이스를 정렬합니다.map.go 파일에 다음 코드를 작성합니다.
package main
import (
"fmt"
"sort"
)
func main() {
// 딕셔너리 초기화
m1 := map[int]string{
3: "Bob",
1: "Alice",
2: "Charlie",
}
keys := make([]int, 0, len(m1)) // 용량을 지정하여 슬라이스 초기화. 이는 성능 최적화 기법으로, 슬라이스가 처음부터 맵 크기만큼의 메모리를 할당받아 재할당을 방지합니다.
for key := range m1 {
// 키를 슬라이스에 추가
keys = append(keys, key)
}
// sort 패키지를 사용하여 키 슬라이스 정렬. `sort.Ints()` 함수는 정수 슬라이스를 정렬합니다.
sort.Ints(keys)
for _, key := range keys {
// 이제 순서대로 출력됩니다.
fmt.Println(key, m1[key])
}
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
1 Alice
2 Charlie
3 Bob
이러한 방식을 통해 키를 기반으로 딕셔너리를 정렬하는 효과를 얻었습니다. 키를 슬라이스로 추출하여 정렬한 뒤, 이를 이용해 맵에서 값을 찾아 출력한 것입니다.
값 (Value) 을 기준으로 정렬하는 방법을 설명하기 전에, 딕셔너리의 키와 값을 서로 바꾸는 방법을 먼저 알아보겠습니다.
키와 값의 교체란 아래 그림과 같이 딕셔너리 내에서 키와 값의 위치를 서로 바꾸는 것을 의미합니다.

구현 코드는 간단합니다. map.go 파일에 다음 코드를 작성하세요.
package main
import "fmt"
func main() {
m := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
m2 := map[int]string{}
for key, value := range m {
m2[value] = key
}
fmt.Println(m2)
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
map[38:Bob 84:Charlie 99:Alice]
이 코드의 핵심은 원래 딕셔너리에서 키와 값을 추출한 다음, 역할을 바꾸어 새로운 딕셔너리에 다시 삽입하는 것입니다. 매우 직관적입니다. 다만 맵은 순서가 없으므로, 교체된 맵의 출력 순서는 실행 시마다 달라질 수 있습니다.
키 기준 정렬 로직과 키 - 값 교체 로직을 결합하면 딕셔너리를 값을 기준으로 정렬할 수 있습니다.
작동 원리는 이렇습니다. 먼저 키와 값을 서로 바꾼 뒤, 바뀐 키 (원래의 값) 를 기준으로 정렬을 수행합니다. 그런 다음 정렬된 "키"(원래의 값) 를 사용하여 교체된 맵에서 원래의 키를 찾아냅니다.
map.go 파일에 다음 코드를 작성합니다.
package main
import (
"fmt"
"sort"
)
func main() {
// 딕셔너리 초기화
m1 := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 키와 값이 반전된 딕셔너리 초기화
m2 := map[int]string{}
for key, value := range m1 {
// 키 - 값 쌍을 교체하여 반전된 딕셔너리 m2 생성
m2[value] = key
}
values := make([]int, 0) // 정렬용 슬라이스 초기화
for _, value := range m1 {
// 원래 딕셔너리의 값들을 슬라이스에 추가
values = append(values, value)
}
// sort 패키지를 사용하여 값 슬라이스 정렬
sort.Ints(values)
for _, value := range values {
// 이제 순서대로 출력됩니다.
fmt.Println(m2[value], value)
}
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
Bob 38
Charlie 84
Alice 99
이제 값을 기준으로 딕셔너리를 정렬했습니다. 값들을 슬라이스로 변환하여 정렬한 뒤, 정렬된 값을 이용해 반전된 맵에서 원래의 키를 찾아 출력했습니다.
Go 버전이 1.7 이상이라면 sort.Slice 함수를 사용하여 맵을 키나 값으로 더 빠르게 정렬할 수 있습니다. sort.Slice를 사용하면 사용자 정의 비교 함수를 지정할 수 있습니다.
다음은 예시입니다.
package main
import (
"fmt"
"sort"
)
func main() {
m1 := map[int]int{
21: 99,
12: 98,
35: 17,
24: 36,
}
type kv struct {
Key int
Value int
}
var s1 []kv
for k, v := range m1 {
s1 = append(s1, kv{k, v})
}
sort.Slice(s1, func(i, j int) bool {
return s1[i].Key < s1[j].Key
})
fmt.Println("Sorted in ascending order by key:")
for _, pair := range s1 {
fmt.Printf("%d, %d\n", pair.Key, pair.Value)
}
}
프로그램을 실행합니다.
go run ~/project/map.go
출력 결과는 다음과 같습니다.
Sorted in ascending order by key:
12, 98
21, 99
24, 36
35, 17
이 프로그램에서는 맵의 키 - 값 쌍을 저장하기 위해 kv 구조체를 사용했습니다. 그런 다음 sort.Slice() 함수와 익명 비교 함수를 사용하여 구조체 슬라이스를 정렬했습니다. 이 비교 함수 (func(i, j int) bool) 는 구조체의 Key 필드를 기준으로 정렬 순서를 결정합니다.
이 비교 함수를 수정하면 키를 기준으로 내림차순 정렬하거나, 값을 기준으로 오름차순 정렬하는 등 맵 데이터를 원하는 방식대로 매우 유연하게 정렬할 수 있습니다.
map2.go 파일을 생성하고 이전 섹션의 코드를 수정하여 맵을 값을 기준으로 내림차순 정렬하도록 만드세요.
예상 출력:
프로그램 실행:
go run ~/project/map2.go
Sorted in descending order by value:
21, 99
12, 98
24, 36
35, 17
요구 사항:
map2.go 파일은 ~/project 디렉토리에 위치해야 합니다.sort.Slice 함수를 사용해야 합니다. 이전 예제에서 사용된 sort.Slice의 비교 함수를 수정해야 할 것입니다.배열이나 슬라이스를 사용하여 관련 데이터를 저장하는 것처럼, 요소가 딕셔너리인 슬라이스를 사용할 수도 있습니다. 이를 통해 맵 데이터의 컬렉션을 관리할 수 있으며, 구조화된 정보를 다룰 때 매우 유용합니다.
map.go 파일에 다음 코드를 작성합니다.
package main
import "fmt"
func main() {
// 맵 슬라이스를 선언하고 make 를 사용하여 초기화합니다.
var mapSlice = make([]map[string]string, 3) // 각 요소가 `map[string]string` 인 용량 3 의 슬라이스를 생성합니다.
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("Initialization")
// 슬라이스의 첫 번째 요소에 값을 할당합니다.
mapSlice[0] = make(map[string]string, 10) // 첫 번째 인덱스에 맵을 생성합니다.
mapSlice[0]["name"] = "labex"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "Paris"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
Initialization
index:0 value:map[address:Paris name:labex password:123456]
index:1 value:map[]
index:2 value:map[]
이 코드는 맵 슬라이스의 초기화 과정을 보여줍니다. 처음에는 슬라이스의 각 요소가 빈 맵입니다. 그 후 첫 번째 요소에 맵을 생성하여 할당하고 데이터를 채워 넣습니다. 이는 맵들의 목록을 관리하는 방법을 잘 보여줍니다.
딕셔너리 내에 더 많은 데이터를 저장하기 위해 슬라이스를 값으로 사용할 수도 있습니다. 이를 통해 맵의 단일 키에 여러 값을 연결할 수 있으며, 결과적으로 "일대다 (one-to-many)" 관계를 형성할 수 있습니다.
map.go 파일에 다음 코드를 작성합니다.
package main
import "fmt"
func main() {
var sliceMap = make(map[string][]string, 3) // 키는 문자열이고 값은 문자열 슬라이스인 맵을 선언합니다. 3 은 용량 힌트입니다.
key := "labex"
value, ok := sliceMap[key] // 키가 존재하는지 확인합니다.
if !ok {
value = make([]string, 0, 2) // 존재하지 않는다면 용량을 지정하여 새 슬라이스를 초기화합니다.
}
value = append(value, "Paris", "Shanghai") // 슬라이스에 값을 추가합니다.
sliceMap[key] = value // 슬라이스를 맵의 해당 키에 대한 값으로 설정합니다.
fmt.Println(sliceMap)
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
map[labex:[Paris Shanghai]]
이 코드는 먼저 값이 슬라이스인 맵 타입을 선언합니다. 연결된 슬라이스에 새 항목을 추가하기 전에 키의 존재 여부를 확인하는데, 이는 슬라이스를 포함하는 맵을 다룰 때 흔히 사용되는 패턴입니다.
배열은 값 타입 (Value Type) 이므로, 함수에 전달하거나 할당할 때 복사본이 생성됩니다. 따라서 복사본을 수정해도 원본 배열에는 영향을 주지 않습니다. 하지만 맵은 참조 타입 (Reference Type) 입니다. 즉, 맵을 할당하거나 함수에 전달할 때 전체 복사본이 생성되지 않고 참조 (주소) 가 전달됩니다.
이는 매우 중요한데, 함수 내부에서 맵을 변경하면 원본 맵 데이터에도 직접적인 영향을 미치기 때문입니다.
map.go 파일에 다음 코드를 작성합니다.
package main
import "fmt"
func modifyMap(x map[string]int) {
x["Bob"] = 100 // 인자로 전달된 맵을 수정합니다.
}
func main() {
a := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 맵은 참조로 전달되므로, modifyMap 에서의 수정이 원본 딕셔너리를 변경합니다.
modifyMap(a)
fmt.Println(a) // map[Alice:99 Bob:100 Charlie:84]
}
프로그램을 실행합니다.
go run ~/project/map.go
프로그램의 출력 결과는 다음과 같습니다.
map[Alice:99 Bob:100 Charlie:84]
이 예제에서는 딕셔너리의 참조 전달 특성을 확인했습니다. a는 내부적으로 동일한 맵 데이터를 가리키는 참조이므로, modifyMap 함수가 원본 맵을 변경하게 됩니다. 맵을 함수 인자로 사용할 때 이 동작 방식을 이해하는 것은 매우 중요합니다.
이 실습에서는 다음과 같은 Go 맵의 고급 활용법을 배웠습니다.
이러한 개념들을 이해하면 실제 애플리케이션에서 Go 맵을 더욱 효과적으로 활용할 수 있습니다.