요즘 학교에 다니면서 학교 공부에 팀 프로젝트를 병행하고 있어서 글을 올리는게 뜸해졌습니다ㅠ
학교 공부, 팀 프로젝트, CS 공부 3개만 반복적으로 하고있는 것 같습니다... 4학년 되서는 널널하겠죠 뭐...
오늘은 공공데이터포털에서 제공하는 오픈 API를 활용하여 버스 시간을 알려주는 어플처럼 버스 시간 알림 로직을 만들어보았습니다.
팀프로젝트에 사용하게 될 로직이다보니까 어떻게 구현은 해야할 것 같은데, 공공데이터를 어떻게 받아와야하고, 어떤식으로 처리해야할지를 몰라서 막막하였습니다.
하지만, 막상 공공데이터포털에 들어가니 개발자분들을 위해서 어떻게 사용해야할지 적어주셔서 사용하기 매우 수월하였습니다.
일단 먼저 공공API를 활용하기 위해서는 공공데이터포털에 회원하가입하여 로그인을 진행해줍니다.
로그인을 진행하였다면, 버스 API 로직을 구현하기 위하여 몇가치 공공 API 사용 승인을 받아야합니다.
저는 3가지를 승인받아서 사용하였는데,
다음과 같이, 저희는 범위를 경기도 용인시로 한정지을 예정이라 경기도와 용인시 위주로 승인을 받았습니다.
신청을하면 승인이 바로바로 나는 것 같습니다.
승인받은 페이지에 들어가면 사용자를 위한 승인키가 주어질 것이고, API에 대한 상세 설명 페이지에 들어가면 다음과 같이 어떻게 코드를 작성해야 공공 데이터를 받아올 수 있는지에 대해서 설명해줍니다.
따라서, 저는 위와 같은 코드를 바탕으로 버스 도착 시간 확인 로직을 구현하였습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class BusController {
private final BusService busService;
@GetMapping("/bus")
@ResponseStatus(HttpStatus.OK)
public Response stationList(@RequestBody BusStationRequestDto requestDto) throws IOException {
return Response.success(busService.getBusStationList(requestDto));
}
@GetMapping("/bus/{stationId}")
@ResponseStatus(HttpStatus.OK)
public Response arriveBusList(@PathVariable String stationId) throws IOException {
return Response.success(busService.getBusArriveTimeList(stationId));
}
@GetMapping("/bus/{stationId}/{busNumber}")
@ResponseStatus(HttpStatus.OK)
public Response getBusRoute(@PathVariable String stationId, @PathVariable String busNumber) throws IOException {
return Response.success(busService.getBusRoute(stationId, busNumber));
}
}
일단 Controller를 통하여 URL을 3개로만 나누었는데,
버스 정류장 검색, 버스 정류장에 도착 예정 버스 확인, 특정 버스 노선 확인 이렇게 3가지로 나누었습니다.
@Service
public class BusService {
private String busStationAuthKey;
private String busArriveAuthKey;
private String busRouteAuthKey;
public BusService(@Value("${authKey.busArriveInfo}") String arriveKey,
@Value("${authKey.busStationInfo}") String stationKey,
@Value("${authKey.busRouteInfo}") String routeKey) {
busStationAuthKey = stationKey;
busArriveAuthKey = arriveKey;
busRouteAuthKey = routeKey;
}
@Transactional(readOnly = true) // 해당 정류장에 도착하는 버스들과 도착 시간들 반환
public List<BusArriveResultDto> getBusArriveTimeList(String stationId) throws IOException {
List<BusArriveInfoDto> lst = getArriveTime(stationId);
List<BusArriveResultDto> result = new LinkedList<>();
for(BusArriveInfoDto s : lst) {
BusArriveResultDto component = new BusArriveResultDto().toDto(s);
component.setBusNumber(getBusNumber(component.getBusNumber()));
result.add(component);
}
return result;
}
@Transactional(readOnly = true) // 해당 버스의 노선 반환
public List<String> getBusRoute(String stationId, String busNumber) throws IOException {
String routeId = getBusRouteId(busNumber);
return getBusRoute(routeId).stream().map(s->s.getStationName()).collect(Collectors.toList());
}
@Transactional(readOnly = true) // 정류장 검색 로직
public List<BusStationSearchResultDto> getBusStationList(BusStationRequestDto requestDto) throws IOException{
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/4050000/busstop/getBusstop");
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=" + busStationAuthKey);
urlBuilder.append("&" + URLEncoder.encode("pageNo","UTF-8") + "=" + URLEncoder.encode("1", "UTF-8"));
urlBuilder.append("&" + URLEncoder.encode("numOfRows","UTF-8") + "=" + URLEncoder.encode("100", "UTF-8"));
urlBuilder.append("&" + URLEncoder.encode("stop_nm","UTF-8") + "=" + URLEncoder.encode(requestDto.getStop_nm(), "UTF-8"));
urlBuilder.append("&" + URLEncoder.encode("gu","UTF-8") + "=" + URLEncoder.encode("", "UTF-8"));
urlBuilder.append("&" + URLEncoder.encode("stty_emd_nm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8"));
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
int statusCode = conn.getResponseCode();
BufferedReader rd;
if(statusCode >= 200 && statusCode <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = rd.readLine();
ObjectMapper objectMapper = new ObjectMapper();
BusStationResponseDto responseDto = objectMapper.readValue(line, BusStationResponseDto.class);
rd.close();
conn.disconnect();
if(responseDto.getResultCode() == null || Integer.parseInt(responseDto.getResultCode()) != 0) {
throw new StationNotFoundException(requestDto.getStop_nm());
}
return responseDto.getItems();
}
@Transactional(readOnly = true) // 버스의 routeId를 통하여 버스의 번호값 반환
public String getBusNumber(String routeId) throws IOException {
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/6410000/busrouteservice/getBusRouteInfoItem");
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=" + busRouteAuthKey);
urlBuilder.append("&" + URLEncoder.encode("routeId","UTF-8") + "=" + URLEncoder.encode(routeId, "UTF-8"));
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
BufferedReader rd;
if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = rd.readLine();
JSONObject rawData = XML.toJSONObject(line);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
JSONObject rmResponse = new JSONObject(rawData.get("response").toString());
JSONObject header = new JSONObject(rmResponse.get("msgHeader").toString());
if(!header.get("resultCode").toString().equals("0")) {
throw new WrongBusInfoException();
}
JSONObject rmMsgBody = new JSONObject(rmResponse.get("msgBody").toString());
JSONObject rmBusRouteInfoItem = new JSONObject(rmMsgBody.get("busRouteInfoItem").toString());
rd.close();
conn.disconnect();
return rmBusRouteInfoItem.get("routeName").toString();
}
@Transactional(readOnly = true) // 버스 번호를 입력할 경우 버스의 routeID를 반환
public String getBusRouteId(String routeName) throws IOException{
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/6410000/busrouteservice/getAreaBusRouteList"); /*URL*/
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=" + busRouteAuthKey); /*Service Key*/
urlBuilder.append("&" + URLEncoder.encode("areaId","UTF-8") + "=" + URLEncoder.encode("23", "UTF-8")); /*운행지역 ID (세부내용 별첨 참조)*/
urlBuilder.append("&" + URLEncoder.encode("keyword","UTF-8") + "=" + URLEncoder.encode(routeName, "UTF-8")); /*노선번호*/
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
BufferedReader rd;
if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = rd.readLine();
JSONObject rawData = XML.toJSONObject(line);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
JSONObject rmResponse = new JSONObject(rawData.get("response").toString());
JSONObject header = new JSONObject(rmResponse.get("msgHeader").toString());
if(!header.get("resultCode").toString().equals("0")) {
throw new WrongBusInfoException();
}
JSONObject rmMsgBody = new JSONObject(rmResponse.get("msgBody").toString());
JSONObject rmBusRouteList = new JSONObject(rmMsgBody.get("busRouteList").toString());
rd.close();
conn.disconnect();
return rmBusRouteList.get("routeId").toString();
}
@Transactional // 해당 버스의 노선 정보 확인
public List<BusRouteSearchResultDto> getBusRoute(String routeId) throws IOException {
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/6410000/busrouteservice/getBusRouteStationList");
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=" + busRouteAuthKey);
urlBuilder.append("&" + URLEncoder.encode("routeId","UTF-8") + "=" + URLEncoder.encode(routeId, "UTF-8"));
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
BufferedReader rd;
if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = rd.readLine();
JSONObject rawData = XML.toJSONObject(line);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
JSONObject rmResponse = new JSONObject(rawData.get("response").toString());
JSONObject header = new JSONObject(rmResponse.get("msgHeader").toString());
if(!header.get("resultCode").toString().equals("0")) {
throw new WrongBusInfoException();
}
JSONObject rmMsgBody = new JSONObject(rmResponse.get("msgBody").toString());
BusRouteListDto lst = objectMapper.readValue(rmMsgBody.toString(), BusRouteListDto.class);
rd.close();
conn.disconnect();
return lst.getBusRouteStationList();
}
@Transactional(readOnly = true) // 해당 정류장에 도착할 예정인 버스들을 조회,
public List<BusArriveInfoDto> getArriveTime(String stationId) throws IOException {
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/6410000/busarrivalservice/getBusArrivalList");
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=" + busArriveAuthKey );
urlBuilder.append("&" + URLEncoder.encode("stationId","UTF-8") + "=" + URLEncoder.encode(stationId, "UTF-8"));
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
BufferedReader rd;
if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = rd.readLine();
JSONObject rawData = XML.toJSONObject(line);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
JSONObject rmResponse = new JSONObject(rawData.get("response").toString());
JSONObject header = new JSONObject(rmResponse.get("msgHeader").toString());
if(!header.get("resultCode").toString().equals("0")) {
throw new EmptyBusStationException();
}
JSONObject rmMsgBody = new JSONObject(rmResponse.get("msgBody").toString());
BusArriveResultListDto resultDto = objectMapper.readValue(rmMsgBody.toString(), BusArriveResultListDto.class);
rd.close();
conn.disconnect();
return resultDto.getBusArrivalList();
}
}
Service는 다음과 같이 구현하였는데, 아무래도 공공 데이터를 받아오는 경험이 처음이고, 받아오는 데이터들도 XML인 경우가 있어서 파싱하는데에 좀 어려움을 겪었습니다ㅠ
뭐 이렇게 버스 시간 알림 로직 구현은 했지만, 개인적으로 조금 아쉬운 부분이 많은게 일단 XML을 JSON으로 파싱하는 과정에서 개인적으로 좀 아쉽다고 느꼈습니다.
추후에 찾아는 보겠지만, 하나하나 키값을 통하여 파싱하다보니 반복되는 코드가 계속적으로 발생하여 좀 난잡한 느낌이 들었습니다.
또 JSON 객체를 받아오는 부분에서도 좀 아쉬웠던 점이 있었는데, value값이 List인 경우가 있어서 List를 받기 위한 객체 1개, 그리고 List의 원소에 해당하는 객체 1개.
즉 2개의 객체를 생성하여야 value값이 List인 JSON을 받을 수 있어서 이 부분이 좀 아쉬웠습니다.