BE 6일차 과제
인프런 워밍업 클럽 1기 BE 6일차 과제를 구현해보겠습니다.
기존에 작성했던 Controller 코드를 레이어별로 3단 분리를 해보겠습니다.
기존 코드
@RestController
public class FruitController {
private final JdbcTemplate jdbcTemplate;
public FruitController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostMapping("/api/v1/fruit")
public void saveFruit(@RequestBody FruitCreateRequest request) {
String sql = "INSERT INTO fruit (name, warehousing_date, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
@PutMapping("/api/v1/fruit")
public void updateFruit(@RequestBody FruitSellingRequest request) {
String readSql = "SELECT * FROM fruit WHERE id = ?";
boolean isNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isNotExist) {
throw new IllegalArgumentException();
}
String sql = "UPDATE fruit SET is_sold = true WHERE id = ?";
jdbcTemplate.update(sql, request.getId());
}
@GetMapping("/api/v1/fruit/stat")
public FruitStatResponse statFruit(@RequestParam String name) {
String saleSql = "SELECT SUM(price) FROM fruit"
+ " WHERE name = ? and is_sold = true"
+ " GROUP BY name ";
Long salesAmount = jdbcTemplate.queryForObject(saleSql, Long.class, name);
String notSaleSql = "SELECT * FROM fruit WHERE name = ? and is_sold = false";
List<Long> notSalesList = jdbcTemplate.query(
notSaleSql, (rs, rowNum) -> rs.getLong("price"), name
);
long notSalesAmount = notSalesList.stream()
.mapToLong(Long::longValue)
.sum();
return new FruitStatResponse(salesAmount, notSalesAmount);
}
}
기존에 작성했던 코드를 보면 하나의 클래스 안에서 HTTP 요청/응답, 데이터베이스 관련 작업을 진행하고 있습니다.
문제 1
해당 클래스를 Controller / Service / Repository 로 3단 분리를 해보겠습니다.
Repository 클래스
- jdbcTemplate를 활용하여 SQL문을 직접 작성하여 Database 와 직접적인 연결을 진행합니다.
- @Repository 어노테이션을 활용하여 생성자를 통한 의존성 주입이 되는 것을 알 수 있습니다.
@Repository
public class FruitRepository {
private final JdbcTemplate jdbcTemplate;
public FruitRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void saveFruit(FruitRequest request) {
String sql = "INSERT INTO fruit (name, warehousing_date, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
public void updateFruit(FruitRequest request) {
String readSql = "SELECT * FROM fruit WHERE id = ?";
boolean isFruitNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isFruitNotExist) {
throw new IllegalStateException();
}
String sql = "UPDATE fruit SET is_sold = true WHERE id = ?";
jdbcTemplate.update(sql, request.getId());
}
public FruitStatResponse statFruit(String name) {
String readSql = "SELECT * FROM fruit WHERE name = ?";
boolean isFruitNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty();
if (isFruitNotExist) {
throw new IllegalStateException();
}
String saleSql = "SELECT SUM(price) FROM fruit"
+ " WHERE name = ? and is_sold = true"
+ " GROUP BY name ";
String notSalesSql = "SELECT SUM(price) FROM fruit"
+ " WHERE name = ? and is_sold = false"
+ " GROUP BY name ";
long salesAmount = jdbcTemplate.queryForObject(saleSql, Long.class, name);
long notSalesAmount = jdbcTemplate.queryForObject(notSalesSql, Long.class, name);
return new FruitStatResponse(salesAmount, notSalesAmount);
}
}
Service 클래스
- 비즈니스 로직을 처리하는 역할을 진행한다.
- 간단한 로직이기 때문에 HTTP 코드만 전달하도록 void 메서드로 진행되어 있는 게 대부분이지만 DTO를 활용해서 클라이언트에서 전달하고 싶은 데이터를 전달할 수도 있다.
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
public void saveFruit(FruitRequest request) {
fruitRepository.saveFruit(request);
}
public void updateFruit(FruitRequest request) {
fruitRepository.updateFruit(request);
}
public FruitStatResponse statFruit(String name) {
return fruitRepository.statFruit(name);
}
}
Controller 클래스
- HTTP의 요청과 응답에 대한 데이터를 송/수신 할 수 있는 역할만 진행한다.
@RestController
public class FruitController {
private final FruitService fruitService;
public FruitController(FruitService fruitService) {
this.fruitService = fruitService;
}
@PostMapping("/api/v1/fruit")
public void saveFruit(@RequestBody FruitRequest request) {
fruitService.saveFruit(request);
}
@PutMapping("/api/v1/fruit")
public void updateFruit(@RequestBody FruitRequest request) {
fruitService.updateFruit(request);
}
@GetMapping("/api/v1/fruit/stat")
public FruitStatResponse statFruit(@RequestParam String name) {
return fruitService.statFruit(name);
}
}
문제 2
FruitMemoryRepository
@Repository
public class FruitMemoryRepository implements FruitRepository {
private Map<String, FruitRequest> fruitDatabase = new HashMap<>();
@Override
public void saveFruit(FruitRequest request) {
fruitDatabase.put(request.getName(), request);
}
@Override
public void updateFruit(FruitRequest request) {
if (fruitDatabase.containsKey(request.getName())) {
fruitDatabase.put(request.getName(), request);
} else {
throw new IllegalArgumentException("해당 과일을 찾을 수 없습니다. : " + request.getName());
}
}
@Override
public FruitStatResponse statResponse(String name) {
if (fruitDatabase.containsKey(name)) {
FruitRequest fruit = fruitDatabase.get(name);
long salesAmount = fruit.isSold() ? 1 : 0;
long notSalesAmount = fruit.isSold() ? 0 : 1;
return new FruitStatResponse(salesAmount, notSalesAmount);
} else {
throw new IllegalArgumentException("해당 과일을 찾을 수 없습니다.");
}
}
}
FruitMySqlRepository
@Repository
public class FruitMySqlRepository implements FruitRepository {
private final JdbcTemplate jdbcTemplate;
public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void saveFruit(FruitRequest request) {
String sql = "INSERT INTO fruit (name, warehousing_date, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
@Override
public void updateFruit(FruitRequest request) {
String updateSql = "UPDATE fruit SET warehousing_date = ?, price = ? WHERE name = ?";
jdbcTemplate.update(updateSql, request.getWarehousingDate(), request.getPrice(), request.getName());
}
@Override
public FruitStatResponse statResponse(String name) {
String selectSql = "SELECT is_sold FROM fruit WHERE name = ?";
List<Boolean> booleans = jdbcTemplate.queryForList(selectSql, boolean.class, name);
if (!booleans.isEmpty()) {
long salesAmount = booleans.stream().filter(n -> n).count();
long notSalesAmount = booleans.stream().filter(n -> !n).count();
return new FruitStatResponse(salesAmount, notSalesAmount);
} else {
throw new IllegalArgumentException("해당 과일을 찾을 수 없습니다.");
}
}
}
위의 코드처럼 FruitMySqlRepository, FruitMemoryRepository 클래스를 만들게 되면 프로젝트에선 어떤 클래스를 사용해야하는지 정해야합니다.
그럴 때는 @Primary 어노테이션을 활용해서 프로젝트가 실행되었을 시에 어떤 레포지토리의 레이어를 사용할 지 정하게 됩니다.
아래의 코드를 보면 FruitMySqlRepository 에는 @Primary 어노테이션이 붙은 것을 확인할 수 있습니다.
그러면 Service 로직에서 레포지토리를 사용할 때 FruitMySqlRepository 를 우선적으로 사용하게 됩니다.
@Primary //우선권을 결정
@Repository
public class FruitMySqlRepository implements FruitRepository {
}
@Repository
public class FruitMemoryRepository implements FruitRepository {
}
@Primary 어노테이션이 아닌 @Qualifier 어노테이션을 사용할 수도 있습니다.
@Qualifier 어노테이션은 어떤 클래스를 사용할 건지 선택해줍니다.
//서비스 로직
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService(@Qualifier("main") FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
}
//레포지토리 로직
@Repository
@Qualifier("main")
public class FruitMemoryRepository implements FruitRepository {
}
위의 예시 코드를 보면 @Qualifier로 Service 로직에서 "main" 이라는 문자열의 Repository 로직을 주입받은 것을 확인 할 수 있습니다.
Repository 로직에서 동일하게 @Qualifier("main")가 적혀져 있는 코드를 찾아서 해당 코드로 작동하게 됩니다.
만약 @Primary 와 @Qualifier 어노테이션을 동시에 사용한다면
사용하는 쪽이 먼저 적어준 @Qualifier가 먼저 실행된다.
Reference
자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다.
'About Me > 인프런 워밍업 클럽' 카테고리의 다른 글
[인프런 워밍업 클럽 1기] BE 7일차 과제 (0) | 2024.05.15 |
---|---|
[인프런 워밍업 클럽 1기] BE 5일차 과제 (0) | 2024.05.09 |
[인프런 워밍업 클럽 1기] BE 4일차 과제 (0) | 2024.05.07 |
댓글