728x90
1. ERD 작성
나름대로 스스로 ERD를 설계했습니다.
- 타임딜 이라는 것은 상품에서 확장된 느낌이라 생각했습니다.
- 재설정된 세일가격, 제한물량, 시작시간을 추가했습니다.
- 타임딜 상품은 일반상품과 동일하게 상품명, 상품설명 등을 가질 수 있다고 생각했습니다.
- 한 상품으로 여러 타임테이블을 생성할 수 있다고 생각하여 상품과 타임딜 테이블을 1:N으로 설정했습니다.
- 유저와 상품의 관계테이블로 구매테이블이 생성됩니다.
2. API목록
각각의 도메인 별로 API 정리
3. 와이어프레임
4. 아키텍처
- 젠킨스 서버와 배포 서버를 운영하였습니다.
- 젠킨스 서버에서 GitHub 코드를 pull 하여 코드를 가져오고 빌드시켜 DockerHub에 Push 합니다.
- 배포 서버에서 젠킨스를 통해 원격으로 DockerHub의 jar 를 pull & run 하여 도커 컨테이너로 Spring Boot 를 실행합니다.
CI/CD 트러블 슈팅은 8번째 글 참고
5. DB 연동
Naver Cloud 의 "Cloud DB for MySQL 을 사용
MySQL Workbench에 연결
6. USER 기능 페이지네이션
JPA를 통해 비교적 간단하게 페이지네이션을 구현할 수가 있었기에 기록해보려고 합니다.
6.1.
UserController
@GetMapping("/v1/users")
public ResponseEntity<Page<UserEntity>> userPagination(@PageableDefault(size = 50)Pageable pageable){
return new ResponseEntity<>(userService.userPagination(pageable), HttpStatus.OK );
}
localhost:8011/v1/users?page=0
주목해야될 부분은 page 부분인데 파라미터는 (@PageableDefault(size = 50)Pageable pageable) 이렇게 작성돼있는데
요청을 할 때 다음과 같이 요청가능 size 생략 가능
/page=0&size=0
6.2.
UserService
public Page<UserEntity> userPagination(Pageable pageable) {
PageRequest pr = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
return userRepository.findAll(pr);
}
PageRequest 클래스를 생성하여 findAll의 파라미터로 담는다.
Sort 를 생성해서 한 필드를 지정하여 정렬 가능
{
"content": [
{
"userId": "admin1371",
"password": "pass",
"name": "admin",
"nickname": "admin",
"phone": "010-1234-1234",
"email": "admin@naver.com",
"profile": null,
"emailCheck": false,
"grade": null,
"role": "ROLE_USER",
"createdDate": "2023-03-13T05:44:44.02354"
},
{
"userId": "client2996",
"password": "pass",
"name": "client",
"nickname": "client",
"phone": "010-1234-1234",
"email": "client@naver.com",
"profile": null,
"emailCheck": false,
"grade": null,
"role": "ROLE_USER",
"createdDate": "2023-03-13T05:44:44.143916"
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"pageNumber": 0,
"pageSize": 50,
"offset": 0,
"paged": true,
"unpaged": false
},
"last": true,
"totalPages": 1,
"totalElements": 2,
"first": true,
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 2,
"size": 50,
"empty": false
}
7. 테스트 코드 작성
JUnit 5
- 회원가입
- 카테고리 등록
- 상품 등록
- 타임딜 등록
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class TimedealApplicationTests {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@Autowired
ProductService productService;
@Autowired
CategoryService categoryService;
@Autowired
TimedealService timedealService;
@Autowired
PurchaseService purchaseService;
@Test
@Order(1)
@DisplayName("회원가입")
void signup() {
APIMessage<Boolean> apiMessage = userService.signup(ReqSignup.builder()
.nickname("admin")
.name("admin")
.email("admin@naver.com")
.phone("010-1234-1234")
.password("pass")
.build());
userService.signup(ReqSignup.builder()
.nickname("client")
.name("client")
.email("client@naver.com")
.phone("010-1234-1234")
.password("pass")
.build());
// then
assertTrue(apiMessage.getData());
}
@Test
@Order(2)
@DisplayName("카테고리 등록")
void saveCategory(){
// when
String categoryCode = "pants-1";
String categoryName = "바지";
// given
CategoryDto categoryDto = categoryService.categoryCreation(new CategoryDto(categoryCode, categoryName));
//then
assertEquals(categoryCode,categoryDto.getCategoryCode());
assertEquals(categoryName,categoryDto.getCategoryName());
}
@Test
@Order(3)
@DisplayName("상품 등록")
void registerProduct(){
// given
String nickname="admin";
ReqProduct reqProduct = new ReqProduct();
String userid= userService.findUserIdByNickname(nickname);
log.info("userid: {}", userid);
reqProduct.setUser_id(userid);
reqProduct.setCategory_code("pants-1");
reqProduct.setProduct_name("트레이닝 팬츠");
reqProduct.setProduct_desc("남녀노소 누구나 입을 수 있는 바지 입니다.");
reqProduct.setProduct_price(30000);
reqProduct.setSale_price(25000);
reqProduct.setProduct_amount(10000);
// when
RespProduct respProduct = productService.productCreation(reqProduct);
// then
assertNotNull(respProduct);
}
RespTimedeal respTimedeal = null;
@Test
@Order(4)
@DisplayName("타임딜 등록")
void registerTimedeal(){
// given
List<RespProduct> productList = productService.findProductList();
if (productList.size() >= 1){
Long productid = productList.get(0).getProductId();
ReqTimedeal reqTimedeal = new ReqTimedeal();
reqTimedeal.setProduct_id(productid);
reqTimedeal.setLimited_amount(100);
reqTimedeal.setSale_price(20000);
reqTimedeal.setStart_datetime(LocalDateTime.of(2023,03,8,00,00));
respTimedeal = timedealService.createTimedeal(reqTimedeal);
}
assertNotNull(respTimedeal);
}
}
구매기능에 대한 테스트
@SpringBootTest
public class PurchaseTests {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@Autowired
ProductService productService;
@Autowired
CategoryService categoryService;
@Autowired
TimedealService timedealService;
@Autowired
PurchaseService purchaseService;
@Test
@DisplayName("여러 유저가 구매")
void manyPplPurchase() throws InterruptedException{
/**
* 여러 유저 생성
* 쓰레드 생성해서
* 구매 러시
* */
// given
int threadCount = 50;
String userId = userService.findUserIdByNickname("client");
ReqPurchase reqPurchase = new ReqPurchase();
reqPurchase.setUser_id(userId);
reqPurchase.setTimedeal_id(1L);
reqPurchase.setCount(2);
// 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
ExecutorService executorService = Executors.newFixedThreadPool(32);
// 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for(int i = 0; i< threadCount; i++){
executorService.submit(()->{
try{
// 로직
purchaseService.purchaseTimedeal(reqPurchase);
}finally {
latch.countDown();
}
});
}
latch.await();
// then
Timedeal timedeal = timedealService.findById(1L);
assertEquals(0,timedeal.getLimitedAmount());
}
@Test
@DisplayName("비관적 락으로 구매")
void manyPplPurchaseWithPessimisticLock() throws InterruptedException{
/**
* 여러 유저 생성
* 쓰레드 생성해서
* 구매 러시
* */
// given
int threadCount = 60;
String userId = userService.findUserIdByNickname("client");
ReqPurchase reqPurchase = new ReqPurchase();
reqPurchase.setUser_id(userId);
reqPurchase.setTimedeal_id(1L);
reqPurchase.setCount(2);
// 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
ExecutorService executorService = Executors.newFixedThreadPool(32);
// 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for(int i = 0; i< threadCount; i++){
executorService.submit(()->{
try{
// 로직
purchaseService.purchaseTimedealWithPessimisticLock(reqPurchase);
}finally {
latch.countDown();
}
});
}
latch.await();
// then
Timedeal timedeal = timedealService.findById(1L);
assertEquals(0,timedeal.getLimitedAmount());
}
@Test
@DisplayName("낙관적 락으로 구매")
void manyPplPurchaseWithOptimisticLock() throws InterruptedException{
/**
* 여러 유저 생성
* 쓰레드 생성해서
* 구매 러시
* */
// given
int threadCount = 500;
String userId = userService.findUserIdByNickname("client");
ReqPurchase reqPurchase = new ReqPurchase();
reqPurchase.setUser_id(userId);
reqPurchase.setTimedeal_id(1L);
reqPurchase.setCount(2);
// 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
ExecutorService executorService = Executors.newFixedThreadPool(32);
// 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for(int i = 0; i< threadCount; i++){
executorService.submit(()->{
try{
// 로직
purchaseService.purchaseTimedealWithOptimisticLock(reqPurchase);
}finally {
latch.countDown();
}
});
}
latch.await();
// then
Timedeal timedeal = timedealService.findById(1L);
assertEquals(0,timedeal.getLimitedAmount());
}
}
'Dev > [넘블] 2023 백엔드 챌린지' 카테고리의 다른 글
[Numble] 연계_내가 만드는 나만의 유형테스트 회고 (0) | 2023.07.17 |
---|---|
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 4 (핀포인트, nGrinder) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 3 (동시성 처리) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 2 (CI/CD) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 클린코드를 포함한 리팩토링 (TOP3 선정!) (0) | 2023.04.09 |
댓글