이번 백오피스 과제에서 상품을 맡게 되었습니다.
제가 맡은 API endpoint들은
| POST | /api/admin/items | (상품 등록) |
|---|---|---|
| GET | /api/admin/items | (상품 전체 조회) |
| GET | /api/admin/items/{itemId} | (상품 상세 조회) |
| - | - | - |
| PATCH | /api/admin/items/{itemId}/info | (상품 정보 수정) |
| PATCH | /api/admin/items/{itemId}/stock | (상품 재고 수정) |
| PATCH | /api/admin/items/{itemId}/status | (상품 상태 수정) |
| - | - | - |
| DELETE | /api/admin/items/{itemId} | (상품 삭제) |
입니다.
상품 entity의 구조는 이렇습니다.

간단한 CRUD를 맡았지만 그래도 어려웠던 부분은 상품 재고 변경 로직에 있었습니다.
요구사항에 의하면 상품 상태는 상품 재고가 변경됨에 따라 자동으로 변경되야 합니다.
상품의 상태가 상품의 재고에 전적으로 의존한다면 문제가 없겠지만 튜터님의 예시에서는 상품 재고와 상관 없이 상품 상태를 변경 할 수 있었습니다.
저는 그게 맞다고 생각했습니다. 어떤 이유에서든지 재고와 상관없이 상품을 품절 상태로 만들고 싶은 경우가 있을테니까요.
하지만 이런 이상한 상황이 발생 할 수도 있습니다.
이를 예방하기 위해 상품 재고가 의미있게 변경 되었을 경우에만 수정되도록 하였습니다.
public void updateStock(Long stock) {
Long previousStock = this.stock;
this.stock = stock;
// item상태가 단종이 아닐 경우 자동으로 업데이트 합니다.
if (this.status != ItemStatus.DISCONTINUED) {
// 실제로 재고가 의미있게 변경 되었는지 확인
if (
(previousStock <= 0 && this.stock > 0) ||
(previousStock > 0 && this.stock <= 0)
) {
if (this.stock <= 0) {
this.status = ItemStatus.SOLD_OUT;
}else {
this.status = ItemStatus.ON_SALE;
}
}
}
}
저는 처음에 상품을 등록한 관리자가 삭제가 되었을경우 상품도 같이 삭제할려고 생각했습니다. 하지만 팀원중 한분이 관리자가 삭제 될 때 상품이 삭제되면 사업에 중요한 정보가 사라지는 거 아닌가 라고 문제를 제기하셨습니다.
저는 그말이 맞다고 생각하여 상품의 관리자가 삭제 되더라도 상품관련 서비스에 문제가 없도록 수정하였습니다.
DB colum을 nullable로 수정하였고
// 상품을 등록한 관리가 어떤 이유에서이든지
// 삭제 될 수 있기 때문에 nullable 입니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = true, name = "admin_id")
private Admin admin;
ResponeEntity를 생성할 때 Null Pointer Exception이 발생하지 않도록 수정하였으며
public static ItemGetDto.Response fromEntity(Item item, Optional<Admin> admin){
Long adminId = null;
String adminName = null;
String adminEmail = null;
if (admin.isPresent()) {
adminId = admin.get().getId();
adminName = admin.get().getName();
adminEmail = admin.get().getEmail();
}
return ItemGetDto.Response.builder()
.id(item.getId())
.name(item.getName())
.category(item.getCategory())
.price(item.getPrice())
.stock(item.getStock())
.status(item.getStatus())
.createdAt(item.getCreatedAt())
.adminId(adminId)
.adminName(adminName)
.adminEmail(adminEmail)
.build();
}
QueryDsl에 innerJoin을 leftJoin으로 수정하였습니다.
List<ItemGetPageDto.Response> content = queryFactory
.select(Projections.constructor(ItemGetPageDto.Response.class,
item.id,
item.name,
item.category,
item.price,
item.stock,
item.status,
item.createdAt,
item.admin.id,
admin.name
))
.from(item)
.where(itemNameContains(dto), itemStatusEquals(dto), itemCategoryEquals(dto))
.leftJoin(admin)
.on(item.admin.eq(admin))
.orderBy(getSortOrder(pageable))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
github에 코드를 올릴 때 항상 비밀번호같은 정보는 어디에 둘지 항상 고민입니다.
이번에는 application.properties에서 secret.properties라는 파일을 찾아 불러오게 하였습니다.
그리고 secret.properties파일을 .gitignore에 추가하였습니다.
application.properties
...
spring.config.import=optional:file:./secret.properties
...
.gitignore
...
secret.properties
...
테스트 자료가 꼭 필요할 거 같은데 일를 어떻게 만들지 몰라 고민이었습니다. 그래서 AI한테 물어보니 CommandLineRunner라는 inerface가 있었습니다. 이를 상속받은 Componenet를 사용하여 시작할 때 test데이터를 추가하였습니다.
그리고 @ConditionalOnProperty를 이용해 application.properties에 설정이 존재할 경우에만 추가하도록 하였습니다.
@Component
@ConditionalOnProperty(
name = "app.add-test-customers",
havingValue = "true",
matchIfMissing = false
)
@RequiredArgsConstructor
public class TestCustomerAdder implements CommandLineRunner {
private final CustomerRepository customerRepository;
@Override
public void run(String... args) throws Exception {
// 고객 데이터를 db에 저장해둠
for (int i = 1; i <= 30; i++) {
CustomerStatus cs;
if (i % 3 == 1) {
cs = CustomerStatus.ACTIVE;
} else if (i % 3 == 2) {
cs = CustomerStatus.INACTIVE;
} else {
cs = CustomerStatus.STOP;
}
Customer customer = new Customer("customer" + i, "customer" + i + "@gmail.com", "010-1234-1234", cs, LocalDateTime.now());
customerRepository.save(customer);
}
}
}
다른 팀원들이 제가 추가한 데이터를 잘 썼다고 얘기 해 주셨을 때 참 기분이 좋았습니다.
저희는 session을 쓰다가 JWT를 구현하는 방식으로 바꾸었는데 제가 JWT token을 잘 활용하지 못했습니다.
service코드를 보면 아직도 id를 이용하여 Admin의 정보를 많이 불러옵니다.
private Admin getAdminIfExistsAndActive(Long adminId) {
// 일단 admin이 있는지 확인한다
Admin admin = adminRepository.findById(adminId).orElseThrow(
() -> new AdminNotFoundException("존재하지 않는 관리자입니다.")
);
// admin 상태가 ACTIVE인지 확인
if (admin.getStatus() != AdminStatus.ACTIVE) {
throw new ForbiddenException("활성된 관리가가 아닙니다.");
}
return admin;
}
이 코드를 admin을 받는 서비스 코드에서 항상 부르는데 사실 이렇게 하면 JWT를 쓰는 의미가 많이 퇴색되어 아쉽습니다.
상품의 관리자가 없는 고아 상품에 대한 처리를 시간이 없어 못하였습니다.
사실 어떻게 처리할지 아직도 잘 모르겠습니다.
지금 현 상황에서는 고아 상품들도 다른 상품들과 마찬가지로 처리하나 이것이 맞는지 모르겠습니다.
지금은 README에 써있지만 secret.properties를 추가했을 때 README에도 적었어야 했습니다. 저희는 pull request를 merge할 때 마다 화상으로 리뷰를 했습니다. 그때 제가 secret.properties에 대해 설명을 했음에도 팀원들이 햇갈려 했습니다.
그리고 그날 회의에 참석 못한 팀원한테 따로 또 설명을 해야 했습니다. README에 적었더라면 이런 일이 없었을 것입니다.
사실 TIL을 작성하기 전에는 상품 상태와 재고에 대해 크게 고민을 하지 않았습니다...
그래서 이런 코드를 작성했습니다.
public void updateStock(Long stock) {
this.stock = stock;
// item상태가 단종이 아닐 경우 자동으로 업데이트 합니다.
if (this.status != ItemStatus.DISCONTINUED) {
if (this.stock <= 0) {
this.status = ItemStatus.SOLD_OUT;
}
}
}
재고가 0보다 많아질 경우에 상태를 변경하는 로직도 없었는데요...
그러다 보니 TIL을 쓰는 와중에 막판에 추가하게 되었습니다. (팀원분들께 다시한번 죄송합니다).
팀원들과 더 상의를 했거나 제가 그냥 넘기지 않고 진지하게 고민했다면 더 낳은 로직을 생각해냈을것입니다.
저는 팀프로젝트를 한번도 해본적이 없어서 나름 즐거웠습니다. 다른 분들도 저만큼 즐거워 했었으면 좋겠습니다.