끝을 보는 용기

Day 103 - 플러스 프로젝트 3단계 중, Baro5Nda(바로온다) 프로젝트의 통합 검색 기능 리팩토링 도전, 기초 특강만 세 개 잡다

writingforever162 2025. 1. 17. 23:35

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를 만들어서 준비해 보기로 했다. 개발 공부를 하기 전까지 다른 분야에서 일하면서 쌓아온 크고 작은 경험을 실컷 녹여내야겠다.