힐링포유 프로젝트 를 진행하면서 마주친 문제들 혹은 어렵게 해결한 것들 중 기억에 남는 것을 기록했습니다.
목차
- Flask로 구동 중인 딥러닝 서버와 node.js로 구동중인 웹서버 간의 통신 (실시간 서비스를 위한 통신 방법)
- Flask로 구동 중인 딥러닝 서버에서 쿼리문 수행 에러
Flask로 구동 중인 딥러닝서버(aws 우분투)와 node.js로 구동중인 웹서버(aws) 간의 통신
저는 node.js로 구현한 웹 서버와 flask 프레임워크로 구현한 딥러닝 서버 간 통신을 구현할 때 어려움을 겪었습니다.
최종 흐름은 다음과 같습니다.
[고민 상황]
사용자가 카메라앞에 인식이 되면 딥러닝 모델을 통해 사용자의 감정값을 분류하고 이 값을 딥러닝 서버가 웹 서버에게 전달하고 DB에 저장하는 방식으로 진행됩니다.
즉, 사용자가 인식되면 인공지능 서버가 사용자의 감정 상태를 분석합니다.
해당 감정값이 도출되는 시점에 웹 서버에게 값이 전달되고, 웹 서버는 데이터를 가공하여 프론트단에 전달하면 화면이 전환되는 상황입니다.
여기서 고민해야했던 것은
1. 모델값이 도출된 시점에 어떻게 웹서버에게 값을 전달하도록 해야하는가
2. node.js 서버는 값을 전달받았을 때 이벤트를 바로 발생시켜야 하는 상황에서, 어느 서버에서 요청을 하도록 해야하는게 맞는가
이었습니다.
즉, 서버에서 클라이언트로 임의 시점에 메시지를 보내는 것에 적합한 통신 방법이 필요했습니다.
우선 통신 방법이 무엇이 있는지 알아보고
소켓 통신, HTTP 통신의 차이점, 그리고 제가 제공하고자하는 서비스에서 어떤 방법이 가장 효율적일지 고민했습니다.
▼실시간 서비스를 위한 방법 ▼
소켓 통신
- 클라이언트와 서버 양쪽에서 서로에게 데이터 전달을 하는 방식의 양방향 통신
- 소켓 통신은 계속해서 Connection을 들고 있기 때문에 HTTP 통신에 비해 많은 리소스가 소모됩니다.
- 자주 데이터를 주고 받아야 하는 환경에서는 소켓 통신이 유리합니다.
- 웹브라우저에서 (TCP/IP socket 처럼) connection이 유지되어 실시간으로 데이터를 주고받을 수 있는 WebSocket 등장!
웹소켓 (WebSocket) 방식 :
- 웹소켓 방식을 사용하면 필자의 상황인 서버가 클라이언트에게 비동기(async) 메시지를 보내는 것이 가능합니다.
- 웹소켓은 방화벽이 있는 환경에서도 잘 동작하며, 서버와 클라이언트간 양방향 메시지 전송을 지원합니다.
- 클라이언트가 처음에 연결을 시작하고, (HTTP 연결) 핸드셰이크 절차를 한번 거치고 나면 웹소켓 연결로 업그레이드됩니다.
- 이때부터 서버는 클라이언트에게(반대도 가능) 비동기적으로 메시지를 전송할 수 있습니다. (양방향 전송 가능!)
- HTML5 WebSocket은 HTTP 실시간 통신 방식인 폴링, 롱폴링, 스트리밍 방식보다 월등한 차이를 보입니다.
HTTP 통신
- 클라이언트에서 서버로 요청을 보내고 서버가 클라이언트의 요청에 따른 결과를 반환하는 단방향 통신.
기존 양방향 통신 방법
웹소켓 방식 이전에는 HTTP 통신으로 마치 실시간인 것처럼 작동하게 하는 방법들이 있었습니다.
폴링(Polling) 방식 :
- 클라이언트가 일정한 주기(n초 간격)으로 서버로 HTTP 요청을 보내는 방식입니다.
- 클라이언트는 서버가 답할 메시지가 없다고 대답하면 연결을 종료하고, 다시 새 메시지를 보냅니다.
- header가 매우 무거운 HTTP 프로토콜이 계속해서 request를 날리는 것이기 때문에
서버의 부담이 증가하고, 또 답해줄 메시지가 없는 경우에는 서버 자원이 불필요하게 낭비된다는 문제가 있습니다.
롱 폴링(Long Polling) 방식 :
- 폴링의 비효율적인 문제로 인해 나온 기법입니다.
- 클라이언트가 주기적으로 HTTP 요청을 하는데, 서버는 해당 요청을 일정 시간 동안 대기 시킵니다. 만약, 대기 시간 안에 데이터가 업데이트되었다면, 그 즉시 클라이언트에게 응답을 보냅니다.
- 만약 타임아웃이 된다면 연결을 종료합니다.
- 클라이언트는 데이터를 전달받으면 연결을 종료하고 다시 서버로 재요청합니다.
- 데이터 업데이트가 빈번한 경우엔 폴링에 비해 성능상 이점이 크지 않습니다.
비동기로 HTTP 통신을 처리하는 문법 및 기법
✔️ 페이지 갱신 없이 필요한 데이터를 받아온다.
- Ajax 방식 : XMLHttpRequest를 사용, JQuery에서 쉽게 사용하도록 지원한다. 기본적으로 많이 사용된다.
- Axios : Promise API 기반, 동기처리를 위한 프로그래밍이 가능하다.
- fetch API보다 좋은 이유 : 응답 데이터 JSON으로 자동 변환, CSRF 보호 기능이 내장, 응답시간 초과 설정 기능
- async await : 비동기 함수에서 순서 보장이 필요한 로직을 간편하게 사용할 수 있다. 오류 디버깅을 위해 try catch 사용한다.
- promise : 이행단계마다 실행되는 함수(resolve, reject, then, catch)가 있다.그럼에도 Promise는 이해하기 어렵고, 예외 처리가 조금 번거롭기 때문에 ES8부터는 Async, await 지원하여 예외처리가 덜 번거롭다.
- fetch API : Web API의 비동기 네트워크 통신 함수로 XHR 대체제
[결정]
현재 상황(일정 시간동안 서버로부터 결과가 나오는 것을 기다리다가 결과가 나오자마자 서비스 화면을 넘겨야하는 상황)에서 폴링 방식으로 진행하는 것이 적절할 것이라 판단하였습니다. 그래서 클라이언트 단에서 타이머를 세팅하여(setInterval()) 일정한 주기로 HTTP 요청을 보내도록 하였습니다.
➕ 웹 서버에서 딥러닝 서버로의 요청에서는 request 라이브러리를 사용했습니다.
[최종 흐름 - 코드]
1. 웹 프론트엔드에서 setInterval()를 이용하여 6초마다 ajax 요청하는 함수를 호출하여 웹 백엔드에게 딥러닝 서버에게 요청하도록 요청한다. (폴링 방식)
2. 웹 서버에서 딥러닝 서버에 요청한다. (request 라이브러리)
3. 딥러닝 서버에서 요청에 대한 응답을 하면 웹 서버는 프론트엔드에게 success 응답과 함께 관련 데이터를 전달한다.
4. 웹 프론트엔드는 적절한 화면으로 전환하기 위해 응답으로 전달된 데이터와 함께 페이지 전환 api를 요청한다.
최종 흐름을 코드와 함께 봐봅시다!
1. 프론트엔드에서 setInterval()를 이용하여 6초마다 ajax 요청하는 함수를 호출한다. (폴링 방식)
[healing.ejs 코드]
<script>
timer = setInterval( function() {
deeplearningServerRequestApi();
}, 6000); //6초마다 감정값(from 딥러닝서버) 도출 여부 체크하는 요청 보내도록 메서드 호출
</script>
2. 프론트엔드에서 백엔드에게 딥러닝 서버에게 물어보도록 요청한다.
[healing.js 코드] 응답이 오면 success가 실행되어 해당 url로 이동하며, 에러 발생 시 다시 메인을 로드합니다.
function deeplearningServerRequestApi() {
$.ajax({
type: "GET",
url: '/dltest',
data: {},
error: function(xhr, status, error) {
if (status == 404) {
alert("서버 응답 실패");
window.location.href = "/";
}
},
success: function(response) {
window.location.href = `${response.ad_url}`;
// '/advertisement/' + user_id + '/' + ad_id
}
});
};
3. 백엔드 응답 시 로직 처리 (딥러닝 서버에게 요청)
request 라이브러리로 해당 딥러닝서버의 uri로 요청하면 딥러닝 서버에서 해당 요청을 받습니다.
[routes/index.js]
/*========== 딥러닝 서버로 api 요청=========*/
var request = require('request');
router.get("/dltest", function(req, res) {
console.log("GET요청 /dltest 호출됨");
//콜백 이 실행되면 그 값이 아래 DLTestResult의 {result} 에 저장된다.
const DLTestResult = (callback) => {
const options = {
method: 'GET',
uri: "http://ec2-3-129-8-135.us-east-2.compute.amazonaws.com:8888/test", //http://{aws ip주소}/test
}
// 위에 정의해논 uri에 request 라이브러리로 요청! request의 응답이 body로 오면 아래 콜백함수 호출
request(options, function (err, res, body) {
console.log("콜백 전 : "+ body);
callback(undefined, {
result: body
});
});
}
//콜백 실행. result는 딥러닝서버로부터 받은 값
DLTestResult((err, {result} = {}) => {
if (err) {
console.log("error!!!!");
res.send({
message: "fail",
status: "fail"
});
}else { //error 아니면
json = JSON.parse(result); //json으로 user_id와 emotion이 넘어왔으니 parsing
const user_id = json.user_id;
const ad_id = json.ad_id;
console.log("userID: " + user_id);
console.log("adID: " + ad_id);
const ad_url = '/advertisement/' + user_id + '/' + ad_id; // /advertisement/2/16
return res.send({
ad_url: ad_url
});
}
})
});
4. 딥러닝 서버에서 요청에 대해 응답한다.
웹서버로부터 요청이 들어오고 모델 수행시키는 함수로부터 사용자 감정값이 도출되면 해당 값을 DB users 테이블에 저장합니다.
저장하면서 생성된 사용자 ID(user_id)와,
감정값과 계절값으로 advertisements 테이블에서 조회한 광고ID(ad_id)를 웹 서버에게 응답데이터로 리턴합니다.
[server.py]
@app.route('/test', methods=['GET'])
def insert():
//... 생략
return jsonify({
'user_id': user_id,
'ad_id': ad_id
})
5. 그 후..
- 해당 값이 응답으로 오면 DLTestResult메서드에서 전달받은 result 값으로 url을 만든 후에 프론트엔드에게 응답합니다.(위 코드 routes/index.js 참고)
- index.js로부터 response를 받으면 데이터(response.ad_url)를 로드합니다.
아쉬운 점
- HTTP Request 라이브러리 중 현재 deprecated된 request 라이브러리를 사용한 점 (더 가벼우면서 Promise 기반인 superagent 라이브러리와 같은 것이 있음을 나중에 깨달았다.)
- 웹소켓을 이용해서도 해결할 수 있는 플로우를 당시에는 폴링 방식으로 진행한 점
- 참고글
기존 polling 방식의 배민 콜센터 주문접수(현재는 fade out)는 주문건이 증가함에 따라 DB Select가 급증하여 서비스가 위험했던 적이 있었기 때문에 BROS개발 당시 실시간성을 유지하면서 db select를 줄이기 위해서 필수로 실시간 이벤트 서버 도입을 해야 했습니다.
- 참고글
Flask로 구동 중인 딥러닝 서버에서 쿼리문 수행 에러
[상황]
딥러닝 서버(Flask)에서 mysql과 연동하여 데이터를 조회, 수정, 추가하는 3개의 쿼리문이 각각 함수를 호출하여 실행하는데, 두 번째 함수 호출(쿼리문 날릴) 시, 오류가 발생하였다. 그로인해 웹서버로의 값 전달이 제대로 이루어지지 않았다.

[문제]
하나의 공유되는 cursor를 생성하여 하나의 스레드만을 생성하였는데, 모든 쿼리문이 실행되기 전에 Connection을 닫은 점
[해결]
본인은 모든 함수마다 db.close()를 작성해준 상태였으므로
즉, 모든 쿼리문이 실행된 후에 connection을 닫아주도록 하여 해결하였다.기존에 각 쿼리문을 실행한 후에 db.close()를 이용해 connection을 닫아준 것을 닫지 않음으로써 해결할 수 있다.
마지막으로 쿼리문을 실행하는 selectUsers() 를 제외하고는 db.close()를 지워야한다.
def selectAD(self, query, data):
self.cursor.execute(query, data)
rows = self.cursor.fetchall()
adData = random.choice(rows)
adId = adData['ad_id']
print("db_config2에서 랜덤으로 출력: ", adData)
print("출력된 데이터의 id: ", adId )
self.db.commit()
# self.db.close()
return adId
def insertUsers(self, query, data):
self.cursor.execute(query, data)
self.cursor.fetchall()
print("db 업데이트 완료")
self.db.commit()
# self.db.close()
def selectUser(self, query):
self.cursor.execute(query)
row = self.cursor.fetchall()
print(row)
userId = row[0]['user_id']
print("출력된 데이터의 user id: ", userId )
self.db.commit()
self.db.close()
return userId
해결!
참고
https://valuefactory.tistory.com/263
https://bluayer.com/34
https://kotlinworld.com/75
https://stackoverflow.com/questions/55365543/pymysql-err-interfaceerror-0-error-when-doing-a-lot-of-pushes-to-sql-tabl
'Project' 카테고리의 다른 글
[2021 한이음 ICT 멘토링] 월별 기록 (0) | 2021.09.24 |
---|