강의 출처
진도표 6일차와 연결됩니다
우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂
과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍
문제 1
FruitController 대폭 수정
일단 기존에 적혀있던 JdbcTemplate 관련 코드를 삭제했다! FruiteRepository를 @Repository로 스프링 빈에 등록해 줄 것이라 Controller에는 필요 없어서 제거했다. 그리고 생성자로 FruitService를 주입받게 하고 Controller 코드를 정리했다.
@RequestMapping("/api/v1")
@RestController
public class FruitController {
private final FruitService fruitService;
public FruitController(FruitService fruitService) {
this.fruitService = fruitService;
}
@PostMapping("/fruit")
public void save(@RequestBody FruitSaveRequest request) {
fruitService.save(request);
}
@PutMapping("/fruit")
public void update(@RequestBody long id) {
fruitService.update(id);
}
@GetMapping("/fruit/stat")
public FruitStatResponse fruitStat (@RequestParam String name) {
return fruitService.fruitStat(name);
}
}
FruitService 생성
FruitService는 스프링 빈에 등록하기 위해서 @Service 어노테이션을 사용했다.
그리고 필드 및 생성자로 FruitMysqlRepository 구현체를 주입받았다. 이 부분은 문제 2에서 변경될 예정이다.
@Service
public class FruitService {
private final FruitMysqlRepository fruitMysqlRepository;
public FruitService(FruitMysqlRepository fruitMysqlRepository) {
this.fruitMysqlRepository = fruitMysqlRepository;
}
public void save(FruitSaveRequest request) {
fruitMysqlRepository.save(request);
}
public void update(long name) {
fruitMysqlRepository.update(name);
}
public FruitStatResponse fruitStat(String name) {
long salesAmount = fruitMysqlRepository.getAmount(name, 1);
long notSalesAmount = fruitMysqlRepository.getAmount(name, 0);
return new FruitStatResponse(salesAmount, notSalesAmount);
}
}
FruitService를 작성하면서 fruitStat() 메서드를 리팩토링 할 수 있을 것 같아 냉큼 변경했다.
기존에 방식은 아래와 같다.
@GetMapping("/fruit/stat")
public FruitStatResponse fruitStat (@RequestParam String name) {
String salesSql = "select sum(price) as sum from fruit where salesYN = 1 group by name having name = ?";
String notSalesSql = "select sum(price) as sum from fruit where salesYN = 0 group by name having name = ?";
long salesAmount = getSum(salesSql, name);
long notSalesAmount = getSum(notSalesSql, name);
return new FruitStatResponse(salesAmount, notSalesAmount);
}
private long getSum(String sql, String fruitNm) {
return jdbcTemplate.queryForObject(sql, Long.class, fruitNm);
}
sql에 salesYN 값을 하드코딩 해 놓아서 sql문을 두 개나 만들었는데 salesYN 부분을 동적으로 줘서 하나의 sql로 변경해주려고 repository의 getAmount() 메서드의 salesYN 값을 매개변수로 넘겨주었다.
FruitRepository 생성 (FruitMysqlRepository)
FruitMysqlRepository에 @Repository 어노테이션을 붙여 스프링 빈으로 등록했다. 덕분에 Controller에서부터 생성자 매개변수로 받아오던 JdbcTemplate을 Repository에서 직접 불러올 수 있게 되었다!
@Repository
public class FruitMysqlRepository {
private final JdbcTemplate jdbcTemplate;
public FruitMysqlRepository(JdbcTemplate jdbcTemplate) {
System.out.println("FruitMysqlRepository.FruitMysqlRepository");
this.jdbcTemplate = jdbcTemplate;
}
public void save(FruitSaveRequest request){
String sql = "insert into fruit (name, warehousingDate, price) values (?, ?, ?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
public void update(long id) {
String sql = "update fruit set salesYN = 1 where id = ?";
jdbcTemplate.update(sql, id);
}
public long getAmount(String name, int salesYN) {
String salesSql = "select sum(price) as sum from fruit where salesYN = ? group by name having name = ?";
return jdbcTemplate.queryForObject(salesSql, Long.class, salesYN, name);
}
}
문제 2
FruitRepository Interface 생성
두 개의 구현체가 상속받을 FruitRepository Interface를 생성한다.
public interface FruitRepository {
void save(FruitSaveRequest request);
void update(long id);
long getAmount(String name, int salesYN);
}
FruitMysqlRepository 수정
@Repository
public class FruitMysqlRepository implements FruitRepository{ ... }
FruitMemoryRepository 생성
@Primary
@Repository
public class FruitMemoryRepository implements FruitRepository{
private final List<Fruit> fruits = new ArrayList<>();
private long sequence = 0L;
@Override
public void save(FruitSaveRequest request) {
System.out.println("FruitMemoryRepository.save");
fruits.add(new Fruit(++sequence, request.getName(),
request.getPrice(), request.getWarehousingDate()));
}
@Override
public void update(long id) {
System.out.println("FruitMemoryRepository.update");
for (Fruit fruit : fruits) {
if(fruit.getId() == id) {
fruit.setSalesYN();
break;
}
}
}
@Override
public long getAmount(String name, int salesYN) {
System.out.println("FruitMemoryRepository.getAmount");
long amount = 0;
for (Fruit fruit : fruits) {
if(fruit.getName().equals(name))
{
if(fruit.getSalesYN() == salesYN) {
amount += fruit.getPrice();
}
}
}
return amount;
}
}
FruitMemoryRepository의 save() 메서드를 구현하면서 Fruit의 domain이 뭔가 수상하다는 걸 알았다..! 필드를 제대로 적어주지 않았고, 생성자 또한 조금 대충 만들었던 듯하다.
기존 Fruit Doamin
public class Fruit {
private long id;
private String name;
private long price;
private int salesYN;
public Fruit(long id, String name, long price, int salesYN) {
this.id = id;
this.name = name;
this.price = price;
this.salesYN = salesYN;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public int getSalesYN() {
return salesYN;
}
}
수정된 코드..
save()에서 매개변수로 받아오는 FruitSaveRequest의 필드가 name, warehousingDate, price 세 개인데 Fruit domain에 warehousingDate가 빠져있고, save() 개발시 필요한 id, name, price, warehousingDate 세 개의 매개변수를 가진 생성자도 만들어줬다. 그리고 update()를 위해 salesYN에 대한 setter를 만들어주었다.
public class Fruit {
private long id;
private String name;
private long price;
private LocalDate warehousingDate; // 추가
private int salesYN;
public Fruit(long id, String name, long price, LocalDate warehousingDate) { // 추가
this.id = id;
this.name = name;
this.price = price;
this.warehousingDate = warehousingDate;
}
public Fruit(long id, String name, long price, LocalDate warehousingDate, int salesYN) {
this.id = id;
this.name = name;
this.price = price;
this.warehousingDate = warehousingDate;
this.salesYN = salesYN;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public LocalDate getWarehousingDate() {
return warehousingDate;
}
public int getSalesYN() {
return salesYN;
}
public void setSalesYN() {
this.salesYN = 1;
}
}
우선순위 정하기
- FruitService 생성자 코드를 변경해준다. 그리고 하단의 변수명도 바꿔준다.
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
public void save(FruitSaveRequest request) {
fruitRepository.save(request);
}
public void update(long name) {
fruitRepository.update(name);
}
public FruitStatResponse fruitStat(String name) {
long salesAmount = fruitRepository.getAmount(name, 1);
long notSalesAmount = fruitRepository.getAmount(name, 0);
return new FruitStatResponse(salesAmount, notSalesAmount);
}
}
@Primary
FruitMemoryRepository에 @Primary를 추가해준다.
- save() 테스트
- update() 테스트하려다가 에러 발생..!
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `long` from Object value (token `JsonToken.START_OBJECT`)]
#4번째 과제를 했었을 땐 잘 돌았었던 거 같은데 FruitMemoryRepository에서는 왜 오류가 날까..? 일단 dto를 만들어 에러를 피해보자.
- FruitSalesUpdateRequest
public class FruitSalesUpdateRequest {
private long id;
public FruitSalesUpdateRequest() {
}
public FruitSalesUpdateRequest(long id) {
this.id = id;
}
public long getId() {
return id;
}
}
- FruitController 수정
@PutMapping("/fruit")
public void update(@RequestBody FruitSalesUpdateRequest request) {
fruitService.update(request.getId());
}
다시 update() 포스트맨 테스트
- 3번째 테스트를 위한 데이터 추가
id가 3인 데이터 판매 여부 업데이트
- fruitStat() 테스트
@Qualifier
- FruitMysqlRepository에 Qualifier 추가
@Qualifier("first")
@Repository
public class FruitMysqlRepository implements FruitRepository{ ... }
- FruitService 수정
public FruitService(@Qualifier("first") FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
- save() 포스트맨 테스트
서버를 실행하면 FruitMysqlRepository 생성자를 통해 콘솔에 println이 찍힌 걸 확인할 수 있다.
DB 확인
- update() 포스트맨 확인
DB 확인
- fruitStat() 포스트맨 확인
이번 과제는 강의에서 많이 다룬 내용이 대부분이라 많이 어렵진 않았다..! 다만 시간이 부족할 뿐..!
내일 출근 누가 해...