//JPA 를 사용한다면, 이런 반복되고 긴 코드를 사용할 필요가 없어진다
package com.sparta.myselectshopbeta.controller;
import com.sparta.myselectshopbeta.dto.ProductMypriceRequestDto;
import com.sparta.myselectshopbeta.dto.ProductRequestDto;
import com.sparta.myselectshopbeta.dto.ProductResponseDto;
import com.sparta.myselectshopbeta.entity.Product;
import org.springframework.web.bind.annotation.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api")
public class AllInOneController {
//----------Repository 로 이동!! (getProducts() 메소드를 사용해서)----------------------
// 관심 상품 등록하기
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
//----------service 로 이동!!---------------------------------------------------------
// 요청받은 DTO 로 DB에 저장할 객체 만들기
Product product = new Product(requestDto);
//------------------------------------------------------------------------------------
// DB 연결
//저번(jpaRepository 를 사용했던)과는 다르게, Connection 과 DriverManager 를 이용해서 직접 DB 에 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:db", "sa", "");
// DB Query 작성
//직법 쿼리도 작성해야 했음
PreparedStatement ps = connection.prepareStatement("select max(id) as id from product"); //JPA 에서는 Id 값을 찾아서 넣지 않아도, @GeneratedValue(strategy = GenerationType.IDENTITY) 에 의해 자동으로 생성된다
ResultSet rs = ps.executeQuery();
if (rs.next()) {
// product id 설정 = product 테이블의 마지막 id + 1
product.setId(rs.getLong("id") + 1);
} else {
throw new SQLException("product 테이블의 마지막 id 값을 찾아오지 못했습니다.");
}
ps = connection.prepareStatement("insert into product(id, title, image, link, lprice, myprice) values(?, ?, ?, ?, ?, ?)");
//아래의 값들이 values(?, ?, ?, ?, ?, ?) 안에 들어가게 됨
ps.setLong(1, product.getId());
ps.setString(2, product.getTitle());
ps.setString(3, product.getImage());
ps.setString(4, product.getLink());
ps.setInt(5, product.getLprice());
ps.setInt(6, product.getMyprice());
// DB Query 실행
ps.executeUpdate();
// DB 연결 해제
ps.close(); //PreparedStatement 도 직접 닫아주기
connection.close(); //Connection 도 직접 닫아주기
//------------------------------------------------------------------------------------
// 응답 보내기
return new ProductResponseDto(product);
}
//----------Repository 로 이동!! (getProduct() 메소드를 사용해서)------------------------
// 관심 상품 조회하기
@GetMapping("/products")
public List<ProductResponseDto> getProducts() throws SQLException {
List<ProductResponseDto> products = new ArrayList<>();
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:db", "sa", "");
// DB Query 작성 및 실행
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("select * from product");
// DB Query 결과를 상품 객체 리스트로 변환
while (rs.next()) { //List 라서 while 문을 돌려서 가져옴
Product product = new Product(); //객체 product 생성
product.setId(rs.getLong("id"));
product.setImage(rs.getString("image"));
product.setLink(rs.getString("link"));
product.setLprice(rs.getInt("lprice"));
product.setMyprice(rs.getInt("myprice"));
product.setTitle(rs.getString("title"));
products.add(new ProductResponseDto(product)); //products 는 ProductResponseDto 를 List 형식으로 만들어놓은 ArrayList 배열
}
// DB 연결 해제
rs.close();
connection.close();
//------------------------------------------------------------------------------------
// 응답 보내기
return products;
}
//----------Repository 로 이동!! (updateProducts() 메소드를 사용해서)--------------------
// 관심 상품 최저가 등록하기
@PutMapping("/products/{id}")
public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
Product product = new Product(); //select * from product where id = ? 를 사용해서, product 객체 안에 넣음
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:db", "sa", "");
// DB Query 작성
//where 문을 사용해서, 내가 등록한 product 의 id 도 같이 넣어줌
//basic.js 에 보면, let targetId 가 전역으로 걸려있는데, 변수 targetId 안에 내가 선택한 product 의 id 가 들어가서 가져올 수 있는 것
//@PathVariable Long id 로 가져옴
PreparedStatement ps = connection.prepareStatement("select * from product where id = ?");
ps.setLong(1, id);
// DB Query 실행
ResultSet rs = ps.executeQuery();
if (rs.next()) {
product.setId(rs.getLong("id"));
product.setImage(rs.getString("image"));
product.setLink(rs.getString("link"));
product.setLprice(rs.getInt("lprice"));
product.setMyprice(rs.getInt("myprice"));
product.setTitle(rs.getString("title"));
} else {
throw new NullPointerException("해당 아이디가 존재하지 않습니다.");
}
// DB Query 작성
//update 의 where 에 받아온 id 값을 넣음
ps = connection.prepareStatement("update product set myprice = ? where id = ?");
ps.setInt(1, requestDto.getMyprice());
ps.setLong(2, product.getId());
// DB Query 실행
ps.executeUpdate();
// DB 연결 해제
rs.close();
ps.close();
connection.close();
//------------------------------------------------------------------------------------
// 응답 보내기 (업데이트된 상품 id)
return product.getId();
}
}
AllInOneController 의 한계점 재확인
코드 이해가 어려움: 처음부터 끝까지 다 읽어야 코드 내용을 이해할 수 있음
--> 3개의 클래스에 역할 별로 코드가 정리됨
현업에서는 코드 추가 혹은 변경 요청이 계속 생김 (--> Controller 의 역할)
1) 관심 상품 등록 시 Client 에게 응답 (Response) 하는 값 변경
: 등록된 Product 전체 정보 → 등록된 Product 의 id
2) 최저가 (Myprice) 업데이트 조건 변경 (--> Service 의 역할)
: Client 가 최저가를 0원 이하로 입력 → 에러 발생
3) DB 테이블 이름 변경 (--> Repository 의 역할)
: Product 테이블의 lprice → lowprice 변경
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.service.ProductService;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLException;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ProductController {
// 멤버 변수 선언 --> 이 코드로 객체 중복 생성 문제 해결! ----------------------------
private final ProductService productService;
public ProductController() {
this.productService = new ProductService();
}
//------------------------------------------------------------------
// 관심 상품 등록하기
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
// 응답 보내기
//Client 에게서 받아온 값 requestDto 을 productService 에 연결해서 넣어줌
return productService.createProduct(requestDto);
}
// 관심 상품 조회하기
@GetMapping("/products")
public List<ProductResponseDto> getProducts() throws SQLException {
// 응답 보내기
return productService.getProducts();
}
// 관심 상품 최저가 등록하기
@PutMapping("/products/{id}")
public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
// 응답 보내기 (업데이트된 상품 id)
return productService.updateProduct(id, requestDto);
}
}
package com.sparta.myselectshop.service;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.repository.ProductRepository;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.util.List;
@Component
public class ProductService {
//DB에 저장하기 위해 DB 와 연결하는 Repository 를 만듦 --> 모든 각 메소드에 이 코드가 들어갔었음 --> 반복되는 코드!
//ProductRepository productRepository = new ProductRepository();
// 멤버 변수 선언 --> 이 코드로 객체 중복 생성 문제 해결! ----------------------------
private final ProductRepository productRepository;
public ProductService() {
this.productRepository = new ProductRepository();
}
//------------------------------------------------------------------
public ProductResponseDto createProduct(ProductRequestDto requestDto) throws SQLException {
// 요청받은 DTO 로 DB에 저장할 객체 만들기
// Controller 에서 넘겨준 requestDto 파라미터값을 받아서 DB 에 저장할 객체 product 를 만듦
Product product = new Product(requestDto);
//Repository 쪽으로 만들었던 객체 product 를 보내줌
return productRepository.createProduct(product);
}
public List<ProductResponseDto> getProducts() throws SQLException {
return productRepository.getProducts();
}
public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) throws SQLException {
//Product 객체를 만들고, Repository 쪽에서 getProduct 메서드를 통해 확인해야할 id 를 넘겨주고, DB 에 접속해서 확인하고, 반환되면 Product 객체에 담는다
Product product = productRepository.getProduct(id);
//DB 에 product 가 없는 상태라면
if(product == null) {
throw new NullPointerException("해당 상품은 존재하지 않습니다.");
}
//DB 에 product 가 있는 상태라면
return productRepository.updateProduct(product.getId(), requestDto);
}
}
DI, IoC, Bean: https://velog.io/@baekgom/DI-IoC-Bean
Contoller1 이 Service1 객체를 생성하여 사용
public class Controller1 {
private final Service1 service1;
public Controller1() {
this.service1 = new Service1();
}
}
Service1 이 Repostiroy1 객체를 생성하여 사용
public class Service1 {
private final Repository1 repository1;
public Service1() {
this.repository1 = new Repository1();
}
}
Repostiroy1 객체 선언
public class Repository1 { ... }
그런데 만약,
Repository1 객체 생성 시 DB에 접속해서,
id와 pw 를 받아서 Repository1를 DB 접속 시에 사용하는 경우(생성자에 DB 접속 id, pw 를 추가)에는?
public class Repository1 {
public Repository1(String id, String pw) {
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:db", id, pw);
}
}
결론적으로,
Controller 5 개가 각각 Service1 을 생성하여 사용하게 되고
Repository1 생성자 변경이 발생한다면, 모든 Contoller와 모든 Service의 코드 변경이 필요하게 됨!
Repository1 클래스 선언 및 객체 생성 → repository1
public class Repository1 { ... }
// new 연산자를 사용해서, 객체 Repository1 생성
Repository1 repository1 = new Repository1();
Service1 클래스 선언 및 객체 생성 (repostiroy1 사용) → service1
Class Service1 {
private final Repository1 repitory1;
// repository1 객체 사용
//Repository1 repository1 이렇게 파라미터로 생성이 된 repository를 받아와서, 변수 repitory1에 넣어주기
public Service1(Repository1 repository) {
//this.repository1 = new Repository1(); --> X
this.repository1 = repository1;
}
}
// 객체 생성
Service1 service1 = new Service1(repository1);
Contoller1 선언 (service1 사용)
Class Controller1 {
private final Service1 service1;
// service1 객체 사용
//Service1 service1 이렇게 파라미터로 받아와서, 변수 service1에 넣어주는 방식으로
public Controller1(Service1 service1) {
//this.service1 = new Service1(); --> X
this.service1 = service1;
}
}
결론적으로,
이 방식으로 Repository1 객체 생성 시 DB 접속 id와 pw 를 받아서 DB 접속 시 사용할 때
repository1 객체를 생성해서, id와 pw를 넣어준다.
--> 생성된 repository1을 계속 재사용하면 된다.
public class Repository1 {
public Repository1(String id, String pw) {
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:db", id, pw);
}
}
// 객체 생성
String id = "sa";
String pw = "";
Repository1 repository1 = new Repository1(id, pw);
결론적으로, 느슨한 결합 이 되었다.
DI, IoC, Bean: https://velog.io/@baekgom/DI-IoC-Bean
@Component //클래스 선언 위에 설정
public class ProductService { ... }
// ProductService 객체 생성
//객체 productService(= 빈) 가 스프링 IoC 컨테이너에 저장된다
//빈 이름? 클래스의 앞글자만 소문자로 변경(public class ProductServcie → productServcie)
ProductService productService = new ProductService();
'빈' 아이콘(나뭇잎 모양. 스프링 IoC 에서 관리할 '빈' 클래스라는 표시)
@Configuration
@ComponentScan(basePackages = "com.sparta.myselectshop")
class BeanConfig { ... }
돋보기 달린 나뭇잎
BeanConfiguration
package com.sparta.myselectshop.config;
import com.sparta.myselectshop.repository.ProductRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //1. 클래스 선언 위에 @Configuration 설정
public class BeanConfiguration {
@Bean //2. 함수 위에 @Bean 설정 (함수를 Bean으로 동록하는 과정)
public ProductRepository productRepository() {
String dbUrl = "jdbc:h2:mem:db";
String username = "sa";
String password = "";
return new ProductRepository(dbUrl, username, password);
}
}
// 3. @Bean 설정된 함수 호출(productRepository())하면, 객체 productRepository(빈) 가 스프링 IoC 컨테이너에 저장됨
//(스프링 서버가 뜰 때 스프링 IoC 에 '빈' 저장)
//@Bean이 설정된 함수명: public ProductRepository productRepository() {..} → productRepository
ProductRepository productRepository = beanConfiguration.productRepository();
스프링 IoC 에 '빈' 에 등록될 것이라는 표시
방법 1
@Component
public class ProductService {
@Autowired //주입하려는 변수(productRepository) 위에 달면, 스프링이 생성된 빈 객체를 자동으로 넣어줌(DI된다)
private ProductRepository productRepository;
// ...
}
방법 2
@Component
public class ProductService {
//변수를 final로 만들고
private final ProductRepository productRepository;
@Autowired //생성자 위에 @Autowired 를 추가
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// ...
}
스프링 IoC 컨테이너에 의해 관리되는 클래스에서만 가능
생성자 선언이 1개 일 때
//오버로딩 되어 파라미터가 여러개가 된 생성자는 생략 불가
public class A {
@Autowired // 생략 불가
public A(B b) { ... }
@Autowired // 생략 불가
public A(B b, C c) { ... }
}
Lombok 의 @RequiredArgsConstructor 를 사용
@RestController
@RequiredArgsConstructor // @RequiredArgsConstructor 붙이고, final로 선언된 멤버 변수를 자동으로 생성합니다.
public class ProductController {
private final ProductService productService; //private final만 써도, @RequiredArgsConstructor가 private final를 찾아서 주입을 해준다(final로 선언해야함)
// @RequiredArgsConstructor를 사용한다면, 아랫 부분은 생략 가능
// @Autowired
// public ProductController(ProductService productService) {
// this.productService = productService;
// }
}
스프링 IoC 컨테이너에서 빈을 수동으로 가져오는 방법
@Component
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ApplicationContext context) {
// 1.'빈' 이름으로 가져오기
ProductRepository productRepository = (ProductRepository) context.getBean("productRepository");
// 2.'빈' 클래스 형식으로 가져오기
// ProductRepository productRepository = context.getBean(ProductRepository.class);
this.productRepository = productRepository;
}
// ...
}
@Component 대신 @Service를 입력해도, @Component가 추가로 자동 적용된다.
JpaRepository를 상속받아도, 자동으로 @Repository 가 추가된다.
JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface 로 선언.
매일 새벽 1시에 관심 상품 목록 제목으로 검색해서, 최저가 정보를 업데이트하는 스케줄러
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor //의존성 주입
public class ProductController {
//HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
private final ProductService productService;
// 관심 상품 등록하기
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto) {
// 응답 보내기
return productService.createProduct(requestDto);
}
// 관심 상품 조회하기
@GetMapping("/products")
public List<ProductResponseDto> getProducts() {
// 응답 보내기
return productService.getProducts();
}
// 관심 상품 최저가 등록하기
@PutMapping("/products/{id}")
public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) {
// 응답 보내기 (업데이트된 상품 id)
return productService.updateProduct(id, requestDto);
}
}
package com.sparta.myselectshop.service;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public ProductResponseDto createProduct(ProductRequestDto requestDto) {
// 요청받은 DTO 로 DB에 저장할 객체 만들기
Product product = productRepository.saveAndFlush(new Product(requestDto));
return new ProductResponseDto(product);
}
@Transactional(readOnly = true)
public List<ProductResponseDto> getProducts() {
List<ProductResponseDto> list = new ArrayList<>();
List<Product> productList = productRepository.findAll();
for (Product product : productList) {
list.add(new ProductResponseDto(product));
}
return list;
}
@Transactional
public Long updateProduct(Long id, ProductMypriceRequestDto requestDto) {
Product product = productRepository.findById(id).orElseThrow(
() -> new NullPointerException("해당 상품은 존재하지 않습니다.")
);
product.update(requestDto);
return product.getId();
}
@Transactional //설정해둔 myprice 값 보다 수정된 lprice 값이 작다면, '최저가' 표시가 뜨도록 js 에서 설정되어 있음
public void updateBySearch(Long id, ItemDto itemDto) {
//가지고 온 id 로 product 가 있는지 없는지 확인부터 한다
Product product = productRepository.findById(id).orElseThrow(
() -> new NullPointerException("해당 상품은 존재하지 않습니다.")
);
//그리고 itemDto 를 넣어서 update 를 실시
product.updateByItemDto(itemDto);
}
}
package com.sparta.myselectshop.entity;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.naver.dto.ItemDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 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; //itemDto 의 lprice 값을 가져온 것을 기존의 lprice 쪽으로 update
@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();
this.myprice = 0;
}
public void update(ProductMypriceRequestDto requestDto) {
this.myprice = requestDto.getMyprice();
}
public void updateByItemDto(ItemDto itemDto) {
//itemDto 의 lprice 값을 가져와서,
this.lprice = itemDto.getLprice();
}
}
package com.sparta.myselectshop.scheduler;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.naver.service.NaverApiService;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@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(); //productRepository 로 모든 product 를 가져온다
for (Product product : productList) { //for 문 돌면서,
// 1초에 한 상품 씩 조회합니다 (NAVER 제한)
TimeUnit.SECONDS.sleep(1);
//product 에서 title 을 가져온다
String title = product.getTitle();
//title 을 통해서, naverApiService 를 사용해서 itemDtoList 를 가져온다
List<ItemDto> itemDtoList = naverApiService.searchItems(title);
//가장 상단에 있는 item 을 가져온다
ItemDto itemDto = itemDtoList.get(0);
//i 번째 관심 상품 정보를 업데이트합니다.
//product 의 id 도 가져와서, 가장 상단에 있는 item 과 id 를 통해 update 를 실시
Long id = product.getId();
productService.updateBySearch(id, itemDto);
}
}
}