본문 바로가기

우테코/오픈미션

오픈미션 9일차

오늘은 서비스 계층을 마무리하고 컨트롤러로 넘어가는 날이다! 

 

나만의 로또 리스트를 조회하고 삭제하는 기능과 구매 로또 리스트를 조회하는 기능을 추가로 구현해야 한다. 근데 조회와 삭제는 JpaRepository에서 자동(?)으로 제공해준다고 한다. 이를 활용해 서비스를 완성해보자 ☺️

 

1. MyLotto 조회

// 나만의 로또 리스트 전체 조회
    @Transactional(readOnly = true)
    public List<MyLottoResponseDTO> findAllMyLotto() {
        return myLottoRepository.findAll().stream()
                // ID(생성순) 오름차순 정렬
                .sorted(Comparator.comparing(MyLotto::getId))
                .map(MyLottoResponseDTO::from) // DTO로 변환
                .collect(Collectors.toList());
    }

 

조회라는 것은 읽기만 하면 되므로 readOnly로 설정하였다. 그리고 가져올 때는 JPA Repository의 기본 메서드인 myLottoRepository.findAll() 을 사용했다. 보여줄 때는 List<MyLottoResponseDTO>가 컨트롤러에게 반환되는 구조이다. 컨트롤러가 읽어와~ 하면 레포지토리에서 가져와서 DTO형태로 컨트롤러로 최종적으로 List<MyLottoResponseDTO> 돌려주는 형태이다. 

 

 

2. MyLotto 삭제 

    // 나만의 로또 리스트에서 로또 삭제
    public void deleteMyLotto(Long myLottoId) {
        // 삭제 전, 존재하는 ID인지 확인
        if (!myLottoRepository.existsById(myLottoId)) {
            throw new IllegalArgumentException("[ERROR] 존재하지 않는 '나만의 로또' ID입니다: " + myLottoId);
        }
        myLottoRepository.deleteById(myLottoId);
    }

 

이 메서드는 컨트롤러로부터 삭제할 로또 아이디를 받아, DB에서 로또를 삭제하는 기능이다. DB에 DELETE 쿼리를 보내기 전에, existsById (JPA 기본 메서드)를 통해 해당 아이디가 존재하는 지를 먼저 확인한다. 만약 존재하지 않는다면 예외를 발생시킨다. 이와 같은 if 검사를 통과했다면 (즉, ID가 DB에 존재한다면) 삭제가 진행된다. 삭제를 하면 DB에서 해당 데이터를 실제로 삭제한다. 메서드 자체는 void이므로 컨트롤러에 따로 반환하는 것은 없고 컨트롤러는 "HTTP 200 OK" 또는 "204 No Content"를 클라이언트에 응답하여 삭제 성공을 알려준다. 

 

 

3. PurchasedLotto 조회 

// 구매 로또 조회
    @Transactional(readOnly = true)
    public List<PurchasedLottoResponseDTO> findAllPurchasedLotto() {
        return purchasedLottoRepository.findAll().stream()
                // 구매일(purchaseDate) 최신순(내림차순) 정렬
                .sorted(Comparator.comparing(PurchasedLotto::getPurchaseDate).reversed())
                .map(PurchasedLottoResponseDTO::from) // DTO로 변환
                .collect(Collectors.toList());
    }

 

4. PurchasedLotto 삭제 

// 구매 로또 삭제
    public void deletePurchasedLotto(Long purchasedLottoId) {
        // 삭제 전, 존재하는 ID인지 확인
        if (!purchasedLottoRepository.existsById(purchasedLottoId)) {
            throw new IllegalArgumentException("[ERROR] 존재하지 않는 '구매한 로또' ID입니다: " + purchasedLottoId);
        }
        purchasedLottoRepository.deleteById(purchasedLottoId);
    }

 

구매한 로또의 조회 & 삭제 로직은 가져오는 Repository가 PurchasedLottoRepository라는 것을 제외하고는 거의 동일하다. 하나 차이를 준 것은 나만의 로또 리스트는 오래된 것을 기준으로 보여주고 구매 로또는 최신 순으로 정렬해서 보여주는 것을 구현해보았다.

 


이제 모든 서비스를 테스트하기 위한 코드를 작성해서 단위 테스트를 진행해보았다. 나는 1주차 미션과 같이 간단한 콘솔 프로그램의 경우 내가 만약 배열로 받았다면 배열에 어떤 값이 저장되었는 지 등과 같이 이러한 코드 과정에서 필요한 모든 것을 하나하나 출력해서 확인하는 방법만 사용해와서 단위 테스트에 대한 필요성(?)을 느껴본 적이 많이 없는데 이번 프리코스를 통해 정말 많이 배운 것 같다. 특히 이번 스프링 부트 프로젝트를 진행하면서 뷰(콘솔 역할)을 먼저 만드는 것이 아닌 평소와는 완전 반대로 객체를 만들고 객체를 사용하는 서비스 계층을 구현하는 식으로 하고 있는데 이렇게 하니까 테스트 없이는 나의 서비스가 잘 동작하는 지 확인할 방법이 없다😱 하지만 아직은 테스트 코드를 막힘없이 작성하는 데는 어려움이 있는 것 같다 ㅠㅠ 더 열심히 해야지☀️

 

이번 테스트 코드는 Mock을 사용해보았다. 

@ExtendWith(MockitoExtension.class)
class LottoServiceTest {

    @Mock
    private MyLottoRepository myLottoRepository;

    @Mock
    private PurchasedLottoRepository purchasedLottoRepository;

    @Mock
    private WinningLottoRepository winningLottoRepository;

    @InjectMocks
    private LottoService lottoService;

    @Test
    @DisplayName("랜덤 로또 생성 시 6개의 숫자를 가진 DTO를 반환한다")
    void generateRandomLotto_ShouldReturnDTO_With6Numbers() {
        RandomLottoResponseDTO response = lottoService.generateRandomLotto();

        assertThat(response).isNotNull();
        assertThat(response.getNumbers()).isNotNull();
        assertThat(response.getNumbers()).hasSize(6);
        assertThat(response.getNumbers()).doesNotHaveDuplicates();
    }

    @Test
    @DisplayName("MyLotto 저장 요청 시, 엔티티를 DB에 저장하고 응답 DTO를 반환한다")
    void saveToMyLotto_ShouldSaveEntity_AndReturnResponseDTO() {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
        String lottoName = "내 첫 로또";
        MyLottoRequestDTO request = new MyLottoRequestDTO();
        request.setNumbers(numbers);
        request.setLottoName(lottoName);

        MyLotto savedEntityMock = mock(MyLotto.class);
        given(savedEntityMock.getId()).willReturn(1L);
        given(savedEntityMock.getLottoName()).willReturn(lottoName);
        given(savedEntityMock.getMyLotto()).willReturn(new Lotto(numbers));

        given(myLottoRepository.save(any(MyLotto.class))).willReturn(savedEntityMock);

        MyLottoResponseDTO response = lottoService.saveToMyLotto(request);

        assertThat(response).isNotNull();
        assertThat(response.getId()).isEqualTo(1L);
        assertThat(response.getLottoName()).isEqualTo(lottoName);
        assertThat(response.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6);

        verify(myLottoRepository, times(1)).save(any(MyLotto.class));
    }

    @Test
    @DisplayName("MyLotto 전체 조회 시, ID 오름차순으로 정렬된 DTO 리스트를 반환한다")
    void findAllMyLotto_ShouldReturnSortedDTOList() {
        MyLotto lotto2 = mock(MyLotto.class);
        given(lotto2.getId()).willReturn(2L);
        given(lotto2.getLottoName()).willReturn("두번째");
        given(lotto2.getMyLotto()).willReturn(new Lotto(List.of(1, 2, 3, 4, 5, 6)));

        MyLotto lotto1 = mock(MyLotto.class);
        given(lotto1.getId()).willReturn(1L);
        given(lotto1.getLottoName()).willReturn("첫번째");
        given(lotto1.getMyLotto()).willReturn(new Lotto(List.of(7, 8, 9, 10, 11, 12)));

        given(myLottoRepository.findAll()).willReturn(List.of(lotto2, lotto1));

        List<MyLottoResponseDTO> responseList = lottoService.findAllMyLotto();

        assertThat(responseList).isNotNull();
        assertThat(responseList).hasSize(2);
        assertThat(responseList.get(0).getId()).isEqualTo(1L);
        assertThat(responseList.get(1).getId()).isEqualTo(2L);
        assertThat(responseList.get(0).getLottoName()).isEqualTo("첫번째");
    }

    @Test
    @DisplayName("MyLotto 삭제 요청 시, 'existsById'와 'deleteById'를 호출한다")
    void deleteMyLotto_ShouldCallExistsAndDelete() {
        Long lottoId = 1L;
        given(myLottoRepository.existsById(lottoId)).willReturn(true);

        lottoService.deleteMyLotto(lottoId);

        verify(myLottoRepository, times(1)).existsById(lottoId);
        verify(myLottoRepository, times(1)).deleteById(lottoId);
    }

    @Test
    @DisplayName("존재하지 않는 MyLotto 삭제 요청 시 예외를 발생시킨다")
    void deleteMyLotto_ShouldThrowException_WhenNotFound() {
        Long notFoundId = 99L;
        given(myLottoRepository.existsById(notFoundId)).willReturn(false);

        assertThatThrownBy(() -> lottoService.deleteMyLotto(notFoundId))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("[ERROR] 존재하지 않는 '나만의 로또' ID입니다");

        verify(myLottoRepository, never()).deleteById(anyLong());
    }

    @Test
    @DisplayName("수동 구매 요청 시, 오늘 날짜로 DB에 저장하고 응답 DTO를 반환한다")
    void purchaseManualLotto_ShouldSaveWithTodayDate_AndReturnDTO() {
        List<Integer> numbers = List.of(7, 8, 9, 10, 11, 12);
        PurchasedLottoRequestDTO request = new PurchasedLottoRequestDTO();
        request.setNumbers(numbers);

        PurchasedLotto savedEntityMock = mock(PurchasedLotto.class);
        given(savedEntityMock.getId()).willReturn(1L);
        given(savedEntityMock.getPurchaseDate()).willReturn(LocalDate.now());
        given(savedEntityMock.getPurchasedLotto()).willReturn(new Lotto(numbers));

        given(purchasedLottoRepository.save(any(PurchasedLotto.class))).willReturn(savedEntityMock);

        PurchasedLottoResponseDTO response = lottoService.purchaseManualLotto(request);

        assertThat(response).isNotNull();
        assertThat(response.getId()).isEqualTo(1L);
        assertThat(response.getPurchaseDate()).isEqualTo(LocalDate.now());
        assertThat(response.getNumbers()).containsExactly(7, 8, 9, 10, 11, 12);

        ArgumentCaptor<PurchasedLotto> captor = ArgumentCaptor.forClass(PurchasedLotto.class);
        verify(purchasedLottoRepository, times(1)).save(captor.capture());

        PurchasedLotto entityPassedToSave = captor.getValue();
        assertThat(entityPassedToSave.getId()).isNull();
        assertThat(entityPassedToSave.getPurchaseDate()).isEqualTo(LocalDate.now());
        assertThat(entityPassedToSave.getPurchasedLotto().getNumbers()).isEqualTo(numbers);
    }

    @Test
    @DisplayName("MyLotto를 구매 요청 시, 번호를 복사하여 DB에 저장하고 DTO를 반환한다")
    void purchaseFromMyLotto_ShouldCopyNumbersAndSave_AndReturnDTO() {
        Long myLottoId = 5L;
        List<Integer> copiedNumbers = List.of(1, 2, 3, 4, 5, 6);

        MyLotto myLottoMock = mock(MyLotto.class);
        given(myLottoMock.getMyLotto()).willReturn(new Lotto(copiedNumbers));
        given(myLottoRepository.findById(myLottoId)).willReturn(Optional.of(myLottoMock));

        PurchasedLotto savedEntityMock = mock(PurchasedLotto.class);
        given(savedEntityMock.getId()).willReturn(1L);
        given(savedEntityMock.getPurchaseDate()).willReturn(LocalDate.now());
        given(savedEntityMock.getPurchasedLotto()).willReturn(new Lotto(copiedNumbers));

        given(purchasedLottoRepository.save(any(PurchasedLotto.class))).willReturn(savedEntityMock);

        PurchasedLottoResponseDTO response = lottoService.purchaseFromMyLotto(myLottoId);

        verify(myLottoRepository, times(1)).findById(myLottoId);
        verify(purchasedLottoRepository, times(1)).save(any(PurchasedLotto.class));

        assertThat(response.getId()).isEqualTo(1L);
        assertThat(response.getNumbers()).isEqualTo(copiedNumbers);
    }

    @Test
    @DisplayName("PurchasedLotto 전체 조회 시 구매일(최신순) 정렬된 DTO 리스트를 반환한다")
    void findAllPurchasedLotto_ShouldReturnSortedByDateDescDTOList() {
        LocalDate yesterday = LocalDate.now().minusDays(1);
        LocalDate today = LocalDate.now();

        PurchasedLotto lottoToday = mock(PurchasedLotto.class);
        given(lottoToday.getId()).willReturn(2L);
        given(lottoToday.getPurchaseDate()).willReturn(today);
        given(lottoToday.getPurchasedLotto()).willReturn(new Lotto(List.of(1, 2, 3, 4, 5, 6)));

        PurchasedLotto lottoYesterday = mock(PurchasedLotto.class);
        given(lottoYesterday.getId()).willReturn(1L);
        given(lottoYesterday.getPurchaseDate()).willReturn(yesterday);
        given(lottoYesterday.getPurchasedLotto()).willReturn(new Lotto(List.of(7, 8, 9, 10, 11, 12)));

        given(purchasedLottoRepository.findAll()).willReturn(List.of(lottoYesterday, lottoToday));

        List<PurchasedLottoResponseDTO> responseList = lottoService.findAllPurchasedLotto();

        assertThat(responseList).hasSize(2);
        assertThat(responseList.get(0).getId()).isEqualTo(2L);
        assertThat(responseList.get(1).getId()).isEqualTo(1L);
        assertThat(responseList.get(0).getPurchaseDate()).isEqualTo(today);
    }

    @Test
    @DisplayName("PurchasedLotto 삭제 요청 시 'existsById'와 'deleteById'를 호출한다")
    void deletePurchasedLotto_ShouldCallExistsAndDelete() {
        Long lottoId = 1L;
        given(purchasedLottoRepository.existsById(lottoId)).willReturn(true);

        lottoService.deletePurchasedLotto(lottoId);

        verify(purchasedLottoRepository, times(1)).existsById(lottoId);
        verify(purchasedLottoRepository, times(1)).deleteById(lottoId);
    }
}

 

해당 테스트 코드로 서비스가 잘 동작하는 것까지 모두 확인하였다!! 이제 컨트롤러 설계로 넘어갈 시간이다. 


설계하기 전에 서비스 계층에서 했던 것과 동일하게 컨트롤러가 어떤 역할인지 부터 다시 정의해보았다. 

 

1. 컨트롤러란?

Controller는 MVC에서 C에 해당 하며 주로 사용자의 요청을 처리 한 후 지정된 뷰에 모델 객체를 넘겨주는 역할이다. 

 

Controller는 클라이언트의 요청에 맞게 함수를 실행해주고, 응답을 전송해주는 역할을 하는 데 이때 데이터베이스에 직접 접근하지 않고, 실행한 함수의 리턴 결과로 오는 데이터만 가지고 응답을 전송한다. 그리고 여기서 주는 데이터는 DTO 인것이다! 

 

여기서 다시 서비스랑 잠깐 비교해보자면 Service는 말 그대로 서비스의 메인 로직을 수행하는 역할을 한다. Controller가 Service의 함수를 실행하면 Service는 Repository를 통해 데이터베이스에 접근하여 필요한 정보를 가져오고 가공해서 Controller에게 다시 전달한다.

Service vs Controller 

나는 3주차까지 이 두 개의 기능을 헷갈렸었는 데 이를 제대로 구분하지 않는다면 유지보수 용이, 코드간 의존성 낮춤, 코드 중복 제거 등의 장점을 모두 잃게 된다. 각자의 역할만을 수행할 수 있도록 하는 코드를 작성하는 것이 핵심이다! (당연한 소리이긴 하지만..)


2. Controller Annotation

- @RestController

 

REST API를 담당하는 컨트롤러라는 것을 의미
method의 반환 결과를 JSON 형태로 반환이 Annotation이 적혀있는 Controller의 method는 HttpResponse로 바로 응답이 가능
(= 컨트롤러가 반환하는 MyLottoResponseDTO같은 자바 객체를 JSON 문자열로 자동 변환해서 응답해준다) 

 

@Controller vs @RestController

- @Controller : API와 view를 동시에 사용하는 경우에 사용. view(화면) return이 주목적
- @RestController : view가 필요없는 API만 지원하는 서비스에서 사용. 

 

 

 

  • @RequestMapping("/api"): 클래스 레벨에 붙여서 공통 주소를 지정 가능
  • @GetMapping("/my-lotto"): GET HTTP 메서드 요청을 처리
  • @PostMapping("/my-lotto"): POST HTTP 메서드 요청을 처리
  • @DeleteMapping("/my-lotto/{id}"): DELETE HTTP 메서드 요청을 처리

3. RESTful API

REST API는 Representational State Transfer의 약자로,  웹 기반 통신에서 자원을 정의하고 자원을 이름으로 구분하여 자원에 대한 주소를 사용하여 자원의 상태(정보)를 주고받는 방법을 의미한다.

 

이는 클라이언트와 서버 간의 통신을 위한 아키텍처 스타일 중 하나로, 웹의 기본적인 기술과 프로토콜을 사용한다. 주로 HTTP 프로토콜을 이용하여 데이터를 송수신하며, 자원의 상태를 CRUD(생성, 조회, 수정, 삭제) 작업을 통해 관리한다.

 

Create : 데이터 생성(POST)
Read : 데이터 조회(GET)
Update : 데이터 수정(PUT, PATCH)
Delete : 데이터 삭제(DELETE)

 

REST 구성 요소

REST는 다음과 같은 3가지로 구성이 되어있다. 

 

  1. 자원(Resource) : HTTP URI
  2. 자원에 대한 행위(Verb) : HTTP Method
  3. 자원에 대한 행위의 내용 (Representations) : HTTP Message Pay Load

 

REST의 특징

  1. Server-Client(서버-클라이언트 구조)
  2. Stateless(무상태)
  3. Cacheable(캐시 처리 가능)
  4. Layered System(계층화)
  5. Uniform Interface(인터페이스 일관성)

 


RESTful API를 구현함으로써, 웹 서비스는 다양한 플랫폼과 언어로 작성된 클라이언트 애플리케이션에 서비스를 제공할 수 있다. 이는 웹 애플리케이션, 모바일 애플리케이션, 다른 서버 애플리케이션 등 다양한 클라이언트에서 사용될 수 있는 효율적이고 확장 가능한 방법을 제공한다.

 

참고)

https://khj93.tistory.com/entry/네트워크-REST-API란-REST-RESTful이란

 

[네트워크] REST API란? REST, RESTful이란?

REST API란 REST를 기반으로 만들어진 API를 의미합니다. REST API를 알기 위해 REST부터 알아보도록 하겠습니다. REST란? REST(Representational State Transfer)의 약자로 자원을 이름으로 구분하여 해당 자원의 상

khj93.tistory.com


4. Controller Test

이번 테스트는 지금까지 해왔던 것처럼 LottoControllerTest 코드를 구현해서 테스트하는 것이 아닌 포스트맨을 활용할 예정이다! 왜냐하면 나는 웹서버랑 통신하는 API 컨트롤러를 구현할 것이고 요청을 보내면 그에 해당하는 응답이 오는지를 확인해야 하기 때문이다😋

https://wikidocs.net/237058

 

 

내가 구현하고 테스트 할 과정 

 

  1. [Client (Postman)]
    • GET "http://.../my-lotto" HTTP 요청을 보내기 
  2. Spring (Dispatcher Servlet)
    • 등록된 URL을 검색
    • LottoController.findAllMyLotto() 메서드를 찾기 
  3. LottoController
    • findAllMyLotto() 메서드가 호출됨
    • lottoService.findAllMyLotto()를 호출하여 작업을 위임
  4. LottoService
    • findAllMyLotto() 메서드가 호출됨
    • myLottoRepository.findAll()을 호출하여 DB 데이터를 요청
  5. MyLottoRepository
    • findAll() 메서드가 호출됨
    • DB(H2)에 SELECT * FROM MY_LOTTO 쿼리를 보내기
  6. H2 데이터베이스 
    • 테이블에서 데이터를 조회
    • List<MyLotto> (엔티티 리스트)를 반환
  7. MyLottoRepository
    • DB로부터 List<MyLotto>를 받아 LottoService에게 반환
  8. LottoService
    • List<MyLotto> (엔티티)를 받기
    •  .stream().map(MyLottoResponseDTO::from)... 로직을 수행
    • List<MyLottoResponseDTO> (DTO 리스트)로 변환하여 LottoController에게 반환
  9. LottoController
    • List<MyLottoResponseDTO> (DTO 리스트)를 받기
    • 이 DTO 리스트를 Spring에게 최종 반환
  10. Spring (Dispatcher Servlet)
    • @RestController 어노테이션을 확인
    • DTO 리스트를 JSON 문자열로 자동 변환
    • HTTP Response (응답)에 이 JSON을 담아 Client에게 보내기 
  11. Client (Postman)
    • [{"id":1, ...}, {"id":2, ...}] 형태의 JSON 데이터를 최종 응답으로 받기 

그리고 이 과정에서 H2 database는 인메모리(실행 시 마다 초기화됨)로 작동하므로 계속해서 초기화된다는 점에 유의해야 한다. 즉 테스트할 때마다 값을 넣어서 확인해야한다 ㅠㅠ 이 부분은 나중에 MySQL, MongoDB 등으로 교체가 가능하다. 교체를 한다면 계속해서 값을 저장해둘 수 있을 것이다!! 


4. LottoController 설계

LottoService의 8개 public 메서드를 기반으로 한 RESTful API 컨트롤러 설계를 해보았다. 모든 엔드포인트는 @RestController 클래스 내에 구현되며, /im-minji라는 공통 경로를 가진다고 가정했다! 

1. 로또 번호 생성 (Generate)

1-1. 랜덤 로또 번호 생성

  • 기능: DB 저장 없이, 1~45 범위의 중복 없는 6개 번호를 생성하여 반환
  • HTTP Method: GET (그저 랜덤 생성된 번호를 받아오는 것) 
  • API Endpoint: /lotto/random
  • 호출하는 서비스 메서드: lottoService.generateRandomLotto()
  • 요청 상세 (Request):
    • @PathVariable: 없음
    • @RequestBody: 없음 (보내야할 값이 없다) 
  • 예상 응답 (Response):
    • 성공 (200 OK): RandomLottoResponseDTO (JSON)
    • { "numbers": [1, 10, 20, 30, 40, 45] }

 

2. MyLotto

2-1.  MyLotto 저장 (Create)

  • 기능: (랜덤/수동) 번호와 이름을 받아 MyLotto를 DB에 저장
  • HTTP Method: POST (저장해달라고 요청을 보내는 것)
  • API Endpoint: /my-lotto
  • 호출하는 서비스 메서드: lottoService.saveToMyLotto(requestDTO)
  • 요청 상세 (Request): 
    • @RequestBody: MyLottoRequestDTO (JSON)
    • { "numbers": [1, 2, 3, 4, 5, 6], "lottoName": "나의 행운 번호" }
  • 예상 응답 (Response):
    • 성공 (201 Created): MyLottoResponseDTO (JSON) - 생성된 ID 포함
    • { "id": 1, "numbers": [1, 2, 3, 4, 5, 6], "lottoName": "나의 행운 번호" }

* 보내고 받을 때의 데이터는 DTO 형태이다. 

 

2-2. MyLotto 전체 조회 (Read)

  • 기능: 저장된 MyLotto 목록 전체를 조회 (ID 오름차순 정렬)
  • HTTP Method: GET (저장되어 있는 데이터를 불러오는 것) 
  • API Endpoint: /my-lotto
  • 호출하는 서비스 메서드: lottoService.findAllMyLotto()
  • 요청 상세 (Request):
    • @PathVariable: 없음
    • @RequestBody: 없음
  • 예상 응답 (Response):
    • 성공 (200 OK): List<MyLottoResponseDTO> (JSON Array)
    • [ { "id": 1, "numbers": [1, 2, 3, 4, 5, 6], "lottoName": "나의 행운 번호" }, { "id": 2, "numbers": [7, 8, 9, 10, 11, 12], "lottoName": "두번째 번호" } ]

 

2-3.  MyLotto 삭제 (Delete)

  • 기능: ID로 특정 MyLotto를 삭제
  • HTTP Method: DELETE
  • API Endpoint: /my-lotto/{myLottoId}
  • 호출하는 서비스 메서드: lottoService.deleteMyLotto(myLottoId)
  • 요청 상세 (Request):
    • @PathVariable: Long myLottoId (@PathVariable: URL 경로에서 ID 추출하는 방법)
    • @RequestBody: 없음
  • 응답 상세 (Response):
    • 성공 (200 OK 또는 204 No Content): Response Body 없음.
    • 실패 (404 Not Found): lottoService가 IllegalArgumentException을 발생시켰을 때 (존재하지 않는 ID)

 

3. PurchasedLotto

3-1. 수동으로 구매 로또 저장 (Create)

  • 기능: 사용자가 직접 입력한 번호(실물 티켓)를 PurchasedLotto로 저장
  • HTTP Method: POST (직접 입력한 번호를 저장해달라고 요청 보냄)
  • API Endpoint: /purchase/manual
  • 호출하는 서비스 메서드: lottoService.purchaseManualLotto(requestDTO)
  • 요청 상세 (Request):
    • @RequestBody: PurchasedLottoRequestDTO (JSON)
    • { "numbers": [1, 2, 3, 4, 5, 6] }
  • 응답 상세 (Response):
    • 성공 (201 Created): PurchasedLottoResponseDTO (JSON) - 생성된 ID와 구매 날짜 포함
    • { "id": 1, "numbers": [1, 2, 3, 4, 5, 6], "purchaseDate": "2025-11-12" }

 

3-2.  MyLotto 복사하여 구매 (Create)

  • 기능: MyLotto ID를 받아 PurchasedLotto로 복사 저장
  • HTTP Method: POST
  • API Endpoint: /purchase/from-my-lotto/{myLottoId}
  • 호출하는 서비스 메서드: lottoService.purchaseFromMyLotto(myLottoId)
  • 요청 상세 (Request):
    • @PathVariable: Long myLottoId (URL 경로에서 ID 추출)
    • @RequestBody: 없음
  • 응답 상세 (Response):
    • 성공 (201 Created): PurchasedLottoResponseDTO (JSON)
    • 실패 (404 Not Found): lottoService가 IllegalArgumentException을 발생시켰을 때 (존재하지 않는 myLottoId)

 

3-3.  PurchasedLotto 전체 조회 (Read)

  • 기능: 저장된 PurchasedLotto 목록 전체를 조회 (날짜 최신순 정렬)
  • HTTP Method: GET
  • API Endpoint: /api/purchase
  • 호출하는 서비스 메서드: lottoService.findAllPurchasedLotto()
  • 요청 상세 (Request):
    • @PathVariable: 없음
    • @RequestBody: 없음
  • 응답 상세 (Response):
    • 성공 (200 OK): List<PurchasedLottoResponseDTO> (JSON Array)
    • [ { "id": 2, "numbers": [...], "purchaseDate": "2025-11-13" }, { "id": 1, "numbers": [...], "purchaseDate": "2025-11-12" } ]

 

3-4. PurchasedLotto 삭제 (Delete)

  • 기능: ID로 특정 PurchasedLotto를 삭제
  • HTTP Method: DELETE
  • API Endpoint: /api/purchase/{purchaseId}
  • 호출하는 서비스 메서드: lottoService.deletePurchasedLotto(purchaseId)
  • 요청 상세 (Request):
    • @PathVariable: Long purchaseId (URL 경로에서 ID 추출)
    • @RequestBody: 없음
  • 응답 상세 (Response):
    • 성공 (200 OK 또는 204 No Content): Response Body 없음.
    • 실패 (404 Not Found): lottoService가 IllegalArgumentException을 발생시켰을 때 (존재하지 않는 ID)

오늘은 이렇게 서비스 계층을 마무리하고 컨트롤러를 설계해보았는 데 역시 설계가 가장 힘든 것 같다.. 개념으로만 공부했었던 API에 대해서 직접 설계해보니 이걸 다 생각하면서 처음부터 구현해야 하는건가..? 다들 어떻게 하는 거지 라는 생각이 들었던 것 같다. 지금은 하루앞만 보면서 개발하고 있지만 내가 이 오픈 미션을 무사히 끝내고 나면 앞을 내다볼 수 있는 개발자로 성장해있고 싶다. 지금과 같은 시간이 언제 주어질 지도 모르니 조금만 더 힘내서 남은 시간을 잘 마무리하고 싶다 아자자!!!

'우테코 > 오픈미션' 카테고리의 다른 글

오픈미션 11일차  (0) 2025.11.14
오픈미션 10일차  (0) 2025.11.13
오픈미션 8일차  (0) 2025.11.11
오픈미션 7일차  (0) 2025.11.10
오픈미션 6일차  (0) 2025.11.09