데이터가 null 이 되는 가장 흔한 이유
"SELECT 는 분명히 성공했어요. 콘솔에 SQL 도 정상으로 보이고요. 근데 객체에 데이터가 다 null 이에요!"
이 증상은 본 과정에서 가장 자주, 가장 오래 학생을 헤매게 하는 함정입니다. 한 번 만나면 평생 기억합니다.
DB 는 스네이크 케이스 (snake_case), 자바는 카멜 케이스 (camelCase) 를 씁니다. 둘이 자동으로 매핑되지 않아서 이 함정이 생깁니다.
| 규칙 | 이유 | 사용처 |
|---|---|---|
스네이크 케이스user_id | SQL 은 대소문자 구별 안 함. 단어 구분에 _ 가 자연스러움 | DB 컬럼·테이블, Python, Ruby |
카멜 케이스userId | 자바·자바스크립트의 표준 컨벤션 | 자바 변수·메서드 |
👉 두 진영이 각자 자기 컨벤션을 따라간 결과. 둘 다 옳음.
-- 일반 예시 (어떤 테이블이든 _ 가 들어간 컬럼이면 같은 함정)
CREATE TABLE sample (
id INT,
user_id VARCHAR(50),
created_at TIMESTAMP
);
INSERT INTO sample VALUES (1, 'hong', NOW());
// VO
public class Sample {
private int id;
private String userId; // 카멜
private LocalDateTime createdAt; // 카멜
}
// Mapper XML
<select id="selectOne" resultType="Sample">
SELECT * FROM sample WHERE id = #{id}
</select>
Sample s = mapper.selectOne(1);
System.out.println(s.getId()); // 1 ✓
System.out.println(s.getUserId()); // null ✗
System.out.println(s.getCreatedAt()); // null ✗
같은 SELECT 한 줄인데, id 만 정상이고 나머지는 모두 null. 다음 슬라이드에서 그 이유를 풀어봅니다.
위 코드에서 id 만 정상으로 매핑된 이유:
ididsetId(int)_ 가 들어간 컬럼만 함정에 빠집니다.
<!-- mybatis-config.xml -->
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
👉 한 번만 켜두면 — 이후 모든 매핑에서 user_id ↔ userId 가 자동 변환.
변환 규칙: _ 다음 글자를 대문자로. 첫 단어는 소문자.
user_id → userId
created_at → createdAt
is_active → isActive
last_login_at → lastLoginAt
USER_NAME → userName (대소문자 구분 없이)
<select id="selectOne" resultType="Sample">
SELECT
id,
user_id AS userId,
created_at AS createdAt
FROM sample
WHERE id = #{id}
</select>
👉 SQL 자체에서 컬럼명을 자바 카멜로 변경.
매 SELECT 에서 반복 작성. 컬럼이 많을수록 길어짐. 해법 ① 이 더 깔끔.
<resultMap id="sampleMap" type="Sample">
<id property="id" column="id" />
<result property="userId" column="user_id" />
<result property="createdAt" column="created_at" />
</resultMap>
<select id="selectOne" resultMap="sampleMap">
SELECT * FROM sample WHERE id = #{id}
</select>
👉 명시적 매핑. 복잡한 객체(중첩, 컬렉션) 매핑 시 필수. 본 과정에선 해법 ① 만 쓰면 충분.
| 해법 | 위치 | 장점 | 단점 |
|---|---|---|---|
| ① 글로벌 옵션 | mybatis-config.xml | 한 번만 설정. 모든 SELECT 자동 | 명시적이지 않음 |
| ② AS 별칭 | 각 SELECT | 명시적. 한눈에 보임 | 반복 작성 |
| ③ resultMap | 각 mapper | 완전 통제. 복잡 매핑 | 장황 |
👉 본 과정 권장: 해법 ① + 필요 시 ③ 조합.
mapUnderscoreToCamelCase 를 false 로 (또는 삭제)👉 한 번 깨뜨리고 고쳐보면 평생 안 잊습니다.
<insert id="insert">
INSERT INTO sample(user_id, created_at)
VALUES(#{userId}, #{createdAt})
</insert>
INSERT 의 #{userId} 는 자바 객체의 getter(getUserId()) 를 호출. 이 방향은 자바 카멜이 그대로.
즉 SELECT 만 매핑 함정이고, INSERT 는 자바 → DB 방향이라 다른 규칙.
| 도구 | 옵션 |
|---|---|
| MyBatis | mapUnderscoreToCamelCase=true |
| JPA / Hibernate | SpringPhysicalNamingStrategy (기본값) |
| Spring Data JDBC | NamingStrategy 빈 등록 |
| Jackson (JSON) | @JsonNaming(SnakeCaseStrategy.class) |
👉 JS / Kotlin / Python 어떤 언어로 가도 같은 함정. 같은 발상의 해법.
user_id, created_at)userId, createdAt)setUserId(Long) 인데 컬럼은 StringUSER_ID → userId 로 변환 (대부분 OK)user-id (하이픈) 은 변환 안 됨Mapper XML 의 SELECT 가 동작은 한다. 가끔 null 이 나오는 이유는 모름.
DB ↔ 자바의 이름 규칙 차이를 알고, 객체 필드가 null 일 때 3 초 만에 진단한다.
mapUnderscoreToCamelCase=true (권장)다음: ◆ SQL 인젝션 직접 시연 — #{} vs ${}.