About Me/인프런 워밍업 클럽

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

블로그 주인장 2024. 5. 13.

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

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

 

 

본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다.

반응형

댓글