https://smgthings.tistory.com/10?category=965902
학교 외박 신청 어플 제작 후기 : serverless를 이용한 aws lambda backend
나는 학교 외박 신청 어플의 API를 만들어서 운영하고 있다.
사실 조금 아쉬운 점이 존재했는데 바로 한 달 외박 신청이 조금 느리다는 것이다.
느린 이유는 28개의 외박 신청을 하나하나 http requests 보내기 때문이다.
바로 이 부분인데, xml을 만들고 axios post requests를 보내는 작업을 28번 반복하고 있다.
golang의 고루틴을 사용하면 속도를 해결할 수 있지 않을까? 라는 생각을 하게 되었다. 그래서 js로 짠 코드를 go로 옮기기 시작했다.
외박 신청을 위한 조건은 다음과 같다.
로그인 -> 학생 이름, 학번 찾기 -> 년도, 학기 찾기 -> 생활관 거주 구분 학생 번호 찾기 -> 외박 신청
우리 학교 로그인은 x-www-form-unlencoded를 사용하므로 그것에 맞게 옮겼다.
이곳의 내용을 참고했다.
func main() {
loginInfo := url.Values{
"internalId": {""},
"internalPw": {""},
"gubun": {""},
}
jar, err := cookiejar.New(nil)
if err != nil {
panic(err)
}
req, err := http.NewRequest("POST", "학교 로그인 주소",
bytes.NewBufferString(loginInfo.Encode()))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{
Jar: jar,
}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
req, err = http.NewRequest("GET", "학교 통합정보시스템 주소", nil)
if err != nil {
panic(err)
}
res, err = client.Do(req)
if err != nil {
panic(err)
}
...
먼저 학생 이름, 학번을 학교 API로 부터 전달 받기 전에 XML을 받을 struct를 만들어야 한다.
golang은 강타입 언어라 js보다 써줘야 할 것이 많았다. js에서는 cheerio로 한번에 xml을 파싱할 수 있었는데, golang에서는 struct를 만들어서 그곳에 담아야했다.
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns="http://www.nexacro.com/platform/dataset" ver="5000">
<Parameters>
<Parameter id="ErrorCode" type="int">0</Parameter>
</Parameters>
<Dataset id="DS_GLIO">
<ColumnInfo>
<Column id="userNm" type="string" size="32"/>
<Column id="persNo" type="string" size="32"/>
</ColumnInfo>
<Rows>
<Row>
<Col id="userNm">이름</Col>
<Col id="persNo">학번</Col>
</Row>
</Rows>
</Dataset>
</Root>
type Root struct {
XMLName xml.Name `xml:"Root"`
Parameters Parameters `xml:"Parameters"`
Dataset Dataset `xml:"Dataset"`
}
type Parameters struct {
Parameter string `xml:"Parameter"`
}
type Dataset struct {
ColumnInfo ColumnInfo `xml:"ColumnInfo"`
Rows Rows `xml:"Rows"`
}
type ColumnInfo struct {
Column []string `xml:"Column"`
}
type Rows struct {
Row Row `xml:"Row"`
}
type Row struct {
Col []Col `xml:"Col"`
}
type Col struct {
Id string `xml:"id,attr"`
Data string `xml:",chardata"`
}
다행히 학교 API에서는 통일된 규격의 xml을 사용해서 이것으로 전부 다 받을 수 있었다.
이 다음으로는 다음 코드로 학생 이름과 학번을 xml로 받아와 struct에 저장했다.
findUserNmXML := []byte(`body에 들어갈 xml`)
req, err = http.NewRequest("POST", "학교 api", bytes.NewBuffer(findUserNmXML))
if err != nil {
panic(err)
}
res, err = client.Do(req)
if err != nil {
panic(err)
}
body, _ := ioutil.ReadAll(res.Body)
var studentInfo Root
xml.Unmarshal(body, &studentInfo)
년도 학기 찾기도 위와 똑같다.
findYYtmgbnXML := []byte(`body에 들어갈 xml`)
req, err = http.NewRequest("POST", "학교 api", bytes.NewBuffer(findYYtmgbnXML))
if err != nil {
panic(err)
}
res, err = client.Do(req)
if err != nil {
panic(err)
}
body, _ = ioutil.ReadAll(res.Body)
var yytmGbnInfo Root
xml.Unmarshal(body, &yytmGbnInfo)
이제 위 정보들을 바탕으로 학생 번호를 찾아야한다.
findLiveStuNoXML := []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns="http://www.nexacroplatform.com/platform/dataset">
<Parameters>
<Parameter id="_ga">GA1.3.1065330987.1626699518</Parameter>
<Parameter id="requestTimeStr">1626877490927</Parameter>
</Parameters>
<Dataset id="DS_COND">
<ColumnInfo>
<Column id="yy" type="STRING" size="256" />
<Column id="tmGbn" type="STRING" size="256" />
<Column id="schregNo" type="STRING" size="256" />
<Column id="stdKorNm" type="STRING" size="256" />
<Column id="outStayStGbn" type="STRING" size="256" />
</ColumnInfo>
<Rows>
<Row type="update">
<Col id="yy">%s</Col>
<Col id="tmGbn">%s</Col>
<Col id="schregNo">%s</Col>
<Col id="stdKorNm">%s</Col>
<OrgRow>
</OrgRow>
</Row>
</Rows>
</Dataset>
</Root>`,
yytmGbnInfo.Dataset.Rows.Row.Col[0].Data,
yytmGbnInfo.Dataset.Rows.Row.Col[1].Data,
studentInfo.Dataset.Rows.Row.Col[1].Data,
studentInfo.Dataset.Rows.Row.Col[0].Data))
req, err = http.NewRequest("POST", "학교 api", bytes.NewBuffer(findLiveStuNoXML))
if err != nil {
panic(err)
}
res, err = client.Do(req)
if err != nil {
panic(err)
}
body, _ = ioutil.ReadAll(res.Body)
var liveStuNo Root
xml.Unmarshal(body, &liveStuNo)
이번에는 xml을 생략하지 않았는데, sprintf를 사용하여 xml을 만들었다는 것을 보여주기 위해서 하지 않았다.
역시 정보를 받아와서 struct에 저장해준다.
이제 외박 신청만 남았다. 먼저 더미데이터를 준비해주고
dateList := []string{"20220715", "20220716", "20220717", "20220718", "20220719",
"20220720", "20220721", "20220722","20220723", "20220724", "20220725", "20220726",
"20220727", "20220728", "20220729", "20220730", "20220731","20220801", "20220802",
"20220803", "20220804", "20220805", "20220806", "20220807", "20220808", "20220809",
"20220810", "20220811"}
isWeekend := []int{0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0}
outStayAplyDt := "20220715"
다음 코드를 작성한다.
var wg sync.WaitGroup
var outStayGbn string
wg.Add(len(dateList))
for i := 0; i < len(dateList); i++ {
if isWeekend[i] == 0 {
outStayGbn = "07"
} else {
outStayGbn = "04"
}
go send(
yytmGbnInfo.Dataset.Rows.Row.Col[0].Data,
yytmGbnInfo.Dataset.Rows.Row.Col[1].Data,
liveStuNo.Dataset.Rows.Row.Col[12].Data,
outStayGbn,
dateList[i],
dateList[i],
outStayAplyDt,
&wg,
client,
)
}
wg.Wait()
fmt.Println("Done")
} // func main 끝
sync.WaitGroup을 이용해 대기해야할 고루틴 개수를 dateList의 길이만큼 추가해준다. 이후 send 함수로 고루틴을 이용한다. main은 고루틴이 끝날 때까지 대기한다.(wg.Wait())
send 함수는 다음과 같다.
func send(yy, tmGbn, livstuNo, outStayGbn, outStayFrDt, outStayToDt, outStayAplyDt string,
wg *sync.WaitGroup, client *http.Client) {
sendStayOutXML := []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns="http://www.nexacroplatform.com/platform/dataset">
<Parameters>
<Parameter id="_ga">GA1.3.1065330987.1626699518</Parameter>
<Parameter id="requestTimeStr">1626795331154</Parameter>
</Parameters>
<Dataset id="DS_DORM120">
<ColumnInfo>
<Column id="chk" type="STRING" size="256" />
<Column id="yy" type="STRING" size="256" />
<Column id="tmGbn" type="STRING" size="256" />
<Column id="livstuNo" type="STRING" size="256" />
<Column id="outStaySeq" type="STRING" size="256" />
<Column id="outStayGbn" type="STRING" size="256" />
<Column id="outStayFrDt" type="STRING" size="256" />
<Column id="outStayToDt" type="STRING" size="256" />
<Column id="outStayStGbn" type="STRING" size="256" />
<Column id="outStayStNm" type="STRING" size="256" />
<Column id="outStayAplyDt" type="STRING" size="256" />
<Column id="outStayReplyCtnt" type="STRING" size="256" />
<Column id="schregNo" type="STRING" size="256" />
<Column id="hldyYn" type="STRING" size="256" />
<Column id="resprHldyYn" type="STRING" size="256" />
</ColumnInfo>
<Rows>
<Row type="insert">
<Col id="yy">%s</Col>
<Col id="tmGbn">%s</Col>
<Col id="livstuNo">%s</Col>
<Col id="outStayGbn">%s</Col>
<Col id="outStayFrDt">%s</Col>
<Col id="outStayToDt">%s</Col>
<Col id="outStayStGbn">1</Col>
<Col id="outStayStNm">미승인</Col>
<Col id="outStayAplyDt">%s</Col>
</Row>
</Rows>
</Dataset>
</Root>`,
yy,
tmGbn,
livstuNo,
outStayGbn,
outStayFrDt,
outStayToDt,
outStayAplyDt,
))
req, err := http.NewRequest("POST", "학교 api", bytes.NewBuffer(sendStayOutXML))
if err != nil {
panic(err)
}
res, err := client.Do(req)
if err != nil {
panic(err)
}
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
wg.Done()
}
각 함수에서 xml을 새로 만든 다음 학교 api로 외박 신청 request를 보낸다. 그 후에 wg.Done()을 통해 고루틴이 종료됐음을 알린다.
내 생각엔 아주 만족스러운 결과를 보여줄 것으로 보였다. request를 보낸 후 받은 body를 출력하게 해놨는데 그 속도가 아주 빠르게 올라갔기 때문이다. 로그인을 빼면 대략 1초만에 Done이 출력됐다.
그러나...
내 기대와는 다르게 모든 외박신청이 들어오지 않았다. 7월 15일 ~ 8월 11일까지 외박 신청을 했으므로 총 28개가 전부 들어와 있어야 정상이었다.
혹시 몰라 고루틴을 제거하고 반복문만으로 요청을 보내게 했다. 물론 속도는 아주 느렸다..
결과는 전부 잘 들어왔다.
그래서 원인이 뭔지 찾아보기 위해 출력된 body를 살펴보았다. 그러자 다음과 같은 정상적인 응답과 에러가 번갈아가면서 나타나고 있었다.
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns="http://www.nexacro.com/platform/dataset" ver="5000">
<Parameters>
<Parameter id="ErrorCode" type="int">0</Parameter>
</Parameters>
<Dataset id="DS_SAVE_RESULT">
<ColumnInfo>
<Column id="outStayCnt" type="bigdecimal" size="16"/>
<Column id="freeOutStayYn" type="string" size="32"/>
</ColumnInfo>
<Rows>
<Row>
<Col id="outStayCnt">10</Col>
<Col id="freeOutStayYn">0</Col>
</Row>
</Rows>
</Dataset>
</Root>
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns="http://www.nexacro.com/platform/dataset" ver="5000">
<Parameters>
<Parameter id="ErrorCode" type="int">-1</Parameter>
<Parameter id="ErrorMsg" type="string">unique constraint violated</Parameter>
</Parameters>
</Root>
unique constraint violated 라는 에러였는데, 검색해보니 오라클 DB에서 같은 unique key를 가지고 컬럼을 추가하려고 할 때 나는 무결성 제약 조건 에러라고 한다.
아마 너무 빠르게 post 요청을 보낸 나머지 동시성 문제가 생긴 것 같다. 정상 응답에서 보이는 outStayCnt가 그 unique key인 것으로 추측되는데...
즉 (outStayCnt 10 20220715), (outStayCnt 11 20220716)... 이런식으로 순서대로 저장되어야 하는데, 너무 빠르게 요청되어서 하나의 요청이 완료되어 outStayCnt 값이 11로 증가하기도 전에 연속으로 요청이 들어온게 문제로 보인다.
한마디로 (outStayCnt 10 20220715)와 (outStayCnt 10 20220716)가 동시에 요청이 들어오고, 그 중에 먼저 하나의 요청이 완료되면 나머지 하나는 unique key 조건 때문에 반영되지 못하므로 이런 오류가 생긴 것 같다.
따라서 이번 작업은 실패했다... DB의 문제는 아닌 것 같고 아마 학교에서 외주 준 API 로직이 문제인 것 같다. DB에서 unique key의 index가 자동으로 증가하게 해놓았다면 딱히 이런일은 안생겼을 것 같다. DB가 알아서 들어온 요청을 하나씩 처리할 것이기 때문에...
내 생각이 틀렸을 수도 있다 ㅠㅠ 아무튼 좋은 경험이긴 했다. 동시성 문제는 어렵다는게 느껴졌다. 그리고 golang을 연습해볼 수 있어서 그재밌었다.
풀 코드는 여기 있다.