인프런 워밍업 클럽 0기 - 백엔드 코스 (과제6)
문제 1. Fruit 로직 계층형식으로 분리
클린 코드에 대해 짤막하게 배우고 난 뒤, 의미 있는 네이밍과 메소드 분리의 중요성에 대해 알게 되었습니다.
이전 과제4에서 수행했던 Fruit API 만들기에서는 Fruit과 관련된 로직을 모두 Controller
에 작성하는 안 좋은?! 코드를 작성했었습니다. 이전의 코드는 다음과 같습니다.
@PostMapping("/api/v1/fruit")
public void createFruit(@RequestBody FruitCreateRequest request){
fruits.add(new Fruit(request));
}
@PutMapping("/api/v1/fruit")
public void sellFruit(@RequestBody Map<String, Long> request){
fruits.stream().forEach(fruit -> {
if(request.get("id")==fruit.getId()){
fruit.sellFruit();
}
});
}
@GetMapping("/api/v1/fruit/stat")
public FruitStatResponse getFruitStat(@RequestParam String name){
List<Fruit> filteredFruits = fruits.stream().filter(fruit -> fruit.getName().equals(name)).collect(Collectors.toList());
return new FruitStatResponse(filteredFruits);
}
보이는 것과 같이 모든 로직이 Controller
메서드에 포함되어 있습니다!
현재는 아직 적은 양의 로직이지만, DB에 실제 접근하는 코드가 추가되고 다른 로직이 추가된다면 Controller
클래스의 크기는 굉장히 커지게 될 것입니다.
그러므로 Controller
, Service
, Repository
계층으로 코드를 구분해 작성해주도록 하겠습니다.
이처럼 계층 형식으로 구분해주는 이유는 하나의 메서드 상에서 모든 로직이 존재한다면 어떠한 곳에서 에러가 났는지 알기 어렵고 이 후 변경 사항이 생길 경우 수정에 어려움을 겪을 수 있기 때문입니다.
그리고! 문제 2번 새로운 구성의 Repository
가 생길 것을 대비해 Repository
는 FruitRepository
인터페이스를 생성하고 이를 구현하는 식으로 변경해보도록 하겠습니다.
각 계층 클래스는 스프링 빈으로 등록한 뒤, 스프링 컨테이너에 의해 주입 받아 사용하는 방식으로 구성하겟습니다.
FruitController
@RestController
public class FruitController {
private final FruitService fruitService;
public FruitController(FruitService fruitService) {
this.fruitService = fruitService;
}
@PostMapping("/api/v1/fruit")
@ResponseStatus(HttpStatus.CREATED)
public void createFruit(@RequestBody FruitCreateRequest request){
fruitService.registerFruit(request);
}
@PutMapping("/api/v1/fruit")
public void sellFruit(@RequestBody Map<String, Long> request){
if(!request.containsKey("id")){
throw new IllegalArgumentException("해당 id의 과일이 존재하지 않습니다.");
}
fruitService.sellFruit(request.get("id"));
}
@GetMapping("/api/v1/fruit/stat")
public FruitStatResponse getFruitStat(@RequestParam String name) {
return fruitService.getFruitByName(name);
}
}
FruitService
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
public void registerFruit(FruitCreateRequest request) {
fruitRepository.registerFruit(request);
}
public void sellFruit(Long fruitId) {
fruitRepository.updateFruit(fruitId);
}
public FruitStatResponse getFruitByName(String fruitName) {
return fruitRepository.findPriceInfoByName(fruitName);
}
}
FruitRepository
public interface FruitRepository {
void registerFruit(FruitCreateRequest request);
void updateFruit(Long fruitId);
FruitStatResponse findPriceInfoByName(String fruitName);
boolean isFruitNotExist(long id);
}
FruitMemoryRepository
@Repository
public class FruitMemoryRepository implements FruitRepository {
List<FruitDTO> fruits = new ArrayList<>();
@Override
public void registerFruit(FruitCreateRequest request) {
fruits.add(new FruitDTO(request));
}
@Override
public void updateFruit(Long fruitId) {
fruits.stream().forEach(fruit -> {
if (fruitId == fruit.getId()) {
fruit.sellFruit();
}
});
}
@Override
public FruitStatResponse findPriceInfoByName(String fruitName) {
List<Fruit> filteredFruits = fruits.stream()
.filter(fruit -> fruit.getName().equals(fruitName))
.collect(Collectors.toList());
return FruitStatResponse.createFruitStatMemory(filteredFruits);
}
@Override
public boolean isFruitNotExist(long id) {
return fruits.stream().anyMatch(fruit -> fruit.getId() == id);
}
}
Fruit
도메인을 DB에서 관리하도록 변경하면서 id 필드가 없어지게 되었습니다. 그래서 id를 통해 Fruit값이 존재하는지 확인했던 MemoryRepository
에 문제가 발생했습니다. 이를 해결하기 위해 Fruit
을 전체로 상속받은 FruitDto
를 추가해주었고 FruitMemoryRepository
는 FruitDto
리스트를 가지도록 해주었습니다.
이에 따라 여러 메서드들의 매개변수나 리턴 타입들을 수정해주었습니다.
문제 2. 새로운 Repository 형식 추가 & @Primary
이전에는 어플리케이션 실행 시 Memory에 리스트 형식으로 Fruit을 저장하는 방식이었습니다. 이번에는 MySQL에 데이터를 영구히 저장하는 MySqlRepository
를 추가해보도록 하겠습니다.
FruitMySqlRepository
@Repository
@Primary
public class FruitMySqlRepository implements FruitRepository {
private final JdbcTemplate jdbcTemplate;
public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
private RowMapper<FruitStatProjection> rowMapper = BeanPropertyRowMapper.newInstance(FruitStatProjection.class);
@Override
public void registerFruit(FruitCreateRequest request) {
String sql = "INSERT INTO fruit(name,warehousing_date,price,is_sold) VALUES(?,?,?,?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice(), false);
}
@Override
public void updateFruit(Long fruitId) {
if(isFruitNotExist(fruitId)){
throw new IllegalArgumentException("해당 id에 일치하는 과일이 없습니다.");
}
String sql = "UPDATE fruit SET is_sold=true WHERE id = ?";
jdbcTemplate.update(sql,fruitId);
}
@Override
public FruitStatResponse findPriceInfoByName(String fruitName) {
String readSql = "SELECT SUM(price) as price,is_sold FROM fruit GROUP BY is_sold";
List<FruitStatProjection> result = jdbcTemplate.query(readSql, rowMapper);
return FruitStatResponse.createFruitStatDB(result);
}
@Override
public boolean isFruitNotExist(long id) {
String readSql = "SELECT * FROM fruit WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
}
이전처럼 List
에 Fruit
을 저장하지 않고 JdbcTemplate
을 이용해 DB에 데이터를 저장하고 조회하는 방식으로 구성하게 되었습니다.
DB에 삽입, 수정하는 방법은 이전 배운 방식 그대로 활용했습니다.
fruit stat을 반환하는 메서드와 같은 경우 모든 fruit을 조회하고 팔린 것과 팔리지 않은 Fruit을 분리하고 가격의 합을 구하는 로직을 또 구성하기 보다는 GROUP BY
문을 이용해 DB 쿼리로 한 번에 조회할 수 있도록 해주었습니다.
또한, 이제 FruitRepository
가 두 개 생기게 되었으므로 MemoryRepository
빈과 MySqlRepository
빈 중 스프링 컨테이너가 헷깔리지 않도록 @Primary
로 주입할 빈을 알 수 있도록 지정해주었습니다.
이외에도 @Qualifer
를 활용해도 스프링 컨테이너가 지정한 하나의 빈만 자동으로 주입해주는 것을 확인할 수 있습니다.
댓글을 작성해보세요.