Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/com/example/demo/controller/ArticleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import java.net.URI;
import java.util.List;

import com.example.demo.controller.Error.ErrorCode;
import com.example.demo.controller.Error.ErrorResponse;
import jakarta.validation.Valid;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -29,40 +34,41 @@ public ArticleController(ArticleService articleService) {

@GetMapping("/articles")
public ResponseEntity<List<ArticleResponse>> getArticles(
@RequestParam Long boardId
@RequestParam Long boardId
) {
List<ArticleResponse> response = articleService.getByBoardId(boardId);
return ResponseEntity.ok(response);
}

@GetMapping("/articles/{id}")
public ResponseEntity<ArticleResponse> getArticle(
@PathVariable Long id
@PathVariable Long id
) {
ArticleResponse response = articleService.getById(id);
return ResponseEntity.ok(response);
}

@PostMapping("/articles")
public ResponseEntity<ArticleResponse> crateArticle(
@RequestBody ArticleCreateRequest request
public ResponseEntity<?> createArticle(
@Valid
@RequestBody ArticleCreateRequest request
) {
ArticleResponse response = articleService.create(request);
return ResponseEntity.created(URI.create("/articles/" + response.id())).body(response);
}

@PutMapping("/articles/{id}")
public ResponseEntity<ArticleResponse> updateArticle(
@PathVariable Long id,
@RequestBody ArticleUpdateRequest request
public ResponseEntity<?> updateArticle(
@PathVariable Long id,
@RequestBody ArticleUpdateRequest request
) {
ArticleResponse response = articleService.update(id, request);
return ResponseEntity.ok(response);
}

@DeleteMapping("/articles/{id}")
public ResponseEntity<Void> updateArticle(
@PathVariable Long id
@PathVariable Long id
) {
articleService.delete(id);
return ResponseEntity.noContent().build();
Expand Down
18 changes: 12 additions & 6 deletions src/main/java/com/example/demo/controller/BoardController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import java.util.List;

import com.example.demo.controller.Error.ErrorCode;
import com.example.demo.controller.Error.ErrorResponse;
import jakarta.validation.Valid;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -32,29 +37,30 @@ public List<BoardResponse> getBoards() {

@GetMapping("/boards/{id}")
public BoardResponse getBoard(
@PathVariable Long id
@PathVariable Long id
) {
return boardService.getBoardById(id);
}

@PostMapping("/boards")
public BoardResponse createBoard(
@RequestBody BoardCreateRequest request
@Valid
@RequestBody BoardCreateRequest request
) {
return boardService.createBoard(request);
}

@PutMapping("/boards/{id}")
public BoardResponse updateBoard(
@PathVariable Long id,
@RequestBody BoardUpdateRequest updateRequest
@PathVariable Long id,
@RequestBody BoardUpdateRequest updateRequest
) {
return boardService.update(id, updateRequest);
}

@DeleteMapping("/boards/{id}")
public ResponseEntity<Void> deleteBoard(
@PathVariable Long id
public ResponseEntity<?> deleteBoard(
@PathVariable Long id
) {
boardService.deleteBoard(id);
return ResponseEntity.noContent().build();
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/example/demo/controller/Error/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.demo.controller.Error;

import org.springframework.http.HttpStatus;

public enum ErrorCode {

NOT_EXIST_ELEMENT(HttpStatus.NOT_FOUND, "존재하지 않는 요소입니다."),
EMAIL_DUPLICATION(HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."),

NULL_ELEMENT(HttpStatus.BAD_REQUEST, "null인 값이 존재합니다. 다시 입력해주세요."),

NOT_EXIST_USER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자 또는 게시판을 참조하고 있습니다."),

ARTICLE_EXIST(HttpStatus.BAD_REQUEST, "작성한 게시물이 존재합니다."),
BOARD_EXIST(HttpStatus.BAD_REQUEST, "해당 게시판에 작성된 게시물이 존재합니다.");


ErrorCode(HttpStatus status, String message) {
this.message = message;
this.status = status;

}

public HttpStatus getStatus() {
return status;
}


public String getMessage() {
return message;
}

private final HttpStatus status;

private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.example.demo.controller.Error;


import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

@RestControllerAdvice
public class ErrorExceptionHandler {


@ExceptionHandler(EmptyResultDataAccessException.class)
public ResponseEntity<ErrorResponse> EmptyResultDataAccessException(EmptyResultDataAccessException e) {
final ErrorResponse response = ErrorResponse.from(ErrorCode.NOT_EXIST_ELEMENT);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}


@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ErrorResponse> NullPointerException(NullPointerException e) {
final ErrorResponse response = ErrorResponse.from(ErrorCode.NULL_ELEMENT);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> DataIntegrityViolationException(DataIntegrityViolationException e) {
final ErrorResponse response = ErrorResponse.from(ErrorCode.NOT_EXIST_USER);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}


@ExceptionHandler(MethodArgumentNotValidException.class)
public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();

StringBuilder stringBuilder = new StringBuilder();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
stringBuilder.append("[");
stringBuilder.append(fieldError.getField());
stringBuilder.append("](은)는 ");
stringBuilder.append(fieldError.getDefaultMessage());
stringBuilder.append(" 입력된 값: [");
stringBuilder.append(fieldError.getRejectedValue());
stringBuilder.append("]\n");
}

return stringBuilder.toString();

}
}
34 changes: 34 additions & 0 deletions src/main/java/com/example/demo/controller/Error/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.demo.controller.Error;

import org.springframework.validation.FieldError;

import java.util.List;

public class ErrorResponse {
private String message;
private int status;

public ErrorResponse(final ErrorCode errorCode) {
this.status = errorCode.getStatus().value();
this.message = errorCode.getMessage();
}

public String getMessage() {
return message;
}

public int getStatus() {
return status;
}


public ErrorResponse(final ErrorCode errorCode, final List<FieldError> errors) {
this.status = errorCode.getStatus().value();
this.message = errorCode.getMessage();
}

public static ErrorResponse from(final ErrorCode errorCode) {
return new ErrorResponse(errorCode);
}

}
39 changes: 28 additions & 11 deletions src/main/java/com/example/demo/controller/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import java.util.List;

import com.example.demo.controller.Error.ErrorCode;
import com.example.demo.controller.Error.ErrorResponse;
import jakarta.validation.Valid;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -33,34 +39,45 @@ public ResponseEntity<List<MemberResponse>> getMembers() {

@GetMapping("/members/{id}")
public ResponseEntity<MemberResponse> getMember(
@PathVariable Long id
@PathVariable Long id
) {
MemberResponse response = memberService.getById(id);
return ResponseEntity.ok(response);
}

@PostMapping("/members")
public ResponseEntity<MemberResponse> create(
@RequestBody MemberCreateRequest request
@Valid
@RequestBody MemberCreateRequest request
) {
MemberResponse response = memberService.create(request);
return ResponseEntity.ok(response);
}

@PutMapping("/members/{id}")
public ResponseEntity<MemberResponse> updateMember(
@PathVariable Long id,
@RequestBody MemberUpdateRequest request
public ResponseEntity<?> updateMember(
@PathVariable Long id,
@RequestBody MemberUpdateRequest request
) {
MemberResponse response = memberService.update(id, request);
return ResponseEntity.ok(response);
try {
MemberResponse response = memberService.update(id, request);
return ResponseEntity.ok(response);
} catch (DuplicateKeyException e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

pr 반영해서 commit 했습니다! 하지만, 처음 try-catch를 대체할 수 있도록 RestControllerAdvice에서 코드를 구현하는 부분을 어떻게 바꿀수 있는지 고민해봐도 떠오르지 않아서 버디님이라면 어떤 방법으로 구현하실지 궁금합니다.

Service 레이어에서 해당 예외를 catch한 뒤 내가 핸들링하고싶은 상황에 대한 새로운 예외를 만들어서 그 예외를 반환해볼 수 있을 것 같아요

try-catch는 Controller의 책임인가? 도 한번 생각해보시면 좋을 것 같아요 :)

Copy link
Author

Choose a reason for hiding this comment

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

보통 controller는 최대한 간단하고 역할이 없게 만들고, Service 에서 try-catch를 사용한다고 들었습니다.

final ErrorResponse response = ErrorResponse.from(ErrorCode.EMAIL_DUPLICATION);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
}

@DeleteMapping("/members/{id}")
public ResponseEntity<Void> deleteMember(
@PathVariable Long id
public ResponseEntity<?> deleteMember(
@PathVariable Long id
) {
memberService.delete(id);
return ResponseEntity.noContent().build();
try {
memberService.delete(id);
return ResponseEntity.noContent().build();
} catch (DataIntegrityViolationException e) {
final ErrorResponse response = ErrorResponse.from(ErrorCode.ARTICLE_EXIST);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.example.demo.controller.dto.request;

import jakarta.validation.constraints.NotNull;

public record ArticleCreateRequest(
Copy link
Contributor

Choose a reason for hiding this comment

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

@JsonNaming 어노테이션을 활용하면 굳이 코드레벨에서 스네이크 케이스의 코드가 돌아다닐 필요가 없습니다.

Suggested change
public record ArticleCreateRequest(
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public record ArticleCreateRequest(

참고자료: https://www.baeldung.com/jackson-deserialize-snake-to-camel-case#use-jsonnaming-annotation

Copy link
Author

Choose a reason for hiding this comment

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

@JsonNaming이라는 어노테이션을 처음 보는데, 공부해서 코드에 실제 적용해 볼수 있도록 하겠습니다.

Long authorId,
Long boardId,
String title,
String description
@NotNull
Long author_id,
@NotNull
Long board_id,
@NotNull
String title,
@NotNull
String content
) {

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.example.demo.controller.dto.request;

import jakarta.validation.constraints.NotNull;

public record ArticleUpdateRequest(
Long boardId,
String title,
String description
@NotNull
Long boardId,
@NotNull
String title,
@NotNull
String description
) {

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.example.demo.controller.dto.request;

import jakarta.validation.constraints.NotNull;

public record BoardCreateRequest(
String name
@NotNull
String name
) {

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.example.demo.controller.dto.request;

public record BoardUpdateRequest(
String name
String name
) {

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.example.demo.controller.dto.request;

import jakarta.validation.constraints.NotNull;

public record MemberCreateRequest(
String name,
String email,
String password
@NotNull
String name,
@NotNull
String email,
@NotNull
String password
) {

}
Loading