진도표 6일차와 연결됩니다
우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂
과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍
controller, service, repository로 분리해보자.
파일의 폴더 구조는 아래와 같다.
controller, domain, dto, repository, service 폴더로 이루어져 있으며, 각각에 해당하는 파일들이 위치한다.
FruitController.java
package com.group.libraryapp.controller.fruit;
import com.group.libraryapp.domain.Fruit;
import com.group.libraryapp.dto.fruit.FruitCreateRequest;
import com.group.libraryapp.dto.fruit.FruitOverviewResponse;
import com.group.libraryapp.service.FruitService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@RestController
public class FruitController {
private final FruitService fruitService;
public FruitController(FruitService fruitService) {
this.fruitService = fruitService;
}
//문제1
@PostMapping("/api/v1/fruit")
public void saveFruit(@RequestBody FruitCreateRequest request) {
fruitService.saveFruit(request);
}
//문제2
@PutMapping("/api/v1/fruit")
public void saledFruit(@RequestParam int id){
fruitService.saleFruit(id);
}
//문제3
@GetMapping("/api/v1/fruit/stat")
public FruitOverviewResponse overviewFruit(@RequestParam String name){
return fruitService.overviewFruit(name);
}
}
controller 파일은 심플하게 service의 메소드를 불러오는 방식으로 리팩토링했다.
즉, 메소드만 명시하고, 실제적인 일은 service, controller가 한다는 뜻!
위와 같이 코드를 짬으로서, api 의 진입점의 역할을 잘 하고 있다 볼 수 있다.
FruitService.java
package com.group.libraryapp.service;
import com.group.libraryapp.domain.Fruit;
import com.group.libraryapp.dto.fruit.FruitCreateRequest;
import com.group.libraryapp.dto.fruit.FruitOverviewResponse;
import com.group.libraryapp.repository.FruitRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class FruitService {
private final FruitRepository fruitRepository;
private FruitOverviewResponse fruitOverviewResponse;
public FruitService(@Qualifier("sql") FruitRepository fruitRepository, FruitOverviewResponse fruitOverviewResponse) {
this.fruitRepository = fruitRepository;
this.fruitOverviewResponse = fruitOverviewResponse;
}
public void saveFruit(FruitCreateRequest request){
fruitRepository.saveFruit(request.getName(),request.getPrice(),request.getWarehousingDate());
}
public void saleFruit(int id) {
fruitRepository.saleFruit(id);
}
public FruitOverviewResponse overviewFruit(String name) {
List<Fruit> list = fruitRepository.overviewFruit(name);
long notsalesamount = 0;
long salesamount = 0;
for(Fruit e : list){
if (e.getSaled() == 1)
notsalesamount += e.getPrice();
else
salesamount += e.getPrice();
}
fruitOverviewResponse.setNotSalesAmount(notsalesamount);
fruitOverviewResponse.setSalesAmount(salesamount);
return fruitOverviewResponse;
}
}
FruitService는 FruitRepository의 메소드를 불러오는 역할과 동시에 유저가 있는지 없는지를 확인하고 예외처리를 하는 부분이다.
위 클래스의 overviewFruit 함수에서
선별한 정보를 아래와 같은 HTTP 응답 Body로 반환하기 위한 처리를 하도록 수정하였다. list 형태의 반환값을 FruitRepository의 메소드로 부터 받은 후, 이를 FruitOverviewResponse 객체로 반환하는 역할을 하고 있다.
FruitRepository.java
package com.group.libraryapp.repository;
import com.group.libraryapp.domain.Fruit;
import java.time.LocalDate;
import java.util.List;
public interface FruitRepository {
public void saveFruit(String name, long price, LocalDate warehousingDate);
public void saleFruit(int id);
public List<Fruit> overviewFruit(String name);
}
문제2의 요구사항을 위해, repository를 바꿔가며 동작시킬 수 있도록
강의에서 이 경우에 interface를 작성하였기 때문에 interface 클래스를 작성하였다.
@Primary 어노테이션 대신, @Qualifer 어노테이션을 FruitService 클래스에 사용했다.
FruitMemoryRepository.java
package com.group.libraryapp.repository;
import com.group.libraryapp.domain.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Repository
public class FruitMemoryRepository implements FruitRepository{
private final JdbcTemplate jdbcTemplate;
private List<Fruit> memory = new ArrayList<>();
private int num = 0;
public FruitMemoryRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void saveFruit(String name, long price, LocalDate warehousingDate) {
memory.add(new Fruit(num+1,name, warehousingDate,price,1));
}
public void saleFruit(int id){
for (Fruit e : memory){
if (e.getId() == id)
e.setSaled(0);
else
throw new IllegalArgumentException();
}
}
public List<Fruit> overviewFruit(String name){
List<Fruit> list = new ArrayList<>();
for (Fruit e : memory){
if (e.getName().equals(name)){
list.add(e);
}else
throw new IllegalArgumentException();
}
return list;
}
}
위 클래스는 클래스 내에 List<Fruit> 를 만들어 저장함으로써, 프로그램을 재실행시키면, 기존 정보가 없어지는 repository이다. db와 연결이 없다.
FruitMySqlRepository.java
package com.group.libraryapp.repository;
import com.group.libraryapp.domain.Fruit;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
@Qualifier("sql")
public class FruitMySqlRepository implements FruitRepository{
private final JdbcTemplate jdbcTemplate;
public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void saveFruit(String name, long price, LocalDate warehousingDate) {
String sql = "INSERT INTO fruits(name, warehousingDate, price,saled) Values (?,?,?,?)";
jdbcTemplate.update(sql, name, warehousingDate,price, 1);
}
public void saleFruit(int id){
String readSql = "SELECT * FROM fruits WHERE id = ?";
boolean fruitNotExist = jdbcTemplate.query(readSql, (rs,rowNum)-> 0,id).isEmpty();
if (fruitNotExist){
throw new IllegalArgumentException();
}
//fruit exists
String sql = "UPDATE fruits SET saled = ? WHERE id = ?";
jdbcTemplate.update(sql, 0,id);
}
public List<Fruit> overviewFruit(String name){
String readSql = "SELECT * FROM fruits WHERE name = ?";
List<Fruit> list = jdbcTemplate.query(readSql, (rs, rowNum) -> {
String rs_name = rs.getString("name");
long rs_price = rs.getLong("price");
LocalDate rs_warehousingDate = rs.getDate("warehousingDate").toLocalDate();
int rs_saled = rs.getInt("saled");
return new Fruit(rs_name,rs_warehousingDate,rs_price,rs_saled);
}, name);
if (list.isEmpty())
throw new IllegalArgumentException();
return list;
}
}
위 부분은 반대로 db와의 연결이 있고, 직접적인 sql 문을 사용하여 작성하였다.
@Qualifier 어노테이션이 클래스 위에 붙어있는 것을 확인할 수 있다.
회고
controller 클래스만 쓰면, 여러 기능이 뭉쳐있어 코드의 가독성이 떨어지는데,
위와 같이 3단 분리 및 interface 를 활용하니, 전보다는 클린코드로 작성이 된 것 같다.
'spring' 카테고리의 다른 글
[인프런 워밍업 클럽 1기/BE] 3번째 발자국 (0) | 2024.05.19 |
---|---|
[인프런 워밍업 클럽 7차 과제-5/16] 백엔드 (0) | 2024.05.16 |
[인프런 워밍업 클럽 4차 과제-5/7] 백엔드 (1) | 2024.05.07 |
[인프런 워밍업 클럽 3차 과제-5/3] (0) | 2024.05.03 |
[인프런 워밍업 클럽 1차 과제-4/29] (0) | 2024.04.29 |