H2 DB → MySql 로 프로젝트 환경 만들기

JungWooLee·2022년 8월 21일
2

SpringBoot ToyProject

목록 보기
8/14

이어서 하기

음식점 정보를 스크래핑하여 View 단에서 볼 수 있도록 작업을 합니다

기존 프로젝트에서는 H2 DB와 JPA 를 사용하여 따로 ddl 작업을 거치지 않았는데 실제 운영되는 서비스라면 이러한 작업은 매우 위험하며 수정할 필요가 있기에 AWS 에 배포를 하기전 mySql 로 DB를 옮기는 작업을 선행합니다

또한 앞에서 구한 사용자의 좌표값을 통하여 Service에서 비즈니스 로직을 수행할 수 있도록 수정하는 작업을 거칩니다


1. Mysql 로 옮기기

  • 기존에 사용하던 h2 DB는 사용하지 않으니 디펜던시에서 제거해준뒤 build를 업데이트 해줍니다

  • MySql 이 설치되어 있으며 User 가 있다는 가정하에 진행합니다 (이에 관련된 포스팅은 참고 자료가 많으니 생략)

CREATE DATABASE Lunch_Recommend default CHARACTER SET UTF8;

CREATE USER admin@localhost IDENTIFIED BY '1234';
GRANT ALL PRIVILEGES ON Lunch_Recommend.* TO admin@localhost;
  • 새롭게 데이터베이스를 생성하고 사용자를 생성합니다. (Admin으로서 모든 권한을 풀어줍니다)
spring.datasource.url=jdbc:mysql://localhost:3306/Lunch_Recommend?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
spring.datasource.username=admin
spring.datasource.password=1234
spring.jpa.hibernate.ddl-auto=create
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver


#hibernate ??
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true

# jpa ??
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb

# ?? ???
spring.session.store-type=jdbc

allowPublicKeyRetrieval

MySql 8.0 버전부터는 allowPublicKeyRetrieval 설정을 해주어야 Public key retrieval is not allowed 에러를 피할 수 있습니다.

< 속성 >

  • useSSL: DB에 SSL로 연결
  • allowPublicKeyRetrieval: 서버에서 RSA 공개키를 검색하거나 가져와야하는지
  • h2 DB에서는 ddl-auto 가 기본적으로 적용되는 것으로 보였지만 Mysql로 바꾸면서는 따로 설정해두어야 합니다

  • 실제 서비스시에는 ddl-auto 설정을 풀고 새롭게 DB에 저장하도록 수정을 거칩니다

  • 아직 배포전에는 RDS 를 사용하지 않을 것이기 때문에 따로 스프링 세션에 대한 테이블을 만들어주어야 합니다

세션 관리 테이블 생성

spring session 디펜던시를 적용했다면 schema-h2.sql 를 검색하면 다음과 같은 파일이 저장되어 있을것입니다.

이는 H2 데이터베이스 문법이 적용된 스키마이므로 MySql version으로 바꾸어 줍니다

테스트

문제없이 잘 작동하는 것을 볼 수 있습니다

  • BaseTimeEntity가 적용되지 않는 문제를 발견하였습니다
@SpringBootApplication
@EnableJpaAuditing
public class LunchSolverApplication {

    public static void main(String[] args) {
        SpringApplication.run(LunchSolverApplication.class, args);
    }
}

생각해보니 EnableJpaAuditing 을 적용하지 않아 적용되지 않는 문제였습니다 (스프링 부트 1.x를 쓴다면 별도로 Hibernate 5.2.10 버전 이상을 사용하도록 설정이 필요)


2. 레스토랑 정보 저장하기

1. SERVICE 수정하기

	@Test
    @DisplayName("카테고리별 100개 스크래핑하여 DB에 담기")
    public void getRestaurantData_v2 () throws Exception {

        for (RestaurantType type : RestaurantType.values()) {
            // 카테고리내의 모든 음식들을 크롤링
            String url = "/graphql";
            String _url = HOST_v2+url;
            GetRestaurantRequest request = GetRestaurantRequest.builder()
                    .x(x)
                    .y(y)
                    .bounds("126.9738873;37.5502692;126.9980272;37.5696434")
                    .query("음식점")
                    .type(type)
                    .build();
            String jsonOperation = naverUtility.getRestaurants(request);
            HttpHeaders httpHeaders = utility.getDefaultHeader();

            HttpEntity requestMessage = new HttpEntity(jsonOperation,httpHeaders);
            ResponseEntity response = restTemplate.exchange(
                    _url,
                    HttpMethod.POST,
                    requestMessage,
                    String.class);
            List<Restaurant> entities = new ArrayList<>();

            JSONArray datas = new JSONArray(response.getBody().toString());
            datas.getJSONObject(0);

            JSONArray items = datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").getJSONArray("items");
            int total = Integer.parseInt(datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").getString("total"));
            int maxCnt = total<100? total:100;
            for (int i = 0; i < maxCnt; i++) {
                GetRestaurantResponse mapped_data = gson.fromJson(items.getString(i),GetRestaurantResponse.class);
                //1. first map with entity : 엔티티와 매핑하기전 validation을 거친다
                Restaurant restaurant = Restaurant.builder()
                        .id(Long.parseLong(mapped_data.getId()))
                        .address(mapped_data.getAddress())
                        .category(mapped_data.getCategory()==null?"없음": mapped_data.getCategory())
                        .imageUrl(mapped_data.getImageUrl()==null?"":URLDecoder.decode(mapped_data.getImageUrl(),"UTF-8"))
                        .name(mapped_data.getName())
                        .distance(utility.stringToLongDistance(mapped_data.getDistance()))
                        .businessHours(mapped_data.getBusinessHours())
                        .visitorReviewScore(mapped_data.getVisitorReviewScore()==null? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore()))
                        .saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount()))
                        .bookingReviewScore(mapped_data.getBookingReviewScore())
                        .restaurantType(type)
                        .build();
                entities.add(restaurant);
            }
            restaurantsRepository.saveAll(entities);

        }
        List<Long> ids = restaurantsRepository.findAllreturnId();
        for (Long id : ids) {
            System.out.println(id);
        }
    }

기존 서비스에서 사용되던 비즈니스 로직입니다.

  • GIVEN 으로 REQUEST 를 받아와 카테고리별로 음식점 100곳을 DB에 추가합니다
  • TYPE의 경우 따로 매핑하여 카테고리를 추가한뒤 REQUEST를 보내 스크래핑을 진행합니다

수정 후

  • REQUEST 에서 X,Y, BOUNDARY 를 갖는 상태에서 빌더를 통하여 다시 REQUEST 를 생성하여 요청을 보내도록 합니다
  • @Transactional 어노테이션을 통하여 db 저장 도중 에러 발생시 rollback 되도록 합니다
	@Transactional
    @Override
    public void getRestaurantData(GetRestaurantRequest getRestaurantRequest) throws UnsupportedEncodingException {
        for (RestaurantType type : RestaurantType.values()) {
            // 카테고리내의 모든 음식들을 크롤링
            GetRestaurantRequest request = GetRestaurantRequest.builder()
                    .x(getRestaurantRequest.getX())
                    .y(getRestaurantRequest.getY())
                    .bounds(getRestaurantRequest.getBounds())
                    .query("음식점")
                    .type(type)
                    .build();
            log.info("Service 에서 모델 : "+request);

            String _url = HOST_v2;
            String jsonOperation = naverUtility.getRestaurants(request);
            HttpHeaders httpHeaders = utility.getDefaultHeader();

            HttpEntity requestMessage = new HttpEntity(jsonOperation, httpHeaders);
            ResponseEntity response = restTemplate.exchange(
                    _url,
                    HttpMethod.POST,
                    requestMessage,
                    String.class);
            List<Restaurant> entities = new ArrayList<>();

            JSONArray datas = new JSONArray(response.getBody().toString());
            datas.getJSONObject(0);

            JSONArray items = datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").getJSONArray("items");
            int total = Integer.parseInt(datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").get("total").toString());
            int maxCnt = total < 100 ? total : 100;
            for (int i = 0; i < maxCnt; i++) {
                GetRestaurantResponse mapped_data = gson.fromJson(items.get(i).toString(), GetRestaurantResponse.class);
                //1. first map with entity : 엔티티와 매핑하기전 validation을 거친다
                Restaurant restaurant = Restaurant.builder()
                        .id(Long.parseLong(mapped_data.getId()))
                        .address(mapped_data.getAddress())
                        .category(mapped_data.getCategory() == null ? "없음" : mapped_data.getCategory())
                        .imageUrl(mapped_data.getImageUrl() == null ? "" : URLDecoder.decode(mapped_data.getImageUrl(), "UTF-8"))
                        .name(mapped_data.getName())
                        .distance(utility.stringToLongDistance(mapped_data.getDistance()))
                        .businessHours(mapped_data.getBusinessHours())
                        .visitorReviewScore(mapped_data.getVisitorReviewScore() == null ? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore()))
                        .saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount()))
                        .bookingReviewScore(mapped_data.getBookingReviewScore())
                        .restaurantType(type)
                        .build();
                entities.add(restaurant);
            }
            log.info("saving in service succeed");
            restaurantsRepository.saveAll(entities);
        }
    }

2. Controller

  • view 에서 AJAX를 통하여 restAPI 호출시 request 에 매핑하여 추가적으로 boundary 를 set 한 이후 service 단에서 비즈니스 로직을 수행할 수 있도록 합니다
 	@PutMapping("/getRestaurantData")
    public void getRestaurantData(GetRestaurantRequest request){
        try {
            String bounds = String.format("%s;%s;%f;%f",
                    request.getX(),
                    request.getY(),
                    Double.parseDouble(request.getX())+0.0241399,
                    Double.parseDouble(request.getY())+0.0193742);
            log.info("bounds : "+bounds);
            request.setBounds(bounds);
            restaurantService.getRestaurantData(request);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
  • 범위의 경우 네이버 graphQL에서 실제 적용되어 있는 범위값을 계산하여 설정되도록 하였습니다

3. View

	function getFullAddress(longtitude, latitude) {

        var AddressRequest={
            x : longtitude,
            y : latitude
        };

        $.ajax({
            url: "/user/api/getFullAddress",
            data: AddressRequest,
            type:"POST",
        }).done(function (fragment) {
            console.log('done');
            $("#fullAddress").replaceWith(fragment);
            getRestaurantData(longtitude, latitude);
        });
    }

    function getRestaurantData(longtitude, latitude){
        var GetRestaurantRequest={
            x : longtitude,
            y : latitude
        };

        $.ajax({
            url: "/user/api/getRestaurantData",
            data: GetRestaurantRequest,
            type:"PUT",
        }).done(function (fragment) {
            console.log('input succeed');
        });
    }
  • 위도, 경도의 경우 사용자의 위치 동의가 없다면 구할 수 없기 때문에 위, 경도 확인이 된 이후에 주변 맛집을 저장하도록 구성하였습니다

4. 결과 확인

VIEW

SERVICE

DATABASE

0개의 댓글