4-frame-photos-map / backend

네컷지도(전국 네컷사진관 정보 제공, 리뷰 공유 사이트) 백엔드 API 개발
6 stars 3 forks source link

fix : Kakao Maps API 데이터로 DB 조회시 공백 제거 및 중복 처리 로직 수정하여 비교 작업 개선 #126

Closed zuminzi closed 1 year ago

zuminzi commented 1 year ago

목적

  1. Kakao Maps API 장소 데이터와 일치하는 DB 데이터 조회 시 공백 제거 및 SQL WHERE절 수정하여 비교 작업 정확도 개선
  2. Kakao Maps API 데이터와 일치하는 DB 데이터 조회 과정에서 중복 발생 시 반환하지 않고 해당 id 캐싱

    문제 및 개선 방안

    문제 1 : 공백 차이로 인한 데이터 불일치

    • 문제
    • Kakao Maps API 장소 데이터와 일치하는 DB 데이터를 조회하는 과정에서 단어 사이 공백 차이로 인해 일치하는 지점임에도 불구하고 반환하지 못하는 경우가 있었습니다.
    • 개선 방안
    • 이를 개선하기 위해 JPA 메서드 인자인 Kakao Maps API 데이터 place_nameaddress에서 공백을 제거하고, DB 조회시에도 Replace Function으로 공백을 제거하여 일치하는 데이터를 더욱 정확하게 반환할 수 있도록 수정했습니다.
    • 공백 제거로 인해 공식 사이트 크롤링 데이터와 Kakao Maps API 데이터 일치도가 높아졌기 때문에 SQL WHERE절도 OR 연산자에서 AND 연산자로 수정했습니다.
    • 수정 전 : place_name = 'place_name_value' OR address LIKE '%address_value%'
    • 수정 후 : place_name = 'place_name_value' AND address LIKE '%address_value%'

      문제 2 : 지점 반환 우선순위(ShopMatchPriority)

    • 문제
    • Kakao Maps API에서 제공하는 장소 정보에는 상세주소를 제외한 도로명주소지번 주소만 제공하고 있기 때문에 주소 중복 데이터가 존재합니다.
    • 따라서 Kakao API 데이터와 일치하는 데이터를 찾기 위해 DB를 조회하는 과정에서 반환 데이터 중복이 발생할 경우, 주로 주소로 인한 중복이 발생하여 아래와 같이 우선순위를 매겨 반환했었습니다.
    • 기존 우선순위 1 : Kakao API 데이터 지점명과 DB 데이터 지점명 일치여부
    • 기존 우선순위 2(모든 데이터가 우선순위 1을 만족하지 못하는 경우) : Kakao API 데이터 브랜드명을 DB 데이터 지점명이 포함하는지 여부
    • 그러나 위 기준은 우선순위 2를 만족하는 데이터가 1개 이상인 경우에도 무조건 첫번째 데이터를 반환하여 제대로된 Kakao API shop - DB shop Match를 하지 못하고 있습니다.
    • 개선 방안
    • DB에서 주소 컬럼이 중복인 데이터 확인하여 지점 조회 API 테스트 결과, 문제 1을 해결하면서 모두 해소되었습니다.
    • 따라서 중복 발생 시 반환하지 않고, Redis에 shop-id 캐시하여 나중에 확인할 수 있도록 코드 수정했습니다.

      문제 상세

    • 문제1과 문제2로 인해춘천 인생네컷으로 지점 조회 시 상세주소 제외 도로명주소가 동일한 id 18765, 18766, 18830 모두 18830으로 잘못 반환 되고 있었습니다. image
Hibernate: 
    select
        distinct shop0_.id as id1_6_,
        shop0_.create_date as create_d2_6_,
        shop0_.modify_date as modify_d3_6_,
        shop0_.address as address4_6_,
        shop0_.brand_id as brand_id9_6_,
        shop0_.favorite_cnt as favorite5_6_,
        shop0_.place_name as place_na6_6_,
        shop0_.review_cnt as review_c7_6_,
        shop0_.star_rating_avg as star_rat8_6_ 
    from
        shop shop0_ 
    where
        shop0_.place_name=? 
        or shop0_.address like ? escape ?
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [인생네컷 춘천터미널로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [%강원 춘천시 지석로 80%]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [CHAR] - [\]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18765]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([create_d2_6_] : [TIMESTAMP]) - [2023-05-01T22:29:24.531879]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([modify_d3_6_] : [TIMESTAMP]) - [2023-05-01T22:29:24.531879]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([address4_6_] : [VARCHAR]) - [강원 춘천시 지석로 80]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([brand_id9_6_] : [BIGINT]) - [1]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([favorite5_6_] : [INTEGER]) - [0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([place_na6_6_] : [VARCHAR]) - [인생네컷 춘천 터미널로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([review_c7_6_] : [INTEGER]) - [0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([star_rat8_6_] : [DOUBLE]) - [0.0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18766]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([create_d2_6_] : [TIMESTAMP]) - [2023-05-01T22:29:24.738638]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([modify_d3_6_] : [TIMESTAMP]) - [2023-05-01T22:29:24.738638]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([address4_6_] : [VARCHAR]) - [강원 춘천시 지석로 80]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([brand_id9_6_] : [BIGINT]) - [1]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([favorite5_6_] : [INTEGER]) - [0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([place_na6_6_] : [VARCHAR]) - [인생네컷 춘천 명동로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([review_c7_6_] : [INTEGER]) - [0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([star_rat8_6_] : [DOUBLE]) - [0.0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18830]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([create_d2_6_] : [TIMESTAMP]) - [2023-05-01T22:29:27.425939]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([modify_d3_6_] : [TIMESTAMP]) - [2023-05-01T22:29:27.425939]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([address4_6_] : [VARCHAR]) - [강원 춘천시 지석로 80]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([brand_id9_6_] : [BIGINT]) - [1]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([favorite5_6_] : [INTEGER]) - [0]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([place_na6_6_] : [VARCHAR]) - [인생네컷 춘전 CGV로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([review_c7_6_] : [INTEGER]) - [0]
 TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([star_rat8_6_] : [DOUBLE]) - [0.0]
INFO 1 --- [io-8080-exec-51] c.i.f.domain.shop.service.ShopService    : Matched: DB shop (인생네컷 춘천 터미널로드점 - 강원 춘천시 지석로 80), Kakao API shop (인생네컷 춘천터미널로드점 - 강원 춘천시 지석로 80 - 강원 춘천시 퇴계동 1017)
Hibernate: 
    select
        distinct shop0_.id as id1_6_,
        shop0_.create_date as create_d2_6_,
        shop0_.modify_date as modify_d3_6_,
        shop0_.address as address4_6_,
        shop0_.brand_id as brand_id9_6_,
        shop0_.favorite_cnt as favorite5_6_,
        shop0_.place_name as place_na6_6_,
        shop0_.review_cnt as review_c7_6_,
        shop0_.star_rating_avg as star_rat8_6_ 
    from
        shop shop0_ 
    where
        shop0_.place_name=? 
        or shop0_.address like ? escape ?
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [인생네컷 춘전CGV로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [%강원 춘천시 지석로 80%]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [CHAR] - [\]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18765]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18766]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18830]
INFO 1 --- [io-8080-exec-51] c.i.f.domain.shop.service.ShopService    : Matched: DB shop (인생네컷 춘천 터미널로드점 - 강원 춘천시 지석로 80), Kakao API shop (인생네컷 춘전CGV로드점 - 강원 춘천시 지석로 80 - 강원 춘천시 퇴계동 1017)
Hibernate: 
    select
        distinct shop0_.id as id1_6_,
        shop0_.create_date as create_d2_6_,
        shop0_.modify_date as modify_d3_6_,
        shop0_.address as address4_6_,
        shop0_.brand_id as brand_id9_6_,
        shop0_.favorite_cnt as favorite5_6_,
        shop0_.place_name as place_na6_6_,
        shop0_.review_cnt as review_c7_6_,
        shop0_.star_rating_avg as star_rat8_6_ 
    from
        shop shop0_ 
    where
        shop0_.place_name=? 
        or shop0_.address like ? escape ?
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [인생네컷 춘천명동로드점]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [%강원 춘천시 지석로 80%]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [CHAR] - [\]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18765]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18766]
TRACE 1 --- [io-8080-exec-51] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_6_] : [BIGINT]) - [18830]
INFO 1 --- [io-8080-exec-51] c.i.f.domain.shop.service.ShopService    : Matched: DB shop (인생네컷 춘천 터미널로드점 - 강원 춘천시 지석로 80), Kakao API shop (인생네컷 춘천명동로드점 - 강원 춘천시 지석로 80 - 강원 춘천시 퇴계동 1017)
ahah525 commented 1 year ago

@zuminzi

질문

1. 카카오 응답과 DB 응답을 지점명, 주소로 비교할 때 DB에서 조회된 결과가 2개 이상인 경우 무조건 null을 반환하는 이유가 궁금합니다. (+ Redis 에 shopId를 기록하는 목적이 구체적으로 무엇인지 설명해주실 수 있나요?)

/**
     * 지점명 일치여부나 주소명 포함여부로 비교하여 Kakao API Shop과 일치하는 DB Shop 객체 반환하는 메서드입니다.
     * @param placeName 카카오 API 지점명
     * @param addresses 카카오 API 도로명주소, 지번주소
     * @return DB Shop
     */
    @Transactional(readOnly = true)
    public Shop compareWithPlaceNameOrAddress(String placeName, String... addresses) {
        for (String address : addresses) {
            List<Shop> matchedShops = shopRepository.findByPlaceNameOrAddressIgnoringSpace(
                    Util.removeSpace(placeName),
                    Util.removeSpace(address)
            );
            if (matchedShops.size() == 1) {
                return matchedShops.get(0);
            } else if (matchedShops.size() > 1){
                matchedShops.stream().map(Shop::getId).forEach(this::cacheDuplicateShopId);
                return null;
            }
        }
        return null;
    }
zuminzi commented 1 year ago

답변

결론부터 말씀드리자면, 승연님이 말씀하신 대로 중복 데이터 처리 코드 수정이 필요할 것 같습니다.

1. 카카오 응답과 DB 응답을 지점명, 주소로 비교할 때 DB에서 조회된 결과가 2개 이상인 경우 무조건 null을 반환하는 이유

2. Redis 에 shopId를 기록하는 목적

3. 1번을 구현한 코드의 문제

결론

위에서 말씀해주신 케이스 1(같은 주소에 다른 지점이 있는 경우)과 케이스 2(같은 주소에 동일한 지점 정보가 DB에 2개 이상 저장되어 있는 경우)에 대해 올바른 데이터를 반환하도록 수정하겠습니다.

현재 고려 중인 수정 방안은 다음과 같습니다. 그러나 더 적절한 방법이 있는지 고려해보겠습니다. 😁