이전에 고루틴을 활용해 외박 신청에 실패한 글을 작성하였다. 그래서 사실 API 코드를 리팩토링 하는 것은 포기하고 있었으나 문득 그런 생각이 들었다.
다른 부분에서 고루틴을 활용(파싱이라던가)하면 되지 않을까? 만약 안되면 고루틴이 아니더라도 Go가 js보다 빠르지 않을까?
따라서 리팩토링을 진행해보기로 했다. 결과는 잘 완료돼서 현재 Lambda에 배포하였다.
https://github.com/AUTO-Overnight/Auto_Overnight_API
리팩토링을 진행하면서 제일 신경 썼던건 고루틴이다. 정말로 성능 향상이 있는지 알아보고 싶었다.
func Login(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
...
// 학생 이름, 학번, 년도, 학기 찾기 위한 채널 생성
findUserNmChan := make(chan models.FindUserNmModel)
findYYtmgbnChan := make(chan models.FindYYtmgbnModel)
// 파싱 시작
go models.RequestFindUserNm(client, findUserNmChan, nil)
go models.RequestFindYYtmgbn(client, findYYtmgbnChan, nil)
studentInfo := <-findUserNmChan
yytmGbnInfo := <-findYYtmgbnChan
if studentInfo.Error != nil || yytmGbnInfo.Error != nil {
return error_response.MakeErrorResponse(err, 500)
}
if studentInfo.XML.Parameters.Parameter == "-600" {
return error_response.MakeErrorResponse(error_response.WrongIdOrPasswordError, 400)
}
...
// 응답 위한 json body 만들기
responseBody := make(map[string]interface{})
// 이름, 년도, 학기 저장
responseBody["name"] = studentInfo.XML.Dataset[0].Rows.Row[0].Col[0].Data
responseBody["yy"] = yytmGbnInfo.XML.Dataset[0].Rows.Row[0].Col[0].Data
responseBody["tmGbn"] = yytmGbnInfo.XML.Dataset[0].Rows.Row[0].Col[1].Data
// 쿠키, 외박 신청 내역 파싱 내역 전달받기 위한 채널 생성
cookiesChan := make(chan map[string]string)
outStayFrDtChan := make(chan []string)
outStayToDtChan := make(chan []string)
outStayStGbnChan := make(chan []string)
// 파싱 시작
go models.ParsingStayoutList(stayOutList, outStayFrDtChan, outStayToDtChan, outStayStGbnChan)
go models.ParsingCookies(req, cookiesChan)
responseBody["cookies"] = <-cookiesChan
responseBody["outStayFrDt"] = <-outStayFrDtChan
responseBody["outStayToDt"] = <-outStayToDtChan
responseBody["outStayStGbn"] = <-outStayStGbnChan
// 응답 json 만들기
responseJson, err := json.Marshal(responseBody)
if err != nil {
return error_response.MakeErrorResponse(error_response.MakeJsonBodyError, 500)
}
response := events.APIGatewayProxyResponse{
StatusCode: 200,
Body: string(responseJson),
}
return response, nil
}
코드가 너무 많아서 다 담진 못하지만 일부 코드만 가져오자면 위와 같이 서로 연관 없는 내용을 학교 API에 요청해서 가져오거나, 가져온 결과를 파싱하여 슬라이스에 담을 때 고루틴을 활용했다.
// ParsingStayoutList 외박 신청 내역 파싱하는 함수
func ParsingStayoutList(stayOutList Root, outStayFrDtChan, outStayToDtChan, outStayStGbnChan chan []string) {
// 외박 신청 내역 파싱 내역 저장 위한 슬라이스 생성
outStayFrDt := make([]string, len(stayOutList.Dataset[1].Rows.Row))
outStayToDt := make([]string, len(stayOutList.Dataset[1].Rows.Row))
outStayStGbn := make([]string, len(stayOutList.Dataset[1].Rows.Row))
var wg sync.WaitGroup
wg.Add(len(stayOutList.Dataset[1].Rows.Row))
// 파싱 시작
for i, v := range stayOutList.Dataset[1].Rows.Row {
go func(i int, v Row) {
outStayFrDt[i] = v.Col[2].Data
outStayToDt[i] = v.Col[1].Data
outStayStGbn[i] = v.Col[5].Data
wg.Done()
}(i, v)
}
wg.Wait()
outStayFrDtChan <- outStayFrDt
outStayToDtChan <- outStayToDt
outStayStGbnChan <- outStayStGbn
}
// ParsingCookies 쿠키 파싱하는 함수
func ParsingCookies(req *http.Request, cookiesChan chan map[string]string) {
// 쿠키 파싱 위한 슬라이스 생성
cookies := make(map[string]string)
// 파싱 시작
for _, info := range req.Cookies() {
cookies[info.Name] = info.Value
}
cookiesChan <- cookies
}
// ParsingPointList 상벌점 내역 파싱하는 함수
func ParsingPointList(pointList Root, cmpScrChan, lifSstArdGbnChan, ardInptDtChan, lifSstArdCtntChan chan []string) {
// 상벌점 내역 파싱 위한 슬라이스 생성
cmpScr := make([]string, len(pointList.Dataset[0].Rows.Row))
lifSstArdGbn := make([]string, len(pointList.Dataset[0].Rows.Row))
ardInptDt := make([]string, len(pointList.Dataset[0].Rows.Row))
lifSstArdCtnt := make([]string, len(pointList.Dataset[0].Rows.Row))
var wg sync.WaitGroup
wg.Add(len(pointList.Dataset[0].Rows.Row))
// 파싱 시작
for i, v := range pointList.Dataset[0].Rows.Row {
go func(i int, v Row) {
cmpScr[i] = v.Col[4].Data
lifSstArdGbn[i] = v.Col[8].Data
ardInptDt[i] = v.Col[10].Data
lifSstArdCtnt[i] = v.Col[2].Data
wg.Done()
}(i, v)
}
wg.Wait()
cmpScrChan <- cmpScr
lifSstArdGbnChan <- lifSstArdGbn
ardInptDtChan <- ardInptDt
lifSstArdCtntChan <- lifSstArdCtnt
}
위는 파싱하는 함수로 쿠키 빼고는 전부 고루틴을 사용했다.
결과는 js 코드, 파싱에 고루틴을 적용한 go 코드, 파싱에 고루틴을 적용하지 않고 반복문, 순차진행으로 적용한 go 코드 이렇게 셋으로 나눴다.
전부 aws lambda에 올리고 login 함수 실행을 기준으로 포스트맨에서 20회 측정했다.
js
2.88
2.92
3.18
2.97
3.10
2.97
3.02
2.88
2.96
3.11
2.98
3.40
3.39
2.98
2.95
3.08
3.13
3.16
3.18
2.96
61.2 / 20 = 3.06
go (no goroutine)
2.67
2.73
2.61
2.67
2.63
3.23
2.56
2.83
2.83
2.95
2.81
2.67
3.12
2.69
2.64
2.75
2.76
2.67
2.74
2.60
55.16 /20 = 2.758
go (goroutine)
3.40
2.71
2.57
2.70
2.70
2.93
3.62
2.84
2.94
2.85
2.66
2.80
2.77
2.68
2.72
2.81
2.78
2.79
3.50
2.66
57.43 / 20 =2.8715
결과적으로는 go 코드가 js 코드보다 더 빨랐고, 고루틴을 적용한 것이 오히려 더 시간이 많이 들었다.
아마 내 생각엔 겨우 슬라이스에 데이터를 집어넣는 간단한 작업인데 고루틴을 써서 오버헤드때문에 시간이 더 걸리는 것 같았다.
그래서 최종적으로는 파싱할 때 고루틴은 제거하기로 했다. 특히 sync.WaitGroup을 통한 고루틴이 시간이 많이 걸리는 것 같았다.
package models
import (
"net/http"
)
// ParsingStayoutList 외박 신청 내역 파싱하는 함수
func ParsingStayoutList(stayOutList Root) ([]string, []string, []string) {
// 외박 신청 내역 파싱 내역 저장 위한 슬라이스 생성
outStayFrDt := make([]string, len(stayOutList.Dataset[1].Rows.Row))
outStayToDt := make([]string, len(stayOutList.Dataset[1].Rows.Row))
outStayStGbn := make([]string, len(stayOutList.Dataset[1].Rows.Row))
// 파싱 시작
for i, v := range stayOutList.Dataset[1].Rows.Row {
outStayFrDt[i] = v.Col[2].Data
outStayToDt[i] = v.Col[1].Data
outStayStGbn[i] = v.Col[5].Data
}
return outStayFrDt, outStayToDt, outStayStGbn
}
// ParsingCookies 쿠키 파싱하는 함수
func ParsingCookies(req *http.Request) map[string]string {
// 쿠키 파싱 위한 슬라이스 생성
cookies := make(map[string]string)
// 파싱 시작
for _, info := range req.Cookies() {
cookies[info.Name] = info.Value
}
return cookies
}
// ParsingPointList 상벌점 내역 파싱하는 함수
func ParsingPointList(pointList Root) ([]string, []string, []string, []string) {
// 상벌점 내역 파싱 위한 슬라이스 생성
cmpScr := make([]string, len(pointList.Dataset[0].Rows.Row))
lifSstArdGbn := make([]string, len(pointList.Dataset[0].Rows.Row))
ardInptDt := make([]string, len(pointList.Dataset[0].Rows.Row))
lifSstArdCtnt := make([]string, len(pointList.Dataset[0].Rows.Row))
// 파싱 시작
for i, v := range pointList.Dataset[0].Rows.Row {
cmpScr[i] = v.Col[4].Data
lifSstArdGbn[i] = v.Col[8].Data
ardInptDt[i] = v.Col[10].Data
lifSstArdCtnt[i] = v.Col[2].Data
}
return cmpScr, lifSstArdGbn, ardInptDt, lifSstArdCtnt
}
이외에 나머지 함수들에서도 js보다 go가 속도를 압도했다. 포스트맨 기준으로 대략 50~150ms 정도 go가 더 빨랐다.
lambda의 cold start일 때는 js는 최대 900ms도 나왔는데, golang은 그렇게까지 나오진 않고 400~500ms정도 나왔다.
다만 실제 어플에서는 그렇게 큰 체감은 되지 않았다. 0.5초~ 1초 사이라서 그런 것 같다. 아마 데이터의 사이즈가 더 커지면 체감이 날 것 같긴한데..
고루틴이 대체 왜 더 느린지에 대해 알아보다가 다음 글을 발견했다. 내용에 대해 다음 글에서 정리했다.