Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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,68 @@
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.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/customer")
Copy link

@hanjo8813 hanjo8813 Jul 16, 2023

Choose a reason for hiding this comment

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

rest api url 규칙을 읽어보시고, 반드시 지켜주세요!
구글링해서 아무거나 ... https://velog.io/@pjh612/REST-API-URI-%EA%B7%9C%EC%B9%99

아래 리뷰부터는 꼭 공부하시고 읽어주세요~


리소스는 복수형으로 써주세요

Suggested change
@RequestMapping("/api/customer")
@RequestMapping("/api/customers")

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

생각보다 정말 많은 규칙이 있는데 너무 마음대로 사용했던 것 같습니다..!

확실히 rest api uri 규칙을 지키면 간결하고 가독성도 좋은 것 같아요ㅎㅎ 말씀하신대로 다 적용해서 고쳐보도록 하겠습니다!

복수형으로 변경한 커밋 : fa951d0

@RequiredArgsConstructor
public class CustomerApiController {

private final CustomerService customerService;

@PostMapping("/save")
public ResponseEntity<CustomerResponse> save(@Validated CustomerCreateRequest customerCreateRequest) {
CustomerValidator.validateCustomer(
customerCreateRequest.getCustomerName(),
customerCreateRequest.getCustomerEmail(),
customerCreateRequest.getCustomerType()
);
Copy link

@hanjo8813 hanjo8813 Jul 16, 2023

Choose a reason for hiding this comment

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

검증 관련한 부분은 이전 PR리뷰를 참고해주세요.

rest api의 uri에는 동사 사용을 지양해주세요.

POST /api/customers

http 메소드와 리소스만으로도 어떤 api인지 바로 알아볼 수 있어야합니다,

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

검증 부분은 1차 PR에서 삭제처리하고 dto에서 검증할 수 있도록 처리했습니다!

말씀하신 uri에서 동사를 제외하고 메소드와 리소스만으로도 알 수 있도록 뒤에 /save부분을 삭제처리했습니다!

적용 커밋 : fa951d0

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 new ResponseEntity<>(customerResponse, HttpStatus.CREATED);
}

@GetMapping("/find")

Choose a reason for hiding this comment

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

여기도 ~

GET /api/customers

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

/find 삭제처리했습니다!

적용 커밋 : fa951d0

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

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

@GetMapping("/find/{id}")

Choose a reason for hiding this comment

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

여기도 ~

GET /api/customers/{id}

어떤 api인지 바로 읽히나요?

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

http메소드를 보고 보니까 오히려 find를 적었을 때보다 더 이해하기 쉬운 것 같아요!

감사합니다!

적용 커밋 : fa951d0

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

return ResponseEntity.ok(customerResponse);
}

@GetMapping("/delete/{id}")

Choose a reason for hiding this comment

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

Suggested change
@GetMapping("/delete/{id}")
@DeleteMapping("/{id}")

http method 종류에 대해 공부해봅시다!
https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A2%85%EB%A5%98-%ED%86%B5%EC%8B%A0-%EA%B3%BC%EC%A0%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

예전에 흘려듣기로 Delete와 Put메소드를 사용하면 보안에 좋지 않다라는 말을 들었었는데 찾아보니 제가 잘 안찾아보고 그냥 안썼던거네요ㅠ http method 종류를 직접 적용할 수 있도록 해보겠습니다!!

delete method 적용 커밋 : fa951d0

Choose a reason for hiding this comment

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

관련된 내용은 요거같네요
https://velog.io/@awdsza/HTTPMethod-PUTDELETE%EB%B3%B4%EC%95%88-%EC%9C%84%ED%97%98%EC%84%B1

http method의 멱등성에 대해서도 공부해보시길 바랍니다~

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,67 @@
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.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/voucher")
@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("/save")
public ResponseEntity<VoucherResponse> save(@Validated VoucherCreateRequest voucherCreateRequest) {
VoucherValidator.validateVoucher(
voucherCreateRequest.getVoucherType(),
String.valueOf(voucherCreateRequest.getDiscountAmount())
);

VoucherResponse voucherResponse = voucherService.save(voucherCreateRequest);

return new ResponseEntity<>(voucherResponse, HttpStatus.CREATED);

Choose a reason for hiding this comment

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

아래처럼 쓸수도 있습니다~~

Suggested change
return new ResponseEntity<>(voucherResponse, HttpStatus.CREATED);
return ResponseEntity.status(HttpStatus.CREATED).body(voucherResponse);

Copy link
Member Author

@Hchanghyeon Hchanghyeon Jul 18, 2023

Choose a reason for hiding this comment

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

헉 이런 방법이! 오히려 builder형식으로 쓰니까 통일성 있어서 더 좋은 것 같아요!

반영했습니다!

적용 커밋 : 51f51b3

}

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

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

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

return ResponseEntity.ok(voucherResponse);
}

@GetMapping("/delete/{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 @@ -6,12 +6,11 @@
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
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