[Co Labor] 스프링과 MySQL에 기업 데이터 추가하기 공개 데이터 포털 API 사용!

2024. 10. 14. 21:03프로젝트: Co Laobr

엔티티 수정

기업 엔티티

  • 추가 : address1(시/도), address2(구/군), address3(기타 주소), type(기업 분류), phone_number
  • 유지 : name, description, address
  • 수정 : enterprise_id(사업자 번호, string)
  • 삭제 : address

채용공고 엔티티

  • 삭제 : age, gender
  • 유지 : job_id, created_at, dead_date, modified_at, views, description, title, EnterpriseUser, Enterprise
  • 추가 : requirement : (string) (예시 4년제 대학 이상, 경력 2년 이상, js, java 사용 경험 등)

EnterpriseUser 엔티티

  • 수정 : enterprise_user_id(Long → String)

LaborUser 엔티티

  • 수정 : labor_user_id(Long → String, Not Generated)

엔티티 수정에 따른 DataPreparationService 및 관련된 클래스 수정


기업 데이터 받아오기

기업 데이터를 받아오고 나면 이제 서버를 켤 때마다 DB가 초기화되지 않도록 설정을 바꾸었다.

이에 따라 지원센터, 병원 데이터, 기업 데이터는 모두 컨트롤러를 통해 데이터를 받아와서 DB에 저장하게끔 구현하였다. fetch url로 들어가면 받아올 수 있는데 fetch 할 때 해당 테이블을 비우므로 중복으로 데이터가 들어갈 일은 없다.

해당 방식은 테이블을 비울 때 id값은 초기화되지 않고 그다음 번호부터 자동으로 매겨진다거나 기업 정보는 초기화하면 안 되는 문제가 있어서 조금 변경하였다.

지원 센터와 병원 데이터는 api에서 제공하는 아이디를 key 값으로 설정하였고 기업 정보의 id는 사업자 등록 번호로 설정하여 중복을 방지하여 fetch 요청을 여러 번 날려도 값이 교체될 뿐 중복해서 삽입되지는 않게끔 구현하여 해결하였다.

 

전체 코드부터 보자.

package pelican.co_labor.service;

import jakarta.annotation.PostConstruct;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import pelican.co_labor.domain.enterprise.Enterprise;
import pelican.co_labor.repository.enterprise.EnterpriseRepository;

import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class EnterpriseFetchApiService {

    @Value("${public.enterprise.list.key.decoded}")
    private String serviceKey;

    @Autowired
    private final EnterpriseRepository enterpriseRepository;

    private RestTemplate restTemplate = new RestTemplate();

    public EnterpriseFetchApiService(EnterpriseRepository enterpriseRepository) {
        this.enterpriseRepository = enterpriseRepository;
    }

    @PostConstruct
    public void init() {
        if (enterpriseRepository.count() == 0) {
            fetchAndSaveEnterpriseData();
        }
    }

    public void fetchAndSaveEnterpriseData() {
        String apiUrl = "<http://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2>";
        int numOfRows = 1000; // 한 번에 가져올 행 수
        int totalCount = getTotalCount(apiUrl); // 전체 데이터 개수
        int totalPages = (totalCount / numOfRows) + 1; // 전체 페이지 수

        try {
            for (int pageNo = 1; pageNo <= totalPages; pageNo++) {
                String urlStr = apiUrl + "?serviceKey=" + serviceKey + "&numOfRows=" + numOfRows + "&pageNo=" + pageNo + "&resultType=json";
                ResponseEntity<String> response = restTemplate.getForEntity(urlStr, String.class);

                String responseBody = response.getBody();
                if (responseBody == null || responseBody.isEmpty()) {
                    System.err.println("Empty response body for URL: " + urlStr);
                    continue;
                }

                System.out.println("Response Body: " + responseBody);

                JSONObject jsonResponse;
                try {
                    jsonResponse = new JSONObject(responseBody);
                } catch (Exception e) {
                    System.err.println("Invalid JSON response: " + responseBody);
                    continue;
                }

                JSONArray items = jsonResponse.getJSONObject("response").getJSONObject("body").getJSONObject("items").getJSONArray("item");

                List<Enterprise> enterprisesToSave = new ArrayList<>();
                for (int i = 0; i < items.length(); i++) {
                    JSONObject item = items.getJSONObject(i);

                    String crno = item.getString("crno");
                    String bzno = item.getString("bzno");

                    if (!enterpriseRepository.existsById(bzno)) {
                        String enpBsadr = item.getString("enpBsadr");
                        if (Pattern.matches(".*[가-힣]+.*", enpBsadr)) {
                            String[] addressParts = enpBsadr.split(" ", 3);

                            Enterprise enterprise = new Enterprise();
                            enterprise.setEnterprise_id(bzno);
                            enterprise.setName(item.getString("corpNm"));
                            enterprise.setType(bzno);
                            enterprise.setPhone_number(item.getString("enpTlno"));

                            if (addressParts.length > 0) {
                                enterprise.setAddress1(addressParts[0]);
                            }
                            if (addressParts.length > 1) {
                                enterprise.setAddress2(addressParts[1]);
                            }
                            if (addressParts.length > 2) {
                                enterprise.setAddress3(addressParts[2]);
                            }

                            enterprisesToSave.add(enterprise);
                        }
                    }
                }

                if (!enterprisesToSave.isEmpty()) {
                    enterpriseRepository.saveAll(enterprisesToSave);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private int getTotalCount(String apiUrl) {
        try {
            String urlStr = apiUrl + "?serviceKey=" + serviceKey + "&numOfRows=1&pageNo=1&resultType=json";
            ResponseEntity<String> response = restTemplate.getForEntity(urlStr, String.class);

            String responseBody = response.getBody();
            if (responseBody == null || responseBody.isEmpty()) {
                System.err.println("Empty response body for URL: " + urlStr);
                return 0;
            }

            System.out.println("Response Body for Total Count: " + responseBody);

            JSONObject jsonResponse = new JSONObject(responseBody);
            return jsonResponse.getJSONObject("response").getJSONObject("body").getInt("totalCount");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

 

상당히 길다... 하나씩 뜯어보자.

private int getTotalCount(String apiUrl) {
    try {
        String urlStr = apiUrl + "?serviceKey=" + serviceKey + "&numOfRows=1&pageNo=1&resultType=json";
        ResponseEntity<String> response = restTemplate.getForEntity(urlStr, String.class);

        String responseBody = response.getBody();
        if (responseBody == null || responseBody.isEmpty()) {
            System.err.println("Empty response body for URL: " + urlStr);
            return 0;
        }

        System.out.println("Response Body for Total Count: " + responseBody);

        JSONObject jsonResponse = new JSONObject(responseBody);
        return jsonResponse.getJSONObject("response").getJSONObject("body").getInt("totalCount");
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

전체 응답 엔티티의 수를 세기 위해 하나씩 요청을 날리면서 총 개수를 세는 방식으로 구현했는데 상당히 비효율적이다. 엄청나게 많은 요청이 필요해서 여기서 문제를 찾았고 다른 문제도 있었다.

 

if (!enterpriseRepository.existsById(bzno))

위의 코드를 통해 기업의 중복 등록을 방지했는데,, 매번 데이터베이스를 조회하면서 엄청나게 많은 조회가 일어나서 너무 오래걸렸다.

 

그래서 다음과 같은 방식으로 오버헤드를 해결하였다.

  1. 전체 응답 수 세기 → 전체 응답의 수를 세는 대신 pageNo를 증가시키면서 계속 요청을 보내다가 응답의 item이 비는 순간, 즉 끝까지 다 순회한 순간 멈추게끔 구현하였다.
  2. 중복 제거 → 중복을 제거하기 위해 리스트에 이미 존재하는 데이터인지 알아보는 대신 같은 키로 save를 호출하면 덮어씌워진다는 사실을 이용해서 중복을 체크하지 않고 제거를 구현했다.
  3. 데이터가 많음 → pageNo를 15로 제한시켜 그 뒤의 데이터는 db에 삽입하지 않았다. 조금 찝찝하지만 오버헤드를 최대한 줄이기 위해서는 데이터를 줄이는 방법이 과장 확실하다.
package pelican.co_labor.service;

import jakarta.annotation.PostConstruct;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import pelican.co_labor.domain.enterprise.Enterprise;
import pelican.co_labor.repository.enterprise.EnterpriseRepository;

import java.util.regex.Pattern;

@Service
public class EnterpriseFetchApiService {

    @Value("${public.enterprise.list.key.decoded}")
    private String serviceKey;

    @Autowired
    private final EnterpriseRepository enterpriseRepository;

    private final RestTemplate restTemplate = new RestTemplate();

    public EnterpriseFetchApiService(EnterpriseRepository enterpriseRepository) {
        this.enterpriseRepository = enterpriseRepository;
    }

    @PostConstruct
    public void init() {
        if (enterpriseRepository.count() == 0) {
            fetchAndSaveEnterpriseData();
        }
    }

    public void fetchAndSaveEnterpriseData() {
        String apiUrl = "<http://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2>";
        int numOfRows = 1000; // 한 번에 가져올 행 수
        int pageNo = 1; // 시작 페이지 번호

        while (true) {
            String urlStr = apiUrl + "?serviceKey=" + serviceKey + "&numOfRows=" + numOfRows + "&pageNo=" + pageNo + "&resultType=json";
            ResponseEntity<String> response = restTemplate.getForEntity(urlStr, String.class);

            String responseBody = response.getBody();
            if (responseBody == null || responseBody.isEmpty()) {
                System.err.println("Empty response body for URL: " + urlStr);
                break;
            }

            JSONObject jsonResponse;
            try {
                jsonResponse = new JSONObject(responseBody);
            } catch (Exception e) {
                System.err.println("Invalid JSON response: " + responseBody);
                break;
            }

            JSONArray items = jsonResponse.getJSONObject("response").getJSONObject("body").getJSONObject("items").getJSONArray("item");

            if (items.isEmpty() || pageNo>=15) {
                break; // 데이터가 없으면 루프 종료
            }

            for (int i = 0; i < items.length(); i++) {
                JSONObject item = items.getJSONObject(i);
                if (item.isNull("corpNm")) continue;

                String enpBsadr = item.getString("enpBsadr");
                if (Pattern.matches(".*[가-힣]+.*", enpBsadr)) {
                    String[] addressParts = enpBsadr.split(" ", 3);

                    Enterprise enterprise = new Enterprise();
                    enterprise.setEnterprise_id(item.getString("bzno"));
                    enterprise.setName(item.getString("corpNm"));
                    enterprise.setType(item.getString("sicNm"));
                    enterprise.setDescription(item.getString("enpMainBizNm"));
                    enterprise.setPhone_number(item.getString("enpTlno"));

                    if (addressParts.length > 0) {
                        enterprise.setAddress1(addressParts[0]);
                    }
                    if (addressParts.length > 1) {
                        enterprise.setAddress2(addressParts[1]);
                    }
                    if (addressParts.length > 2) {
                        enterprise.setAddress3(addressParts[2]);
                    }

                    enterpriseRepository.save(enterprise); // 이미 존재하면 덮어씌움
                }
            }

            pageNo++; // 다음 페이지로 넘어감
        }
    }
}