인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.
스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고 과제로 수행했던 내용들을 정리해 보고자 한다.
(1) 4일 차 : 2024-02-22(Thu)
1. 과제 수행 : 문제 1번
(1) 과일 가게에 입고된 과일 정보를 저장하는 API를 설계하자.
(2) HTTP Method : POST
(3) HTTP Path : /api/v1/fruit
(4) HTTP Request Body의 형태
(5) 성공 시 200 OK를 서버 측에서 반환받도록 한다.
1-2. 데이터베이스 설계
create table fruit
(
fruit_id bigint auto_increment,
name varchar(20),
warehousing_date datetime,
price int,
is_sale boolean default 0,
primary key (fruit_id)
)
(1) 데이터베이스는 위와 같이 설계하였고 id 값은 auto_increment를 주어 아이템이 추가될 때마다 자동으로 증가하도록 설정했다.
(2) 판매 여부를 설정해 주기 위해 is_sale 컬럼을 넣었으며 입고된 과일은 판매되지 않았으니 기본값은 0으로 간주하자.
(3) 판매된 과일의 is_sale은 1이다.
1-3. FruitCreateRequestDto.java
@Data
public class FruitCreateRequestDto {
private String name;
private LocalDate warehousingDate;
private Long price;
}
(1) POST 요청 바디로 넘어온 데이터들과 각 필드들을 매핑한다.
1-4. FruitController.java - 입고된 과일의 과일 정보 생성
@RestController
@RequiredArgsConstructor
public class FruitController {
private final JdbcTemplate jdbcTemplate;
@PostMapping("/api/v1/fruit")
public void createFruit(@RequestBody FruitCreateRequestDto request) {
String sql = "insert into fruit (name, warehousing_date, price) values (?, ?, ?)";
jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
}
(1) POST 요청임에 따라 넘어온 요청 바디의 name, warehousingDate, price를 모두 받아와서 jdbcTemplate.update() 메서드를 통해 쿼리의 ?(와일드 카드) 부분에 모두 매핑시킨다.
(2) 따라서 위의 URL로 요청 바디와 함께 요청이 오게 되면 과일 정보를 성공적으로 저장할 수 있을 것이다.
1-5. 과일 정보 생성 API 반환 테스트, 데이터베이스 반영 확인
(1) Postman을 실행하여 요구사항에 맞게 과일 이름은 사과, warehousing_date는 2024-02-21, 가격은 10,000원으로 설정하고 요청을 보내게 되면 200 OK를 응답받은 것을 확인했다.
(2) 데이터베이스 확인 시에도 데이터가 정상적으로 저장된 것을 확인할 수 있다.
(3) 다음 문제도 확인해 보자.
1-6. 데이터 타입 int에서 long을 사용하는 이유?
(1) 본인이 생각하고 있는 이유가 정확하진 않을 수도 있지만, 수의 표현 범위에서의 차이이지 않을까 싶다. int의 경우 약 21억 정도의 크기를 갖는 수를 저장할 수 있는데 해당 자료형이 담는 수도 매우 커보이지만, 이 부분이 실무를 넘어가면 21억보다 큰 수를 받아야 하는 상황이 종종 생길 수도 있기 때문에 long 타입을 사용하는 것으로 알고 있다.
2. 과제 수행 : 문제 2번
(1) 과일이 판매되면 판매된 과일 정보를 기록하는 API를 설계하자.
(2) HTTP Method : PUT
(3) HTTP Path : /api/v1/fruit
(4) HTTP Request Body
(5) 성공 시 200 OK를 서버 측에서 반환받도록 한다.
2-1. FruitSaleStateUpdateRequestDto.java
@Data
public class FruitSaleStateUpdateRequestDto {
private long id;
}
(1) POST 요청 바디로 넘어온 데이터들과 각 필드들을 매핑한다.
2-2. FruitController - 판매된 과일에 대한 과일 정보를 기록
@RestController
@RequiredArgsConstructor
public class FruitController {
private final JdbcTemplate jdbcTemplate;
@PostMapping("/api/v1/fruit")
public void createFruit(@RequestBody FruitCreateRequestDto 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 updateSaleState(@RequestBody FruitSaleStateUpdateRequestDto request) {
String selectSql = "select * from fruit where fruit_id = ?";
boolean isFruitNotExist = jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isFruitNotExist) {
throw new IllegalStateException("존재하지 않는 과일 정보입니다.");
}
String sql = "update fruit set is_sale = 1 where fruit_id = ?";
jdbcTemplate.update(sql, request.getId());
}
}
(1) 이전 회원 컨트롤러와 마찬가지로 입고된 과일 중, 판매가 이루어진 과일의 is_sale의 값을 1로 수정해야 한다.
(2) 그러나 입고되지 않은 과일 대상으로는 판매 상태를 바꾼다는 것 자체가 모순이므로 위와 같이 selectSql을 처음에 먼저 실행시키고 만약 요청 바디에 넘어온 아이디 값을 기준으로 입고된 과일이 아니라면 IllegalStateException 예외를 발생시켜서 과일의 상태를 업데이트할 수 없도록 설계한다.
2-3. 우선 서버를 실행시켜서 여러 개의 과일을 테스트 케이스로 넣는다.
(1) 여러 개의 과일을 테스트하고 싶었다. 따라서 포스트맨을 이용해 이전에 설계했던 입고된 과일 정보를 기록하는 API를 호출해서 여러 가지 케이스를 추가했다.
(2) 위의 입고된 상품들은 아직 판매되지 않았으니 is_sale의 값들은 모두 0이다.
2-4. 판매된 과일의 판매 상태(is_sale)의 값을 1로 변경시키자.
(1) 만약 귤, 오렌지가 판매되었다고 가정해 보자. 포스트맨에서 아래와 같이 id=1, id=3을 대상으로 판매 상태가 변경되도록 서버로 PUT 요청을 보냈다.
(2) 정상적으로 귤, 오렌지의 판매 상태가 1로 변경된 것을 확인할 수 있다!
(3) 아직 끝이 아니다. 예외를 터뜨리는 상황도 테스트해 보자.
(4) 데이터베이스에서 존재하지 않는 ID 값으로 요청을 주니 기대했던 IllegalStateException이 발생한 것을 확인할 수 있다.
(5) 서버사이드에서도 기대했던 예외를 성공적으로 발생시킨다.
(6) 다음 문제로 넘어가 보자!
3. 과제 수행 : 문제 3번
(1) 특정한 과일을 대상으로 판매된 금액, 판매되지 않은 금액의 총합을 계산해보는 API를 설계하자.
(2) HTTP Method : GET
(3) HTTP Path : /api/v1/fruit/stat
(4) HTTP Query parameter : name
(5) HTTP 응답 바디의 결과
3-1. 데이터베이스 테이블 값 세팅
(1) 기존의 로우들을 모두 날리고, 동일한 과일들을 모두 세팅하면서 판매되지 않은 과일, 판매된 과일을 각각 위와 같이 나누었다.
3-2. FruitSaleNoSaleTotalPriceDto.java
@Data
public class FruitSaleNoSaleTotalPriceDto {
private long salesAmount;
private long noSalesAmount;
public FruitSaleNoSaleTotalPriceDto(long salesAmount, long noSalesAmount) {
this.salesAmount = salesAmount;
this.noSalesAmount = noSalesAmount;
}
}
(1) 판매 금액, 미판매 금액을 파라미터로 받아서 응답으로 반환해주기 위한 생성자를 별도로 생성한다.
3-3. FruitController - 판매된 과일, 판매되지 않은 과일에 대한 금액을 출력
@RestController
@RequiredArgsConstructor
public class FruitController {
private final JdbcTemplate jdbcTemplate;
@PostMapping("/api/v1/fruit")
public void createFruit(@RequestBody FruitCreateRequestDto 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 updateSaleState(@RequestBody FruitSaleStateUpdateRequestDto request) {
String selectSql = "select * from fruit where fruit_id = ?";
boolean isFruitNotExist = jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isFruitNotExist) {
throw new IllegalStateException("존재하지 않는 과일 정보입니다.");
}
String sql = "update fruit set is_sale = 1 where fruit_id = ?";
jdbcTemplate.update(sql, request.getId());
}
@GetMapping("/api/v1/fruit/stat")
public List<FruitSaleNoSaleTotalPriceDto> getSaleAndNoSaleTotalPrice(@RequestParam String name) {
String sql = "select " +
"(select sum(price) from fruit where is_sale = 1) as salesAmount, " +
"(select sum(price) from fruit where is_sale = 0) as notSalesAmount";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
long salesAmount = rs.getLong("salesAmount");
long notSalesAmount = rs.getLong("notSalesAmount");
return new FruitSaleNoSaleTotalPriceDto(salesAmount, notSalesAmount);
});
}
}
(1) 현재 메서드를 보면 쿼리가 서브쿼리 두 개를 조회하는 것을 알 수 있다.
(2) 이것보다 더 좋은 방법으로 풀으신 분도 있겠지만 우선 위에서 정의한 판매상태 0, 1을 기준으로 각각의 총합 금액을 계산하는 쿼리를 작성해서 rs 변수에서 각각의 결과를 받고 FruitSaleNoSaleTotalPriceDto 생성자를 만들어서 해당 값들을 API로 반환할 수 있도록 설계했다.
(3) 이제 잘 동작하는지 테스트해 보자.
3-4. 포스트맨으로 테스트
(1) 현재 결과가 정상적으로 반환되는 것을 확인할 수 있었다!
(2) 위의 귤 항목만 나열된 테이블들을 확인해 보면 판매 상태에 따라 판매 금액(is_sale = 1)의 총합 43,120 판매되지 않은(is_sale = 0) 총 금액 63,200이 정상 반환된 것을 확인할 수 있었다.
3-5. sum 집계 함수는 적용시켰으므로 Group by를 사용해 코드를 수정하고 동일한 결과가 나오는지 확인.
(1) 우선 group by의 경우 특정 컬럼 기준에 따라 그룹으로 나누고, 동일한 값을 가진 로우들이 하나의 그룹으로 묶이게 된다. 이후 집계 함수를 사용해서 각 그룹의 통계 정보를 계산할 수 있다.
@GetMapping("/api/v1/fruit/stat")
public List<FruitSaleNoSaleTotalPriceDto> getSaleAndNoSaleTotalPrice(@RequestParam String name) {
String sql = "select is_sale, sum(price) as total_price from fruit group by is_sale";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
long salesAmount = rs.getLong("salesAmount");
long notSalesAmount = rs.getLong("notSalesAmount");
return new FruitSaleNoSaleTotalPriceDto(salesAmount, notSalesAmount);
});
}
(2) 위에서 보았던 getSaleAndNoSaleTotalPrice() 메서드의 쿼리 부분을 위와 같이 수정할 수 있다.
select
fruit.is_sale,
sum(price) as total_price
from fruit
group by is_sale
order by is_sale desc;
(3) 위와 같이 판매 상태에 따라 그룹화하는 쿼리를 날리게 되면 아래와 같은 결과를 얻는다.
(4) 차이점은 하나의 컬럼으로 그룹화하여 데이터를 조회했다는 것을 확인할 수 있다.
4. 개인 회고
(1) 저번 과제에 이어 API를 다시 한 번 설계해 볼 수 있는 과제였다.
(2) 차이점은 저번엔 데이터베이스가 없었지만, 이번엔 별도의 데이터베이스를 구축하고 테이블을 만들어서 클라이언트에서 요청을 줄 때마다 서버의 데이터가 변하는 것을 확인하면서 실습해 볼 수 있다는 점이 큰 변화인 것 같다.
(3) 많이 어렵지 않은 내용이더라도 한 번쯤은 짚고 넘어가면 좋을 것 같다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다!
'기록, 회고 > InFlearn Warming-up 0기 BE' 카테고리의 다른 글
[5일 차] - 과제 수행 : 클린 코드, 코드 리팩토링 (0) | 2024.02.21 |
---|---|
[5일 차] - 내용 정리, 개인 회고 (0) | 2024.02.21 |
[4일 차] - 내용 정리, 개인 회고 (0) | 2024.02.21 |
[3일 차] - 과제 수행 : 익명 클래스, 함수형 프로그래밍(람다식), Stream API, 메서드 참조(Method Reference) (4) | 2024.02.20 |
[3일 차] - 내용 정리, 개인 회고 (0) | 2024.02.20 |
댓글