
식당에 대한 정보를 수집하기 위해 공공데이터를 이용하는 과정 중 발생한 문제점 및 해결방법을 기록하고자 글을 작성하였다.
우리가 필요한 정보는 전국의 식당 정보(이름, 도로명 주소, 위경도, 전화번호)였다. 그 중에서도 식당을 지도상에서 보여주기 위해 이름과 위경도는 필수적이었다.
전체 과정은 다음과 같은 순서로 진행하였다.
우선 데이터를 수집한 과정부터 살펴보자.
https://www.data.go.kr/data/15045016/fileData.do
식당에 대한 정보는 공공데이터 포털에서 쉽게 구할 수 있었다.
위 링크에서 csv 파일로 다운 받을 수 있다.
백만개가 넘는 데이터가 들어있어 용량이 크고 로딩도 오래 걸린다.
그래서 우선 이 csv 파일을 분할을 해야될 것 같다.
해당 내용은 이전에 작성한 블로그 글을 참고해주세요.
https://velog.io/@ch0jm/atyvb51f
데이터를 불러오고 나서 원하는 데이터만 추출하고 가공하여서 DB에 저장해야 한다.
우선 데이터중 폐업인 가게와 위치 정보가 등록되지 않은 가게는 제외시키자
public void readCSV(String name) {
try {
String filePath = new String(FILE_PATH.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
File file = new File(filePath + name + ".csv");
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "EUC-KR"));
String line;
while ((line = br.readLine()) != null) {
List<String> aLine;
String[] lineArr = line.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)", -1);
aLine = Arrays.asList(lineArr);
// 폐업했거나 위치정보가 없으면 패스
if (!checkState(aLine)) {
continue;
}
// 데이터 저장
StoreDto store = StoreDto.builder()
.id(Long.parseLong(aLine.get(0)))
.location(new Location(aLine.get(26),aLine.get(27)))
.address(aLine.get(19))
.name(aLine.get(21))
.build();
storeService.save(store);
}
}
public boolean checkState(List<String> aLine) {
if (!aLine.get(10).equals("영업") || aLine.get(26).equals("") || aLine.get(27).equals("")) {
return false;
}
return true;
}
다음과 같이 checkState()를 통해 영업중인 가게와 위치정보를 가지고 있는 데이터가 아니면 건너뛰게 하였다.
그리고 데이터를 저장해주었는데 데이터를 저장하는 과정을 살펴보자
나는 JPA @Entity와 Elasticsearch @Document를 통해 하나의 엔티티로 RDB와 Elasticsearch에 전부 저장되도록 하였다.
@Document 를 통해 Elasticsearch에 저장될 index 를 명시해준다.
(Elasticsearch 연동 관련 설정은 이전글 참고)
Store Entity
@Document(indexName = "stores")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@Entity
public class Store {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
@Embedded
private Location location;
public static Store from(StoreDto storeDto) {
return Store.builder()
.name(storeDto.getName())
.location(storeDto.getLocation())
.address(storeDto.getAddress())
.build();
}
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Embeddable
public class Location {
@Column(nullable = false)
double lat;
@Column(nullable = false)
double lon;
}
ElasticStoreRepository
public interface ElasticStoreRepository extends ElasticsearchRepository<Store,Long> {
}
JPA 와 사용법은 비슷하다. ElasticRepository를 상속하여 Repository 클래스를 만든다.
StoreService
@Slf4j
@RequiredArgsConstructor
@Service
public class StoreService {
@Transactional
public void save(StoreDto storeDto) {
Store savedStore = storeRepository.save(Store.from(storeDto));
elasticStoreRepository.save(savedStore);
}
RDB에 저장하고 Elasticsearch에 저장 하였다.
데이터가 잘 저장되었고 일반적인 데이터를 저장하면 여기서 끝일 것 이다.
하지만 공공데이터에서 제공하는 좌표값이 변환이 필요했다.
공공데이터에서 제공하는 위경도를 지도에 찍어보니 완전히 다른 위치가 찍혔다.
공공데이터는 식당의 위경도를 TM 좌표값 방식으로 제공하고 있었고 이걸 이용하기 위해서는 WGS84 방식으로 변환해야했다.
💁♂️ TM 방식이랑 WGS84 방식이 뭔가요?
WGS84 : 위경도
TM(Transverse Mercator) : 직교좌표계 (지역마다 다 다름, 공공데이터에서는 중부원점TM 사용)
이거 때문에 지도의 좌표계까지 찾아가며 공부했는데 이 글을 보고 계신 분들은 그냥 그렇구나 하고 넘어 가시는걸 추천합니다
구글 스프레드 시트에서 확장 프로그램을 제공하여 주소를 좌표계로 변환 할 수 있다.
예를 들어 이런식으로 변환된다. 서울시 강남구 역삼로 12길 1 -> (37.05,127.63)
하지만 너무 느리기 때문에 몇십만개를 하려면 수일이 걸릴 듯 싶다.
데이터가 많지 않다면 이것도 나쁘지 않은 것 같다.
지오코딩주소-좌표-변환
https://developers.kakao.com/docs/latest/ko/local/dev-guide#trans-coord
다행히도 Kakao에서 API로 제공하고 있는 것 중 좌표계 변환 API 가 있다.
(1일 30만회 까지 무료로 제공된다)


해당 API 를 사용하여 TM 좌표를 WGS84 로 변환하여보자.

잘 받아오는 것을 볼 수 있고 이 x,y 값은 위,경도 값이다.
그러면 이제 수십만개의 데이터에 대해서 이 작업을 진행해야한다..
이제 데이터를 저장하기 전 다음과 같은 과정을 거쳐야 한다.
csv 에서 식당정보 추출 -> 영업중인 가게에 대해서 좌표 변환 -> 데이터 저장
위에서 구현한 부분을 Feign Client를 이용해 Kakao API 요청을 하여 좌표를 변환해서 저장하는 방식으로 변경해보자
다음은 구현한 클래스이다.
Feign 사용법은 이전에 작성한 글 참고 😃
https://velog.io/@ch0jm/Spring-feign
FeignConfig
@Configuration
@EnableFeignClients(clients ={ KakaoFeignClient.class})
public class FeignConfig {
@Value(value = "${kakao.apiKey}")
private String API_KEY;
@Value(value = "${kakao.prefix}")
private String PREFIX;
@Bean
Level feignLoggerLevel() {
return Level.ALL; // log레벨 설정
}
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("Content-Type", "application/json");
requestTemplate.header("Accept", "application/json");
requestTemplate.header("Authorization",PREFIX + API_KEY);
};
}
}
FeignService
@Service
@RequiredArgsConstructor
public class FeignService {
private final KakaoFeignClient kakaoFeignClient;
public CoordinateDto getCoordinate( double x, double y) {
return kakaoFeignClient.getCoordinate(x, y, "TM", "WGS84");
}
}
FeignClient
@Component
@FeignClient(name = "${feign.kakao.name}", url = "${feign.kakao.url}", configuration = FeignConfig.class)
public interface KakaoFeignClient {
@GetMapping("/geo/transcoord.json")
CoordinateDto getCoordinate(@RequestParam double x, @RequestParam double y, @RequestParam String input_coord,
@RequestParam String output_coord);
}
CoordinateDto
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class CoordinateDto {
private Meta meta;
private Document[] documents;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public static class Meta {
int total_count;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
public static class Document {
double x;
double y;
}
}
application.yml
kakao:
apiKey: "발급받은 API 키"
prefix: "KakaoAK "
feign:
kakao:
name: "KakaoFeign"
url: "https://dapi.kakao.com/v2/local"
다음과 같이 클래스를 만들고 CSV를 읽는 클래스를 수정해보자.
public void readCSV(String name) {
try {
String filePath = new String(FILE_PATH.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
File file = new File(filePath + name + ".csv");
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "EUC-KR"));
String line;
while ((line = br.readLine()) != null) {
List<String> aLine;
String[] lineArr = line.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)", -1);
aLine = Arrays.asList(lineArr);
if (!checkState(aLine)) {
continue;
}
// 추출한 TM 좌표계 기반 좌표 값을 위 경도로 변경하는 작업 추가
CoordinateDto coordinate = feignService.getCoordinate(Double.parseDouble(aLine.get(26)),
Double.parseDouble(aLine.get(27)));
StoreDto store = StoreDto.builder()
.id(Long.parseLong(aLine.get(0)))
.location(new Location(coordinate.getDocuments()[0].getY(), coordinate.getDocuments()[0].getX()))
.address(aLine.get(19))
.name(aLine.get(21))
.build();
storeService.save(store);
}
br.close();
} catch (Exception e) {
log.error(e.getMessage());
}
}
추출한 TM 좌표계 기반 좌표 값을 위 경도로 변경하는 작업 추가 되었다.
이렇게 하여 저장하면 끝이다!