
진도표 6일차와 연결됩니다.
우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다.
앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다. 🙂
과제#4 에서 만들었던 API를 강의 내용처럼 Controller - Service - Repository로 분리해보세요!
과제#4 에서 만들었던 API를 강의 내용처럼 Controller - Service - Repository로 분리해보세요!
💡 문제점
- Day 4 때 이미 분리해서 만들어버렸다 ..
그래서 반대로 합쳐서 만들어서 다시 한번 계층 간 분리의 필요성을 느껴보자고 마음먹었습니다.
Controller//private final JdbcTemplate jdbcTemplate; ...
@PostMapping("/api/v1/fruit")
public ResponseEntity<Void> saveFruitInfo (@RequestBody FruitInfoRequest fruitInfoRequest ){
FruitInfoEntity fruitInfoEntity = fruitInfoRequest.toEntity();
String sql = "INSERT INTO fruit (name, price, stocked_date, sold_out) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, fruitInfoEntity.getName(), fruitInfoEntity.getPrice(), fruitInfoEntity.getStocked_date(), fruitInfoEntity.isSold_out());
return ResponseEntity.ok().build();
}
DTO(Request)@Getter
public class FruitInfoRequest {
private String name;
private LocalDate warehousingDate;
private long price;
public FruitInfoEntity toEntity(){ //builder를 사용해 Entity로 변환
return FruitInfoEntity.builder()
.name(name)
.stocked_date(warehousingDate)
.price(price)
.build();
}
}
Entity@Getter
@NoArgsConstructor
public class FruitInfoEntity {
private long id; //Id는 database에 생성시 auto_increment로 자동으로 증가
private String name;
private long price;
private LocalDate stocked_date;
private boolean sold_out;
@Builder
public FruitInfoEntity(String name, long price, LocalDate stocked_date) {
this.name = name;
this.price = price;
this.stocked_date = stocked_date;
this.sold_out = false; .
}
}
결과
Controller//private final JdbcTemplate jdbcTemplate; ...
@PutMapping("/api/v1/fruit")
public ResponseEntity<Void> soldFruitInfo(@RequestBody SoldFruitInfoRequest soldFruitInfoRequest){
String idCheck = "SELECT sold_out FROM fruit WHERE id=?";
List<Boolean> isNotExist = jdbcTemplate.query(idCheck, (rs, rowNum)->rs.getBoolean("sold_out"), soldFruitInfoRequest.getId());
if(isNotExist.isEmpty() || isNotExist.get(0)) throw new IllegalArgumentException("존재하지 않거나 이미 팔린 상품입니다.");
String sql = "UPDATE inflearn.fruit SET sold_out=1 WHERE id=?";
jdbcTemplate.update(sql, soldFruitInfoRequest.getId());
return ResponseEntity.ok().build();
}
DTO(Request)@Getter
public class SoldFruitInfoRequest {
private long id;
}
결과
Controller//private final JdbcTemplate jdbcTemplate; ...
@GetMapping("/api/v1/fruit/stat")
public ResponseEntity<SalesAmountResponse> salesAmount(@RequestParam String name){
String salesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 1 AND name=? GROUP BY name";
String notSalesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 0 AND name=? GROUP BY name";
List<Long> salesAmount = jdbcTemplate.query(salesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
List<Long> notsalesAmount = jdbcTemplate.query(notSalesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
if(salesAmount.isEmpty() && notsalesAmount.isEmpty()) throw new IllegalArgumentException("존재하지 않는 과일입니다.");
long sales = salesAmount.isEmpty()?0:salesAmount.get(0);
long notsales = notsalesAmount.isEmpty()?0:notsalesAmount.get(0);
return ResponseEntity.ok()
.body(new SalesAmountResponse(sales, notsales));
}
DTO(Response)public record SalesAmountResponse(long salesAmount, long notSalesAmount) {}
결과
💡 생각보다 어려웠습니다..
Controller에 기능이 집중되다보니 가독성도 떨어지고 ,
정확히 어떤 기능을 하는 API인지 파악하기 어려웠습니다.
문제 2번은 다시Controller-Service-Repository계층으로 나눠서 진행하도록 하겠습니다!
문제 1에서 코드가 분리되면
FruitController/FruitService/FruitRepository가 생겼을 것입니다.
기존에 작성했던FruitRepository를FruitMemoryRepository와FruitMySQLRepository로 나누고@Primary어노테이션을 활용해 두 Repository를 바꿔가며 동작시킬 수 있도록 코드를 변경해보세요! 😊
💡일단
FruitMemoryRepository와FruitMySQLRepository가 상속받을 수 있게
FruitRepository부터 생성하였습니다.
Interfacepublic interface FruitRepository {
void saveFruitInfo(FruitInfoEntity fruitInfoEntity);
void soldFruitInfo(SoldFruitInfoRequest soldFruitInfoRequest);
SalesAmountResponse salesAmount(String name);
}
💡
FruitMySQLRepository가 FruitRepository로 부터 상속받도록implements처리를 해 주었고,
기존의 메서드들을@Override로 처리했습니다.
FruitMySQLRepository@Repository
@RequiredArgsConstructor
public class FruitMySQLRepository implements FruitRepository{
private final JdbcTemplate jdbcTemplate;
@Override
public void saveFruitInfo(FruitInfoEntity fruitInfoEntity){
String sql = "INSERT INTO inflearn.fruit (name, price, stocked_date, sold_out) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, fruitInfoEntity.getName(), fruitInfoEntity.getPrice(), fruitInfoEntity.getStocked_date(), fruitInfoEntity.isSold_out());
}
@Override
public void soldFruitInfo(SoldFruitInfoRequest soldFruitInfoRequest){ //없는 ID값에 접근하려하면 IllegalArgumentException
String idCheck = "SELECT sold_out FROM inflearn.fruit WHERE id=?";
List<Boolean> isNotExist = jdbcTemplate.query(idCheck, (rs, rowNum)->rs.getBoolean("sold_out"), soldFruitInfoRequest.getId());
if(isNotExist.isEmpty() || isNotExist.get(0)) throw new IllegalArgumentException("존재하지 않거나 이미 팔린 상품입니다.");
String sql = "UPDATE inflearn.fruit SET sold_out=1 WHERE id=?";
jdbcTemplate.update(sql, soldFruitInfoRequest.getId());
}
@Override
public SalesAmountResponse salesAmount(String name){
String salesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 1 AND name=? GROUP BY name";
String notSalesAmountsql = "SELECT sum(price) FROM fruit WHERE sold_out = 0 AND name=? GROUP BY name";
List<Long> salesAmount = jdbcTemplate.query(salesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
List<Long> notsalesAmount = jdbcTemplate.query(notSalesAmountsql, (rs, rowNum)-> rs.getLong("sum(price)"), name);
if(salesAmount.isEmpty() && notsalesAmount.isEmpty()) throw new IllegalArgumentException("존재하지 않는 과일입니다.");
long sales = salesAmount.isEmpty()?0:salesAmount.get(0);
long notsales = notsalesAmount.isEmpty()?0:notsalesAmount.get(0);
return new SalesAmountResponse(sales, notsales);
}
}
💡 가장 재밌었던 부분입니다.
기존의MySQLRepository에서는 데이터베이스에 값이 저장될때,
auto_increment로 Id를 증가시켰기 때문에,
그 부분을 처리하기 위하여FruitInfoEntity를toMemoryEntity를 통해 변환하며
Id를 증가시키는 로직을 처리해 주었습니다.
그리고 제가 아는 제일 간단한 인메모리 데이터베이스인
List를 사용해서 데이터를 저장해 보았습니다.
FruitMemoryRepository@Repository
public class FruitMemoryRepository implements FruitRepository{
List<FruitMemoryEntity> memoryEntities = new ArrayList<>();
private long id = 1;
public FruitMemoryEntity toMemoryEntity(FruitInfoEntity fruitInfoEntity){
return new FruitMemoryEntity(this.id, fruitInfoEntity);
}
@Override
public void saveFruitInfo(FruitInfoEntity fruitInfoEntity){
memoryEntities.add(toMemoryEntity(fruitInfoEntity));
this.id++;
} //saveFruitInfo 메서드가 실행되면 fruitInfoEntity를 toMemoryEntity를 통해 변환 후 id를 1 증가시킨다.
@Override
public void soldFruitInfo(SoldFruitInfoRequest soldFruitInfoRequest){
List<FruitMemoryEntity> FruitInfoEntity = memoryEntities.stream()
.filter(fruitMemoryEntity-> fruitMemoryEntity.getId() == soldFruitInfoRequest.getId()&&!fruitMemoryEntity.isSold_out())
.toList(); //Id가 일치하고 ,Sold_out값이 false(팔리지않음)
if(FruitInfoEntity.isEmpty()) throw new IllegalArgumentException();
FruitInfoEntity.get(0).updateSold_out();
}
@Override
public SalesAmountResponse salesAmount(String name){
List<FruitMemoryEntity> FruitInfoEntity = memoryEntities.stream()
.filter(fruitMemoryEntity-> fruitMemoryEntity.getName().equals(name))
.toList();
if(FruitInfoEntity.isEmpty()) throw new IllegalArgumentException("존재하지 않는 과일입니다.");
long sales = 0;
long notsales = 0;
for(FruitMemoryEntity FruitEntity : FruitInfoEntity){
if(FruitEntity.isSold_out()) sales += FruitEntity.getPrice();
else notsales += FruitEntity.getPrice();
}
return new SalesAmountResponse(sales, notsales);
}
}
FruitMemoryEntity@Getter
@NoArgsConstructor
public class FruitMemoryEntity {
private long id;
private String name;
private long price;
private LocalDate stocked_date;
private boolean sold_out;
public FruitMemoryEntity(long id, FruitInfoEntity fruitInfoEntity){
this.id = id;
this.name = fruitInfoEntity.getName();
this.price = fruitInfoEntity.getPrice();
this.stocked_date = fruitInfoEntity.getStocked_date();
this.sold_out = fruitInfoEntity.isSold_out();
}
public void updateSold_out(){ //sold_out 처리해주는 메서드
this.sold_out = true;
}
}
💡
@Primary어노테이션으로 Repository 우선권을 설정했습니다.
FruitMySQLRepository




FruitMemoryRepository




제어의 역전과 의존성 주입에 대해 그냥 암기만 하고 있었는데, 그것을 이용할 줄 알면
정말 마법같은 일을 할 수 있구나 하고 느끼게 되었다.
또한, 계층간 분리의 필요성을 다시 한번 느끼게 된 하루였다.