개요
관광데이터 공모전에서 필요한 지역별 관광사진을 오픈 API에서 가져와 데이터베이스에 저장해야 한다. 관련된 데이터베이스를 보면 다음과 같은 형식으로 설계되어 있다.
API를 개발중인 언어는 Java, 프레임워크는 Spring이지만, 굳이 클라우드와 배포 서버를 활용해서 관련 코드를 퍼블릭하게 배포할 이유가 없고, 단발성으로 사용하고 사라질 코드이기 때문에 속도가 빠르고 손에 익숙한 Go를 활용해 보기로 했다.
개발 튜토리얼
1. 데이터베이스와 연결하기
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-sql-driver/mysql"
)
func GetConnector() *sql.DB {
config := mysql.Config{
User: "데이터베이스 유저 이름",
Passwd: "데이터베이스 유저 패스워드",
Net: "tcp",
Addr: "rds 인스턴스 정보에서 제공하는 연결 uri",
Collation: "utf8mb4_general_ci",
Loc: time.UTC,
MaxAllowedPacket: 4 << 20.,
AllowNativePasswords: true,
CheckConnLiveness: true,
DBName: "DB 이름",
}
connector, err := mysql.NewConnector(&config)
if err != nil {
panic(err)
}
db := sql.OpenDB(connector)
return db
}
func main() {
db := GetConnector()
err := db.Ping()
if err != nil {
panic(err)
}
}
다음과 같은 코드를 사용해 mysql에 대한 기본적인 정보를 담은 config 객체를 생성하고, config를 활용해 rds로 생성해 둔 mysql instance과 연결한다.
2. `area` 테이블의 값 처리하기
전체 area 테이블의 데이터 개수는 160개이고, '대도시'를 기준으로 '소도시' 여러개로 나뉘어져 있다. '소도시'는 '/'를 기준으로 3-4개 정도 묶여서 한 개의 데이터를 구성한다. 이 부분을 아래 코드와 같이 처리한다.
for i := 1; i <=160; i++ {
var city string
err = db.QueryRow("SELECT city FROM area WHERE area_id = " + strconv.Itoa(i)).Scan(&city)
if err != nil {
log.Fatal(err)
}
var arr = strings.Split(city, "/")
}
3. `area`의 분리된 `city` 배열(==`arr`)을 기준으로 OPEN API에 데이터 요청하기
'강남/역삼/삼성/논현'을 2번에서 작성한 코드에 맞춰 배열로 만들면(arr), [강남, 역삼, 삼성, 논현]과 같은 형식이 될 것이다.
따라서 배열의 length에 따라, 개별 지역 한 개 씩 OPEN API에 쿼리로 넣어 데이터를 요청한다.
for j := 0; j < len(arr); j++ {
encodedArea := url.QueryEscape(arr[j])
fmt.Println(arr[j], encodedArea)
url := "https://apis.data.go.kr/B551011/PhotoGalleryService1/gallerySearchList1?serviceKey=''_type=json&MobileOS=ETC&MobileApp=Snap&numOfRows=150&keyword=" + encodedArea
}
4. json으로 불러온 결과값을 파싱하기
{
"response": {
"header": {
"resultCode": "0000",
"resultMsg": "OK"
},
"body": {
"items": {
"item": [
{
"galContentId": "1058713",
"galContentTypeId": "17",
"galTitle": "기차팬션",
"galWebImageUrl": "http://tong.visitkorea.or.kr/cms2/website/13/1058713.jpg",
"galCreatedtime": "20100713095739",
"galModifiedtime": "20150828095430",
"galPhotographyMonth": "201006",
"galPhotographyLocation": "강원도 정선군",
"galPhotographer": "한국관광공사 김지호",
"galSearchKeyword": "정선 기차팬션 안내문, 숙박시설, 팬션"
},
{
"galContentId": "1058787",
"galContentTypeId": "17",
"galTitle": "풍경열차",
"galWebImageUrl": "http://tong.visitkorea.or.kr/cms2/website/87/1058787.jpg",
"galCreatedtime": "20100713104650",
"galModifiedtime": "20150828100329",
"galPhotographyMonth": "201006",
"galPhotographyLocation": "강원도 정선군",
"galPhotographer": "한국관광공사 김지호",
"galSearchKeyword": "정선 풍경열차, 기차여행"
},
{
"galContentId": "1961825",
"galContentTypeId": "17",
"galTitle": "정선에서 본 한반도",
"galWebImageUrl": "http://tong.visitkorea.or.kr/cms2/website/25/1961825.jpg",
"galCreatedtime": "20141103154608",
"galModifiedtime": "20170215135646",
"galPhotographyMonth": "201401",
"galPhotographyLocation": "강원도 정선군",
"galPhotographer": "김용일",
"galSearchKeyword": "2014 제42회 대한민국 관광사진 공모전, 입선, 강원도 정선, 한반도지형, 스카이워크, 정선 스카이워크"
}
]
},
"numOfRows": 3,
"pageNo": 1,
"totalCount": 21
}
}
}
type JSONBody struct {
Response struct {
Header struct {
ResultCode string
ResultMsg string
}
Body struct {
Items struct {
Item []struct {
GalContentId string
GalContentTypeId string
GalTitle string
GalWebImageUrl string
GalCreatedtime string
GalModifiedtime string
GalPhotographyMonth string
GalPhotographyLocation string
GalPhotographer string
GalSearchKeyword string
}
}
NumOfRows int
PageNo int
TotalCount int
}
}
}
1) Json Body를 String으로 변환하고, String을 json으로 변환할 때 필요한 객체를 정의하면 위와 같다.
func getJSON(url string) string {
r, err := client.Get(url)
if err != nil {
panic(err)
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
return string(body)
}
2) 위 함수는 3번의 url로 데이터를 요청해서, Body를 우리가 해석할 수 있는 String으로 만드는 함수이다.
for i := 1; i <=160; i++ {
var city string
err = db.QueryRow("SELECT city FROM area WHERE area_id = " + strconv.Itoa(i)).Scan(&city)
if err != nil {
log.Fatal(err)
}
var arr = strings.Split(city, "/")
for j := 0; j < len(arr); j++ {
encodedArea := url.QueryEscape(arr[j])
fmt.Println(arr[j], encodedArea)
url := "https://apis.data.go.kr/B551011/PhotoGalleryService1/gallerySearchList1?serviceKey=''_type=json&MobileOS=ETC&MobileApp=Snap&numOfRows=150&keyword=" + encodedArea
var body JSONBody
err := json.Unmarshal([]byte(getJSON(url)), &body)
if err != nil {
fmt.Println("JSON 파싱 오류:", err)
}
fmt.Println("Items:")
for _, item := range body.Response.Body.Items.Item {
fmt.Printf(" GalContentId: %s\n", item.GalContentId)
fmt.Printf(" GalTitle: %s\n", item.GalTitle)
fmt.Printf(" GalWebImageUrl: %s\n", item.GalWebImageUrl)
fmt.Printf(" GalPhotographyMonth: %s\n", item.GalPhotographyMonth)
fmt.Printf(" GalPhotographyLocation: %s\n", item.GalPhotographyLocation)
fmt.Printf(" GalPhotographer: %s\n", item.GalPhotographer)
fmt.Printf(" GalSearchKeyword: %s\n", item.GalSearchKeyword)
fmt.Println("------------------------")
query := fmt.Sprintf("insert into area_image (area_id, title, location, photographer, url) value (%d, '%s', '%s', '%s', '%s')", i, item.GalTitle, item.GalPhotographyLocation, item.GalPhotographer, item.GalWebImageUrl)
log.Println("query: ", query)
result, err := db.Exec(query)
if err != nil {
panic(err)
}
log.Println(result)
}
}
}
함수를 적절히 변형해서 json의 item을 하나씩 데이터베이스에 삽입하는 쿼리를 실행시키는 최종 함수는 위와 같다.
결과
약 4000개가 넘는 데이터가 로컬 환경에서 3분 안에 처리되었다.