전기자전거 API

공부는 혼자하는 거·2021년 11월 20일
0

업무

목록 보기
3/15

요구사항

전기자전거 charger 중앙 컨트롤러 API 서버를 만들어달라는 요청이 들어왔다. 기존에 이미 완성된 App Server가 있고, 기기들(charger)도 준비되어있다. 기기는 당장 마련하지 못하니 기기와 유사한 얘뮬레이터도 만들고 중앙 컨트롤러 api(Station)도 만들어서 작업했다. 대충 시연 시나리오는 다음과 같다.

1. 스테이션에서 1분 주기로 정보 전송 
 -. 도킹된 충전기 배터리 랜덤값 전송 (MobilityId 는 에물레이터에 입력된 값으로 전송)
 -. 도킹되지 않은 충전기는 고정값 전송 

2. 사용자 대여 요청 
 -. 스테이션에서 충전기 언락 (언락은 로그로 표현) 
 -. 사용자 도킹 해제 (에물레이터에서 도킹 해제 입력)
 -. 도킹 해제 정보 전송 

3. 사용자 반납 신청
 -. 자전거 도킹 -> 도킹정보 앱서버 전송 (도킹된 MobilityId 는 에물레이터에서 인자로 입력)
 -.-> 스테이션 반납
 -. 해당 충전기 락 (락은 로그로 표현)
 
4. 자전거 도킹 -> 2번 규격에 이미 포함

5. 자전거 도킹해제 -> 3번 규격에 이미 포함

사용자 역할은 telnet으로 대체하려고 했다가, 글자를 확인하지 못하는 이슈가 있어서, socketTest라는 jar 파일을 다운받아서 대체했다. 전체적인 구조를 대충 설명하자면 이런 그림이다.

개발 과정

  1. 먼저 station과 통신할 가상의 charger 애뮬레이터를 만들었다. 역시 telnet이나 socketTest 프로그램으로 사용자 역할을 대체하는 곳으로부터 데이터를 전송받아야 하기 때문에 연결할 socket은 2가지였다. 하나는 사용자로부터 소켓으로 데이터를 받을 서버소켓과, station에게 데이터를 전송할 clientsocket. 동시에 열어야 하니 비동기적으로 통신해야 했다.

  2. charger들과 station 간의 통신규격은 5가지 정도로 압축할 수 있었다. 먼저 station 서버가 켜져있다는 가정하에, charger가 켜지자마자 최초 자기 ID를 넘겨주는 INIT, 그리고 지속적 상태 업데이트 UPDATE, 사용자가 Docking이나 unDocking을 할 시 정보를 전송해주는 Docking, Station이 Charger에게 대여나 반납 요청 시, Return, Rental.

  3. 모든 통신규격은 서버 socket은 가만히 있고 client 쪽이 정보를 정의하는 쪽으로, 규격을 설계하였다. 그리고, APP Server 쪽은 지속적 전송으로 인해 chargerId를 이미 다 알고 있는 상황에서 station 쪽으로 chargerId를 전송해주면, station은 그 chargerId를 기준으로 각각 연결된 charger들 중 특정 charger들에게 정보를 전송해주는 식으로 짰다. 만들어진 테스트 시나리오는 다음과 같다.

  1. 내가 가짜로 만든 애뮬레이터 charger는 끝내고, 실제 기기(Charger)와 연동하면서는 프로토콜 방식이 달라졌다. 원래는 사용자가 docking 상태를 Json으로 전송해주는 방식으로 짰는데, 그쪽에서는 이미 정해진 프로토콜이 있다고 하더라.. 나는 못 들었는데.. 아무튼 그래서, 헥사코드를 byte로 16진수로 변환하여 다시 json으로 파싱하도록 프로토콜을 수정했다.

  2. 도킹 상태들도 기기에서 전송해주는 게 아니라, station에서 요청을 해야지만 받아올 수 있더라. 원래는 서버는 가만히 있고, 클라이언트 쪽에서 정보를 전송해줘야 되는데, 설계가 마음에 안 든다. 어쩔 수 없다. 스케줄러를 통해서 지속적 pooling을 통해 10초 주기로 charger에게 정보를 요청하고, station에게 그 정보를 전송해주었다.

바뀐 시나리오 그림은 대충 다음과 같다.

개발 난항

  1. 처음 내 로컬 서버에서 charger들을 여러대 구동시켜서 테스트를 하려고 했는데, 점유하고 있는 serversocketPort는 하나만 지정할 수 있어서 매번 yml 설정을 바꿔줘서 실행해줘야 되는 이슈가 있었다. (이 과정에서 devtool 설정이 켜져있으면 에러가 뜨니, scope을 runtime을 다른 범위로 대체하거나 꺼놓자.) 이 부분은 커멘드라인 인자로 port 번호를 넘겨주는 형식으로 바꿔서 해결할 수 있었다.

  2. socketChanel 에 대한 이해가 부족했다. 처음엔 socket 채널을 여는 포트는 하나뿐이니, 여러개의 클라이언트 socket이 하나의 server socket port에 접근할 수 있는지 부터가 의문이었는데, station에서 socketchannel을 accept 부분에서 멀티스레드로 통신을 하니 하나의 포트에 여러개의 스레드에 대한 자원을 배정해주는 걸 확인할 수 있었다.

  3. Bean의 순환참조에러도 새로 확인할 수 있었다. 서로 전역으로 등록한 Bean을 각자 불러일으키면(DI), 서로 계속 호출하면서 exception이 발생한다.. 이건 기본적인 사항인데 파악하지 못한 내가 잘못이다.

  4. 내가 만든 가상의 charger 애뮬레이터 대신 직접 기기 charger과 연동하는 작업을 진행했다. 아래의 기기인데, 대충 녹색부분은 charger들이고 중앙에 보이는 기기에 내가 만든 Station server를 심어놓아야 했다. 원래는 c#으로 만들다 만 프로젝트가 있는데, 거기 코드를 보면서 바뀐 프로토콜로 알아서 수정해줘야 했다.

  5. 암튼 중앙의 기기에 우분투 OS를 깔고, ssh로 접속해서 내가 만든 jar 를 파일을 복사붙여넣기 해서 실행했다. 우분투 OS를 깔아주는 과정은 대표님이 담당하여 맡으셨다.

  6. 이 charger들은 station server로부터 전송해주는 protocol이 달랐다. 대충 비트 단위 프로토콜인데, 형식을 보자면 다음과 같다.

A는 고정이고, IP 는 각각 charger들의 연결 IP, BT는 배터리 상태, SI 는 stationId, CI는 chargerId, MI는 모빌리티ID(전기자전거) D는 도킹 상태를 의미한다.

00 = lock = 2(해제) =  안눌러진상태 
01 = unlock =1(도킹) = 눌러진상태
  1. 이 charger들의 연결 IP가 다 다르다보니, station에서 8개의 스레드를 돌려, 각각 다른 IP의 소켓채널을 열어야 하는데, 이 과정에서 하나만 열고, 나머지 소켓채널은 pending 되는 현상이 발생했다. 알고보니 내가 serversocket 객체를 클래스 단위로 선언해서 생긴 문제였다. 메소드 안에서 선언함으로서 이 부분을 해결했다.

  2. 8개의 소켓을 연결했더니 프로그램이 CPU 자원을 과도하게 먹으면서 뻗어버리는 현상이 발생했다. 하나의 소켓당 무한루프로 실행되는데, 버퍼로부터 읽어오는 과정에서 버퍼의 값이 없다면, Thread.sleep(100)을 넣어줬더니 자원을 먹는 게 엄청나게 줄어들었다. 여러분들도 무한루프로 어떤 로직을 수행할때, Thread.sleep을 무조건 걸어주자.. 생각보다 자원을 먹는 양이 엄청나게 줄어들더라..

  3. 이렇게 Socket를 연결해놓고, charger에게 상태질의 패킷을 던졌을 때, 응답을 제대로 받아오는 것을 wireshark로 확인했다. 그런데 어떤 시점에서, 또 세션이 끊어지면서 다른 세션이 연결시도하면서 정상적으로 진행이 되지 않더라.. 이 부분을 파악하는데 시간이 오래 걸렸다.

  4. 알고보니 비트 단위로 받아온 mobilityId 를 String으로 변환 후, Integer로 파싱하는 과정에서, 16진수로 변환하지 않아서 파싱을 못해서 생긴 이슈였다. 이 에러를 try catch 로그로 감싸지 않고, 로그를 꼼꼼히 작성하지 않아서, connect refused 가 뜬 것이었다. 항상 꼼꼼하게 try catch로 예외처리를 구현하고, 그게 아니면 스레드 예외처리 핸들러를 따로 만들어서 로그를 확실하게 기록해두자.,.아무튼 아래와 같이 코드를 수정함으로서 해결했다.

.mobilityid(Integer.valueOf(mobilityId, 16))
  1. 이렇게 해서 드디어 성공하나 했더니, 또다시 concurrentModification 에러가 떴다. 응? 어딘가 threadsafe 하지 않는 구조가 있나 헸더니 역시나 charger로 부터 받은 정보들을 등록해서, App Server에게 다시 전달할때, RestTemplet을 사용했는데, 여기서 이슈가 발생했다. 항상 까먹지만 비동기적 흐름을 작성할때는 괜히 메모리 아끼자고, 객체를 전역이나 클래스 단위로 설계하지 말고, 전역으로 쓸거면 thread safe한 자료구조를 쓰던가 아니면, 비동기적 흐름 안에서 객체를 매번 new로 새로 만들어주자.

실제로는 응답메시지 자체는 station에서 바로 응답을 보내므로, 응답은 올 것이다. 하지만 실제 기기와 연동된 ip 주소의 기기로 메시지는 못 보냄.. 헷갈리지 말자..

profile
시간대비효율

0개의 댓글