Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

경북대 BE_권다운_1주차 과제(1단계~3단계) #203

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
999696c
feat(Product): model 제작
momnpa333 Jun 25, 2024
bdf237c
feat(ProductController): controller 제작
momnpa333 Jun 25, 2024
bd50b26
feat(ProductDao): Dao 제작
momnpa333 Jun 25, 2024
352aecc
feat(ProductRequest): Request 제작
momnpa333 Jun 25, 2024
f9282b6
test(ProductControllerTest): test 제작
momnpa333 Jun 26, 2024
80e6352
chore(.gitkeep): keeper 삭제
momnpa333 Jun 27, 2024
d06d515
fix(ProductController.java): @RestController -> @Controller로 변경
momnpa333 Jun 27, 2024
ff0e728
refact(Product.java): Product 생성자 제거
momnpa333 Jun 27, 2024
04a1fcd
refact(ProductDao.java): id 부여 로직 변경
momnpa333 Jun 27, 2024
ed6b29e
feat(adminPage.html): admin페이지 제작
momnpa333 Jun 27, 2024
78bee3b
feat(product.js): ajxa 관련 js 제작
momnpa333 Jun 27, 2024
d1dd665
chore(product.js): 로그 제거
momnpa333 Jun 27, 2024
1aa5601
refact(ProductRequest.java): int->Interger로 변경
momnpa333 Jun 27, 2024
9aaa919
chore(adminPage.html): 태그 정리
momnpa333 Jun 27, 2024
5610a97
refact(product.js): async await 으로 코드 리팩토링
momnpa333 Jun 27, 2024
62517f9
refact(productControllerTest.java): 생성자 변경으로 인한 변경
momnpa333 Jun 27, 2024
f9d49c3
chore(.gitkeep): gitkeep 제거
momnpa333 Jun 27, 2024
a82f2e2
feat(Application.java): jdbcTemplate 의존성 주입 및 테이블 생성
momnpa333 Jun 27, 2024
dbb9494
feat(application.properties): properties 설정
momnpa333 Jun 27, 2024
5264f0e
fix(Product.java): 생성자 접근지정자 변경
momnpa333 Jun 27, 2024
beb1782
refact(ProductController.java): save 인자 변경에 따른 코드 refact
momnpa333 Jun 27, 2024
677c141
refact(ProductControllerTest.java): save 인자 변경에 따른 코드 refact
momnpa333 Jun 27, 2024
d540c4b
feat(ProductDao.java): dao 인터페이스 제작
momnpa333 Jun 27, 2024
5e5d08a
feat(ProductDaoInMemoryImpl.java): daoImpl 제작
momnpa333 Jun 27, 2024
03341ba
feat(ProductDaoMapImpl.java): 이름 변경
momnpa333 Jun 27, 2024
2561f41
feat(ProductRequest.java): toEntity 메서드 제작
momnpa333 Jun 27, 2024
b40b8d2
feat(ProductController.java): api 스펙 변경
momnpa333 Jun 27, 2024
355930f
feat(ProductDao.java): save, update 반환값 변경
momnpa333 Jun 27, 2024
3abb10d
feat(ProductDao.java): save, update 반환값 변경
momnpa333 Jun 27, 2024
3cabdd5
feat(ProductController.java): save, update 반환값 변경
momnpa333 Jun 27, 2024
d516dac
delete(ProductControllerTest.java): save, update test 제거
momnpa333 Jun 27, 2024
92967b0
feat(ProductRowMapper.java): RowMapper 제작
momnpa333 Jun 27, 2024
234fbfb
refeat(ProductDaoInMemoryImpl.java): RowMapper 반영 refact
momnpa333 Jun 27, 2024
c450ce5
delete(ProductRowMapper.java): RowMapper 제거
momnpa333 Jun 27, 2024
fe2d31c
chore(Product.java): import 문 삭제
momnpa333 Jun 28, 2024
7f1e12d
chore(ProductResponse.java): ResponseDto 제작
momnpa333 Jun 28, 2024
a910172
feat(ProductController.java): ProductResponse 반영 및 예외처리
momnpa333 Jun 28, 2024
a6e717f
feat(ProductJdbcTemplateDao.java): 네이밍 변경
momnpa333 Jun 28, 2024
d6c3e48
feat(ProductDao.java): update 메서드 인자 변경
momnpa333 Jun 28, 2024
86e28f9
feat(ProductMapDao.java): 네이밍 수정
momnpa333 Jun 28, 2024
21f0a4e
feat(ProductRowMapper.java): rowMapper 제작
momnpa333 Jun 28, 2024
611b87e
feat(adminPage.html): 페이징 기능 추가
momnpa333 Jun 28, 2024
bd260f1
refact(Application.java): jdbcTemplate 의존성 위치 변경
momnpa333 Jun 28, 2024
fd1a317
refact(Application.java): jdbcTemplate 의존성 위치 변경
momnpa333 Jun 28, 2024
5d2b030
feat(data.sql): 더미데이터 제작
momnpa333 Jun 28, 2024
cb07c8f
feat(schema.sql): 스키마 제작
momnpa333 Jun 28, 2024
16cf41a
fix(ProductRowMapper.java): column 네이밍 수정
momnpa333 Jun 28, 2024
16fc2f4
feat(ProductJdbcTemplateDao.java): Paging 기능 추가
momnpa333 Jun 28, 2024
07c17a8
feat(ProductJdbcTemplateDao.java): Paging 기능 추가
momnpa333 Jun 28, 2024
ed99804
feat(ProductJdbcTemplateDao.java): Paging 기능 추가
momnpa333 Jun 28, 2024
2ed0c50
feat(ProductJdbcTemplateDao.java): Paging 기능 추가
momnpa333 Jun 28, 2024
92b470b
fix(data.sql): id 삭제
momnpa333 Jun 28, 2024
c1a67f4
refact(): 피드백 반영
momnpa333 Jul 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/gift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
87 changes: 87 additions & 0 deletions src/main/java/gift/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package gift.controller;

import gift.model.Product;
import gift.model.ProductDao;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class ProductController {

private final ProductDao productDao;

public ProductController(ProductDao productDao) {
this.productDao = productDao;
}

@GetMapping("/admin")
public String admin() {
return "adminPage";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드처럼 뷰를 처리하기 위한 API와 아래처럼 데이터를 처리하는 역할의 API가 하나의 클래스로 응집되어 있는 것 같네요! 이 둘을 분리하면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뷰를 처리하기 위한 api가 하나라서 같이 묶어놨었는데, 분리해서 추후 확장성을 고려하도록 하겠습니다!

}

@GetMapping("/products")
public ResponseEntity<List<ProductResponse>> getProducts() {
var products = productDao.findAll();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 부분은 inline 처리해도 좋을 것 같아요ㅎㅎ

Suggested change
var products = productDao.findAll();
var response = productDao.findAll()
.stream()
.map(ProductResponse::from)
.toList();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다!

var response = products.stream()
.map(ProductResponse::from)
.toList();
return ResponseEntity.ok(response);
}

@GetMapping("/products/{id}")
public ResponseEntity<ProductResponse> getProduct(@PathVariable("id") Long id) {
var product = productDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다."));
var response = ProductResponse.from(product);
return ResponseEntity.ok(response);
}

@PostMapping("/products")
public ResponseEntity<String> createProduct(@RequestBody ProductRequest request) {
productDao.save(request.toEntity());
return ResponseEntity.ok().body("Product created successfully.");
}


@PutMapping("/products/{id}")
public ResponseEntity<String> updateProduct(@PathVariable("id") Long id,
@RequestBody ProductRequest request) {
productDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다."));
productDao.update(request.toEntity(id));
return ResponseEntity.ok().body("Product updated successfully.");
}

@DeleteMapping("/products/{id}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적절한 HTTP Method와 Status를 사용해주셨네요! 👍

public ResponseEntity<Void> deleteProduct(@PathVariable("id") Long id) {
productDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다."));
productDao.deleteById(id);
return ResponseEntity.noContent().build();
}

@GetMapping("/products/paging")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이징 처리 관련하여 작업해주신 부분을 보고 몇 가지 코멘트를 통합하여 남겨드릴께요~

  1. @GetMapping("/products")와 @GetMapping("/products/paging")는 동일한 목록 조회 요청이므로 분리될 필요가 없음
  2. 목록 조회는 @GetMapping("/products")로 일원화하고, size와 page를 요청받으면 됨
  3. @GetMapping("/products")에 size와 page를 요청 받으면, 이때 응답 값으로 응답 목록과 count 정보를 함께 보내주면 됨

따라서 상품 목록 조회의 응답 클래스용 DTO가 다음과 같은 정보들을 담고 있으면 됩니다ㅎㅎ

  • 전체 개수
  • 상품 목록
  • 다음에 요청할 page 정보
  • 기타 등등

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 response에 count를 넣을까 고민했었는데, 전체 데이터의 개수를 받아오는 api, 페이징 조회 api 두 api의 용도가 다르다고 생각해서 나눴었는데, 정보를 합쳐서 보내는 군요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한점이 있습니다. count 쿼리를 날릴때 full scan을 한다고 알고 있는데 페이지 하나를 요청할 때 마다 count 를 받으면 리소스 낭비가 너무 심하지 않나요? 감사합니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MySQL을 기준으로 설명드리면, PK가 하나의 프라이머리 인덱스처럼 동작하게 되는데요ㅎㅎ
개수를 세기 위해서 테이블 풀스캔이 아닌 인덱스 풀스캔이 일어나다 보니, 풀테이블 스캔 만틈의 비효율이 발생하지는 않습니다!
추가로 이러한 부분 역시 부담이 된다면, 전체 row 개수를 캐싱하는 전략을 활용합니다!

public ResponseEntity<List<ProductResponse>> getProductsPaging(@RequestParam("page") int page,
@RequestParam("size") int size) {
var products = productDao.findPaging(page, size);
var response = products.stream()
.map(ProductResponse::from)
.toList();
return ResponseEntity.ok(response);
}

@GetMapping("/products/count")
public ResponseEntity<Long> getProductsCount() {
return ResponseEntity.ok(productDao.count());
}
}
19 changes: 19 additions & 0 deletions src/main/java/gift/controller/ProductRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gift.controller;


import gift.model.Product;

public record ProductRequest(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto라는 네이밍이 아닌 Request 라는 네이밍을 사용한 부분과 record 를 사용한 부분 좋네요! 👍

String name,
Integer price,
String imageUrl
) {

public Product toEntity() {
return Product.create(null, name(), price(), imageUrl());
}

public Product toEntity(Long id) {
return Product.create(id, name(), price(), imageUrl());
}
}
16 changes: 16 additions & 0 deletions src/main/java/gift/controller/ProductResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gift.controller;

import gift.model.Product;

public record ProductResponse(
Long id,
String name,
Integer price,
String imageUrl
) {

public static ProductResponse from(Product product) {
return new ProductResponse(product.getId(), product.getName(), product.getPrice(),
product.getImageUrl());
}
}
38 changes: 38 additions & 0 deletions src/main/java/gift/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gift.model;


public class Product {

private Long id;
private String name;
private Integer price;
private String imageUrl;

public Product(Long id, String name, Integer price, String imageUrl) {
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public Integer getPrice() {
return price;
}

public String getImageUrl() {
return imageUrl;
}

public static Product create(Long id, String name, Integer price, String imageUrl) {
return new Product(id, name, price, imageUrl);
}

}
23 changes: 23 additions & 0 deletions src/main/java/gift/model/ProductDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package gift.model;

import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Component;

@Component
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터페이스는 해당 애노테이션을 등록해줄 필요가 없습니다ㅎㅎ
왜냐하면 해당 인터페이스의 실제 구현은 ProductJdbcTemplateDao 또는 ProductMapDao로 존재하기 때문입니다

public interface ProductDao {

void save(Product product);

Optional<Product> findById(Long id);

List<Product> findAll();

void deleteById(Long id);

void update(Product product);

List<Product> findPaging(int page, int size);

Long count();
}
75 changes: 75 additions & 0 deletions src/main/java/gift/model/ProductJdbcTemplateDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package gift.model;

import java.util.List;
import java.util.Optional;
import javax.sql.DataSource;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

@Component
@Primary
public class ProductJdbcTemplateDao implements ProductDao {

private static final String SQL_INSERT = "INSERT INTO products(name, price, image_url) VALUES (?, ?, ?)";
private static final String SQL_SELECT_BY_ID = "SELECT id, name, price, image_url FROM products WHERE id = ?";
private static final String SQL_SELECT_ALL = "SELECT id, name, price, image_url FROM products";
private static final String SQL_DELETE_BY_ID = "DELETE FROM products WHERE id = ?";
private static final String SQL_UPDATE = "UPDATE products SET name = ?, price = ?, image_url = ? WHERE id = ?";
private static final String SQL_SELECT_PAGING = "SELECT id, name, price, image_url FROM products LIMIT ? OFFSET ?";
private static final String SQL_COUNT = "SELECT COUNT(*) FROM products";


private final JdbcTemplate jdbcTemplate;
private final RowMapper<Product> productRowMapper = new ProductRowMapper();

public ProductJdbcTemplateDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

@Override
public void save(Product product) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save라는 저장한다는 표현은 크게 2가지 의미로 해석될 수 있어서요ㅎㅎ

  1. 존재하지 않는 데이터를 추가함
  2. 존재하는 데이터를 변경함

현재 상황에서는 insert를 사용하는 것이 더욱 명시적일 것 같아요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또는 다음과 같이 내부 구현을 변경하여 save라는 네이밍을 유지해도 괜찮을 것 같습니다ㅎㅎ

  1. id가 있으면 insert함
  2. id가 없으면 update함

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍에 대한 지적 감사합니다!

jdbcTemplate.update(SQL_INSERT, product.getName(), product.getPrice(),
product.getImageUrl());
}

@Override
public Optional<Product> findById(Long id) {
try {
Product product = jdbcTemplate.queryForObject(SQL_SELECT_BY_ID,
productRowMapper, id);
return Optional.of(product);
} catch (Exception e) {
return Optional.empty();
}
}

@Override
public List<Product> findAll() {
return jdbcTemplate.query(SQL_SELECT_ALL,
productRowMapper);
}

@Override
public void deleteById(Long id) {
jdbcTemplate.update(SQL_DELETE_BY_ID, id);
}

@Override
public void update(Product product) {
jdbcTemplate.update(SQL_UPDATE, product.getName(), product.getPrice(),
product.getImageUrl(), product.getId());
}

@Override
public List<Product> findPaging(int page, int size) {
int offset = (page) * size;
return jdbcTemplate.query(SQL_SELECT_PAGING, productRowMapper, size, offset);
}

@Override
public Long count() {
return jdbcTemplate.queryForObject(SQL_COUNT, Long.class);
}
}
50 changes: 50 additions & 0 deletions src/main/java/gift/model/ProductMapDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package gift.model;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;

@Component
public class ProductMapDao implements ProductDao {

private final Map<Long, Product> database = new ConcurrentHashMap<>();

@Override
public void save(Product product) {
Product newProduct = Product.create(Long.valueOf(database.size() + 1), product.getName(),
product.getPrice(), product.getImageUrl());
database.put(newProduct.getId(), newProduct);
}

@Override
public Optional<Product> findById(Long id) {
return Optional.ofNullable(database.get(id));
}

@Override
public List<Product> findAll() {
return List.copyOf(database.values());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터를 복사하여 반환하는 부분 좋네요! 👍

}

@Override
public void deleteById(Long id) {
database.remove(id);
}

@Override
public void update(Product product) {
database.replace(product.getId(), product);
}

@Override
public List<Product> findPaging(int page, int size) {
return null;
}

@Override
public Long count() {
return Long.valueOf(database.size());
}
}
18 changes: 18 additions & 0 deletions src/main/java/gift/model/ProductRowMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gift.model;

import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ProductRowMapper implements RowMapper<Product> {

@Override
public Product mapRow(ResultSet resultSet, int rowNum) throws SQLException {
return new Product(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getInt("price"),
resultSet.getString("image_url")
);
}
}
7 changes: 7 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
spring.application.name=spring-gift
# h2-console ??? ??
spring.h2.console.enabled=true
# db url
spring.datasource.url=jdbc:h2:mem:test
spring.sql.init.schema-locations=classpath:/static/sql/schema.sql
spring.sql.init.data-locations=classpath:/static/sql/data.sql
spring.sql.init.mode=always
Empty file removed src/main/resources/static/.gitkeep
Empty file.
Loading