테스트와 벤치마크
Go 언어는 테스트 코드 작성과 실행을 언어 자체에서 지원합니다. 빠르고 손쉽게 테스트 코드를 작성할 수 있어 버그를 사전에 막는 데 효과적입니다.
다음의 3가지 표현 규약을 따라 테스트 코드를 작성해야 합니다.
- 파일명이 _test.go 로 끝나야 합니다.
- testing 패키지를 import해야 합니다.
- 테스트 코드는 func TestXxxx(t *testing.T) 형태여야 합니다.
다음은 테스트 코드 예시입니다.
// sample.go
package main
import "fmt"
func squre(x int) int {
return x * x
}
func main() {
fmt.Printf(square(9)
}
// sample_test.go
package main
import "testing"
func TestSquare(t *testing.T) {
rst := sqaure(9)
if rst != 81 {
t.Errorf("sqaure(9) returns %d", rst)
}
}
testing.T 객체의 Error()와 Fail() 메서드를 이용해서 테스트 실패를 알릴 수 있습니다. Error()는 테스트가 실패하면 모든 테스트를 중단하지만, Fail()은 테스트가 실패해도 다른 테스트들을 계속 진행합니다.
go test
go test -run 테스트명
go test 명령어는 모든 테스트를 실행하고, -run 플래그를 추가해 특정 테스트만 진행할 수도 있습니다.
◎ "stretchr/testify"
위 코드를 테스트 코드 작성을 돕는 외부 패키지인 "stretchr/testify"를 사용해서 작성해보겠습니다.
go get github.com/stretchr/testify
터미널을 열어서 go get 명령으로 패키지를 설치합니다.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSquare(t *testing.T) {
assert := assert.New(t)
assert.Equal(81, square(9), "square(9) should be 81")
}
위 코드와 같이 테스트 코드를 작성할 수 있습니다.
그 외에도 stretchr/testify/assert 패키지에서는 Equal(), Greater(), Len(),NotNilf(), NotEqualf() 함수도 제공합니다.
또한 stretchr/testify 에서는 목업 객체를 제공하는 mock 패키지와 테스트의 준비 작업이나 종료 후 뒤처리 작업을 도와주는 suite 패키지도 제공합니다.
테스트 주도 개발(TDD)
테스트의 중요성은 과거의 비해 점차 커지고 있습니다. 첫 번째 이유는 과거에 비해서 프로그램 규모가 커졌다는 것이고, 두 번째 이유는 고가용성(high availability)에 대한 요구사항이 높아진 것입니다. 고가용성이란 서비스가 얼마나 오랫동안 정상 동작하는가를 의미합니다. 무중단 서비스에 대한 사용자 요구가 늘어나면서 서비스 중단으로 이어지는 치명적 버그를 줄이는 것이 중요해졌습니다.
블랙박스 테스트
블랙박스 테스트란 제품 내부를 오픈하지 않은 상태에서 진행되는 테스트를 말합니다. 사용자 입장에서 테스트한다고 해서 사용성 테스트(usability test)라고 하기도 합니다. 블랙박스 테스트는 프로그램 내부 코드를 직접 검증하는 게 아니라 프로그램을 실행한 상태로 실행 동작을 검증하는 방식으로 주로 QV, QA 직군에서 담당합니다.
화이트박스 테스트
화이트박스 테스트는 프로그램 내부 코드를 직접 검증하는 방식입니다. 유닛 테스트(unit test)라고 부릅니다. 이 테스트는 프로그래머가 직접 테스트 코드를 작성해서 내부 테스트를 검사하는 방식입니다. 테스트는 일반적으로 다음과 같이 진행됩니다.
코드작성 -> 테스트 -> 코드 수정 -> ...(반복) -> 완성
이러한 방식에는 두 가지 문제점이 있습니다.
첫 번째 문제는 빈약한 테스트 케이스입니다. 코드 작성 이후에 테스트 코드를 작성하다 보면 메인 시나리오에 의존해 테스트하고 예외 상황이나 경게 체크가 무시되기 쉽습니다. 두 번째 문제는 테스트 통과를 목적으로 형식적인 테스트 코드를 작성하기 쉽다는 것입니다.
테스트 주도 개발(Test Driven Development)은 이런 문제를 해결하는 대안입니다.
테스트 작성 -> 테스트 실패 -> 코드 작성 -> 테스트 성공 -> 개선 -> ...(반복) -> 완성
제일 먼저 테스트 코드부터 작성합니다. 구현하기 전에 테스트 코드부터 작성하기 때문에 당연히 테스트가 실패합니다. 방금 작성한 테스트를 성공시키는 코드를 작성해 테스트를 통과시키고 개선 작업을 통해 코드를 개선합니다. 개선은 SOLID 원칙에 입각해 리팩터링을 진행합니다.
벤치마크
Go 언어는 테스트 외 코드 성능을 검사하는 벤치마크 기능도 지원합니다. 마찬가지로 testing 패키지를 통해 제공되고 다음과 같은 표현 규약을 갖고 있습니다.
- 파일명이 _test.go 로 끝나야 합니다.
- testing 패키지를 import해야 합니다.
- 테스트 코드는 func BenchmarkXxxx(b *testing.B) 형태여야 합니다.
반복문을 사용하여 테스트 코드를 작성하고 성능을 테스트해봅니다.
func BenchmarkFibonacci(b *testing.B) {
for i = 0; i < b.N; i++ {
fibonacci(20)
}
}
Go는 N 값을 적절히 증가시키면서 충분한 테스트를 진행해 함수 성능을 테스트합니다.
go test - bench .
위 명령어를 사용하여 현재 폴더의 모든 테스트와 벤치마크 테스트를 실행할 수 있습니다.
웹 서버 만들기
Go 언어에서는 net/http 패키지를 사용하여 손쉽게 웹 서버를 만들 수 있습니다. 몇 줄 안되는 코드로 강력한 웹 서버를 만들 수 있기 때문에 웹 서버를 만들 때 Go를 자주 사용합니다. Go 언어에서 웹 서버를 만드려면 핸들러 등록과 웹 서버 시작이라는 두 단계를 거쳐야 합니다.
1. 핸들러 등록
각 HTTP 요청 URL 경로에 대응할 수 있는 핸들러를 등록해야하는데, 핸들러는 구현체와 함수 두 가지 방법으로 등록할 수 있습니다.
- Handler 인터페이스의 구현체
type Handler interface {
serveHTTP(ResponseWriter, *Rquest)
}
type IndexPathHandler struct {}
func (h IndexPathHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//
}
func main() {
http.handle("/", IndexPathHandler{})
}
- HandleFunc() 함수
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
func IndexPathHandler(w http.ResponseWriter, r *http.Request) {
//
}
func main() {
http.HandleFunc("/", IndexPathHandler)
}
HandlerFunc 타입은 일반 함수를 HTTP 핸들러로 사용할 수 있게 해주는 어댑터로 Handler의 serveHTTP()를 구현하고 있습니다. (Go에서는 함수 타입도 메서드를 정의할 수 있기 때문에, 인터페이스를 구현할 수 있습니다.)
2. 웹 서버 시작
이렇게 각 경로에 대한 핸들러 등록을 마치면 본격적으로 웹 서버를 시작하게 됩니다.
ListenAndServe() 함수를 호출해 웹 서버를 시작합니다.
func ListenAndServe(addr string, handler Handler) error
첫 번째 인수인 addr은 HTTP 요청을 수신하는 주소를 나타냅니다. 일반적으로 ":3000" 과 같이 요청을 수신하는 포트 번호를 적어주게 됩니다.
두 번째 인수는 핸들러 인스턴스를 넣어주게 됩니다. 이 값을 nil로 넣으면 디폴트 핸들러가 실행됩니다. http.HandleFunc() 함수로 등록할 때는 두 번째 인수로 nil을 넣어줍니다.
Hello World 를 출력하는 웹 서버를 만들어 보겠습니다.
package main
import(
"fmt"
"net/http"
)
func main() {
http.handleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})
http.ListenAndServe(":3000", nil)
}
단 6줄로 웹 서버를 만들었습니다.
Fprint() 는 사용자가 지정한 출력 스트림에 값을 쓰는 함수입니다.
HTTP 쿼리 인수 사용하기
package main
import (
"fmt"
"net/http"
"strconv"
)
func barHandler(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
name := values.Get("name")
if name == "" {
name = "World"
}
if, _ := strconv.Atoi(values.Get("id"))
fmt.Fprintf(w, "Hello %s! id:%d", name, id)
}
func main() {
http.HandleFunc("/bar", barHandler)
http.ListenAndServe(":3000", nil)
}
ServeMux 인스턴스 이용하기
앞서 ListenAndServe() 함수 두 번째 인수로 nil을 넣어서 DefaultServeMux를 사용하는 예제를 보았습니다.
DefaultServeMux를 사용해도 문제는 없지만, 별도의 Mux를 만들어 사용하면 Router의 기능도 사용할 수 있고 테스트에도 용이합니다.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})
mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello Bar")
})
http.ListenAndServe(":3000", mux)
}
Mux는 Multiplexer의 약자로 여러 입력 중 하나를 선택해서 반환하는 디지털 장치를 의미합니다.
FileServer
HTML의 <img src="">태그와 같은 정적 이미지 요청을 처리하는 방법에 대해 알아보겠습니다. 일반적으로 Go에서는 정적파일을 /static 에 저장합니다. 다음은 "/" 경로에 static 폴더를 매핑해준 코드입니다. 이 경우 서버 내 /static에서 파일을 찾게 됩니다.
다음 코드를 실행하고 웹 브라우저에서 http://localhost:3000/sample.jpg 를 요청하면 됩니다.
func main() {
http.Handle("/", http.FileServer(http.Dir("static")))
http.ListenAndServe(":3000", nil)
}
그러면 만약 다음과 같이 작성한다면 어떻게 될까요? "/static/" 경로에 static 폴더를 매핑해주었기 때문에 /static/sample.jpg로 요청이 들어오면 서버 내 /static/static 에서 파일을 찾게 됩니다.
http.Handle("/static/", http.FileServer(http.Dir("static")))
서버의 올바른 정적파일 경로인 /static 에서 처리하기 위해서는 다음과 같이 중복된 경로를 제거해줘야 합니다.
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
웹 서버 테스트 코드 만들기
테스트를 위해 앞서 ServeMux를 사용해 만들었던 Handler를 별도의 메서드로 분리했습니다.
func MakeWebHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})
mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello Bar")
})
return mux
}
func main() {
http.ListenAndServe(":3000", MakeWebHandler())
}
package main
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIndexHandler(t *testing.T) {
assert := assert.New(t)
res := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
mux := MakeWebHandler()
mux.ServeHTTP(res, req)
assert.Equal(http.StatusOK, res.Code)
data, _ := io.ReadAll(res.Body)
assert.Equal("Hello World", string(data))
}
func TestBarHandler(t *testing.T) {
assert := assert.New(t)
res := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/bar", nil)
mux := MakeWebHandler()
mux.ServeHTTP(res, req)
assert.Equal(http.StatusOK, res.Code)
data, _ := io.ReadAll(res.Body)
assert.Equal("Hello Bar", string(data))
}
JSON 데이터 전송
JSON은 자바스크립트 오브젝트 표기법의 약자로 말 그대로 자바스크립트에서 오브젝트를 표현하는 방법으로 사용되는 포맷입니다. 하지만 이 표기법이 매우 간단하기 때문에 자바스크립트뿐 아니라 다양한 용도로 광범위하게 사용됩니다. encoding/json 패키지를 사용해 구조체를 JSON 데이터로 변환하고, JSON을 구조체로 변환하는 예제와 테스트를 작성해보겠습니다.
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Student struct {
Name string
Age int
Score int
}
func MakeWebHandler() http.Handler {
mux := http.NewServerMux()
mux.HandleFunc("/student", StudentHandler)
return mux
}
func StudentHandler(w http.ResponseWriter, r *http.Request) {
var student = Student{"aaa", 16, 87}
data, _ := json.Marshal(student)
w.Header().Add("content-type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fpint(w, string(data))
}
func main() {
http.ListenAndServe(":3000", MakeWebHandler())
}
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestJsonHandler(t *testing.T) {
assert := assert.Net(t)
res := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/student", nil)
mux := MakeWebHandler()
mux.ServeHTTP(res, req)
assert.Equal(http.StatusOK, resCode)
student := New(Student)
err := json.NewDecoder(resBody).Decode(student)
assert.Nil(err)
assert.Equal("aaa", student.Name)
assert.Equal(16, student.Age)
assert.Equal(87, student.Score)
}
HTTPS 웹서버 만들기
HTTPS는 HTTP에 보안 기능을 강화한 프로토콜입니다. 본래 HTTP는 보안을 염두에 두지 않고 설계된 프로토콜이라 모든 요청이 평문(일반 문자열)입니다. 그래서 보안에 매우 취약해, 스니핑과 같은 해킹으로 HTTP 전문을 볼 수 있어서 비밀번호, 개인정보와 같은 중요 데이터를 보호하지 못하는 문제가 있습니다. 이를 방지하고자 나온 게 HTTPS 입니다. HTTPS는 기본 HTTP 요청과 응답을 공개키 암호화방식을 사용해서 암호화한 프로토콜입니다. 패킷이 암호화되기 때문에 해커가 스니핑을 한다고해도 어떤 내용인지 알 수 없어서 비밀번호나 개인정보가 노출되지 않게 됩니다.
공개키 암호화 방식
공개키 암호화 방식은 공개키와 비밀키 두 가지 키를 생성해서 공개키는 클라이언트에 알려주고 비밀키는 서버에 비공개 상태로 놔두게 됩니다. 클라이언트에서 HTTPS 요청을 보낼 때 공개키로 암호화하고 서버는 비밀키를 이용해서 다시 원문으로 돌리는 복호화를 하게 됩니다. 그래서 해커가 공개키를 획득한다 해도 비밀키가 없으면 메시지를 복호화할 수 없어 안전합니다. 공개키 암호화 방식은 암호화와 복호화에 쓰이는 키가 서로 다르기 때문에 비대칭 암호화 방식입니다. 공개키 암호화 방식에서는 비밀키가 노출되지 않도록 각별히 주의해야 합니다.
공개키 암호화 원리에 대해서는 칸 아카데미에서 제작한 영상이 매우 유익합니다. (https://ko.khanacademy.org/computing/computer-science/cryptography/modern-crypt/v/diffie-hellman-key-exchange-part-1)
HTTPS 서버를 실행하려면 비밀키와 함께 인증서도 필요합니다. 공개키 암호화 방식은 통신 내용이 암호화되기 때문에 해커가 통신 내용을 감청해도 안전합니다. 그러나 해커가 겉모양을 똑같이 만든 뒤 사용자의 개인정보를 입력하게 만드는 피싱(phishing) 사이트를 만들 수 있기 때문에 별도 외부 공인기관을 통해 신뢰할 수 있는 웹사이트인지 인증을 합니다.
서버는 자신의 공개키, 도메인 정보, 그리고 CA의 비밀키로 서명된 서명을 포함한 인증서(cert.pem)를 클라이언트에게 전달합니다. 클라이언트는 브라우저에 내장된 CA의 공개키로 인증서의 서명을 검증하고, 유효한 경우 인증서에 포함된 서버의 공개키를 사용해 Pre-Master Secret을 암호화해 서버로 전송합니다. 서버는 자신의 비밀키(key.pem)로 이를 복호화하고, 이후 양측은 세션 키를 생성하여 안전한 통신을 시작합니다.
'Lang > Go' 카테고리의 다른 글
| Go (9) - 더 생각해보기 (0) | 2025.04.25 |
|---|---|
| Go (8) - Go 문법 추가 내용 (0) | 2025.04.25 |
| Go (6) - 고루틴과 동시성 프로그래밍, 채널과 컨텍스트, Generic (0) | 2025.04.17 |
| Go (5) - 함수 고급, 자료구조, 에러 핸들링 (0) | 2025.04.13 |
| Go (4) - 슬라이스, 메서드, 인터페이스 (0) | 2025.04.13 |