ELK 도입기(4) - 공공데이터 크롤링 및 저장 feat 중부좌표계

Jongmyung Choi·2023년 11월 8일
post-thumbnail

개요

식당에 대한 정보를 수집하기 위해 공공데이터를 이용하는 과정 중 발생한 문제점 및 해결방법을 기록하고자 글을 작성하였다.

우리가 필요한 정보는 전국의 식당 정보(이름, 도로명 주소, 위경도, 전화번호)였다. 그 중에서도 식당을 지도상에서 보여주기 위해 이름과 위경도는 필수적이었다.

전체 과정은 다음과 같은 순서로 진행하였다.

  1. 데이터 수집
    공공데이터에서 전국 음식점 정보를 가지고 온 뒤 자바로 csv 파일을 크롤링 하였다.
  2. 데이터 가공 작업
    필요한 데이터를 추출 및 가공 하였다.
    현재 폐업하지 않은 가게만 추출, Kakao API를 이용한 좌표계 변환 과정을 거쳤다.
  3. 데이터 저장
    MySQL과 Elasticsearch에 해당 데이터들을 저장하였다.

데이터 수집

우선 데이터를 수집한 과정부터 살펴보자.

1. 공공데이터 수집

https://www.data.go.kr/data/15045016/fileData.do
식당에 대한 정보는 공공데이터 포털에서 쉽게 구할 수 있었다.
위 링크에서 csv 파일로 다운 받을 수 있다.
백만개가 넘는 데이터가 들어있어 용량이 크고 로딩도 오래 걸린다.
그래서 우선 이 csv 파일을 분할을 해야될 것 같다.

2. 파일 분할 및 Java로 CSV 파일 읽기

해당 내용은 이전에 작성한 블로그 글을 참고해주세요.
https://velog.io/@ch0jm/atyvb51f

3. 데이터 가공 및 저장

데이터를 불러오고 나서 원하는 데이터만 추출하고 가공하여서 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)
하지만 너무 느리기 때문에 몇십만개를 하려면 수일이 걸릴 듯 싶다.
데이터가 많지 않다면 이것도 나쁘지 않은 것 같다.
지오코딩주소-좌표-변환

Kakao API 를 이용한 좌표계 변환

https://developers.kakao.com/docs/latest/ko/local/dev-guide#trans-coord
다행히도 Kakao에서 API로 제공하고 있는 것 중 좌표계 변환 API 가 있다.
(1일 30만회 까지 무료로 제공된다)

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

잘 받아오는 것을 볼 수 있고 이 x,y 값은 위,경도 값이다.
그러면 이제 수십만개의 데이터에 대해서 이 작업을 진행해야한다..

Feign Client 를 활용하여 Kakao API 이용

이제 데이터를 저장하기 전 다음과 같은 과정을 거쳐야 한다.
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 좌표계 기반 좌표 값을 위 경도로 변경하는 작업 추가 되었다.

이렇게 하여 저장하면 끝이다!

profile
총명한 개발자

0개의 댓글