assertThatThrownBy(() -> delegator.getPaymentInfo(...))...처럼 한 줄로 테스트를 작성할 수도 있지만, 다음과 같은 이유로 예외 처리 테스트를 나눠서 (예: catchThrowable() 방식 등) 쓰는 것이 더 낫다고 한다.
람다 내부에 여러 연산(searchQuery() 등)이 있을 경우 예외가 어디서 발생하는지 명확하지 않다고 판단한다
searchQuery()가 예외를 던질 수도 있고, getPaymentInfo(...)가 던질 수도 있기 때문에 Sonar는 테스트 신뢰성이 낮다고 경고한다.
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
//form-data, application/x-www-form-urlencoded에 적용
//POST 요청에서 setter가 없어도 되게끔
binder.initDirectFieldAccess(); // 컨트롤러 실행 전 바인딩 설정
}
@ModelAttribute
public void addGlobalAttributes(Model model) {
model.addAttribute("appName", "MyApp"); // 모든 뷰에 공통 속성 추가
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleAll(Exception ex) {
return ResponseEntity.status(500).body("서버 에러 발생");
}
}
순서
요청 수신 ↓ @InitBinder ↓ @ModelAttribute ↓ @RequestMapping (실제 컨트롤러 메서드 실행) ↓ @ExceptionHandler (예외 발생 시)
참고로
binder.initDirectFieldAccess();
@InitBinder는 주로 form-data, application/x-www-form-urlencoded에 적용된다.
JSON 요청이라면 @InitBinder는 영향이 없음
@RequestBody로 받는 JSON 요청은WebDataBinder가 아니라 Jackson이 처리
기존과 같이 new PageImpl()을 이용하여 Page<T> 객체를 내리는데 아래와 같은 에러(warn)가 발생한다.
Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
에러 발생 이유
Spring Boot 3.x
내부적으로 사용하는:
Spring Data Commons 3.x (2023.0.x 이상)
Spring Framework 6.x
이 조합에서 PageImpl을 그대로 @RestController에서 반환하면 Jackson 직렬화 구조가 불안정하다는 경고가 발생한다.
Spring 팀은 PageImpl<T>의 직렬화가 다음 문제를 가진다고 판단했다:
Jackson 직렬화 시 필드 순서나 구조가 변경될 수 있음
필드 이름이 내부 구현에 의존
API 스펙을 안정적으로 유지하기 어려움
그래서 Spring Data 팀이 공식적으로 PagedModel<T> 또는 DTO 변환을 권장하게 됨.
해결 방법
1. hate oas 를 사용하는 경우
PagedModel<T> 사용
return pagedResourcesAssembler.toModel(page);
: 결과가 PagedModel<EntityModel<T>> 형태가 되어 안정적인 JSON 구조를 보장
2. hate oas 사용 안하는 경우
1) 설정 추가로 해결
@Configuration
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
public class WebConfig {
}
2) 직접 커스텀 DTO를 만들어서 사용
public class PageResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
public PageResponse(List<T> content, int page, int size, long totalElements) {
this.content = content;
this.page = page;
this.size = size;
this.totalElements = totalElements;
}
// getters, setters
}
UUID는 Universally Unique Identifier의 줄임말로, 전 세계적으로 고유한 ID를 의미
UUID란?
128비트(16바이트) 크기의 고유 식별자
일반적인 문자열 표현: "faca5a65-3500-4855-be9f-455c6544f856" (총 36자)
보통 중복되지 않는 고유한 값을 자동으로 생성할 때 사용
32자리 16진수
하이픈 -으로 5개 구간으로 구분: 8-4-4-4-12
UUID uuid = UUID.randomUUID();
왜 UUID를 쓰나?
고유 식별
여러 시스템, 여러 서버에서 생성해도 충돌 위험이 거의 없음
데이터 병합 안정성
분산 시스템에서 병합해도 ID 충돌 없음
보안성(노출 방지)
숫자 증가 ID보다 추측 어렵고 노출에 강함
왜 이걸 키로 쓰지? 장단점은?
디비로 조회
## string to uuid
select unhex('faca5a6535004855be9f455c6544f856') from dual;
select * from poh where uuid = unhex('faca5a6535004855be9f455c6544f856') ;
환경: java17, spring boot 3.1.5, spring batch 5.0.3, mysql5.7
이슈:
아래 에러가 간헐적으로 발생하며 배치 실패. 재실행 시 정상 처리
Caused by: com.mysql.cj.jdbc.MysqlXAException: XAER_DUPID: The XID already exists
at com.mysql.cj.jdbc.MysqlXAConnection.mapXAExceptionFromSQLException(MysqlXAConnection.java:344)
at com.mysql.cj.jdbc.MysqlXAConnection.dispatchCommand(MysqlXAConnection.java:329)
at com.mysql.cj.jdbc.MysqlXAConnection.start(MysqlXAConnection.java:290)
at com.atomikos.datasource.xa.XAResourceTransaction.resume(XAResourceTransaction.java:217)
... 81 common frames omitted
Caused by: java.sql.SQLException: XAER_DUPID: The XID already exists
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:130)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:763)
at com.mysql.cj.jdbc.StatementImpl.execute(StatementImpl.java:648)
at com.mysql.cj.jdbc.MysqlXAConnection.dispatchCommand(MysqlXAConnection.java:323)
디비에서 xa recover; 로 검색 시 남아있는 트랜젝션 없는 것 확인
해결:
XID 중복이라 우선 XID가 어떻게 생성되는지 확인
XID: <gtrid>:<bqual>
gtrid (Global Transaction ID): 분산 트랜잭션을 식별하는 고유한 값
bqual (Branch Qualifier): 트랜잭션 내에서 개별 브랜치를 구분하는 값
16진수 → ASCII 디코딩 제공된 XID는 16진수(Hex)로 인코딩 되어 있음. 이를 ASCII 문자로 변환해야 함