4일차에 만든 api를 분리해서 리팩토링 해야한다.
@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
private final JdbcTemplate template;
public FruitShopController(JdbcTemplate template) {
this.template = template;
}
@PostMapping
public void saveFruit(@RequestBody FruitSaveRequest request) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
@PutMapping
public void sellFruit(@RequestBody Map<String, Object> requestJson) {
long id = Long.parseLong(requestJson.get("id").toString());
String readSql = "select id from fruitshop where id = ?";
Boolean isUserNotExist = template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
if (isUserNotExist) {
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@GetMapping("/stat")
public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
문제
각 메서드마다 여러개의 역할을 가지고있는걸알수있다.
Controller에서는 api의 진입지점과 어떤 동작을 하는지 명확하게 알수있게 정리하고
SQL문은 repository에서 처리하고
에러처리 나 추가적인 로직은 service에서 처리할수 있도록 메서드를 계층별로 분리해야한다.
각 메서드를 분리한 예시
한 개로 되어있는 메서드를
SQL부분은 repostory에서
에러처리와 repository의 메서드 호출은 service에서
Controller 에서는 service 계층의 메서드만 호출해준다.
완성 코드
Controller
@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
private FruitService service;
public FruitShopController(FruitService service) {
this.service = service;
}
@PostMapping
public void saveFruit(@RequestBody FruitSaveRequest request) {
service.saveFruit(request.getName(),request.getWarehousingDate(),request.getPrice());
}
@PutMapping
public void updateFruit(@RequestBody Map<String, Object> requestJson) {
long id = Long.parseLong(requestJson.get("id").toString());
service.updateFruit(id);
}
@GetMapping("/stat")
public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
return service.getFruitStat(name);
}
}
Service
public interface FruitService {
void saveFruit(String name, String date, Long price);
void updateFruit(long id);
FruitShopAmoutResponse getFruitStat(String name);
}
@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(FruitRepository repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Long price) {
repository.saveFruit(name,date,price);
}
@Override
public void updateFruit(long id) {
if(repository.isFruitExist(id)){
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
repository.updateFruit(id);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return repository.getFruitStat(name);
}
}
Repository
public interface FruitRepository {
void saveFruit(String name,String date,Long price);
void updateFruit(long id);
boolean isFruitExist(long id);
FruitShopAmoutResponse getFruitStat(String name);
}
@Repository
public class FruitSQLRepository implements FruitRepository {
private final JdbcTemplate template;
public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
this.template = jdbcTemplate;
}
@Override
public void saveFruit(String name, String date, Long price) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, name, date, price);
}
@Override
public void updateFruit(long id) {
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@Override
public boolean isFruitExist(long id) {
String readSql = "select id from fruitshop where id = ?";
return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
이제 컨트롤러는 api의 진입점 역활을 하고 어떤동작을 하는지 한눈에 파악하기 쉬워졌다.
그리고 JdbcTemplate을 의존하지않고 Service클래스만 의존해서 사용하게된다.
스프링 컨테이너 & 스프링 빈
각 계층에서 생성자를 주입받을때 인스턴스를 생성하지 않고 자동으로 생성이 되는걸 알수있다.
이는 스프링에서 지원하는 스프링 컨테이너와 스프링 빈 덕분이다.
각 클래스의 위에 붙어있는 @RestController , @Service , @Repostory 덕분에
스프링은 이 객체들을 빈으로 인식하고 시작되는시점에 컨테이너에 이 객체들을 생성해서 보관하게된다
이로 인해서 필요할때에 객체를 생성하지않고 자동으로 주입해서 사용할수있다.
JdbcTemplate객체의 경우에는 스프링에서 기본으로 등록되어있는 빈이기때문에 따로 설정을 해주지않아도 주입받아서 사용할수있다 이를 의존성주입 (Dependency Injection) 이라고 한다.
나는 서비스와 저장소 객체를 인터페이스로 만들고 그 구현체를 만드는 방법으로 사용했는데
이렇게 하면 Controller나 Service에서 생성자에 주입받을때에 interface객체를 타입으로 넣게되면
스프링컨테이너에서 구현체인 빈을 자동으로 주입해주기 때문에 추후에 구현체를 변경해야할일이 생겼을때
구현체의 코드만 수정해주면 Service계층이나 Controller계층의 코드는 수정하지 않고
자동으로 구현체가 변경되어서 주입된다.
ex) MemoryRepository
@Primary
@Repository
public class FruitMemoryRepository implements FruitRepository{
List<Fruit> fruitRepository = new ArrayList<>();
@Override
public void saveFruit(String name, String date, Long price) {
fruitRepository.add(new Fruit(name,price,date));
}
@Override
public void updateFruit(long id) {
fruitRepository.set((int) id,new Fruit("수정",000,"00-00-00"));
}
@Override
public boolean isFruitExist(long id) {
if(fruitRepository.get((int)id) == null){
return false;
}
return true;
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return new FruitShopAmoutResponse(0,fruitRepository.stream().mapToLong(Fruit::getPrice).sum());
}
}
이렇게 Repository의 구현체를 하나 더 만들고 @Repository 어노테이션을 붙여주면
스프링에서는 어떤 구현체를 주입해야할지 몰라서 에러가 발생하게된다.
이럴때는 @Primary어노테이션을 주입할 객체에 붙여서 우선순위를 만들어주거나
@Qulifier 어노테이션을 이용해서 주입할 빈의 이름을 지정하고
주입받는곳에서 파라미터에 @Qualifier 어노테이션을 같이붙여서 가져오고싶은 빈의 이름을 넣어서 받아올수도있다.
@Qualifier("first") // 빈의 이름을 설정
@Repository
public class FruitMemoryRepository implements FruitRepository{
@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(@Qualifier("first")FruitRepository repository) { //주입시 이름으로 지정해서 받아옴
this.repository = repository;
}
'백엔드 > 자바' 카테고리의 다른 글
[인프런 워밍업 클럽_0기] BE과제 미니프로젝트 1단계 - 1 JpaMappedBy,JoinColumn,1:N,OneToMany,ManyToOne (0) | 2024.03.07 |
---|---|
[인프런 워밍업 클럽_0기] BE세번째 과제 진도표 7일 - Jpa (0) | 2024.02.27 |
[인프런 워밍업 클럽_0기] BE세번째 과제 진도표 2일 - Controller (0) | 2024.02.25 |
[인프런 워밍업 클럽_0기] BE세번째 과제 진도표 1일 - 어노테이션(annotation) (0) | 2024.02.25 |
[인프런 워밍업 클럽_0기] BE네번째 과제 진도표 5일 - 클린코드 (1) | 2024.02.23 |