[SpringBoot] DynamoDB 연동 및 서울시 모범식당 공공데이터 활용

두의 개발 고민 블로그·2023년 5월 27일
0

SpringBoot

목록 보기
1/3

목표

Android Map에서 사용자의 위치(위도, 경도) 를 기준으로 가까운 모범식당 5곳을 뽑아주는 API 설계

SpringBoot로 공공데이터를 가져와 DynamoDB에 저장

구 이름과 위도 경도값을 이용하여 가까운 모범식당 Top5 반환

RestAPI

URL


http://localhost:8080/dynamo/query/near?guName=강남구&latitude=37.497616&longitude=127.061726

Request


RequestParam

  • guName
    • 서울시 구이름
  • latitude
    • 위도
  • longitude
    • 경도

Response


[
    {
        "upsoName": "방이샤브칼국수",
        "cggCode": "3220000",
        "asgnDate": "20141006",
        "latitude": 37.4979570557423,
        "siteAddress": "서울특별시 강남구 대치동  1019번지 12호  ",
        "siteAddressRd": "서울특별시 강남구 삼성로 229, (대치동)",
        "id": "da54d555-e1af-4bbc-bc39-2ac66187f460",
        "admdngName": "대치4동",
        "mainFood": "칼국수",
        "longitude": 127.060481182415,
        "asgnYear": "2014"
    },
    {
        "upsoName": "막둥이냉면",
        "cggCode": "3220000",
        "asgnDate": "20211116",
        "latitude": 37.4989054318323,
        "siteAddress": "서울특별시 강남구 대치동  941번지 16호  지상1층",
        "siteAddressRd": "서울특별시 강남구 삼성로 307, (대치동,지상1층)",
        "id": "492eb527-360d-49fc-9bfc-0a3f5504cb78",
        "sitePhoneNumber": "5573188",
        "admdngName": "대치4동",
        "mainFood": "냉면",
        "asgnYear": "2021",
        "longitude": 127.058679239793
    },
    {
        "upsoName": "진가샤브샤브",
        "cggCode": "3220000",
        "asgnDate": "20141006",
        "latitude": 37.4972015266228,
        "siteAddress": "서울특별시 강남구 대치동  1023번지 3호  지상4층",
        "siteAddressRd": "서울특별시 강남구 도곡로 426, 지상4층 (대치동)",
        "id": "35dc5157-250f-4dee-abbc-f93178afa4d2",
        "admdngName": "대치1동",
        "mainFood": "샤브샤브",
        "longitude": 127.056552114829,
        "asgnYear": "2014"
    },
    {
        "upsoName": "(주)아구본가첨벙대치",
        "cggCode": "3220000",
        "asgnDate": "20100915",
        "latitude": 37.4972015266228,
        "siteAddress": "서울특별시 강남구 대치동  1023번지 3호  지상2층",
        "siteAddressRd": "서울특별시 강남구 도곡로 426, (대치동,지상2층)",
        "id": "a6cd5dde-db4c-4078-bdb8-954924c167f1",
        "sitePhoneNumber": "02  538 0104",
        "admdngName": "대치1동",
        "mainFood": "아구찜",
        "asgnYear": "2010",
        "longitude": 127.056552114829
    },
    {
        "upsoName": "엠브이샤브샤브",
        "cggCode": "3220000",
        "asgnDate": "20100210",
        "latitude": 37.4933573176217,
        "siteAddress": "서울특별시 강남구 대치동  507번지 0호  원플러스상가지하1층105,106호",
        "siteAddressRd": "서울특별시 강남구 남부순환로 2936, (대치동,원플러스상가지하1층105,106호)",
        "id": "28a463bc-d96f-40db-baff-7100a6cbf2c7",
        "sitePhoneNumber": "5617573",
        "admdngName": "대치1동",
        "mainFood": "샤브샤브",
        "asgnYear": "2010",
        "longitude": 127.061908985635
    }
]

Code

build.gradle

// AWS SDK
    implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.11.563'
//GeoCoder
implementation 'com.google.code.geocoder-java:geocoder-java:0.16'
  • DynamoDB관련 라이브러리와 주소를 통해 위도 경도 데이터로 바꿔주는 GeoCoder 라이브러리를 가져온다.

application.yml

amazon:
  aws:
    access-key: [엑세스 키]
    secret-key: [비밀 키]
    region: ap-northeast-2
    dynamodb:
      endpoint: dynamodb.ap-northeast-2.amazonaws.com
  • DynamoDB에 접속 하기 위해 관련 키와 리전값을 설정한다.
  • 엑세스 키와 비밀 키는 IAM에서 얻을 수 있다.

DynamoDBConfig.java: Config

@Configuration
public class DynamoDBConfig {

    @Value("${amazon.aws.access-key}")
    private String accessKey;

    @Value("${amazon.aws.secret-key}")
    private String secretKey;

    @Value("${amazon.aws.region}")
    private String region;

    @Value("${amazon.aws.dynamodb.endpoint}")
    private String dynamoDBEndpoint;
    @Bean
    public DynamoDBMapper dynamoDBMapper() {
        DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder()
                .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.CLOBBER)
                .withConsistentReads(DynamoDBMapperConfig.ConsistentReads.CONSISTENT)
                .withTableNameOverride(null)

                .withPaginationLoadingStrategy(DynamoDBMapperConfig.PaginationLoadingStrategy.EAGER_LOADING)
                .build();
        DynamoDBMapper mapper = new DynamoDBMapper(amazonDynamoDB(), mapperConfig);
        return mapper;
    }
    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        AmazonDynamoDB amazonDynamoDB =  AmazonDynamoDBClientBuilder.standard()
                .withEndpointConfiguration(new EndpointConfiguration(dynamoDBEndpoint, region))
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
        System.out.println(amazonDynamoDB.listTables());
        return amazonDynamoDB;
    }
}
  • DynamoDB 객체를 사용할 때 늘 이렇게 생성할 필요 없기 때문에 @Bean으로 저장해두고 사용한다.
  • @Value 어노테이션으로 yml에 있는 값을 사용 할 수 있다. (보안적 측면)

RestaurantItem.java 모범 음식점 데이터 Entity

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@DynamoDBTable(tableName = "restaurantItem") //해당 클래스를 엔티티로 설정합니다.
public class RestaurantItem {

    @DynamoDBHashKey(attributeName = "id") //해당 필드를 HashKey로 설정합니다.
    @DynamoDBAutoGeneratedKey // 자동으로 Key를 생성합니다. UUID를 활용합니다.
    private String id;

    @DynamoDBAttribute  // 해당 필드를 Attribute로 설정합니다.
    @DynamoDBIndexHashKey(globalSecondaryIndexName = "cggCode") // global secondary index HashKey 설정
    private String cggCode;

    @DynamoDBAttribute(attributeName = "latitude")
    private Double latitude; // 위도

    @DynamoDBAttribute(attributeName = "longitude")
    private Double longitude; // 경도
    @DynamoDBAttribute
    private String asgnYear; // 지정년도
    @DynamoDBAttribute
    private String asgnDate; // 지정일자
    @DynamoDBAttribute
    private String upsoName; // 업소명
    @DynamoDBAttribute
    private String siteAddressRd; // 소재지도로명
    @DynamoDBAttribute
    private String siteAddress; // 소재지지번
    @DynamoDBAttribute
    private String mainFood; // 주된음식
    @DynamoDBAttribute
    private String admdngName; // 행정동명
    @DynamoDBAttribute
    private String sitePhoneNumber; // 소재지전화번호

}
  • JPA 관점에서 보면 Entity 즉, 데이터베이스의 테이블 자체라고 이해하면 쉽다.

LocationToLatiLongi.java: 주소 → 좌표

public class LocationToLatiLongi {
    public static Double[] findGeoPoint(String location) {

        String apikey = [api 키];
        String searchType = "parcel";
        String searchAddr = location;
        String epsg = "epsg:4326";

        StringBuilder sb = new StringBuilder("https://api.vworld.kr/req/address");
        sb.append("?service=address");
        sb.append("&request=getCoord");
        sb.append("&format=json");
        sb.append("&crs=" + epsg);
        sb.append("&key=" + apikey);
        sb.append("&type=" + searchType);
        sb.append("&address=" + URLEncoder.encode(searchAddr, StandardCharsets.UTF_8));

        try{
            URL url = new URL(sb.toString());
            BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));

            JsonParser jspa = new JsonParser();
            JsonObject jsob = (JsonObject) jspa.parse(reader);
            JsonObject jsrs = (JsonObject) jsob.get("response");
            JsonObject jsResult = (JsonObject) jsrs.get("result");
            JsonObject jspoitn = (JsonObject) jsResult.get("point");

//            System.out.print("위도 :" +jspoitn.get("y"));
//            System.out.println("경도 : "+jspoitn.get("x"));
            Double[] coords = new Double[2];
            coords[0] = Double.parseDouble ((jspoitn.get("y")+"").replace("\"",""));
            coords[1] = Double.parseDouble ((jspoitn.get("x")+"").replace("\"",""));
            return coords;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 파라메터 식당의 주소를 입력하면 위도 경도로 나오는 클래스
  • 사용한 코드 홈페이지

VWorld - GeoCoderGuide 사이트

ModelRestaurant.java: Entity

@RequiredArgsConstructor
public class ModelRestaurant {
    private final DynamoDBMapper dynamoDBMapper;

    // tag값의 정보를 가져오는 메소드
    private String getTagValue(String tag, Element eElement) {
        NodeList nlList = eElement.getElementsByTagName(tag).item(0).getChildNodes();
        Node nValue = (Node) nlList.item(0);
        if(nValue == null)
            return null;
        return nValue.getNodeValue();
    }

    public Boolean getAPIList() throws ParserConfigurationException, IOException, SAXException {
        Map<String, String> guMap = ModelUrl.getMap();
        int page = 1;  // 페이지 초기값
        // 총 개수 가져오기
        int totalCount = 0;
        int successCount=0;
        int errorCount=0;
        boolean check = true;

        List<RestaurantItem> itemList = new ArrayList<>();
        List<ErrorRestaurant> errorRestaurants = new ArrayList<>();

        for(String guName : guMap.keySet()) {
            try {
                String tempUrl = guMap.get(guName);
                System.out.println(guName+" 주소 : " +tempUrl);
                DocumentBuilderFactory dbFactoty = DocumentBuilderFactory.newInstance();
                DocumentBuilder dBuilder = dbFactoty.newDocumentBuilder();
                Document doc = dBuilder.parse(tempUrl);

                // root tag
                doc.getDocumentElement().normalize();
//                System.out.println("Root element :" + doc.getDocumentElement().getNodeName());

                // 파싱할 tag
                NodeList nList = doc.getElementsByTagName("row");

                for (int temp = 0; temp < nList.getLength(); temp++) {
                    Node nNode = nList.item(temp);
                    if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                        Element eElement = (Element) nNode;
                        RestaurantItem restaurantItem = new RestaurantItem();
                        restaurantItem.setCggCode(getTagValue("CGG_CODE", eElement));
                        restaurantItem.setAsgnYear(getTagValue("ASGN_YY", eElement));
                        restaurantItem.setAsgnDate(getTagValue("ASGN_YMD", eElement));
                        restaurantItem.setUpsoName(getTagValue("UPSO_NM", eElement));
                        restaurantItem.setSiteAddressRd(getTagValue("SITE_ADDR_RD", eElement));
                        restaurantItem.setSiteAddress(getTagValue("SITE_ADDR", eElement));
                        restaurantItem.setMainFood(getTagValue("MAIN_EDF", eElement));
                        restaurantItem.setAdmdngName(getTagValue("ADMDNG_NM", eElement));
                        restaurantItem.setSitePhoneNumber(getTagValue("UPSO_SITE_TELNO", eElement));
                        try {
                            Double[] coords = LocationToLatiLongi.findGeoPoint(restaurantItem.getSiteAddress());
                            restaurantItem.setLatitude(coords[0]);
                            restaurantItem.setLongitude(coords[1]);
                            itemList.add(restaurantItem);
                        } catch (Exception e) {
                            ErrorRestaurant errorRestaurant = new ErrorRestaurant();
                            errorRestaurant.setCggCode(getTagValue("CGG_CODE", eElement));
                            errorRestaurant.setAsgnYear(getTagValue("ASGN_YY", eElement));
                            errorRestaurant.setAsgnDate(getTagValue("ASGN_YMD", eElement));
                            errorRestaurant.setUpsoName(getTagValue("UPSO_NM", eElement));
                            errorRestaurant.setSiteAddressRd(getTagValue("SITE_ADDR_RD", eElement));
                            errorRestaurant.setSiteAddress(getTagValue("SITE_ADDR", eElement));
                            errorRestaurant.setMainFood(getTagValue("MAIN_EDF", eElement));
                            errorRestaurant.setAdmdngName(getTagValue("ADMDNG_NM", eElement));
                            errorRestaurant.setSitePhoneNumber(getTagValue("UPSO_SITE_TELNO", eElement));
                            errorRestaurants.add(errorRestaurant);
                        }
                        totalCount++;

                    }
                    System.out.println("정상:" + itemList.size() + ", 주소에러:" + errorRestaurants.size());

                }  // for end

            } catch (Exception e) {
                e.printStackTrace();
            }  // try~catch end
            System.out.println(guName + " 데이터베이스 데이터 삽입 시작");
            try{
                // 병렬로하면 프로비저닝 오류 떴을 때 어디부터 다시 해야하는지 모름
                successCount+= itemList.size();
                errorCount+=errorRestaurants.size();
                itemList.stream().parallel().forEach(dynamoDBMapper::save);
                errorRestaurants.stream().parallel().forEach(dynamoDBMapper::save);
            }catch(Exception e){
                System.out.println(guName + " 데이터베이스 통신 중 오류 발생"+e);
                check = false;
            }

        }

        System.out.println("XXXXXXXXXXXXXXXXX");
        System.out.println("총 데이터 : " + totalCount + "개");
        System.out.println("삽입 가능 데이터 : " + successCount + "개");
        System.out.println("에러 데이터 : " + errorCount + "개");
        System.out.println("XXXXXXXXXXXXXXXXX");
        // 데이터 삽입

        System.out.println("데이터베이스 데이터 삽입 종료");

        return check;
    }  // main end

}  // class end
  • 서울시의 25개의 구에서 모범데이터를 받아와서 RestaurantItem.java 객체에 삽입하는 클래스
itemList.stream().parallel().forEach(dynamoDBMapper::save);
  • 병렬 처리, DynamoDB의 데이터베이스에 넣는 역할
  • errorRestaurant : 주소오류가 떠서 GeoCoder 측에서 오류가 난 데이터

ModelUrl.java: 서울시 구 정보

public class ModelUrl {
    private static final String KEY = "6c514646726a6f6e32395044784652";

    public static Map<String,String> getMap(){
        Map<String, String> guMap = new HashMap<>();
        guMap.put("영등포구", "http://openapi.ydp.go.kr:8088/"+KEY+"/xml/YdpModelRestaurantDesignate/1/1000/");
        guMap.put("송파구", "http://openapi.songpa.seoul.kr:8088/"+KEY+"/xml/SpModelRestaurantDesignate/1/1000/");
        guMap.put("구로구", "http://openapi.guro.go.kr:8088/"+KEY+"/xml/GuroModelRestaurantDesignate/1/1000/");
        guMap.put("강동구", "http://openapi.gd.go.kr:8088/"+KEY+"/xml/GdModelRestaurantDesignate/1/1000/");
        guMap.put("중랑구", "http://openapi.jungnang.seoul.kr:8088/"+KEY+"/xml/JungnangModelRestaurantDesignate/1/1000/");
        guMap.put("성동구", "http://openapi.sd.go.kr:8088/"+KEY+"/xml/SdModelRestaurantDesignate/1/1000/");
        guMap.put("관악구", "http://openapi.gwanak.go.kr:8088/"+KEY+"/xml/GaModelRestaurantDesignate/1/1000/");
        guMap.put("강남구", "http://openapi.gangnam.go.kr:8088/"+KEY+"/xml/GnModelRestaurantDesignate/1/1000/");
        guMap.put("은평구", "http://openapi.ep.go.kr:8088/"+KEY+"/xml/EpModelRestaurantDesignate/1/1000/");
        guMap.put("광진구", "http://openapi.gwangjin.go.kr:8088/"+KEY+"/xml/GwangjinModelRestaurantDesignate/1/1000/");
        guMap.put("중구", "http://openapi.junggu.seoul.kr:8088/"+KEY+"/xml/JungguModelRestaurantDesignate/1/1000/");
        guMap.put("서초구", "http://openapi.seocho.go.kr:8088/"+KEY+"/xml/ScModelRestaurantDesignate/1/1000/");
        guMap.put("용산구", "http://openapi.yongsan.go.kr:8088/"+KEY+"/xml/YsModelRestaurantDesignate/1/1000/");
        guMap.put("성북구", "http://openapi.sb.go.kr:8088/"+KEY+"/xml/SbModelRestaurantDesignate/1/1000/");
        guMap.put("서대문구", "http://openapi.sdm.go.kr:8088/"+KEY+"/xml/SeodaemunModelRestaurantDesignate/1/1000/");
        guMap.put("금천구", "http://openapi.geumcheon.go.kr:8088/"+KEY+"/xml/GeumcheonModelRestaurantDesignate/1/1000/");
        guMap.put("도봉구", "http://openapi.dobong.go.kr:8088/"+KEY+"/xml/DobongModelRestaurantDesignate/1/1000/");
        guMap.put("마포구", "http://openapi.mapo.go.kr:8088/"+KEY+"/xml/MpModelRestaurantDesignate/1/1000/");
        guMap.put("광북구", "http://openapi.gangbuk.go.kr:8088/"+KEY+"/xml/GbModelRestaurantDesignate/1/1000/");
        guMap.put("동대문구", "http://openapi.ddm.go.kr:8088/"+KEY+"/xml/DongdeamoonModelRestaurantDesignate/1/1000/");
        guMap.put("종로구", "http://openapi.jongno.go.kr:8088/"+KEY+"/xml/JongnoModelRestaurantDesignate/1/1000/");
        guMap.put("노원구", "http://openapi.nowon.go.kr:8088/"+KEY+"/xml/NwModelRestaurantDesignate/1/1000/");
        guMap.put("동작구", "http://openapi.dongjak.go.kr:8088/"+KEY+"/xml/DjModelRestaurantDesignate/1/1000/");
        guMap.put("강서구", "http://openapi.gangseo.seoul.kr:8088/"+KEY+"/xml/GangseoModelRestaurantDesignate/1/1000/");
        guMap.put("양천구", "http://openapi.yangcheon.go.kr:8088/"+KEY+"/xml/YcModelRestaurantDesignate/1/1000/");
        return guMap;
    }
    public static String getCggCode(String gu){
        Map<String, String> districtCodeMap = new HashMap<>();
        districtCodeMap.put("동대문구", "3050000");
        districtCodeMap.put("중랑구", "3060000");
        districtCodeMap.put("강남구", "3220000");
        districtCodeMap.put("은평구", "3110000");
        districtCodeMap.put("성동구", "3030000");
        districtCodeMap.put("성북구", "3070000");
        districtCodeMap.put("종로구", "3000000");
        districtCodeMap.put("영등포구", "3180000");
        districtCodeMap.put("강동구", "3240000");
        districtCodeMap.put("광진구", "3040000");
        districtCodeMap.put("마포구", "3130000");
        districtCodeMap.put("구로구", "3160000");
        districtCodeMap.put("양천구", "3140000");
        districtCodeMap.put("중구", "3010000");
        districtCodeMap.put("용산구", "3020000");
        districtCodeMap.put("관악구", "3200000");
        districtCodeMap.put("노원구", "3100000");
        districtCodeMap.put("서대문구", "3120000");
        districtCodeMap.put("서초구", "3210000");
        districtCodeMap.put("동작구", "3190000");
        districtCodeMap.put("송파구", "3230000");
        districtCodeMap.put("강서구", "3150000");
        districtCodeMap.put("도봉구", "3090000");
        districtCodeMap.put("광북구", "3080000");
        districtCodeMap.put("금천구", "3170000");
        return districtCodeMap.get(gu);
    }
}
  • 각 구마다 다른 공공데이터 때문에 하나하나 다 넣어줌
  • 반복문으로 구이름만 넣어주려 했지만, 구마다 다른 형식의 URL을 써서 URL 자체를 value 값으로 넣어줌
  • public static String getCggCode(String gu) 메소드 같은 경우는 gu 이름 구로구, 마포구 같은 이름을 입력하면 해당 코드로 변경시켜주는 메소드

DynamoDBController.java: Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("dynamo")
public class DynamoDBController {
    private final DBtestServiceByMapper dBtestServiceByMapper;

    @GetMapping("query/near")
    public List<Map<String,Object>> queryTest(
            @RequestParam("guName") String guName,
            @RequestParam("latitude") Double latitude,
            @RequestParam("longitude") Double longitude){
        return dBtestServiceByMapper.loadNearByRestaurant(ModelUrl.getCggCode(guName),latitude,longitude);
    }

}
  • 다른 메서드가 많지만 해당 요청에 대한 메서드만 보여주겠다 !
  • @GetMapping(”dynamo/query/near”)
  • @RequestParam("guName") String guName : 구 이름
  • @RequestParam("latitude") Double latitude : 위도
  • @RequestParam("longitude") Double longitude : 경도
  • return : 음식점 정보가 담긴 Map 데이터의 List를 반환한다.

DBtestServiceByMapper.java: Service

@Service
@RequiredArgsConstructor
public class DBtestServiceByMapper {

    private final AmazonDynamoDB amazonDynamoDb;
    private final DynamoDBMapper dynamoDbMapper;
    private final RestaurantRepository restaurantRepository;

    public List<Map<String, Object>> loadNearByRestaurant(String cggCode, Double latitude, Double longitude) {
        List<Map<String, Object>> list = restaurantRepository.findByCggCode(cggCode);

        double targetLatitude = latitude; // 주어진 latitude
        double targetLongitude = longitude; // 주어진 longitude

        // 거리 계산에 사용할 Comparator
        Comparator<Map<String, Object>> distanceComparator = (m1, m2) -> {
            double lat1 = Double.valueOf(m1.get("latitude")+"");
            double lon1 = Double.valueOf(m1.get("longitude")+"");
            double lat2 = Double.valueOf(m2.get("latitude")+"");
            double lon2 = Double.valueOf(m2.get("longitude")+"");

            // 주어진 latitude와 longitude와의 거리 계산
            double distance1 = calculateDistance(targetLatitude, targetLongitude, lat1, lon1);
            double distance2 = calculateDistance(targetLatitude, targetLongitude, lat2, lon2);

            return Double.compare(distance1, distance2);
        };
        // 가까운 거리에 있는 데이터 5개 추출
        List<Map<String, Object>> nearestRestaurants = list.stream()
                .sorted(distanceComparator)
                .limit(5)
                .collect(Collectors.toList());

        // 추출된 데이터 출력
//        for (Map<String, Object> restaurant : nearestRestaurants) {
//            latitude = (double) restaurant.get("latitude");
//            longitude = (double) restaurant.get("longitude");
//            System.out.println("Latitude: " + latitude + ", Longitude: " + longitude);
//        }
        return nearestRestaurants;
    }

    private static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
        // 거리 계산 로직을 구현해야 함
        // 예시: Haversine formula를 활용한 거리 계산

        double earthRadius = 6371; // 지구 반지름 (단위: km)

        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);

        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                        Math.sin(dLon / 2) * Math.sin(dLon / 2);

        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        double distance = earthRadius * c;

        return distance;
    }
}

메인 메서드

  • public List<Map<String, Object>> loadNearByRestaurant(String cggCode, Double latitude, Double longitude)
List<Map<String, Object>> nearestRestaurants = list.stream()
        .sorted(distanceComparator)
        .limit(5)
        .collect(Collectors.toList());
  • 좌표 거리 순으로 정렬 → 5개만 추출 → List 반환

좌표 계산 메서드

  • private static double calculateDistance(double lat1, double lon1, double lat2, double lon2)

RestaurantRepository.java: Repository

@Repository
@RequiredArgsConstructor
public class RestaurantRepository {
    private final AmazonDynamoDB amazonDynamoDB;
    private final DynamoDBMapper dynamoDBMapper;

    // 글로벌 인덱스 cggCode 조회
    public List<Map<String,Object>> findByCggCode(String cggCode){
        DynamoDB dynamoDB = new DynamoDB(amazonDynamoDB);

        Table table = dynamoDB.getTable("restaurantItem");
        Index index = table.getIndex("cggCode-index");

        QuerySpec spec = new QuerySpec()
                .withKeyConditionExpression("cggCode = :v_code")
                .withValueMap(new ValueMap()
                        .withString(":v_code",cggCode));

        ItemCollection<QueryOutcome> items = index.query(spec);
        Iterator<Item> iter = items.iterator();
        List<Map<String,Object>> restaurantItem = new ArrayList<>();

        while (iter.hasNext()) {
            restaurantItem.add(iter.next().asMap());
        }
        return restaurantItem;
    }

}
  • Global Second Index 로 생성한 CggCode를 통하여 데이터를 query하는 메서드
  • 관련 문서

글로벌 보조 인덱스로 작업: Java - Amazon DynamoDB

회고

이번 기회에 DynamoDB를 사용하면서 빠질 수 있는 머리털은 다 빠진거 같다.. 
새로 써본 NoSQL 형식에 새로 써본 데이터베이스 정말 쉽지않았다. 
처음엔 금방 연결 되겠지 하고 Tistory, blog 에서 사람들이 사용한 코드를 가져와 
DynamoDB와 SpringBoot를 연동 시켰다. 
 
 너무 급했던 마음 때문이었나? MySQL에서는 기본적인 연결 과정조차 쉽지 않았다. 
정말 정보가 많은 Mysql 과는 다르게, 
대부분 DynamoDB를 local에서 작동시키는 코드였고, 처음 접해보는 코드이기 때문에 뜯어보고 할
실력조차 되지 않았기에, 계속 나에게 맞는 blog나 StackOverflow 등을 찾아 나섰지만, 
결국 다양한 오류만 접할 뿐 해결되지 않았다.

 원점으로 돌아가서 차근차근 배워서 해보자, 이렇게 하지않으면 될 일이 아닌듯 싶었다. 
 AWS에서 제공하는 개발자 안내서와 java 라이브러리 상에서 AmazonDynamoDB, DynamoDBMapper 
 클래스를 뜯어보았다. 개발자 안내서는 무수히 많은 정보와 나에게 해당되지 않는 정보가 많았기 때문에 
 필요한 정보를 찾는데 오래걸렸다. (심지어 영어라 번역해서 봐야한다..)
 
 테이블의 키 속성, 타입부터 시작해서 프로비저닝하는 방법, 읽기, 쓰기용량 Auto Scaling, 
 Global Secondary Index와 이 인덱스의 Auto Scaling 등 처음부터 이해하기 시작했고, 
 김명재님께서 작성한 우아한 기술 블로그를 천천히 따라하며 Spring의 코드를 완성시켜 갔다. 
 우아한 블로그는 dynamoDB를 local에서 작동 시키고, Test Code로 작성하는 내용이었지만, 
 이 내용을 조금씩 변경해 가며 나의 코드로 개선해 갔다.

 결국, 이 API를 완성시키는데 필요한 주요 코드는 결국 AWS에서 제공하는 개발자 안내서였다. 
 옛날에 개발을 하면 구글링만하면 다양한 코드, 내가 필요한 코드가 모두 나왔다. 어떻게 보면 
 난 개발자가 아닌 코더였다.
하지만, 요즘 난이도가 높은(?) 개발(현재 DynamoDB와 Zoom meeting API 개발 중)을 
하면 할 수록, 회사에서 제공하는 개발 안내서나 백서, 등 공식 문서를 활용하여 
개발을 하게 되는 거 같다. 
 
 이미 해본 사람들이 블로그에 공유한 덕분에 지금 내가 개발을 하고 있지만, 요즘 느끼는 것은 
 내가 추후에 처음 개척해 나가야 할 길에 대해선 아무에게 물어볼 수 없는 망망대해에서 코드를 짜야할 
 거 같은 느낌이 들었다. 그래서 공식 문서를 읽는 연습과, 영어로 된 자료들을 절대 거부하고 멀리해선 
 안된다고 느꼈다. 

 이렇게 한발짝 나의 기술 스택이 찔끔 늘었다. 고수들의 경지에 가기엔 한참 멀었지만, 
 성장했음을 느낀다.

조금조금씩 하다보면, 클라우드 개발자가 되어 있겠지. . .
profile
고민이 많은 개발자

0개의 댓글