표준화된 응답 객체 설계
API 응답의 일관성을 위해 제네릭 기반의 결과 래퍼 클래스를 구현합니다. 상태 코드, 메시지, 실제 데이터를 포함하며, 다양한 생성 패턴을 정적 팩토리 메서드로 제공합니다.
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiResponse<T> {
private final int status;
private final String msg;
private final T payload;
public static <T> ApiResponse<T> ok(T payload) {
return new ApiResponse<>(HttpStatus.OK.value(), "처리 완료", payload);
}
public static <T> ApiResponse<T> ok() {
return ok(null);
}
public static <T> ApiResponse<T> error(int status, String msg) {
return new ApiResponse<>(status, msg, null);
}
}
비즈니스 예외 계층 구축
애플리케이션 특화 예외를 정의하여 예외별 처리 전략을 분리합니다. RuntimeException을 상속받아 트랜잭션 롤백이 가능하도록 설정하고, 클라이언트 전달용 상태 정보를 포함시킵니다.
@Getter
public class BizException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private final int errorStatus;
public BizException(ErrorCode code) {
super(code.getDescription());
this.errorStatus = code.getStatus();
}
public BizException(int status, String desc) {
super(desc);
this.errorStatus = status;
}
}
중앙집중식 예외 처리
@RestControllerAdvice를 활용한 글로벌 예외 핸들러에서 모든 예외를 포착하여 표준 응답 형식으로 변환합니다. 특정 예외 타입별로 세분화된 처리 로직을 구성할 수 있습니다.
@Slf4j
@RestControllerAdvice
public class ExceptionResolver {
@ExceptionHandler(BizException.class)
public ResponseEntity<ApiResponse<Void>> handleBizException(BizException ex) {
return ResponseEntity
.status(ex.getErrorStatus())
.body(ApiResponse.error(ex.getErrorStatus(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneralException(Exception ex) {
log.error("시스템 오류 발생: {}", ex.getMessage(), ex);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "서버 내부 오류"));
}
}
Vue3 환경 HTTP 클라이언트 모듈
Axios 인스턴스를 기반으로 인증 흐름, 에러 처리, 응답 변환을 통합 관리합니다. 모듈 내부에서 Pinia 스토어와 Vue Router를 연동하여 인증 상태에 따른 자동 분기를 처리합니다.
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
import { ElNotification } from 'element-plus'
const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
})
httpClient.interceptors.request.use(
(config) => {
const auth = useAuthStore()
if (auth.accessToken) {
config.headers.Authorization = `Bearer ${auth.accessToken}`
}
return config
},
(err) => Promise.reject(err)
)
httpClient.interceptors.response.use(
(res) => {
const result = res.data
if (result.status !== 200) {
ElNotification.error(result.msg || '요청 처리 중 오류')
return Promise.reject(new Error(result.msg))
}
return result.payload
},
(err) => {
const { response } = err
if (response?.status === 401) {
const router = useRouter()
router.push('/signin')
}
ElNotification.error(err.message || '네트워크 연결 실패')
return Promise.reject(err)
}
)
export default httpClient
UniApp 크로스 플랫폼 요청 유틸리티
UniApp의 uni.request API를 Promise 기반으로 래핑하며, 환경별 엔드포인트 분기와 토큰 갱신, 오류 피드백을 통합합니다.
const API_ENDPOINT = import.meta.env.DEV
? 'http://127.0.0.1:8080'
: 'https://api.production.com'
const DEFAULT_HEADER = {
'Content-Type': 'application/json;charset=UTF-8'
}
function createRequest() {
const userStore = getUserStore()
return function request(config) {
const { path, method = 'GET', body = {}, customHeader = {} } = config
const headers = {
...DEFAULT_HEADER,
...customHeader
}
if (userStore.token) {
headers.Authorization = `Token ${userStore.token}`
}
return new Promise((resolve, reject) => {
uni.request({
url: `${API_ENDPOINT}${path}`,
method,
data: body,
header: headers,
timeout: 8000,
success: (resp) => {
const { statusCode, data: respData } = resp
const { code, data, message } = respData || {}
if (statusCode === 200 && code === 200) {
resolve(data)
return
}
const errMsg = message || `응답 오류 (코드: ${code || statusCode})`
uni.showToast({ title: errMsg, icon: 'none' })
reject(new Error(errMsg))
},
fail: (err) => {
const isTimeout = err.errMsg?.includes('timeout')
uni.showToast({
title: isTimeout ? '요청 시간 초과' : '네트워크 연결 확인 필요',
icon: 'none',
duration: 2500
})
reject(err)
}
})
})
}
}
export default createRequest()