TL;DR: 순서 보장하지 않음

사내 API 서버를 외부에서 사용하기 위해서는 API 게이트웨이를 통해 hmac인증을 해야 한다. 요청을 보내야 하는 url에 해시로 만든 새로운 파라미터를 뒤에 이어붙여 보내면 외부에서 API를 사용할 수 있게 된다.

hmac이야 간단해서 만드는 것은 문제가 없었는데 자꾸 에러가 발생했다. 만들어낸 주소를 python의 requests에서 테스트해도 문제가 없고, 웹 브라우저를 통해 테스트해봐도 문제가 발생하지 않는다. go에서의 gorequest에서만 문제가 발생하는 것이었다. 잘 이해가 가지 않아서 생각을 해보니, 결국 에러가 발생하는 이유는 gorequest에서 뭔가를 다르게 처리한다는 것이므로 깃허브에서 코드를 좀 살펴보니, 요청을 보내기 전에 MakeRequest를 통해 요청을 만든다는 것을 알게 되었다. 코드를 계속 살펴보다 의심되는 부분을 찾게 되어 확인차, 요청을 보낸 후 http.request의 URL을 찍어 보았다. 원인은 hmac 생성에 사용한 파라미터의 순서와 실제 요청에 사용한 쿼리 파라미터의 순서가 달랐던 것.

net/url의 values.Encode()에서 쿼리의 순서를 정렬하여 변경한다. 해당 부분의 코드는 다음과 같다.

// Encode encodes the values into ``URL encoded'' form
// ("bar=baz&foo=quux") sorted by key.
func (v Values) Encode() string {
	if v == nil {
		return ""
	}
	var buf bytes.Buffer
	keys := make([]string, 0, len(v))
	for k := range v {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		vs := v[k]
		prefix := QueryEscape(k) + "="
		for _, v := range vs {
			if buf.Len() > 0 {
				buf.WriteByte('&')
			}
			buf.WriteString(prefix)
			buf.WriteString(QueryEscape(v))
		}
	}
	return buf.String()
}

Map?

url.Values는 뭐냐 하면 아래와 같이 정의된다.

type Values map[string][]string

처음에는 map의 key를 순회할 때 순서가 항상 다르다는 것을 예상하지 못했다. 결국 간단한 코드를 작성해 보고 나서야 이를 알게 됐는데 결과는 다음과 같다.

$ cat testmap.go
package main

import "fmt"

func main() {
	testmap := map[string]string{
		"a": "aa",
		"b": "bb",
		"c": "cc",
		"d": "dd",
		"e": "ee",
	}

	for i := 0; i < 10; i++ {
		keys := []string{}
		for k, _ := range testmap {
			keys = append(keys, k)
		}
		fmt.Println(keys)
	}
}

$ go run testmap.go
[d e a b c]
[e a b c d]
[a b c d e]
[e a b c d]
[e a b c d]
[a b c d e]
[d e a b c]
[a b c d e]
[a b c d e]
[b c d e a]

실제로 go blog를 조금 살펴보면 다음과 같은 내용을 확인할 수 있다.

When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. Since Go 1 the runtime randomizes map iteration order, as programmers relied on the stable iteration order of the previous implementation. If you require a stable iteration order you must maintain a separate data structure that specifies that order. This example uses a separate sorted slice of keys to print a map[int]string in key order:

결국 map에서는 순서 보장 안하니까, 일정한 순서를 원하면 키를 정렬해서 쓰라는 것이다. 만약 net/url의 url.Values.Encode()에서 정렬을 하지 않게 된다면 사용자가 요청하는 주소는 매번 다르게 될 것이니, 아마 그런 이유로 정렬을 했던 것이 아닐까?

그러고 보면 python 3.6 이상부터는 dict의 순서를 보장하도록 변경되었는데, 물론 그 전에는 dict도 순서가 항상 이상하게 나와서 collection의 OrderedDict를 썼던 기억이 있다.