[Spring]4주차

[학습목표]
1. Controller - Service - Repository 학습
2. 자바를 이용해 API를 이용하는 방법
3. 스프링 스케줄러를 이용하여, 서버에게 원하는 작업을 원하는 시간에 시키는 방법

시작하기 전

개발의 핵심

분업 + 느슨한 결합

  • 분업: 각자가 맡은 바 책임을 다하면 기능이 온전히 작동
  • 느슨히 결합: 느슨히 결합하여 유연성과 확장성 가지기

Spring 3계층

  • 3계층(Controller, Service, Repository)은 분업과 느슨한 결합의 대표적인 예시
  • 스프링이 돌아가게 만드는 척추 역할
  • 각 레이어 간에는 절대 Entity를 직접 사용하지 않고, DTO를 만들어 사용

Controller

제일 바깥 쪽에서 요청을 받고, 응답을 되돌려주는 역할

Service

중간에서 구체적인 작업 순서 결정

Repository

DB와 직접 소통함으로써 자료를 생성하고, 조회하고, 변경하고, 삭제함

API Handling

  • 느슨한 결합의 대표적인 예시
  • 우리가 이용할 네이버 검색 API의 내부 로직이 어떻게 되든 상관없이, 우리는 정해진 약속대로 요구하면 정해진 결과를 받을 수 있음.
  • JSON으로 주고받는 데이터를 어떻게 JAVA로 요청하고 그 결과를 다룰 수 있는지 알아야함.

네이버 쇼핑 API

네이버 쇼핑 API 설명 문서

{
  "title": "<b>아디다스</b> 알파바운스 BB슬라이드 BA8775",
  "link": "https://search.shopping.naver.com/gate.nhn?id=24457175865",
  "image": "https://shopping-phinf.pstatic.net/main_2445717/24457175865.20201014195220.jpg",
  "lprice": "27990",
  "hprice": "",
  "mallName": "네이버",
  "productId": "24457175865",
  "productType": "1",
  "brand": "아디다스",
  "maker": "아디다스",
  "category1": "패션잡화",
  "category2": "남성신발",
  "category3": "슬리퍼",
  "category4": ""
},

JAVA로 네이버 쇼핑 API 이용하기

RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("X-Naver-Client-Id", "**********");
headers.add("X-Naver-Client-Secret", "**********");
String body = "";

HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
ResponseEntity<String> responseEntity = rest.exchange("https://openapi.naver.com/v1/search/shop.json?query=아디다스", HttpMethod.GET, requestEntity, String.class);
HttpStatus httpStatus = responseEntity.getStatusCode();
int status = httpStatus.value();
String response = responseEntity.getBody();
System.out.println("Response status: " + status);
System.out.println(response);

IntelliJ 설정하기

src > main > java > com.sparta.week04 > utils

API 설계

키워드로 상품 검색하고 그 결과를 목록으로 보여주기
GET /api/search?query=검색어
return List<ItemDto>

관심 상품 등록하기
POST /api/products

관심 상품 조회하기
GET /api/products

관심 상품에 관심가격 등록하고, 그 가격보다 낮은 경우 표시하기
PUT /api/products/{id}

3계층 설계하기

  1. Controller

    • ProductRestController: 관심 상품 관련 컨트롤러
    • SearchRequestController: 검색 관련 컨트롤러
  2. Service

    • ProductService: 관심 상품 가격 변경
  3. Repository

    여기서 DB에 저장되는 녀석은 Product 뿐이라는 점!

    • Product: 관심 상품 테이블
    • ProductRepository: 관심 상품 조회, 저장
    • ProductRequestDto: 관심 상품 등록하기
    • ProductMypriceRequestDto: 관심 가격 변경하기
    • ItemDto: 검색 결과 주고받기

org.json Java에서 json을 다루는데 도와주는 라이브러리

Models

com > sparta > week04 > models

1) 각각의 모델들, Repository, Timestamped

Product.java
상품 테이블, update의 경우 사용자가 지정한 최저가를 변경하는 update함수와 스케줄러로 변경되는 가격들을 불러오는 updateByItemDto가 있습니다.

package com.sparta.week04.models;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class Product extends Timestamped {

    // ID가 자동으로 생성 및 증가합니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    // 반드시 값을 가지도록 합니다.
    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String image;

    @Column(nullable = false)
    private String link;

    @Column(nullable = false)
    private int lprice;

    @Column(nullable = false)
    private int myprice;

    public Product(ProductRequestDto requestDto) {
        this.title = requestDto.getTitle();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.image = requestDto.getImage();
        this.myprice = 0;
    }

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }

    public void updateByItemDto(ItemDto itemDto) {
        this.lprice = itemDto.getLprice();
    }
}

Timestamped
생성날짜, 수정날짜를 만들기 위한 class

package com.sparta.week04.models;

@Getter 
// get 함수를 자동 생성합니다.
@MappedSuperclass 
// 멤버 변수가 컬럼이 되도록 합니다.
@EntityListeners(AuditingEntityListener.class) 
// 변경되었을 때 자동으로 기록합니다.

public abstract class Timestamped {
    @CreatedDate // 최초 생성 시점
    private LocalDateTime createdAt;

    @LastModifiedDate // 마지막 변경 시점
    private LocalDateTime modifiedAt;
}

ProductRepository.java

package com.sparta.week04.models;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Dto 작성하기

com > sparta > week04 > models

ProductRequestDto.java

package com.sparta.week04.models;

import lombok.Getter;

@Getter
public class ProductRequestDto {
    private String title;
    private String link;
    private String image;
    private int lprice;
}

ItemDto.java

package com.sparta.week04.models;

import lombok.Getter;
import org.json.JSONObject;

@Getter
public class ItemDto {
    private String title;
    private String link;
    private String image;
    private int lprice;

    public ItemDto(JSONObject itemJson) {
        this.title = itemJson.getString("title");
        this.link = itemJson.getString("link");
        this.image = itemJson.getString("image");
        this.lprice = itemJson.getInt("lprice");
    }
}

ProductMypriceDto.java

package com.sparta.week04.models;

import lombok.Getter;

@Getter
public class ProductMypriceRequestDto {
    private int myprice;
}

Service

com > sparta > week04 > service

ProductService.java

package com.sparta.week04.service;

...

import javax.transaction.Transactional;

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@Service // 서비스임을 선언합니다.
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional // 메소드 동작이 SQL 쿼리문임을 선언합니다.
    public Long update(Long id, ProductMypriceRequestDto requestDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 아이디가 존재하지 않습니다.")
        );
        product.update(requestDto);
        return id;
    }
    @Transactional // DB 가 업데이트 되어야한다.
    public Long updateBySearch(Long id, ItemDto itemDto) {
        Product product = productRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당 아이디가 존재하지 않습니다.")
        );
        product.updateByItemDto(itemDto);
        return id;
    }
}

Utils

com > sparta > week04 > utils

NaverShopSearch.java
NAVER API 사용하여 값을 불러오는 class

컴포넌트 등록

쉽게 얘기하면 스프링한테 권한을 주는 것! 필요할 때 알아서 써! 라고 하는 것

스프링이 해당 컴포넌트를 자동으로 가져오고 사용할 수 있게 권한을 주는 것

Repository 나 Service 모두 컴포넌트 등록이 되어있어서 스프링이 자유롭게 사용할 수 있음

스프링에 내가 마음대로 가져다 쓸 수 있는 클래스 목록이 나열되어 있음

그 목록에 있는 것들이 컴포넌트로 등록된 것들

컨트롤러, 서비스, 엔티티 등이 컴포넌트로 등록이 되어 있음

NaverShopSearch 의 경우 수동으로 등록을 해줘야 한다.

itemDto 형태는 JSON, ${} 괄호 안에 써야하는 건 문자열

JSON을 제대로 넣으면 오류가 난다.

그래서 JSON이 아닌 String 형태로 넣어준다.

org.json 패키지 설치하기

JSON을 자바에서 다루기 위해, JSONObject, JSONArray 클래스가 필요함. import해오기

1) maven central 검색
2) json 검색
3) JSON In Java 클릭
4) 숫자 가장 높은 버전 클릭
5) Gradle 탭 클릭
6) 내용 복사 후 build.gradle > dependencies 안에 붙여넣기
7) dependencies 옆 run 버튼 클릭
=> import 완료!

package com.sparta.week04.utils;

...

@Component
public class NaverShopSearch {
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "p234pvd2_wQpIjp6KlKi");
        headers.add("X-Naver-Client-Secret", "bsVXyLy17e");
        String body = "";

        HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
        ResponseEntity<String> responseEntity = rest.exchange("https://openapi.naver.com/v1/search/shop.json?query=" +  query, HttpMethod.GET, requestEntity, String.class);
        HttpStatus httpStatus = responseEntity.getStatusCode();
        int status = httpStatus.value(); // 응답 상태 코드
        String response = responseEntity.getBody();
        System.out.println("Response status: " + status);
        System.out.println(response);

        return response;
    }

    public List<ItemDto> fromJSONtoItems(String result) {
        JSONObject rjson = new JSONObject(result);
        JSONArray items = rjson.getJSONArray("items");
        List<ItemDto> itemDtoList = new ArrayList<>();

        // JSONArray 에서는 length 로 꺼냄
        for (int i = 0; i < items.length(); i++) {
            JSONObject itemJson = (JSONObject) items.get(i);
            // JSONObject itemJson = items.getJSONObject(i);
            ItemDto itemDto = new ItemDto(itemJson);
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }
}

Scheduler.java
스케줄러를 통해 매일 오전 1시에 새로운 가격을 요청해서 가져온다.

package com.sparta.week04.utils;

...

@RequiredArgsConstructor // final 멤버 변수를 자동으로 생성합니다.
@Component // 스프링이 필요 시 자동으로 생성하는 클래스 목록에 추가합니다.
public class Scheduler {

    private final ProductRepository productRepository;
    private final ProductService productService;
    private final NaverShopSearch naverShopSearch;

    // 초, 분, 시, 일, 월, 주 순서
    // cron : 시간이 맞을때 작동울 해라
    // 0~23시까지 가능 1시  0분 1초부터 1시 59분 59초까지 매초 실행 * * 1 * * *
    // 1시 0분 0초일 때 실행
    @Scheduled(cron = "0 0 1 * * *")
    public void updatePrice() throws InterruptedException { // 만약에 오류가 발생하면, 방해하는 요소가 발생했다고 오류를 보여줘라
        System.out.println("가격 업데이트 실행");
        // 저장된 모든 관심상품을 조회합니다.
        List<Product> productList = productRepository.findAll();
        for (int i=0; i<productList.size(); i++) {
            // 1초에 한 상품 씩 조회합니다 (Naver 제한: 요청이 너무 자주 오면 네이버에서 막아버림)
            TimeUnit.SECONDS.sleep(1); // 타임 단위 기준으로 초마다 한번씩 잠깐 쉬어라. = 1초에 한번 씩 for 문이 돌게 된다.
            // i 번째 관심 상품을 꺼냅니다.
            Product p = productList.get(i);
            // i 번째 관심 상품의 제목으로 검색을 실행합니다.
            String title = p.getTitle();
            String resultString = naverShopSearch.search(title);
            // i 번째 관심 상품의 검색 결과 목록 중에서 첫 번째 결과를 꺼냅니다.
            List<ItemDto> itemDtoList = naverShopSearch.fromJSONtoItems(resultString);
            ItemDto itemDto = itemDtoList.get(0);
            // i 번째 관심 상품 정보를 업데이트합니다.
            Long id = p.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}

Controller

ProductRestController.java

package com.sparta.week04.controller;

...

@RequiredArgsConstructor // final 로 선언된 멤버 변수를 자동으로 생성합니다.
@RestController // JSON 으로 데이터를 주고받음을 선언합니다.
public class ProductRestController {

    private final ProductRepository productRepository;
    private final ProductService productService;

    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() {
        return productRepository.findAll();
    }

    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto) {
        Product product = new Product(requestDto);
        return productRepository.save(product);
    }

    // 최저가 변경 API
    @PutMapping("/api/product/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) {
        return productService.update(id, requestDto);
    }
}

SearchRequestController.java
네이버 API 값 불러오는 API

package com.sparta.week04.controller;

...

@RequiredArgsConstructor // final 로 선언된 클래스를 자동으로 생성합니다.
@RestController // JSON 으로 응답함을 선언합니다.
public class SearchRequestController {

    private final NaverShopSearch naverShopSearch;

    @GetMapping("/api/search")
    public List<ItemDto> getItems(@RequestParam String query) {
        String resultString = naverShopSearch.search(query);
        return naverShopSearch.fromJSONtoItems(resultString);
    }
}

좋은 웹페이지 즐겨찾기