블로그
전체 132025. 03. 30.
0
[워밍업 클럽 3기 BE code] 4주차 발자국
강의 수강 Mock Dummy: 아무 것도 하지 않는 깡통 객체Fake: 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체(ex. FakeRepository)Stub: 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 않는다.Spy: Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체. 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다.Mock: 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체Stub vs. MockStub: 상태 검증(State Verification)Mock: 행위 검증(Behavior Verification) 더 나은 테스트를 작성하기 위한 구체적인 조언 1. 한 문단에 한 주제!반복문, 조건문 지양Parameterized Test 활용2. 완벽하게 제어하기현재시간, 랜덤값 등은 분리해서 상위 레벨로 올리기3. 테스트 환경의 독립성을 보장하자.테스트가 실패하더라도 그 부분은 when, then 절이어야 함given 절에서 테스트가 깨지면 왜 실패했는지 유추하기 힘들어짐테스트에서는 팩토리 메서드도 지양, 생성자나 빌더로 생성하기4. 테스트 간 독립성을 보장하자.두 가지 이상의 테스트가 하나의 자원을 공유하면 안됨(static 변수 x)테스트는 순서와 무관해야하고 각각 독립적으로 작동해야 함하나의 인스턴스가 차례대로 변화하는 과정을 테스트하고 싶다면 DynamicTest 사용5. 한 눈에 들어오는 Test Fixture 구성하기Fixture: 고정물, 고정되어 있는 물체테스트를 위해 원하는 상태로 고정시킨 일련의 객체 (주로 given절 구성할 때)setUp에서 공통된 Fixture들을 구성하고 테스트할 수 있지만, 공유 변수는 테스트간 결합도가 생기게 만들기 때문에 지양하는 것이 좋음setUp에 given절이 위치해 있으면 문서로서의 기능도 사라짐setUp을 구성할 때는 "각 테스트 입장에서 봤을 때, 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?", "수정해도 모든 테스트에 영향을 주지 않는가?" 질문을 던져볼 것data.sql을 활용해서 테스트 데이터를 넣어놓을 수 있지만, 이는 데이터가 파편화되어서 무얼 테스트하는지 파악하기 어렵게 함프로젝트가 커질수록 data.sql 관리가 어려워짐프로젝트에서 필요한 빌더메서드를 만들어서 사용할 때는, 필요한 파라미터만 사용할 것빌더를 클래스마다 만들려면 힘드니까 모아서 사용하면 안될까? - 추천하지 않음실무에서 사용하는 객체는 필드가 수십개가 되는 경우도 있음그럼 한 클래스에 각자 사용하는 빌더를 계속 만들기 시작하다보면 관리가 어려워짐코틀린을 사용하면 어느정도 해소가 됨6. Test Fixture 클렌징deleteAll()은 건별로 지우는 다수 쿼리때문에 속도 저하deleteAllInBatch()는 관계만 잘 생각하면 더 좋은 방법@Transactional 롤백을 주로 사용하긴 함Spring Batch같은 걸 사용한 Batch 통합테스트를 쓰면 여러 트랜잭션이 참여를 하기 때문에 트랜잭션 롤백이 사용하기 어려워 질 수 있음 그럴때는 deleteAllInBatch() 사용7. @ParameterizedTest하나의 테스트케이스인데 값을 여러 개로 바꾸어가며 테스트해보고 싶을 때 사용8. @DynamicTest하나의 환경을 설정해놓고 시나리오를 테스트하고 싶을 때9. 테스트 수행도 비용이다. 환경 통합하기서버가 뜨는 횟수가 많아지면 시간적 비용이 큼상위 추상클래스를 만들어서 각 테스트가 상속받게 함Mocking을 하면 새로운 환경이기 때문에 새로 서버가 띄워짐 Mocking 처리한 것들을 가장 위로 올리거나, 테스트환경을 분리해야함Q. private 메서드의 테스트는 어떻게 하나요?할 필요가 없고, 하려고 해서도 안된다.클라이언트 입장에서는 공개 API만 알면 됨 private 메서드를 테스트하고 싶은 시점에 해야 할 고민 - "객체를 분리할 시점인가?"팩토리 클래스의 public 메서드로 변경해서 테스트Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면?만들어도 된다. 하지만 보수적으로 접근하기! 회고 테스트코드 강의를 마무리하며 워밍업스터디 3기가 끝이 났다.강의를 수강하면서 어렵기도 했지만 유익한 팁들을 많이 얻었던 것 같다.하지만 진도를 따라가기가 조금은 벅차서 아직 내 것으로 만들지는 못했다.이제 내 프로젝트에 배운 부분들을 하나씩 적용해보면서 학습을 진행할 생각이다.
2025. 03. 23.
0
[워밍업 클럽 3기 BE code] 3주차 발자국
강의 수강 Persistence LayerData Access 역할비즈니스 가공 로직이 포함되어서는 안된다.Data에 대한 CRUD에만 집중한 레이어 Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다. Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행한다. MockMvc- Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크 회고이번 주가 가장 듣고 싶었던 부분이었는데, 강의가 굉장히 길고 코드를 치는 부분도 많아서 쉽지 않았다.강의를 들으면서 좀 놀랐던 점은 정말 테스트를 꼼꼼하게, 바로바로 작성한다는 것이었다.나는 이전에 구현을 어느 정도 끝내고 테스트를 작성하는 식으로 했었다. 하지만 강사님은 정말 간단한 메서드를 만들더라도 바로바로 테스트코드를 작성하셨다.구현을 다하고 나면 테스트코드를 짜기가 귀찮았었는데 강사님이 작성하시는 방식을 체화하면 테스트코드를 짜는 것이 더 자연스러워지지 않을까 하는 생각이 들었다.이번 주도 많이 배웠고 배운 것들을 적용하며 내 것으로 만들어야겠다.
2025. 03. 16.
0
[워밍업 클럽 3기 BE code] 2주차 발자국
강의 수강 테스트는 왜 필요할까?추가한 기능이 기존 프로덕션 코드와 겹치는 상황(기존 코드를 건드리는 상황) 발생 이런 일들이 비일비재함이런 상황에서 사람이 테스트를 진행하면 문제가 발생할 확률이 높음- 커버할 수 없는 영역 발생- 경험과 감에 의존- 늦은 피드백- 유지보수 어려움- 소프트웨어 신뢰도 낮아짐 테스트코드를 통해 얻고자 하는 것- 빠른 피드백- 자동화- 안정감 만약 테스트코드가 엉망이라면?프로덕션코드를 잘 지원하지 못한다.테스트코드를 잘 짜야한다. 테스트코드를 작성하지 않는다면?- 변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.- 변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 한다.- 빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다. 테스트코드가 병목이 된다면?- 프로덕션 코드의 안정성을 제공하기 힘들어진다.- 테스트코드 자체가 유지보수하기 어려운, 새로운 짐이 된다.- 잘못된 검증이 이루어질 가능성이 생긴다. 올바른 테스트코드는?- 자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.- 소프트웨어의 빠른 변화를 지원한다.- 팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.- 가까이 보면 느리지만, 멀리 보면 가장 빠르다.단위 테스트 - 작은 코드 단위(클래스, 메서드)를 독립적으로 검증하는 테스트- 검증 속도가 빠르고, 안정적이다. JUnit 5- 단위 테스트를 위한 테스트 프레임워크- Xunit(SUnit, JUnit, NUnit...) - Kent Beck AssertJ- 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리- 풍부한 API, 메서드 체이닝 지원 테스트 케이스 세분화하기질문하기: 암묵적이거나 아직 드러나지 않은 요구사항이 있는가?- 해피 케이스- 예외 케이스경계값 테스트 중요! -> 범위(이상, 이하, 초과, 미만), 구간, 날짜 등 회고 테스트코드 강의를 처음 학습하였다.이전부터 테스트코드가 중요하다는 것을 많이 들어왔지만, 실제로 많이 작성해보진 못했다.강의를 통해 테스트코드의 중요성과 테스트 방법에 대해 배웠고 내 프로젝트에도 적용해 볼 생각이다.앞으로 배우는 구체적인 내용들도 학습해서 테스트코드를 잘 작성할 수 있도록 노력해야겠다.
2025. 03. 09.
0
[워밍업 클럽 3기 BE code] 1주차 발자국
강의 수강 추상 추상의 정의: 사물이나 개념의 핵심적인 부분만을 추출하여 표현하는 과정클린 코드와 추상: 읽기 쉽고 유지보수하기 쉬운 코드는 중요한 정보를 남기고 불필요한 부분을 제거하는 추상화 과정을 포함이름 짓기의 중요성: 적절한 이름 부여를 통해 구체적인 내용을 추상화하고, 도메인 내에서 의미를 전달하는 것이 중요함 논리 사고의 흐름 인지적 경제성: 최소한의 인지적 노력으로 최대한의 정보를 전달하는 방법코드 구조화: 중첩 분기나 반복문을 줄이고, 변수는 가능한 가까운 곳에 선언하여 사고의 깊이를 낮추는 방법명확한 흐름: Early return 사용, 부정어 대신 긍정적 표현을 통해 코드 가독성을 높이는 방법 등 객체 지향 패러다임 객체와 추상화: 객체는 데이터와 기능을 하나로 묶어 추상화하며, 서로 협력하여 역할을 수행함책임과 협력: 객체 간의 역할 분담(협력과 책임)을 통해 높은 응집도와 낮은 결합도를 실현하는 방법SOLID 원칙: 단일 책임, 개방-폐쇄, 리스코프 치환, 인터페이스 분리, 의존성 역전 원칙 등 객체 설계의 핵심적인 원칙 어떻게 작성하는게 좋은 코드인지 배울 수 있었다.하지만 아직 익숙치 않아서 코드에 적용하려면 연습이 많이 필요할 것 같다. 미션 Day2: 추상의 예시추상이란 것에 대해 더 자세히 이해할 수 있는 미션이었다.Day4: 리팩토링 및 SOLID원칙을 나의 말로 바꾸어 쓰기예시코드에 수업에서 말씀하신 것들을 잘 적용해볼 수 있었고, SOLID 원칙도 나의 말로 바꾸어 써보니 좀 더 이해도가 올라갔던 것 같다.
2024. 03. 10.
0
[인프런 워밍업 클럽 BE 0기] 3주차 발자국
안녕하세요.인프런 워밍업 클럽 3주차 발자국입니다. 강의 수강 저번 주에 듣지 못했던 JPA 파트 강의를 포함해서 나머지 모든 강의를 들었습니다.JPA파트는 미션 수행에 필요해서 미션을 진행하면서 들었고, 나머지 강의는 다시 전체적인 흐름을 복습하는 느낌으로 들었던 것 같습니다. 미션 미니프로젝트 미션을 진행했습니다.1단계까지 얼른 진행해서 완주러너 조건을 갖추고 이후의 단계를 진행하려고 했는데, 아쉽게도 이후의 단계는 아직 진행하지 못했습니다. 미션 1단계는 이전에 했던 미션과 큰 차이가 없어서 엄청 어렵진 않았습니다. 하지만 제출하고 다른 분들 코드를 보니 개선해야 할 부분도 많은 것 같고 많이 부족하다고 느꼈습니다. 강의에서는 들었지만 생각이 안나서 고려하지 못한 부분도 많았고 구현 방식 자체도 아쉬웠던 부분이 많았습니다.미션 2단계부터는 동기부여가 조금 떨어져서인지 잘 손이 안갔습니다. 다른 할 것들을 먼저 하고 하다보니 결국에는 못하게 됐습니다. 느낀 점 스터디에 열정적으로 임하지 못해서 생각보다 아쉽습니다. 강의에서는 얻어갈 것이 참 많았는데, 그것들을 다 소화하진 못한 느낌입니다. 스터디가 종료된 이후에 강의를 다시 들어보고 못했던 미션들을 다시 진행해 볼 생각입니다.강의 외적인 부분에서 얻어간 것이 많습니다. 이렇게 다수가 스터디 형식으로 하다보니 다른 분들은 어떻게 공부하고 있는지 볼 수 있어서 도움이 됐고, 자극도 많이 얻었던 것 같습니다.마지막으로 이런 강의를 열어주신 코치님, 스터디를 기획해주신 인프런에게 감사드립니다.
2024. 03. 03.
0
[인프런 워밍업 클럽 BE 0기] 2주차 발자국
안녕하세요.인프런 워밍업 클럽 BE 0기 2주차 발자국입니다. 강의 수강 저번 주에 했던 섹션3 이후로 강의 진도는 나가지 못했습니다ㅜㅜ이전에 들었던 내용으로 미션6 까지 밖에 진행을 못했네요. 아쉬운 점 이번 주는 개인적인 일정 때문에 공부를 거의 못했습니다.다행히 이전에 들었던 내용으로 미션 6까지는 진행할 수 있었지만, JPA를 이용하는 미션 7은 진행하지 못했네요.너무 아쉬웠던 한 주였습니다.미션 Layered Architecture 과제 (6일차)Controller, Service, Repository 분리하는 것은 익숙했던 부분이기도 했고, 강의에서도 설명해주셨기 때문에 할만했던 것 같습니다.기존에 작성된 코드가 있기 때문에, 각 계층의 역할을 생각하며 분리를 진행했습니다.다음 주 목표 미니 프로젝트 step1 완료강의 완강이번 주에 공부를 많이 못해서 다음주는 급하게 달려야할 것 같습니다.다음 주도 모두 화이팅입니다!
2024. 02. 26.
0
[인프런 워밍업 클럽 BE 0기] 6일차 과제
Controller, Service, Repository 분리하기 문제 1 FruitController@RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping("/api/v1/fruit") @ResponseStatus(HttpStatus.OK) public void saveFruit(@RequestBody Fruit fruit) { fruitService.saveFruit(fruit); } @PutMapping("/api/v1/fruit") @ResponseStatus(HttpStatus.OK) public void sellFruit(@RequestBody FruitSellRequest fruitSellRequest) { fruitService.sellFruit(fruitSellRequest.getId()); } @GetMapping("/api/v1/fruit/stat") public Map fruitStat(@RequestParam String name) { return fruitService.getFruitStat(name); } } FruitService@Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruit(Fruit fruit) { fruitRepository.saveFruit(fruit); } public void sellFruit(long fruitId) { fruitRepository.sellFruit(fruitId); } public Map getFruitStat(String name) { long soldAmount = fruitRepository.calculateSoldAmount(name); long unsoldAmount = fruitRepository.calculateUnsoldAmount(name); Map result = new HashMap(); result.put("soldAmount", soldAmount); result.put("unsoldAmount", unsoldAmount); return result; } } FruitRepository@Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(Fruit fruit) { String sql = "insert into fruit (name, warehousingDate, price) values (?, ?, ?)"; jdbcTemplate.update(sql, fruit.getName(), fruit.getWareHousingDate(), fruit.getPrice()); } public void sellFruit(int fruitId) { String sql = "UPDATE fruit set is_sold = true where id = ?"; jdbcTemplate.update(sql, fruitId); } public long calculateSoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = true"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); } public long calculateUnsoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = false"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); } } 문제 2 FruitRepositorypublic interface FruitRepository { void saveFruit(Fruit fruit); void sellFruit(int fruitId); long calculateSoldAmount(String name); long calculateUnsoldAmount(String name); } FruitMysqlRepository@Primary @Repository public class FruitMysqlRepository implements FruitRepository{ private final JdbcTemplate jdbcTemplate; public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(Fruit fruit) { String sql = "insert into fruit (name, warehousingDate, price) values (?, ?, ?)"; jdbcTemplate.update(sql, fruit.getName(), fruit.getWareHousingDate(), fruit.getPrice()); } public void sellFruit(long fruitId) { String sql = "UPDATE fruit set is_sold = true where id = ?"; jdbcTemplate.update(sql, fruitId); } public long calculateSoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = true"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); } public long calculateUnsoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = false"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); } } FruitMemoryRepository@Repository public class FruitMemoryRepository implements FruitRepository{ private final Map fruitMap = new HashMap(); private int idCounter = 1; public void saveFruit(Fruit fruit) { fruit.setId(idCounter++); fruitMap.put(fruit.getId(), fruit); } public void sellFruit(long fruitId) { Fruit fruit = fruitMap.get(fruitId); if (fruit != null) { fruit.setSold(true); } } public long calculateSoldAmount(String name) { long soldAmount = 0; for (Fruit fruit : fruitMap.values()) { if (fruit.getName().equals(name) && fruit.isSold()) { soldAmount += fruit.getPrice(); } } return soldAmount; } public long calculateUnsoldAmount(String name) { long unsoldAmount = 0; for (Fruit fruit : fruitMap.values()) { if (fruit.getName().equals(name) && !fruit.isSold()) { unsoldAmount += fruit.getPrice(); } } return unsoldAmount; } }
2024. 02. 25.
0
[인프런 워밍업 클럽 BE 0기] 1주차 발자국
안녕하세요.인프런 워밍업 클럽 BE 0기 1주차 발자국입니다. 스터디 신청 계기자바 스프링에 대해 파편화된 개념만 보다 보니 전체적인 흐름을 한번 정리하고 싶었습니다. 그러던 중에 이 강의를 발견했고 인프런에서 스터디도 계획하고 있는 것을 보고 신청하게 되었습니다. 강의 수강 섹션 1 - 생애 최초 API 만들기섹션 2 - 생애 최초 Database 조작하기섹션 3 - 역할의 분리와 스프링 컨테이너 이번 주차는 많이 보았던 내용이라 키워드들은 익숙했지만, 구체적인 내용은 확실히 숙지하지 못한 부분들이 많이 있었습니다. 다시 복습하며 다지는 시간이 되었던 것 같아요! 아쉬운 점 이번 주 할당량은 어떻게든 보긴 했지만, 다른 분들이 정리하신 내용을 보니까 내가 한 것은 형편없다고 느꼈습니다. 익숙한 키워드라 대충 보고 정리했던 것 같아요. 나중에 다시 봤을 때 이 내용을 정확하게 설명할 수 있을까? 라고 질문을 해본다면 아닐 것 같다는 생각이 들었습니다. 다음 주는 배운 개념들을 설명할 수 있을 정도까지 공부를 해 볼 생각입니다. 미션리서치 과제 (1, 3일차)어노테이션 / 람다식, 익명클래스 리서치 과제는 구글링과 chatGPT를 이용해서 해결했습니다. 처음 들어 본 개념은 아니었기 때문에 복습하는 느낌으로 1-2번 읽어보고 글을 작성했습니다. 하지만 조금 더 깊게 공부하는게 어땠을까 하는 아쉬움이 있었습니다. API 만들기 과제 (2, 4일차)이런 류의 과제를 해본 적이 있어서 쉽게 할 수 있다고 생각했는데, 생각보다는 어려웠습니다. 일단 혼자 작성해보고 어려웠던 부분이나 고민이 되었던 부분은 다른 분들의 코드도 조금 참고해서 작성했습니다. 다른 분들의 코드를 보는 것이 많이 도움이 되었던 것 같습니다. 리팩토링 과제 (5일차)다른 일정 때문에 시간이 부족해서 빨리 작성하는데 급급했던 것 같습니다. 더 개선할 부분이 많을 것 같은데, 코치님이 라이브 세션으로 설명해주신다고 하니 잘 듣고 배울 생각입니다. 1주간 스터디를 진행하면서 정말 모르는게 많다는 것을 느꼈습니다. 저는 스터디에 대해 회의적이었습니다. 하지만 다른 분들이 미션을 수행하는 것을 보면 자극도 되고 도움도 많이 되는 것 같습니다. 남은 기간은 이번 주보다 더 열심히 해서 다른 분들께 도움이 됐으면 좋겠습니다.
2024. 02. 23.
0
[인프런 워밍업 클럽 BE 0기] 5일차 과제
제시된 코드를 리팩토링 해보기 기존 코드public class Main { public static void main(String[] args) { System.out.println("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; for (int i = 0; i = 0 && b = 1 && b = 2 && b = 3 && b = 4 && b = 5 && b 리팩토링 내용 주사위 결과를 counts 배열에 저장하는 것으로 변경 Random 클래스를 이용하여 더 랜덤한 정수 값을 전달 받고, counts 배열에 저장 -> if-else문도 제거됨 주사위 결과를 받는 부분을 메서드 분리 출력 부분은 반복문으로 처리하여 중복코드 제거 결과 코드public class Main { public static void main(String[] args) { System.out.println("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int times = scanner.nextInt(); int[] counts = getDiceResult(times); for (int i = 0; i
2024. 02. 22.
0
[인프런 워밍업 클럽 BE 0기] 4일차 과제
문제 1 먼저 데이터베이스 테이블을 생성한다.create table fruit ( id bigint auto_increament, name varchar(25), warehousingDate date, price long, primary key (id) ) 컨트롤러 과일 저장 코드@RestController public class FruitController { private JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 문제 1 @PostMapping("/api/v1/fruit") @ResponseStatus(HttpStatus.OK) public void saveFruit(@RequestBody Fruit fruit) { String sql = "insert into fruit (name, warehousingDate, price) values (?, ?, ?)"; jdbcTemplate.update(sql, fruit.getName(), fruit.getWareHousingDate(), fruit.getPrice()); } } 과일 저장 DTOpublic class Fruit { private String name; private LocalDate wareHousingDate; private long price; public Fruit(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; } } 문제 2 table에 is_sold 컬럼 추가Fruit 클래스에 isSold 필드 추가 컨트롤러에 코드 추가 @PutMapping("/api/v1/fruit") @ResponseStatus(HttpStatus.OK) public void sellFruit(@RequestBody FruitSellRequest fruitSellRequest) { String sql = "UPDATE fruit set is_sold = true where id = ?"; jdbcTemplate.update(sql, fruitSellRequest.getId()); } 과일 판매 DTOpublic class FruitSellRequest { private long id; public FruitSellRequest(long id) { this.id = id; } public long getId() { return id; } } 문제 3 컨트롤러에 과일 통계 요청 처리 메서드 추가@GetMapping("/api/v1/fruit/stat") public Map fruitStat(@RequestParam String name) { long soldAmount = calculateSoldAmount(name); long unsoldAmount = calculateUnsoldAmount(name); Map result = new HashMap(); result.put("soldAmount", soldAmount); result.put("unsoldAmount", unsoldAmount); return result; } public long calculateSoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = true"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); } public long calculateUnsoldAmount(String name) { String sql = "select sum(price) from fruit where name = ? and is_sold = false"; return jdbcTemplate.queryForObject(sql, new Object[]{name}, Long.class); }