[패스트캠퍼스X야놀자 : 미니 프로젝트] 공공데이터포털 OpenAPI를 활용한 숙소 및 객실 정보 저장하기

꼬마요리사레미·2023년 12월 5일
0

공공데이터포털TourAPI - 한국관광공사 에서 제공하는 OpenAPI 를 활용하여 숙박 시설 및 객실에 대한 정보를 받아와서 데이터베이스에 저장하는 과정을 다루어 보겠습니다.

1. 숙박 시설 관련 ERD

숙박 정보 조회

String url = String.format(
        "%s?numOfRows=%s&MobileOS=%s&MobileApp=%s&_type=%s&arrange=%s&serviceKey=%s",
        LINK_STAY, numOfRows, MOBILE_OS, MOBILE_APP, TYPE, ARRANGE, SERVICE_KEY);

URI uri = new URI(url);

RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(uri, String.class);

Map<String, Object> map = new ObjectMapper().readValue(response, Map.class);
Map<String, Object> responseMap = (Map<String, Object>) map.get("response");
Map<String, Object> bodyMap = (Map<String, Object>) responseMap.get("body");
Map<String, Object> itemsMap = (Map<String, Object>) bodyMap.get("items");
List<Map<String, Object>> itemMap = (List<Map<String, Object>>) itemsMap.get("item");

Spring 의 RestTemplate 을 사용하여 한국관광공사의 OpenAPI 로부터 숙박 정보를 가져오고 Jackson 의 ObjectMapper 를 사용하여 응답을 파싱한다.

URL 생성

  1. String.format 메서드를 사용하여 동적으로 URL 을 생성한다.
  2. %s 가 있는 위치에 각각의 매개변수 값을 대체하여 완전한 URL 을 생성한다.
  3. 각각의 매개변수는 ApiConstants 클래스에 정의된 상수이다.
  4. 이들은 OpenAPI 호출에 필요한 요청 엔드포인트 및 요청 파라미터 값을 채우기 위한 목적으로 사용된다.
* LINK_STAY : 숙박 정보 조회 API 엔드포인트
* NUM_OF_ROWS : 한 페이지에 표시할 결과의 수
* MOBILE_OS : 운영 체제 구분
* MOBILE_APP : 서비스 또는 애플리케이션의 이름
* TYPE : 응답 메시지 형식 
* ARRANGE : 정렬 구분 
* SERVICE_KEY : API를 사용하기 위한 인증키

URI 객체 생성

  1. 생성된 URL 을 기반으로 URI 객체를 만든다.
  2. RestTemplate 을 사용하여 API에 HTTP 요청을 보낼 때 필요한 URI 이다.

이렇게 한국관광공사의 OpenAPI 에 보낼 요청의 URL 형식을 갖추었다. 해당 URL 은 동적으로 요청 파라미터를 조절할 수 있어, 여러 상황에 맞게 API 에 접근할 수 있다.

HTTP 요청 및 응답 파싱

  1. HTTP 요청 전송 : RestTemplate 을 사용하여 URI 에 HTTP GET 요청을 보내고, 문자열 형태의 HTTP 응답을 받아온다.
  2. JSON 응답 파싱 : Jackson 라이브러리의 ObjectMapper 를 사용하여 JSON 문자열을 Java Map 으로 변환한다. 이를 활용하여 데이터를 추출한다.
  1. RestTemplate
  • Spring Framwork 에서 제공하는 HTTP 통신을 간편하게 처리할 수 있는 클래스.
  • 주로 클라이언트 측에서 RESTful 웹 서비스와 통신할 때 사용한다.
  • 다양한 HTTP 메서드를 지원하며, HTTP 요청과 응답을 처리하는 다양한 메서드를 제공한다.
  1. ObjectMapper
  • Jackson 라이브러리에서 제공하는 클래스로, Java 객체와 JSON 데이터 간의 변환을 담당한다.
  • 주로 JSON 데이터를 Java 객체로 Deserialization (역직렬화) 하거나, Java 객체를 JSON 으로 Serialization (직렬화) 하는 데 사용된다.

이러한 과정을 통해 OpenAPI 로부터 받은 JSON 응답을 Java 코드에서 사용 가능한 데이터 구조로 변환하고, 필요한 정보를 추출하여 활용할 수 있다.

데이터 활용 및 데이터베이스 저장

  1. 각각의 키를 활용하여 맵 객체에서 값을 추출하여 새로운 엔티티 객체를 생성한다.
  2. 이렇게 OpenAPI 로부터 받은 데이터를 활용하여 엔티티를 생성하고, 해당 정보를 데이터베이스에 저장한다.
for (Map<String, Object> item : itemMap) {
    Category category = createAndSaveCategory(item);

    Accommodation accommodation = createAccommodation(item);
    AccommodationFacility accommodationFacility = createAccommodationFacility(item);
    Accommodation saveAccommodation = saveAccommodationAndFacility(category, accommodation, accommodationFacility);

    createAndSaveAccommodationImages(item, saveAccommodation);

    saveProductData(item, saveAccommodation);
}

1. 카테고리 생성 및 저장

private Category createAndSaveCategory(Map<String, Object> item) {
    String categoryCode = (String) item.get("cat3");

    return categoryRepository.findByCategoryCode(categoryCode)
            .orElseGet(() -> categoryRepository.save(new Category(categoryCode)));
}
  1. createAndSaveCategory 메서드는 숙박 시설의 정보를 가진 맵 객체에서 카테고리 코드를 추출하고 해당 코드로 데이터베이스에서 카테고리를 조회한다.
  2. 만약 해당 카테고리가 존재하지 않으면 새로운 카테고리를 생성하고 저장한다.

2. 숙소 생성

private Accommodation createAccommodation(Map<String, Object> item) {
    String name = extractAccommodationName((String) item.get("title"));
    String address = (String) item.get("addr1");
    String phone = (String) item.get("tel");
    String longitude = (String) item.get("mapx");
    String latitude = (String) item.get("mapy");
    String areaCode = (String) item.get("areacode");

    return Accommodation.builder()
            .name(name)
            .address(address)
            .phone(phone)
            .longitude(longitude)
            .latitude(latitude)
            .areaCode(areaCode)
            .build();
}

private String extractAccommodationName(String title) {
     return title.contains("[") ? title.split("\\[")[0] : title;
}
  1. createAccommodation 메서드는 OpenAPI 응답에서 추출한 데이터를 사용하여 Accommodation 객체를 생성한다. 해당 객체는 숙박 시설의 이름, 주소, 전화번호, 경도 및 위도, 지역 코드를 가지고 있다.
  2. extractAccommodationName 메서드는 숙박 시설의 이름을 처리하는 메서드이다. 숙박 시설 이름에 [한국관광 품질인증/Korea Quality] 포함된 경우, 이를 제거하고 깔끔한 형태로 반환한다.

3. 숙소 편의시설 정보 조회 및 생성

한국관광공사의 OpenAPI 로부터 특정 숙소의 소개정보를 가져오며, 각각의 편의시설 여부를 확인하고 해당 정보를 가진 AccommodationFacility 객체를 생성한다.

String contentId = (String) item.get("contentid");
String contentTypeId = (String) item.get("contenttypeid");

String url = String.format("%s?ServiceKey=%s&contentTypeId=%s&contentId=%s&MobileOS=%s&MobileApp=%s&_type=%s",
        LINK_INTRO, SERVICE_KEY, contentTypeId, contentId, MOBILE_OS, MOBILE_APP, TYPE);
URI uri = new URI(url);

RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(uri, String.class);

Map<String, Object> map = new ObjectMapper().readValue(response, Map.class);
Map<String, Object> responseMap = (Map<String, Object>) map.get("response");
Map<String, Object> bodyMap = (Map<String, Object>) responseMap.get("body");
Map<String, Object> itemsMap = (Map<String, Object>) bodyMap.get("items");
Map<String, Object> itemMap = ((List<Map<String, Object>>) itemsMap.get("item")).get(0);
  1. contentId 및 contentTypeId 를 추출하여 API 요청에 필요한 매개변수로 설정한다.
  2. 설정된 매개변수를 사용하여 OpenAPI 에 HTTP GET 요청을 보낸다.
  3. 받은 응답을 JSON 형식에서 Map 형식으로 변환하여 필요한 정보를 추출한다.
boolean hasCooking = String.valueOf(itemMap.get("chkcooking")).contains("가능");
boolean hasParking = String.valueOf(itemMap.get("parkinglodging")).contains("가능");
boolean hasSports = String.valueOf(itemMap.get("sports")).equals("1");
boolean hasSauna = String.valueOf(itemMap.get("sauna")).equals("1");
boolean hasBeauty = String.valueOf(itemMap.get("beauty")).equals("1");

return AccommodationFacility.builder()
        .hasCooking(hasCooking)
        .hasParking(hasParking)
        .hasSports(hasSports)
        .hasSauna(hasSauna)
        .hasBeauty(hasBeauty)
        .build();
  1. 숙소 편의시설 정보를 담고 있는 맵 객체에서 각 편의시설의 여부를 나타내는 값을 추출한다.
  2. 각각의 값을 확인하여 해당 시설이 가능한지 여부를 불리언 값으로 설정한다.
  3. 설정된 값을 사용하여 새로운 AccommodationFacility 객체를 생성하고 반환한다.

4. 숙소 및 숙소 편의시설 저장

private Accommodation saveAccommodationAndFacility(Category category, Accommodation accommodation, AccommodationFacility accommodationFacility) {
    accommodation.setCategory(category);
    Accommodation savedAccommodation = accommodationRepository.save(accommodation);

    accommodationFacility.setAccommodation(savedAccommodation);
    accommodationFacilityRepository.save(accommodationFacility);

    return savedAccommodation;
}
  1. saveAccommodationAndFacility 메서드는 카테고리, 숙소, 그리고 숙소 시설 정보를 활용하여 데이터베이스에 숙소와 숙소 시설을 저장한다.
  2. 우선, 숙소에 해당하는 엔티티를 저장하고 저장된 숙소를 기반으로 숙소 시설 엔티티를 저장한다. 마지막으로, 저장된 숙소 엔티티를 반환한다.

5. 숙소 이미지 생성 및 저장

private void createAndSaveAccommodationImages(Map<String, Object> item, Accommodation accommodation) {
    List<AccommodationImage> accommodationImages = new ArrayList<>();

    for (int i = 1; i <= 2; i++) {
        String imageUrl = (String) item.get("firstimage" + (i == 1 ? "" : i));

        if (imageUrl != null) {
            AccommodationImage accommodationImage = AccommodationImage.builder()
                    .imageUrl(imageUrl)
                    .build();
            accommodationImage.setAccommodation(accommodation);
            accommodationImages.add(accommodationImage);
        }
    }

    accommodationImageRepository.saveAll(accommodationImages);
}
  1. createAndSaveAccommodationImages 메서드는 숙소 정보를 담고 있는 맵 객체에서 숙소 이미지를 추출하여 데이터베이스에 저장한다.
  2. 각각의 키를 이용하여 이미지 URL 을 추출하고, 해당 URL 이 존재하는 경우
    AccommodationImage 객체를 생성하고 AccommodationImage 객체 리스트에 추가한다.
  3. 최종적으로 AccommodationImage 객체를 데이터베이스에 저장한다.

2. 객실 관련 ERD

객실 정보 조회 및 유효성 검증

한국관광공사의 OpenAPI 로부터 특정 숙소의 객실정보를 가져오며, 유효성 검증을 통해 각각의 로직을 수행한다.

String contentId = (String) item.get("contentid");
String contentTypeId = (String) item.get("contenttypeid");

String url = String.format("%s?ServiceKey=%s&contentTypeId=%s&contentId=%s&MobileOS=%s&MobileApp=%s&_type=%s",
        LINK_INFO, SERVICE_KEY, contentTypeId, contentId, MOBILE_OS, MOBILE_APP, TYPE);
URI uri = new URI(url);

RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(uri, String.class);

Map<String, Object> map = new ObjectMapper().readValue(response, Map.class);
Map<String, Object> responseMap = (Map<String, Object>) map.get("response");
Map<String, Object> bodyMap = (Map<String, Object>) responseMap.get("body");
Object itemsObject = bodyMap.get("items");

if (itemsObject instanceof Map) {
    // 처리 로직
} else {
    // 처리 로직
}
  1. 객실 정보가 제공되었을 시, 해당 정보를 가진 Product 및 ProductFacility 및 ProductImage 객체를 생성하고 데이터베이스에 저장한다.
  2. 객실 정보가 제공되지 않았을 시, 임의로 생성해서 데이터베이스에 저장한다.

객실 정보 존재 시 객실 생성 및 저장

private void createAndSaveProductData(List<Map<String, Object>> itemList, String checkintime, String checkouttime, Accommodation accommodation) throws URISyntaxException, JsonProcessingException {
    for (Map<String, Object> item : itemList) {
        Product product = createProduct(item, checkintime, checkouttime, accommodation);
        product.setAccommodation(accommodation);
        productRepository.save(product);

        ProductFacility productFacility = createProductFacility(item);
        productFacility.setProduct(product);
        productFacilityRepository.save(productFacility);

        List<ProductImage> productImages = createProductImages(item);
        for (ProductImage productImage : productImages) {
            productImage.setProduct(product);
            productImageRepository.save(productImage);
        }

        saveProductInfoPerNight(product);
    }
}
  1. createAndSaveProductData 메서드는 주어진 객실 정보들에서 각 객실 정보별로 다음 작업을 수행한다.
  2. 객실 정보를 생성하고 데이터베이스에 저장한 후, 해당 숙박 시설의 편의 시설 정보를 생성하고 저장한다. 마지막으로, 숙박 시설에 속한 이미지 정보들을 생성하고 저장한다.

1. 객실 정보 생성

private Product createProduct(Map<String, Object> item, String checkintime, String checkouttime, Accommodation accommodation) {
    Random random = new Random();

    String name = (String) item.get("roomtitle");
    int count = item.get("roomcount") != null && !"0".equals(item.get("roomcount"))
            ? Integer.parseInt((String) item.get("roomcount"))
            : random.nextInt(10) + 1;

    int standardNumber = item.get("roombasecount") != null && !"0".equals(item.get("roombasecount"))
            ? Integer.parseInt((String) item.get("roombasecount"))
            : random.nextInt(10) + 1;

    int maximumNumber = item.get("roommaxcount") != null && !"0".equals(item.get("roommaxcount"))
            ? Integer.parseInt((String) item.get("roommaxcount"))
            : standardNumber + random.nextInt(10) + 1;

    Product product = Product.builder()
            .name(name)
            .checkInTime(checkintime)
            .checkOutTime(checkouttime)
            .standardNumber(standardNumber)
            .maximumNumber(maximumNumber)
            .count(count)
            .build();

    product.setAccommodation(accommodation);

    return product;
}
  1. createProduct 메서드는 객실 정보를 생성하는 역할을 수행한다.
  2. 객실 정보가 담긴 맵 객체에서 객실의 이름, 숙박 가능 인원 수, 기본/최대 숙박 가능 인원 수를 추출하고, 값이 존재하지 않을 경우 랜덤하게 객실을 생성한다.
  3. 생성된 객실은 특정 숙박 시설에 속하며, 숙박 가능 시간 등의 정보가 설정된다.

2. 객실 편의시설 생성

private ProductFacility createProductFacility(Map<String, Object> item) {
    return ProductFacility.builder()
            .hasBath("Y".equals(item.get("roombath")))
            .hasAirCondition("Y".equals(item.get("roomaircondition")))
            .hasTv("Y".equals(item.get("roomtv")))
            .hasPc("Y".equals(item.get("roompc")))
            .hasCable("Y".equals(item.get("roomcable")))
            .hasInternet("Y".equals(item.get("roominternet")))
            .hasRefrigerator("Y".equals(item.get("roomrefrigerator")))
            .hasToiletries("Y".equals(item.get("roomtoiletries")))
            .hasSofa("Y".equals(item.get("roomsofa")))
            .canCook("Y".equals(item.get("roomcook")))
            .hasTable("Y".equals(item.get("roomtable")))
            .hasHairDryer("Y".equals(item.get("roomhairdryer")))
            .build();
}
  1. createProductFacility 메서드는 객실 편의시설 정보를 생성한다.
  2. 주어진 맵 객체에서 객실에 있는 시설들의 여부를 추출하여 불리언 값으로 설정한다.
  3. 설정된 값을 사용하여 새로운 ProductFacility 객체를 생성하여 반환한다.

3. 객실 이미지 생성

private List<ProductImage> createProductImages(Map<String, Object> item) {
    List<ProductImage> productImages = new ArrayList<>();

    for (int i = 1; i <= 4; i++) {
        String imageUrl = (String) item.get("roomimg" + i);

        if (!imageUrl.isEmpty()) {
            ProductImage productImage = ProductImage.builder()
                    .imageUrl(imageUrl)
                    .build();
            productImages.add(productImage);
        }
    }

    return productImages;
}
  1. createProductImages 메서드는 객실 이미지를 생성한다.
  2. 주어진 맵 객체에서 객실에 대한 이미지 URL들을 확인하고, 비어 있지 않은 경우에만 ProductImage 객체를 생성하고 ProductImage 객체 리스트에 추가한다.
  3. 최종적으로 ProductImage 객체 리스트를 반환한다.

객실 정보 존재하지 않을 시 객실 생성 및 저장

Random random = new Random();

for (int i = 0; i < 3; i++) {
    Product product = createRandomProduct(accommodation, random);
    product.setAccommodation(accommodation);
    productRepository.save(product);

    ProductFacility productFacility = createRandomProductFacility(random);
    productFacility.setProduct(product);
    productFacilityRepository.save(productFacility);

    product.setProductFacility(productFacility);

    List<ProductImage> productImages = createRandomProductImages();
    for (ProductImage productImage : productImages) {
        productImage.setProduct(product);
        productImageRepository.save(productImage);
    }
}
  1. 위의 saveProductData 메서드에서 else 구문에 해당하는 부분이다.
  2. 객실 정보가 존재하지 않을 경우 createRandomProduct, createRandomProductFacility, createRandomProductImages 메서드를 사용하여 랜덤으로 생성된 객실 정보, 편의시설 정보, 이미지 정보를 데이터베이스에 저장한다.

1. 랜덤 객실 정보 생성

private static Product createRandomProduct(Accommodation accommodation, Random random) {
    String[] roomTypes = {"슈페리어 룸", "디럭스 룸", "캐릭터 룸", "그랜드 디럭스 룸",
            "그랜드 디럭스 패밀리 트윈 룸", "프리미어 패밀리 트윈 룸", "주니어 스위트 룸",
            "디럭스 스위트 룸", "로얄 스위트 룸", "주니어 스위트 룸", "프리미어 스위트 룸"};
    String roomType = roomTypes[random.nextInt(roomTypes.length)];

    return Product.builder()
            .name(roomType)
            .checkInTime("15:00")
            .checkOutTime("11:00")
            .standardNumber(random.nextInt(5) + 1)
            .maximumNumber(random.nextInt(5) + 6)
            .count(random.nextInt(20) + 1)
            .build();
}
  1. createRandomProduct 메서드는 숙박 시설의 랜덤 객실 정보를 생성한다.
  2. 사용 가능한 객실 유형 중에서 하나를 랜덤으로 선택하고, 해당 객실에 대한 정보를 생성한다.

2. 랜덤 객실 편의시설 생성

private static ProductFacility createRandomProductFacility(Random random) {
    boolean hasBath = random.nextBoolean();
    boolean hasAirCondition = random.nextBoolean();
    boolean hasTv = random.nextBoolean();
    boolean hasPc = random.nextBoolean();
    boolean hasCable = random.nextBoolean();
    boolean hasInternet = random.nextBoolean();
    boolean hasRefrigerator = random.nextBoolean();
    boolean hasToiletries = random.nextBoolean();
    boolean hasSofa = random.nextBoolean();
    boolean canCook = random.nextBoolean();
    boolean hasTable = random.nextBoolean();
    boolean hasHairDryer = random.nextBoolean();
    
    return ProductFacility.builder()
            .hasBath(hasBath)
            .hasAirCondition(hasAirCondition)
            .hasTv(hasTv)
            .hasPc(hasPc)
            .hasCable(hasCable)
            .hasInternet(hasInternet)
            .hasRefrigerator(hasRefrigerator)
            .hasToiletries(hasToiletries)
            .hasSofa(hasSofa)
            .canCook(canCook)
            .hasTable(hasTable)
            .hasHairDryer(hasHairDryer)
            .build();
}
  1. createRandomProductFacility 메서드는 랜덤으로 객실의 편의시설 정보를 생성한다.
  2. 각 편의시설의 존재 여부를 랜덤으로 결정하고, 해당 정보를 반환한다.

3. 랜덤 객실 이미지 생성

private static List<ProductImage> createRandomProductImages() {
    List<String> imageUrls = Arrays.asList(
            "http://tong.visitkorea.or.kr/cms/resource/50/2705650_image2_1.jpg",
            "http://tong.visitkorea.or.kr/cms/resource/51/2705651_image2_1.jpg",
            "http://tong.visitkorea.or.kr/cms/resource/35/2705635_image2_1.jpg"
    );

    return imageUrls.stream()
            .map(imageUrl -> ProductImage.builder().imageUrl(imageUrl).build())
            .collect(Collectors.toList());
}
  1. createRandomProductImages 메서드는 미리 정의된 이미지 URL 목록을 이용하여 랜덤 객실 이미지 목록을 생성한다.
  2. 이미지 URL을 사용하여 ProductImage 객체를 생성하고, 해당 객체들을 리스트로 반환한다.

애플리케이션 실행 후 데이터베이스에 접속하면 다음과 같이 데이터들이 잘 저장되어 있는걸 확인할 수 있다.

0개의 댓글