공부/인프런 워밍업 클럽_BE

[인프런 워밍업 클럽_0기] BE 여섯 번째 과제 (진도표 6일차)

데부한 2024. 2. 26. 02:38
반응형

배너를 클릭하면 해당 사이트로 이동

 

강의 출처

 

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인

Java와 Spring Boot, JPA, MySQL, AWS를 이용해 서버를 개발하고 배포합니다. 웹 애플리케이션을 개발하며 서버 개발에 필요한 배경지식과 이론, 다양한 기술들을 모두 학습할 뿐 아니라, 다양한 옵션들

www.inflearn.com

 

진도표 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() 포스트맨 확인

 

이번 과제는 강의에서 많이 다룬 내용이 대부분이라 많이 어렵진 않았다..! 다만 시간이 부족할 뿐..!

내일 출근 누가 해...

 

반응형