1. 프로젝트 진행 상황 및 계획
🥇 Baro5Nda(바로온다) 프로젝트의 통합 검색 기능 리팩토링하기 (진행 중, 2025.01.21 완료 목표)
🥈 'Request Body vs Request Param vs Path Variable' 특강 준비하기 (진행 전, 2025.01.19 완료 목표)
🥉 도전 과제 1단계 끝내기 (진행 중, 2025.01.22 완료 목표)
4️⃣ 'AWS의 모든 것(All about AWS)' 강의 모두 듣기 (진행 중, 2025.01.20 완료 목표)
5️⃣ '자주 사용하는 메서드(method) 및 변수 이름 잘 짓기' 특강 준비하기 (진행 전, 2025.01.20 완료 목표)
2. 통합 검색 기능을 리팩토링(refactoring)하기로 결심했다. [깃허브 링크]
(1) ERD (Entity Relationship Diagram) ▼
(2) 작성한 코드 모음 ▼
package com.example.outsourcingproject.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalTime;
import lombok.Getter;
import org.hibernate.annotations.Comment;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "stores")
@Getter
public class Store extends BaseEntity {
@Comment("가게 식별자")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT")
private Long id;
@Comment("사장님 식별자")
private Long ownerId;
@Comment("가게 이름")
@Column(
name = "store_name",
nullable = false
)
private String storeName;
@Comment("가게 전화번호")
@Column(
name = "store_telephone",
nullable = false
)
private String storeTelephone;
@Comment("가게 주소")
@Column(
name = "store_address",
nullable = false
)
private String storeAddress;
@Comment("주문 최소 금액")
@Column(
name = "minimum_purchase",
nullable = false
)
private Integer minimumPurchase;
@Comment("여는 시간")
@Column(
name = "opens_at",
nullable = false
)
private LocalTime opensAt;
@Comment("닫는 시간")
@Column(
name = "closes_at",
nullable = false
)
private LocalTime closesAt;
@Comment("첫 번째 가게 카테고리 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_category_one_id")
private StoreCategory storeCategoryOne;
@Comment("두 번째 가게 카테고리 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_category_two_id")
private StoreCategory storeCategoryTwo;
protected Store() {
}
public Store(
Long ownerId,
String storeName,
String storeAddress,
String storeTelephone,
Integer minimumPurchase,
LocalTime opensAt,
LocalTime closesAt,
StoreCategory storeCategoryOne,
StoreCategory storeCategoryTwo
) {
this.ownerId = ownerId;
this.storeName = storeName;
this.storeTelephone = storeTelephone;
this.storeAddress = storeAddress;
this.minimumPurchase = minimumPurchase;
this.opensAt = opensAt;
this.closesAt = closesAt;
this.storeCategoryOne = storeCategoryOne;
this.storeCategoryTwo = storeCategoryTwo;
}
public void update(
String storeName,
String storeAddress,
String storeTelephone,
Integer minimumPurchase,
LocalTime opensAt,
LocalTime closesAt
) {
this.storeName = storeName;
this.storeAddress = storeAddress;
this.storeTelephone = storeTelephone;
this.minimumPurchase = minimumPurchase;
this.opensAt = opensAt;
this.closesAt = closesAt;
}
}
package com.example.outsourcingproject.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import org.hibernate.annotations.Comment;
@Entity
@Table(name = "menus")
@Getter
public class Menu extends BaseEntity {
@Comment("메뉴 식별자")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT")
private Long id;
@Comment("메뉴 이름")
@Column(
name = "menu_name",
nullable = false,
length = 100
)
private String menuName;
@Comment("메뉴 가격")
@Column(
name = "menu_price",
nullable = false
)
private Integer menuPrice;
@Comment("메뉴 정보")
@Column(
name = "menu_info",
nullable = false
)
private String menuInfo;
@Comment("가게 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "store_id",
nullable = false
)
private Store store;
@Comment("첫 번째 메뉴 카테고리 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_category_one_id")
private MenuCategory menuCategoryOne;
@Comment("두 번째 메뉴 카테고리 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_category_two_id")
private MenuCategory menuCategoryTwo;
@Comment("세 번째 메뉴 카테고리 식별자")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_category_three_id")
private MenuCategory menuCategoryThree;
protected Menu() {
}
public Menu(
String menuName,
Integer menuPrice,
String menuInfo,
Store store,
MenuCategory menuCategoryOne,
MenuCategory menuCategoryTwo,
MenuCategory menuCategoryThree
) {
this.menuName = menuName;
this.menuPrice = menuPrice;
this.menuInfo = menuInfo;
this.store = store;
this.menuCategoryOne = menuCategoryOne;
this.menuCategoryTwo = menuCategoryTwo;
this.menuCategoryThree = menuCategoryThree;
}
public void update(
String menuName,
Integer menuPrice,
String menuInfo
) {
if (menuName != null) {
this.menuName = menuName;
}
if (menuPrice != null) {
this.menuPrice = menuPrice;
}
if (menuInfo != null) {
this.menuInfo = menuInfo;
}
}
}
package com.example.outsourcingproject.store.repository;
import com.example.outsourcingproject.entity.Store;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StoreRepository extends JpaRepository<Store, Long> {
List<Store> findByStoreCategoryOne_NameOrStoreCategoryTwo_NameAndIsDeleted(
String storeCategoryOneName,
String storeCategoryTwoName,
Integer isDeleted
);
}
package com.example.outsourcingproject.menu.repository;
import com.example.outsourcingproject.entity.Menu;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MenuRepository extends JpaRepository<Menu, Long> {
List<Menu> findByMenuCategoryOne_NameOrMenuCategoryTwo_NameOrMenuCategoryThree_NameAndIsDeleted(
String categoryOneName,
String categoryTwoName,
String categoryThreeName,
Integer isDeleted
);
}
package com.example.outsourcingproject.store.service;
import com.example.outsourcingproject.auth.repository.OwnerAuthRepository;
import com.example.outsourcingproject.category.repository.StoreCategoryRepository;
import com.example.outsourcingproject.entity.Menu;
import com.example.outsourcingproject.entity.Owner;
import com.example.outsourcingproject.entity.Store;
import com.example.outsourcingproject.entity.StoreCategory;
import com.example.outsourcingproject.exception.badrequest.CategoryInvalidCountException;
import com.example.outsourcingproject.exception.badrequest.StoreInvalidCountExcessException;
import com.example.outsourcingproject.exception.notfound.OwnerNotFoundException;
import com.example.outsourcingproject.menu.repository.MenuRepository;
import com.example.outsourcingproject.store.dto.request.CreateStoreRequestDto;
import com.example.outsourcingproject.store.dto.response.CreateStoreResponseDto;
import com.example.outsourcingproject.store.dto.response.StoreCategorySearchResponseDto;
import com.example.outsourcingproject.store.repository.StoreRepository;
import com.example.outsourcingproject.utils.JwtUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class StoreServiceImpl implements StoreService {
private final StoreRepository storeRepository;
private final OwnerAuthRepository ownerAuthRepository;
private final JwtUtil jwtUtil;
private final MenuRepository menuRepository;
private final StoreCategoryRepository storeCategoryRepository;
@Transactional
@Override
public CreateStoreResponseDto createStore(
CreateStoreRequestDto requestDto,
String token
) {
String ownerEmail = jwtUtil.extractOwnerEmail(token);
Owner foundOwner = ownerAuthRepository.findByEmail(ownerEmail)
.orElseThrow(OwnerNotFoundException::new);
Long storeCount = storeRepository.countByOwnerIdAndIsDeleted(
foundOwner.getId(),
0
);
if (storeCount >= 3) {
throw new StoreInvalidCountExcessException();
}
List<String> storeCategoryNameList = new ArrayList<>();
storeCategoryNameList = requestDto.getStoreCategoryNameList();
if (storeCategoryNameList.size() != 2) {
throw new CategoryInvalidCountException();
}
List<StoreCategory> storeCategoryList = new ArrayList<>();
storeCategoryList = storeCategoryRepository.findAllByNameIn(
storeCategoryNameList,
Sort.unsorted()
);
Store storeToSave = new Store(
foundOwner.getId(),
requestDto.getStoreName(),
requestDto.getStoreAddress(),
requestDto.getStoreTelephone(),
requestDto.getMinimumPurchase(),
requestDto.getOpensAt(),
requestDto.getClosesAt(),
storeCategoryList.get(0),
storeCategoryList.get(1)
);
Store savedStore = storeRepository.save(storeToSave);
return new CreateStoreResponseDto(savedStore);
}
@Transactional
@Override
public List<StoreCategorySearchResponseDto> readAllStoresByCategories(
String storeCategoryName
) {
List<Store> storeList = new ArrayList<>();
storeList = storeRepository.findByStoreCategoryOne_NameOrStoreCategoryTwo_NameAndIsDeleted(
storeCategoryName,
storeCategoryName,
0
);
List<Menu> menuList = new ArrayList<>();
menuList = menuRepository.findByMenuCategoryOne_NameOrMenuCategoryTwo_NameOrMenuCategoryThree_NameAndIsDeleted(
storeCategoryName,
storeCategoryName,
storeCategoryName,
0
);
Set<Long> storeIdSet = new HashSet<>();
menuList.stream()
.map(menu -> menu.getStore().getId())
.forEach(storeIdSet::add);
storeList.stream()
.map(Store::getId)
.forEach(storeIdSet::add);
List<Store> searchedStoreList = new ArrayList<>();
searchedStoreList = storeRepository.findAllById(storeIdSet);
List<StoreCategorySearchResponseDto> responseDtoList = new ArrayList<>();
responseDtoList = searchedStoreList.stream()
.map(StoreCategorySearchResponseDto::new)
.toList();
return responseDtoList;
}
}
package com.example.outsourcingproject.store.dto.request;
import java.time.LocalTime;
import java.util.List;
import lombok.Getter;
@Getter
public class CreateStoreRequestDto {
private final String storeName;
private final String storeAddress;
private final String storeTelephone;
private final Integer minimumPurchase;
private final LocalTime opensAt;
private final LocalTime closesAt;
private final List<String> storeCategoryNameList;
public CreateStoreRequestDto(
String storeName,
String storeAddress,
String storeTelephone,
Integer minimumPurchase,
LocalTime opensAt,
LocalTime closesAt,
List<String> storeCategoryNameList
) {
this.storeName = storeName;
this.storeAddress = storeAddress;
this.storeTelephone = storeTelephone;
this.minimumPurchase = minimumPurchase;
this.opensAt = opensAt;
this.closesAt = closesAt;
this.storeCategoryNameList = storeCategoryNameList;
}
}
package com.example.outsourcingproject.store.dto.response;
import com.example.outsourcingproject.entity.Store;
import java.time.LocalTime;
import lombok.Getter;
@Getter
public class StoreCategorySearchResponseDto {
private final String storeName;
private final String storeAddress;
private final String storeTelephone;
private final Integer minimumPurchase;
private final LocalTime opensAt;
private final LocalTime closesAt;
public StoreCategorySearchResponseDto(Store store) {
this.storeName = store.getStoreName();
this.storeAddress = store.getStoreAddress();
this.storeTelephone = store.getStoreTelephone();
this.minimumPurchase = store.getMinimumPurchase();
this.opensAt = store.getOpensAt();
this.closesAt = store.getClosesAt();
}
}
package com.example.outsourcingproject.menu.dto.request;
import java.util.List;
import lombok.Getter;
@Getter
public class CreateMenuRequestDto {
private final String menuName;
private final Integer menuPrice;
private final String menuInfo;
private final List<String> menuCategoryNameList;
public CreateMenuRequestDto(
String menuName,
Integer menuPrice,
String menuInfo,
List<String> menuCategoryNameList
) {
this.menuName = menuName;
this.menuPrice = menuPrice;
this.menuInfo = menuInfo;
this.menuCategoryNameList = menuCategoryNameList;
}
}
package com.example.outsourcingproject.menu.dto.response;
import com.example.outsourcingproject.entity.Menu;
import com.example.outsourcingproject.entity.MenuCategory;
import lombok.Getter;
@Getter
public class CreateMenuResponseDto {
private final Long id;
private final String menuName;
private final Integer menuPrice;
private final String menuInfo;
private final MenuCategory menuCategoryOne;
private final MenuCategory menuCategoryTwo;
private final MenuCategory menuCategoryThree;
public CreateMenuResponseDto(Menu menu) {
this.id = menu.getId();
this.menuName = menu.getMenuName();
this.menuPrice = menu.getMenuPrice();
this.menuInfo = menu.getMenuInfo();
this.menuCategoryOne = menu.getMenuCategoryOne();
this.menuCategoryTwo = menu.getMenuCategoryTwo();
this.menuCategoryThree = menu.getMenuCategoryThree();
}
}
(3) 데이터베이스(database)에 생성한 가게용 카테고리 및 메뉴용 카테고리 ▼
(4) 가게 및 메뉴 생성 ▼
[가게1]
- 가게용 카테고리: 치킨, 야식
- 메뉴용 카테고리: 퓨전, 초밥, 회
[가게2]
- 가게용 카테고리: 초밥, 일식
- 메뉴용 카테고리: 치킨, 야식, 초밥
(5) '치킨' 검색어로 가게 조회 ▼
[가게1]
- 가게용 카테고리: 치킨, 야식
- 메뉴용 카테고리: 퓨전, 초밥, 회
[가게2]
- 가게용 카테고리: 초밥, 일식
- 메뉴용 카테고리: 치킨, 야식, 초밥
'치킨'을 검색하면 가게1은 가게용 카테고리에, 가게2는 메뉴용 카테고리에 '치킨'이 있어서 검색되었다.
(6) '회' 검색어로 가게 조회 ▼
[가게1]
- 가게용 카테고리: 치킨, 야식
- 메뉴용 카테고리: 퓨전, 초밥, 회
[가게2]
- 가게용 카테고리: 초밥, 일식
- 메뉴용 카테고리: 치킨, 야식, 초밥
'회'로 검색했을 때는 메뉴용 카테고리로 '회'를 고른 가게1만 조회되었다. 이렇게 가게용 카테고리뿐만 아니라 메뉴용 카테고리까지 있으면, 사장님이 대표 메뉴를 2개만 고를 수 있어서 내심 아쉬웠을 점을 어느 정도 해소해 줄 수 있을 듯했다.
(7) '초밥' 검색어로 가게 조회 ▼
[가게1]
- 가게용 카테고리: 치킨, 야식
- 메뉴용 카테고리: 퓨전, 초밥, 회
[가게2]
- 가게용 카테고리: 초밥, 일식
- 메뉴용 카테고리: 치킨, 야식, 초밥
가게2에는 가게용 카테고리와 메뉴용 카테고리에 '초밥'이 있어서 HashSet을 사용하여 중복으로 조회되지 않도록 했다. List와 HashSet과 Query method처럼 기존에 배운 지식을 모두 활용하여 기능을 구현했을 때 정말 뿌듯했다. 물론 완전히 만족스럽지는 않았다. 카테고리 개수가 바뀌면 테이블의 열 또한 개수를 늘리거나 줄여야 했기 때문이었다. 그뿐인가. DTO도 모조리 고쳐야 했다. 그런 문제점을 발견한 만큼, 주말 동안 쉽진 않겠으나 리팩토링하기로 했다.
3. 정신 차리고 보니 기초 특강만 세 개를 잡았다.
거창하진 않지만, 메서드(method)나 변수 이름을 어떻게 짓는지, JPA를 쓰면서 자주 사용하는 메서드가 도대체 무엇인지, 여기에 Request가 붙은 건 참 많은데 대체 무슨 차이가 있는지 설명해 주면 좋겠다는 수요가 있어서 정신 차리고 보니 기초 특강만 세 개를 맡았다. 곰곰이 생각해 보니 꼭 어려운 개념과 기술만 특강해야 한다는 규칙은 없었다. 한 번 간단하게 PPT를 만들어서 준비해 보기로 했다. 개발 공부를 하기 전까지 다른 분야에서 일하면서 쌓아온 크고 작은 경험을 실컷 녹여내야겠다.
'끝을 보는 용기' 카테고리의 다른 글
Day 105 - 오랜만의 휴식 후 특강 준비 중 (수정 중) (0) | 2025.01.19 |
---|---|
Day 104 - 플러스 프로젝트 3단계 중, 더 나은 통합 검색 기능이 되려면 어떤 데이터베이스 구조가 좋을까? (0) | 2025.01.18 |
Day 102 - 플러스 프로젝트 2단계 완료, 지금이야말로 느긋함과 멀어져야 할 때 (0) | 2025.01.16 |
Day 101 - 플러스 프로젝트 1단계 완료, 'update' vs 'change' vs 'modify', 'invalid' vs 'wrong' (0) | 2025.01.15 |
Day 100 - JPA 심화 강의 완강, 유독 잠이 쏟아진 하루 (0) | 2025.01.14 |