일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 알고리즘
- 리액트
- Go언어실무
- 자료구조
- 개발 공식문서 읽기
- jsx
- postgredb
- 개발자되기
- 개발공식문서 어려움
- 개발 영어실력
- 코딩강의
- 데이터스키마
- Go언어
- HTTP
- 데이터베이스
- 자바
- 파이썬
- 코멘토
- 웹서버
- 개발자공통지식
- 개발공부
- 컴공과개념정리
- 스키마모델
- 유데미
- golang
- tableplus
- 개발 공식문서
- 실무PT
- 개발실무
- 코멘토실무PT
- Today
- Total
웹개발일지
웹앱 어플리케이션 구현 (1) - HTTP 서버구현 본문
이번 글은 유데미에서 Go 언어로 gin, postgre db, 쿠버네티스를 활용한 웹앱 어플리케이션을 구축하는 강좌를 들으며 공부한 내용입니다. gin 프레임워크와 웹서버 개발이 처음이어서 코드 하나하나 세세하게 설명하였습니다.
실습 목록
- db패키지 필요한 요소들 파악
- 은행계좌 서버로 사용할 api 구현
- 메인 파일인 server.go 구현
- POST요청을 받을시 동작할 CreateAccount() 핸들러 구현
실습 프로젝트 구조
- 은행 계좌를 만드는 과정을 미니멀하게 구현하는 프로젝트인 듯 하다. api폴더의 account, server 파일을 제외한 나머지는 깃허브에서 내려받아 진행했다.
.
├── Dockerfile
├── Makefile
├── README.md
├── api
│ ├── account.go
│ └── server.go
├── app.env
├── backend-master.png
├── db
│ ├── migration
│ │ ├── 000001_init_schema.down.sql
│ │ ├── 000001_init_schema.up.sql
│ │ ├── 000002_add_users.down.sql
│ │ ├── 000002_add_users.up.sql
│ │ ├── 000003_add_sessions.down.sql
│ │ └── 000003_add_sessions.up.sql
│ ├── mock
│ │ └── store.go
│ ├── query
│ │ ├── account.sql
│ │ ├── entry.sql
│ │ ├── session.sql
│ │ ├── transfer.sql
│ │ └── user.sql
│ └── sqlc
│ ├── account.sql.go
│ ├── account_test.go
│ ├── db.go
│ ├── entry.sql.go
│ ├── entry_test.go
│ ├── main_test.go
│ ├── models.go
│ ├── querier.go
│ ├── session.sql.go
│ ├── store.go
│ ├── store_test.go
│ ├── transfer.sql.go
│ ├── transfer_test.go
│ ├── tx_create_user.go
│ ├── tx_transfer.go
│ ├── user.sql.go
│ └── user_test.go
├── doc
│ ├── db.dbml
│ ├── schema.sql
│ ├── statik
│ │ └── statik.go
│ └── swagger
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── index.css
│ ├── index.html
│ ├── oauth2-redirect.html
│ ├── simple_bank.swagger.json
│ ├── swagger-initializer.js
│ ├── swagger-ui-bundle.js
│ ├── swagger-ui-bundle.js.map
│ ├── swagger-ui-es-bundle-core.js
│ ├── swagger-ui-es-bundle-core.js.map
│ ├── swagger-ui-es-bundle.js
│ ├── swagger-ui-es-bundle.js.map
│ ├── swagger-ui-standalone-preset.js
│ ├── swagger-ui-standalone-preset.js.map
│ ├── swagger-ui.css
│ ├── swagger-ui.css.map
│ ├── swagger-ui.js
│ └── swagger-ui.js.map
├── docker-compose.yaml
├── eks
│ ├── aws-auth.yaml
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── issuer.yaml
│ └── service.yaml
├── gapi
│ ├── authorization.go
│ ├── converter.go
│ ├── error.go
│ ├── logger.go
│ ├── metadata.go
│ ├── rpc_create_user.go
│ ├── rpc_login_user.go
│ ├── rpc_update_user.go
│ └── server.go
├── go.mod
├── go.sum
├── main.go
├── pb
│ ├── rpc_create_user.pb.go
│ ├── rpc_login_user.pb.go
│ ├── rpc_update_user.pb.go
│ ├── service_simple_bank.pb.go
│ ├── service_simple_bank.pb.gw.go
│ ├── service_simple_bank_grpc.pb.go
│ └── user.pb.go
├── proto
│ ├── google
│ │ └── api
│ │ ├── annotations.proto
│ │ ├── field_behavior.proto
│ │ ├── http.proto
│ │ └── httpbody.proto
│ ├── protoc-gen-openapiv2
│ │ └── options
│ │ ├── annotations.proto
│ │ └── openapiv2.proto
│ ├── rpc_create_user.proto
│ ├── rpc_login_user.proto
│ ├── rpc_update_user.proto
│ ├── service_simple_bank.proto
│ └── user.proto
├── sqlc.yaml
├── start.sh
├── token
│ ├── jwt_maker.go
│ ├── jwt_maker_test.go
│ ├── maker.go
│ ├── paseto_maker.go
│ ├── paseto_maker_test.go
│ └── payload.go
├── tools
│ └── tools.go
├── util
│ ├── config.go
│ ├── currency.go
│ ├── password.go
│ ├── password_test.go
│ └── random.go
├── val
│ └── validator.go
├── wait-for.sh
└── worker
├── distributor.go
├── logger.go
├── processor.go
└── task_send_verify_email.go
Server.go
package api
import (
db "github.com/techschool/simplebank/db/sqlc"
"github.com/gin-gonic/gin"
)
type Server struct {
store *db.Store
router *gin.Engine
}
//New Server creates a new HTTP server and setup routing.
func NewServer(store *db.Store) * Server {
server := &Server{store:store}
router := gin.Default()
router.POST("/accounts", server.createAccount)
server.router = router
return server
}
func errorResponse(err error) gin.H {
return gin.H{"error": err.Error()}
}
server.go 는 HTTP 서버를 실행하는 페이지다. 파일이 속한 폴더명인 api를 패키지명으로 지정해주고 db 폴더와 gin 라이브러리를 import한다.
1. Server{}
구조체 Server는 요청사항들을 처리한다. Store 인터페이스의 인스턴스와 gin의 Engine 인스턴스 라우터를 필드로 생성했다. 이 프로젝트에서 store는 새 은행계좌 account를 담는데 db에 접근하는 용도로 쓰인다. 라우터 router는 gin 에서 핸들러 function을 연결해주는데 필요하다.
2. New Server()
Store의 인스턴스를 인자로 받고 Server의 주소값을 반환하는 함수를 생성했다. NewServer()는 새로운 HTTP 서버를 만들고 라우터 설정을 한다. Server 주소의 메모리 변수에 인자로 받은 store를 값으로 넣어주고 라우터 객체를 생성하여 router로 지정해준다.
라우터 router로 POST()메서드를 호출한다. 이는 router객체가 Engine 구조체에 속해있고 Engine구조체가 가진 RouterGroup 구체가 POST()메서드를 구현하고있기때문에 사용할 수 있는것이다. POST()메서드의 첫 번째 인자로는 path를 두 번째 인자로는 핸들러를 넣어준다. 두 번째인자에 createAccount라고 넣어주고 account.go 파일에 별도로 만들어주었다. 다른 파일로 분리해서 만들어준 이유는 다른 함수에서도 자주 불러다 쓸 기능이기 때문이다. Server 구조체의 메서드로 만들었기 때문이다. createAccount() 함수는 은행계정 account를 생성하는 기능을 구현할 것이다. NewServer()는 server를 return 하고 마무리해준다.
account.go
새로운 account를 생성하는 createAccount 기능을 구현한 api이다.
package api
import (
"net/http"
db "github.com/techschool/simplebank/db/sqlc"
"github.com/gin-gonic/gin"
"udemy/restfulapi/simplebank/api"
)
type createAccountRequest struct {
// remove this balance field.We only allow clients to specify the owner’s name and the currency of the account.
Owner string `json:"owner" binding: "required"`
Currency string `json:"currency" binding:"required, oneof=USD EUR"`
}
func (server *Server) createAccount(c *gin.Context) {
var req createAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
// it the error is not nil, it means the client has provided invalid data.
//first, give a json response to client. first is status code, second is JSON object that we want to send to client.
// need to convert this err into a key-value object so that Gin can serialize it to JSON before returning to the client.
c.JSON(http.StatusBadRequest, errorResponse(err)) // errorResponse() will used in various so implement in the server.go
return
}
//if the input data has no error. So we just go ahead to insert a new account into the database
arg := db.CreateAccountParams{
Owner : req.Owner,
Currency: req.Currency,
Balance: 0,
}
account, err := server.store.CreateAccount(c, arg)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
c.JSON(http.StatusOK, account)
}
1. createAccountRequest{}
Request를 담는 자료구조이다. Owner와 Currency를 클라이언트가 요청할 수 있는 요소로서 담았다.Balance도 있었는데 우리는 사용자에게 owner name과 account의 currency만 설정하도록 허용한다. 바인드에 "required"를 줘서 이건 꼭 설정해줘야하는 값이라는 걸 나타냈다. currency에는 USD와 EUR로 바인딩되도록 값을 주었다. json은 이름과 값의 쌍으로 이루어진다.
2. createAccount()
createAccountRequest의 인스턴스 req를 생성해준다. shoudBind를 통해 클라이언트로 부터 들어온 데이터가 위에서 정의한 Request에 바인딩 형식과 일치하는지 검사하고 에러구문을 작성한다. Go는 이렇게 에러확인을 통해 유효성 검사를 바로바로 진행한다. 에러가 있다면, 클라이언트로 부터 들어온 정보가 invalid하단 뜻이다. 따라서 if문의 에러처리 구문에서 JSON메서드를 통해 응답을 날려준다. 참, 여기서 JSON()메서드를 불러온 c인스턴스는 gin의 Context 구조체의 인스턴스객체인데, Context는 gin에서 활용할 편리한 대부분의 메서드들을 갖고있는 구조체이다. 대부분의 메서드들은 Context에 담겨있고 gin은 그걸통해 구현한 핸들러를 가지고 서버를 동작킨다. c.JSON()에 대해 설명하기 전에 shouldBind()함수 관련하여 좀 더 살펴보면 gin에서 사용할 수 있는 바인딩 함수가 다양한 것을 알 수 있다.
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set json:"fieldname".
Also, Gin provides two sets of methods for binding:
- Type - Must bind
- Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML, BindHeader, BindTOML
- Behavior - These methods use MustBindWith under the hood. If there is a binding error, the request is aborted with c.AbortWithError(400, err).SetType(ErrorTypeBind). This sets the response status code to 400 and the Content-Type header is set to text/plain; charset=utf-8. Note that if you try to set the response code after this, it will result in a warning [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422. If you wish to have greater control over the behavior, consider using the ShouldBind equivalent method.
- Type - Should bind
- Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindHeader, ShouldBindTOML,
- Behavior - These methods use ShouldBindWith under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use MustBindWith or ShouldBindWith.
You can also specify that specific fields are required. If a field is decorated with binding:"required" and has an empty value when binding, an error will be returned.
c.JSON()은 클라이언트에게 응답 객체를 전해준다. 첫번째 인자로 상태코드를, 두 번째 인자로 응답객체를 반환한다. 두번째 인자로는 어떠한 객체도 올 수 있어 obj any 로 지정되어있는 것을 볼 수 있다. 여기선 err객체를 반환해주었는데 이를 나중에 JSON 형태로 직렬화(serialize) 하여 전달해주기 위해 key-value 형태로 변환해주는 함수를 만들어 담았다. 해당 함수는 errorResponse()로 메인 파일인 server.go에 구현하였다. 이어 해당 함수를 설명하자면, gin.H라는 맵타입 shortcut 객체를 반환형으로 지정하여 어떠한 자료든 key-value타입이면 저장할 수 있도록 도와주는 기능을 구현하였다. 부연 설명을 덧붙이자면, 맵의 value는 interface{} 타입이라 어떠한 자료형도 가능하다.
만약, shouldBind()를 통한 결과가 err를 갖지 않았다면 db에 저장할 새로운 account를 생성해줘야한다. db에 구현된 CreateAccountParams{}를 불러와 우리가 생성한 createAccountRequest{}의 인스턴스객체 req로 멤버변수를 가져와 (Go에선 멤버변수란 말이 쓰이지 않는데 적당한 표현이 생각이 나지 않는다. ) CreateAccountParams를 초기화시켜 arg 변수를 생성한다. 그런 다음 account를 생성해주는 CreateAccount구조체에 arg변수를 담아 드디어 new account로 account 변수를 생성하고 반환값으로 err도 같이 생성한다. 여기서도 에러처리를 해줌으로써 에러가 있다면 StatusInternalServerError코드를, 그리고 에러객체를 반환한다. 에러가 없다면 statusOK와 새로 생성된 account 객체를 반환해준다.
오늘의 학습 내용은 여기까지였습니다. 부족한 부분이 있을 것인데 관심갖고 봐주셨으면 감사드리고, 부족한 부분 혹은 공감가는 부분 등 어떤 의견이든 댓글 달아주시면 같이 학습하고 Go언어 강자로 성장할 수 있는 기회가 될 것 같습니다. 꾸준히 공부하여 기록하겠습니다. 감사합니다.