| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- JDBC
- 클린아키텍처
- 커뮤니티서버
- TDD
- Transaction
- DDD
- JPA
- MVC요청플로우
- string
- 단위테스트
- 디스크i/o
- MVC
- 로그백
- index
- 사면초가
- OOP
- AOP
- 공통규약
- 냄새라도
- model
- 전전긍긍
- springsecurity
- 설정세팅
- SpringLegacy
- SEQUENCE
- 이벤트핸들러등록
- Ajax
- 이벤트핸들러this
- 공통기술
- 문제선택근거
- Today
- Total
개발이군고구마
[전전긍긍 설정세팅] 1. Rest API 규격 (#공통규약) 본문
2년 동안 나름 열심히 아등바등 거렸다고 생각했는데, 아뿔싸 그게 서해안 갯벌 위였을 줄이야.
경력자 주제에(?) 감히 어지간히 몰라야지, 이건 뭐 완전히 모르는 수준이라.....^.^
질문받는 쪽도 당황 하는 나도 민망, 하루 하루 쪽팔림과의 전쟁 중이다.

하지만 도망칠 수 없지!!!!!!
무지의 쪽팔림을 온전히 마주해보자!!!!!! 아...나는 어른이다!!!!
용의 (새끼)발톱 때 냄새라도 맡아보자는 목적으로 시작하는 "전전긍긍 시리즈" 1탄
1. ResponseEntity로 응답하는 경우

- ok 메서드를 사용하는 경우
@GetMapping("/detail")
public ResponseEntity<MemberDetailDto> detail() {
Member currentMember = MemberUtil.getMember();
return ResponseEntity.ok(memberService.getMemberById(currentMember.getId()).toMemberDetailDto());
}
제네릭 T - body에 담기게 될 데이터 값을 넘겨준다
- 생성자 객체를 생성하는 경우
@GetMapping("/detail")
public ResponseEntity<?> detail() {
Member currentMember = MemberUtil.getMember();
//...
return new ResponseEntity<>(dto, header, HttpStatus.OK);
}
바로 ResponseEntity 인스턴스를 생성해준다
☑️ 두 방법 모두 호출부로 전달되는 데이터는 같다.
T body -> 실제 body에 담겨서 보내지는 데이터
headers -> Http 응답메세지 헤더
status code (100-500) -> Http 응답메세지 startline에 응답코드
2. 커스텀 ApiResponse 만들기 - 정상응답
ResponseEntity 가 주는 정보 외에
비즈니스 의미, 에러 등에 대한 정보를 담고 싶다면 더 명시적이고 자세하게 알려주고 싶다면? → 래퍼 클래스 설계
이 경우는 응답값이 body에만 담겨서 보내진다.

[ApiResponse 클래스의 구조]
public record ApiResponse<T>(
boolean success,
String message,
T result
) {
public static <T> ApiResponse<T> ok(ResponseMessage responseMessage, T result) {
return new ApiResponse<>(true, responseMessage.getMessage(), result);
}
}
- 비즈니스 적으로 추가하고 싶은 정보들
- boolean success
- String message
- ok 메서드
- 무조건 success를 넣어 인스턴스를 만든다
- message는 인터페이스의 필드에서 가져온다
- 현재는 문자 그대로 보내주기 때문에 String message로 하여 상속 없이 가져올 수 있긴함
- T 제네릭엔 호출부로 넘길 data가 담기게 된다
👋🏻 여기서 잠깐 문법 학습
❓ record 클래스
- 자동 필드 캡슐화 (private 접근제어자 불필요)
- 생성자 자동생성 (멤버 필드의 값으로 컴파일링 시점에 생성자는 자동 생성)
- 클래스 생성을 간단하게 해주는 효과
❓ ExceptionMessage 인터페이스를 구현하여 처리하는 걸까?
- 유연성과 확장성 (enum이 여러개 관점으로 분리가 가능)
- 추상화와 결합도 낮추기 (다른 클래스로 교체가 가능함)
- 🚿 CLEAN Code
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
ExceptionMessage exceptionMessage = e.getExceptionMessage(); // 타입에 상관없이!
return new ResponseEntity<>(
new ErrorResponse(exceptionMessage.getMessage()),
exceptionMessage.getHttpStatus()
);
}
- 표현력 (그냥 message보단 ExceptionMessage로 쓰는게 명확함)
- 테스트와 유닛 테스트 용이성
+ 실제 사용 예시
@GetMapping("/api-response")
public ApiResponse<?> getApi() {
return ApiResponse.ok(() -> "성공", "OK");
}
result 부분에는 어떤 자료형이 들어와도 상관이 없음. 예제에는 String으로 반환함.
👋🏻 여기서 잠깐 문법 학습
❓ 인터페이스가 올 자리에 왠 함수?
- ResponseMessage 인터페이스에서 구현해야할 메서드가 1개이기 때문에
- 람다식을 사용하여 익명의 구현 객체를 자동으로 생성함
☑️ 호출부로 전달되는 값은 다음과 같다 (모두 다 body)
result : T
success : true
message : "성공"
여기엔 무조건 ok만 정의되어있는데, 만약 에러가 나면 그때는 어떻게 응답할 것인가?
에러도 규격화해서 사용을 해보자!
3. 커스텀 ExceptionResponse 만들기 - Exception

[Exception 계층구도]
- Error - 도저히 복구 불가! 바로 던져버린다
- Exception - 잡고 복구하자!
- RuntimeException: try catch 호출 안하면 놔둬도 됨, 예외 상황에서만 catch 호출해야 함
[Exception 처리 후 ExceptionResponse 구조]

[상세분석]
1) ExceptionHandler - 리스너
@Controller 나 @RestController 어노테이션이 붙어 있는 클래스에서 발생하는 예외를 잡아서 특정 메서드로 처리해주는 기능
@ExceptionHandler
public ResponseEntity<ExceptionResponse> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException exception
) {
log.error("{} : {}", exception.getClass(), extractErrorSpot(exception));
return buildExceptionResponse(GlobalExceptionMessage.ARGUMENT_TYPE_MISMATCH_MESSAGE);
}
- Enum 사용하여 에러메세지 간결하고 확실히 보여주기
- buildExceptionResponse : GlobalExceptionMessage로 값을 변환하기 위한 메서드
- CustomException은 필드로 이미 가지고 있어 생성자에서 사용하기 때문에 다시 넣어줄 필요는 없음
- Exception은 로그가 중요
- extractErrorSpot : log를 남기기 위한 레코드값 - exception의 정보를 받아 toString으로 변환해준다)
- extractExceptionLocation : custom Exception의 경우
2) BusinessException extend RundtimeException - CustomException의 뼈대
@Getter
public class BusinessException extends RuntimeException {
private final ExceptionMessage exceptionMessage;
private final String className;
private final String methodName;
private final int lineNumber;
public BusinessException(ExceptionMessage exceptionMessage) {
super(exceptionMessage.getMessage());
this.exceptionMessage = exceptionMessage;
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
this.className = stack[2].getClassName();
this.methodName = stack[2].getMethodName();
this.lineNumber = stack[2].getLineNumber();
}
public String extractExceptionLocation() {
return String.format("[%s][%s][%d]: ", className, methodName, lineNumber);
}
}
- 멤버 필드값 분석
- 메세지와 상태값을 한번에 보여주는 enum ExceptionMessage
- 로그를 만들때 사용하는 각종 정보
- extractExceptionLocation 는 왜?
- 로그를 남기기 위해서 custom exception을 만들었기 때문에 그때 보여질 className, methodName, lineNumber을 명확히 보여주기 위해 만든 메서드
- customException외에 다른 에러들은 exception 내부에서 보내는 값을 사용한다.
2-1) GlobalExceptionMessage implements ExceptionMessage - 공통 메세지 enum
Enum 클래스로 생성해두면 호출시 메세지의 값이 명확하게, 더 간결하게 인스턴스를 생성할 수 있게 된다.
@AllArgsConstructor
@Getter
public enum GlobalExceptionMessage implements ExceptionMessage {
INTERNAL_SERVER_ERROR_MESSAGE(INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다."),
private final HttpStatus httpStatus;
private final String message;
}
- enum으로 관리하게 될때의 이점
- 전역으로 생성된 enum의 name 값만 호출해도 필드로 주입된 HttpStatus와 message를 담은 인스턴스가 생성된다
- 한 곳에 두고 관리하기도 편함
- ExceptionMessage 인터페이스를 사용
3) ExceptionResponse - 최종 응답값 생성
public record ExceptionResponse(
boolean success,
String message
) {
public static ExceptionResponse fail(ExceptionMessage exceptionMessage) {
return new ExceptionResponse(false, exceptionMessage.getMessage());
}
}
- 비즈니스 적으로 추가하고 싶은 정보들
- boolean success
- fail 메서드
- 무조건 false 를 넣어 인스턴스를 만든다
- message는 인터페이스의 필드(여기서는 enum)에서 가져온다
+ 실제 사용 예시
@GetMapping("/business-exception")
public void occurBusinessException() {
throw new BusinessException(new ExceptionMessage() {
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_GATEWAY;
}
@Override
public String getMessage() {
return "에러 발생";
}
});
}
@GetMapping("/internal-server-exception")
public void occurInternalServerException() {
throw new InternalException("서버 에러 발생!");
}
@GetMapping("/no-endpoint")
public void occurNoResourceFoundException() throws NoResourceFoundException {
throw new NoResourceFoundException(HttpMethod.GET, "/no-endpoint");
}
☑️ 호출부로 전달되는 값은 다음과 같다 (모두 다 body)
success : false
message : GlobalExceptionMessage 인스턴스의 message 필드 "알 수 없는 서버 에러가 발생!"
👋🏻 여기서 잠깐 문법 학습
❓ InternalException과 NoResourceFoundException은 문자열이나 개별 파라미터(메시지, HttpMethod 등)를 직접 받을까?
- 자주 바뀌거나 다양하게 사용될 예외(비즈니스)는 ExceptionMessage 라는 enum 파일로 관리를 하고
- 상황이 고정적이고 명확한 예외(시스템, 자원)는 "별도의 래핑" 없이 바로 전달
❓ try{ ..} catch {} 문에서 잡지 못했지만 runtime 하위 영역에 에러가 정의된 경우?
하위 클래스들을 우선으로 해당하는 것이 없을 때 상위 에러를 띄운다.
❓ @Validation 에서 message를 정의한 경우 400 에러가 발생하면?
message를 정의했다면 default 메세지보다 우선한다. 참고자료
ARGUMENT_TYPE_MISMATCH_MESSAGE(BAD_REQUEST, "경로 변수 또는 쿼리 파라미터의 타입이 잘못되었습니다."),
@NotBlank(message = "제목은 필수입니다.")
@Size(min = 5, max = 100, message = "제목은 5자 이상 20자 이하로 입력해주세요.")
@Schema(description = "게시글 제목", example = "오늘의 공부 일지")
private String title;
@ExceptionHandler
public ResponseEntity<ExceptionResponse> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException exception
) {
log.error("{} : {}", exception.getClass(), extractErrorSpot(exception));
return buildExceptionResponse(GlobalExceptionMessage.ARGUMENT_TYPE_MISMATCH_MESSAGE);
}
- 에러는 느슨하게 가기로 했기 때문에, Enum에서 정한 message가 노출됨
- valid로 정의한 메세지를 띄우기 위해선 후처리가 필요한 부분
cf. 해당 코드는 코딩천재 팀원분께서 작성한 코드를 분석 정리한 글입니다.
'SERVER > Architecture' 카테고리의 다른 글
| [전전긍긍 설정세팅] 5. Profile별 설정 과 보안 파일 관리 (#공통규약) (0) | 2025.05.22 |
|---|---|
| [전전긍긍 설정세팅] 4. 어노테이션과 BaseEntity/QueryDSL (#공통규약 #공통기술) (5) | 2025.05.22 |
| [전전긍긍 설정세팅] 3. 2단계 파일 업로드 (#공통기술) (0) | 2025.05.21 |
| [전전긍긍 설정세팅] 2. Swagger API 명세 (#공통규약) (0) | 2025.05.19 |
| [사면초가 TDD/OOP] 1. 구현 원칙 확립 (나는 나의 역할 만을 담당한다) (5) | 2025.05.19 |