Spring :: Project Structure에서도 언급했지만, 팀 단위 Back-End 개발에 앞서 Convention의 결정은 필수적이다. 개인별로 상이한 개발 경험과 수준을 생각했을 때, 다른 팀원이 작성한 코드를 이해하는 속도는 Convention의 유무에 따라 극명한 차이가 나타난다. 당장 협업을 진행하는 것 뿐만 아니라, 버그 픽스, 기능 확장 등 많은 영역에서 Convention은 주요하게 작용한다.
이에 따라 이번 프로젝트에서 역시 Convention을 적용하기로 하였으며, 적용된 Convention들 중 주요한 사항들을 기록해보고자 한다.
계층 간 데이터 교환을 하기 위해 사용하는 객체로, 로직을 가지지 않는 순수한 데이터 객체. Controller, Service Layer에서만 사용하며, Repository Layer에서는 사용을 지양한다.
Request
or Response
] + [Service
이름] + Dto
RequestSignUpDto
Service
이름] + Dto
SignUpDto
Controller
이후 Layer에서 발생하는 모든 Response는 ResponseEntity
를 사용해 반환한다.ResponseCode
의 네 자리 중 끝자리는 도메인 별로 분류한다.Member:0
Post:1
CollaboRequest:2
Music:3
Comment:4
Tag:5
Like:6
SSE:7
Filter
에서 진행되므로 해당 규칙에서 제외한다.✅ 사용 예시 : return new Response.toResponseEntity(LOGIN_USER_SUCCESS);
message
와 StatusCode
, CustomHttpStatus
, DTO
를 HTTP의 Body에 넣고 ResponseEntity
를 구성하기 위해 사용한다.
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;
@Getter
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SuccessResponse<T> {
private final Integer customHttpStatus;
private final String message;
private T data;
public static<T> ResponseEntity<SuccessResponse<T>> toResponseEntity(SucessCode sucessCode, T data){
return ResponseEntity
.status(sucessCode.getHttpStatus())
.body(SuccessResponse.<T>builder()
.customHttpStatus(sucessCode.getCustomHttpStatusCode())
.message(sucessCode.getMessage())
.data(data)
.build());
}
}
message
와 HttpStatusCode
를 정의한 Enum. 주로 Controller
에서 사용.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
// Domain이 커지게 될 경우, 해당 객체를 Abstract로 선언 후 Domain별 extend
@Getter
@AllArgsConstructor
public enum SucessCode {
// Member
SIGNUP_MEMBER(HttpStatus.OK, "회원 가입 성공", 2000);
private final HttpStatus httpStatus;
private final String message;
private final Integer customHttpStatusCode;
}
✅ 사용 예시 : throw new DuplicationException(MEMBER, SERVICE, DUPLICATED_EMAIL, "Email : " + email);
ExceptionResponse의 핵심은 log4j
를 통해 Exception에 관련된 Log를 남길 때,
필요한 정보를 얼마나 간결하게 담아내는가?였다. Exception의 원인 파악을 위해서는 1)도메인
, 2)계층
, 3)ExceptionCode
, 4)Exception의 원인이된 Value
총 4가지 정보로 충분할 것이라 판단했고, CustomException에 한해 적용했다.
프로그래머가 의도적으로 throw
한 Exception을 처리하기 위해 선언
package com.bluehair.hanghaefinalproject.common.exception;
import com.bluehair.hanghaefinalproject.common.response.error.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class DuplicationException extends IllegalArgumentException{
private Domain domain;
private Layer layer;
private ErrorCode errorCode;
private Object causeVariable;
}
message
와 HttpStatusCode
를 정의한 Enum. 주로 throw를 통해 Exception을 발생시키고자 할 때 사용한다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
// Domain이 커지게 될 경우, 해당 객체를 Abstract로 선언 후 Domain별 extend
@Getter
@AllArgsConstructor
public enum ErrorCode {
INVALID_EMAIL(HttpStatus.OK, "유효하지 않은 이메일입니다.", 4041);
private final HttpStatus httpStatus;
private final String message;
private final Integer customHttpStatusCode;
}
Message
와 StatusCode
, EnumName
을 HTTP의 Body에 담아 ResponseEntity를 구성하기 위해 사용한다.
package com.bluehair.hanghaefinalproject.common.response.error;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;
import java.time.LocalDateTime;
@Getter
@Builder
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final Integer customHttpStatus;
private final String message;
public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(ErrorResponse.builder()
.customHttpStatus(errorCode.getCustomHttpStatusCode())
.message(errorCode.getMessage())
.build());
}
}
ErrorResponse.java
, Domain.java
, Layer.java
에 의해 생성된 ResponseEntity
를 Exception이 발생했을 때 반환하기 위해 사용한다.
package com.bluehair.hanghaefinalproject.common.exception;
import com.bluehair.hanghaefinalproject.common.response.error.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleNotFoundException(NotFoundException e) {
log.error("NotFoundException throwed at " + e.getDomain() + "_"+ e.getLayer() + " : " + e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleNotAuthorizedMemberException(NotAuthorizedMemberException e) {
log.error("NotAuthorizedMemberException throwed at " + e.getDomain() + "_"+ e.getLayer() + " : " + e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleFormatException(FormatException e) {
log.error("FormatException throwed at " + e.getDomain() + "_"+ e.getLayer() + " : " + e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleDuplicationException(DuplicationException e) {
log.error("DuplicationException throwed at " + e.getDomain() + "_"+ e.getLayer() + " : " + e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleInvalidRequestException(InvalidRequestException e) {
log.error("InvalidException throwed at " + e.getDomain() + "_"+ e.getLayer() + " : " + e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
}
아래는 Swagger 3.0을 도입하면서 팀 노션에 작성했던 글(매뉴얼)이다. 코드에 대한 설명, Swagger 적용 후 사진 등 정보량이 많아 노션 내용을 그대로 공유한다.
한주님이 사용하셨던 컨벤션의 도움을 많이 받았다.
Tag Name | Description |
---|---|
feat | 새로운 기능 추가 |
fix | 버그 수정 |
design | CSS 등 사용자 UI 디자인 변경 |
!HOTFIX | 급하게 치명적인 버그를 고쳐야하는 경우 |
style | 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우 |
refactor | 프로덕션 코드 리팩토링 |
comment | 필요한 주석 추가 및 변경 |
docs | 문서 수정 |
test | 테스트 코드 |
chore | 빌드 업무 수정, 패키지 매니저 수정 |
rename | 파일 혹은 폴더명을 수정하거나 옮기는 작업 |
remove | 파일을 삭제하는 작업 |
커밋 메세지 앞에 Tag Name을 작성한다.
작업 전 Issue
를 만들고 시작
이슈 템플릿을 활용해 형식에 맞는 Issue
생성
---
name: "✔ Feature"
about: 새로 시작할 작업의 내용과 방향에 대해 작성해주세요.
title: ''
labels: ''
assignees: ''
---
<!--
✔ Feat:
-->
## I. Description
### 1. 내용
### 2. 방향
## II. Todo
- [ ] 할 일
- [ ] 할 일
## Etc
> 추가 내용(인용 등)
---
name: "\U0001F41E BUG"
about: 발생한 문제와 상황을 작성해주세요
title: ''
labels: ''
assignees: ''
---
<!--
🐞 BUG:
-->
## Description
버그 내용을 작성하세요.
## 재현 과정
어떤 상황에 버그가 발생하는지 작성하세요.
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## 예상하는 정상 작동
정상 작동시 어떤 결과가 나와야 하는지 작성해주세요.
## 스크린샷
가능하다면 스크린샷을 첨부해주세요.
## 버그 환경
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
## ETC
---
name: "\U0001F48E Refactor"
about: 기능 상의 변경이 없는 구조의 변경이나 코드스타일의 변경에 대해 작성해주세요.
title: ''
labels: ''
assignees: ''
---
<!--
💎 Refactor:
-->
## Description
설명을 입력해주세요.
## Before
변경전의 상황과 변경하려는 이유를 작성해주세요.
## After
변경후의 예상하는 구조를 작성해주세요.
## Todo
- [ ] todo
- [ ] todo
## ETC
PR 템플릿 형식에 맞게 작성
PR 보낸 후 슬랙에 코드 리뷰 요청
리뷰어 지정은 PR 보낸 사람이 알아서 지정
PR 머지는 보낸 사람이 머지
PR 상황 FE 에게 공유
## PR 체크사항
PR이 다음 사항을 만족하는지 확인해주세요.
<!--
체크하려면 괄호 안에 "x"를 입력하세요.
각 규칙은 Convention 문서에 있습니다.
PR 제목에 쓰는 prefix는 다음과 같습니다.
🚀 Release
🐞 BugFix
✨ Feat
📝 Doc
💎 Refactor
🔧 Chore
⏪️ Revert
🧪 Test
🎉 Init
-->
- [ ] 커밋 제목 규칙
- [ ] 커밋 메시지 작성 가이드라인
- [ ] 라벨, 담당자, 리뷰어 지정
## PR 타입
어떤 유형의 PR인지 체크해주세요.
<!-- 체크하려면 괄호 안에 "x"를 입력하세요. -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Documentation content changes
- [ ] Other... Please describe:
## PR 설명
내용을 적어주세요.
## 작업사항
- 내용을 적어주세요.
## 변경로직
- 내용을 적어주세요.
feature/#1
refactor/#10