ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공공데이터를 활용한 의약품 검색 사이트 구축 #1 백엔드 with Golang
    미니프로젝트 2021. 12. 27. 18:28
    반응형
    반응형

    공공데이터를 활용한 의약품 검색 사이트 구축


    1. 프로젝트 구성


    이번 의약품 검색 사이트 구축 프로젝트에 사용될 구성이다.

    - 백엔드(golang)

    - 프론트엔드(vue.js)

    - 배포서버(AWS EC2)

     

    백엔드에서 공공데이터 API를 요청하여 가공한 후 프론트엔드에서 백엔드를 통해 데이터를 확인할 수 있는 방식으로 설계를 할 것이다.


    2. 백엔드 개발환경

    [개발환경]

    OS : Ubuntu 21.10
    LANGUAGE : Golang 1.17.1 linux/amd64
    Framework : Echo Web Framework
    IDE : Visual Studio Code

    이번 프로젝트에서는 공공데이터를 가공하고 반환하는 Rest API 형태로 구축할 것이므로 Golang의 경량 웹 프레임 워크인 Echo를 사용할 것이다.

     

    Echo에 대한 자세한 설명 및 사용법은 공식 문서를 통해 확인할 수 있다.

    https://echo.labstack.com/guide/

     

    Echo - High performance, minimalist Go web framework

    Echo is a high performance, extensible, minimalist web framework for Go (Golang).

    echo.labstack.com


    3. 프로젝트 생성 및 구조 확립

    본인이 사용하고 있는 workspace 혹은 적당한 경로에 프로젝트를 생성한다.

    mkdir project-backend

    프로젝트 루트에 해당하는 폴더를 생성한 것이다.

    project-backend 폴더로 이동 후 main.go 파일을 생성하고 아래 내용을 입력한다.

    // main.go
    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("Hello World!")
    }

    코드를 실행한다.

    go run main.go

    실행결과

    Hello World!

    이제 main.go 파일이 있는 경로에서 현재 프로젝트를 모듈로 구성하기 위해 go.mod 파일을 생성한다.

    go mod init project-backend

    다음으로 프로젝트의 폴더 구조를 확립하기 위해 아래와 같이 폴더를 생성하고 파일을 나눈다.

    .project-backend
    ├── handler
    │    ├── lists.go
    │    └── sendmail.go
    ├── models
    │    └── model.go
    ├── router
    │    └── router.go
    ├── middleware
    │    └── middleware.go
    ├── backend.Dockerfile
    ├── .env
    ├── go.mod
    ├── go.sum
    └── main.go

    > handler

     - lists.go 는 의약품의 리스트 검색 결과 및 단일 항목 검색 결과를 처리하는 로직을 담을 것이다.

     - sendmail.go 는 프로젝트에서 버그가 발생하거나 문제가 있을 경우 메일로 문의를 할 수 있는 로직을 담는다.

    > models

     - model.go 는 handler에서 사용하게 될 검색 결과에 대한 맵핑 테이블을 정의한다.

    > router

     - router.go 는 프로젝트에서 요청에 따른 라우팅을 하기 위해 설정한다.

    > middleware

     - middleware.go 는 추후에 API에서 검증이나 필요한 기능을 중간에서 먼저 적용하기 위해 설정한다.

    > backend.Dockerfile 는 이번 프로젝트를 도커 이미지로 생성하고 컨테이너를 기동하기 위해 생성한다.

     

    .env는 공공데이터 API Key를 부여받고 데이터 요청을 하게 될 경우 사용하고 더 나아가 API URL, 이메일 발신 기능에 필요한 정보를 숨겨서 담기 위해 사용한다.

     

    go.modgo.sum 은 아까 go mod init 으로 자동으로 생성된 파일이다.


    4. 공공데이터 이용 신청

    공공데이터의 이용 신청을 위해 아래의 사이트로 접속을 한다.

    https://www.data.go.kr/index.do

     

    공공데이터 포털

    국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

    www.data.go.kr

    공공데이터를 이용하기 위해선 우선 로그인을 해야 하므로 회원가입을 하고 로그인을 한다.

    로그인을 한 후 목록 탭에서 데이터 찾기 > 데이터목록 탭을 클릭한다.

    검색창에 "e약은요" 라는 키워드로 검색하면 분류 조건에서 오픈 API에 1건의 정보가 확인되는데 우리는 이 오픈 API를 활용할 것이므로 활용신청 버튼을 누른다.

     

    활용신청에 필요한 정보를 입력하는 란이 나오는데 우리는 웹 사이트를 구축하기 위함으로 웹사이트 개발을 선택하고 목적의 내용에 "공공데이터를 활용한 웹 개발 스터디" 등을 입력하고 첨부파일은 패스한다.

    나머지 내용은 디폴트로 설정하고 라이선스 표시에서 이용허락 범위에 "동의합니다"를 체크하고 활용신청을 누른다.

     

    공공데이터의 신청이 완료되면 상단에 마이페이지로 이동하면 위와 같이 신청이 된 내용을 확인할 수 있으며 우리가 이번에 사용할 데이터는 자동승인이 되는 것으로 활용에 1건이 검색이 될 것이다. 그럼 검색된 결과를 클릭해보자.

     

    데이터의 상세 정보가 나오며 우리가 실제로 사용할 End Point와 인증키를 확인할 수 있다.

    실제로 어떤 파라미터를 통해 데이터를 요청할 수 있는지 확인하기 테스트를 해볼 수도 있다.

    미리보기의 확인 버튼을 눌러본다.

    요청 변수를 넣어 요청할 수 있는 패널이 나오므로 서비스 키에 위에서 받았던 인코딩 된 인증키를 복사해서 넣고 제품명에 "아스피린"을 입력 후 타입을 json으로 선택해서 미리보기를 눌러보았다.

    브라우저의 새 탭이 열리면서 요청에 대한 응답이 오는 것을 확인할 수 있었다.

    참고 문서를 확인해서 파라미터에 대한 디폴트 건수를 확인하고자 했으나 설명이 없었기에 몇 번의 테스트를 더 진행해서 실제로 사용할 때 적용할 파라미터를 확인했다.

    인증키와 타입을 제외하고 나머지 파라미터는 비워두고 요청을 해봤다.

    확인할 수 있었던 점은 페이지의 번호는 1로 적용되는 것이고, 한 페이지에 표현될 데이터의 개수는 10개인 것이 확인된다. 그리고 토탈카운트의 개수로 보아 전체 데이터가 검색된 것이다. 그렇다면 검색에 사용할 경우 부분 검색이 가능한지 여부를 판단하기 위해 일부 키워드로 데이터를 다시 요청해보았다.

    업체명에 "보령"이라는 키워드를 입력하고 한 페이지의 결과 수는 1000으로 설정 후 다시 요청해본다.

    한 페이지의 결과 수는 100이란 것을 이제야 알았다. 10으로 수정하고 다시 요청해본다.

    "보령"이란 키워드가 업체명에 포함된 데이터를 반환한다. 부분 검색을 지원하는 것이다. 그럼 이제 실제 프로그램에서 데이터를 가공해본다.


    5. 프로젝트 코드 작성

    > .env

    아까 공공데이터 포털 사이즈에서 발급받은 인증키와 API 엔트포인트를 각각의 키값에 선언한다.

    // .env
    apiKey=ㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁ
    apiUrl=http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList

    > models

     - model.go

    공공데이터의 response 값을 매핑해줄 구조체를 선언한다. 또한 추후에 사용할 메일 기능에 사용할 구조체도 추가해준다.

    // models/model.go
    package models
    
    // 공공데이터 RESPONSE DATA 매핑 모델
    type ResData struct {
    	Header Header `json:"header"`
    	Body   Body   `json:"body"`
    }
    type Header struct {
    	ResultCode string `json:"resultCode"`
    	ResultMsg  string `json:"resultMsg"`
    }
    type Items struct {
    	EntpName            string `json:"entpName"`
    	ItemName            string `json:"itemName"`
    	ItemSeq             string `json:"itemSeq"`
    	EfcyQesitm          string `json:"efcyQesitm"`
    	UseMethodQesitm     string `json:"useMethodQesitm"`
    	AtpnWarnQesitm      string `json:"atpnWarnQesitm"`
    	AtpnQesitm          string `json:"atpnQesitm"`
    	IntrcQesitm         string `json:"intrcQesitm"`
    	SeQesitm            string `json:"seQesitm"`
    	DepositMethodQesitm string `json:"depositMethodQesitm"`
    	OpenDe              string `json:"openDe"`
    	UpdateDe            string `json:"updateDe"`
    	ItemImage           string `json:"itemImage"`
    }
    type Body struct {
    	PageNo     int     `json:"pageNo"`
    	TotalCount int     `json:"totalCount"`
    	NumOfRows  int     `json:"numOfRows"`
    	Items      []Items `json:"items"`
    }
    
    // 문의 메일 기능 매핑 구조체
    type MailContent struct {
    	Name     string `json:"name"`
    	MailAddr string `json: "mailAddr"`
    	Content  string `json: "content"`
    	IpAddr   string `json: "ipAddr"`
    }

    > handler

     - lists.go

     

    검색을 통해 결과를 리스트로 반환하고 단일 항목 검색 결과를 반환하는 기능을 설정한다.

    // hanlder/lists.go
    package handler
    
    import (
    	"encoding/json"
    	"fmt"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"net/url"
    	"os"
    
    	"project-backend/models"
    
    	"github.com/labstack/echo/v4"
    )
    
    const format string = "json"
    const numOfRows string = "10"
    
    // 의약품 검색 결과 리스트를 반환
    func GetSearchResultLists(c echo.Context) error {
    	// 의약품명
    	itemName := c.QueryParam("itemName")
    	// 페이지번호
    	pageNo := c.QueryParam("pageNo")
    	// 업체명
    	entpName := c.QueryParam("entpName")
    	// 약의 효능
    	efcyQesitm := c.QueryParam("efcyQesitm")
    	// 인증키
    	apiKey := os.Getenv("apiKey")
    	params := "serviceKey=" + url.QueryEscape(apiKey) + "&" +
    		"pageNo=" + url.QueryEscape(pageNo) + "&" +
    		"numOfRows=" + url.QueryEscape(numOfRows) + "&" +
    		"itemName=" + url.QueryEscape(itemName) + "&" +
    		"entpName=" + url.QueryEscape(entpName) + "&" +
    		"efcyQesitm=" + url.QueryEscape(efcyQesitm) + "&" +
    		"type=" + url.QueryEscape(format)
    
    	path := fmt.Sprintf("%s?%s", os.Getenv("apiUrl"), params)
    
    	resp, err := http.Get(path)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer resp.Body.Close()
    
    	body, err := ioutil.ReadAll(resp.Body)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	resData := models.ResData{}
    	json.Unmarshal(body, &resData)
    
    	return c.JSON(http.StatusOK, resData)
    }
    
    // 의약품 1개에 대한 상세페이지용 정보를 반환
    func GetOneDrugDetail(c echo.Context) error {
    	apiKey := os.Getenv("apiKey")
    	// 페이지에서 상세페이지를 연결할 경우 의약품 기준코드로 단일 항목을 가져온다.
    	itemSeq := c.QueryParam("itemSeq")
    	params := "serviceKey=" + url.QueryEscape(apiKey) + "&" +
    		"itemSeq=" + url.QueryEscape(itemSeq) + "&" +
    		"type=" + url.QueryEscape(format)
    
    	path := fmt.Sprintf("%s?%s", os.Getenv("apiUrl"), params)
    
    	resp, err := http.Get(path)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer resp.Body.Close()
    
    	body, err := ioutil.ReadAll(resp.Body)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	resData := models.ResData{}
    	json.Unmarshal(body, &resData)
    	// 공공데이터의 Response 에서 공개일과 수정일의 날짜 형식을 통일 시킴
    	resData.Body.Items[0].OpenDe = resData.Body.Items[0].OpenDe[0:10]
    	resData.Body.Items[0].UpdateDe = resData.Body.Items[0].UpdateDe[0:4] + "-" + resData.Body.Items[0].UpdateDe[4:6] + "-" + resData.Body.Items[0].UpdateDe[6:8]
    	return c.JSON(http.StatusOK, resData)
    }

     - sendmail.go

    일단 이번 프로젝트에는 Gmail SMTP 설정을 통해 문의 메일을 발송하는 방식을 사용한다.

    우선 본인의 Gmail 설정을 변경을 위해 아래의 문서대로 메일용 토큰 값을 발행받아야 한다.

    https://support.google.com/a/answer/176600?hl=ko#zippy=%2Cgmail-smtp-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0 

     

    프린터, 스캐너, 앱에서 이메일 보내기 - Google Workspace 관리자 고객센터

    도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요

    support.google.com

    토큰 발행이 완료된다면 코드를 작성한다. 내 메일 주소를 통해 내 메일로 전송하는 트릭일 뿐이다. 대신 보내는 이와 받는 이를 다른 주소로 설정해야 한다. 필자는 보내는 사람을 테스트용 구글 계정을 만들어서 사용했다.

    // handler/sendmail.go
    package handler
    
    import (
    	"net/http"
    	"net/smtp"
    	"os"
    
    	"project-backend/models"
    
    	"github.com/labstack/echo/v4"
    )
    
    func SendMail(c echo.Context) error {
    	var m models.MailContent
    	if err := c.Bind(&m); err != nil {
    		panic(err)
    	}
    	
    	auth := smtp.PlainAuth("", os.Getenv("emailSender"), os.Getenv("emailToken"), "smtp.gmail.com")
    	// 보내는 사람
    	from := os.Getenv("emailSender")
    	// 받는 사람
    	to := []string{os.Getenv("emailReciver")}
    	headerSubject := "개발사이트 문의글: " + m.Name + "님으로 부터\r\n"
    	headerBlank := "\r\n"
    	body := m.MailAddr + ", " + m.Name + "님의 문의 글입니다.\n\n"
    	body += m.Content + "\n\n클라이언트 IP: "
    	body += m.IpAddr
    	msg := []byte(headerSubject + headerBlank + body)
    
    	err := smtp.SendMail("smtp.gmail.com:587", auth, from, to, msg)
    	if err != nil {
    		panic(err)
    	}
    
    	return c.JSON(http.StatusOK, "Completed Sending Email")
    }

    위의 내용과 함께 .env 파일을 수정해준다.

    apiKey=ㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁ
    apiUrl=http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList
    emailToken=ㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁㅁ
    emailSender=ㅁㅁㅁㅁㅁ@gmail.com
    emailReciver=ㅁㅁㅁㅁㅁㅁ@gmail.com
    HTTP_PORT=5000

    이메일을 인증에 필요한 토큰과 보내는 사람, 받는 사람을 추가해준다. HTTP_PORT도 추후에 사용할 것이므로 추가해준다.


    > middleware

     - middleware.go

     

    핸들러와 라우터의 중개의 역할이 필요할 경우를 대비하여 만든다. 우리는 추후에 로컬 프론트엔드에서 동일 출처에 포트만 달라진 환경에서 요청할 경우 CORS가 발생할 수 있는 상황이므로 Echo 프레임워크에서 제공하는 미들웨어를 사용해서 일단 모든 오리진을 허용해두었다.

    // middleware/middleware.go
    package middleware
    
    import (
    	"net/http"
    
    	"github.com/labstack/echo/v4/middleware"
    )
    
    var Cors = middleware.CORSWithConfig(middleware.CORSConfig{
    	AllowOrigins: []string{"*"},
    	AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
    })

    > router

     - router.go

     

    라우터를 main.go 파일에서 직접 구성을 해도 상관없지만 코드가 길어지는 경우를 대비하여 작성한다. 우리가 핸들러에 작성했던 검색 기능과 메일 발송 기능을 요청에 맞게 라우팅 한다. API 서버 상태를 체크를 위해 헬스체크를 넣었다.

    // router/router.go
    package router
    
    import (
    	"net/http"
    
    	"project-backend/handler"
    
    	md "project-backend/middleware"
    	"github.com/labstack/echo/v4"
    	"github.com/labstack/echo/v4/middleware"
    )
    
    func Router() *echo.Echo {
    	e := echo.New()
    	e.Debug = true
    	e.Use(middleware.Logger())
    	e.Use(middleware.Recover())
    	e.Use(md.Cors)
    
    	// health check
    	e.GET("/healthy", func(c echo.Context) error {
    		return c.String(http.StatusOK, "Health Check OK!!")
    	})
    
    	// 의약품 검색 리스트 취득 API
    	e.GET("/api/search", handler.GetSearchResultLists)
    
    	// 품목기준코드를 통한 의약품 정보 취득 API
    	e.GET("/api/detail", handler.GetOneDrugDetail)
    
    	// 문의글 발송
    	e.POST("/api/contact", handler.SendMail)
    
    	return e
    }

    > main.go

     

    우리 프로젝트의 구동점이 되는 main.go 파일을 작성한다. 라우터를 생성하고 포트 설정과 함께 서버를 구동한다.

    package main
    
    import (
    	"fmt"
    	"os"
    
    	router "project-backend/router"
    	"github.com/joho/godotenv"
    )
    
    func main() {
    	// 고언어에서 .env를 환경변수를 세팅하는 모듈
    	// 프로덕션 환경에서는 필요없음(직접 환경변수를 세팅하기때문에)
    	err := godotenv.Load(".env")
    
    	if err != nil {
    		log.Fatal("환경변수를 읽는 과정에서 오류가 발생했습니다.")
    	}
        
    	// 라우터 생성
    	echoR := router.Router()
    
    	// 서버 가동
    	fmt.Println("백엔드 서버를 구동합니다.")
    	httpPort := os.Getenv("HTTP_PORT")
    	if httpPort == "" {
    		httpPort = "8080"
    	}
    	echoR.Logger.Fatal(echoR.Start(":" + httpPort))
    }

    6. API 테스트

    우리는 프로젝트 구조를 확립하고 코드를 작성했으므로 프로젝트에서 사용될 외부 모듈을 설치해야 한다.

    아래와 같은 명령어를 통해 설치를 진행한다.

    go mod tidy

    설치가 완료된다면 서버를 실행한다.

    go run main.go
      ____    __
      / __/___/ /  ___
     / _// __/ _ \/ _ \
    /___/\__/_//_/\___/ v4.6.1
    High performance, minimalist Go web framework
    https://echo.labstack.com
    ____________________________________O/_______
                                        O\
    ⇨ http server started on [::]:5000

    서버가 정상적으로 실행된 것을 확인하면 포스트맨을 통해서 API를 요청해보자.

    의약품명은 "아스피린", 페이지 번호는 1번으로 요청을 보내본다.

     

    localhost:5000/api/search?itemName=아스피린&pageNo=1

    status 200으로 요청된 결과가 잘 오는 것을 볼 수가 있다.

    핸들러에서 페이지당 개수는 고정값으로 지정해놨기 때문에 10개의 데이터가 반환된 것을 볼 수 있다.


    7. 마무리

    이번 프로젝트는 백엔드, 프론트엔드, 배포를 순으로 진행하며 3편에 나눠서 작성을 한다. 다음 2편에서 프론트엔드를 진행하며 vue.js를 통한 웹페이지 구축 및 1편에서 작성한 API와 통신을 할 예정이다. 현재 이 프로젝트는 배포가 된 상태이며 아래의 문서를 통해 코드 확인이 가능하다. 현재 클라우드 서버의 무료 기간이 종료되어 배포된 사이트에 접속할 수 없습니다.

     

    https://github.com/JEONG-YUNHO01/project-backend

     

    GitHub - JEONG-YUNHO01/project-backend

    Contribute to JEONG-YUNHO01/project-backend development by creating an account on GitHub.

    github.com

    사이트 주소

    https://medicineinfo.n-e.kr/ 

     

    의약품 검색

     

    medicineinfo.n-e.kr

     

    반응형

    댓글

Designed by Tistory.