외박 신청 어플 API go 리팩토링

lsy·2022년 10월 22일
0

좀 더 나은 성능으로

이전에 고루틴을 활용해 외박 신청에 실패한 글을 작성하였다. 그래서 사실 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초 사이라서 그런 것 같다. 아마 데이터의 사이즈가 더 커지면 체감이 날 것 같긴한데..

추가

고루틴이 대체 왜 더 느린지에 대해 알아보다가 다음 글을 발견했다. 내용에 대해 다음 글에서 정리했다.

https://velog.io/@sunaookamisiroko/%EA%B3%A0%EB%A3%A8%ED%8B%B4goroutine%EC%9D%80-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EB%B9%A0%EB%A5%BC%EA%B9%8C

profile
server를 공부하고 있습니다.

0개의 댓글