[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - API 레이어 분리 테스트 (Day6)

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - API 레이어 분리 테스트 (Day6)

과제

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

우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂

과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍

image


문제 1

과제4에서 만들었던 API를 Controller - Service - Repository로 분리하라고 하셨다.

하지만 이전에 과제4를 진행하면서 나는 이미 레이어를 분리했지만 강의에 대한 복습 겸, 다시 진행해보기로 했다.

step0. DB 생성 및 테이블 생성

먼저 데이터베이스부터 다시 만들기로 하였다. 아래와 같이 쿼리를 작성하여 데이터베이스를 생성한다.

create database fruit;

다음으로 내가 생성한 fruit 데이터베이스에 접속한다.

use fruit;

그리고 테이블 목록을 조회해본다. 당연히 비어 있을 것이다.

show tables;

그러면 아래와 같이 테이블 목록들이 비어있는 것을 확인할 수 있을 것이다.

image

그러면 이제 아래와 같이 쿼리를 작성해서 테이블을 만들어보자. 테이블 컬럼들은 기존과 동일하게 적용한다.

CREATE TABLE fruit
(
    id              bigint auto_increment,
    name            varchar(20) not null,
    warehousingDate date        not null,
    price           bigint      not null,
    is_sold         boolean     not null default false,
    primary key (id)
);

그리고 테이블이 잘 생성 되었는지 조회를 해서 확인해본다.

show tables;

image


step1. DB 설정 정보 적용

이제 DB 연결정보를 Spring Boot 프로젝트와 연결해보자.

프로젝트의 src/main/resources의 경로에 있는 application.properties를 application.yml로 변경하고 설정정보를 아래와 같이 작성한다.

spring:
  datasource:
    url: "jdbc:mysql://localhost/fruit"
    username: "root"
    password: ""
    driver-class-name: com.mysql.cj.jdbc.Driver

주의

username과 password는 본인에 따라 달리 작성한다.

또한 굳이 application.yml 로 확장자 변경을 안하고 properties 확장자로 이용해도 무관하다.


step2. 기존 컨트롤러 클래스 파일 가져오기

나는 이미 과제4에서 레이어를 분리해두었다. 하지만 이번 과제의 취지에 맞게 기존에 파일들을 가져오기는 하지만 비즈니스 로직들을 컨트롤럴 클래스에 포함된 파일들로 가져오기로 하였다.

Fruit.java

package me.sungbin.entity.fruit;

import java.time.LocalDate;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : Fruit
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public class Fruit {

    private long id;

    private String name;

    private LocalDate warehousingDate;

    private long price;

    private boolean isSold;

    public Fruit() {
    }

    public Fruit(String name, LocalDate warehousingDate, long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }

    public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
        this.isSold = isSold;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }

    public boolean isSold() {
        return isSold;
    }
}

SaveFruitInfoRequestInfo.java

package me.sungbin.dto.fruit.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

/**
 * @author : rovert
 * @packageName : me.sungbin.dto.fruit.request
 * @fileName : SaveFruitInfoRequestDto
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public class SaveFruitInfoRequestDto {

    @NotBlank(message = "과일 이름이 공란일 수 없습니다.")
    @NotNull(message = "과일 이름이 null일 수는 없습니다.")
    private String name;

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate warehousingDate;


    @Min(value = 0, message = "가격이 음수가 될 수는 없습니다.")
    private long price;

    public SaveFruitInfoRequestDto(String name, LocalDate warehousingDate, long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }

    public Fruit toEntity() {
        return new Fruit(name, warehousingDate, price);
    }
}

UpdateFruitRequestDto.java

package me.sungbin.dto.fruit.request;

/**
 * @author : rovert
 * @packageName : me.sungbin.dto.fruit.request
 * @fileName : UpdateFruitRequestDto
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public class UpdateFruitRequestDto {

    private long id;

    public UpdateFruitRequestDto() {
    }

    public UpdateFruitRequestDto(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }
}

GetFruitResponseDto.java

package me.sungbin.dto.fruit.response;

/**
 * @author : rovert
 * @packageName : me.sungbin.dto.fruit.response
 * @fileName : GetFruitResponseDto
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

public class GetFruitResponseDto {
    private long salesAmount;

    private long notSalesAmount;

    public GetFruitResponseDto(long salesAmount, long notSalesAmount) {
        this.salesAmount = salesAmount;
        this.notSalesAmount = notSalesAmount;
    }

    public long getSalesAmount() {
        return salesAmount;
    }

    public long getNotSalesAmount() {
        return notSalesAmount;
    }
}

FruitController.java

package me.sungbin.controller.fruit;

import jakarta.validation.Valid;
import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : FruitController
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitController {

    private final JdbcTemplate jdbcTemplate;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping
    public void saveFruitInfo(@RequestBody @Valid Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    @PutMapping
    public void updateFruitInfo(@RequestBody Fruit fruit) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty();

        if (isNotExistsFruitInfo) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }

        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, fruit.getId());
    }

    @GetMapping("/stat")
    public GetFruitResponseDto getFruitInfo(@RequestParam String name) {
        long salesAmount = 0;
        long notSalesAmount = 0;

        String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        if (aggregatedData.containsKey(true)) {
            salesAmount = aggregatedData.get(true);
        }
        if (aggregatedData.containsKey(false)) {
            notSalesAmount = aggregatedData.get(false);
        }

        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }
}

step3. 레이어 분리

이제 레이어를 분리해보겠다. 일단 현재 컨트롤러에는 HTTP 통신하는 부분과 DB처리 관련 로직, 예외로직이 엄청 많다. 이것은 클린코드의 단일책임원칙에 위배가 되므로 서비스 레이어를 만들어 분리해보도록 하자.

FruitService.java

package me.sungbin.service.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @author : rovert
 * @packageName : me.sungbin.service.fruit
 * @fileName : FruitService
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

public class FruitService {

    private final JdbcTemplate jdbcTemplate;

    public FruitService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveFruitInfo(Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void updateFruitInfo(Fruit fruit) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty();

        if (isNotExistsFruitInfo) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }

        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, fruit.getId());
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        long salesAmount = 0;
        long notSalesAmount = 0;

        String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        if (aggregatedData.containsKey(true)) {
            salesAmount = aggregatedData.get(true);
        }
        if (aggregatedData.containsKey(false)) {
            notSalesAmount = aggregatedData.get(false);
        }

        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }
}

FruitController.java

package me.sungbin.controller.fruit;

import jakarta.validation.Valid;
import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.service.fruit.FruitService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : FruitController
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitController {

    private final FruitService fruitService;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.fruitService = new FruitService(jdbcTemplate);
    }


    @PostMapping
    public void saveFruitInfo(@RequestBody @Valid Fruit fruit) {
        this.fruitService.saveFruitInfo(fruit);
    }

    @PutMapping
    public void updateFruitInfo(@RequestBody Fruit fruit) {
        this.fruitService.updateFruitInfo(fruit);
    }

    @GetMapping("/stat")
    public GetFruitResponseDto getFruitInfo(@RequestParam String name) {
        return this.fruitService.getFruitInfo(name);
    }

}

좀 더 컨트롤러 클래스가 깔끔해진 것을 볼 수 있다. 하지만 서비스 클래스에 DB 관련 처리과 더해 예외로직들이 있는 것은 클린코드에 위배되는 것 같다. 따라서 FruitService 코드도 레파지토리 레이어를 만들어서 분리해보도록 하자. 그리고 각각 리팩토링 작업도 거쳤다. 아래의 코드를 보자.

 

FruitService.java

package me.sungbin.service.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.repository.fruit.FruitRepository;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * @author : rovert
 * @packageName : me.sungbin.service.fruit
 * @fileName : FruitService
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(JdbcTemplate jdbcTemplate) {
        this.fruitRepository = new FruitRepository(jdbcTemplate);
    }

    public void saveFruitInfo(Fruit fruit) {
        this.fruitRepository.saveFruitInfo(fruit);
    }

    public void updateFruitInfo(Fruit fruit) {
        validate(fruit.getId());

        this.fruitRepository.updateFruitInfo(fruit);
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name);

        validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount());

        return fruitData;
    }

    private void validate(long id) {
        if (this.fruitRepository.isNotExistsFruitInfo(id)) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }
    }

    private void validateGetFruitAmount(long salesAmount, long notSalesAmount) {
        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }
    }
}

FruitRepository.java

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public class FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveFruitInfo(Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void updateFruitInfo(Fruit fruit) {
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, fruit.getId());
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

하지만 뭔가 이상한 점을 발견할 수 있다. 현재 DB를 이용하는 것은 레파지토리 레이어이다. 즉, JdbcTemplate을 이용하는 것은 레파지토리 레이어뿐인 것이다. 하지만 코드를 보면 알 수 있듯이 컨트롤러, 서비스 레이어에도 전부 JdbcTemplate을 매개변수로 넣고 있다. 이런 것을 어떻게 해결할까? 바로 서비스와 레파지토리 레이어에 빈을 주입할 수 있는 어노테이션을 붙여준다. 이 부분은 오늘 강의시간에도 다뤘으니 적용해보자.

 

FruitController.java

package me.sungbin.controller.fruit;

import jakarta.validation.Valid;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.service.fruit.FruitService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : FruitController
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitController {

    private final FruitService fruitService;

    public FruitController(FruitService fruitService) {
        this.fruitService = fruitService;
    }

    @PostMapping
    public void saveFruitInfo(@RequestBody @Valid Fruit fruit) {
        this.fruitService.saveFruitInfo(fruit);
    }

    @PutMapping
    public void updateFruitInfo(@RequestBody Fruit fruit) {
        this.fruitService.updateFruitInfo(fruit);
    }

    @GetMapping("/stat")
    public GetFruitResponseDto getFruitInfo(@RequestParam String name) {
        return this.fruitService.getFruitInfo(name);
    }

}

FruitService.java

package me.sungbin.service.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.repository.fruit.FruitRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * @author : rovert
 * @packageName : me.sungbin.service.fruit
 * @fileName : FruitService
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void saveFruitInfo(Fruit fruit) {
        this.fruitRepository.saveFruitInfo(fruit);
    }

    public void updateFruitInfo(Fruit fruit) {
        validate(fruit.getId());

        this.fruitRepository.updateFruitInfo(fruit);
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name);

        validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount());

        return fruitData;
    }

    private void validate(long id) {
        if (this.fruitRepository.isNotExistsFruitInfo(id)) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }
    }

    private void validateGetFruitAmount(long salesAmount, long notSalesAmount) {
        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }
    }
}

 

FruitRepository.java

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Repository
public class FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveFruitInfo(Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void updateFruitInfo(Fruit fruit) {
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, fruit.getId());
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

각각 레파지토리 레이어와 서비스 레이어에 @Repository, @Service 어노테이션을 붙여주었고 이 어노테이션들은 @Component 어노테이션들을 붙어 있어서 빈을 주입 받을 수 있다. 그래서 각각 생성자 주입 방식으로 주입을 받았다.


step4. 엔티티 대신에 DTO로!

검색을 해보면 request나 response로 받아주는 것을 DTO로 받는게 좋다고 했다. 그이유는 아래와 같다.

 

📚 엔티티 대신에 DTO를 사용하는 이유?
DTO(Data Transfer Object)를 엔티티 대신 사용하는 이유는 여러 가지가 있다. 첫째, DTO를 사용하면 애플리케이션의 프레젠테이션 계층과 데이터 접근 계층 사이의 의존성을 줄일 수 있어, 애플리케이션의 확장성과 유지보수성이 향상된다. 각 계층이 서로에 대해 덜 알고 있기 때문에, 변경 사항이 한 계층에만 국한되어 다른 계층에는 영향을 주지 않는 경우가 많다.

둘째, DTO를 사용하면 클라이언트에 전송되는 데이터의 양과 형식을 조정할 수 있어, 네트워크를 통한 데이터 전송량을 최적화하고, 클라이언트가 필요로 하는 데이터 형식을 맞춤 제공할 수 있다. 이는 특히 모바일 애플리케이션 개발이나 대역폭이 제한된 환경에서 중요하다.

셋째, DTO를 사용하면 엔티티의 모든 정보를 클라이언트에 노출하지 않아도 된다. 이는 보안 측면에서 매우 중요한데, 예를 들어 사용자 엔티티에는 비밀번호와 같은 민감한 정보가 포함될 수 있으나, 이를 DTO를 통해 필터링하고 클라이언트에 필요한 정보만 전달할 수 있다.

넷째, 엔티티의 경우 JPA와 같은 ORM 기술을 사용할 때 지연 로딩(Lazy Loading) 등의 문제로 인해 직렬화에 어려움이 있을 수 있습니다. DTO를 사용하면 이러한 문제를 피하고, 데이터 전송을 위해 최적화된 객체를 생성할 수 있습니다.

이러한 이유로 한번 DTO로 변경해보자. 현재 DTO는 과제4에서 사용했던 DTO를 이용할 것이다. 그리고 이 DTO의 코드내용은 step2에서 보여줬으므로 이것을 이용해보자.

FruitController.java

package me.sungbin.controller.fruit;

import jakarta.validation.Valid;
import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.request.UpdateFruitRequestDto;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.service.fruit.FruitService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : FruitController
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitController {

    private final FruitService fruitService;

    public FruitController(FruitService fruitService) {
        this.fruitService = fruitService;
    }

    @PostMapping
    public void saveFruitInfo(@RequestBody @Valid SaveFruitInfoRequestDto requestDto) {
        this.fruitService.saveFruitInfo(requestDto);
    }

    @PutMapping
    public void updateFruitInfo(@RequestBody UpdateFruitRequestDto requestDto) {
        this.fruitService.updateFruitInfo(requestDto);
    }

    @GetMapping("/stat")
    public GetFruitResponseDto getFruitInfo(@RequestParam String name) {
        return this.fruitService.getFruitInfo(name);
    }

}

FruitService.java

package me.sungbin.service.fruit;

import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.request.UpdateFruitRequestDto;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.repository.fruit.FruitRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * @author : rovert
 * @packageName : me.sungbin.service.fruit
 * @fileName : FruitService
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) {
        Fruit fruit = requestDto.toEntity();
        this.fruitRepository.saveFruitInfo(fruit);
    }

    public void updateFruitInfo(UpdateFruitRequestDto requestDto) {
        validate(requestDto.getId());

        this.fruitRepository.updateFruitInfo(requestDto.getId());
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name);

        validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount());

        return fruitData;
    }

    private void validate(long id) {
        if (this.fruitRepository.isNotExistsFruitInfo(id)) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }
    }

    private void validateGetFruitAmount(long salesAmount, long notSalesAmount) {
        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }
    }
}

FruitRepository.java

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Repository
public class FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveFruitInfo(Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void updateFruitInfo(long id) {
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

step5. postman 테스트

이제 이렇게 리팩토링한 것을 postman을 이용해서 테스트해보자.

현재 fruit 테이블은 아래와 같이 비어있다.

image

과일 생성

image

image

수정

위의 생성 테스트가 잘 되었으니, 몇개의 데이터를 아래와 같이 만들었다.

image

이제 2000원짜리 오렌지가 팔린 테스트를 해보겠다.

image

image

조회 테스트

이제 조회 테스트를 해보자. 오렌지가 팔린 금액과 안 팔린 금액을 조회해보자.

 image

step6. 테스트 코드

이제 테스트 코드로 다시 한번 검증해보자.

 

  • FruitControllerTest.java

package me.sungbin.controller.fruit;

import com.fasterxml.jackson.databind.ObjectMapper;
import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.request.UpdateFruitRequestDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDate;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : FruitControllerTest
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@SpringBootTest
@AutoConfigureMockMvc
class FruitControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    @DisplayName("문제 1번 통합 테스트 - 실패 (가격이 음수거나 과일 이름이 공란)")
    void question1_test_fail_caused_by_price_is_minus_or_fruit_name_is_empty() throws Exception {
        SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("", LocalDate.of(2024, 1, 1), -1000);

        this.mockMvc.perform(post("/api/v1/fruit")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(requestDto)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("문제 1번 통합 테스트 - 성공")
    void question1_test_success() throws Exception {
        SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("파인애플", LocalDate.of(2024, 2, 2), 20000);

        this.mockMvc.perform(post("/api/v1/fruit")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(requestDto)))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("문제 2번 통합 테스트 - 성공")
    void question2_test_success() throws Exception {
        UpdateFruitRequestDto requestDto = new UpdateFruitRequestDto(1);

        this.mockMvc.perform(put("/api/v1/fruit")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(requestDto)))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("문제 3번 통합 테스트 - 성공")
    void question3_test_success() throws Exception {
        this.mockMvc.perform(get("/api/v1/fruit/stat")
                        .param("name", "사과"))
                .andDo(print())
                .andExpect(status().isOk());
    }
}

image


문제2

문제2는 FruitRepository를 FruitMemoryRepository 와 FruitMysqlRepository로 나누고 @Primary 어노테이션을 이용하여 두 Repository를 번갈아가며 동작시키는 것을 구현하시라고 하셨다.

 

step1. FruitRepository 코드를 FruitMysqlRepository로 변경

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Repository
public class FruitMysqlRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitMysqlRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveFruitInfo(Fruit fruit) {
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void updateFruitInfo(long id) {
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

step2. FruitRepository 인터페이스 생성

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public interface FruitRepository {

    void saveFruitInfo(Fruit fruit); // 과일 생성

    void updateFruitInfo(long id); // 과일 정보 업데이트

    GetFruitResponseDto getFruitInfo(String name); // 과일 조회

    boolean isNotExistsFruitInfo(long id);
}

step3. FruitMemoryRepository 생성 및 로직 개발

step3-1. Fruit 클래스에 다중 생성자 추가(메모리 용 때문에)

package me.sungbin.entity.fruit;

import java.time.LocalDate;

/**
 * @author : rovert
 * @packageName : me.sungbin.controller.fruit
 * @fileName : Fruit
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */
public class Fruit {

    private long id;

    private String name;

    private LocalDate warehousingDate;

    private long price;

    private boolean isSold;

    public Fruit() {
    }

    public Fruit(String name, LocalDate warehousingDate, long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }

    public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
        this.isSold = isSold;
    }

    public Fruit(long id, String name, LocalDate warehousingDate, long price, boolean isSold) {
        this.id = id;
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
        this.isSold = isSold;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }

    public boolean isSold() {
        return isSold;
    }
}

step3-2. FruitMemoryRepository 생성 및 로직 추가

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitMemoryRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Primary
@Repository
public class FruitMemoryRepository implements FruitRepository {

    private final List<Fruit> fruits = new ArrayList<>();

    private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());

    @Override
    public void saveFruitInfo(Fruit fruit) {
        log.info("[FruitMemoryRepository] - saveFruitInfo");
        fruits.add(fruit);

        System.out.println(fruits);
    }

    @Override
    public void updateFruitInfo(long id) {
        log.info("[FruitMemoryRepository] - updateFruitInfo");
        for (int i = 0; i < fruits.size(); i++) {
            Fruit fruit = fruits.get(i);
            if (fruit.getId() == id) {
                // Assuming Fruit class has an appropriate constructor to handle this case
                Fruit updatedFruit = new Fruit(fruit.getId(), fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice(), true);
                fruits.set(i, updatedFruit);
                break;
            }
        }

        System.out.println(fruits);
    }

    @Override
    public GetFruitResponseDto getFruitInfo(String name) {
        log.info("[FruitMemoryRepository] - getFruitInfo");
        List<Fruit> filteredFruits = fruits.stream()
                .filter(fruit -> fruit.getName().equals(name))
                .toList();

        long salesAmount = filteredFruits.stream()
                .filter(Fruit::isSold)
                .mapToLong(Fruit::getPrice)
                .sum();

        long notSalesAmount = filteredFruits.stream()
                .filter(fruit -> !fruit.isSold())
                .mapToLong(Fruit::getPrice)
                .sum();

        System.out.println(fruits);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    @Override
    public boolean isNotExistsFruitInfo(long id) {
        return fruits.stream().noneMatch(fruit -> fruit.getId() == id);
    }

}

step4. FruitMysqlRepository 수정

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Repository
public class FruitMysqlRepository implements FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());

    public FruitMysqlRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void saveFruitInfo(Fruit fruit) {
        log.info("[FruitMysqlRepository] - saveFruitInfo");
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    @Override
    public void updateFruitInfo(long id) {
        log.info("[FruitMysqlRepository] - updateFruitInfo");
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    @Override
    public GetFruitResponseDto getFruitInfo(String name) {
        log.info("[FruitMysqlRepository] - getFruitInfo");
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    @Override
    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

step5. FruitService 수정

package me.sungbin.service.fruit;

import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto;
import me.sungbin.dto.fruit.request.UpdateFruitRequestDto;
import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import me.sungbin.repository.fruit.FruitMysqlRepository;
import me.sungbin.repository.fruit.FruitRepository;
import org.springframework.stereotype.Service;

/**
 * @author : rovert
 * @packageName : me.sungbin.service.fruit
 * @fileName : FruitService
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) {
        Fruit fruit = requestDto.toEntity();
        this.fruitRepository.saveFruitInfo(fruit);
    }

    public void updateFruitInfo(UpdateFruitRequestDto requestDto) {
        validate(requestDto.getId());

        this.fruitRepository.updateFruitInfo(requestDto.getId());
    }

    public GetFruitResponseDto getFruitInfo(String name) {
        GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name);

        validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount());

        return fruitData;
    }

    private void validate(long id) {
        if (this.fruitRepository.isNotExistsFruitInfo(id)) {
            throw new IllegalArgumentException("존재하는 과일정보가 없습니다.");
        }
    }

    private void validateGetFruitAmount(long salesAmount, long notSalesAmount) {
        if (salesAmount == 0L && notSalesAmount == 0L) {
            throw new IllegalArgumentException("존재하는 과일이 없습니다.");
        }
    }
}

step6. postman 테스트

현재 @Primary 어노테이션을 FruitMemoryRepository로 붙여두고 테스트를 해보았다.

생성 (메모리)

image

 

image

수정 (메모리)

image

image

조회 (메모리)

image

image

이제 FruitMysqlRepository로 이용해보자! FruitMemoryRepository의 @Primary 어노테이션을 지워주고 FruitMysqlRepository에 붙여주자!

package me.sungbin.repository.fruit;

import me.sungbin.dto.fruit.response.GetFruitResponseDto;
import me.sungbin.entity.fruit.Fruit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : rovert
 * @packageName : me.sungbin.repository.fruit
 * @fileName : FruitRepository
 * @date : 2/25/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/25/24       rovert         최초 생성
 */

@Primary
@Repository
public class FruitMysqlRepository implements FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());

    public FruitMysqlRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void saveFruitInfo(Fruit fruit) {
        log.info("[FruitMysqlRepository] - saveFruitInfo");
        String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)";

        this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    @Override
    public void updateFruitInfo(long id) {
        log.info("[FruitMysqlRepository] - updateFruitInfo");
        String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    @Override
    public GetFruitResponseDto getFruitInfo(String name) {
        log.info("[FruitMysqlRepository] - getFruitInfo");
        String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold";

        Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> {
            HashMap<Boolean, Long> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getBoolean("is_sold"), rs.getLong("total"));
            }
            return map;
        });

        long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L);
        long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L);

        return new GetFruitResponseDto(salesAmount, notSalesAmount);
    }

    @Override
    public boolean isNotExistsFruitInfo(long id) {
        String readSQL = "SELECT * FROM fruit WHERE id = ?";

        return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty();
    }
}

 

생성 (Mysql)

image

image

image

수정 (MySQL)

image

image

image

조회 (Mysql)

image

image


회고

오늘의 강의 핵심은 의존성 주입제어의 역전이었다. 나는 기존에 이런 개념들이 뭔지는 대강 알고는 있었지만 확실히 강의와 이렇게 실습함으로 뭔가 체득이 되었다. 아직 많이 부족한 부분이 있을테니 나 따로 더 연습을 해봐야겠다.

 

댓글을 작성해보세요.