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

[#4] 상품 등록, 조회, 수정, 삭제 #9

Merged
merged 9 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,25 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'org.apache.commons:commons-lang3:3.12.0'

// lombok
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
compileOnly 'org.projectlombok:lombok'

// Mapstruct
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'

annotationProcessor(
'org.projectlombok:lombok',
'org.projectlombok:lombok-mapstruct-binding:0.2.0'
)

runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
35 changes: 35 additions & 0 deletions http-test/item-api.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### 아이템 등록
POST http://localhost:8080/api/v1/items
Content-Type: application/json

{
"itemName": "한옥스테이조아당",
"itemPrice": 35000,
"itemStock": 10
}

### 아이템 전체 조회
GET http://localhost:8080/api/v1/items
Content-Type: application/json

### 아이템 상세 조회
GET http://localhost:8080/api/v1/items/itm_m01CJuHJ6ogMx7f0
Content-Type: application/json

### 아이템 수정
PUT http://localhost:8080/api/v1/items/itm_m01CJuHJ6ogMx7f0
Content-Type: application/json

{
"itemName": "한옥스테이조아당 업데이트",
"itemPrice": 35000,
"itemStock": 10
}

### 아이템 비활성화
POST http://localhost:8080/api/v1/items/change-end-of-sales
Content-Type: application/json

{
"itemToken": "itm_m01CJuHJ6ogMx7f0"
}
42 changes: 42 additions & 0 deletions src/main/java/com/flab/goodchoice/item/application/ItemFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.flab.goodchoice.item.application;


import com.flab.goodchoice.item.domain.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ItemFacade {

private final ItemEntryService itemEntryService;

private final ItemModifyService itemModifyService;

private final ItemOneService itemOneService;

private final ItemListService itemListService;

public String registerItem (ItemCommand.RegisterItemRequest request) {
var itemToken = itemEntryService.registerItem(request);
return itemToken;
}

public ItemInfo.Main retrieveItemInfo(String itemToken) {
return itemOneService.retrieveItemInfo(itemToken);
}

public void changeEndOfSaleItem(String itemToken) {
itemModifyService.changeEndOfSale(itemToken);
}

public void updateItem(String itemToken, ItemCommand.UpdateItemRequest request) {
itemModifyService.updateItem(itemToken, request);
}

public List<Item> retrieveAllItemInfo() {
return itemListService.retrieveAllItemInfo();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.flab.goodchoice.item.common.util;

import org.apache.commons.lang3.RandomStringUtils;

public class TokenGenerator {
private static final int TOKEN_LENGTH = 20;

public static String randomCharacter(int length) {
return RandomStringUtils.randomAlphanumeric(length);
}

public static String randomCharacterWithPrefix(String prefix) {
return prefix + randomCharacter(TOKEN_LENGTH - prefix.length());
}
}
82 changes: 82 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/Item.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.flab.goodchoice.item.domain;

import com.flab.goodchoice.item.common.util.TokenGenerator;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

import org.apache.commons.lang3.StringUtils;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.security.InvalidParameterException;
import java.time.ZonedDateTime;

@Getter
@Entity
@NoArgsConstructor
@Table(name = "items")
public class Item {

private static final String ITEM_PREFIX = "itm_";

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String itemToken;

private String itemName;

private Long itemPrice;

private Long itemStock;

@Enumerated(EnumType.STRING)
private Status status;

@CreationTimestamp
private ZonedDateTime createdAt;

@UpdateTimestamp
private ZonedDateTime updatedAt;

@UpdateTimestamp
private ZonedDateTime deletedAt;

@Getter
@RequiredArgsConstructor
public enum Status {
PREPARE("판매준비중"),
ON_SALE("판매중"),
END_OF_SALE("판매종료");

private final String description;
}

@Builder
public Item(String itemName, Long itemPrice, Long itemStock) {
if (StringUtils.isBlank(itemName)) throw new InvalidParameterException("Item.itemName");
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

if (itemPrice == null) throw new InvalidParameterException("Item.itemPrice");
if (itemStock == null) throw new InvalidParameterException("Item.itemStock");

this.itemToken = TokenGenerator.randomCharacterWithPrefix(ITEM_PREFIX);
this.itemName = itemName;
this.itemPrice = itemPrice;
this.itemStock = itemStock;
this.status = Status.PREPARE;
}

public void update(String itemName, Long itemPrice, Long itemStock) {
this.itemName = itemName;
this.itemPrice = itemPrice;
this.itemStock = itemStock;
}

public void changeEndOfSale() {
this.status = Status.END_OF_SALE;
}

}
46 changes: 46 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.flab.goodchoice.item.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

public class ItemCommand {

@Getter
@Builder
@ToString
@AllArgsConstructor
public static class RegisterItemRequest {
private final String itemName;
private final Long itemPrice;
private final Long itemStock;

public Item toEntity() {
return Item.builder()
.itemName(itemName)
.itemPrice(itemPrice)
.itemStock(itemStock)
.build();
}

}

@Getter
@Builder
@ToString
@AllArgsConstructor
public static class UpdateItemRequest {
private final String itemName;
private final Long itemPrice;
private final Long itemStock;

public Item toEntity() {
return Item.builder()
.itemName(itemName)
.itemPrice(itemPrice)
.itemStock(itemStock)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.flab.goodchoice.item.domain;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemEntryService {

private final ItemStore itemStore;

@Transactional
public String registerItem(ItemCommand.RegisterItemRequest command) {
var initItem = command.toEntity();
var item = itemStore.store(initItem);
return item.getItemToken();
}

}
26 changes: 26 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.flab.goodchoice.item.domain;

import lombok.Getter;
import lombok.ToString;

public class ItemInfo {

@Getter
@ToString
public static class Main {
private final String itemToken;
private final String itemName;
private final Long itemPrice;
private final Long itemStock;
private final Item.Status status;

public Main(Item item) {
this.itemToken = item.getItemToken();
this.itemName = item.getItemName();
this.itemPrice = item.getItemPrice();
this.itemStock = item.getItemStock();
this.status = item.getStatus();
}
}

}
19 changes: 19 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemListService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.flab.goodchoice.item.domain;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemListService {

private final ItemReader itemReader;

public List<Item> retrieveAllItemInfo() {
return itemReader.getItems();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.flab.goodchoice.item.domain;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemModifyService {

private final ItemReader itemReader;

@Transactional
public void changeEndOfSale(String itemToken) {
var item = itemReader.getItemBy(itemToken);
item.changeEndOfSale();
}

@Transactional
public ItemInfo.Main updateItem(String itemToken, ItemCommand.UpdateItemRequest request) {
var item = itemReader.getItemBy(itemToken);
item.update(request.getItemName(), request.getItemPrice(), request.getItemStock());
return new ItemInfo.Main(item);
}

}
21 changes: 21 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemOneService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.flab.goodchoice.item.domain;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemOneService {

private final ItemReader itemReader;

@Transactional(readOnly = true)
public ItemInfo.Main retrieveItemInfo(String itemToken) {
var item = itemReader.getItemBy(itemToken);
return new ItemInfo.Main(item);
}

}
9 changes: 9 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flab.goodchoice.item.domain;

import java.util.List;

public interface ItemReader {
Item getItemBy(String itemToken);

List<Item> getItems();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Item 엔티티는 JPA 스펙에 종속된 클래스인데,
해당 인터페이스에 두는건 안 맞지 않을까요?

Copy link
Collaborator Author

@KATEKEITH KATEKEITH May 16, 2023

Choose a reason for hiding this comment

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

@f-lab-thor JPA 스펙에 종속된 엔티티 클래스의 사용범위는 어디까지 일까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 이해한 의미를 조심스럽게 말씀 드리면,
ItemReader 인터페이스가 Item entity 클래스를 알아야 하나? 이런 말씀인거 같습니다.
인터페이스를 사용하는 클라이언트에게 entity 클래스 보다는 vo 객체를 만들어 전달하는게 좋지 않을까?
라는 의미이지 않을까 생각됩니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@zzangoobrother ItemReader의 구현체에서 itemRepository에 접근해서 item엔티티를 리턴 받도록 구현되어 있는 상태입니다. vo 객체로 어떻게 변환이 가능할까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

itemVO 라고 클래스 만드셔서 dto로 변환 하듯이 하셔도 될 겁니다.
제가 이해한거는 jpa에서 한번 꺼내고 리턴하는데 entity를 리턴하느거 보다는
중간 변환 객체를 사용해서 리턴하는데 entity를 보호하는게 좋다라고 이해했습니다.

Copy link
Collaborator

@f-lab-thor f-lab-thor May 18, 2023

Choose a reason for hiding this comment

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

우선 이미 은지님이 그 답을 여기에 말씀해 주셨네요

해당 인터페이스가 존재하는 이유는 ItemReader의 구체적인 방식을 의존하지 않고
추상화된 또는 고수준의 역활만 바라보게 하여
변경이 있어도 그것을 사용하는 쪽에 영향이 없게 하기 위함입니다.

내가 jpa를 더이상 안쓰고 다른 구현체를 사용한다고 했을때, 도메인 영역까지 영향이 침범 되지 않을까요?
entity 패키지 명만 보시면 아실거에요

인터페이스가 도메인 영역에 있고, 인터페이스 구현체가 infrastructure에 있다는 것은 DIP 원칙이 훤히~~ 보이는 구조로 말씀드렸습니다.

}
5 changes: 5 additions & 0 deletions src/main/java/com/flab/goodchoice/item/domain/ItemStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.flab.goodchoice.item.domain;

public interface ItemStore {
Item store(Item initItem);
}
Loading