인스타그램 클론코딩 프로젝트의 backend 부분 github입니다.
1. Explore the Organization
2. Explore Front Repository
Report Bug
·
Request Feature
Table of Contents
Backend
- Spring Boot
- Spring Security
- Spring Data JPA
- Spring Data Redis
- Spring WebSocket
- Springfox Swagger UI
- JSON Web Token
- Querydsl
- MySQL
- Amazon Web Services
-
통일된 Error Response 객체
- Error Response JSON
{ "message": "Invalid Input Value", "status": 400, "errors": [ { "field": "name.last", "value": "", "reason": "must not be empty" }, { "field": "name.first", "value": "", "reason": "must not be empty" } ], "code": "C001" }
- message : 에러에 대한 message를 작성합니다.
- status : http status code를 작성합니다.
- errors : 요청 값에 대한 field, value, reason 작성합니다. 일반적으로 @Validated 어노테이션으로 Bean Validation에 대한 검증을 진행 합니다.
- 만약 errors에 binding된 결과가 없을 경우 null이 아니라 빈 배열 []을 응답합니다.
- code : 에러에 할당되는 유니크한 코드 값입니다.
- Error Response 객체
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ErrorResponse { private String message; private int status; private List<FieldError> errors; private String code; ... @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public static class FieldError { private String field; private String value; private String reason; ... } }
- Error Response JSON
-
Error Code 정의
public enum ErrorCode { // Common INVALID_INPUT_VALUE(400, "C001", " Invalid Input Value"), METHOD_NOT_ALLOWED(405, "C002", " Invalid Input Value"), .... HANDLE_ACCESS_DENIED(403, "C006", "Access is Denied"), // Member EMAIL_DUPLICATION(400, "M001", "Email is Duplication"), LOGIN_INPUT_INVALID(400, "M002", "Login input is invalid"), ; private final String code; private final String message; private int status; ErrorCode(final int status, final String code, final String message) { this.status = status; this.message = message; this.code = code; } }
-
비즈니스 예외를 위한 최상위 BusinessException 클래스
@Getter public class BusinessException extends RuntimeException { private ErrorCode errorCode; private List<ErrorResponse.FieldError> errors = new ArrayList<>(); public BusinessException(String message, ErrorCode errorCode) { super(message); this.errorCode = errorCode; } public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } public BusinessException(ErrorCode errorCode, List<ErrorResponse.FieldError> errors) { super(errorCode.getMessage()); this.errors = errors; this.errorCode = errorCode; } }
- 모든 비지니스 예외는 BusinessException을 상속 받고, 하나의 BusinessException handler 메소드로 한 번에 처리합니다.
-
@RestControllerAdvice로 모든 예외를 핸들링
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler protected ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { final ErrorResponse response = ErrorResponse.of(INPUT_VALUE_INVALID, e.getParameterName()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException e) { final ErrorResponse response = ErrorResponse.of(INPUT_VALUE_INVALID, e.getConstraintViolations()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { final ErrorResponse response = ErrorResponse.of(INPUT_VALUE_INVALID, e.getBindingResult()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleBindException(BindException e) { final ErrorResponse response = ErrorResponse.of(INPUT_VALUE_INVALID, e.getBindingResult()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleMissingServletRequestPartException(MissingServletRequestPartException e) { final ErrorResponse response = ErrorResponse.of(INPUT_VALUE_INVALID, e.getRequestPartName()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { final ErrorResponse response = ErrorResponse.of(e); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { final ErrorResponse response = ErrorResponse.of(HTTP_MESSAGE_NOT_READABLE); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { final List<ErrorResponse.FieldError> errors = new ArrayList<>(); errors.add(new ErrorResponse.FieldError("http method", e.getMethod(), METHOD_NOT_ALLOWED.getMessage())); final ErrorResponse response = ErrorResponse.of(HTTP_HEADER_INVALID, errors); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { final ErrorCode errorCode = e.getErrorCode(); final ErrorResponse response = ErrorResponse.of(errorCode, e.getErrors()); return new ResponseEntity<>(response, BAD_REQUEST); } @ExceptionHandler protected ResponseEntity<ErrorResponse> handleException(Exception e) { final ErrorResponse response = ErrorResponse.of(INTERNAL_SERVER_ERROR); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } }
-
통일된 Result Response 객체
- Result Response JSON
{ "status": 200, "code": "M109", "message": "회원 이미지 변경에 성공하였습니다.", "data": { "status": "success", "imageUrl": "https://xxx.com/A.jpg" } }
- message : 결과에 대한 message를 작성합니다.
- status : http status code를 작성합니다.
- data : 결과 객체를 JSON 형태로 나타냅니다.
- code : 결과에 할당되는 유니크한 코드 값입니다.
- Result Respone 객체
@Getter public class ResultResponse { private int status; private String code; private String message; private Object data; public static ResultResponse of(ResultCode resultCode, Object data) { return new ResultResponse(resultCode, data); } public ResultResponse(ResultCode resultCode, Object data) { this.status = resultCode.getStatus(); this.code = resultCode.getCode(); this.message = resultCode.getMessage(); this.data = data; } }
- Result Response JSON
-
@RestController에서 통일된 응답 사용
@RestController @RequiredArgsConstructor public class PostController { private final PostService postService; @ApiOperation(value = "게시물 업로드", consumes = MULTIPART_FORM_DATA_VALUE) @PostMapping("/posts") public ResponseEntity<ResultResponse> createPost(@Validated @ModelAttribute PostUploadRequest request) { ... return ResponseEntity.ok(ResultResponse.of(CREATE_POST_SUCCESS, response)); } ... }
[Common]
- 소문자 사용
- 단어 임의로 축약 x
ex) register_date⭕ reg_date❌
- 동사는 능동태 사용
ex) register_date⭕ registered_date❌
- 이름을 구성하는 각각의 단어를
underscore(_)
로 연결 (snake case)
[Table]
- 복수형 사용
- 교차 테이블의 이름에 사용할 수 있는 직관적인 단어가 없다면, 각 테이블의 이름을
_and_
또는_has_
로 연결ex)
- 복수형:
articles
,movies
- 약어도 예외 없이 소문자 & underscore 연결:
vip_members
- 교차 테이블 연결:
articles_and_movies
- 복수형:
[Column]
- PK는
테이블 명 단수형_id
으로 사용ex)
article_id
- FK는 부모 테이블의 PK 이름을 그대로 사용
- self 참조인 경우, PK 이름 앞에 적절한 접두어 사용
- boolean 유형의 컬럼은
_flag
접미어 사용 - date, datetime 유형의 컬럼은
_date
접미어 사용
[Index]
- 접두어
- unique index:
uix
- spatial index:
six
- index:
nix
- unique index:
접두어-테이블 명-컬럼 명
ex)
uix-accounts-login_email
[Reference]
└── src
├── main
│ ├── java
│ │ └── cloneproject.instagram
│ │ ├── domain
│ │ │ ├── member
│ │ │ │ ├── controller
│ │ │ │ ├── service
│ │ │ │ ├── repository
│ │ │ │ │ ├── jdbc
│ │ │ │ │ └── querydsl
│ │ │ │ ├── entity
│ │ │ │ ├── dto
│ │ │ │ ├── vo
│ │ │ │ └── exception
│ │ │ ├── feed
│ │ │ │ ├── controller
│ │ │ │ ├── service
│ │ │ │ ├── repository
│ │ │ │ │ ├── jdbc
│ │ │ │ │ └── querydsl
│ │ │ │ ├── entity
│ │ │ │ ├── dto
│ │ │ │ ├── vo
│ │ │ │ └── exception
│ │ │ ├── ...
│ │ ├── global
│ │ │ ├── config
│ │ │ │ ├── SwaggerConfig.java
│ │ │ │ ├── ...
│ │ │ │ └── security
│ │ │ ├── dto
│ │ │ ├── error
│ │ │ │ ├── ErrorResponse.java
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ ├── ErrorCode.java
│ │ │ │ └── exception
│ │ │ │ ├── BusinessException.java
│ │ │ │ ├── EntityNotFoundException.java
│ │ │ │ ├── ...
│ │ │ │ └── InvalidValueException.java
│ │ │ ├── result
│ │ │ │ ├── ResultResponse.java
│ │ │ │ └── ResultCode.java
│ │ │ ├── util
│ │ │ ├── validator
│ │ │ └── vo
│ │ └── infra
│ │ ├── aws
│ │ ├── geoip
│ │ └── email
│ └── resources
│ ├── application-dev.yml
│ ├── application-local.yml
│ ├── application-prod.yml
│ └── application.yml
Type: Subject
ex) Feat: 회원가입 API 추가
Description
Footer
ex) Resolves: #1, #2
- Type
- Feat: 기능 추가, 삭제, 변경
- Fix: 버그 수정
- Refactor: 코드 리팩토링
- Style: 코드 형식, 정렬 등의 변경. 동작에 영향 x
- Test: 테스트 코드 추가, 삭제 변경
- Docs: 문서 추가 삭제 변경. 코드 수정 x
- Etc: 위에 해당하지 않는 모든 변경
- Description
- 한 줄당 72자 이내로 작성
- 최대한 상세히 작성(why - what)
- Footer
- Resolve(s): Issue 해결 시 사용
- See Also: 참고할 Issue 있을 시 사용
- Rules
- 관련된 코드끼리 나누어 Commit
- 불필요한 Commit 지양
- 제목은 명령조로 작성
- Reference
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
seonpilKim 💻 |
bluetifulc 💻 |
JunhuiPark 💻 |
Distributed under the MIT License. See LICENSE.txt
for more information.
SeonPil Kim - [email protected]
Use this space to list resources you find helpful and would like to give credit to. I've included a few of my favorites to kick things off!