4일차에 만들었던 API를 Controller - Service - Repository로 나누어 보았다.
4일차 과제 보기 👉 https://ddonydev.tistory.com/74
- Controller
@RestController
public class FruitController {
private final FruitService fruitService;
public FruitController(FruitService fruitService) {
this.fruitService = fruitService;
}
@GetMapping("/api/v1/fruit/stat")
public FruitResponse selectFruits(@RequestParam String name) {
return fruitService.selectFruits(name);
}
@PostMapping("/api/v1/fruit")
public void insertFruits(@RequestBody FruitRequest fruitRequest) {
fruitService.insertFruits(fruitRequest);
}
@PutMapping("/api/v1/fruit")
public void updateFruits(@RequestBody FruitUpdateRequest request) {
fruitService.updateFruits(request);
}
}
- Service
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
public FruitResponse selectFruits(String name) {
return fruitRepository.selectFruits(name);
}
public void insertFruits(FruitRequest fruitRequest){
fruitRepository.insertFruits(fruitRequest.getName(), fruitRequest.getWarehousingDate(), fruitRequest.getPrice());
}
public void updateFruits(FruitUpdateRequest request) {
boolean isExist = fruitRepository.isExist(request.getId());
if (isExist) {
throw new RuntimeException("존재 하지 않는 과일입니다.");
}
fruitRepository.updateFruits(request.getId());
}
}
- Repository
@Repository
public class FruitRepository {
private final JdbcTemplate jdbcTemplate;
public FruitRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public FruitResponse selectFruits(String name) {
String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";
long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);
return new FruitResponse(salesAmount, notSalesAmount);
}
public void insertFruits(String name, LocalDate warehousingDate, long price) {
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
public boolean isExist(long id) {
String readSql = "SELECT * FROM fruits WHERE ID = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
public void updateFruits(long id) {
String updateSql = "UPDATE fruits SET isSold = true where id = ?";
jdbcTemplate.update(updateSql, id);
}
}
전 코드에서는 Controller에서 SQL 처리까지 다 해주었는데 Service에서는 isExist와 같이 "존재하는지 판별하기" 등 주요 비즈니스 처리를 해주었고, Repository에서는 DB에 접근 할 수 있도록 역할을 나누었다. 그리고 이전 코드에서는 Controller에서 JDBCTemplate를 주입받아 사용하였는데 이번 코드에서는 Repository에서 JDBCTemaplate를 주입받아 DB와 통신하고 Service, Controller에는 결과를 return해주었다.
- Interface
public interface FruitRepository {
FruitResponse selectFruits(String name);
void insertFruits(String name, LocalDate warehousingDate, long price);
boolean isExist(long id);
void updateFruits(long id);
}
- FruitMemoryRepository
@Repository
public class FruitMemoryRepository implements FruitRepository{
private final JdbcTemplate jdbcTemplate;
public FruitMemoryRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public FruitResponse selectFruits(String name) {
String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";
long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);
return new FruitResponse(salesAmount, notSalesAmount);
}
@Override
public void insertFruits(String name, LocalDate warehousingDate, long price) {
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
@Override
public boolean isExist(long id) {
String readSql = "SELECT * FROM fruits WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public void updateFruits(long id) {
String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
jdbcTemplate.update(updateSql, id);
}
}
- FruitMysqlRepository
@Repository
@Primary
public class FruitMySqlRepository implements FruitRepository{
private final JdbcTemplate jdbcTemplate;
public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public FruitResponse selectFruits(String name) {
String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";
long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);
return new FruitResponse(salesAmount, notSalesAmount);
}
@Override
public void insertFruits(String name, LocalDate warehousingDate, long price) {
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
@Override
public boolean isExist(long id) {
String readSql = "SELECT * FROM fruits WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public void updateFruits(long id) {
String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
jdbcTemplate.update(updateSql, id);
}
}
이렇게 위의 요구사항과 같이 Interface로 나누고 FruitMemoryRepository와 FruitMySqlRepository가 FruitRepository를 구현하도록 하였다. 이렇게 되면 Service에 코드 수정 없이 Repository를 Memory로 사용할지 Mysql로 사용할지 사용할 수 있다.
💡그렇다면 어떻게 사용할까?
현재 FruitMySqlRepository를 보면 @Primary 어노테이션이 있다. 이 어노테이션은 "FruitMySqlRepository을 사용할거야~"라고 알려주는 어노테이션이다. 만약 @Primary가 없다면 Service 코드의 생성자 부분에서 오류가 나게된다.
다음으론 @Qualifier어노테이션이 있다. @Qualifier("fruitMemoryRepository")를 Service 코드의 생성자 부분에 사용하면 FruitMySqlRepository에 @Primary가 붙어 있더라도 fruitMemoryRepository를 사용하게 된다.
(스프링에서는 사용자가 직접 적어준 @Qualifier가 우선순위를 가진다.)
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService
(@Qualifier("fruitMemeoryRepository")FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
// 이하 생략
}
@Qualifier 어노테이션은 @Qualifier("fruitMemoryRepository") 이런 방식이 아니어도 아래와 같은 방식으로도 사용할 수 있다.
// 서비스 코드
@Service
public class FruitService {
private final FruitRepository fruitRepository;
public FruitService
(@Qualifier("fruit")FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
// 이하 생략
}
// 레포지토리 코드
@Repository
@Qualifier("fruit")
public class FruitMemoryRepository implements FruitRepository{
// 이하 생략
}
사용하려는 곳에 직접 이름을 지정해주고, 지정해준 이름을 생성자 앞에 적어주는 방식으로도 사용할 수 있다.
@Qualifier를 사용시 클래스 이름을 그대로 적어주면 아래와 같이 오류가 났다.
Inspection 'Incorrect autowiring in Spring bean components' has no quick-fixes for this problem.
Click to edit inspection options, suppress the warning, or disable the inspection completely.
이는 Spring 프레임워크에서 사용되는 빈(Bean)에 대한 자동 와이어링(Autowiring) 설정이 올바르지 않을 때 발생하는 경고이다. 경고가 "quick-fixes"를 제공하지 않는다는 것은 IDE가 자동으로 이 문제를 해결할 수 있는 방법이 없다는 것을 의미한다. 보통은 해당 빈을 명시적으로 주입하도록 코드를 수정하거나, 인스펙션 설정을 변경하여 경고를 무시하거나 비활성화하는 등의 수동 조치가 필요하다.
라고 chat-gpt가 이야기 해주었다..
@Autowired 어노테이션을 추가하거나 IntelliJ 의 Settings 메뉴에서 Editor > Inspections로 이동하여 "Incorrect autowiring in Spring bean components" 검색하여 해당 인스펙션의 옵션을 수정하라고 하였다.
자세히 강의를 보니 강사님께서 클래스 이름을 FruitMemoryRepository로 작성하셨는데 첫 글자를 소문자로 작성하신것을 보고 바꾸니까 에러가 없어졌다.
왜 그런지 아직 잘 모르겠다.. 조금 찾아보아야겠다.
[이유를 알아냈다..!]
@Qualifier는 직접 빈으로 등록해준 것이 아니라면 빈으로 설정된 Default 이름을 적어주어야한다.
FruitMemoryRepository의 등록된 이름을 확인해보니 아래와 같았다.
즉, Default 이름은 첫 글자가 소문자인 이름으로 등록되는 것이었다.
요것도 몰랐다니..아무튼 이번에 알게 되었으니 꼭 기억해야겠다!
'개발 > 인프런 워밍업 스터디' 카테고리의 다른 글
[인프런 워밍업 클럽 0기 BE] 2주차 발자국 (0) | 2024.03.03 |
---|---|
[인프런 워밍업 클럽 0기 BE] 7일차 - JPA (1) | 2024.02.27 |
[인프런 워밍업 클럽 0기 BE] 1주차 발자국 (0) | 2024.02.25 |
[인프런 워밍업 클럽 0기 BE] 5일차 - 클린 코드(Clean Code) (0) | 2024.02.23 |
[인프런 워밍업 클럽 0기 BE] 4일차 - 추가적인 API 만들기 (0) | 2024.02.22 |