실제 카탈로그 데이터 반환하기

GolangBeginner
지금 연습하기

소개

지역 안내소에는 작은 분실물 모음이 있습니다. 준비된 웹페이지는 물품을 표시할 수 있지만, 예제 API는 레코드 하나만 반환합니다. 실제 Go 레코드를 목록 및 상세 응답에 연결하여 방문자가 올바른 수령 장소를 찾도록 만듭니다.

Go 함수, 구조체, 슬라이스, 맵, 모듈과 기본 터미널 경로를 이미 알고 있어야 합니다. HTTP, JSON, 핸들러 테스트는 여기서 소개합니다. 브라우저 환경에는 Go와 준비된 페이지가 포함되어 있습니다. 이 단원은 독립적으로 시작하며 다른 랩의 파일이 필요하지 않습니다. 읽기와 일반적인 실수를 포함해 약 55분을 예상하세요. 이는 작성자의 추정치입니다.

목록을 유지하며 물품 하나 선택하기

이 단계에서는 동작하는 한 카드 모음을 유지하면서 실제 물품 상세 정보를 추가합니다. Workspace 탭에서 시작하세요. 셸은 /home/labex/project/lost-found로 들어갑니다. 나중에 돌아오면 다음 cd로 그 위치를 복원할 수 있습니다.

Go는 /usr/local/go/bin에 설치되어 있습니다. export는 기존 $PATH를 유지하면서 이 디렉터리를 현재 셸의 명령 검색 경로에 추가합니다. 현재 터미널 세션에만 적용되므로 새 셸에서는 반복하세요. 전역 셸 설정은 바꾸지 않습니다.

cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH

curl은 요청을 보냅니다. -i는 응답 헤더를 포함하고 작은따옴표는 URL을 하나로 유지합니다. GET은 리소스를 읽도록 요청합니다. /api/items는 모음을 나타내며 127.0.0.1:8080은 같은 머신의 서비스입니다. 먼저 정상 예제를 사용하세요.

curl -i 'http://127.0.0.1:8080/api/items'

200 OK, Content-Type: application/json 헤더, 도서관의 파란 우산 LF-101을 포함한 배열이 예상됩니다. HTTP 상태는 요청 결과를 설명하고 본문은 데이터를 담습니다. JSON은 배열에 대괄호, 객체에 중괄호를 사용합니다. 200만으로는 전체 모음이 있다는 것을 증명하지 못합니다. Lost and Found로 전환해 카드 하나를 확인한 뒤 Workspace로 돌아오세요.

cat은 파일을 편집하지 않고 표시합니다. 원본 레코드, 입력 구조체, 작은 시작 핸들러를 읽으세요.

cat items.json
cat main.go
cat handler.go

레코드는 여섯 개입니다. Item은 ID, Name, Category, Location을 묶으며 대문자로 시작하는 내보낸 필드는 인코딩할 수 있습니다. `json:"id"` 같은 태그는 소문자 JSON 키를 지정합니다. main.go는 모음을 로드하고 준비된 페이지를 제공하며 물품 슬라이스를 catalogHandler에 넘깁니다. main.go, go.mod, items.json, serve.py, web/는 보존하고 handler.go와 추가 Go 파일이나 테스트만 편집하세요.

코드를 바꾸기 전에 LF-101 요청과 LF-103 요청의 차이를 예측하세요. 이름은 둘 다 Blue umbrella이지만 장소는 Library desk와 Cafe counter입니다. 어떤 필드로 레코드 하나만 선택할 수 있을까요? 이제 두 번째 ID를 요청하세요.

curl -i 'http://127.0.0.1:8080/api/items/LF-103'

시작 핸들러는 여전히 첫 레코드의 배열을 반환합니다. 응답은 받았지만 올바른 상세 정보는 아닙니다. 현재 목록 응답을 계속 사용할 수 있게 하면서 ID로 선택한 객체를 반환하는 것이 과제입니다. 모든 경로에 첫 객체를 반환하는 방식으로 해결하지 마세요.

핸들러는 요청마다 호출되는 Go 코드입니다. r *http.Request는 들어오는 요청을 가리킵니다. w http.ResponseWriter는 응답 목적지입니다. Header는 헤더를 설정하고 쓰기는 본문을 보냅니다. http.Handler는 이 동작을 설명하며 http.HandlerFunc는 함수를 그 동작에 맞춥니다. 반환되는 익명 함수는 전달된 items를 기억합니다. HTTP 파서를 작성할 필요는 없습니다.

편집 전에 복사본을 저장하세요. cp는 파일을 복사합니다. .backup 확장자는 의도적으로 .go를 피한 것입니다. 그 확장자라면 Go가 패키지를 컴파일할 때 중복 정의가 생깁니다.

cp handler.go handler.go.backup

터미널 편집기에서 실제 파일을 여세요. 명령에만 적용하는 LC_ALL=C.UTF-8은 nano가 악센트 문자를 포함한 텍스트를 UTF-8로 해석하게 합니다. 이 편집기 실행에만 영향을 주며 전역 셸 설정에는 영향을 주지 않습니다.

LC_ALL=C.UTF-8 nano handler.go

nano에서 Ctrl은 Control을 누른 채 해당 키를 누르라는 뜻입니다. 방향키로 이동하세요. Ctrl+W를 누르고 catalogHandler를 입력한 뒤 Enter로 함수를 찾으세요. 끝내지 않은 검색은 Ctrl+C로 취소합니다. 일반 입력, Backspace, Delete로 편집하세요. Ctrl+K는 현재 줄을 잘라내므로 교체하려는 줄에만 사용하세요. 아래쪽 단축키 표시는 ^로 Control을 나타냅니다. 두 번째 프로그램을 만드는 것이 아니라 기존 핸들러를 편집하는 작업입니다.

반환되는 함수 안에서 기존 content-type 헤더 다음에 아래 판단을 구현하세요. 한 번에 분기 하나씩 작업하세요.

먼저 r.URL.Path를 읽으세요. /api/items와 같으면 시작 코드의 items[:1] 배열 인코딩을 유지하고 즉시 반환하세요. 상세 정보를 작업하는 동안 정상적인 한 카드 목록을 보존합니다.

다음으로 그 밖의 경우에는 /api/items/ 접두사를 확인하세요. 기존 import 문 옆에 표준 라이브러리 strings를 추가하세요. strings.HasPrefix(path, prefix)는 접두사를 확인하고 strings.TrimPrefix(path, prefix)는 제거합니다. 남은 ID를 지역 변수에 저장하세요.

이어서 for _, item := range items로 전달된 슬라이스를 순회하세요. 이름이 아니라 item.ID를 요청한 ID와 비교하세요. 일치하면 해당 item 객체를 인코딩하고 즉시 반환하세요. json.NewEncoder(w).Encode(value)는 선택한 Go 값을 JSON으로 씁니다. 슬라이스는 배열, 구조체 하나는 객체가 됩니다. 반복문은 items[0]만이 아니라 모든 레코드를 살펴야 합니다.

그 뒤 ID가 일치하지 않았거나 경로가 이 경로들 밖이라면 JSON 오류를 쓰기 전에 404를 설정하세요. w.WriteHeader(http.StatusNotFound)는 상태를 설정합니다. 작은 JSON 호환 오류 값으로 map[string]string{"error": "item not found"}를 쓸 수 있습니다. error 문자열은 비어 있으면 안 됩니다. 그 맵을 인코딩하세요. 오류 뒤에 다른 목록을 쓰지 마세요.

응답의 첫 쓰기는 보통 200을 확정하므로 다른 상태는 인코딩 전에 설정하세요. Encode는 error를 반환하며 nil은 실패가 없다는 뜻입니다. 이 예제의 모든 값은 JSON 호환 문자열입니다. 클라이언트 연결이 끊기면 쓰기가 실패할 수는 있습니다. 본문 전송이 시작된 뒤 두 번째 응답을 보내도 연결을 복구할 수 없습니다. 조기 반환은 두 응답이 이어 붙는 일도 막습니다.

Ctrl+O로 저장하고 기존 파일 이름 handler.go를 Enter로 확인한 뒤 Ctrl+X로 종료하세요. 저장한 파일을 살펴보세요.

cat handler.go

잘못된 파일을 열었다면 저장하지 않고 종료하세요. Ctrl+X를 누르고 저장 여부를 물으면 N을 누르세요. pwd로 현재 위치를 확인하고 첫 cd로 돌아간 뒤 올바른 파일 이름을 다시 여세요. 저장 프롬프트에 의도하지 않은 이름이 보이면 Ctrl+C로 취소합니다. 편집 실수를 만회하려고 원본 데이터를 덮어쓰지 마세요.

gofmt -w는 지정한 Go 파일을 직접 포맷합니다. 준비된 serve.py는 코드를 빌드한 뒤 이 작업 환경에 기록된 서버만 교체합니다. 핸들러 구현을 제공하지는 않습니다. 빌드 오류가 나면 이전의 동작하는 프로세스를 유지하므로 오래된 브라우저 응답은 새 코드가 컴파일되었다는 증거가 아닙니다.

gofmt -w handler.go
python3 serve.py

Lost and Found ready on port 8080이 예상됩니다. 포맷이나 컴파일에 실패하면 파일 이름과 줄 번호를 읽으세요. nano handler.go를 다시 열고 Ctrl+W로 주변 표현식을 찾아 그 부분만 고친 뒤 저장하고 종료하여 두 명령을 반복하세요. 이미 동작하는 로직은 보존하고 이후 진행한 내용을 초기 백업으로 덮어쓰지 마세요. No such file이 나오면 첫 cd를 복원하세요. 서비스 진단은 .work/server.log에 있으며 cat .work/server.log로 읽을 수 있습니다. 오타는 VM을 초기화할 이유가 아닙니다.

curl -i 'http://127.0.0.1:8080/api/items/LF-103'

id가 LF-103, location이 Cafe counter인 객체 하나가 예상됩니다. 배열이나 도서관 우산이 아니어야 합니다. 정상 목록이 여전히 샘플 배열을 반환하는지 확인하세요.

curl -i 'http://127.0.0.1:8080/api/items'

동작하는 페이지의 모음 형식을 유지하면서 선택 기능을 추가했습니다. 다음 단계에서는 의도적으로 그 목록을 모든 레코드로 확장합니다.

전체 모음 반환하기

이 단계에서는 작성한 상세 정보를 잃지 않으면서 한 레코드 목록을 완전한 읽기 전용 카탈로그로 바꿉니다. Workspace의 /home/labex/project/lost-found에서 시작하세요.

편집 전에 모음 분기의 슬라이스 표현식을 읽으세요. items[:1]은 인덱스 1 직전까지의 앞부분을 선택하므로 다른 다섯 레코드를 숨깁니다. 인코더에 items 자체를 넘기면 어떤 응답일지 예측하세요. 여섯 물품을 하드코딩한 배열과 달리 그 인수는 다른 슬라이스가 전달되어도 동작합니다.

LC_ALL=C.UTF-8 nano handler.go

Ctrl+W와 Enter로 items[:1]을 찾으세요. 모음에서 인코딩하는 값만 전체 입력 슬라이스로 바꾸세요. JSON 헤더, 상세 반복문, 없는 ID의 오류, 조기 반환은 유지하세요. Ctrl+O와 Enter로 같은 파일 이름에 저장한 뒤 Ctrl+X로 종료하세요.

빈 모음은 반드시 []로 인코딩하며 null이면 안 됩니다. 비어 있고 nil이 아닌 슬라이스는 []로 인코딩하고 nil 슬라이스는 null로 인코딩합니다. 이 단원의 독립적인 빈 입력은 []Item{}입니다. 빈 결과를 없는 물품 오류로 바꾸지 마세요. 목록과 상세는 의미가 다릅니다. 배열 순서와 객체 키 순서는 자유롭지만 각 ID에 속한 이름, 카테고리, 장소는 함께 유지되어야 합니다.

gofmt -w handler.go
python3 serve.py

모음을 요청한 다음 서로 다른 ID로 두 우산을 각각 요청하세요.

curl -i 'http://127.0.0.1:8080/api/items'
curl -i 'http://127.0.0.1:8080/api/items/LF-101'
curl -i 'http://127.0.0.1:8080/api/items/LF-103'

배열에 완전한 여섯 레코드가 모두 있어야 합니다. 이름은 둘 다 Blue umbrella이지만 LF-101은 여전히 Library desk에, LF-103은 Cafe counter에 속합니다. 상세 정보 중 하나라도 바뀌었다면 ID 비교와 인코딩한 반복문 값을 살펴보세요.

다음 요청은 의도적으로 없는 ID를 사용합니다. 빈 목록이 아니라 HTTP 404와 비어 있지 않은 error 문자열을 담은 JSON 객체가 예상됩니다. curl은 보통 HTTP 오류 응답을 받아도 성공으로 종료합니다. 응답을 받는 것과 연결에 실패하는 것은 다릅니다.

curl -i 'http://127.0.0.1:8080/api/items/no-such-id'

이 핸들러는 아직 필터링하지 않고 HTTP 메서드도 제한하지 않습니다. 다음 가이드 랩에서 이 동작을 도입합니다. 정상 홈페이지나 임의의 200을 상세 엔드포인트가 동작한다는 증거로 보지 마세요. 이제 모음의 범위를 바꾸면서 목록과 상세를 함께 보존했습니다.

식별 관계를 구분하는 테스트 작성하기

이 단계에서는 두 번째 실제 서버를 실행하지 않고 집중적인 핸들러 테스트를 직접 설계한 뒤 실제 페이지를 사용합니다. 같은 디렉터리의 Workspace에서 시작하세요.

먼저 테스트가 무엇을 잡아야 할지 정하세요. 핸들러가 요청한 ID 대신 첫 번째로 이름이 일치하는 항목을 반환할 수 있습니다. 웹페이지의 모음 밖에서 이름은 같지만 ID와 장소는 다른 작은 레코드 두 개를 직접 고르세요. 두 번째 ID를 요청하세요. 반환해야 할 상태, ID, 장소를 적으세요. 이 독립 입력은 고정된 실제 응답을 실패시키며 준비된 여섯 레코드 밖에서도 유용한 테스트를 만듭니다.

Go는 이름이 _test.go로 끝나는 파일에서 테스트를 찾습니다. 테스트 함수는 Test로 시작하고 t *testing.T를 받습니다. nano로 테스트 파일을 만드세요.

LC_ALL=C.UTF-8 nano handler_test.go

package main으로 시작하세요. encoding/json, net/http/httptest, testing을 가져오세요. func TestSecondIdentity(t *testing.T) { ... } 같은 함수를 추가하고 점 부분을 테스트 문장으로 바꾸세요. 함수 본문은 네 부분입니다.

먼저 main.go의 내보낸 필드 이름을 사용해 두 레코드의 []Item 입력을 만드세요. items.json을 로드하거나 바꾸지 말고 그 입력을 catalogHandler에 전달하세요.

다음으로 두 번째 레코드 상세 요청을 만드세요. httptest.NewRequest("GET", path, nil)은 요청을 만듭니다. 마지막 인수는 요청 본문이 없다는 뜻입니다. httptest.NewRecorder()는 응답 레코더를 반환합니다. 핸들러의 ServeHTTP(recorder, request)를 호출하면 실제 핸들러를 실행하고 헤더, 상태, 본문을 기록합니다.

이어서 var got Item을 선언하세요. json.Unmarshal(recorder.Body.Bytes(), &got)으로 디코딩하세요. &got은 필드를 저장할 주소입니다. 먼저 반환된 error를 확인하세요. 잘못된 JSON을 유효한 빈 구조체로 오인해서는 안 됩니다. 예를 들어 if err != nil { t.Fatal(err) }는 읽기 쉬운 실패 메시지로 테스트를 멈춥니다.

그 뒤 예상 상태와 식별 관계를 검증하세요. recorder.Code는 상태를, got.ID와 got.Location은 디코딩된 필드를 담습니다. 두 번째 테스트 입력 레코드와 비교하세요. if와 t.Fatalf를 사용하여 실제 값을 표시하며 실패하게 하세요. %d는 숫자를, %+v는 구조체 필드를 보여 줍니다. 단지 어떤 JSON이 존재하는지만 검증하지 마세요.

요청과 검증에서 직접 고른 ID와 장소를 일관되게 사용하세요. 추론을 따라가기 쉽도록 입력 데이터, 실행, 검증을 같은 테스트 함수 안에 두세요. Ctrl+O와 Enter로 저장하고 Ctrl+X로 종료하세요. 환경은 통과하는 테스트를 대신 제공하지 않습니다.

go test -v .는 이 패키지의 테스트를 실행합니다. -v는 테스트 이름을 표시하고 .은 현재 패키지를 선택합니다.

gofmt -w handler_test.go
go test -v .

테스트 이름 뒤에 PASS가 예상됩니다. 실패하면 실제 상태나 물품이 출력되어야 합니다. 예상한 테스트 입력 관계와 핸들러 중 어느 쪽이 틀렸는지 판단한 뒤 nano로 그 부분만 고쳐 테스트를 다시 실행하세요. 컴파일 오류는 살펴볼 파일과 줄을 알려 줍니다. 핸들러를 고칠 때마다 브라우저도 현재 코드를 사용하도록 python3 serve.py로 다시 빌드하세요. 이 작업 환경에 통과하는 Go 핸들러 테스트를 적어도 하나 유지하세요. 최종 검사는 테스트를 격리해서 실행하고 다른 입력 레코드로 API를 확인합니다.

Lost and Found로 전환하여 새로 고치세요. 6 items found가 예상됩니다. LF-103의 View item을 열어 참조 번호와 수령 장소를 읽고 Back to collection을 선택하세요. 두 우산은 구분 가능한 상태여야 합니다. 이 단원은 모음과 상세 탐색을 제공하며 다음 가이드 랩에서는 API 필터링 동작과 함께 검색 컨트롤을 추가합니다.

모든 원본 레코드가 있는 실제 카탈로그 페이지

여섯 카드와 개별 참조 번호를 보세요. 페이지는 실행 중인 API를 사용하고 백업 모음을 포함하지 않습니다. 수정이 필요하면 Workspace로 돌아가세요. 이제 사용 가능한 카탈로그와 실제 식별 오류를 드러내도록 직접 고른 입력 및 검증을 갖춘 테스트가 있습니다.

요약

Go 레코드를 JSON으로 인코딩하고, 경로에 따라 목록과 상세 응답을 선택하고, 같은 이름을 각각의 ID에 연결한 채 유지하고, 없는 물품에 실제 오류 상태를 반환했습니다. 또한 격리된 httptest 테스트를 작성하고 실행 중인 웹페이지를 사용했습니다. 검색과 더 엄격한 오류 처리는 다음 가이드 랩의 독립적인 과제입니다.