선행 실험final 필드에 전 인자 생성자만 둔 타입도 설정 없이 채워진다는 것을 보였다. 그래서 DTO 생성자 표준MapperResult에만 @Setter·@NoArgsConstructor를 허용한 근거가 흔들렸고, 이슈 #165 「MyBatis는 생성자만으로도 매핑된다」가 열렸다.

그런데 그 실험은 컬럼 순서와 생성자 파라미터 순서가 같았다. 생성자가 이름으로 받았는지 순서로 받았는지가 갈리지 않았다. 이슈도 「어느 조건에서 깨지는가」는 재지 않았다고 적었다. 이 실험은 순서만 바꿔 그 둘을 가른다.


주장 — 생성자 인자는 컬럼 순서로 들어가 같은 타입 두 컬럼이 조용히 뒤바뀐다

기본 설정에서 기본 생성자 없이 resultType 으로 매핑하면 생성자 인자는 컬럼 순서로 들어가,
같은 타입 두 컬럼의 SELECT 순서가 생성자 파라미터 순서와 다르면 예외 없이 뒤바뀐 값을 받는다.

근거 종류는 1(기술 사실)이다. 출발점은 MyBatis 문서의 argNameBasedConstructorAutoMapping(3.5.10+, 기본값 false) 설명과 3.5.17 소스를 읽은 추론이었다. 추론에는 한 가지가 더 붙어 있었다 — 생성자 뒤에 이름 기준 자동 매핑이 final 필드까지 덮어써서 최종 값은 맞아 보일 수 있다. 그래서 생성자가 받은 인자와 최종 getter 값을 따로 쟀다.


반증 조건 — (가)의 생성자 인자만으로 가른다

돌리기 전에 claim.md에 적었다.

  • refuted — (가) 실험 쿼리에서 생성자가 이름대로 맞는 인자를 받는다. 또는 예외를 낸다(주장은 「예외 없이」다)
  • supported — (가) 실험 쿼리에서 예외 없이 생성자가 뒤바뀐 인자를 받는다
  • inconclusive — (가) 대조군 쿼리가 실패하거나 생성자 인자가 기대값이 아니다

최종 getter 값과 (나)(다)(라)는 기록만 하고 판정에 쓰지 않는다. 주장이 「생성자 인자」에 대한 것이라서다.


설계 — 같은 행을 SELECT 순서 둘 × 결과 타입 넷으로 읽는다

행은 (id=1, name='alpha', code='C-7') 하나다. namecode를 둘 다 varchar로 둔 것이 핵심이다 — 타입이 다르면 타입 검사에서 걸려 뒤바뀜이 드러나 버린다.

대조군  select id, name, code   컬럼 순서 = 생성자 파라미터 순서
실험    select id, code, name   name · code 자리만 바꿨다

(가) 일반 class · final 필드 · 생성자 (Long id, String name, String code) 하나만
(나) record (Long id, String name, String code)
(다) (가)와 같은 클래스 · arg-name-based-constructor-auto-mapping=true
(라) private 기본 생성자 · final 아닌 private 필드 · setter 없음

생성자 안에서 받은 인자를 정적 기록기에 적고, 매핑이 끝난 뒤 getter 값을 따로 읽었다. (다)는 설정이 SqlSessionFactory 전역이라 앱을 한 번 더 띄웠고, 설정이 실제로 켜졌는지(isArgNameBasedConstructorAutoMapping())와 -parameters 컴파일로 파라미터 이름이 남았는지(isNamePresent())를 같이 찍었다.

환경은 선행 실험과 같다 — Spring Boot 3.5.16 · mybatis-spring-boot-starter 3.0.4 · 실제로 올라온 MyBatis 3.5.17. DB는 로컬 PostgreSQL에 실험 전용 데이터베이스를 만들고 지웠다.


결과 — 생성자는 순서대로 받았고, 그 뒤가 경우마다 달랐다

실험 쿼리(select id, code, name)만 옮긴다. 대조군 쿼리는 네 경우 모두 1,alpha,C-7로 맞았다.

                          생성자가 받은 인자    최종 getter 값
(가) class · final         1,C-7,alpha          1,alpha,C-7
(나) record                1,C-7,alpha          1,C-7,alpha
(다) 이름 기준 설정 true    1,alpha,C-7          1,alpha,C-7
(라) private 기본 생성자    (기본 생성자 호출)    1,alpha,C-7

어느 경우도 예외가 나지 않았고 생성자는 매번 한 번만 불렸다. (다)에서 설정 값은 true, 파라미터 이름은 id,name,code로 남아 있었다. 원본은 proofs/mybatis-constructor-column-order/result.md에 있다.

판정은 supported다. (가)의 생성자는 예외 없이 nameC-7을, codealpha를 받았다.


예상과 달랐던 점 — 없었다, 그래서 선행 실험이 왜 통과했는지가 설명된다

결과는 소스를 읽은 추론과 한 칸도 어긋나지 않았다. 대신 이 결과가 선행 실험의 「채워짐」을 다시 읽게 만든다.

(가)의 최종 getter 값은 맞았다. 생성자는 틀린 값을 받았는데 뒤에서 누군가 이름대로 고쳐 놓았다. 선행 실험이 getter만 봤다면 순서를 바꿨어도 「채워짐」으로 찍혔을 것이다. getter로 확인하는 테스트는 이 뒤바뀜을 못 잡는다. 생성자 안에서 검증하거나 파생값을 계산하는 코드는 틀린 값을 본다.

record는 덮어쓸 수 없어서 뒤바뀐 값이 그대로 남았다. 여기서도 예외는 없었다.

고쳐 놓은 주체가 이름 기준 자동 매핑의 필드 리플렉션이라는 것은 재지 않았다. 결과와 맞는 추론일 뿐이다. record에서 덮어쓰기를 시도하다 조용히 넘어갔는지도 재지 않았다.


이 결과가 표준에 남기는 것 — 예외 조항을 그냥 지우면 안 된다는 쪽을 지지한다

이슈 #165가 따질 안은 셋이다. 이 실험은 결정하지 않고, 각 안이 어느 경우에 해당하는지만 대 준다.

안 A — 예외 조항을 삭제한다(MapperResult도 생성자만). (가)·(나)에 해당한다. 기본 설정 그대로면 SELECT 순서가 생성자 순서와 어긋나는 순간 조용히 뒤바뀐다. class는 getter가 가려 주고 record는 그대로 틀린다. 이 결과는 안 A를 단독으로는 지지하지 않는다.

안 B — 기본 생성자만 허용하고 setter는 금지한다. (라)에 해당한다. private 기본 생성자와 setter 없는 필드로 순서와 무관하게 이름대로 채워졌다. 설정에 기대지 않는다. 대신 필드가 final이 아니고, 금지 목록의 구멍이 좁아질 뿐 닫히지는 않는다.

안 C — 이름 기준 설정을 강제한다. (다)에 해당한다. 설정 한 줄로 class와 record 모두 순서와 무관하게 맞았다. 금지 목록의 구멍도 닫힌다. 대신 설정이 빠진 프로젝트에서는 경고 없이 안 A의 뒤바뀜으로 돌아간다. -parameters 없이 켰을 때 어떻게 되는지는 재지 않았다.

정리하면 결과가 지지하는 것은 안 B와 안 C이고, 안 A는 안 C와 묶일 때만 안전하다. 어느 쪽인지는 #165에서 정한다.


정리

  • 기본 설정에서 생성자 자동 매핑은 컬럼 순서로 넣는다. 같은 타입 두 컬럼이 예외 없이 뒤바뀌었다
  • class는 최종 getter 값이 맞아 보여 뒤바뀜이 가려진다. record는 뒤바뀐 채 남는다
  • arg-name-based-constructor-auto-mapping=true에서는 순서와 무관하게 맞았다. private 기본 생성자 + setter 없음도 맞았다
  • 예외 조항을 그냥 지우는 안 A는 이 결과로 지지되지 않는다
  • 재지 않은 것 — getter를 고쳐 놓은 기전, -parameters 없이 이름 기준 설정을 켰을 때, 타입이 다른 컬럼끼리 뒤바뀔 때, 생성자가 여럿일 때, 다른 MyBatis 버전

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기