[인프런 워밍업 클럽 0기] BE 6일차 과제

김영훈·2024년 2월 25일
post-thumbnail

진도표 6일차와 연결됩니다.

우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다.
앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다. 🙂
과제#4 에서 만들었던 API를 강의 내용처럼 Controller - Service - Repository로 분리해보세요!


문제 1.

과제#4 에서 만들었던 API를 강의 내용처럼 Controller - Service - Repository로 분리해보세요!

💡 문제점

  • Day 4 때 이미 분리해서 만들어버렸다 ..


    그래서 반대로 합쳐서 만들어서 다시 한번 계층 간 분리의 필요성을 느껴보자고 마음먹었습니다.

Day 4 | 문제 1.

  • 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; .
    }
}
  • 결과


Day 4 | 문제 2.

  • 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;
}
  • 결과


Day 4 | 문제 3.

  • 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 계층으로 나눠서 진행하도록 하겠습니다!



문제 2.

문제 1에서 코드가 분리되면 FruitController / FruitService / FruitRepository 가 생겼을 것입니다.
기존에 작성했던 FruitRepositoryFruitMemoryRepositoryFruitMySQLRepository로 나누고 @Primary 어노테이션을 활용해 두 Repository를 바꿔가며 동작시킬 수 있도록 코드를 변경해보세요! 😊


해결과정

💡일단 FruitMemoryRepositoryFruitMySQLRepository 가 상속받을 수 있게
FruitRepository부터 생성하였습니다.

  • Interface

public 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를 증가시켰기 때문에,
그 부분을 처리하기 위하여 FruitInfoEntitytoMemoryEntity를 통해 변환하며
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

    리스트 조회를 해보면 잘 저장되는것을 확인할 수 있다.

후기

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


0개의 댓글