![[OSSCA] Terraform Provider 개발(1) - Go API 분석하기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7W9x%2FbtsIPtyZ6oz%2F4HJmcZ1BGdtVrVBM482Ah1%2Fimg.webp)
내가 만든 API를 Terraform Provider의 형태로 개발하기 위해서는, 우선적으로 API가 필요하다. 따라서, 해당 게시글에서는 Terraform Provider 개발을 위한 Go CRUD API 개발에 관한 내용을 담아보고자 한다.
https://github.com/inpyu/product-api-go
GitHub - inpyu/hashicups-client-go: [OSSCA] Terraform Provider 개발을 위한 Cafe package client 개발
[OSSCA] Terraform Provider 개발을 위한 Cafe package client 개발 - inpyu/hashicups-client-go
github.com
실제 완성된 API는 해당 Github에서 참고가 가능하다.
실제 API 작동을 보면 위 그림과 같이 구성되어 있다. 해당 포스트에서는 Hashicup Client 이전, Product API 개발에 대해 우선적으로 알아본 뒤, 이후 Client와 Provider 개발 순으로 진행해보고자 한다.
프로젝트 구조
.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── blueprint
│ ├── README.md
│ ├── config.json
│ └── stack.hcl
├── client
│ ├── http.go
│ └── http_test.go
├── conf.json
├── config
│ ├── config.go
│ └── config_test.go
├── data
│ ├── connection.go
│ ├── mockcon.go
│ └── model
│ ├── cafe.go
│ ├── cafe_test.go
│ ├── coffee.go
│ ├── coffee_test.go
│ ├── ingredient.go
│ ├── ingredient_test.go
│ ├── order.go
│ ├── order_test.go
│ ├── token.go
│ ├── token_test.go
│ ├── user.go
│ └── user_test.go
├── database
│ ├── Dockerfile
│ └── products.sql
├── docker_compose
│ ├── conf.json
│ └── docker-compose.yml
├── functional_tests
│ ├── features
│ │ ├── basic_functionality.feature
│ │ ├── coffee.feature
│ │ ├── order.feature
│ │ └── user.feature
│ ├── helper_test.go
│ └── main_test.go
├── go.mod
├── go.sum
├── handlers
│ ├── auth.go
│ ├── cafe.go
│ ├── coffee.go
│ ├── coffee_test.go
│ ├── health.go
│ ├── ingredients.go
│ ├── ingredients_test.go
│ ├── order.go
│ ├── order_test.go
│ ├── user.go
│ └── user_test.go
├── main
├── main.go
├── open_api.yaml
├── telemetry
│ └── telemetry.go
└── test_docker
└── docker-compose.yaml
우선적으로 개략적으로 디렉토리 구조에 대해 파악하면 다음과 같다.
client 디렉토리
- client/: HTTP 클라이언트 관련 코드가 포함된다.
- http.go: HTTP 클라이언트 코드이다.
data 디렉토리
- data/: 데이터베이스와의 연결 및 데이터 모델이 포함된다.
- connection.go: 데이터베이스 연결 코드이다.
- mockcon.go: 모의 데이터베이스 연결 코드이다.
- model/: 데이터 모델 관련 코드가 포함된다.
- cafe.go, coffee.go, ingredient.go, order.go, token.go, user.go: 각각 카페, 커피, 재료, 주문, 토큰, 사용자 모델 코드이다.
handlers 디렉토리
- handlers/: 요청을 처리하는 핸들러 코드가 포함된다.
- auth.go: 인증 관련 핸들러 코드이다.
- cafe.go, coffee.go, ingredients.go, order.go, user.go: 각각 카페, 커피, 재료, 주문, 사용자 관련 핸들러 코드이다.
주요 구성 요소 설명
- main.go: main.go 파일은 Spring에서 Controller 역할을 하는 것처럼, URI 경로에 대해 요청을 처리하는 코드가 작성되는 것을 볼 수 있다.
- client/http.go: http.go 파일은 외부 API와 상호작용하여 데이터를 가져오거나 전송하는 역할을 한다. 들어오는 HTTP 요청을 처리하는 구간으로 생각하면 된다.
- data/model: 이 디렉토리에는 엔티티 정보가 정의되어 있다.
- data/connection.go: 서비스 로직이 작성된다. 데이터베이스와의 연결 및 데이터 처리 로직이 포함된다.
- handlers: 서버측에서 HTTP 요청을 처리하는 역할을 한다. 클라이언트로부터 받은 역할을 처리하고 적절한 응답을 반환한다.
Client와 handler가 유사한 듯 보이는데, Client의 경우 데이터 수집과 같은 클라이언트 측의 기능을 수행하는 반면, 핸들러는 서버에서 데이터베이스와 상호작용하여 데이터를 반환하거나 조작하는 방식으로 차이가 있었다.
main.go
main.go에서 하는 기능과, 해당 파일에 작업한 코드에 대해 설명하고자 한다.
라우터 및 미들웨어 설정
gorilla/mux 패키지를 사용하여 라우터를 설정하고, cors 패키지를 사용하여 CORS 설정을 적용한다.
r := mux.NewRouter()
r.Use(hckit.TracingMiddleware)
r.Use(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"POST", "GET", "OPTIONS", "PUT", "DELETE"},
AllowedHeaders: []string{"Accept", "content-type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"},
}).Handler)
Handler 등록하기
다양한 엔드포인트에 대해 핸들러를 등록한다. 핸들러는 데이터베이스 연결과 로거를 전달받아 초기화된다.
healthHandler := handlers.NewHealth(t, logger, db)
r.Handle("/health", healthHandler).Methods("GET")
coffeeHandler := handlers.NewCoffee(db, logger)
r.Handle("/coffees", coffeeHandler).Methods("GET")
ingredientsHandler := handlers.NewIngredients(db, logger)
r.Handle("/coffees/{id:[0-9]+}/ingredients", ingredientsHandler).Methods("GET")
userHandler := handlers.NewUser(db, logger)
r.HandleFunc("/signup", userHandler.SignUp).Methods("POST")
orderHandler := handlers.NewOrder(db, logger)
r.Handle("/orders", authMiddleware.IsAuthorized(orderHandler.GetUserOrders)).Methods("GET")
cafeHandler := handlers.NewCafe(db, logger)
r.Handle("/cafes", cafeHandler).Methods("GET")
r.Handle("/cafes/{id:[0-9]+}", cafeHandler).Methods("GET")
- 헬스 체크 핸들러:
- /health 엔드포인트를 통해 애플리케이션의 상태를 확인한다.
- 커피 핸들러:
- /coffees 엔드포인트를 통해 커피 목록을 조회한다.
- 인증된 사용자만 커피를 생성할 수 있다.
- 재료 핸들러:
- 특정 커피에 대한 재료 목록을 조회한다.
- 사용자 핸들러:
- 사용자 가입 및 로그인 기능을 제공한다.
- 주문 핸들러:
- 인증된 사용자는 주문을 조회, 생성, 수정, 삭제할 수 있다.
Data/Coffee.go
model 패키지의 코드 분석을 하고자 한다. 이 패키지는 주로 커피 관련 데이터를 정의하고 JSON으로 직렬화 및 역직렬화하는 기능을 포함한다.
Coffees 타입
Coffees는 Coffee 객체들의 슬라이스이다.
FromJSON 메서드
func (c *Coffees) FromJSON(data io.Reader) error {
de := json.NewDecoder(data)
return de.Decode(c)
}
- JSON 데이터를 Coffees 타입으로 역직렬화한다.
- io.Reader로부터 JSON 데이터를 읽어와서 Coffees 객체에 디코딩한다.
func (c *Coffees) ToJSON() ([]byte, error) {
return json.Marshal(c)
}
Coffee 타입
type Coffee struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Teaser string `db:"teaser" json:"teaser"`
Collection string `db:"collection" json:"collection"`
Origin string `db:"origin" json:"origin"`
Color string `db:"color" json:"color"`
Description string `db:"description" json:"description"`
Price float64 `db:"price" json:"price"`
Image string `db:"image" json:"image"`
CreatedAt string `db:"created_at" json:"-"`
UpdatedAt string `db:"updated_at" json:"-"`
DeletedAt sql.NullString `db:"deleted_at" json:"-"`
Ingredients []CoffeeIngredient `json:"ingredients"`
}
Coffee는 데이터베이스에 저장된 커피를 정의한다. 여러 필드를 포함하며, json 및 db 태그를 사용하여 직렬화 및 데이터베이스 맵핑을 한다.
- ID: 커피의 고유 식별자
- Name: 커피 이름
- Teaser: 커피 티저
- Collection: 컬렉션 이름
- Origin: 원산지
- Color: 커피 색상
- Description: 설명
- Price: 가격
- Image: 이미지 URL
- CreatedAt: 생성 시간
- UpdatedAt: 수정 시간
- DeletedAt: 삭제 시간 (null 가능)
- Ingredients: 커피에 포함된 재료 목록
Handler : Coffee.go
다양한 핸들러가 있지만, 그중 Coffee.go의 핸들러에 대해서 분석해보고자 한다. Coffee 핸들러는 커피와 관련된 요청을 처리하는 역할을 한다. 이 핸들러는 커피 목록을 조회하거나 새로운 커피를 생성하는 등의 기능을 제공한다.
구조체 및 생성자
type Coffee struct {
con data.Connection
log hclog.Logger
}
// NewCoffee
func NewCoffee(con data.Connection, l hclog.Logger) *Coffee {
return &Coffee{con, l}
}
Coffee 구조체는 데이터베이스 연결을 나타내는 con 필드와 로깅을 담당하는 log 필드를 포함한다. NewCoffee 함수는 새로운 Coffee 인스턴스를 생성하고 초기화한다.
ServeHTTP 함수
ServeHTTP 함수는 커피 목록을 조회하는 역할을 한다. 이 함수는 HTTP 요청을 처리하고 적절한 응답을 반환한다.
func (c *Coffee) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
c.log.Info("Handle Coffee")
vars := mux.Vars(r)
var coffeeID *int
if vars["id"] != "" {
cId, err := strconv.Atoi(vars["id"])
if err != nil {
c.log.Error("CoffeeID provided could not be converted to an integer", "error", err)
http.Error(rw, "Unable to list ingredients", http.StatusInternalServerError)
return
}
coffeeID = &cId
}
cofs, err := c.con.GetCoffees(coffeeID)
if err != nil {
c.log.Error("Unable to get products from database", "error", err)
http.Error(rw, "Unable to list products", http.StatusInternalServerError)
return
}
d, err := cofs.ToJSON()
if err != nil {
c.log.Error("Unable to convert products to JSON", "error", err)
http.Error(rw, "Unable to list products", http.StatusInternalServerError)
return
}
rw.Write(d)
}
- 로그 출력: c.log.Info("Handle Coffee")를 통해 요청이 처리되고 있음을 로그에 기록한다.
- URL 변수 추출: mux.Vars(r)를 사용하여 URL 경로에서 변수를 추출한다.
- 커피 ID 파싱: vars["id"]가 존재하면 이를 정수로 변환하여 coffeeID에 저장한다. 변환에 실패하면 오류를 기록하고 클라이언트에 오류 응답을 반환한다.
- 커피 목록 조회: c.con.GetCoffees(coffeeID)를 호출하여 데이터베이스에서 커피 목록을 조회한다. 조회에 실패하면 오류를 기록하고 클라이언트에 오류 응답을 반환한다.
- JSON 변환 및 응답: 조회한 커피 목록을 JSON 형식으로 변환하여 클라이언트에 응답으로 반환한다.
CreateCoffee 함수
CreateCoffee 함수는 새로운 커피를 생성하는 역할을 한다. 이 함수는 POST 요청을 처리하고 새로운 커피 객체를 생성한다.
func (c *Coffee) CreateCoffee(_ int, rw http.ResponseWriter, r *http.Request) {
c.log.Info("Handle Coffee | CreateCoffee")
body := model.Coffee{}
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
c.log.Error("Unable to decode JSON", "error", err)
http.Error(rw, "Unable to parse request body", http.StatusInternalServerError)
return
}
coffee, err := c.con.CreateCoffee(body)
if err != nil {
c.log.Error("Unable to create new coffee", "error", err)
http.Error(rw, fmt.Sprintf("Unable to create new coffee: %s", err.Error()), http.StatusInternalServerError)
return
}
d, err := coffee.ToJSON()
if err != nil {
c.log.Error("Unable to convert coffee to JSON", "error", err)
http.Error(rw, "Unable to create new coffee", http.StatusInternalServerError)
return
}
rw.Write(d)
}
- 로그 출력: c.log.Info("Handle Coffee | CreateCoffee")를 통해 요청이 처리되고 있음을 로그에 기록한다.
- 요청 본문 파싱: json.NewDecoder(r.Body).Decode(&body)를 사용하여 요청 본문을 model.Coffee 구조체로 디코딩한다. 디코딩에 실패하면 오류를 기록하고 클라이언트에 오류 응답을 반환한다.
- 커피 생성: c.con.CreateCoffee(body)를 호출하여 데이터베이스에 새로운 커피 객체를 생성한다. 생성에 실패하면 오류를 기록하고 클라이언트에 오류 응답을 반환한다.
- JSON 변환 및 응답: 생성한 커피 객체를 JSON 형식으로 변환하여 클라이언트에 응답으로 반환한다.
Connection.go
Connection 인터페이스는 데이터베이스와 상호작용하는 여러 메서드들을 정의한다. Coffee와 관련된 메서드는 다음과 같다
type Connection interface {
CreateCoffee(model.Coffee) (model.Coffee, error)
UpsertCoffeeIngredient(model.Coffee, model.Ingredient) (model.CoffeeIngredient, error)
}
PostgresSQL 구조체
PostgresSQL 구조체는 Connection 인터페이스를 구현한다. 이 구조체에서 Coffee와 관련된 메서드를 구현하는 코드를 설명하겠다.
type PostgresSQL struct {
db *sqlx.DB
}
GetCoffees 메서드
func (c *PostgresSQL) GetCoffees(coffeeid *int) (model.Coffees, error) {
cos := model.Coffees{}
if coffeeid != nil {
err := c.db.Select(&cos, "SELECT * FROM coffees WHERE id = $1", coffeeid)
if err != nil {
return nil, err
}
} else {
err := c.db.Select(&cos, "SELECT * FROM coffees")
if err != nil {
return nil, err
}
}
for n, cof := range cos {
i := []model.CoffeeIngredient{}
err := c.db.Select(&i, "SELECT ingredient_id FROM coffee_ingredients WHERE coffee_id=$1 AND quantity > 0", cof.ID)
if err != nil {
return nil, err
}
cos[n].Ingredients = i
}
return cos, nil
}
- GetCoffees 메서드는 특정 Coffee ID 또는 모든 커피를 데이터베이스에서 가져온다.
- coffeeid가 nil이 아니면 해당 ID의 커피를 가져오고, nil이면 모든 커피를 가져온다.
- 각 커피에 대해 관련된 재료들도 함께 가져온다.
CreateCoffee 메서드
func (c *PostgresSQL) CreateCoffee(coffee model.Coffee) (model.Coffee, error) {
m := model.Coffee{}
rows, err := c.db.NamedQuery(
`INSERT INTO coffees (name, teaser, description, price, image, created_at, updated_at)
VALUES(:name, :teaser, :description, :price, :image, now(), now())
RETURNING id;`, map[string]interface{}{
"name": coffee.Name,
"teaser": coffee.Teaser,
"description": coffee.Description,
"price": coffee.Price,
"image": coffee.Image,
})
if err != nil {
return m, err
}
defer rows.Close()
if rows.Next() {
err := rows.StructScan(&m)
if err != nil {
return m, err
}
}
return m, nil
}
- CreateCoffee 메서드는 새로운 커피를 데이터베이스에 추가한다.
- 커피 정보를 입력받아 데이터베이스에 삽입하고, 삽입된 커피의 ID를 반환한다.
UpsertCoffeeIngredient 메서드
func (c *PostgresSQL) UpsertCoffeeIngredient(coffee model.Coffee, ingredient model.Ingredient) (model.CoffeeIngredient, error) {
i := model.CoffeeIngredient{}
rows, err := c.db.NamedQuery(
`INSERT INTO coffee_ingredients (coffee_id, ingredient_id, quantity, unit, created_at, updated_at)
VALUES(:coffee_id, :ingredient_id, :quantity, :unit, now(), now())
ON CONFLICT ON CONSTRAINT unique_coffee_ingredient
DO UPDATE SET quantity = :quantity, unit = :unit
RETURNING id;`, map[string]interface{}{
"coffee_id": coffee.ID,
"ingredient_id": ingredient.ID,
"quantity": ingredient.Quantity,
"unit": ingredient.Unit,
})
if err != nil {
return i, err
}
defer rows.Close()
if rows.Next() {
err := rows.StructScan(&i)
if err != nil {
return i, err
}
}
return i, nil
}
- UpsertCoffeeIngredient 메서드는 커피와 재료의 관계를 데이터베이스에 삽입하거나 업데이트한다.
- 커피 ID와 재료 ID를 받아 해당 관계를 데이터베이스에 삽입하거나 업데이트한다.
'Infra > Terraform' 카테고리의 다른 글
[OSSCA] Terraform Provider 개발(3) - Go API 패키지 만들기 (0) | 2024.07.30 |
---|---|
[OSSCA] Terraform Provider 개발(2) - Go API 제작하기 (0) | 2024.07.30 |
[OSSCA] Terraform Provider SDK 와 Framework 버전 차이를 알아보자 (1) | 2024.07.24 |
[OSSCA] Terraform Provider 살펴보기 (2) | 2024.07.23 |
[OSSCA] Terraform으로 NCP 사용하기 (6) | 2024.07.23 |
보안 전공 개발자지만 대학로에서 살고 싶어요
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!