[기초 API 구현 3] - 메뉴 등록 & 조회 API

박성규·2022년 6월 9일
0

[스프링 부트]

목록 보기
38/38
post-thumbnail

1.조건

  • 음식점 ID 및 음식 정보 입력받아 등록
    1. 음식점 ID (restaurantId)
      • 음식점 DB 테이블 ID
    2. 음식명 (name)
      • 같은 음식점 내에서는 음식 이름이 중복될 수 없음 (예. '자담치킨 강남점'에 '후라이드치킨' 이 이미 등록되어 있다면 중복하여 등록할 수 없지만, 다른 음식점인 '맘스터치 강남점'에는 '후라이드치킨' 을 등록 가능)
    3. 가격 (price)
      • 허용값: 100원 ~ 1,000,000원
      • 100 원 단위로만 입력 가능 (예. 2,220원 입력 시 에러발생. 2,300원 입력 가능)
      • 허용값이 아니거나 100원 단위 입력이 아닌 경우 에러 발생시킴

2.Entity 설계

@Entity
@Getter
@Setter
@Table(name = "menu")
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected로 생성자 만들기
public class Menu {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "menu_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "restaurant_id")
    private Restaurant restaurant;

    private String foodName;
    private int foodPrice;

    //==생성 메서드==//
    public static Menu createMenu(Restaurant restaurant, String foodName, int foodPrice){
        Menu menu = new Menu();
        menu.setRestaurant(restaurant);
        menu.setFoodPrice(foodPrice);
        menu.setFoodName(foodName);
        return menu;
    }
}

Menu 엔티티는 Restaurant, OrderItem 엔티티와 다대일 관계를 가지고 있지만, OrderItem 엔티티와는 양뱡향을 가지지 않고 단뱡향을 가지도록 설계했다.

양뱡향이란, 자식도 부모를 호출해서 사용할 수 있도록 설계하는 것인데, 비즈니스 로직에서 Menu가 어디 OrderItem에 속해 있을지는 크게 중요하지 않다고 판단했다.


3.MenuRequestDto

@Data
public class MenuRegisterDto {
    private Long restaurantId;
    private String name;
    @Max(value = 1000000)
    @Min(value = 100)
    private int price;

    @AssertTrue(message = "100원 단위로 입력하세요.")
    private boolean priceCheck;

    public boolean priceBound(){
        if(getPrice() % 100 == 0){
            return true;
        }else{
            return false;
        }
    }

    public MenuRegisterDto(Long restaurantId, String name, int price, boolean priceCheck) {
        this.restaurantId = restaurantId;
        this.name = name;
        this.price = price;
        this.priceCheck = priceBound();
    }
}

설계 조건에 해당하는 부분은 Validation을 사용했다. 서비스 로직에서 Exception에 대해 던져줘도 되지만, 이번 설계는 도메인 로직 구현에 집중했기 때문에 Dto에서 값을 받아올 때부터 처리하도록 설계했다.


4.MenuRepository

@Repository
@RequiredArgsConstructor
public class MenuRepository {
    private final EntityManager em;

    //메뉴 저장
    public void save(Menu menu){
        em.persist(menu);
    }

    public List<Menu> findAll(Long restaurantId){
        return em.createQuery("select m from Menu m where m.restaurant.id = :restaurantId", Menu.class)
                .setParameter("restaurantId", restaurantId)
                .getResultList();
    }

    public Menu findOne(Long menuId){
        return em.find(Menu.class, menuId);
    }

    public Menu findByName(String menuName, Long restaurantId){
        try {
            return em.createQuery(" select  m from Menu m where m.foodName = :menuName and m.restaurant.id = :restaurantId",Menu.class)
                    .setParameter("menuName", menuName)
                    .setParameter("restaurantId", restaurantId)
                    .getSingleResult();
        }catch (NoResultException e){
            return null;
        }

    }
}

다른 메서드는 단순 저장 & 조회 로직이라서 크게 알아볼 것은 없다.
마지막 메서드인 findByName에서는 설계 조건 중 하나인 하나의 식당에서 중복된 이름의 메뉴를 등록시킬 수 없는 제약사항을 구현했다.

menuName과 restaurantId를 입력받아 restaurantId의 메뉴에서 menuName과 같은게 있는지 확인했고, NoResultException에 대해서 처리해줬다.


5.MenuService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MenuService {
    private final MenuRepository menuRepository;
    private final RestaurantRepository restaurantRepository;
    
    //메뉴 저장
    @Transactional
    public void save(List<MenuRegisterDto> menuRegisters, Long restaurantId) {
        Restaurant restaurant = restaurantRepository.findOne(restaurantId);
        for(MenuRegisterDto menuRegister:menuRegisters){
            Menu menu = Menu.createMenu(restaurant,menuRegister.getName(), menuRegister.getPrice());
            Menu byName = menuRepository.findByName(menu.getFoodName(), restaurantId);
            if (byName == null){
                menuRepository.save(menu);
            }else {
                throw new IllegalArgumentException("해당 메뉴가 존재합니다.");
            }

        }
    }
    //메뉴 조회
    public List<Menu> findAll(Long restaurantId){
        return menuRepository.findAll(restaurantId);
    }
}

서비스에선 메뉴를 저장할 때 앞서 보았던 menuRepository.findByName을 먼저 실행시킨 후 결과가 null일 경우(NoResultException 이 발생할 경우)에만 save되도록 설계했다. 만약 null이 아닐경우 Exception을 통해 트랜잭션 롤백되도록 설계했다.


6.MenuController

@Validated
@RestController
@RequiredArgsConstructor
public class MenuController {
    private final MenuService menuService;

    @PostMapping("/restaurant/{restaurantId}/food/register")
    public void save(@RequestBody @Valid List<MenuRegisterDto> menuRegisters, @PathVariable Long restaurantId){
        menuService.save(menuRegisters, restaurantId);
    }

    @GetMapping("/restaurant/{restaurantId}/foods")
    public JsonResult findAll(@PathVariable Long restaurantId){
        List<Menu> menus = menuService.findAll(restaurantId);

        List<MenuResponse> collect = menus.stream()
                .map(m -> new MenuResponse(m.getId(), m.getFoodName(), m.getFoodPrice()))
                .collect(Collectors.toList());

        return new JsonResult(collect);
    }
}

메뉴 컨트롤러에서는 stream과 map을 활용해 응답받은 Menu 엔티티 객체를 MenuResponse 객체로 변경해서 결과에 담아 주었다.


7.JsonResult

@Data
@AllArgsConstructor
public class JsonResult<T> {
    private T data;
}

이번에 새롭게 알게된 사실인데, Dto 객체를 바로 넘겨주기 보단 다른 클래스로 감싸서 보내게 된다면 json의 데이터별로 이름을 씌어서 보내줄 수가 있다고 한다.
잘 써먹어야지.

0개의 댓글

관련 채용 정보