본문 바로가기
Go

[go/gin] Basic REST API 만들기 튜토리얼 (3)

by weero 2021. 12. 18.

[go/gin/gorm] Basic REST API 만들기 튜토리얼 (4)

이전 포스트 (1) : https://dev2som.tistory.com/152

이전 포스트 (2) : https://dev2som.tistory.com/153

 

 

DB에 데이터를 적재하지 않는 방식으로 CRUD를 구현하는 API를 만들어보려고 한다.

데이터베이스와 연결하지 않으므로 이번 편에서는 gorm 패키지를 사용하지 않는다.

왕간단하다...

 

main 함수

func main() {
	r := gin.Default()
	r.GET("/info", readInfo)
	r.POST("/info", createInfo)
	r.PUT("/info/:id", updateInfo)
	r.DELETE("/info/:id", deleteInfo)

	r.Run("localhost:8080")
}

gin.Default() 함수로 Gin 엔진을 만들 수 있다. 만약 로거가 없는 엔진을 사용해 커스텀을 하고 싶으면 gin.New() 함수를 사용한다.

api를 호스트 할 url과 포트번호는 r.Run()으로 지정할 수 있다.

 

URI : localhost:8080/info

HTTP Method : GET

이면 readInfo()라는 핸들러 함수가 호출된다.

 

URI : localhost:8080/info

HTTP Method : POST

이면 createInfo()라는 핸들러 함수가 호출된다.

 

URI : localhost:8080/info/{id}

HTTP Method : PUT

이면 updateInfo()라는 핸들러 함수가 호출된다.

 

URI : localhost:8080/info/{id}

HTTP Method : DELETE

이면 deleteInfo()라는 핸들러 함수가 호출된다.

 

 

 

주인공 infoList

DB에 데이터를 적재하지 않는 대신, 서버가 켜져 있을 동안 데이터를 유지할 수 있는 구조체 슬라이스를 전역으로 만들어준다.

CRUD 연산의 주인공인 infoList는 CreateInput 구조체의 슬라이스이다. (구조체 안에는 Id, Name, Email 필드가 들어있다.)

type CreateInput struct {
	Id    int    `json:"id" binding:"required"`
	Name  string `json:"name" binding:"required"`
	Email string `json:"email"`
}

var infoList []CreateInput

 

 

GET - readInfo

func readInfo(c *gin.Context) {
	if infoList == nil {
		c.JSON(http.StatusNoContent, nil)

		return
	}

	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
		"data":   infoList,
	})
}

데이터를 읽는 GET 연산의 경우 호출되는 readInfo이다. 

 

infoList가 nil인 경우에 내용이 없는 것을 알려주는 상태 코드를 리턴한다.

 

 

POST - createInfo

func createInfo(c *gin.Context) {
	input := CreateInput{}

	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"status": err.Error(),
		})
		return
	}

	infoList = append(infoList, input)
	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
		"data":   input,
	})
}

CreateInput 구조체를 담을 수 있는 input 변수를 만들어준다.

c.ShouldBind()를 통해 입력받은 JSON 바디 데이터를 input에 저장한다. 이후 input을 infoList 슬라이스에 추가해준다.

 

 

 

PUT - updateInfo

url 뒤에 id 값을 받아 해당 id 값을 가지는 구조체의 Name, Email 값을 변경해준다.

func updateInfo(c *gin.Context) {
	id, isExist := c.Params.Get("id")

	if !isExist {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	n, err := strconv.Atoi(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	input := UpdateInput{}

	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"status": err.Error(),
		})
		return
	}

	for i, v := range infoList {
		if n == v.Id {
			if input.Name != "" {
				infoList[i].Name = input.Name
			}

			if input.Email != "" {
				infoList[i].Email = input.Email
			}

			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
				"data":   infoList[i],
			})

			return
		}
	}

	c.JSON(http.StatusNotFound, gin.H{
		"status": "Not Found",
	})
}

PUT과 DELETE는 url 뒤에 파라미터 형식으로 id를 보낸다.

이 id를 받는 방법에는 새로 struct를 만들어서 `uri:"id"`라고 지정해주어도 되지만 이번에는 c.Params.Get() 함수를 이용한다.

 

id, isExist := c.Params.Get("id")

updateInfo가 호출될 때 relativePath가 "info/:id"인데 이 id를 c.Params.Get("id")으로 불러오는 것이다.

이때 반환되는 값이 relativePath 내의 :id(id)id가 있는지 여부(isExist)이다.

 

	if !isExist {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

isExist로 id 값이 제대로 넘어왔는지  체크한다. 제대로 안 넘어왔을 경우 잘못된 요청임을 리턴해준다.(400 Bad Request)

 

	n, err := strconv.Atoi(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

받은 id는 string 형태이기 때문에 int 형태로 바꿔 변수 n에 저장한다.

id가 숫자가 아닌 경우에는 잘못 넘어왔기 때문에 400 에러를 리턴해준다.

 

	input := UpdateInput{}

	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"status": err.Error(),
		})
		return
	}

요청 시 JSON 형태로 보낸 Body를 input에 저장해준다.

 

	for i, v := range infoList {
		if n == v.Id {
			if input.Name != "" {
				infoList[i].Name = input.Name
			}

			if input.Email != "" {
				infoList[i].Email = input.Email
			}

			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
				"data":   infoList[i],
			})

			return
		}
	}

	c.JSON(http.StatusNotFound, gin.H{
		"status": "Not Found",
	})

아까 받은 n 값과 같은 ID 값을 가지는 구조체를 찾고,

만약 같다면 Email 값과 Name 값을 변경해주고 HTTP 상태 코드는 요청이 성공했음을 알리는 200을 리턴해준다.

같은 ID 값을 찾지 못하고 for문이 끝난다면 아무것도 찾지 못했다는 것이므로 404 에러를 리턴한다.

 

 

DELETE - deleteInfo

func deleteInfo(c *gin.Context) {
	id, isExist := c.Params.Get("id")

	if !isExist {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	n, err := strconv.Atoi(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	for i, v := range infoList {
		if n == v.Id {
			if len(infoList) > 1 {
				infoList = append(infoList[:i], infoList[i+1:]...)
			} else {
				infoList = []CreateInput{}
			}

			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
				"data":   infoList,
			})

			return
		}
	}

	c.JSON(http.StatusNotFound, gin.H{
		"status": "Not Found",
	})
}

updateInfo와 비슷하게 id 값을 찾아온다.

구조체 슬라이스의 크기가 1인 경우에는 아예 infoList에 값이 없어지므로 CreateInput 슬라이스를 새로 할당해준다.

크기가 1 이상이면 해당 i 인덱스의 구조체만 삭제해준다.

 

 

 

전체 코드

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"strconv"
)

type CreateInput struct {
	Id    int    `json:"id" binding:"required"`
	Name  string `json:"name" binding:"required"`
	Email string `json:"email"`
}

type UpdateInput struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

var infoList []CreateInput

func readInfo(c *gin.Context) {
	if infoList == nil {
		c.JSON(http.StatusNoContent, nil)

		return
	}

	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
		"data":   infoList,
	})
}

func createInfo(c *gin.Context) {
	input := CreateInput{}

	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"status": err.Error(),
		})
		return
	}

	infoList = append(infoList, input)
	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
		"data":   input,
	})
}

func updateInfo(c *gin.Context) {
	id, isExist := c.Params.Get("id")

	if !isExist {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	n, err := strconv.Atoi(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	input := UpdateInput{}

	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"status": err.Error(),
		})
		return
	}

	for i, v := range infoList {
		if n == v.Id {
			if input.Name != "" {
				infoList[i].Name = input.Name
			}

			if input.Email != "" {
				infoList[i].Email = input.Email
			}

			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
				"data":   infoList[i],
			})

			return
		}
	}

	c.JSON(http.StatusNotFound, gin.H{
		"status": "Not Found",
	})
}

func deleteInfo(c *gin.Context) {
	id, isExist := c.Params.Get("id")

	if !isExist {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	n, err := strconv.Atoi(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": "No ID",
		})
		return
	}

	for i, v := range infoList {
		if n == v.Id {
			if len(infoList) > 1 {
				infoList = append(infoList[:i], infoList[i+1:]...)
			} else {
				infoList = []CreateInput{}
			}

			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
				"data":   infoList,
			})

			return
		}
	}

	c.JSON(http.StatusNotFound, gin.H{
		"status": "Not Found",
	})
}

func main() {
	r := gin.Default()
	r.GET("/info", readInfo)
	r.POST("/info", createInfo)
	r.PUT("/info/:id", updateInfo)
	r.DELETE("/info/:id", deleteInfo)

	r.Run("localhost:8080")
}