이번 팀 프로젝트에서 MSA 기반 e-commerce를 만들게 되었는데 MSA다 보니 다들 모듈이 나뉘어져 있는 상태여서 공통 모듈을 만들어 이 안에서 공통 dto와 예외 처리를 한번에 다 하기로 했다!
❓ ResponseEntity 대신 CommonResponse 사용하는 이유
- 일관된 응답 형식 제공 : 마이크로 서비스는 모두 다른 서비스를 담당하지만 클라이언트에게는 일관된 형식의 응답을 제공해야 함.
- API 버전 관리 용이 : 마이크로 서비스는 지속적으로 개발되기에 API 버전 관리가 중요하기에 필드 유지보수가 편리하도록 CommonResponseDto로 관리해야 함.
- 코드 재사용 : 반복적인 코드 작성을 줄이고 생산성을 높일 수 있음.
- 관심사 분리 : ResponseEntity는 상태와 헤더 값 등 통신과 관련 정보를 담당하는 반면 CommonResponse는 비즈니스 로직 관련 데이터를 담당.
common-module 은 다음과 같이 구성되어있다.
common-module
├── domain
│ ├── model
│ │ └── BaseEntity.java
│ │
├── exception
│ ├── CommonErrorCode.java
│ ├── CustomException.java
│ ├── ErrorCode.java
│ └── CommonErrorCode.java
│ │
├── presentation
│ ├── dto
│ │ └── CommonResponse.java
0️⃣ common-module 주입하기
현재 만들고 있는 payment-service 프로젝트에 common-module을 사용하려 할 때 어떻게 가져와야 할까?
common-module이라는 모듈을 따로 만들어준거기 때문에 의존성을 주입해줘야 사용 할 수 있다!
먼저 settings.gradle에 위 캡처와 같이 적어준다.
settings.gradle은 root 프로젝트와 같이 함께 빌드할 sub project의 정보를 추가해주는 곳이다.
그 다음 build.gradle에 위와 같이 의존성을 추가해주면 다른 클래스처럼 import 해서 사용 가능하다.
1️⃣ CommonResponse 사용하기
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public class CommonResponse<T> {
private HttpStatus status;
private String message;
private T data;
public static <T> CommonResponse<T> success(HttpStatus status, String message, T data) {
return new CommonResponse<>(status, message, data);
}
}
CommonResponse는 위와 같다.
클라이언트에게 3가지 값을 보내주게 되는데 HttpStatus(상태값), message, data(각자 모듈에서 보여주려는 반환값)이다.
payment-service의 결제 생성 API에 적용해서 만들어보면 다음과 같다.
반환값을 CommonResponse<T>로 해주고 T 안에 내가 반환하려는 값을 담은 responseDTO를 넣어준다.
※ T는 자바의 제네릭 Generic으로 Type을 뜻함.
위 코드에는 안 나와있지만 컨트롤러에서 @RequestBody로 받은 requestDTO를 payment-service에서는 DB 저장을 위해 Payment 엔티티로 변환해 처리하기 때문에 클라이언트에 반환할 때는 그걸 다시 responseDTO로 변환해줘야한다.
보통 서비스 단에서 변환하지만 아직 서비스를 구현하지 않았기에 컨트롤러 단에서 responseDTO 클래스 내에 from 메소드를 사용해서 변환해줬다.
포스트맨으로 API 테스트를 해보면 CommonResponse로 잘 반환됨을 알 수 있다!
2️⃣ ErrorResponse 사용하기
Exception 디렉토리에는 파일이 4개나 있는데 모두가 연관이 있기 때문에 하나씩 짚고 넘어가보자.
import java.util.HashMap;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@Builder
public class ErrorResponse {
private HttpStatus status;
private String message;
private Map<String, String> validation;
public static ErrorResponse error(ErrorCode errorCode) {
return ErrorResponse.builder()
.status(errorCode.getStatus())
.message(errorCode.getMessage()).build();
}
public void addValidation(String field, String errorMessage) {
if (validation == null) {
validation = new HashMap<>();
}
this.validation.put(field, errorMessage);
}
}
먼저 ErrorResponse는 위의 CommonResponse처럼 사용해주면 된다.
두 가지 메소드가 있는데 error 메소드는 에러 코드와 에러 메세지를 반환해주고 addValidation 메소드는 반환 값에 추가로 @Valid 검증 오류 정보를 더해주는 것이다.
import org.springframework.http.HttpStatus;
public interface ErrorCode {
String getMessage();
HttpStatus getStatus();
}
상태 값을 불러올 때 사용되는 ErrorCode는 인터페이스이고 이를 상속받아 구현한 것이 CommonErrorCode이다.
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
@AllArgsConstructor
public enum CommonErrorCode implements ErrorCode {
// 공통 서버 에러
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부에서 문제가 발생했습니다."),
// 데이터베이스 관련 에러
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 처리 중 문제가 발생했습니다."),
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT, "데이터 무결성 위반이 발생했습니다."),
// 외부 서비스 호출 관련 에러
EXTERNAL_SERVICE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "외부 서비스 호출 중 문제가 발생했습니다."),
TIMEOUT_ERROR(HttpStatus.GATEWAY_TIMEOUT, "서버 응답 시간이 초과되었습니다."),
// 인증 및 권한 관련 에러
AUTHENTICATION_ERROR(HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."),
AUTHORIZATION_ERROR(HttpStatus.FORBIDDEN, "해당 리소스에 접근 권한이 없습니다."),
// 잘못된 요청 에러
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "입력 값이 유효하지 않습니다."),
// 지원하지 않는 기능 관련 에러
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 요청 방식입니다."),
// 리소스 관련 에러
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."),
// 서비스 불가 에러
SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "현재 서비스 이용이 불가합니다.");
private final HttpStatus status;
private final String message;
@Override
public String getMessage() {
return message;
}
@Override
public HttpStatus getStatus() {
return status;
}
}
애플리케이션에 전반적으로 발생할 수 있는 공통적인 에러 코드를 정의한 enum이다.
각 에러는 httpStatus와 사용자에게 전달된 메세지를 포함한다.
ErrorResponse에서 getMessage( )와 getStatus( )를 호출해 에러 상태와 에러 메세지 값을 담는다.
위 CommonErrorCode는 CustomException에서도 사용 가능하다.
import lombok.Getter;
@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
private final String message;
public CustomException(ErrorCode errorCode) {
this.errorCode = errorCode;
this.message = errorCode.getMessage();
}
}
보통 CustomException을 만들 때는 RuntimeException을 상속받아 구현하게 된다.
이 클래스에서도 ErrorCode의 상태와 메세지를 사용하는걸 확인 할 수 있다.
throw new CustomException(CommonErrorCode.INTERNAL_SERVER_ERROR);
CustomException은 코드 상에서 위와 같이 사용하면 된다!
3️⃣ BaseEntity 사용하기
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity implements Serializable {
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@CreatedBy
@Column(name = "created_by")
private String createdBy;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;
@Column(name = "deleted_at")
protected LocalDateTime deletedAt;
@Column(name = "deleted_by")
protected String deletedBy;
@Column(name = "is_delete")
protected Boolean isDelete;
@PrePersist
protected void onCreate() {
if (this.isDelete == null) {
this.isDelete = false;
}
}
}
DB에 데이터를 저장, 갱신 및 삭제 할 때 사용되는 감사 로그는 모든 모듈의 엔티티가 필요로 한다.
따라서 BaseEntity로 만들어 놓고 각 모듈의 엔티티가 상속받아 사용하여 중복된 코드를 작성할 필요없게 한다.
- @EntityListeners(AuditingEntityListener.class) : 엔티티에 감사 기능을 적용하기 위한 어노테이션.
- @CreatedBy, @LastModifiedBy : 각각 생성자와 수정자의 ID를 저장할 필드를 지정.
- @CreatedDate, @LastModifiedDate: 각각 생성 시각과 수정 시각을 저장할 필드를 지정.
@PrePersist : 엔티티가 영속화되기 직전에 실행되어야 하는 메소드를 지정해주는 어노테이션. 즉, 데이터 베이스에 값을 넣기 전에 특정 로직을 수행하고 싶을 때 사용한다. 여기선 isDelete의 값을 false로 초기화 및 선언 해주는 용도로 사용되었다.
※ isDelete는 이 프로젝트에서 논리적 삭제를 처리하기 위한 boolean 값을 담는 칼럼이다.
이 외에도 JpaAudit 기능을 적용시키려면 @EnableJpaAuditing 해주고...AuditorAware 만들어야 하지만 그건 따로 정리하는거로...
Payment 엔티티에 다음과 같이 상속을 받아 사용하면 된다!
🐙 Github 레포지토리 >
https://github.com/pickple-ecommerce/backend
'Spring' 카테고리의 다른 글
서킷 브레이커 Resilience4j와 API 게이트웨이 이해하기 (1) | 2024.08.17 |
---|---|
클라이언트 사이드 로드 밸런싱 FeignClient와 Ribbon 이해하기 (0) | 2024.08.17 |
서비스 디스커버리 Eureka 서버 이해하기 및 실습 (0) | 2024.08.17 |
MSA와 Spring Cloud 이해하기 (0) | 2024.08.17 |
Spring의 RestTemplate 이해하기 (0) | 2024.08.17 |