๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๋ฐฑ์—”๋“œ/spring&springboot

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„_Spring Boot ์ˆ™๋ จ์ฃผ์ฐจ] My Select Shop(์ƒํ’ˆ ๊ฒ€์ƒ‰, ๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก, ๊ด€์‹ฌ ์ƒํ’ˆ ํฌ๋ง ์ตœ์ €๊ฐ€ ์ˆ˜์ •, ๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒ API ๊ตฌํ˜„, Scheduler ๊ตฌํ˜„)

by alswlfl 2026. 6. 30.

์„ค๊ณ„ ๋ฐ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ/์„ค์ •

์š”๊ตฌ์‚ฌํ•ญ ํŒŒ์•…

1. ํ‚ค์›Œ๋“œ๋กœ ์ƒํ’ˆ์˜ ์ •๋ณด ๊ฒ€์ƒ‰

  • ๋„ค์ด๋ฒ„ ์‡ผํ•‘ API ์ด์šฉ
  • ์ƒํ’ˆ ์ด๋ฆ„(title), ๋งํฌ URL(link), ์ด๋ฏธ์ง€ URL(image), ์ตœ์ €๊ฐ€(price)

2. ๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋กํ•˜๊ธฐ

  • DB์— ์ƒํ’ˆ์ •๋ณด ์ž…๋ ฅ(INSERT)
    • ์ƒํ’ˆ ์ด๋ฆ„(title), ๋งํฌ URL(link), ์ด๋ฏธ์ง€ URL(image), ์ตœ์ €๊ฐ€(price)

3. ๊ด€์‹ฌ ์ƒํ’ˆ์˜ "ํฌ๋ง ์ตœ์ €๊ฐ€" ์„ค์ •ํ•˜๊ธฐ

  • DB ์—…๋ฐ์ดํŠธ(UPDATE): ๋“ฑ๋ก๋œ ๊ด€์‹ฌ์ƒํ’ˆ์˜ "ํฌ๋ง ์ตœ์ €๊ฐ€"๋งŒ ์—…๋ฐ์ดํŠธ

4. ๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒํ•˜๊ธฐ

  • DB ์กฐํšŒ(Select)
  • UI์— "์ตœ์ €๊ฐ€" ํ‘œ์‹œ ์กฐ๊ฑด: ํฌ๋งํ•˜๋Š” ์ตœ์ €๊ฐ€๋ณด๋‹ค ์‹ค์ œ ์ตœ์ €์ž๊ฐ€ ๋‚ฎ์€ ๊ฒฝ์šฐ

API ์„ค๊ณ„

๊ธฐ๋Šฅ Method URL ๋ฐ˜ํ™˜
์ƒํ’ˆ ๊ฒ€์ƒ‰ API GET /api/search?query=๊ฒ€์ƒ‰์–ด List<ItemDto>
๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋กํ•˜๊ธฐ POST /api/products ProductResponseDto
๊ด€์‹ฌ ์ƒํ’ˆ์˜ ํฌ๋ง ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ PUT /api/products/{id} ProductResponseDto
๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒํ•˜๊ธฐ GET /api/products List<ProductResponseDto>

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

1. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์‹œ ์ข…์†์„ฑ ์ถ”๊ฐ€

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์‹œ ์ข…์†์„ฑ ์ถ”๊ฐ€

2. build.gradle ๋‚ด JWT์™€ json ์˜์กด์„ฑ ์ถ”๊ฐ€

// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// json
implementation 'org.json:json:20230227'

 

3. application.properties ๋‚ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •๋ณด ์ถ”๊ฐ€

spring.datasource.url=jdbc:mysql://localhost:3306/shop
spring.datasource.username=root
spring.datasource.password={๋น„๋ฐ€๋ฒˆํ˜ธ}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

 

4. controller > HomeController ์ถ”๊ฐ€

package com.sparta.myselectshop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("/")
    public String home() {
        return "index";
    }
}

 

5. templates > index.html, static > css, js, images ๋„ฃ๊ธฐ

๊ฒฐ๊ณผ ํ™”๋ฉด


Naver OpenAPI๋ฅผ ํ™œ์šฉํ•œ ์ƒํ’ˆ ๊ฒ€์ƒ‰ API

1. naver > controller > NaverApiController์™€ naver > dto > ItemDto ์ƒ์„ฑ

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class NaverApiController {
    private final NaverApiService naverApiService;

    @GetMapping("/search")
    public List<ItemDto> searchItems(@RequestParam String query) {
        return naverApiService.searchItems(query);
    }
}
@Getter
@NoArgsConstructor
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");
    }
}

 

2. naver > service > NaverApiService ์ƒ์„ฑ

@Slf4j(topic = "NAVER API")
@Service
public class NaverApiService {

    private final RestTemplate restTemplate;

    public NaverApiService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    public List<ItemDto> searchItems(String query) {
        // ์š”์ฒญ URL ๋งŒ๋“ค๊ธฐ
        URI uri = UriComponentsBuilder
                .fromUriString("https://openapi.naver.com")
                .path("/v1/search/shop.json")
                .queryParam("display", 15)
                .queryParam("query", query)
                .encode()
                .build()
                .toUri();
        log.info("uri = " + uri);

        RequestEntity<Void> requestEntity = RequestEntity
                .get(uri)
                .header("X-Naver-Client-Id", "{Client-Id}")
                .header("X-Naver-Client-Secret", "{Client-Secret}")
                .build();

        ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

        log.info("NAVER API Status Code : " + responseEntity.getStatusCode());

        return fromJSONtoItems(responseEntity.getBody());
    }

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

        for (Object item : items) {
            ItemDto itemDto = new ItemDto((JSONObject) item);
            itemDtoList.add(itemDto);
        }

        return itemDtoList;
    }
}

 

Naver OpenAPI ํ™œ์šฉํ•œ ์ƒํ’ˆ ๊ฒ€์ƒ‰ API ๊ธฐ๋Šฅ ๊ฒฐ๊ณผ


๊ด€์‹ฌ ์ƒํ’ˆ API ๊ตฌํ˜„

๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก API

๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก API ๋กœ์ง

1. Entity์™€ Dto ๋งŒ๋“ค๊ธฐ

  • entity > Product ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ
@Entity // JPA๊ฐ€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” Entity ํด๋ž˜์Šค ์ง€์ •
@Getter
@Setter
@Table(name = "product") // ๋งคํ•‘ํ•  ํ…Œ์ด๋ธ”์˜ ์ด๋ฆ„์„ ์ง€์ •
@NoArgsConstructor
public class Product extends Timestamped {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    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.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
    }
}
  • dto > ProductRequestDto์™€ ProductResponseDto ์ƒ์„ฑ
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductRequestDto {
    // ๊ด€์‹ฌ์ƒํ’ˆ๋ช…
    private String title;
    // ๊ด€์‹ฌ์ƒํ’ˆ ์ธ๋„ค์ผ image URL
    private String image;
    // ๊ด€์‹ฌ์ƒํ’ˆ ๊ตฌ๋งค๋งํฌ URL
    private String link;
    // ๊ด€์‹ฌ์ƒํ’ˆ์˜ ์ตœ์ €๊ฐ€
    private int lprice;
}
@Getter
@NoArgsConstructor
public class ProductResponseDto {
    private Long id;
    private String title;
    private String link;
    private String image;
    private int lprice;
    private int myprice;

    public ProductResponseDto(Product product) {
        this.id = product.getId();
        this.title = product.getTitle();
        this.link = product.getLink();
        this.image = product.getImage();
        this.lprice = product.getLprice();
        this.myprice = product.getMyprice();
    }
}
  • entity > Timestamped ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}

 

2. controller > ProductController ์ƒ์„ฑ

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ProductController {

    private final ProductService productService;

    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) {
        return productService.createProduct(requestDto);
    }
}

 

3. service > ProductService ์ƒ์„ฑ

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    public ProductResponseDto createProduct(ProductRequestDto requestDto) {
        Product product = productRepository.save(new Product(requestDto));
        
        return new ProductResponseDto(product);
    }
}

 

4. repository > ProductRepository ์ƒ์„ฑ

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

 

๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก API ํ˜ธ์ถœ
ํ˜ธ์ถœ ํ›„ ๊ฒฐ๊ณผ

๊ด€์‹ฌ์ƒํ’ˆ ํฌ๋ง ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ API

๊ด€์‹ฌ ์ƒํ’ˆ ํฌ๋ง ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ API ๋กœ์ง

1. controller > ProductController์™€ dto > ProductMyPriceRequestDto ์ถ”๊ฐ€

@Getter
@Setter
public class ProductMypriceRequestDto {
    private int myprice;
}
@PutMapping("/products/{id}")
public ProductResponseDto updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) {
    return productService.updateProduct(id, requestDto);
}

 

2. service > ProductService ๋‚ด ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

  • Product ์—”ํ‹ฐํ‹ฐ ๋‚ด update ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
...
@Entity
public class Product extends Timestamped {

    ...

    public void update(ProductMypriceRequestDto requestDto) {
        this.myprice = requestDto.getMyprice();
    }
}
@Transactional // Dirty Checking ์ˆ˜ํ–‰
public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
    int myprice = requestDto.getMyprice(); // myprice ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ(์กฐ๊ฑด: myprice๊ฐ€ 100์› ์ด์ƒ์ด์–ด์•ผํ•จ)

    if(myprice < MIN_MY_PRICE) {
        throw new IllegalArgumentException("์œ ํšจํ•˜์ง€ ์•Š์€ ๊ด€์‹ฌ ๊ฐ€๊ฒฉ์ž…๋‹ˆ๋‹ค. ์ตœ์†Œ "+MIN_MY_PRICE+"์› ์ด์ƒ์œผ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”.");
    }

    // ํ•ด๋‹น ์ƒํ’ˆ์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
    Product product = productRepository.findById(id).orElseThrow(() ->
            new NullPointerException("ํ•ด๋‹น ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));

    product.update(requestDto); // ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ˆ˜ํ–‰

    return new ProductResponseDto(product);
}

๊ด€์‹ฌ ์ƒํ’ˆ ํฌ๋ง ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ API ํ˜ธ์ถœ ํ›„ ๊ฒฐ๊ณผ

๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ API

๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒ API ๋กœ์ง

1. controller > ProductController ๋‚ด ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

@GetMapping("/products")
public List<ProductResponseDto> getProducts() {
    return productService.getProducts();
}

 

2. service > ProductService ๋‚ด ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

public List<ProductResponseDto> getProducts() {
    List<Product> productList = productRepository.findAll();
    List<ProductResponseDto> responseDtoList = new ArrayList<>();

    for (Product product : productList) {
        responseDtoList.add(new ProductResponseDto(product));
    }
    return responseDtoList;
}

๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒ API ํ˜ธ์ถœ ๊ฒฐ๊ณผ


Scheduler(์Šค์ผ€์ฅด๋Ÿฌ) ๊ตฌํ˜„

ex) ๋งค์ผ ์ƒˆ๋ฒฝ 1์‹œ์— ๊ด€์‹ฌ ์ƒํ’ˆ ๋ชฉ๋ก ์ œ๋ชฉ์œผ๋กœ ๊ฒ€์ƒ‰ํ•ด์„œ ์ตœ์ €๊ฐ€ ์ •๋ณด ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ธฐ๋Šฅ

 

@Scheduled ์–ด๋…ธํ…Œ์ด์…˜๊ฐ€ ๋‹ฌ๋ ค์žˆ๋Š” ๋ฉ”์„œ๋“œ๋Š” ์ง€์ •ํ•œ ํŠน์ • ์‹œ๊ฐ„๋งˆ๋‹ค ๋ฉ”์„œ๋“œ ๋™์ž‘ํ•จ

  • Scheduled ๊ธฐ๋Šฅ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Spring์—๊ฒŒ ์•Œ๋ ค์ค˜์•ผํ•จ
@EnableScheduling // ์„ค์ • ์ถ”๊ฐ€
@EnableJpaAuditing
@SpringBootApplication
public class MyselectshopApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyselectshopApplication.class, args);
    }
}
  • scheduler > Scheduler ํด๋ž˜์Šค ์ƒ์„ฑ
@Slf4j(topic = "Scheduler")
@Component
@RequiredArgsConstructor
public class Scheduler {

    private final NaverApiService naverApiService;
    private final ProductService productService;
    private final ProductRepository productRepository;

    // ์ดˆ, ๋ถ„, ์‹œ, ์ผ, ์›”, ์ฃผ ์ˆœ์„œ
    @Scheduled(cron = "0 0 1 * * *") // ๋งค์ผ ์ƒˆ๋ฒฝ 1์‹œ
    public void updatePrice() throws InterruptedException {
        log.info("๊ฐ€๊ฒฉ ์—…๋ฐ์ดํŠธ ์‹คํ–‰");
        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            // 1์ดˆ์— ํ•œ ์ƒํ’ˆ ์”ฉ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค (NAVER ์ œํ•œ)
            TimeUnit.SECONDS.sleep(1);

            // i ๋ฒˆ์งธ ๊ด€์‹ฌ ์ƒํ’ˆ์˜ ์ œ๋ชฉ์œผ๋กœ ๊ฒ€์ƒ‰์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
            String title = product.getTitle();
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);

            if (itemDtoList.size() > 0) {
                ItemDto itemDto = itemDtoList.get(0);
                // i ๋ฒˆ์งธ ๊ด€์‹ฌ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
                Long id = product.getId();
                try {
                    productService.updateBySearch(id, itemDto);
                } catch (Exception e) {
                    log.error(id + " : " + e.getMessage());
                }
            }
        }
    }

}
  • cron ๋ช…๋ น์–ด๋Š” ์šด์˜์ฒด์ œ์—์„œ ์–ด๋–ค ํŠน์ • ์‹œ๊ฐ„๋งˆ๋‹ค ์–ด๋–ค ์ž‘์—…์„ ์ž๋™ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ช…๋ น์–ด
  • cron ํ‘œํ˜„์‹ 
 

CronExpression (Spring Framework 7.0.8 API)

Determine whether the given string represents a valid cron expression.

docs.spring.io

  • Product ์—”ํ‹ฐํ‹ฐ ๋‚ด ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
public void updateByItemDto(ItemDto itemDto) {
    this.lprice = itemDto.getLprice();
}
  • Service ๋‚ด ์ตœ์ €๊ฐ€ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
@Transactional
public void updateBySearch(Long id, ItemDto itemDto) {
    Product product = productRepository.findById(id).orElseThrow(() ->
            new NullPointerException("ํ•ด๋‹น ์ƒํ’ˆ์€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
    product.updateByItemDto(itemDto);
}