프로젝트를 진행하면서 팀원들 간에 공통적으로 맞춰야 하는 작업이나 개발 효율을 위해 설정한 작업들에 대한 글입니다.
설정한 공통 작업들로는 코드 컨벤션, 공통 응답, 예외처리, Git 커밋 컨벤션 정의, Git 브랜치 전략 정의, 패키지 구조가 있습니다.
코드 컨벤션
hobbytip Java Style Guide는 우아한 테크코스 Java Style Guide를 기준으로 작성되었습니다.
우아한 테크코스 Java Style Guide와 다른 부분, 추가적인 부분을 깃허브 위키에 명시했습니다.
기존 내용과 다른 부분은 제목에 목차를 명시, 새로 추가된 부분은 ✅ 를 표기하였습니다.
https://github.com/hobbytrip/hobbytrip/wiki/BE-%EC%BB%A8%EB%B2%A4%EC%85%98
공통 응답
현재 프로젝트에서 프론트와 값을 주고받는 서비스는 총 5개입니다.
서비스별로 응답값 형식이 다르면 프론트분들이 혼란이 많을 거 같아
통일된 API response 형식이 필요하다고 생각했습니다.
응답 방식으로는 아래 2가지 방식을 고려했습니다.
1. 공통응답 Dto를 예외응답 Dto와 정상응답 Dto가 상속해서 사용하는 방식
2. 예외응답이든 정상응답이든 공통응답 Dto로 한 번에 쓰는 방식
2번 같은 경우 간편하지만 예외 응답, 정상 응답별로 커스텀하기 어려워 1번 방식을 선택했습니다.
Response 형식
Response는 크게 success, code, message, data 4가지 파트로 나뉘어 있습니다.
- success - Response 응답 성공 여부
- 성공하면 true, 실패하면 false
- code - Response 응답 코드
- 성공하면 0, 실패하면 실패 원인을 코드 번호로 표현
- message - Response 응답 메시지
- 성공하면 "Ok", 실패하면 실패 원인을 텍스트로 표현
- data - Response 응답 결과가 담길 공간
- 응답에 실패할 경우, data가 없는 채로 response가 돌아옴
{
"success": true,
"code": 0,
"message": "Ok",
"data": [
1,
2,
3
]
}
{
"success": false,
"code": 10001,
"message": "Requested resource is not found"
}
GeneralException
@Getter
public class GeneralException extends RuntimeException {
private final Code errorCode;
public GeneralException() {
super(Code.INTERNAL_ERROR.getMessage());
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(String message) {
super(Code.INTERNAL_ERROR.getMessage(message));
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(String message, Throwable cause) {
super(Code.INTERNAL_ERROR.getMessage(message), cause);
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(Throwable cause) {
super(Code.INTERNAL_ERROR.getMessage(cause));
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(Code errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, String message) {
super(errorCode.getMessage(message));
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, String message, Throwable cause) {
super(errorCode.getMessage(message), cause);
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, Throwable cause) {
super(errorCode.getMessage(cause), cause);
this.errorCode = errorCode;
}
}
Code.java
- Response Error 코드 번호와 메시지를 담당하는 코드
- enum을 통해 관리
@Getter
@RequiredArgsConstructor
public enum Code {
// 충돌 방지를 위한 Code format
// 50xxx: 상태관리 서비스
// 60xxx: 미디어 서비스
// 70xxx: 채팅 서비스
// ex) 채팅 서비스에서 발생한 잘못 요청한 에러코드면
// USER_NICKNAME_DUPLICATED(70000, HttpStatus.BAD_REQUEST, "Bad request"),
OK(0, HttpStatus.OK, "Ok"),
BAD_REQUEST(70000, HttpStatus.BAD_REQUEST, "Bad request"),
VALIDATION_ERROR(70001, HttpStatus.BAD_REQUEST, "Validation error"),
NOT_FOUND(70002, HttpStatus.NOT_FOUND, "Requested resource is not found"),
FILE_COUNT_EXCEEDED(70003, HttpStatus.BAD_REQUEST, "파일 업로드 가능 개수는 10개 이하 입니다."),
FILE_SIZE_EXCEEDED(70004, HttpStatus.BAD_REQUEST, "업로드 할 수 있는 파일의 최대 크기는 5MB 입니다."),
FILE_UPLOAD_FAILED(70005, HttpStatus.BAD_REQUEST, "파일 업로드에 실패했습니다."),
FILE_NOT_FOUND(70006, HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."),
INTERNAL_ERROR(70010, HttpStatus.INTERNAL_SERVER_ERROR, "Internal error"),
DATA_ACCESS_ERROR(70011, HttpStatus.INTERNAL_SERVER_ERROR, "Data access error"),
UNAUTHORIZED(70009, HttpStatus.UNAUTHORIZED, "User unauthorized");
private final Integer code;
private final HttpStatus httpStatus;
private final String message;
public String getMessage(Throwable e) {
return this.getMessage(this.getMessage() + " - " + e.getMessage());
// 결과 예시 - "Validation error - Reason why it isn't valid"
}
public String getMessage(String message) {
return Optional.ofNullable(message)
.filter(Predicate.not(String::isBlank))
.orElse(this.getMessage());
}
public static Code valueOf(HttpStatus httpStatus) {
if (httpStatus == null) {
throw new GeneralException("HttpStatus is null.");
}
return Arrays.stream(values())
.filter(errorCode -> errorCode.getHttpStatus() == httpStatus)
.findFirst()
.orElseGet(() -> {
if (httpStatus.is4xxClientError()) {
return Code.BAD_REQUEST;
} else if (httpStatus.is5xxServerError()) {
return Code.INTERNAL_ERROR;
} else {
return Code.OK;
}
});
}
@Override
public String toString() {
return String.format("%s (%d)", this.name(), this.getCode());
}
ResponseDto
- DataResponseDto와 ErrorResponseDto가 ResponseDto를 상속
@Getter
@RequiredArgsConstructor
public class ResponseDto {
private final Boolean success;
private final Integer code;
private final String message;
public static ResponseDto of(Boolean success, Code code) {
return new ResponseDto(success, code.getCode(), code.getMessage());
}
public static ResponseDto of(Boolean success, Code errorCode, Exception e) {
return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(e));
}
public static ResponseDto of(Boolean success, Code errorCode, String message) {
return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(message));
}
}
DataResponseDto
@Getter
public class DataResponseDto<T> extends ResponseDto {
private final T data;
private DataResponseDto(T data) {
super(true, Code.OK.getCode(), Code.OK.getMessage());
this.data = data;
}
private DataResponseDto(T data, String message) {
super(true, Code.OK.getCode(), message);
this.data = data;
}
public static <T> DataResponseDto<T> of(T data) {
return new DataResponseDto<>(data);
}
public static <T> DataResponseDto<T> of(T data, String message) {
return new DataResponseDto<>(data, message);
}
public static <T> DataResponseDto<T> empty() {
return new DataResponseDto<>(null);
}
}
ErrorResponseDto
public class ErrorResponseDto extends ResponseDto {
private ErrorResponseDto(Code errorCode) {
super(false, errorCode.getCode(), errorCode.getMessage());
}
private ErrorResponseDto(Code errorCode, Exception e) {
super(false, errorCode.getCode(), errorCode.getMessage(e));
}
private ErrorResponseDto(Code errorCode, String message) {
super(false, errorCode.getCode(), errorCode.getMessage(message));
}
public static ErrorResponseDto of(Code errorCode) {
return new ErrorResponseDto(errorCode);
}
public static ErrorResponseDto of(Code errorCode, Exception e) {
return new ErrorResponseDto(errorCode, e);
}
public static ErrorResponseDto of(Code errorCode, String message) {
return new ErrorResponseDto(errorCode, message);
}
}
위 형식은 아래 블로그를 참고해서 만들었습니다.
https://velog.io/@leeeeeyeon/Spring-boot-Response-%ED%98%95%EC%8B%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0
예외처리
예외 코드
예외 code란 우리가 지정해서 만드는 에러코드입니다.
맨 앞자리 번호는 서비스 번호를 의미합니다.
두 번째와 세 번째 번호는 비워뒀습니다.
네 번째와 다섯 번째는 비즈니스 로직에 따른 예외 코드입니다.
예를 들어 예외 코드가 70003이면 채팅 서비스에서 발생한 파일 업로드 가능 개수에 대한 에러라는 의미를 가집니다.
# 도메인(패키지) 별 에러코드 부여
api-gateway 10000~
user 20000~
community 30000~
notice 40000~
state 50000~
sig 60000~
chat 70000~
# 에러코드 10001
1 : 서비스 번호
2~3: 비워두기 (00)
45: 비즈니스로직에 따른 코드
API HTTP 응답코드 및 에러코드 명세
응답코드 에러 내용
400 BAD_REQUEST | 유효하지 않은 요청 내용 |
401 UNAUTORIZED | 사용자 인증 실패 |
403 FORBIDDEN | 사용자 권한 없음 (다른 사용자의 자원에 접근) |
404 NOT_FOUND | 조회를 요청한 자원이 존재하지 않음 |
409 CONFLICT | 요청이 이미 존재하는 자원과 충돌 |
500 INTERNAL_SERVER_ERROR | 서버 에러 (클라이언트 잘못으로 인해 발생한 에러가 아님) |
Git 커밋 컨벤션 정의
git 커밋 로그를 볼 때 빠르게 확인하기 위해 git 커밋 컨벤션을 정의했습니다.
아래는 저희 팀이 사용한 git 커밋 컨벤션입니다.
✨Git Convention✨
Feat: 새로운 기능 추가 (흐름 단위)
Fix: 버그 고침
Refactor: 코드 리팩토링
Test: 데스트 코드, 리팩토링 테스트 코드 추가
Comment: 필요한 추가 및 변경
Style: 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
Docs: 문서 수정
Rename: 파일명(or 폴더명) 수정한 경우
Remove: 코드(파일)의 삭제
Design: CSS 등 사용자 UI 디자인 변경
Chore: (코드의 수정 없이) 설정 변경
예시
Feat: "Add login API" // 타입: 제목
로그인 API 개발 // 본문
Resolves: #123 // 꼬리말 => 이슈 123을 해결했으며,
Ref: #456 이슈 456 를 참고해야하며,
Related to: #48, #45 현재 커밋에서 아직 이슈 48 과 45 가 해결되지 않았다.
Git 브랜치 전략 정의
효율적으로 프로젝트를 관리하기 위해 서비스별로 브랜치를 나눴습니다.
예를 들어 채팅 서비스인 경우 BE/chat과 같습니다.
각 서비스별 브랜치에서 일정 기능 단위마다 BE/dev로 pr를 날려 BE/dev에 merge 합니다.
브랜치 역할 규칙
Main | 운영 서버 배포 | 운영 서버에 배포되는 branch |
develop | 개발 서버 배포 | 개발 서버에 배포되는 branch |
dev | 신규 기능 추가 | Issue별 기능 단위로 merge 되는 branch |

패키지 구조
패키지 구성은 크게 레이어 계층형, 도메인형 이렇게 2 가지 유형이 있다고 합니다.
레이어 계층형인 경우 전체적인 구조를 빠르게 파악할 수 있는 장점이 있습니다.
단점으로는 디렉터리에 클래스들이 너무 많이 모이게 되는 점입니다.
아래는 과거 레이어 계층형으로 프로젝트를 했을 당시 controller 패키지에 모여있는 클래스들입니다.
보시다시피 굉장히 지저분해 보이고 네이밍을 잘못할 경우 어떤 도메인의 controller인지 파악하기 쉽지 않아 보입니다.

도메인형의 장점은 관련된 코드들이 응집해 있는 장점이 있습니다.
단점으로는 프로젝트에 대한 이해도가 낮을 경우 전체적인 구조를 파악하기 어려운 점이 있습니다.
이번 프로젝트에서는 레이어 계층 대신 도메인형 계층을 선택했습니다.
아래 패키지 구조는 현재 6/25일 기준 채팅 서비스의 패키지 구조입니다.
+---main
| +---generated
| +---java
| | \---capstone
| | \---chatservice
| | +---domain
| | | +---dm
| | | | +---controller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---exception
| | | | +---repository
| | | | \---service
| | | | +---command
| | | | | \---impl
| | | | \---query
| | | | \---impl
| | | +---emoji
| | | | +---contorller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---repository
| | | | \---service
| | | +---file
| | | | +---controller
| | | | | \---query
| | | | +---domain
| | | | +---dto
| | | | | \---response
| | | | +---exception
| | | | \---service
| | | | \---query
| | | +---forum
| | | | +---controller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---exception
| | | | +---repository
| | | | \---service
| | | | +---command
| | | | | \---impl
| | | | \---query
| | | | \---impl
| | | +---model
| | | \---server
| | | +---controller
| | | +---domain
| | | +---dto
| | | | +---request
| | | | \---response
| | | +---exception
| | | +---repository
| | | \---service
| | | +---command
| | | | \---impl
| | | \---query
| | | \---impl
| | +---global
| | | +---common
| | | | \---dto
| | | +---config
| | | | \---kafka
| | | | +---consumer
| | | | | +---chat
| | | | | +---community
| | | | | +---state
| | | | | \---voice
| | | | \---producer
| | | | +---alarm
| | | | +---chat
| | | | \---state
| | | +---exception
| | | | \---handler
| | | \---util
| | \---infra
| | +---client
| | +---kafka
| | | +---consumer
| | | | +---chat
| | | | +---community
| | | | | \---dto
| | | | +---state
| | | | | \---dto
| | | | \---voice
| | | | \---dto
| | | \---producer
| | | +---alarm
| | | | \---dto
| | | +---chat
| | | \---state
| | | \---dto
| | +---S3
| | \---websocket
| \---resources
| +---static
| \---templates
\---test
\---java
\---capstone
\---chatservice
Domain
model 디렉터리는 Domain Entity 객체들이 공통적으로 사용할 객체들로 구성됩니다.
대표적으로 Embeddable 객체, Enum 객체 등이 있습니다.
- controller: 컨트롤러 클래스들이 존재합니다. 외부 rest api로 구성되어 있습니다.
- domain : 도메인 엔티티에 대한 클래스로 구성됩니다. 특정 도메인에만 속하는 Embeddable, Enum 같은 클래스도 구성됩니다.
- dto : 주로 Request, Response 객체들로 구성됩니다.
- exception : 해당 도메인이 발생시키는 Exception으로 구성됩니다.
- service: service 디렉터리는 도메인 객체와 외부 영역을 연결해 주는 파사드와 같은 역할을 주로 담당하는 클래스로 구성됩니다. 비즈니스 로직이 있으며 저장소 계층을 사용합니다.
- repository: DB에 접근하는 모든 코드가 모여있습니다.
Global
global은 프로젝트 전방위적으로 사용되는 객체들로 구성됩니다. global로 지정한 이유는 common, util, config 등 프로젝트 전체에서 사용되는 클래스들이 global이라는 디렉터리에 모여 있는 것이 좋다고 생각했습니다.
- common : 공통으로 사용되는 Value 객체들로 구성됩니다. 페이징 처리를 위한 Request, 공통된 응답을 주는 Response 객체들이 있습니다.
- config : 스프링 각종 설정들로 구성됩니다.
- error : 예외 핸들링을 담당하는 클래스로 구성됩니다.
- util : 유틸성 클래스들이 위치합니다.
Infra
infra 디렉터리는 인프라스트럭처 관련된 코드들로 구성됩니다.
인프라스트럭처는 대표적으로 이메일 알림, SMS 알림 등 외부 서비스에 대한 코드들이 존재합니다.
그렇기 때문에 domain, global에 속하지 않습니다. global로 볼 수는 있지만 이 계층도 잘 관리해야 하는 대상이기에 별도의 디렉터리 했습니다.
채팅 서비스에서 해당 디렉터리에는 kafka producer, consumer 그리고 S3와 다른 서비스와 연동하기 위해 사용한 OpenFeign과 관련된 코드들이 존재합니다.
위 패키지 구조는 아래 블로그 글을 참고하였습니다.
'프로젝트 > FitTrip' 카테고리의 다른 글
트러블 슈팅 - 채팅 서비스 scalue out 문제 (0) | 2024.06.27 |
---|---|
개발 기록 - 개발 언어 및 기반 기술 조사 (0) | 2024.06.27 |
개발 기록 - MSA에서 EDA 설계하기 (0) | 2024.06.26 |
개발 기록 - 그라운드 룰 (0) | 2024.06.25 |
캡스톤 프로젝트 FitTrip 회고 (0) | 2024.06.25 |
프로젝트를 진행하면서 팀원들 간에 공통적으로 맞춰야 하는 작업이나 개발 효율을 위해 설정한 작업들에 대한 글입니다.
설정한 공통 작업들로는 코드 컨벤션, 공통 응답, 예외처리, Git 커밋 컨벤션 정의, Git 브랜치 전략 정의, 패키지 구조가 있습니다.
코드 컨벤션
hobbytip Java Style Guide는 우아한 테크코스 Java Style Guide를 기준으로 작성되었습니다.
우아한 테크코스 Java Style Guide와 다른 부분, 추가적인 부분을 깃허브 위키에 명시했습니다.
기존 내용과 다른 부분은 제목에 목차를 명시, 새로 추가된 부분은 ✅ 를 표기하였습니다.
https://github.com/hobbytrip/hobbytrip/wiki/BE-%EC%BB%A8%EB%B2%A4%EC%85%98
공통 응답
현재 프로젝트에서 프론트와 값을 주고받는 서비스는 총 5개입니다.
서비스별로 응답값 형식이 다르면 프론트분들이 혼란이 많을 거 같아
통일된 API response 형식이 필요하다고 생각했습니다.
응답 방식으로는 아래 2가지 방식을 고려했습니다.
1. 공통응답 Dto를 예외응답 Dto와 정상응답 Dto가 상속해서 사용하는 방식
2. 예외응답이든 정상응답이든 공통응답 Dto로 한 번에 쓰는 방식
2번 같은 경우 간편하지만 예외 응답, 정상 응답별로 커스텀하기 어려워 1번 방식을 선택했습니다.
Response 형식
Response는 크게 success, code, message, data 4가지 파트로 나뉘어 있습니다.
- success - Response 응답 성공 여부
- 성공하면 true, 실패하면 false
- code - Response 응답 코드
- 성공하면 0, 실패하면 실패 원인을 코드 번호로 표현
- message - Response 응답 메시지
- 성공하면 "Ok", 실패하면 실패 원인을 텍스트로 표현
- data - Response 응답 결과가 담길 공간
- 응답에 실패할 경우, data가 없는 채로 response가 돌아옴
{
"success": true,
"code": 0,
"message": "Ok",
"data": [
1,
2,
3
]
}
{
"success": false,
"code": 10001,
"message": "Requested resource is not found"
}
GeneralException
@Getter
public class GeneralException extends RuntimeException {
private final Code errorCode;
public GeneralException() {
super(Code.INTERNAL_ERROR.getMessage());
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(String message) {
super(Code.INTERNAL_ERROR.getMessage(message));
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(String message, Throwable cause) {
super(Code.INTERNAL_ERROR.getMessage(message), cause);
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(Throwable cause) {
super(Code.INTERNAL_ERROR.getMessage(cause));
this.errorCode = Code.INTERNAL_ERROR;
}
public GeneralException(Code errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, String message) {
super(errorCode.getMessage(message));
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, String message, Throwable cause) {
super(errorCode.getMessage(message), cause);
this.errorCode = errorCode;
}
public GeneralException(Code errorCode, Throwable cause) {
super(errorCode.getMessage(cause), cause);
this.errorCode = errorCode;
}
}
Code.java
- Response Error 코드 번호와 메시지를 담당하는 코드
- enum을 통해 관리
@Getter
@RequiredArgsConstructor
public enum Code {
// 충돌 방지를 위한 Code format
// 50xxx: 상태관리 서비스
// 60xxx: 미디어 서비스
// 70xxx: 채팅 서비스
// ex) 채팅 서비스에서 발생한 잘못 요청한 에러코드면
// USER_NICKNAME_DUPLICATED(70000, HttpStatus.BAD_REQUEST, "Bad request"),
OK(0, HttpStatus.OK, "Ok"),
BAD_REQUEST(70000, HttpStatus.BAD_REQUEST, "Bad request"),
VALIDATION_ERROR(70001, HttpStatus.BAD_REQUEST, "Validation error"),
NOT_FOUND(70002, HttpStatus.NOT_FOUND, "Requested resource is not found"),
FILE_COUNT_EXCEEDED(70003, HttpStatus.BAD_REQUEST, "파일 업로드 가능 개수는 10개 이하 입니다."),
FILE_SIZE_EXCEEDED(70004, HttpStatus.BAD_REQUEST, "업로드 할 수 있는 파일의 최대 크기는 5MB 입니다."),
FILE_UPLOAD_FAILED(70005, HttpStatus.BAD_REQUEST, "파일 업로드에 실패했습니다."),
FILE_NOT_FOUND(70006, HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."),
INTERNAL_ERROR(70010, HttpStatus.INTERNAL_SERVER_ERROR, "Internal error"),
DATA_ACCESS_ERROR(70011, HttpStatus.INTERNAL_SERVER_ERROR, "Data access error"),
UNAUTHORIZED(70009, HttpStatus.UNAUTHORIZED, "User unauthorized");
private final Integer code;
private final HttpStatus httpStatus;
private final String message;
public String getMessage(Throwable e) {
return this.getMessage(this.getMessage() + " - " + e.getMessage());
// 결과 예시 - "Validation error - Reason why it isn't valid"
}
public String getMessage(String message) {
return Optional.ofNullable(message)
.filter(Predicate.not(String::isBlank))
.orElse(this.getMessage());
}
public static Code valueOf(HttpStatus httpStatus) {
if (httpStatus == null) {
throw new GeneralException("HttpStatus is null.");
}
return Arrays.stream(values())
.filter(errorCode -> errorCode.getHttpStatus() == httpStatus)
.findFirst()
.orElseGet(() -> {
if (httpStatus.is4xxClientError()) {
return Code.BAD_REQUEST;
} else if (httpStatus.is5xxServerError()) {
return Code.INTERNAL_ERROR;
} else {
return Code.OK;
}
});
}
@Override
public String toString() {
return String.format("%s (%d)", this.name(), this.getCode());
}
ResponseDto
- DataResponseDto와 ErrorResponseDto가 ResponseDto를 상속
@Getter
@RequiredArgsConstructor
public class ResponseDto {
private final Boolean success;
private final Integer code;
private final String message;
public static ResponseDto of(Boolean success, Code code) {
return new ResponseDto(success, code.getCode(), code.getMessage());
}
public static ResponseDto of(Boolean success, Code errorCode, Exception e) {
return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(e));
}
public static ResponseDto of(Boolean success, Code errorCode, String message) {
return new ResponseDto(success, errorCode.getCode(), errorCode.getMessage(message));
}
}
DataResponseDto
@Getter
public class DataResponseDto<T> extends ResponseDto {
private final T data;
private DataResponseDto(T data) {
super(true, Code.OK.getCode(), Code.OK.getMessage());
this.data = data;
}
private DataResponseDto(T data, String message) {
super(true, Code.OK.getCode(), message);
this.data = data;
}
public static <T> DataResponseDto<T> of(T data) {
return new DataResponseDto<>(data);
}
public static <T> DataResponseDto<T> of(T data, String message) {
return new DataResponseDto<>(data, message);
}
public static <T> DataResponseDto<T> empty() {
return new DataResponseDto<>(null);
}
}
ErrorResponseDto
public class ErrorResponseDto extends ResponseDto {
private ErrorResponseDto(Code errorCode) {
super(false, errorCode.getCode(), errorCode.getMessage());
}
private ErrorResponseDto(Code errorCode, Exception e) {
super(false, errorCode.getCode(), errorCode.getMessage(e));
}
private ErrorResponseDto(Code errorCode, String message) {
super(false, errorCode.getCode(), errorCode.getMessage(message));
}
public static ErrorResponseDto of(Code errorCode) {
return new ErrorResponseDto(errorCode);
}
public static ErrorResponseDto of(Code errorCode, Exception e) {
return new ErrorResponseDto(errorCode, e);
}
public static ErrorResponseDto of(Code errorCode, String message) {
return new ErrorResponseDto(errorCode, message);
}
}
위 형식은 아래 블로그를 참고해서 만들었습니다.
https://velog.io/@leeeeeyeon/Spring-boot-Response-%ED%98%95%EC%8B%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0
예외처리
예외 코드
예외 code란 우리가 지정해서 만드는 에러코드입니다.
맨 앞자리 번호는 서비스 번호를 의미합니다.
두 번째와 세 번째 번호는 비워뒀습니다.
네 번째와 다섯 번째는 비즈니스 로직에 따른 예외 코드입니다.
예를 들어 예외 코드가 70003이면 채팅 서비스에서 발생한 파일 업로드 가능 개수에 대한 에러라는 의미를 가집니다.
# 도메인(패키지) 별 에러코드 부여
api-gateway 10000~
user 20000~
community 30000~
notice 40000~
state 50000~
sig 60000~
chat 70000~
# 에러코드 10001
1 : 서비스 번호
2~3: 비워두기 (00)
45: 비즈니스로직에 따른 코드
API HTTP 응답코드 및 에러코드 명세
응답코드 에러 내용
400 BAD_REQUEST | 유효하지 않은 요청 내용 |
401 UNAUTORIZED | 사용자 인증 실패 |
403 FORBIDDEN | 사용자 권한 없음 (다른 사용자의 자원에 접근) |
404 NOT_FOUND | 조회를 요청한 자원이 존재하지 않음 |
409 CONFLICT | 요청이 이미 존재하는 자원과 충돌 |
500 INTERNAL_SERVER_ERROR | 서버 에러 (클라이언트 잘못으로 인해 발생한 에러가 아님) |
Git 커밋 컨벤션 정의
git 커밋 로그를 볼 때 빠르게 확인하기 위해 git 커밋 컨벤션을 정의했습니다.
아래는 저희 팀이 사용한 git 커밋 컨벤션입니다.
✨Git Convention✨
Feat: 새로운 기능 추가 (흐름 단위)
Fix: 버그 고침
Refactor: 코드 리팩토링
Test: 데스트 코드, 리팩토링 테스트 코드 추가
Comment: 필요한 추가 및 변경
Style: 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
Docs: 문서 수정
Rename: 파일명(or 폴더명) 수정한 경우
Remove: 코드(파일)의 삭제
Design: CSS 등 사용자 UI 디자인 변경
Chore: (코드의 수정 없이) 설정 변경
예시
Feat: "Add login API" // 타입: 제목
로그인 API 개발 // 본문
Resolves: #123 // 꼬리말 => 이슈 123을 해결했으며,
Ref: #456 이슈 456 를 참고해야하며,
Related to: #48, #45 현재 커밋에서 아직 이슈 48 과 45 가 해결되지 않았다.
Git 브랜치 전략 정의
효율적으로 프로젝트를 관리하기 위해 서비스별로 브랜치를 나눴습니다.
예를 들어 채팅 서비스인 경우 BE/chat과 같습니다.
각 서비스별 브랜치에서 일정 기능 단위마다 BE/dev로 pr를 날려 BE/dev에 merge 합니다.
브랜치 역할 규칙
Main | 운영 서버 배포 | 운영 서버에 배포되는 branch |
develop | 개발 서버 배포 | 개발 서버에 배포되는 branch |
dev | 신규 기능 추가 | Issue별 기능 단위로 merge 되는 branch |

패키지 구조
패키지 구성은 크게 레이어 계층형, 도메인형 이렇게 2 가지 유형이 있다고 합니다.
레이어 계층형인 경우 전체적인 구조를 빠르게 파악할 수 있는 장점이 있습니다.
단점으로는 디렉터리에 클래스들이 너무 많이 모이게 되는 점입니다.
아래는 과거 레이어 계층형으로 프로젝트를 했을 당시 controller 패키지에 모여있는 클래스들입니다.
보시다시피 굉장히 지저분해 보이고 네이밍을 잘못할 경우 어떤 도메인의 controller인지 파악하기 쉽지 않아 보입니다.

도메인형의 장점은 관련된 코드들이 응집해 있는 장점이 있습니다.
단점으로는 프로젝트에 대한 이해도가 낮을 경우 전체적인 구조를 파악하기 어려운 점이 있습니다.
이번 프로젝트에서는 레이어 계층 대신 도메인형 계층을 선택했습니다.
아래 패키지 구조는 현재 6/25일 기준 채팅 서비스의 패키지 구조입니다.
+---main
| +---generated
| +---java
| | \---capstone
| | \---chatservice
| | +---domain
| | | +---dm
| | | | +---controller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---exception
| | | | +---repository
| | | | \---service
| | | | +---command
| | | | | \---impl
| | | | \---query
| | | | \---impl
| | | +---emoji
| | | | +---contorller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---repository
| | | | \---service
| | | +---file
| | | | +---controller
| | | | | \---query
| | | | +---domain
| | | | +---dto
| | | | | \---response
| | | | +---exception
| | | | \---service
| | | | \---query
| | | +---forum
| | | | +---controller
| | | | +---domain
| | | | +---dto
| | | | | +---request
| | | | | \---response
| | | | +---exception
| | | | +---repository
| | | | \---service
| | | | +---command
| | | | | \---impl
| | | | \---query
| | | | \---impl
| | | +---model
| | | \---server
| | | +---controller
| | | +---domain
| | | +---dto
| | | | +---request
| | | | \---response
| | | +---exception
| | | +---repository
| | | \---service
| | | +---command
| | | | \---impl
| | | \---query
| | | \---impl
| | +---global
| | | +---common
| | | | \---dto
| | | +---config
| | | | \---kafka
| | | | +---consumer
| | | | | +---chat
| | | | | +---community
| | | | | +---state
| | | | | \---voice
| | | | \---producer
| | | | +---alarm
| | | | +---chat
| | | | \---state
| | | +---exception
| | | | \---handler
| | | \---util
| | \---infra
| | +---client
| | +---kafka
| | | +---consumer
| | | | +---chat
| | | | +---community
| | | | | \---dto
| | | | +---state
| | | | | \---dto
| | | | \---voice
| | | | \---dto
| | | \---producer
| | | +---alarm
| | | | \---dto
| | | +---chat
| | | \---state
| | | \---dto
| | +---S3
| | \---websocket
| \---resources
| +---static
| \---templates
\---test
\---java
\---capstone
\---chatservice
Domain
model 디렉터리는 Domain Entity 객체들이 공통적으로 사용할 객체들로 구성됩니다.
대표적으로 Embeddable 객체, Enum 객체 등이 있습니다.
- controller: 컨트롤러 클래스들이 존재합니다. 외부 rest api로 구성되어 있습니다.
- domain : 도메인 엔티티에 대한 클래스로 구성됩니다. 특정 도메인에만 속하는 Embeddable, Enum 같은 클래스도 구성됩니다.
- dto : 주로 Request, Response 객체들로 구성됩니다.
- exception : 해당 도메인이 발생시키는 Exception으로 구성됩니다.
- service: service 디렉터리는 도메인 객체와 외부 영역을 연결해 주는 파사드와 같은 역할을 주로 담당하는 클래스로 구성됩니다. 비즈니스 로직이 있으며 저장소 계층을 사용합니다.
- repository: DB에 접근하는 모든 코드가 모여있습니다.
Global
global은 프로젝트 전방위적으로 사용되는 객체들로 구성됩니다. global로 지정한 이유는 common, util, config 등 프로젝트 전체에서 사용되는 클래스들이 global이라는 디렉터리에 모여 있는 것이 좋다고 생각했습니다.
- common : 공통으로 사용되는 Value 객체들로 구성됩니다. 페이징 처리를 위한 Request, 공통된 응답을 주는 Response 객체들이 있습니다.
- config : 스프링 각종 설정들로 구성됩니다.
- error : 예외 핸들링을 담당하는 클래스로 구성됩니다.
- util : 유틸성 클래스들이 위치합니다.
Infra
infra 디렉터리는 인프라스트럭처 관련된 코드들로 구성됩니다.
인프라스트럭처는 대표적으로 이메일 알림, SMS 알림 등 외부 서비스에 대한 코드들이 존재합니다.
그렇기 때문에 domain, global에 속하지 않습니다. global로 볼 수는 있지만 이 계층도 잘 관리해야 하는 대상이기에 별도의 디렉터리 했습니다.
채팅 서비스에서 해당 디렉터리에는 kafka producer, consumer 그리고 S3와 다른 서비스와 연동하기 위해 사용한 OpenFeign과 관련된 코드들이 존재합니다.
위 패키지 구조는 아래 블로그 글을 참고하였습니다.
'프로젝트 > FitTrip' 카테고리의 다른 글
트러블 슈팅 - 채팅 서비스 scalue out 문제 (0) | 2024.06.27 |
---|---|
개발 기록 - 개발 언어 및 기반 기술 조사 (0) | 2024.06.27 |
개발 기록 - MSA에서 EDA 설계하기 (0) | 2024.06.26 |
개발 기록 - 그라운드 룰 (0) | 2024.06.25 |
캡스톤 프로젝트 FitTrip 회고 (0) | 2024.06.25 |