<차곡차곡>은 주차장 입장시 최적의 자리를 배정해줌으로써 주차 스트레스를 줄여주는 AIoT 프로젝트다.
학부생시절 IoT프로젝트를 진행한적은 있지만, 대개 IoT 시스템만 맡거나, 웹만 맡거나 했으므로 전체적인 구조에 대한 고민은 부족했었다. 그렇다보니 특히 이 프로젝트에서 아키텍처 구조상의, 통신문제를 많이 마주쳤던것 같다.
가장 크게 고민했던 문제는 총 두가지였다.
1. 키오스크 - 관리자화면 - IoT간의 실시간성을 어떻게 반영할것인가?
2. 포트포워딩이 불가능한 시연환경에서, 로컬서버인 IoT서버와 배포서버인 AWS서버가 통신하게 할것인가?
1. 키오스크 - 관리자화면 - IoT간의 실시간성을 어떻게 반영할것인가?
주차장 시스템이다 보니, 관리자페이지 - 키오스크 - IoT시스템간의 실시간성이 매우 중요했다. 이미 배정해준 자리를 또 배정해줘선 안되고, 관리자 입장에서도 실제 주차현황과 오차가 있으면 안되니까!
당시의 우리팀은 이러한 아키텍처 설계가 처음이어서 큰 혼란에 빠졌다. Vue.js를 활용해서 개발한 키오스크 화면과 관리자 화면은 클라이언트인데, 어떻게 자동으로 실시간성을 반영하는가?
즉, 한 차주가 키오스크를 통해 A1번 자리를 발급받았을 경우, 관리자와 IoT환경에 어떻게 자동 업데이트 해주는가?
또한 A1번 차주가 주차장에서 출차했을때, 입차중인 차주들에게 빈자리에 대한 정보를 어떻게 전달하는가?
이외에도 주차장이 만차이거나, 만차가 해소되었을 경우 등 동적으로 변화하는 환경에서 상태를 어떻게 반영하는가?
Vue.js와 같은 프론트 환경의 경우, 클라이언트기 때문에 서버에게 요청을 받지 못한다. 즉, 입/출차 감지 데이터를 알리기 위해 서버 -> 클라이언트로 요청을 보내는것은 부자연스럽다.
당시 내가 팀에서 개발 경험이 가장 많았기 때문에 이부분을 해결을 전적으로 도맡았다. 포인트는 서버->클라이언트로의 요청이 필요한 상황이므로, 해결법으로 'SSE'를 활용하는 방법을 떠올렸다.
즉, 위 그림과 같이 구조화 하는것이다. 서버가 요청된 차 번호를 바탕으로, 자리를 배정함과 동시에 해당 정보를 키오스크 및 관리자 페이지라는 Vue 클라이언트를 향해 SSE 갱신알림을 보낸다. 이를통해 키오스크를 사용하던 운전자에겐 배정된 자리값을, 관리자페이지엔 차량배정현황을 실시간 업데이트 하게된다.
이렇게 하면 클라이언트에서 요청하지 않아도 차량 배정 현황을 실시간으로 감시할 수 있게된다.
관련 코드를 간략히 설명하자면 다음과 같다. 이전에 트래블 캐리어를 개발할땐 하나의 이벤트만 만들었기 때문에 굉장히 단순했지만, 이번엔 다양한 이벤트에 대한 메소드를 작성해주었다.
/**
* 만차의 경우 차량 출차시 대기자를 위한 키오스크 요청
* @param keyValue
*/
public void congestionClear(String keyValue) {
SseEmitter sseEmitter = SseController.sseEmitters.get(keyValue);
try {
System.out.println("=> "+sseEmitter);
if(sseEmitter == null) throw new SseEmitterIsNullException(keyValue+" 연결이 존재하지 않음");
else{
sseEmitter.send(SseEmitter.event()
.name(String.valueOf(SseStatus.CONGESTION_CLEAR))
.data("CONGESTION_CLEAR"));
}
} catch (SseEmitterIsNullException e) {
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* AI서버로부터 추출된 차번호텍스트를 SSE를 통해 키오스크로 전송
* @param carNum
*/
public void validateCarnum(boolean validation, String carNum, String keyValue) {
SseEmitter sseEmitter = SseController.sseEmitters.get(keyValue);
try {
if(sseEmitter == null) throw new SseEmitterIsNullException(keyValue+" 연결이 존재하지 않음");
else{
if(validation) {
sseEmitter.send(SseEmitter.event()
.name(String.valueOf(SseStatus.VALID_CAR_NUM))
.data(carNum));
} else{ //적합하지 않으면 키오스크 화면에 재인식 코드 띄우기
sseEmitter.send(SseEmitter.event()
.name(String.valueOf(SseStatus.INVALID_CAR_NUM))
.data("INVALID_CAR_NUM"));
}
}
} catch (SseEmitterIsNullException e) {
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
이벤트명은 ENUM을 활용해서 통신할 수 있도록 규칙을 세웠다. sseEmitter가 없을경우를 대비해 에러핸들링도 해줬다.
자세한 백엔드 코드는 다음에서 볼 수 있다.
마찬가지로 백엔드에서도 위 이벤트들을 받아 업데이트를 실행하는 로직이 필요하다. 세워둔 규칙을 바탕으로 만들면 된다.
const sseEvent = new EventSource(adminUrl);
onMounted(() => {
//연결 리스너
sseEvent.addEventListener("open", function (e) {
//캐치할 에러코드를 써줌
console.log(e.data);
store.updateSSEStatus("connected");
});
//에러 리스너
sseEvent.addEventListener("error", function (e) {
console.log(e);
store.updateSSEStatus("disconnected");
});
//자동신고시스템 - 플로팅알림
sseEvent.addEventListener("SENSOR_REPORT", function (e) {
const data = JSON.parse(e.data);
store.sendNotification(data);
});
//공통바 업데이트
sseEvent.addEventListener("REALTIME_COMMON", function (e) {
store.updateBar();
store.updateVisitChart();
parkingSectionStore.update();
});
});
키오스크와 관리자화면의 각 View에 위와 같은 각자의 이벤트 리스너를 등록해주면 된다.
자세한 프론트 코드는 다음에서 볼 수 있다.
2. 포트포워딩이 불가능한 시연환경에서, 로컬서버인 IoT서버와 배포서버인 AWS서버가 통신하게 할것인가?
개발중 마주친 두번째 문제였다. 전체 센서를 관할하는 아두이노는 SSAFY 캠퍼스의 내부망을 사용하는 라즈베리 파이를 통해 제어하고있었고, 이 라즈베리파이에 어떠한 요청을 보내기 위해선 AWS에 배포된 장고서버를 이용해야했다.
개발중엔 장고를 로컬환경으로 사용중이었으므로 문제가 없었지만, 배포후엔 문제가 된것이였다. 이를 해결하기 위한 대표적 방법은 포트포워딩이 있었다. SSAFY망 자체에서 특정 포트를 열어두고 라즈베리파이에 연결하는것이다. 다만 기술프로님과 상담한 결과, 보안상 포트포워딩은 절대 불가하다는 답변을 받았다. 그래서 라즈베리파이에 접근이 불가능해진 이슈가 생긴것...
결국 시연환경 특성의 문제였고, 남은 개발기간도 많지 않아 고민이 많았다. 서비스 특성상 실제 서비스에선 같은 내부망을 사용할 것이므로 로컬로 시연해도 괜찮다~ 라는 조언도 받았지만, 뭔가 기술적으로 해결하고싶었다.
결론적으로, SSE를 공부하며 함께 공부했던 Polling방식을 떠올렸다. 로컬이기 때문에 요청을 받지 못하는 라즈베리파이에서, 일정 시간마다 배포서버인 장고서버에 요청을 반복한다. 쉽게말해 "차단바를 열까요?"라는 요청을 계속한다. 그러다가 특정 시점, 예를들어 관리자가 차단바를 해제해달라는 요청을 보낼 경우나, A10번을 배정받은 차량이 A10번 근처에 도착했을경우, 장고서버는 드디어 "열어달라"는 응답을 보낸다. 그럼 라즈베리파이는 해당 응답을 바탕으로, 차단바를 열게된다.
이로써 로컬서버와 배포환경간의 통신문제를 해결했다!
관련 IoT서버 코드는 다음에서 볼 수 있다.
참고사항으로, 전체프로젝트 아키텍처는 다음과 같이 설계되었다.
<차곡차곡>프로젝트는 도메인 선택 당시 한 팀원의 말, "IoT는 지금이 아니면 하기 힘들걸!"에 설득당해서 시작하게 된 프로젝트다. 시간이 지나고 보니 정말 맞는말이였다.. 학교, 부트캠프처럼 모두가 모여서 하는 프로젝트가 아닌 이상, 웹/앱 개발 프로젝트에 비해 진행이 힘들다.
이런 이유로도 의미가 있었지만, IoT환경이 아니었다면 고민해보지 못했을 트러블 슈팅들이 있어 더욱 좋은 프로젝트였다. 결국 모든 문제를 해결하고 정상작동하는 주차장을 보았을땐 굉장히 뿌듯...^-^ 기회가되면 IoT프로젝트도 더 해보고싶다.
'프로젝트 > 차곡차곡' 카테고리의 다른 글
chart.js 커스텀 하기(그라데이션, 가로선 생략 등) (4) | 2024.11.16 |
---|