[SPRING_입문]/Code Review

[Localhost] 나만의 셀렉샵 만들기! (A-Y)

Code_Otaku 2022. 6. 29. 15:25

http://spring.spartacodingclub.kr/

 

00만의 셀렉샵

관심상품을 선택하고, 최저가 알림을 확인해보세요!

spring.spartacodingclub.kr

이러한 방식으로 작동되는 웹 어플리케이션을 하나 만들어보겠다고 했다.

여러분은 분명 저 링크를 타고 들어가지 않을테니.. 어떠한 웹 사이트인지 필자가 간단하게 설명해야 겠다.

 

1) 관심상품 조회

2) 조회결과를 목록으로 보여주기

3) 관심상품 등록

4) 관심상품에 원하는 가격 (최저가) 등록

5) 최저가 상품이 [Naver Shopping]에 올라오면 자동으로 목록 생성

 

이 정도로 기능을 요약할 수 있겠다.

그래도 감이 잘 오지 않는다면 직접 링크를 타고 들어가서 성능을 테스트 해봐라.

쪼물닥 거리다 보면 CREATE / READ / UPDATE / DELETE 네 가지 기능이 전부 다 들어가 있음을 확인할 수 있을 것이다.

 

아! 가장 중요한 사실.

상품의 가격정보는 뭘 기반으로 불러오는 거지?

그것을 위해서 네이버에 별도의 API를 따로 만들어 놓았다.

사용자가 무엇을 검색하던, 하늘이 두 쪽 나는 일이 있어도!

네이버 쇼핑샵에 올라와 있는 상품 관련 데이터베이스를 정확하게 가지고 오는 것이다.

 

필자는 새벽 5시를 기준으로 매일 리스트가 갱신되는 어플리케이션을 만들어 볼 것이다.

그것을 위해 사전에 밑작업을 해놓은 소스코드는 다음과 같고..

public String search() {
    RestTemplate rest = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Naver-Client-Id", "사용자가 입력한 ID");
    headers.add("X-Naver-Client-Secret", "사용자가 입력한 PW");
    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);

    return response;

툭 까놓고 말해서 이 코드가 검색엔진 기능을 수행할 것이다.

참고로 이 코드는 필자가 직접 한땀, 한땀..

작업한 것들이 아니다.

예전에 소개해드린 ARC ([2주차] 개발일지 (2-10) ARC (Advanced REST Client) (tistory.com))라는 어플을 기억하는가?

 

API를 만들면서 발급받은 아이디비밀번호를 헤더에 입력한 다음..

GET 요청을 날린다면 200으로 응답이 되돌아올 것이다.

거기에서 코드스니펫 탭으로 들어가 보면 각 언어에 맞는 소소코드들이 자동으로 컴파일 되어있다.

그것을 긁어오면 아주 유용하게 쓸 수 있는 것이다.

[ARC]

요런 식으로 말이다.

세상 참 좋아졌다..

 

암튼!

 

본격적으로 코드 리뷰를 함에 앞서서 먼저 프로젝트를 설계해 보자.

사실 이미 개발일지에 올라와 있는 내용이지만..

필자의 리마인드를 위해 다시 설계해 보도록 하겠다.

 

코드리뷰인 만큼 글이 다소 늘어지고, 내용도 지루할 것이다.

쓰는 사람도 죽을 맛인데 보는 여러분은 오죽할까?

쓰다가 영 아니다 싶으면 글을 세 번 정도 나누어 올리도록 하겠다.

 

그럼 프로젝트 설계부터 시작해보자!


1. Design & Build up My Project

Function Method URL Return Type
관심상품 검색 / 결과 조회 GET /api/search?query=검색어 List<ItemDto>
관심상품 등록 POST /api/products Product
관심상품 조회 GET /api/products List<Product>
최저가 등록 / 결과 조회 PUT /api/products{id} id

DB 서버에서 테이블은 분명 저런 식으로 생성이 될 것이다.

표현방식만 다를 뿐이지 JSON 역시 크게 다를 것은 없다.

 

사실 웹 페이지는 아무리 어려워봤자 크게 세 가지 계층으로 나뉘어진다.

 

클라이언트 쪽에서 요청을 받고 응답을 되돌려주는 Controller

클라이언트와 서버 사이에서 구체적인 작업 순서를 결정 해주는 Service

DB 서버와 직접 소통함으로서 데이터를 CRUD 해주는 Repository

 

저 세 놈을 중심으로 필요한 기능에 따라 하위 브랜치를 쭉, 쭉, 쭉 생성해주는 식이다.

이해를 돕기 위해 맥도날드를 예로 들어보겠다.

 

Repository는 주방으로 비유할 수 있다.

매장방문이 되었건, 드라이브 스루가 되었건, 딜리버리가 되었건 하는 역활은 달라지지 않는다.

빵을 삶고, 패티를 굽고, 감튀를 튀기는 등등..

단지 주문량 (Traffic)이 늘어나면 그만큼 겁나 분주해지겠지?

 

Service는 Controller와 Repository의 중간 단계쯤 놓인 녀석이다. 

필요하면 주방으로 투입될 수도 있고, 카운터도 볼 수 있을테고..

평소에는 메뉴를 포장하는 역활을 주로 수행할 수도 있겠지.

 

Controller는 손님 (클라이언트)와 가장 밀접한 관계에 놓인 녀석이다.

손님은 매장을 직접 방문해서 카운터를 통해 주문을 할 수도 있고..

포스트 코로나 시대로 인해 완벽하게 자리잡은 키오스크를 통해서도 주문이 가능하다.

드라이브 스루나, 맥 딜리버리를 이용할 수도 있겠지?

어찌 되었건 주문 (Request)을 접수하고, 그것을 주방으로 전달하고..

완성된 메뉴를 다시 손님에게 전달 (Response)하고..

그런 총체적인 역활을 수행하는 녀석이다.

 

이 정도면 각 계층이 각각 어떤 기능을 담당하는지 확실하게 와닿지?

IDE에서는 각각 패키지로 구성해 필요한 하위 브랜치를 생성해 나갈 예정이다.

그렇다면 이번에는 패키지별로 어떠한 구성요소가 들어가 있는지 확인해보자.

 

1-1)  Controller

  • ProductRestController: 관심 상품 관련 REST Controller
  • SearchRequestController: 상품 검색 관련 Controller

 

1-2) Service

  • ProductService: 관심상품의 가격을 변경 (Update / Put)

 

1-3) Repository

  • Product: 관심상품 Table (서버 DB에 저장되는 유일한 클래스)
  • ProductRepository: 관심상품 조회 / 저장
  • ProductRequestDto: 관심상품 등록
  • ProductMyServiceRequestDto: 관심상품 가격 변경
  • ItemDto: 상품 검색결과 Request / Response

 

이 정도면 프로젝트 설계는 충분한 것 같다.

코드의 양이 많고, 복잡하기 때문에 이 정도의 가이드라인은 필요하다고 생각했다.

필자가 설계한 컨셉 디자인을 충분히 숙지한 다음

앞으로의 내용을 따라와주기 바란다.

우선 여기서 끊도록 하겠다!

 


2. 관심 상품을 조회해보자!

[관심상품 조회]

딱!

이 화면을 사용자에게 제공해주려는 것이다.

물론 지금은 서버를 굴려주기 위한 3Layer 중 하나를 구축하는 단계이기 때문에..

클라이언트 화면이 직접적으로 나타나지는 않는다.

 

다만 API 테스트를 통해 원하는 기능이 잘 구현이 됐는지는 확인할 수 있다.

필요한 소스코드를 살펴보도록 하겠다.

 

2-1) Time Stamped

@Getter // Get 메서드를 자동으로 생성: Read
@MappedSuperclass // createAt, modifiedAt 이 상속받은 클래스에서 멤버변수 (컬럼)이 되어야 한다.
@EntityListeners(AuditingEntityListener.class) // class 가 변경되었을 때 자동으로 recording

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

    @LastModifiedDate
    // 최종 변경 시점
    private LocalDateTime modifiedAt;
}

타임 스탬프!

아 시간에 도장을 찍겠다고~ ㅋㅋㅋㅋ

진짜읾..

그렇게 이해하는 게 가장 와닿지 않겠는가?

 

데이터의 생성시간과 변경시간마다 도장을 꽝! 꽝! 찍어주는 셈이지 모..

이 녀석은 여러 클래스에서 상속받아서 써야하는 공중변소 같은 느낌이기 때문에 추상클래스로 만들어줬다.

 

필요한 어노테이션은 Getter / MappedSuperClass / EntityListners() 가 있다.

사실 스프링 부트는 안그래도 불편한 언어인 자바에서 편의성을 극대화 시켜준 프레임워크이기 때문에..

코드 그 자체의 내용도 중요하지만 적재적소에 어노테이션을 선언해주는 것이 더 중요하다.

각 기능들은 코멘트를 참고하기 바란다.

 

2-2) My Select Shop Application

@SpringBootApplication // Hey, Java! I will use Spring Boot Framework!
@EnableJpaAuditing // Enable TimeStamped

public class MyselectshopApplication {

    public static void main(String[] args) {

        SpringApplication.run(MyselectshopApplication.class, args);
    }

}

 프로젝트를 생성하면 기본으로 Main 패키지에 깔려있는 녀석이다.

사실 스프링 부트 서버를 가동시킬 때 바로 이 녀석을 Run 상태로 돌려주는 것이다.

필요한 어노테이션만 살펴보자

 

@SpringBootAplication

내가 선언 해준거 아니다.

기본으로 선언된 어노테이션이다.

해당 자바 클래스가 스프링 부트로 가동되어야 함을 자바에게 주지시켜주는 기능을 수행한다.

 

@EnableJpaAuditing

Enable Jpa Auditing...

JPA 기능 수행이 가능하게끔 선언해준 녀석이다.

테이블을 생성하고, 조회하고, 수정하고, 조회하고..

이런 DB 관련 작업을 정해진 규칙에 따라 수행하는 ORM이 바로 JPA이다.

이 녀석보다 조금 더 오래된 툴인 MyBatis가 같은 기능을 수행한다.

해당 프로젝트에서는 시간 자동변경 (Time Stamped)이 가능하도록 도와준다.

 

2-3) Product

@Getter // Get Method 자동 생성
@NoArgsConstructor // 기본생성자
@Entity // 클래스가 DB 서버에서 테이블의 역할 수행함을 Java 에게 주지

public class Product extends Timestamped {
    // Table No: Long id
    // 상품 이름: String title
    // 상품 이미지: String image
    // 상품 링크: String link
    // 상품 최저가: String lprice
    // 설정 최저가: String myprice

    @Id // Table No.
    @GeneratedValue(strategy = GenerationType.AUTO) // Id++;
    private Long id;

    @Column(nullable = false)
    // This Table Column must have values!
    private String title;

    @Column(nullable = false)
    // This Table Column must have values!
    private String image;

    @Column(nullable = false)
    // This Table Column must have values!
    private String link;

    @Column(nullable = false)
    // This Table Column must have values!
    private int lprice;

    @Column(nullable = false)
    // This Table Column must have values!
    private int myprice;
}

사실상 이번 프로젝트의 몸통같은 클래스이다.

DB에 생성해줘야 할 데이터를 직접적으로 담고 있는 녀석이기 때문이다.

[나만의 셀렉샵] 페이지에 들어갔을 때, 사용자에게 가장 먼저 노출되는 화면을 다시 한 번 살펴보자.

당장 눈에 보이는 것만 하더라도

최소한 4개의 정보는 자바에서 멤버변수로 선언되어야 할 것이다.

 

상품의 이름 관련 정보를 담고 있는 title

상품의 이미지 링크를 전송시켜주는 image

상품의 링크를 전송시켜주는 link

상품의 최저가 관련 정보를 담고 있는 lprice

거기에 더해 내가 설정한 최저가 정보를 담고 있는 myprice

 

이 정도 멤버변수를 각각 자바에서 생성해줬다.

 

@Entity 어노테이션을 놓지지 말기 바란다.

이 녀석을 선언해줌으로서 상품 관련 DB가 테이블이나 JSON 형식으로 만들어지는 것이다.

DB 서버와 가장 가까이에 위치한 녀석이라고 이해하면 쉽다.

 

나머지는 코멘트를 참고하기 바란다.

 

2-4) Product Repository

// 프로젝트에서 멤버변수의 생성, 조회, 삭제 기능을 담당할 Java Interface

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

public interface ProductRepository extends JpaRepository<Product, Long> {
    // @Entity
    // public class Product
    // extends Timestamped
}

이 녀석은 일반적인 자바 클래스가 아닌 인터페이스이다.

특히 Jpa Repository 클래스를 상속받아 쓰고 있는 것이 인상적이다.

Product 클래스를 참조해 DTO 클래스들에게 필요한 정보를 부왘! 뿌려주기 위한 녀석이다.

이 녀석을 기반으로 멤버변수들을 생성하고, 조회하고, 삭제할 수 있는 것이다.

소스코드 자체는 별 거 없지만 그 기능을 잘 기억해주길 바란다.

 

2-5) Product REST Controller

@RequiredArgsConstructor // final 로 선언된 멤버변수 자동생성
@RestController // Data Request-Response as JSON Type

public class ProductRestController {

    private final ProductRepository productRepository;
    // POST, PUT, DELETE 기능을 수행하는 ProductRepository Interface 상속
    // Hey, REST CON! This is essentially required method!

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

거두절미하고 말하자면 이 녀석은 아직 완성된 클래스가 아니다.

REST가 뭐라고 했지?

POST / GET / PUT / DELETE!

이 4가지 기능에 자동으로 응답해주기 위해 만든 클래스인 것이다.

그걸 위해서 @RestController 라고 어노테이션을 선언해준 것이고..

그것도 JSON 타입으로!

 

지금은 관심상품을 조회하는 기능만 우선적으로 구현해 놓은 상태이기 때문에

@GetMapping 이하 생성자만 만들어줬다.

Product 클래스의 모든 멤버변수들을 배열 형태로 받아와 전부 조회하겠다는 뜻이다.

 

어떻게???

이렇게.. ㅎㅎㅎ

 

아직 관심상품을 등록 (POST) 해주지 않았기 때문에

텅 비어있는 배열만 날라올 것이다.

그래도 응답이 제대로 이루어졌음을 뜻하는 200번 코드가 같이 날라왔다.

 

다음으로는 관심상품을 등록해주기 위한 절차를 밟아도 되는지에 대한 여부를 지금 이 자리에서 여쭐만한 기회를 제가 감히 가져도 되는지의 여부를...


3. 관심상품을 등록해보자!

사용자에게 직접적으로 노출되는 클라이언트 화면은 다음과 같다.

저번 시간에는 사용자가 '모아보기 (GET)' 탭을 눌렀을 때,

등록한 관심상품의 목록이 리스트로 나타나는 코드를 구현해봤다.

 

하지만 아직까지 관심상품을 '등록 (POST)' 해주는 코드가 없기 때문에

API에서는 텅 비어있는 상태의 배열만 200번 코드와 함께 JSON 형식으로 날라왔다.

 

이번 시간에는 사용자가 원하는 상품을 직접 등록까지 해주는 과정을 살펴볼 것이다.

딱 봐도 '담기' 처럼 생긴 아이콘을 사용자가 클릭하였을 때,

최저가 설정까지 할 수 있도록 도와주는 것이다.

 

그 전에 키워드 (Keyword)로 상품을 조회하는 기능이 있으면 좋겠지만..

우선은 등록하기 기능을 먼저 구현해보자.

기능 구현을 위해 사용한 소스코드를 순차적으로 살펴보자!

 

3-1) ProductRequestDto

// 관심상품을 등록하기 위해 필요한 정보를 몰고 다니는 DTO Class
// Required Member Variable: 상품의 품명, 이미지, 최저가, 네이버 쇼핑 링크

import lombok.Getter;

@Getter

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

요 놈은 무엇에 쓰는 물건인고?

필자가 아주 친절하게 코멘트를 달아놨다.

상품을 등록하기에 앞서 필요한 정보를 싣고 다니면서,

적재적소에 내려주는 역할을 담당하는 녀석이 바로 DTO (Data Transfer Object) 클래스이다.

 

클라이언트 화면 상에서도 한 눈에 보이는 정보들을 차근차근 살펴보자.

 

관심상품의 제목 (title)

관심상품의 이미지 (image)

관심상품의 가격 (lprice)

관심상품의 링크 (link)

 

이것들을 자바에서 일일이 멤버변수로 선언한 것 뿐이다.

왜?

관심상품을 등록하기 위한 필수 정보들이니까!

private으로 접근을 제한해줬기 때문에 @Getter를 선언해줬다.

 

필자도 이렇게 포스팅을 하면서 머리 속에서 내용을 정리하고 있으니

독자 여러분들도 클라이언트 화면과 대조해가면서 코드를 살펴보기 바란다.

어렵지 않다.

 

3-2) ProductMyPriceRequestDto

// 사용자가 설정한 관심상품의 가격을 '변경'하기 위한 정보를 몰고 다니는 DTO 클래스

import lombok.Getter;

@Getter

public class ProductMypriceRequestDto {
    private int myprice;
}

이 녀석 역시 정보를 화물차처럼 싣고 다니는 DTO 클래스이다.

사용자가 원하는대로 관심상품의 가격을 '변경 (PUT)' 하였을 경우,

자바에서는 그것을 myprice 변수로 선언해줬다.

말 그대로 최저가 (lprice)와는 별도로 내 가격이 되는 셈!

사실 조금 이따가 나와도 되는 내용이지만,

뒤 이어 다뤄볼 소스코드와 연결이 되어 있어서

미리 조금 소개해본다.

 

3-3) Product [Modified]

// 사용자의 관심상품 등록 (POST)
public Product(ProductRequestDto requestDto){
    this.title = requestDto.getTitle();
    this.link = requestDto.getLink();
    this.image = requestDto.getImage();
    this.lprice = requestDto.getLprice();
    this.myprice = 0;
    // 해당 칼럼 역시 값이 반드시 존재해야 하므로 default 값 (0)이라도 설정을 해줘야 한다.
    // lprice < myprice 일 때만 최저가 정보가 떠야 한다.
}

Modified 라고 분명 구분을 해줬다.

이전에 관심상품 모아보기 기능에서 본 적이 있는 클래스이다.

다만 필자가 업데이트 한 내용만 긁어왔을 뿐이다.

 

막연하게 코드만 뚫어지게 쳐다보면 얼른 감이 오지 않을 수도 있다.

저 코드블록은 '관계지향형' 언어인 자바의 종특을 가장 잘 설명해주는 녀석이라고 할 수 있다.

ProductRequestDto는 필요한 정보를 몰고 다니는 화물차와도 같은 클래스라고 한 거..

바로 위에서 다뤘는데 기억하는가?

그 녀석을 마치 부모-자식 관계처럼 상속받아 쓴 것이다.

 

title과 link, image, lprice는 모두 requestDto 클래스에서 사용된 멤버변수들이다.

그런데 자바에서는 자식 객체가 부모 객체에서 사용된 변수와 메서드를 모두 가져다가 쓸 수 있다는 규칙이 있다.

public으로 선언한 Product 객체에서는 DTO 클래스에서 상속받아 쓴 세 가지 멤버 변수 외에..

고유의 myprice 변수를 하나 더 선언해 준 것 뿐이다.

 

어째서?

@Column(nullable = false)
// This Table Column must have value!
private int myprice;

요 놈 때문에...

null 값이 존재하면 안된다고 @Column(nullable = false) 라고 어노테이션까지 달아놨다.

그러니까 최소한의 Default 값이라도 있어야..

사용자가 설정한 가격보다 최저가가 떴을 때, 모아보기에 목록이 로드될 것이다.

0보다 작은 값은 애초에 존재하지도 않을테고..

 

3-4) ProductService

@RequiredArgsConstructor
@Service // Hey, Java! This class is Service Application!

// 관심상품의 가격정보를 변경해주기 위한 '기능'을 수행하는 서비스 객체

public class ProductService {

    private final ProductRepository productRepository;
    // 조회 (SELECT) 기능을 수행하기 위해 productRepository 를 받아와야 한다.

    @Transactional
    // pstmt.updateQuery();
    public void update(Long id, ProductMypriceRequestDto requestDto){
        // 가격정보를 물어오는 ProductMypriceRequestDto 를 받아와야 한다.
        Product product = productRepository.findById(id).orElseThrow(
                // String sql =  SELECT * FROM PRODUCT WHERE 'ID' LIKE ?;
                //        sql += NVL2();
                () -> new IllegalArgumentException("해당 Id가 존재하지 않습니다!")
        );

        product.update(requestDto);
        // ProductMypriceRequestDto requestDto
        // return id;
    }
}
 // 사용자가 설정한 관심상품의 가격 (myprice) 변경 (PUT)
    public void update(ProductMypriceRequestDto requestDto){
        this.myprice = requestDto.getMyprice();
    }
}

사실상 두 코드블록 모두 사용자가 설정한 상품가격 (myprice)를 변경 (PUT / UPDATE) 하기 위해 필요한 놈들이다.

 

ProductService 클래스에서는 1차적으로 컬럼 Id를 검증 (ProductRepository)하는 단계를 거친 다음에..

ProductMypriceRequestDto에 담긴 상품가격 정보를 업데이트 해주고 있다.

그 과정에서 Product Class로 넘어가 update 메서드를 통해 값을 얻어와야 한다.

 

코드블록만 놓고 본다면 이게 무슨 상관이지?

ProductRequestDto만 있으면 되는거 아님?

라고 할 수도 있겠다.

 

최저가 설정 (myprice)을 위한 밑 작업이라고 생각해라.

어쨌든 클라이언트에게 제공되는 화면에서는 등록과 변경이 한꺼번에 이루어지고 있지 않은가?

 

3-5) ProductRestController [Modified]

// 관심 상품 신규 등록 (POST)
@PostMapping("/api/products")
public Product postProduct(@RequestBody ProductRequestDto requestDto){
    // 배열 목록 전체를 업데이트 해줄 필요는 없기 때문에 단일 객체만 선언해주면 된다.
    // 관심상품의 신규 정보를 등록해야 하기 때문에 ProductRequestDto 를 받아온다.
    Product product = new Product(requestDto);
    // ProductRequestDto 내장객체의 정보를 담고 있는 빵을 만들어달라!
    return productRepository.save(product);
    /* productRepository.save(product);
     * return product;
      */

}

이번에도 Modified읾..

원래 쓰던 REST Controller에서 살을 더 붙였다는 뜻이다.

모아보기 탭을 클릭하면 등록한 관심상품을 조회할 수 있는 코드를 바로 이전 챕터에서 구현해보았다.

@GetMapping("/api/products") 어노테이션을 선언하여 데이터를 조회하는 부분

 

기억하려나..?

 

이번에도 마찬가지로 데이터를 생성해주기 위한 @PostMapping("/api/products") 어노테이션을 선언했다.

이후에도 기능이 하나씩 추가 될 때마다 어노테이션이 늘어나겠지?

UPDATE와 동일한 기능을 수행하는 PUT!

데이터를 삭제해주기 위한 DELETE!

추후에 모두 추가해 나갈 예정이다.

 

데이터 조회와 마찬가지로 생성기능을 구현하기 위해

productRepository 인터페이스를 오버로 (Overload)드 해서 쓰고 있다.

철자에 유의하기 바란다.

OverLord가 아니라 Overload이다.

뭐.. 필요한 메서드를 바리바리 싣고 있는 걸로 보아선 대군주 역시 비슷한 놈이긴 한 거 같다.

 

관심상품을 등록하기 위한 기능구현은 이 정도면 충분한 거 같다.

미리 스포일러를 하자면 어플리케이션만 만들었다고 끝난 게 아니다.

이 놈을 실제로 웹으로 배포해 자랑질 좀 해야 하지 않겠는가?

Localhost 서버에서 웹 서버로 빌드하는 과정 역시 한꺼번에 포스팅 해서 올릴 예정이다.

우선 여기서 끊겠다..


4. Keyword로 상품을 검색하자! [Ⅰ]

프로젝트를 생성하면서 가장 먼저 만들었던 클래스 기억하는가?

public String search() {
    RestTemplate rest = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Naver-Client-Id", "사용자가 발급받은 Client ID");
    headers.add("X-Naver-Client-Secret", "사용자가 발급받은 Client P/W");
    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);

    return response;
}

    public static void main(String[] args) {
            NaverShopSearch naverShopSearch = new NaverShopSearch();
            naverShopSearch.search();
        }

요 녀석 말이다.

필자는 클래스 이름을 NaverShopSearch.java 라고 명명해줬다.

Naver Shop Search!

이름만 봐도 감이 팍팍 오지 않는가?

 

사용자가 특정 'Keyword' 로 상품을 검색 하였을 때,

이에 대한 응답 (Response)으로 네이버 쇼핑에서 상품 목록을 좌르륵 받아오는 기능을 구현한 것이다.

이전에 네이버 개발자 도구에서 클라이언트를 발급 받았다고 했지?

 

하지만 이 상태로는 아직 미흡한 감이 있기 때문에 코드를 약간 수정해 볼 것이다.

필자와 함께 차근 차근 살펴보도록 하자!

 

4-1) 검색 키워드를 바꿔보자!

{
	"lastBuildDate":"Fri, 01 Jul 2022 10:39:34 +0900",
	"total":16229,
	"start":1,
	"display":10,
	"items":[
		{
			"title":"정품 <b>원신 굿즈<\/b> 게임 캐릭터 랜덤박스 [이벤트\/당일발송]",
			"link":"https:\/\/search.shopping.naver.com\/gate.nhn?id=83906867903",
			"image":"https:\/\/shopping-phinf.pstatic.net\/main_8390686\/83906867903.jpg",
			"lprice":"13900",
			"hprice":"",
			"mallName":"잇츠하비",
			"productId":"83906867903",
			"productType":"2",
			"brand":"",
			"maker":"",
			"category1":"생활\/건강",
			"category2":"수집품",
			"category3":"모형\/프라모델\/피규어",
			"category4":"피규어"
		}

기능 개선을 위한 첫 단추를 채워보도록 하겠다.

NaverShopSearch 클래스를 '실행' 하였을 때, 자동으로 날라오는 JSON 값들이다.

검색어를 딱히 입력하지 않아도 말이다.

 

그렇다면! 우선적으로 사용자가 검색어를 입력했을 때에만 결과값이 전송되도록 코드를 수정해야 한다.

그렇지 않으면 필자가 API 테스트를 위해 임의로 입력한 원신굿즈라는 키워드가 결과값으로 반영되어

수시로 사용자들에게 노출이 될 것 아닌가?

이런 식으로 커밍아웃을 할 수는 없어..

 

암튼!

public static void main(String[] args) {
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        naverShopSearch.search("에어팟");
    }
}

사용자가 키워드를 에어팟이라고 입력을 했다고 가정하고!

 

public String search(String query) {
    RestTemplate rest = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Naver-Client-Id", "joewLaLti4xv_gnC5SKz");
    headers.add("X-Naver-Client-Secret", "C1q56mPWva");
    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);

요런 식으로 바디를 수정해주었다.

뭐가 바뀌었는지 눈에 들어오는가?

String 타입의 query 값을 대신 받아오고 있는 것이다.

개씹덕냄새 나는 그런 거 집어치우고 말이다.

 

{
			"title":"<b>에어팟<\/b> 프로 한쪽 유닛 <b>에어팟<\/b> 프로 오른쪽 RIGHT 이어폰 단품 구매",
			"link":"https:\/\/search.shopping.naver.com\/gate.nhn?id=82398210877",
			"image":"https:\/\/shopping-phinf.pstatic.net\/main_8239821\/82398210877.982.jpg",
			"lprice":"88000",
			"hprice":"",
			"mallName":"사라구 saragoo",
			"productId":"82398210877",
			"productType":"2",
			"brand":"",
			"maker":"Apple",
			"category1":"디지털\/가전",
			"category2":"음향가전",
			"category3":"블루투스셋",
			"category4":"블루투스이어폰\/이어셋"
		}

그렇다면 이런 식으로 JSON이 날라오겠지.

이제야 좀 사람같다...

첫번째로 개선해야 할 기능인 검색 키워드 바꾸기를 우선적으로 구현해봤다.

 

여기서 끝이 아니다.

아직 한 발 더 남았다..

 

4-2) 검색 결과를 문자열에서 DTO로 변환해주자!

public String search(String query) {
    RestTemplate rest = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Naver-Client-Id", "발급받은 Client ID");
    headers.add("X-Naver-Client-Secret", "발급받은 Client P/W");
    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);
    (생략...)
}

코드블록을 살펴보면 query의 데이터타입이 뭐지?

String, 즉 문자열이다.

이 query라는 값을 DTO..

그러니까 화물 (title, image, link, lprice, myprice)을 가득 적재한 5톤짜리 카고 트럭으로 바꿔줘야 하는 것이다.

이 과정이 꽤나 복잡하니 집중해서 필자를 따라와주기 바란다.

 

4-2-1) org.json 패키지 설치하기

우선 JSON은 자바의 공식 문법이 아니다.

그 자체로는 JavaScript Object Notation 이라고 하여, 엄연한 JS 문법이다.

이것을 자바 환경의 IDE에서 다루기 위해서는?

 

별도의 라이브러리가 필요하다..

물론 필자가 직접 만들지는 않을 것이다.

남들이 만들어놓은 라이브러리 패키지를 Import 해서 가져다 쓸 것이다.
Follow me!

 

 

우선 구글에 요 녀석을 검색해주자.

 

 

 

들어가라.

 

 

 

검색해라.

 

 

 

들어가라.

 

 

 

들어가라.

일반적으로 가장 많은 유저들이 사용하는 날짜의 버전을 가져다가 쓰면 된다.

 

 

 

클릭해라.

자동으로 클립보드에 복사가 된다.

 

 

들어가라.

 

 

붙여넣어라.

위치는 상관없다.

 

 

실행시켜라.

 

 

확인해라

 

 

들어가라.

 

 

새로고침 해라.

 

앞으로 모든 패키지나 라이브러리는 Grdle의 Dependencies에 복붙하는 식으로 쓰게 될 것이다.

이번에 긁어온 라이브러리는 JSON 단일 객체를 다루기 위한 JSON Objet 하나하고..

JSON 배열 형태를 다루기 위한 JSON Array이다.

여기까지 실행했으면 자바에서 JSON 라이브러리를 쓰기 위한 모든 준비가 끝났다.

 

하지만 끝이 끝이 아니다.

 

이 JSON 이라는 녀석을 어떻게 써먹어야 하는지에 대해서는 얘기를 하지 않았다.

복잡하지 않으니 하나하나 살펴보자.

public static void main(String[] args) {
    NaverShopSearch naverShopSearch = new NaverShopSearch();
    String result = naverShopSearch.search("에어팟");

    JSONObject rjson = new JSONObject(result);
    JSONArray items = rjson.getJSONArray("items");

    for (int i=0; i<items.length(); i++) {
        JSONObject itemJson = (JSONObject) items.get(i);
        System.out.println(itemJson);
    }
}

에어팟이라는 상품의 정보를 JSONObject객체에 우선 저장을 해주고..

 

{
	"lastBuildDate":"Fri, 01 Jul 2022 11:46:36 +0900",
	"total":1196923,
	"start":1,
	"display":10,
	"items":[
		{
			"title":"Apple <b>에어팟<\/b> 3세대 2021년형 (MME73KH\/A)",
			"link":"https:\/\/search.shopping.naver.com\/gate.nhn?id=29413009627",
			"image":"https:\/\/shopping-phinf.pstatic.net\/main_2941300\/29413009627.20211028160140.jpg",
			"lprice":"200000",
			"hprice":"",
			"mallName":"네이버",
			"productId":"29413009627",
			"productType":"1",
			"brand":"Apple",
			"maker":"Apple",
			"category1":"디지털\/가전",
			"category2":"음향가전",
			"category3":"블루투스셋",
			"category4":"블루투스이어폰\/이어셋"
		}

저기에서 items에 담겨있는 정보를 JSONArray 배열 형태로 뽑아올 것이다.

 

{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2941300/29413009627.20211028160140.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"29413009627","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=29413009627","maker":"Apple","title":"Apple <b>에어팟<\/b> 3세대 2021년형 (MME73KH/A)","lprice":"200000","hprice":"","brand":"Apple","productType":"1"}

요런 식으로 말이다.

itmes라는 배열 안에 들어가 있는 모든 정보들이 중괄호 안에 다 포함되어 있다.

 

하나만 더 다뤄볼까?

public static void main(String[] args) {
    NaverShopSearch naverShopSearch = new NaverShopSearch();
    String result = naverShopSearch.search("에어팟");

    JSONObject rjson = new JSONObject(result);
    JSONArray items = rjson.getJSONArray("items");

    for (int i=0; i<items.length(); i++) {
        JSONObject itemJson = (JSONObject) items.get(i);
        System.out.println(itemJson);

        String title = itemJson.getString("title");
        String image = itemJson.getString("image");
        String link = itemJson.getString("link");
        int lprice = itemJson.getInt("lprice");

        System.out.println(lprice);

for문을 한 번 살펴도록 하자.

itemJson 배열 안에 들어있는 값들을 (JSONObject)로 형 변환을 시켜준 다음에..

내가 보고싶은 정보들만 쏙! 쏙! 뽑아다가 쓰기 위해서 다음 코드를 작성하였다.

 

그 다음에 메인메서드를 한 번 실행 해볼까?

{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2941300/29413009627.20211028160140.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"29413009627","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=29413009627","maker":"Apple","title":"Apple <b>에어팟<\/b> 3세대 2021년형 (MME73KH/A)","lprice":"200000","hprice":"","brand":"Apple","productType":"1"}
200000
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2941245/29412453621.20211028154519.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"29412453621","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=29412453621","maker":"Apple","title":"Apple <b>에어팟<\/b> 프로 2021년형 맥세이프 호환 (MLWK3KH/A)","lprice":"235900","hprice":"","brand":"Apple","productType":"1"}
235900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_1862208/18622086330.20200831140839.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"18622086330","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=18622086330","maker":"Apple","title":"Apple <b>에어팟<\/b> 2세대 유선충전 모델 (MV7N2KH/A)","lprice":"144900","hprice":"","brand":"Apple","productType":"1"}
144900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2992330/29923300618.20211202151514.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"29923300618","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=29923300618","maker":"Apple","title":"Apple <b>에어팟<\/b> 3세대","lprice":"139900","hprice":"","brand":"Apple","productType":"1"}
139900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2970967/29709676621.20211116111731.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"29709676621","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=29709676621","maker":"Apple","title":"Apple <b>에어팟<\/b> 프로 MLWK3AM/A","lprice":"204900","hprice":"","brand":"Apple","productType":"1"}
204900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_1875485/18754856889.20200831140938.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"18754856889","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=18754856889","maker":"Apple","title":"Apple <b>에어팟<\/b> 2세대 무선충전 모델 (MRXJ2KH/A)","lprice":"148900","hprice":"","brand":"Apple","productType":"1"}
148900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_2119271/21192710714.20200831140731.jpg","mallName":"네이버","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"21192710714","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=21192710714","maker":"Apple","title":"Apple <b>에어팟<\/b> 프로 (MWP22KH/A)","lprice":"235900","hprice":"","brand":"Apple","productType":"1"}
235900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_8277440/82774409818.3.jpg","mallName":"설빈","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"82774409818","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=82774409818","maker":"Apple","title":"<b>에어팟<\/b> 프로 애플코리아 정품 당일발송","lprice":"263900","hprice":"","brand":"Apple","productType":"2"}
263900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_8386103/83861030308.jpg","mallName":"설빈","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"83861030308","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=83861030308","maker":"Apple","title":"애플 <b>에어팟<\/b> 3세대 MME73KH/A","lprice":"222900","hprice":"","brand":"에어팟","productType":"2"}
222900
{"category2":"음향가전","image":"https://shopping-phinf.pstatic.net/main_8239821/82398210877.982.jpg","mallName":"사라구 saragoo","category3":"블루투스셋","category4":"블루투스이어폰/이어셋","productId":"82398210877","category1":"디지털/가전","link":"https://search.shopping.naver.com/gate.nhn?id=82398210877","maker":"Apple","title":"<b>에어팟<\/b> 프로 한쪽 유닛 <b>에어팟<\/b> 프로 오른쪽 RIGHT 이어폰 단품 구매","lprice":"88000","hprice":"","brand":"","productType":"2"}
88000

보여? 보여?

시험 삼아 최저가 (lprice)만 한 번 출력해본 것이다.

 

기왕 소개하는 기능이니 그 신박함을 보여줘야 하지 않겠는가?

똒똒한 스프링부트는 이렇게 JS 형태의 데이터도 잘 뽑아낸다.. ㅎㅎ

독자 여러분들도 프레임워크와 라이브러리 임포트를 습관화 들여놓도록 하자.

 

참고로 프레임워크는 강한 규칙성을 띄고 있어서, 거기에 명시된 프로토콜을 개발자가 무조건 따라야 하지만..

라이브러리는 그것에 비해 상대적으로 제약이 덜 하다는 특징이 있다.

면접 때 물어볼지도 모르니 이 정도는 숙지하고 넘어가자고~

 

4-2-2) ItemDTO

참으로 미안하지만 여기서 끝이 아니다.

JSON으로 필요한 데이터를 쏙! 쏙! 뽑아내서 뭐 어쩌라고?

데이터를 요청 (Request) 했으면 마땅이 리턴 (Return)이나 응답 (Response)을 받아야지?

 

사용자가 에어팟.. 하고 입력을 한 게 요청이라고 하면

검색 결과를 파바밧! 하고 보여주는 것이 응답이라고 할 수 있겠다.

 

그러기 위해서는 우리가 원하는 데이터를 운송해주는 Dto 클래스가 따로 또 필요하다.

Dto은 뭐다?

5톤짜리 카고트럭..

필자는 그 카고트럭의 이름을 ItemDto 라고 명명해줬을 뿐이다.

 

@Getter
// @Setter: ItemDto 생성자에서 필요한 정보만 꺼내다가 쓸 것이기 때문에 Setter 는 따로 필요하지 않음

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

    // Get JSONObject to transfer items of JSON Array
    public ItemDto(JSONObject itemJson){
        this.title = itemJson.getString("title");
        // Key (Value) is title.
        this.image = itemJson.getString("image");
        // Key (Value) is image.
        this.link = itemJson.getString("link");
        // Key (Value) is link.
        this.lprice = itemJson.getInt("lprice");
        // Key (Value) is lprice.
    }
}

사용자에게 노출시켜 줘야 하는 정보는 상품의 제목, 이미지 파일, 링크, 최저가 정도가 되겠다.

그것을 위해 각 항목마다 멤벼변수를 선언해주고..

ItemDto 생성자를 선언해줘서 필요한 값들을 Get으로 받아오는 과정이다.

 

JSON을 소개하기 위해서 NaverShopSearch 클래스 하단에 작성해 놓은 테스트 코드는 이제 필요 없겠지?

다시 NaverShopSerach 클래스로 돌아가서..

public static void main(String[] args) {
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        String result = naverShopSearch.search("에어팟");

        JSONObject rjson = new JSONObject(result);
        JSONArray items = rjson.getJSONArray("items");

        for (int i=0; i<items.length(); i++) {
            JSONObject itemJson = (JSONObject) items.get(i);
            System.out.println(itemJson);

            String title = itemJson.getString("title");
            String image = itemJson.getString("image");
            String link = itemJson.getString("link");
            int lprice = itemJson.getInt("lprice");

            System.out.println(lprice);
        }
    }

 

메인 메서드 싹 다 지워줘부러..

필요한 정보는 전부 ItemDto에 만들어 줬는데 중복된 코드가 있을 필요가 없잖아?

어짜피 메서드를 따로 만들어 줄 것이다.

아~ 지우는 것을 무서워 하지마!

 

4-2-3) fromJSONtoItems 메소드 만들기

public class NaverShopSearch {
    // Step01: 검색 키워드 (query) 를 문자열 (JSON) 형태로 Get
    public String search(String query) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "발급받은 Client ID");
        headers.add("X-Naver-Client-Secret", "발급받은 Client P/W");
        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;
    }

    // Step02: String 형태로 받은 JSON 데이터를 ItemDto 로 반환
    public List<ItemDto> fromJSONtoItems(String result){
        JSONObject rjson = new JSONObject(result);
        JSONArray items = rjson.getJSONArray("items");

        List<ItemDto> itemDtoList = new ArrayList<>();
        // 필요한 정보를 하나만 노출시켜 주는 것이 아니라 여러개 불러오기 위해 배열 사용..
        for (int i=0; i<items.length(); i++) {
            JSONObject itemJson = (JSONObject) items.get(i);
            ItemDto itemDto = new ItemDto(itemJson);
            // Insert itemJson to itemDto Object.
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }

이렇게 쪼물딱 쪼물딱 해서 코드를 완성해주면 의도가 더 명확해지지 않은가?

 

스텝 원!

사용자가 검색창에 특정 키워드를 입력하면 그에 대한 결과값을 JSON으로 받는다.

 

스텝 투!

JSON으로 받은 데이터를 다시 ItemDto 카고트럭으로 반환해준다.

From JSON To ItemDto인 셈이다.

참고로 JSON은 문자열 타입이다.

고 놈을 화물차 (DTO)에 보기 좋게 적재만 해주면 적재적소에 데이터를 떨궈주겠지?

 

 

아무리 복잡해보이는 코드라도 그 의도를 정확히 파악만 한다면

맥을 짚는 것 정도는 어렵지 않다.

 

덧붙여서..

같이 협업을 하는 동료들은 물론이거니와!

코드를 작성한 필자 본인이 나중에 알아보기 위해서라도

주석에 상당히 공을 들여야겠지?

 

그렇다면 코드가 돌아가는 원리를 이해해야 할 것이고..

필자는 그 원리를 이해하기 위해 해당 주차 강의를 세 번 정도 돌려봤다.

 

아직 갈 길이 개같이 남아있지만 조금 만 더 힘내보자, 아쎄이!


 

5. Keyword로 상품을 검색하자! [Ⅱ]

이번에도 키워드로 상품을 검색하는 기능이다.

아직 개선해야 할 내용이 좀 남아있어서 말이다..

저번 장에서 키워드로 상품을 검색하고, JSON을 다시 DTO로 반환해주는 내용을 다뤄봤다.

이번 장에서 살펴볼 내용은 다음과 같다.

 

  • 사용자가 검색어를 키워드로 입력하면, 컨트롤러가 그것을 전달 받아야 한다.
  • 전달받은 키워드를 네이버 API에 요청하고, 그 결과를 사용자에게 응답해야 한다.

 

이 두 가지 내용에 포커싱 해볼 것이다.

내용 자체는 그리 길지.. 않으니 집중해주길 바란다.

 

그러면 전방에 힘찬 함성 5초간.. 발사!

 

5-1) NaverShopSearch 클래스에 Component 등록 (부여)하기

이게 뭔 개소리야?

라고 할 수도 있겠다.

당연하다.

필자도 그랬으니 말이다.

우선 코드블록부터 살펴보도록 할까?

@Component
/* @RequiredArgsConstructor
 * @NoArgsConstructor
 * @Entity
 * @Service
 * ...
 * */

public class NaverShopSearch {
    // Step01: 검색 키워드 (query) 를 문자열 (JSON) 형태로 Get
    public String search(String query) {
        (생략...)
        String response = responseEntity.getBody();
        System.out.println("Response status: " + status);
        System.out.println(response);

        return response;
    }

    // Step02: String 형태로 받은 JSON 데이터를 ItemDto 로 반환
    public List<ItemDto> fromJSONtoItems(String result){
        JSONObject rjson = new JSONObject(result);
        JSONArray items = rjson.getJSONArray("items");

       (생략...)
        }
        return itemDtoList;
    }
}

더 모르겠는가?

사실 어려운 개념이 맞다.

코드블록 자체를 살펴봐야 별 의미 없는 것도 맞고..

 

자! 상단 @Component 이하 주석 처리한 부분을 유심히 살펴봐라.

하나같이 눈에 익는 녀석들이 아닌가?

그렇다.

어지간한 일들은 시키지 않아도 알아서 처리하는 스프링부트의 가장 핵심적인 기능인 어노테이션 묶음이다.

뭐라고?

어노테이션 묶음

 

그냥.. 그때, 그때 필요한 거 가져다가 생성해주면 안되나?

물론 그래도 된다.

하지만 여기에서 굳이 컴포넌트를 선언한 이유가 있다.

public static void main(String[] args) {
        NaverShopSearch naverShopSearch = new NaverShopSearch();
        String result = naverShopSearch.search("에어팟");

        JSONObject rjson = new JSONObject(result);
        JSONArray items = rjson.getJSONArray("items");

        (생략...)
        }
    }

이 코드블록을 기억하는가?

NaverShopSearch 에플리케이션을 실행시켜주는 메인 메소드였다.

하지만 지난 시간에 필자가 직접 날려주라고 했던 적이 있다.

코드가 중복되면 좋을 게 없으니까..

 

문제는 이 녀석을 삭제해줌으로서 검색엔진 본연의 기능을 상실한 것이다.

그것 기능을 충족 시켜주기 위해서 스프링 부트 자체에게 전권을 위임한 것이다.

바로 @Componnet를 선언함으로서 말이다.

요 녀석이 있음으로 말미암아 스프링은 지가 알아서 필요한 클래스를 꺼내 쓸 것이다.

사용자가 키워드를 입력할 때마다 말이다.

 

이 정도면 충분히 설명이 됐을까?

코딩은 단순 복붙 노가다 작업이 주류를 이루기는 하지만..

 

왜? Why? 난데스까? 이게 굉장히 중요하다.

우리는 승모근 통증과 거북목, 라운드 숄더를 벗삼아 살아가는 엔지니어다.

본인이 설계한 시스템을 본인이 몰라서야 되겠는가?

필자가 감히 그러한 꼰대같은 소리를 할 입장은 아니지만..

공자께서는 세 사람이 함께 길을 걸어가면 그 중에 반드시 스승이 있다고 했다.

필자는 아직 스승으로 삼을만한 선배가 없으니, 스스로의 실수를 거울 삼아 배워야 하지 않겠는가?

 

5-2) Search Request Controller 만들기

Function Method URL
1. 키워드로 상품 검색

2. 검색 결과를 목록으로 보여주기
GET /api/search?query=검색어

고지가 눈 앞이다. 조금만 더 힘을 내자.

이제 마지막으로 테이블과 같은 기능을 수행하는 컨트롤러를 별도로 만들어야 한다.

요 앞에서 만든 REST Controller는 얻다 두고?

그 놈은 이미 본인의 역활을 충실하게 수행하고 있다.

관심 상품의 목록을 조회하고..

마찬가지로 관심상품을 등록하고..

따위의 기능 말이다.

 

이번 자동응답기는 사용자가 키워드로 상품을 검색하면 거기에 대한 목록만 따로 조회해주는 기능을 수행한다.

앞선 REST Controller가 이미 관심 목록으로 등록된 상품을 '모아보기' 탭에서 한꺼번에 조회해주는 기능과 차이가 있다.

거기에 대한 증거로 API 테스트 하는 장면을 보여주지 않았는가?

아직 등록된 관심상품이 없어서 텅 빈 배열만 200번 코드와 함께 날라오던 것..

 

 

그러니까 요런 화면을 사용자에게 보여주려는 것이다.

사용자가 어쩌구 저쩌구 키워드를 입력하면,

마찬가지로 네이버 API에서도 실시간으로 검색하여 결과를 반환해주는 식이다.

유남쌩?

 

자! 코드블록을 통해 내용을 더 상세하게 파헤쳐보자.

@RestController
// 키워드 입력 → 검색결과 반환 (자동응답기)
// Return as JSON
@RequiredArgsConstructor
// 여기에 파이널로 선언된 생성자 있다..
// private final NaverShopSearch naverShopSearch;
// @Component: ㅇㅋ...

public class SearchRequestController {
    private final NaverShopSearch naverShopSearch;
    // NaverShopSearch 클래스를 가져다쓰지 않으면 넌 죽는다.
    // @Component 올림..

    @GetMapping("/api/search")
    public List<ItemDto> getItems(@RequestParam String query){
        // @RequestParam: JSON 형식으로 날라오는 query 변수를 파라미터 값으로 List<ItemDto>에 저장하겠음
        // "api/search ?query=검색어"
        // doGet(request, response);
        String result = naverShopSearch.search(query);
        // 일해라, 핫싼..!

        return naverShopSearch.fromJSONtoItems(result);
        // From JSON to ItemDto ArrayList<>
    }
}

하.. 겁나 친절해!

필자의 브레인 스토밍을 위해서!

그리고 독자 여러분들의 이해를 위해서!

저렇게 친절하게 코멘트를 달아놨다.

 

어지간한 것들은 주석을 참고하면 될 것이다.

단! NaverShopSearch 에플리케이션에서 선언한 @Component의 기능에 중점을 두고 읽어보길 바란다.

저 코드블록 자체를 이클립스에서 구현한다고 하면 솔직히 대부분의 기능이 먹통이 될 것이다.

당장 롬복부터 깔아야 할 것이고..

라이브러리도 jar 파일 형식으로 웹 컨텐트 lib 폴더에 밀어넣어야 함.

컴포넌트? 꿈도 꾸지 말아라.

 

자! 이제 요놈이 제대로 동작을 하는지 테스트를 해봐야겠지?

필자는 바로 ARC를 켜주겠다.

 

 

앗싸!!!!!!!

 

뜬다!!!!!

 

뜬다고!!!!

 

제가 뜬다니까요????

 

따흐흐흐흑...

 

미안하다.. 필자가 너무 기뻐서 그만..

티를 내지는 않지만 내 블로그 눈팅하는 분들이 더러 계신걸로 알고 있다.

선배 개발자들이라면 이 감격이 뭔지 당연히 아실테고..

필자처럼 문과생 + 뉴비에요 라면 이참에 개발 해보싈?

내가 어렵게 어렵게 구현한 서버가 이렇게 잘 돌아가고 있다니..ㅠㅠㅠㅠㅠ


여기까지가 서버단이다.

필자가 글의 서두부터 강조하였던 3Layer 기법을 완벽하게 훑어본 것이다.

장난감 하나 가지고서..

 

이 다음부터는 프론트엔드 영역이다.

HTML로 웹 페이지의 뼈대를 세우고..

CSS로 색칠을 하고..

JS로 웹에 생명력을 불어놓고..

이런 작업들 말이다.

 

사실 필자가 돈 주고서 조금이나마 깊게 배웠다고 자신하는 내용은 여기까지다.

이 다음에 이어질 프론트 단은 필자로서는 영 껄끄러운 내용들 뿐이다.

실무에서도 기왕이면 건드리고 싶지 않은 부분이다.

200번 잘 날라오고 있잖아?

그럼 된거 아님?

 

예.. 아닙니다. 훠훠훠..

필자가 저번에 면접보러 갔던 SI업체도 그렇고,

자바를 이용한 웹 에플리케이션 개발자 채용은 풀스텍이 대세인거 같더라.

풀스텍? 얼마나 업무에 깊이가 없으면 꼴랑 그걸 가지고서 풀스텍이라고 할까..

하지만 어떡하겠는가? 취업시장 트랜드가 그러한 것을..

 

그렇기 때문에 프론트 단도 필자가 아는만큼.. 공부한 만큼..

다루고 넘어가지 않을 수가 없다.

자신은 없지만 여기서 한 템포 쉬고, 다시 한 번 필자와 기열?

아니아니.. 기합차게 달려보도록 하자.

 

거기까지만 잘 넘기면 마지막 빌드하고, 웹 서버에 배포하는 내용은 진짜 재밌다.

참고로 지금 필자가 구현한 3Layer 서버단은 오직 로컬 호스트 서버에서만 작동한다.

그러니까 여러분이 백번이고 천번이고..

http://localhost:8090

을 타고 들어온다고 한들, 볼 수 있는 건 아무 것도 없을 것이다.

그것을 모두가 볼 수 있는 웹 공간에 배포해보는 과정까지 달려볼 것이다.

그것을 기대하면서 남은 내용도 함께 해보자.

이상!

 

academy3746/My_Select_Shop_ver_1.0.1: 나만의 셀렉샵 기능 개선 (github.com)

 

GitHub - academy3746/My_Select_Shop_ver_1.0.1: 나만의 셀렉샵 기능 개선

나만의 셀렉샵 기능 개선. Contribute to academy3746/My_Select_Shop_ver_1.0.1 development by creating an account on GitHub.

github.com

 


6. Client 준비단계

자! 아기다리고기다리던 프론트 단을 꾸며보는 시간이 왔다.

내 웹 페이지를 방문하는 고객들에게 직접적으로 노출되는 화면단 말이다.

여태까지 작업한 3Layer가 주방에서 음식을 만들기 위한 과정을 세분화하는 과정이었다면

지금부터는 손님들을 위한 홀을 인테리어 하는 시간이다.

 

테이블을 몇 개나 두어야 할 것이며..

바닥 타일은 어떤 재질로 미장을 할 것인지..

벽체 페인트질은 무슨 색상으로 정할 것인지..

따위의 작업 말이다.

 

주방이 아무리 척, 척, 척 분주하게 돌아가고 있다고 한들

손님들에게만 개방되는 공간이 부실하다면 그것도 영 아니지 않겠는가?

 

물론 홀은 따로 관리 안하는 배달전문 업체로 만들 수도 있겠지..

하지만 필자가 최종적으로 완성하고자 하는 웹 애플리케이션은!

기본적으로 매장을 방문하는 손님들을 상대로 하는 가게가 메인 컨셉이다.

 

그것을 위한 과정을 차근차근 밟아보자.

 

우선 기본적으로 프론트 작업을 할 때에는 다음 세 가지 요소가 기본적으로 들어간다.

 

  • HTML: 웹 페이지의 뼈대
  • CSS: 웹 페이지의 인테리어
  • JavaScript: 웹 페이지의 동적 요소

 

이래저래 귀찮을 때는 HTML 파일 하나에 모든 작업을 몰아넣어 한꺼번에 작업을 진행할 수도 있지만..

필자는 안그래도 각각의 코드량이 무식하게 많은 녀석들을 한 공간에 몰아넣을 생각이 없다.

 

본인이 비록 코딩 뉴비이기는 하지만 구조설계를 안해본 것이 아니다.

기본적으로 Auto-Cad로 작업하는 2D 도면도 관계자들 모두가 알아보기 쉽도록

최대한 간결하고, 깔끔하게 작성하여야 한다.

약어나 심볼마크도 당연히 따로 주석을 첨부해서 이해를 도와야 하고..

 

그런 것처럼 필자도 HTML / CSS / JavaScript 파일들을 하나씩 따로 만들어서 관리할 생각이다.

그렇게 하면 HTML을 작성할 때도 CSS와 JS를 임포트 하는 식으로 간결하게 코딩이 가능하다.

<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="basic.js"></script>

요런 식으로 말이다.

 

한 눈에 보기에도 코드가 아주 많이 절약될 것 같지 않은가?

근데 뭣하러 그렇게 까지..?

그냥 HTML에 몰아넣으면 안됨?

가능하대매?

귀찮은데..

디버깅 할 때 오류 발생하면 님이 책임질거임?

 

물론 그렇게 생각하는 개발자들도 분명 있을 것이다.

하지만 필자의 생각은 조금 다르다.

 

첫째는 HTML 파일이 지나치게 지저분해 지는 것을 방지하려는 목적이 우선이요,

둘째는 가능한 코드의 가독성을 높이려는 것이다.

 

안그래도 이쪽 업계는 하나부터 열까지 팀 프로젝트인데..

이놈이 HTML 코드인지, 스타일 시트 코드인지, jQuey인지..

눈을 비벼가면서 일일이 찾아보는 것이 동료들에게는 여간 짜증나는 일이 아닐 것이다.

시간이 좀만 지나면 나조차도 헷갈릴걸?

 

그것을 방지하고자 하는 것이다.

 

아! 페이지에 들어갈 이미지 파일들은 미리 준비해놨다.

그리고 다시 한 번 말하는 거지만..

필자는 현업에서 종사하더라도 프론트 단은 가급적이면 건들고 싶지 않다.

그렇기 때문에 수업에서 제공하는 기본 뼈대는 그대로 가져다가 쓸 것이다.

 

다만 코드 하나 하나의 기능에 중점을 두고 포스팅을 올릴 생각이다.

백엔드 단을 다룰 때 처럼 말이다.

그러다보니 아무래도 JavaScript에 무게가 실릴 수 밖에 없다는 점은 미리 양해 바란다.

 

HTML과 CSS는 차치하더라도.. JS는 정말 유용한 기술임은 확실하다.

기본적으로 어떠한 환경의 검색엔진에서든 호환이 되는 스크립트 언어이기 때문이다.

적어도 그 놈 하나만은 확실하게 공부하고 넘어가는 것이 좋겠다.

 

그렇다면 다음 챕터부터 본격적으로 클라이언트를 만들어보도록 하자.

 


 

7. 상품 검색기능을 만들어보자!

????

 

백엔드 구축하면서 만들었던 기능 아님?

같은 일을 두 번 하겠다고???

 

예아! 그 말이 맞다.

이미 서버단에서는 API 기능 테스트까지 완벽하게 끝난 작업이다.

 

하지만 아직 클라이언트에는 구현되지 않는 기능이다.

이번 시간에는 그 기능을 클라이언트에 이식하는 작업을 해볼 것이다.

 

 

이런 식으로 말이다.

그것을 위해 만들어야 할 함수가 두 녀석이 있다.

차례대로 살펴보자.

 

7-1) execSearch 함수 만들기

클라이언트에서 실제로 검색기능을 수행하는 녀석이다.

참고로 JS 페이지에서 진행을 할 것이다.

필자 같은 경우는 미리 파일을 만들어 두었다.

 

function execSearch() {
    /**
     * 검색어 input id: query
     * 검색결과 목록: #search-result-box
     * 검색결과 HTML 만드는 함수: addHTML
     */
    // 1. 검색창의 입력값을 가져온다.
    // 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
    // 3. GET /api/search?query=${query} 요청
    // 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!

}

요런 식으로 말이다.

함수를 Execute 하기 위해서 필요한 기능들을 주석처리 해놨다.

필자와 함께 하나씩.. 하나씩 해결해보도록 하자!

 

7-1-1) 검색창의 입력값을 가져온다.

 

요 녀석을 가져오겠다는 것이다.

 

 

id 값이 query라고 대놓고 알려주고 있지?

 

// 1. 검색창의 입력값을 가져온다.
    let query = $('#query').val();

그렇다면 요 정도로 변수를 선언해주면 될 것이다.

참고로 저 달러 표기는 자바에서 EL이라고 하지만..

JS와는 아무런 상관이 없는 녀석이다.

오히려 근본없는 JS에서 자바의 EL을 훔쳐온 것이다!

JS에서는 jQuery 라는 기법이라고 한다.

스크립트 라이브러리 정도로 이해하고 넘어가도록 하자.

 

7-1-2) 검색창의 입력값을 검사한다.

// 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
if(query == ''){
    alert("검색어를 입력해라, 아쎄이!")
    $('#query').focus();
    return;
}

if문은 어느 문법에서나 똑같다.

검색어에 아무 것도 입력하지 않았을 때..

그러니까 공백일 때는

"검색어를 입력해라, 아쎄이!"

라는 경고문이 뜰 것이다.

 

 

요렇게 말이다.

 

 

곧 이어서 커서가 검색창에서 깜빡깜빡 거리며 존재감을 과시할 것이다.

focus 함수를 써서 가능한 것이다.

 

아! 참고로 사용자가 아무런 값도 입력하지 않았다는 것은..

공백을 입력했다는 것이다.

 

값이 존재하지 않았다는 뜻의 'null' 이 아니라는 것이다.

필자도 이 부분에서 개념이 혼동되어 실수를 했으니

독자 여러분도 주의하시길 바란다.

 

7-1-3) GET /api/search?query=${query} 요청

// 3. GET /api/search?query=${query} 요청
$.ajax({
    type:'GET',
    url:`/api/search?query=${query}`,
    // contentType, data는 doGet 에서 사용하지 않는다.

doGet 방식을 통해 검색 결과 'query'를 요청해주기 위하여 만든 함수이다.

JS에서는 API 테스트처럼 GET이건 POST이건 PUT 요청을 날려줄 때, $.ajax({}); 를 쓴다.

 

url 부분을 주의깊게 살펴보시길 바란다.

쌍따음표나 홑따음표와는 뭔가 다르지 않은가?

저 녀석을 '백틱'이라고 한다.

키보드로 따지자면 숫자 1Key 바로 옆에 있을 것이다.

 

url을 ``로 감싸주는 이유는?

doGet 처리를 위해서다

query의 값을 그대로 query라고 입력하면..

자바 환경에서는 모르겠지만 JS에서는 문자열 그대로 q.u.e.r.y가 날라온다.

그렇기 때문에 jQuery를 사용하여 특정 변수를 요청해주는 기법을 사용한 것이다.

 

주석처리 한 부분도 한 번씩 읽고 넘어가주길 바란다.

 

7-1-4) for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!

// 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
        success:function (response){
            for(let i=0 ; i<response.left ; i++){
                let itemDto = response[i];
                let tempHtml = addHTML(itemDto);
                $('search-result-box').append(tempHtml);
            }

for 문 자체는 익숙할 것이다.

내부 구조만 파악해보도록 하자.

 

우선 성공적으로 요청이 이루어졌을 경우..

라고 해서 function 함수를 써봤다.

거 늘 메서드라고 쓰다가 함수라고 하니까 진짜 근본 없네.. ㅡㅡ

지금 누가 함수 소리를 내었어?

암튼..

 

function(response){} 함수 안에서 for문을 돌리고 있다.

itemDto 변수를 따로 선언해서 i번째에 있는 값을 꺼내오고..

잠시 뒤에 다룰 내용이지만 addHTML 함수에 itemDto 값을 저장해줄 것이다.

실제로 HTML에 반영해줘야 하지 않겠는가?

그리고 키워드 검색창 박스 div인 #search-result-box를 jQuery로 선언하여..

tempHtml의 결과 값을 추가해주는 메서드를 썼다.

 

7-2) addHTML 완성하기

이제 쿼리를 실제 HTML 구조에 반영할 차례이다.

function addHTML(itemDto) {
    /**
     * class="search-itemDto" 인 녀석에서
     * image, title, lprice, addProduct 활용하기
     * 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
     */
    return ``
}

요런 식으로 말이다.

함수를 온전하게 굴러가도록 하기 위하여 사전에 만들어두었던 HTML 파일로 돌아가보자.

 

 

저 search-result-box 전체를 컴파운드 하고 있는 div를 통채로 긁어와서..

그것을 js 파일에서 수정하는 작업을 해볼거야.

집중해서 필자를 따라올 것!

 

 

보여?

HTML 파일에서 이 녀석을 통채로 글거올 것이다.

뭐 하나 빼먹으면 안되니까 축소시킨 다음 블록 통채로 복사를 해오자.

어디로? 다시 JS 파일로...

 

 

커서가 깜박이고 있는 백틱 사이에 통채로 붙여넣기 해줄 것이다.

잘 따라오고 있는 거 맞지?

 

return `<div class="search-itemDto">
        <div class="search-itemDto-left">
        </div>
        <div class="search-itemDto-center">
            <div>Apple 아이맥 27형 2020년형 (MXWT2KH/A)</div>
            <div class="price">
                2,289,780
                <span class="unit">원</span>
            </div>
        </div>
        <div class="search-itemDto-right">
            <img src="images/icon-save.png" alt="" onclick='addProduct()'>
        </div>
    </div>`

 

이렇게 가져올거임..ㅎㅎㅎ

이제 필요한 데이터에 각각 jQuery를 선언하여 하나씩 수정해보도록 하자.

백틱 안에 넣어줬으니까 jQuery를 쓸 수 있을거야.

 

function addHTML(itemDto) {
    /**
     * class="search-itemDto" 인 녀석에서
     * image, title, lprice, addProduct 활용하기
     * 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
     */
    return `<div class="search-itemDto">
        <div class="search-itemDto-left">
            <img src="${itemDto.image}" alt="">
        </div>
        <div class="search-itemDto-center">
            <div>${itemDto.title}</div>
            <div class="price">
                ${numberWithCommas(itemDto.lprice)}
                <span class="unit">원</span>
            </div>
        </div>
        <div class="search-itemDto-right">
            <img src="images/icon-save.png" alt="" onclick='addProduct(${JSON.stringify(itemDto)})'>
        </div>
    </div>`
}

 

자! 요렇게 코드를 완성해봤다.

원래 있던 HTML 코드를 약간 수정하여 데이터가 들어가는 값에 jQuery로 요청해주고 있는 식이다.

어지간한 코드들은 코드블록을 자세하게 살펴보면 금방 이해가 될 것이다.

중요한 코드만 몇 개 짚고 넘어가보자.

 

${numberWithCommas(itemDto.lprice)}

 

요놈..

가격정보를 요청해주는 jQuery이다.

itemDto 클래스에서 lprice에 관한 정보만 가져다 달라는 식이지..

그런데 굳이 이상하게 생긴 함수 안에 객체를 집어넣는 이유는?

별 거 없다.

가령 가격을 표시할 때, 12567890..

이런 식으로 노출되면 읽기가 불편하잖슴?

그래서 10,000 요런 식으로 컴마를 넣어서 단위를 구분해달라는 뜻이다.

 

onclick='addProduct(${JSON.stringify(itemDto)})'

 

요 놈도 주의해서 볼 필요가 있다.

onclick 태그는 사용자가 특정 아이콘이나 이미지 파일 따위를 클릭하였을 때..

어떠어떠한 명령을 수행하시오.. 정도의 명령어다.

 

그렇다면 addProduct()는?

 

 

필자가 체크 해놓은 아이콘이 보이는가?

그것을 눌렀을 때, 사용자가 가격을 설정할 수 있는 창이 따로 떠야 한다.

아직은 구현하지 않았기 때문에 별다른 동작을 하지 않을 것이다.

우선은 코드 자체에만 집중하도록 하자.

 

'담기' 아이콘을 클릭하였을 때, 관련된 함수 (addProduct)가 실행되는데..

itemDto 클래스에 담긴 정보가 뭐뭐 있었지?

title, image, link, lprice가 있었다.

이것들이 우선 JSON 형식으로 넘어올텐데..

다시 문자열 형태로 바꿔서 값을 저장해주기 위해 JSON.stringify()를 추가해준 것이다.

모르긴 몰라도 itemDto JSON 그대로 입력하고 서버를 재시작 해주면

분명 오류가 뜰 것이다.

 

 

자! 다음 챕터에서는 저 '담기' 아이콘을 추가하였을 때,

실제로 상품을 관심목록에 추가할 수 있는 코드를 작성해보자.

어떻게 보면 클라이언트에서 두 번째로 중요한 소스코드라고 할 수 있겠지..

첫번째는 뭐냐고?

알아맞춰 봐라..

 


8. 관심상품을 등록해보자!

이 기능 역시 서버단에는 구현이 되어있는 상태이다.

Post를 요청했을 때, 당당하게 200번 코드가 날라오는 것을..

우리는 API 테스트에서 확인해 본 바가 있다.

적어도 500번대 Internal Server 에러가 날아올 일은 없는 셈이지.

 

이제 오류가 발생하면 어지간해선 클라이언트 오류에 해당하는 400번 코드가 응답으로 돌아올 것이다.

이 앞으로는 프론트 개발자의 몫이라는 것이다.

하지만 이 프로젝트에서는 필자가 서버 개발자이자, 클라이언트 개발자이다.

400번 이슈도 온전히 본인의 책임이라는 뜻.. ㅡㅡ

 

자! 각설하고 필요한 제반사항을 차근차근 살펴보자.

우선 필자는 키워드 입력창에 에어팟을 검색해서 마음에 드는 상품을 찾아냈다.

 

그리고 최저가로 210,000원을 설정하였고..

 

 

모아보기 탭으로 자동으로 넘어가있다.

필자가 21만원에 상당하는 상품을 관심목록으로 등록해줬기 때문에

저렇게 조건에 부합하는 상품을 응답으로 받아올 수 있는 것이다.

 

우리는 이 중에서 절반만 구현해볼 것이다.

최저가를 설정 (Put)하는 과정은 가장 핵심기능이기 때문에 가장 뒤로 미뤄놓고..

우선은 등록 (Post)만 먼저 다뤄볼 것이다.

필요한 소스코드를 통해 그 원리를 파헤쳐보자.

 

8-1) addProduct 함수 완성시키기

 

 

필자가 빨간 색으로 마킹해놓은 '담기' 버튼을 활성화시켜주려는 것이다.

서버에서와 마찬가지로 HTML 화면에서도 기능이 제대로 작동하도록 코드를 작성해줘야 한다.

 

8-1-1) 관심상품 생성 요청

function addProduct(itemDto) {
    /**
     * modal 뜨게 하는 법: $('#container').addClass('active');
     * data를 ajax로 전달할 때는 두 가지가 매우 중요
     * 1. contentType: "application/json",
     * 2. data: JSON.stringify(itemDto),
     */
    // 1. POST /api/products 에 관심 상품 생성 요청
    // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.id 로 설정 (숙제로 myprice 설정하기 위함)
}

요 코드를 단계별로 완성해 나갈 것이다.

modal은 고구려에서 장성 급에 준하는 야전 사령관을 일컫는 직책으로..

미안하다.

그 부분은 조금 뒤에서 다루도록 하겠다.

 

우선은 POST 요청을 서버에 날렸을 때, 관심상품이 생성되는 코드를 먼저 완성해보자.

본격적으로 코드를 완성하기 전에 쫌 중요한 부분..!

addProduct 함수 자체는 itemDto를 내장객체로 전달받고 있지?

요 녀석은 JSON으로 날라오기 때문에 나중에 문자열로 변환시켜주는 과정이 필요하다.

 

정 의심되면 console.log(itemDto); 를 쳐서 출력테스트를 해봐라.

필자가 거짓말을 하고 있는지..

 

console.log(itemDto);
console.log(JSON.stringify(itemDto));

 

아휴.. 보여줄게! 보여주면 될 거 아니야!

 

{
			"title":"원신 호두 미니 피규어 캐릭터 미호요 <b>페이몬<\/b> 굿즈",
			"link":"https:\/\/search.shopping.naver.com\/gate.nhn?id=33228860684",
			"image":"https:\/\/shopping-phinf.pstatic.net\/main_3322886\/33228860684.20220701140016.jpg",
			"lprice":"17310",
			"hprice":"",
			"mallName":"네이버",
			"productId":"33228860684",
			"productType":"1",
			"brand":"",
			"maker":"",
			"category1":"생활\/건강",
			"category2":"수집품",
			"category3":"모형\/프라모델\/피규어",
			"category4":"피규어"
		}

 

자.. 일차적으로는 서버에서 JSON이 아주 솔직하게 날아오고 있고..

 

 

콘솔창에서도 귀여운 페이몬이 하나는 JSON 형태로, 다른 하나로는 문자열 형태로 날라오고 있다.

내 말이 맞지?

나중에 발생할지도 모르는 서버 오류를 미연에 방지하기 위해 이 부분을 먼저 짚고 가자는 거다.

서버에는 문제가 없는데요? 소리에 책임을 질 줄 알아야 프론트 단에 일을 떠맡길 거 아닌가?

 

// 1. POST /api/products 에 관심 상품 생성 요청
$.ajax({
    type: "POST",
    url: "/api/products",
    data: JSON.stringify(itemDto),
    contentType: "application/json",
    // Local Server: 요 놈이 JSON 맞는가? 검증해봐야겠다.
    success: function (response){
        console.log(response);
    }
});

 

자! 본론으로 넘어왔다.

관심 상품을 생성해줘! 라고 서버에 요청을 때려주기 위해 ajax를 선언하였다.

형식을 잘 살펴보길 바란다.

특히 data 부분에서 JSON으로 넘어온 녀석들을 다시 문자열로 바꿔주는 작업을 하고 있지?

필자가 괜히 글을 늘어뜨린게 아니라니까?

 

참고로 아직 완성은 아니다.

우리가 의도한대로 데이터가 잘 넘어오고 있는지 기능테스트를 잠깐 해보자.

 

 

야~ 기분 좋다!!!

우리가 의도한대로 필요한 데이터만 쏙! 쏙! 뽑아서 콘솔창에 응답해주고 있다.

itemDto 카고 트럭에 기본적으로 담겨있는 정보들은 당연히, 당연히 잘 전달해주고 있고..

상속받은 TimeStamped의 LocalDate 시간 데이터들도 정상적으로 응답받고 있다.

 

이런 식으로 다 끝났다고 생각하기 전에 콘솔 테스트까지 해보는 것을 습관화하도록 하자.

우리는 절대로 500 Internal... 이 날라오는 꼴을 봐선 안된다.

이제 안심하고 다음 단계로 넘어가도록 하자.

 

8-1-2) Modal 팝업창을 만들어주자!

 

 

짜잔~!

이 팝업창이 바로 modal이다.

modal의 개념 자체를 이해하기보다는..

필자가 왜 ajax로 요청처리를 따로 해줬는지를 고민해보자.

전체 구성에서 굳이 없어도 상관 없는 쿼리 아님?

 

예아. 그 말이 맞다.

하지만 ajax 안에 modal을 만들어 준 이유에는 무시할 수 없는 장점이 있기 때문이다.

지금 HTML 화면을 다시 한 번 집중해서 살펴봐라.

팝업창만 띄워줬을 뿐이지, 페이지가 이동한 흔적이 있는가?

하다못해 포워딩 (Forwarding)이 된 흔적이라든지?

 

없다. 그런거..

늘 서비스를 이용하는 입장에 있던 우리에게는 페이지가 넘어가고 그러한 것들이 대수가 아닐 수도 있다.

하지만 서버에서는 트래픽이 늘어날수록 그만큼의 비용이 요구되는 것이다.

그 트래픽을 최소화 시켜주기 위해 페이지 이동이 없는 팝업창을 클라이언트에 띄워줬다.

바로 ajax를 이용해서 말이다!

신박하지? 신박하지? 신박하다고 말해.. ㅡㅡ

 

function addProduct(itemDto) {
    /**
     * modal 뜨게 하는 법: $('#container').addClass('active');
     * data를 ajax로 전달할 때는 두 가지가 매우 중요
     * 1. contentType: "application/json",
     * 2. data: JSON.stringify(itemDto),
     */
    // 1. POST /api/products 에 관심 상품 생성 요청
    $.ajax({
        type: "POST",
        url: "/api/products",
        data: JSON.stringify(itemDto),
        contentType: "application/json",
        // Local Server: 요 놈이 JSON 맞는가? 검증해봐야겠다.
        success: function (response){
            // 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.id 로 설정 (숙제로 myprice 설정하기 위함)
            $('#container').addClass('active');
            // index.html: <div id="container">...</div>
            targetId = response.id;
            // let targetId;
            // 가장 최근에 저장된 관심상품의 id
        }
    });
}

 

최종적으로 완성된 소스코드는 다음과 같다.

JSON이 문자열 형태로 잘 응답되고 있는지를 보려고 consol.log() 메서드로 테스트를 해줬었지?

문제없이 필요한 데이터가 날라오고 있으므로 기존에 있던 콘솔 로그를 지워주고..

그 자리에 팝업창 생성을 위한 jQuery를 새롭게 선언해줬다.

 

왜 팝업창을 별도로 생성해줬는지는 필자가 위에서 설명한 바 있다.

나머지는 코멘트를 꼼꼼하게 읽어보면 금방 이해가 될 것이다.

이제 슬슬 클라이언트가 완성되어 가고 있다.

나머지도 기운내서 다뤄보도록 하자!

 


9. 사용자에게 관심상품 보여주기!

 

이렇게 관심상품을 등록하면...

 

 

이렇게 자동으로 모아보기 탭으로 이동하여 목록 (List)를 사용자에게 노출시켜 주겠다는 것이다.

물론 관심상품을 신규 등록하지 않아도 이전에 등록한 상품들을 자동으로 보여준다.

페이지 접속하자 마자 페이지가 로드되는 식으로 말이다!

기능만 놓고 보면 별 거 없지?

 

누.누.휘 말하는 거지만..

우리는 이미 한참 전에 백엔드 쪽 작업을 끝내놨다.

기본적으로 서버에서 200번 응답이 날라오고 있기 때문에

클라 개발자도 그것을 베이스로 개발을 할 수 있는 것이다.

 

지금부터 새로 추가할 기능 역시 클라이언트에서 할 일이다.

오류가 뜬다면 분명 400이 날라오겠지?

500번이 뜰 일은 없다는 말씀!

 

9-1) showProduct 함수 완성하기!

 

function showProduct() {
    /**
     * 관심상품 목록: #product-container
     * 검색결과 목록: #search-result-box
     * 관심상품 HTML 만드는 함수: addProductItem
     */
    // 1. GET /api/products 요청
    // 2. 관심상품 목록, 검색결과 목록 비우기
    // 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
}

 

페이지에 접속하자마자 관심상품 생성목록을 로드 해줘야 하기 때문에

이 녀석을 가장 먼저 완성할 것이다.

함수 이름이야 만드는 사람 마음이니 너무 큰 의미를 두지 말자.

다만 함께 작업하는 동료들이 한 눈에 알아볼 수 있게끔 만들어야 겠지?

그 보노보노같은 String jumin; 이딴거 말고..  제발 좀.. ㅡㅡ

 

function showProduct() {
    /**
     * 관심상품 목록: #product-container
     * 검색결과 목록: #search-result-box
     * 관심상품 HTML 만드는 함수: addProductItem
     */
    // 1. GET /api/products 요청
    $.ajax({
        type: "GET",
        url: "/api/products",
        success: function (response){
            // 2. 관심상품 목록, 검색결과 목록 비우기
            $('#product-container').empty();
            $('#search-result-box').empty();

            // 3. for 문마다 관심 상품 HTML 만들어서 관심상품 목록에 붙이기!
            for(let i=0 ; i<response.length ; i++){
                let product = response[i];
                let tempHtml =  addProductItem(product);
                $('#product-container').append(tempHtml);
            }
        }
    });
}

 

함수 하나를 뚝딱! 완성했다.

jQuery 몇 개로 말이다.

근본도, 체계도, 질서도 없는 언어 주제에 더럽게 편리하고 유용한 기능이다.

그럼 소스코드를 하나씩 살펴볼까?

 

url 주소가 /api/products인 녀석을 GET 방식으로 요청해야 한다고 했지?

이쯤 되면 더 고민할 것도 없이 ajax부터 호출해주면 된다.

요청이라는 녀석을 그냥 항상 머리속에 박아놓고 다녀라.

 

요청이 성공적으로 이루어지면 중괄호 이하의 값을 응답해달라고 하고 있네?

 

 

#product-container가 어떤 놈인지..

#search-result-box는 또 어떤 놈인지..

코멘트를 못믿겠다면 직접 HTML 창을 띄워서 개발자 도구를 열어봐라.

 

오히려 그렇게 일일이 확인하는 게 일을 확실하게 처리하는 방법일 수도 있다.

내가 직접 작업한 거라면 상관 없는데..

남들이 만든 작업물을 이름만 봐선 아리까리 할 수가 있잖슴?

필자 역시 저 HTML을 직접 만든 것은 아니기 때문에 굳이 개발자도구 열어서 확인해봤다.

그러니까 각 객체의 스코프가 어디부터 어디까지인지 확실히 알겠더라.

HTML에서는 일반적으로 <div></div> 태그 안에 다 때려박아 놨다.

 

그 놈들을 우선적으로 비워주라는 의미에서 $('#...').empty(); jQuery를 쓰고 있지?

JS에서 세미콜론은 굳이 안써줘도 되는데 첫 언어를 자바부터 배운 필자는 버릇이 그렇게 들여졌다.

 

그 다음으로는 익숙한 for문을 돌 차례이다.

product라는 이름으로 멤버변수를 선언해준 다음에 i번째로 응답받은 값을 저장하겠댄다.

쪼끔 이따가 다뤄볼 거지만

관심상품마다 HTML을 만들어주기 위해서 addProductItem(product) 메서드를 오버로드 해줬다.

 

여기서 중요한 것은 각 멤버변수들이 아니다.

맨 윗단 코멘트에 달아놓은 실제로 값들이 화면에 반영되는 '#' 이하 컨테이너 박스들이다.

자바의 내장객체와 같은 기능을 수행하는 셈이다.

마지막으로 실제 상품 목록의 데이터를 담고 있는

#product-container 내장객체에 tempHtml을 '추가 (append)' 해주겠단다.

 

엄밀히 따지면 자바의 내장객체와 아무런 상관이 없는 내용이겠지만..

필자는 자바를 먼저 배웠기 때문에 비슷한 개념을 그런 식으로 매칭을 해야 까먹질 않는다.

이제 addProductItem() 함수만 완성해주면 일단락 된다.

 

9-2) addProduct 함수 완성하기!

 

 

이번에도 뭔가 애매해서 개발자도구를 먼저 열어봤다.

관심상품의 목록 전체를 묶어주는 div class가 "product-card" 라는 이름으로 묶여있네?

우선 사전에 만들어놓았던 index.html 파일로 되돌아가자!

 

 

접어라.

 

 

복사해라.

통채로 가져갈 것이다.

 

어디로?

 

 

여기에다가 복붙해줄거읾..

지금은 가리워서 잘 안보이지만 return 다음 백틱 (``)안에 넣어줄 것이다.

왜???

그래야 jQuery를 자유자재로 쓸 수 있으니까!

슬슬 개념이 잡히지 않는가?

필자도 해당 강의 4번 정도 돌려보니까 쬐끔.. 알겠더라.

감이 더 좋으신 분들은 한 두번만 돌려봐도 개념이 잡힐 것이다.

아! 참고로 내가 듣고 있는 인강은 공짜가 아니지롱~ ㅋㅋㅋㅋ

 

function addProductItem(product) {
    // link, image, title, lprice, myprice 변수 활용하기
    return `<div class="product-card" onclick="window.location.href='https://spartacodingclub.kr'">
            <div class="card-header">
                
                     alt="">
            </div>
            <div class="card-body">
                <div class="title">
                    Apple 아이폰 11 128GB [자급제]
                </div>
                <div class="lprice">
                    <span>919,990</span>원
                </div>
                <div class="isgood">
                    최저가
                </div>
            </div>
        </div>`;
}

 

자! 이제 우리의 입맛대로 복사해온 이 녀석을 샥! 샥! 샥! 바꿔치기 하면 되는 것이다.

거 아무리 봐도 자바에서 쓰던 Request 내장객체 같단 말이지..

 

function addProductItem(product) {
    // link, image, title, lprice, myprice 변수 활용하기
    // Essentially Required Member Variable
    return `<div class="product-card" onclick="window.location.href='${product.link}'">
            <div class="card-header">
                <img src="${product.image}"
                     alt="">
            </div>
            <div class="card-body">
                <div class="title">
                    ${product.title}
                </div>
                <div class="lprice">
                    <span>${numberWithCommas(product.lprice)}</span>원
                </div>
                <div class="isgood ${product.lprice <= product.myprice? '' : 'none'}">
                    <!-- 최저가 (lprice < myprice) -->
                    <!-- 
                        if, 상품 가격이 사용자가 설정한 가격보다 같거나 낮으면 blank tab을 반환하고..
                        -> isgood 최저가 아이콘이 그대로 뜨게 하고! (class = "isgood")
                        else, 그렇지 않다면 'none' 클래스 (style.css)를 반환하겠다!
                        -> (class = "isgood none"): 안보여!
                     -->
                </div>
            </div>
        </div>`;
}

 

완성된 코드는 다음과 같다.

상품의 링크정보나 이미지, 제목, 가격정보는 모두 jQuery를 사용하여 내장객체처럼 전달해주고 있다.

하단 division class에 뭔가 복잡하게 이것저것 붙어있는거 보이는가?

코드를 간결하게 만들어주기 위해 if{}else{}를 삼항 연산자로 대체해준 것이다.

구조만 파악하면 어려울 거 없는 코드이다.

 

상품의 가격이 내가 설정한 최저가보다 같거나, 작으면?

css 스타일 시트에 있는 최저가 마크인 isgood을 고대로 노출해주겠다는 뜻이다.

만약 그렇지 않다면?

즉 내가 설정한 최저가가 상품의 가격보다 크다면?

class = "isgood none" 이라고 해서 마크를 보여주지 않겠다는 뜻이겠지?

 

if(lprice <= myprice){
	class = "isgood"
}else{
	class = "isgood none"
}

 

이런 조건문이랑 똑같은 구조를 가지고 있다.

5줄이나 써야 하는 코드를 한 줄로 줄여주기 때문에 삼항식은 필자 역시 자주 쓰는 기법이다.

코드는 최대한 간결하되, 동료들이 모두 알아볼 수 있게끔!

아직 아무것도 시작하지 않은 필자의 개발 철학이다..

 

자! 이제 서버를 재시작하여 클라이언트가 제대로 동작하는지 테스트 해보자.

 

 

empty 함수를 선언해준 대로 우선은 빈 화면이 뜨지? ㅎㅎㅎ

 

 

어디 최저가를 설정해볼까?

12,900원 짜리 상품이니..

그 이상으로 가격을 입력하면 모아두기 탭에서 반드시 떠야 한다.

 

 

우선은 뜨지?

13,000원보다 낮은 가격이니까!

 

근데 최저가 딱지는 왜 안붙음???

 

 

개발자 도구를 열어보자.

우리는 아직 최저가 설정 기능을 구현하지 않았기 때문에

default 값이 0으로 잡혀있다.

거기에다가 css 파일을 확인해보면..

 

.product-card .card-body .isgood {
    margin-top: 10px;
    padding: 10px 20px;
    color: white;
    border-radius: 2.6px;
    background-color: #ff8787;
    width: 42px;
}

.none {
    display: none;
}

 

아무 것도 보여주지 말라는 조건이 붙어있다.

최저가 딱지를 보여주지 마셈! 하면서 .none {diaplay: none;} 식으로 처리하고 있다.

 

거의 다 완성되었지만 아직까지 개선해야 할 부분이 좀 남아있다는 것이다.

 

오늘은 이 정도까지만 다뤄보고 다음 시간에 스케쥴러를 만들어보자!

 

오늘만 해도 벌써 두 개 챕터를 다뤘다..

너무 많은 사람들에게 신세를 졌고..

건강이 좋지않아 글을 읽을 수도, 쓸 수도 없다..

화장해라..

오래된 생각이다..

 


10. 스케쥴러를 만들어보자!

반갑다 아쎄이!

이번 시간에는 타임 스케쥴러라는 녀석을 만들어 볼 것이다.

 

그게 뭐야?

어렵게 생각할 거 없다.

모처럼 네이버 API랑 에플리케이션을 연동시켰는데..

못해도 상품 정보를 최신화하는 기능 정도는 구현해야 하지 않겠는가?

 

가령 사용자가 100만원으로 최저가를 등록한 상품이 이틀 뒤에는 90만원 밑으로 떨어진다면?

바로 가격 정보가 업데이트 되어야 한다.

필자는 매일 새벽 5시에 정보가 갱신되도록 만들어 볼 것이다.

 

어렵지 않으니 이번 시간에도 필자와 함께 기합차게 달려보자!

 

10-1) Schedular.java

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

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

    // 초, 분, 시, 일, 월, 주 순서
    @Scheduled(cron = "0 0 5 * * *")
    // A cron-like expression, extending the usual UN*X definition to include triggers on the second, minute, hour, day of month, month, and day of week.
    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);
            // 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);
        }
    }
}

와우.. 뭔가 복잡해 보이지?

그러니까 하나씩 끄집어내서 분석해보자.

로직 자체를 파악하는 게 중요하다.

 

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

    private final ProductRepository productRepository;
    private final ProductService productService;
    private final NaverShopSearch naverShopSearch;
    
    (생략...)
    
    }

 

어노테이션부터 살펴보자.

@RecuiredArgsConstructor는 이제 지겹도록 많이 봐 왔을거다.

Scheduler 클래스를 생성하면서 가장 먼저 final로 선언한 생성자가 줄줄이 세 놈이서 모여있다.

 

우리가 사전에 만들어 놓은 ProductRepository 자바 클래스.

데이터를 조회, 생성, 삭제하기 위해 필요한 인터페이스로 기억하고 있다.

 

역시 서버단을 구축하면서 만들어놓은 ProductService 자바 클래스.

전형적인 서비스 페이지지만 상품의 최저가 (myprice) 관련 정보를 업데이트 하기 위해서 만들었다.

 

NaverShopSearch 자바 클래스.

네이버 API로부터 발급받은 클라이언트 id와 password를 이용하여..

우선은 키워드로 검색을 실시한 상품의 결과 목록을 JSON 형태로 응답 해줘야 한다.

그 다음에는 데이터를 ItemDto 클래스로 반환시켜 줘야겠지?

 

기억이 가물가물하다면 이전에 올려놓은 소스코드를 참고하길 바란다.

필자도 이게 뭐였더라..? 하면서 다시 살펴보고 오는 길이다.

그래도 주석을 꼼꼼이 달아놔서 금방 알아볼 수 있었음..ㅎㅎ

 

또한 @Component를 통해 스프링에게 자체 권한을 부여한 것도 잊지 말아라.

 

// 초, 분, 시, 일, 월, 주 순서
@Scheduled(cron = "0 0 5 * * *")
// A cron-like expression, extending the usual UN*X definition to include triggers on the second, minute, hour, day of month, month, and day of week.
public void updatePrice() throws InterruptedException {
    System.out.println("가격 정보를 최신화 하고 있습니다!");
    
    (생략...)
    
}

 

상단 @Scheduled 라는 이름의 어노테이션부터 살펴보도록 하자.

저 녀석 역시 JPA에서 강제하는 클래스이기 때문에 무조건 저 형태로 써야 한다.

cron에 6자리로 되어있는 이상한 값들을 대입해주고 있지?

코멘트 달아놓은대로 초, 분, 시, 일, 월, 주 순서로 값을 기입할 수 있다.

양놈들은 우리랑 거꾸로 환산하니까 순서를 잘 기억해둬라.

주와 일은 신경쓰지 않고, 5시 00분 00초마다 상품 가격정보를 최신화 해주겠단다.

 

그 밑은 이러이러한 방식으로 예외 처리를 하겠다는 거니까 읽어만 봐라.

 

List<Product> productList = productRepository.findAll();
// SELECT * FROM PRODUCT

 

우선은 Product 자바 클래스에서 만들어놓은 멤버 변수들을 배열 형태로 선언할건데..

상품의 전체 카테고리를 조회하기 위해 productRepository 메서드를 오버로드 하여 쓰고 있다.

sql 문으로 치면 SELECT * FROM PRODUCT가 되겠지.

 

for (int i=0; i<productList.size(); i++) {
    // 1초에 한 상품 씩 조회합니다 (Naver 제한)
    // 너무 짧은 시간동안 요청을 남발하면 네이버 자체적으로 서비스를 정지시킴..
    TimeUnit.SECONDS.sleep(1);

 

다음.. 생소한 녀석들은 외울려고 하지 말고 기능 자체에 집중해라.

코멘트에 달아놓았듯이 서비스가 막혀버리는 불상사를 막기 위해 부득불 1초간 타임슬립을 해주고 있다.

 

// i 번째 관심 상품을 꺼냅니다.
Product p = productList.get(i);

 

for문 돌고 있던거 기억하지?

i번째 녀석을 꺼내주고 싶나보다.

 

// i 번째 관심 상품의 제목으로 검색을 실행합니다.
String title = p.getTitle();
String resultString = naverShopSearch.search(title);

 

우리는 @Getter를 선언한 적이 없는데?

스읍.. @Component 안보이냐?

스프링에서 알아서 꺼내 쓴거다.

 

resultString으로 멤버변수를 선언하고..

naverShopSearch 메서드를 오버로드하여 상품의 제목으로 검색을 실행하겠다고 한다.

하나씩 뜯어보니까 별 거 없지?

 

// i 번째 관심 상품의 검색 결과 목록 중에서 첫 번째 결과를 꺼냅니다.
List<ItemDto> itemDtoList = naverShopSearch.fromJSONtoItems(resultString);
ItemDto itemDto = itemDtoList.get(0);

 ItemDto 자바 클래스에 담긴 멤버변수들을 배열 형태로 선언해주고..

JSON 형식의 데이터들을 Dto 클래스에 문자혈 형태로 저장해주고자 한다.

그 중에서 첫 번째 실행결과를 끄집어 내고 싶은거고..

 

// i 번째 관심 상품 정보를 업데이트합니다.
Long id = p.getId();
productService.updateBySearch(id, itemDto);

i 번째로 끄집어 낸 관심상품의 정보를 컬럼 전체에서 업데이트 해줄건데..

우리는 아직 updateBySearch 메서드를 만들어주지 않았다.

이제부터 만들어주러 갈거다.

 

10-2) ProductService Modified

 

@Transactional
// pstmt.updateQuery();
public void updateBySearch(Long id, ItemDto itemDto){
    Product product = productRepository.findById(id).orElseThrow(
            () -> new NullPointerException("해당 Id가 존재하지 않습니다!")
    );
    product.updateByItemDto(itemDto);
    // return id;
}

 

이전에 만들어놓은 ProductService 자바 클래스를 살짝 수정해줬다.

updateBySearch 메서드를 써먹어야 할 거 아니겠노?

그래서 서비스 페이지에 추가해줬다.

그런데 이번에는 또 updateByItemDto에서 빨간줄이 뜨네?

만들러 가면 되지 모..

 

 

ALT + ENTER!

 

public void updateByItemDto(ItemDto itemDto) {
}

그러면 Product 자바 클래스에 자동으로 메서드가 생성된다.

저 놈을 조금만 손 봐주면 되겠지?

 

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

 

요렇게 최저가 정보는 업데이트 해주자.

 

이렇게 되면 Scheduler 자바 클래스가 완성되었다.

하지만 아직 끝난게 아니다.

 

@SpringBootApplication // Hey, Java! I will use Spring Boot Framework!
@EnableJpaAuditing // Enable TimeStamped
@EnableScheduling // Scheduling JPA Activate!

public class MyselectshopApplication {

    public static void main(String[] args) {

        SpringApplication.run(MyselectshopApplication.class, args);
    }

}

MySelectshop 에플리케이션 클래스로 돌아가서 딱 하나만 추가해줬다.

@EnableScheduling!

뭔지 모르겠으면 마우스를 슬쩍 갖다 놔보자.

 

Enables Spring's scheduled task execution capability, similar to functionality found in Spring's <task:*> XML namespace. To be used on @Configuration classes as follows:
  @Configuration
  @EnableScheduling
  public class AppConfig {
 
      // various @Bean definitions
  }

 

스프링에게 스케쥴러를 다룰 권한을 부여해줬다고 생각하면 쉽다.

 

여기까지 완성시켜주면 상품정보가 매일 새벽 5시마다 갱신될 것이다.

못 믿겠으면 1분 후로 cron 값을 변경해보고 실험해보면 되겠지?

코딩을 할 때 뭐든 의심부터 하고 보는 습관 나쁘지 않다.

남들보다 느리게 가는 거 같아도, 사실은 그게 가장 빨리 가는 길이다.

필자가 아직 이쪽 업계로 들어오진 않았지만 엔지니어링은 뭐든 궤를 같이 한다고 본다.

괜찮겠지~ 하고 넘어가면 바로 설계사고로 이어지는 거시여..

 

이제 가장 중요한 상품의 최저가 설정으로 넘어가보도록 하자.

 


11. 최저가로 관심가격을 설정하자!

사실상 여태까지 우리가 구현한 에플리케이션의 핵심 기능이다.

긴 말 할 거 없이 클라이언트 화면부터 확인해보자.

 

 

페이몬이 35,900원이니까..

값이 제대로 조회되고 있는지 확인하기 위해서 36,000원으로 등록해줬다.

 

 

떴지? ㅎㅎㅎㅎㅎ

떴다아!!!

서버는 물론이거니와 클라이언트도 정상적으로 작동하고 있다.

로직은 어떻게 돌아가고 있는지 소스코드를 통해 살펴보도록 하자.

 

function setMyprice() {
    /**
     * 숙제! myprice 값 설정하기.
     * 1. id가 myprice 인 input 태그에서 값을 가져온다.
     * 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
     * 3. PUT /api/product/${targetId} 에 data를 전달한다.
     *    주의) contentType: "application/json",
     *         data: JSON.stringify({myprice: myprice}),
     *         빠뜨리지 말 것!
     * 4. 모달을 종료한다. $('#container').removeClass('active');
     * 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
     * 6. 창을 새로고침한다. window.location.reload();
     */

 

우선 요 놈을 순차적으로 완성시켜줘야 한다.

얼핏 어려워보일 수도 있는데, 사실상 문제에 답이 다 나와 있다.

필자와 함께 차근 차근 파헤쳐보자.

 

 

// 1. id가 myprice 인 input 태그에서 값을 가져온다.
let myprice = $('#myprice').val();

 

자바랑 똑같이 JS 환경에서도 변수를 선언한 것 뿐이다.

id 값이 myprice인 녀석의 값을 myprice 변수에 저장하겠다고 하네?

다음.

 

// 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
if (myprice == '') {
    alert('올바른 가격을 입력해주세요!');
    return;
}

 

 

요런 화면을 띄어주겠다는 것이다.

 

200,000은 HTML에서 디폴트로 입력된 값이고..

사실상 사용자가 아무 것도 입력해주지 않는 상황

이라고 할 수 있겠지만 이미 공백 탭을 입력해준 것과 마찬가지이다.

myprice == null로 처리하면 그냥 값이 존재하지 않는 것이므로 주의바람!

벌써 두 번째 언급하는 것 같다.

 

// 3. PUT /api/product/${targetId} 에 data를 전달한다.
$.ajax({
    type: "PUT",
    url: `/api/products/${targetId}`,
    contentType: "application/json",
    data: JSON.stringify({myprice: myprice}),

 

클라이언트가 서버에게 PUT을 요청!

그럼 이젠 자동으로 ajax가 튀어 나와야 한다.

아직도 뭐가 뭔지 감이 잡히지 않는 독자 분들은.. 기열..!

 

내부 형태는 이전에 다룬 적이 있으므로 따로 설명하지는 않겠다.

깃허브 리드미에서도 링크 타고 들어올 수 있는데 코드리뷰를 대충대충 봐?

필자와 함께 공부한다는 느낌으로 봐라.

본인은 승모근 통증을 참아가며 포스팅 올리는 것이다.

 

success: function (response) {
    // 4. 모달을 종료한다. $('#container').removeClass('active');
    $('#container').removeClass('active');
    // 5. 성공적으로 등록되었음을 알리는 alert를 띄운다.
    alert('성공적으로 등록되었습니다!');
    // 6. 창을 새로고침한다. window.location.reload();
    window.location.reload();

 

요청이 성공적으로 이루어진다면?

modal의 id값인 #container를 클래스에서 제거해주는 것을 활성화시켜도 되는지의 여부를 지금 이 자리에서 여쭤볼 수 있는지의 여부를..

 

myprice 설정이 제대로 이루어졌다면 성공적으로 등록되었다는 식의 알람이 뜰 것이다.

 

동시에 창을 자동으로 새로고침하여 모아보기 탭으로 가서 목록을 보여주면 딱일 것이다.

 


 

여기까지 정말 숨가쁘게 달려왔다.

서버단을 구축하면서 API 테스트도 완벽하게 해봤고,

이러쿵 저러쿵 클라이언트 화면도 문제 없이 동작하고 있음을 확인했다.

 

하지만 우리가 만든 에플리케이션에는 심각한 결함이 있다.

소스코드에 결함이 있다는 게 아니다.

 

첫째, 서버를 재시작 할 때마다 기존 데이터가 삭제되는 문제

둘째, 오직 필자의 PC에서만 웹 에플리케이션에 접속할 수 있는 문제

 

그 외에도 내용을 다루다 보면 계속 나올 것일 터인데..

가장 중요한 이슈는 내부 서버망 (Localhost)에서만 셀렉샵을 이용할 수 있다는 것이다.

 

다음 시간에는 이것을 '퍼블리싱 (Publishing)' 하는 과정을 살펴볼 것이다.

셀렉샵의 주소만 알고 있으면 누구나 웹에 접근 할 수 있게끔 배포해주는 과정 말이다.

 

남아있는 내용은 진짜 재밌으니까 끝까지 필자와 달려보자.

 


 

12. 나만의 셀렉샵을 배포하자!

말인 즉슨, 여태까지 우리가 지지고 볶고 하면서 만들었던 셀렉샵을

Domain으로 배포해서 모두가 접근할 수 있게끔 만들어 주겠다는 것이다.

여태까지는 내 컴퓨터, 즉 로컬 서버에서 테스트만 하는 수준이었으니까!

 

그 전에 몇 가지 이슈를 정리해보자

 

12-1) 서버를 재시작하면 기존 데이터가 모두 삭제되요.. ㅠㅠ

그럴 것이다.

우리는 여태까지 H2 Database 서버에서만 작업했기 때문이다.

에플리케이션을 배포하기 전까지는 테스트용으로 안성맞춤이었다.

앞으로는 그 놈을 H2가 아니라 MySQL DB에서 가동시킬 것이다.

MySQL은 관계형 데이터베이스 (RDBMS)이므로, AWS RDS에 서비스를 설치해야 한다.

거기까지만 완료해도 더 이상 데이터가 리셋되는 일은 없을 것이다.

 

12-2) 링크만 올라가니까 뭔가 허전한데쑤..?

그럴 것이다.

요즘 링크를 복사해서 붙여넣기 하면...

 

 

어지간해선 이런 화면이 뜰 것이다.

링크만 딸랑 올라가는 시대는 애진즉에 지나가버렸다.

저런 방식으로 링크를 공유해주기 위해서는 HTML에서 <og></og> 라는 Tag를 생성해줘야 한다.

이것 역시 다뤄보도록 하자.

 

12-3) http://localhost:8090... 왜 접속이 안되지?

그럴 수밖에..

로컬 호스트 자체가 내부 서버망이다.

게다가 IDE 스프링 부트 서버까지 정지된 상태라면 당연히 접속이 안될 것이다.

고 놈을 상시 접근할 수 있게 EC2 라는 컴퓨터를 장만해줄 것이다.

스포일러는 여기까지...

 


글이 너무 길어지다 보니까 타이핑 하나 치는데 로드되는 시간이 너무 길다.

배포하는 과정은 따로 올리도록 하겠다.

여기까지 같이 따라오느라 정말 정말 고생 많으셨다.

또 뵙도록 하겠다.