Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f0b4c04
feat: 고객 CREATE, READ, DELETE 기능을 하는 RestController 구현
Hchanghyeon Jul 16, 2023
2f36332
feat: RestControllerAdvice를 활용하여 API ErrorHandling 기능 구현
Hchanghyeon Jul 16, 2023
71137a2
refactor: RestControllerAdive에 ResponseStatus 제외
Hchanghyeon Jul 16, 2023
c033680
feat: 바우처 CREATE, READ, DELETE 기능을 하는 RestController 구현
Hchanghyeon Jul 16, 2023
9896465
refactor: modelAttribute 어노테이션 추가
Hchanghyeon Jul 16, 2023
389b423
docs: 기능 명세 추가, 과제 설명 추가
Hchanghyeon Jul 16, 2023
583b133
docs: 미션 구현 테스트 사진 추가
Hchanghyeon Jul 16, 2023
2ebb955
Merge branch 'changhyeon/w3-1' into changhyeon/w3-2
Hchanghyeon Jul 16, 2023
fa56608
refactor: ModelAttribute제외
Hchanghyeon Jul 16, 2023
6e8dd1a
Merge branch 'changhyeon/w3-2' of https://github.com/prgrms-be-devcou…
Hchanghyeon Jul 16, 2023
5488b63
refactor: VoucherCreateDto Validation 수정
Hchanghyeon Jul 16, 2023
2d8a83a
refactor: 접근제어자, 기본 제어자 순서 변경
Hchanghyeon Jul 16, 2023
1f885b1
Merge branch 'changhyeon/w3-1' into changhyeon/w3-2
Hchanghyeon Jul 18, 2023
fa951d0
refactor: uri 경로, http 메서드 수정
Hchanghyeon Jul 18, 2023
51f51b3
refactor: ResponseEntity body, status 이용하기
Hchanghyeon Jul 18, 2023
686db55
Merge branch 'changhyeon/w3-2' of https://github.com/prgrms-be-devcou…
Hchanghyeon Jul 18, 2023
3655a01
Merge branch 'changhyeon/w3-1' into changhyeon/w3-2
Hchanghyeon Jul 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ Type list to list all vouchers.

**3-2**

- [ ] Spring MVC를 적용해서 JSON과 XML을 지원하는 REST API를 개발해보세요
- [ ] 전체 조회기능
- [ ] 조건별 조회기능 (바우처 생성기간 및 특정 할인타입별)
- [ ] 바우처 추가기능
- [ ] 바우처 삭제기능
- [ ] 바우처 아이디로 조회 기능
- [x] Spring MVC를 적용해서 JSON과 XML을 지원하는 REST API를 개발해보세요
- [x] 전체 조회기능
- [x] 조건별 조회기능 (바우처 생성기간 및 특정 할인타입별)
- [x] 바우처 추가기능
- [x] 바우처 삭제기능
- [x] 바우처 아이디로 조회 기능
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.programmers.springweekly.controller;

import com.programmers.springweekly.dto.customer.request.CustomerCreateRequest;
import com.programmers.springweekly.dto.customer.response.CustomerListResponse;
import com.programmers.springweekly.dto.customer.response.CustomerResponse;
import com.programmers.springweekly.service.CustomerService;
import com.programmers.springweekly.util.validator.CustomerValidator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
public class CustomerApiController {

private final CustomerService customerService;

@PostMapping
public ResponseEntity<CustomerResponse> save(@Validated CustomerCreateRequest customerCreateRequest) {
CustomerValidator.validateCustomer(
customerCreateRequest.getCustomerName(),
customerCreateRequest.getCustomerEmail(),
customerCreateRequest.getCustomerType()
);
Comment on lines +31 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom Validator 를 적용하면 한방에 처리되지 않을까요?


CustomerResponse customerResponse = customerService.save(customerCreateRequest);

return ResponseEntity.status(HttpStatus.CREATED).body(customerResponse);
}

@GetMapping
public ResponseEntity<List<CustomerResponse>> getFindAll() {
CustomerListResponse customerListResponse = customerService.findAll();

return ResponseEntity.ok(customerListResponse.getCustomerList());
}

@GetMapping("/{id}")
public ResponseEntity<CustomerResponse> findById(@PathVariable("id") UUID customerId) {
CustomerResponse customerResponse = customerService.findById(customerId);

return ResponseEntity.ok(customerResponse);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteById(@PathVariable("id") UUID customerId) {
boolean isExistCustomerId = customerService.existById(customerId);

if (!isExistCustomerId) {
throw new NoSuchElementException("사용자가 삭제하려는 아이디 " + customerId + "는 없는 ID입니다.");
}

customerService.deleteById(customerId);

return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.programmers.springweekly.controller;

import com.programmers.springweekly.dto.voucher.request.VoucherCreateRequest;
import com.programmers.springweekly.dto.voucher.response.VoucherListResponse;
import com.programmers.springweekly.dto.voucher.response.VoucherResponse;
import com.programmers.springweekly.service.VoucherService;
import com.programmers.springweekly.util.validator.VoucherValidator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/vouchers")
@RequiredArgsConstructor
public class VoucherApiController {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customer 쪽과 동일하게 리팩토링해주세요~~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 동일하게 반영했습니다!


private final VoucherService voucherService;

@PostMapping
public ResponseEntity<VoucherResponse> save(@Validated VoucherCreateRequest voucherCreateRequest) {
VoucherValidator.validateVoucher(
voucherCreateRequest.getVoucherType(),
String.valueOf(voucherCreateRequest.getDiscountAmount())
);

VoucherResponse voucherResponse = voucherService.save(voucherCreateRequest);

return ResponseEntity.status(HttpStatus.CREATED).body(voucherResponse);
}

@GetMapping
public ResponseEntity<List<VoucherResponse>> getFindAll() {
VoucherListResponse voucherListResponse = voucherService.findAll();

return ResponseEntity.ok(voucherListResponse.getVoucherList());
}

@GetMapping("/{id}")
public ResponseEntity<VoucherResponse> findById(@PathVariable("id") UUID voucherId) {
VoucherResponse voucherResponse = voucherService.findById(voucherId);

return ResponseEntity.ok(voucherResponse);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteById(@PathVariable("id") UUID voucherId) {
boolean isExistVoucherId = voucherService.existById(voucherId);

if (!isExistVoucherId) {
throw new NoSuchElementException("사용자가 삭제하려는 바우처 " + voucherId + "는 없는 ID입니다.");
}

voucherService.deleteById(voucherId);

return ResponseEntity.noContent().build();
}

}
32 changes: 29 additions & 3 deletions src/main/java/com/programmers/springweekly/docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,6 @@ blacklist

![스크린샷 2023-07-10 오전 12 23 51](https://github.com/Hchanghyeon/springboot-basic/assets/92444744/a0c5395b-0b0f-4f21-99ae-f9a5f36a7b03)



### Wallet

- NamedParameterJdbcTemplate을 이용
Expand Down Expand Up @@ -244,8 +242,36 @@ blacklist
![ezgif com-video-to-gif](https://github.com/prgrms-be-devcourse/springboot-basic/assets/92444744/c85de587-903e-493d-8fde-5e2d0ba3116b)

### 웹 동작 화면(에러)
![ezgif com-video-to-gif (1)](https://github.com/prgrms-be-devcourse/springboot-basic/assets/92444744/36e41e81-6d1f-4298-8527-43ee10d43e11)
![ezgif com-video-to-gif (1)](https://github.com/prgrms-be-devcourse/springboot-basic/assets/92444744/36e41e81-6d1f-4298-8527-43ee10d43e11

<hr>

### 3-2 JSON을 지원하는 바우처 관리페이지 REST API 구현

### 요약

- 고객, 바우처 CREATE, UPDATE, DELETE API Controller 구현
- RestControllerAdvice를 통한 Global 에외 처리
- 예외 발생시 ErrorResponseDto를 통해 에러 코드와 메시지 전달

### Voucher(api)

- 바우처 입력 기능
- 바우처 삭제 기능
- 바우처 전체 조회, 상세 조회 기능

### Customer(api)

- 고객 입력 기능
- 고객 삭제 기능
- 고객 전체 조회, 상세 조회 기능

### RestControllerAdvice

- Controller, Serivce, Repository 등 Controller 하위 단에서 발생한 예외를 전역으로 잡는 ExceptionHandler 클래스 생성
- 프로그램 내에서 발생할 것 같은 예외들을 모두 구분하여 로깅하고, 메세지를 ErrorResponseDto에 담아서 클라이언트에게 응답
- 예외 별 상태 코드 적용
- 발생할 것 같은 예외들을 모두 잡고 예상치 못한 예외가 발생했을 때 프로그램이 꺼지지 않도록 제일 하위에 최상위 Exception으로 예외 처리

### Postman Test
![ezgif com-video-to-gif (2)](https://github.com/prgrms-be-devcourse/springboot-basic/assets/92444744/b3d8ce89-78cd-44be-a16b-eb006f202f81)
14 changes: 14 additions & 0 deletions src/main/java/com/programmers/springweekly/dto/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.programmers.springweekly.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;


@Getter
@RequiredArgsConstructor
public class ErrorResponse {

private final int errorCode;
private final String errorMsg;

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.web.bind.annotation.ResponseStatus;

@Slf4j
@ControllerAdvice
// @ControllerAdvice
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RestControllerAdvice에서도 동일한 클래스에 대한 예외 처리를 하고 있어서 @ControllerAdvice를 주석처리하여 사용하지 않도록 했습니다!

동일한 클래스에 대해서 ControllerAdvice와 RestControllerAdvice 2개를 동시에 사용할 수 있는지 확인해보았을 때 basePackage 설정을 통해 할 수는 있었으나 현재 디렉토리 구조성 불가하다고 판단되어 주석처리로 미사용 상태로 두었습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ControllerAdvice와 RestControllerAdvice 차이점은 무엇일까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ControllerAdvice는 에러처리 후 반환 값으로 ViewResolver를 통해 페이지를 반환하지만 RestControllerAdvice는 ResponseBody를 이용해 객체를 반환합니다!

Controller와 RestController처럼 Rest 붙고 안붙고의 차이는 @ResponseBody 인 것 같습니다!

그래서 위 처럼 생각했을 때 RestController API로 작동하는 URI의 경우 RestControllerAdvice를 통해 동작할 것이고
Controller API로 작동하는 URI의 경우 ControllerAdvice로 작동해서 서로 다르게 처리할 줄 알았지만 테스트해보니 무조건 Controller로 동작되는 것을 확인할 수 있었습니다! 그래서 위와같이 // 주석처리하여 RestControllerAdivce가 동작할 수 있도록 했습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 제거?ㅎㅎ

public class GlobalExceptionHandler {

private static final String ERROR_MSG = "errorMsg";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.programmers.springweekly.exception;

import com.programmers.springweekly.dto.ErrorResponse;
import java.util.NoSuchElementException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class RestGlobalExceptionHandler {

@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<ErrorResponse> handleNoSuchElementException(NoSuchElementException e) {
log.warn("RestGlobalExceptionHandler - NoSuchElementException 발생, 찾는 데이터가 없음 {}", e.getMessage(), e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.warn("RestGlobalExceptionHandler - NoSuchElementException 발생, 찾는 데이터가 없음 {}", e.getMessage(), e);
log.warn("찾는 데이터가 없음", e);

ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e) {
log.error("RestGlobalExceptionHandler - NullPointerException 발생 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("RestGlobalExceptionHandler - IllegalArgumentException 발생, 클라이언트의 잘못된 입력 값 예상 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(IndexOutOfBoundsException.class)
public ResponseEntity<ErrorResponse> handleIndexOutOfBoundsException(IndexOutOfBoundsException e) {
log.error("RestGlobalExceptionHandler - IndexOutOfBoundsException 발생, 배열의 범위를 초과한 작업 예상 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<ErrorResponse> handleDuplicateKeyException(DuplicateKeyException e) {
log.error("RestGlobalExceptionHandler - DuplicateKeyException 발생, 유니크/중복 키 충돌 예상 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.CONFLICT.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
}

@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorResponse> handleDataAccessException(DataAccessException e) {
log.error("RestGlobalExceptionHandler - DataAccessException 발생, 데이터 접근 관련 예외 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("RestGlobalExceptionHandler - Exception 발생, 개발자가 잡지 못한 예외 {}", e.getMessage(), e);
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ public class VoucherService {

private final VoucherRepository voucherRepository;

public void save(VoucherCreateRequest voucherCreateRequest) {
public VoucherResponse save(VoucherCreateRequest voucherCreateRequest) {
Voucher voucher = VoucherFactory.createVoucher(
UUID.randomUUID(),
voucherCreateRequest.getVoucherType(),
voucherCreateRequest.getDiscountAmount()
);

voucherRepository.save(voucher);
Voucher resultVoucher = voucherRepository.save(voucher);

return new VoucherResponse(resultVoucher);
}

public void update(VoucherUpdateRequest voucherUpdateRequest) {
Expand Down