<aside> 👉 우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂

과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍

</aside>

1. Controller - Service 계층으로 나누기

Controller는 API의 진입 지점으로써 HTTP Body를 객체로 변환하는 역할만 하게 해주고 로직 처리를 Service로 넘기자.

// FruitService.java

package com.inflearn.Inflearn.Study.day06.service;

import com.inflearn.Inflearn.Study.day06.dto.FruitRequest;
import com.inflearn.Inflearn.Study.day06.dto.FruitSoldResponse;
import com.inflearn.Inflearn.Study.day06.dto.FruitUpdateRequest;
import com.inflearn.Inflearn.Study.day06.entity.Fruit;
import com.inflearn.Inflearn.Study.day06.repository.FruitRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service // 스프링 빈으로 만들어 줌
public class FruitService {

    public void saveFruit(FruitRequest request, JdbcTemplate jdbcTemplate) {
        String sql = "insert into fruit(name, warehousingDate, price) values (?, ?, ?)";
        Fruit fruit = new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice());
        jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void sellFruit(FruitUpdateRequest updateRequest, JdbcTemplate jdbcTemplate) {
        String readSql = "select * from fruit where id = ?";
        boolean isExistFruit = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, updateRequest.getId()).isEmpty();
        if(isExistFruit) {
            throw new IllegalArgumentException("데이터베이스에 팔 과일이 없습니다.");
        }
        String sql = "update fruit set is_sold = 1 where id = ?";

        jdbcTemplate.update(sql, updateRequest.getId());
    }

    public FruitSoldResponse getFruitIsSoldOrNot(String name, JdbcTemplate jdbcTemplate) {
        String sql = "select is_sold, sum(price) from fruit where name = ? group by is_sold";
        boolean isExistFruit = jdbcTemplate.query(sql, (rs, rowNum) -> 0, name).isEmpty();
        if(isExistFruit) {
            throw new IllegalArgumentException("이름과 일치하는 과일이 없습니다.");
        }
        Map<Boolean, Long> soldStatusMap = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            Map<Boolean, Long> map = new HashMap<>();
            while(rs.next()) {
                map.put(rs.getBoolean(1), rs.getLong(2));
            }
            return map;
        });

        Long soldPrice = soldStatusMap.getOrDefault(true, 0L);
        Long notSoldPrice = soldStatusMap.getOrDefault(false, 0L);
        return new FruitSoldResponse(soldPrice, notSoldPrice);
    }
}

// FruitController.java

package com.inflearn.Inflearn.Study.day06.controller;

import com.inflearn.Inflearn.Study.day06.dto.FruitRequest;
import com.inflearn.Inflearn.Study.day06.dto.FruitSoldResponse;
import com.inflearn.Inflearn.Study.day06.dto.FruitUpdateRequest;
import com.inflearn.Inflearn.Study.day06.entity.Fruit;
import com.inflearn.Inflearn.Study.day06.service.FruitService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
public class FruitController {

    private final FruitService fruitService;
    private final JdbcTemplate jdbcTemplate;

    public FruitController(FruitService fruitService, JdbcTemplate jdbcTemplate) {
        this.fruitService = fruitService;
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/api/v2/fruit")
    public void saveFruit(@RequestBody FruitRequest request) {
        fruitService.saveFruit(request, jdbcTemplate);
    }

    @PutMapping("/api/v2/fruit")
    public void sellFruit(@RequestBody FruitUpdateRequest updateRequest) {
        fruitService.sellFruit(updateRequest, jdbcTemplate);
    }

    @GetMapping("/api/v2/fruit/stat")
    public FruitSoldResponse getFruitIsSoldOrNot(@RequestParam String name) {
        return fruitService.getFruitIsSoldOrNot(name, jdbcTemplate);
    }
}

2. Controller - Service - Repository 계층으로 나누기

DB와 연동되어 SQL문을 처리하는 로직은 Repository에게 넘겨주었다.

// FruitRepository.java

package com.inflearn.Inflearn.Study.day06.repository;

import com.inflearn.Inflearn.Study.day06.dto.FruitRequest;
import com.inflearn.Inflearn.Study.day06.dto.FruitSoldResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;

@Repository
public class FruitRepository {
    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save(String name, LocalDate warehousingDate, Long price) {
        String sql = "insert into fruit(name, warehousingDate, price) values (?, ?, ?)";
        jdbcTemplate.update(sql, name, warehousingDate, price);
    }

    public void update(Long id) {
        String readSql = "select * from fruit where id = ?";
        boolean isExistFruit = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
        if(isExistFruit) {
            throw new IllegalArgumentException("데이터베이스에 팔 과일이 없습니다.");
        }
        String sql = "update fruit set is_sold = 1 where id = ?";
        jdbcTemplate.update(sql, id);
    }

    public FruitSoldResponse getList(String name) {
        String sql = "select is_sold, sum(price) from fruit where name = ? group by is_sold";
        boolean isExistFruit = jdbcTemplate.query(sql, (rs, rowNum) -> 0, name).isEmpty();
        if(isExistFruit) {
            throw new IllegalArgumentException("이름과 일치하는 과일이 없습니다.");
        }
        Map<Boolean, Long> soldStatusMap = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            Map<Boolean, Long> map = new HashMap<>();
            while(rs.next()) {
                map.put(rs.getBoolean(1), rs.getLong(2));
            }
            return map;
        });

        Long soldPrice = soldStatusMap.getOrDefault(true, 0L);
        Long notSoldPrice = soldStatusMap.getOrDefault(false, 0L);
        return new FruitSoldResponse(soldPrice, notSoldPrice);
    }
}

// FruitService.java

package com.inflearn.Inflearn.Study.day06.service;

import com.inflearn.Inflearn.Study.day06.dto.FruitRequest;
import com.inflearn.Inflearn.Study.day06.dto.FruitSoldResponse;
import com.inflearn.Inflearn.Study.day06.dto.FruitUpdateRequest;
import com.inflearn.Inflearn.Study.day06.entity.Fruit;
import com.inflearn.Inflearn.Study.day06.repository.FruitRepository;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service // 스프링 빈으로 만들어 줌
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void saveFruit(FruitRequest request) {
        Fruit fruit = new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice());
        fruitRepository.save(fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void sellFruit(FruitUpdateRequest updateRequest) {
        fruitRepository.update(updateRequest.getId());
    }

    public FruitSoldResponse getFruitIsSoldOrNot(String name) {
        return fruitRepository.getList(name);
    }
}

// FruitController.java

package com.inflearn.Inflearn.Study.day06.controller;

import com.inflearn.Inflearn.Study.day06.dto.FruitRequest;
import com.inflearn.Inflearn.Study.day06.dto.FruitSoldResponse;
import com.inflearn.Inflearn.Study.day06.dto.FruitUpdateRequest;
import com.inflearn.Inflearn.Study.day06.service.FruitService;
import org.springframework.web.bind.annotation.*;

@RestController
public class FruitController {

    private final FruitService fruitService;

    public FruitController(FruitService fruitService) {
        this.fruitService = fruitService;
    }

    @PostMapping("/api/v2/fruit")
    public void saveFruit(@RequestBody FruitRequest request) {
        fruitService.saveFruit(request);
    }

    @PutMapping("/api/v2/fruit")
    public void sellFruit(@RequestBody FruitUpdateRequest updateRequest) {
        fruitService.sellFruit(updateRequest);
    }

    @GetMapping("/api/v2/fruit/stat")
    public FruitSoldResponse getFruitIsSoldOrNot(@RequestParam String name) {
        return fruitService.getFruitIsSoldOrNot(name);
    }
}

3. 예외 처리 Service로 넘기기

  1. DB에 값이 있나 확인하는 것은 비즈니스 로직이다. 비즈니스 로직은 Service 에서 처리하는 것이 바람직하다.
  2. isFruitExistByXX()라는 메소드를 Repository에 만들어서 Service단에서 예외를 던지도록 해준다.
// FruitRepository.java

public boolean isFruitExistById(long id) {
        String sql = "select * from fruit where id = ?";
        return jdbcTemplate.query(sql, (rs, rowNum) -> 0, id).isEmpty();
}

public boolean isFruitExistByName(String name) {
    String sql = "select * from fruit where name = ?";
    return jdbcTemplate.query(sql, (rs, rowNum) -> 0, name).isEmpty();
}
// FruitService.java

public void sellFruit(FruitUpdateRequest updateRequest) {
    boolean isExist = fruitRepository.isFruitExistById(updateRequest.getId());
    if (isExist) {
        throw new IllegalArgumentException("데이터베이스에 팔 과일이 없습니다.");
    }
    fruitRepository.update(updateRequest.getId());
}

public FruitSoldResponse getFruitIsSoldOrNot(String name) {
    boolean isExist = fruitRepository.isFruitExistByName(name);
    if (isExist) {
        throw new IllegalArgumentException("이름과 일치하는 과일이 없습니다.");
    }
    return fruitRepository.getList(name);
}