์ค๊ณ ๋ฐ ํ๋ก์ ํธ ์์ฑ/์ค์
์๊ตฌ์ฌํญ ํ์
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;
}
}

๊ด์ฌ ์ํ 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

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

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;
}

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);
}