포스트

MyBatis 실무 패턴 — 시퀀스 채번·1:N 컬렉션 매핑·${} vs #{}·동적 컬럼 바인딩

시퀀스 채번, 1:N 컬렉션 매핑, ${} vs #{} 차이, Map의 동적 컬럼 바인딩 등 Mybatis 사용 중 마주친 패턴들.

MyBatis 실무 패턴 — 시퀀스 채번·1:N 컬렉션 매핑·${} vs #{}·동적 컬럼 바인딩

MyBatis로 개발하다 보면 프레임워크가 알아서 처리해주지 않아 매번 XML 매퍼를 직접 손봐야 하는 지점들이 있다. 시퀀스로 PK를 채번하는 방법, 조인 결과를 객체 하나에 1:N으로 묶는 방법, 언뜻 비슷해 보이는 ${}#{}의 차이, 그리고 컬럼명 자체를 런타임에 결정하는 동적 바인딩이 대표적이다.

이 글은 그 네 가지 패턴을 실무에서 마주친 순서대로 정리한 기록이다. 아래 순서로 다룬다.

  • Sequence 채번 — INSERT 전에 시퀀스 값을 미리 확보해 PK로 사용하기
  • 1:N 컬렉션 매핑 — 조인 결과를 부모 객체 + 자식 리스트로 묶기
  • ${} vs #{} — 문자열 치환과 값 바인딩의 차이, 그리고 보안
  • 동적 컬럼 바인딩 — 컬럼명을 런타임에 Map으로 주입하기

Sequence 사용하기

  • 오라클처럼 시퀀스로 PK를 채번하는 DB에서, INSERT 직전에 다음 시퀀스 값을 미리 받아 그대로 PK 컬럼에 넣고 싶을 때 쓴다. <selectKey>로 채번 쿼리를 INSERT에 붙여 한 번의 호출로 처리한다.
1
2
3
4
5
6
7
8
9
<mapper namespace="io.test.TestDao">
    <insert id="save">
        <selectKey keyProperty="IDENTIFIER_SEQUENCE" resultType="java.lang.Integer" order="BEFORE">
            select SEQUENCE_GENERATOR.NEXTVAL from dual
        </selectKey>
        INSERT INTO tbl_test (id, title, description)
        VALUES (#{IDENTIFIER_SEQUENCE}, 'title', 'desciption')
    </insert>
</mapper>
  • order는 before, after 두가지가 있다. before는 쿼리문 실행 전에 시퀀스 값을 생성하고, after는 쿼리문 실행 후에 시퀀스 값을 생성한다.
  • 가 사용하는 parameterType 에 sequence keyProperty 를 setting 할 수 있는 속성과 setter 메서드가 있어야 한다.

컬렉션 매핑

  • 팀 하나에 여러 멤버가 딸린 것처럼, 조인으로 펼쳐진 여러 행을 부모 객체 한 개 + 자식 리스트로 묶고 싶을 때 쓴다. 조인 결과의 자식 행들을 <collection>으로 모아 하나의 부모 객체에 담는다.
  • 컬렉션 매핑은 1:N 관계를 매핑할 때 사용한다.
  • 컬렉션 매핑은 collection 태그를 사용한다.
  • collection 태그는 property, javaType, ofType 속성을 사용한다.
    • property : 컬렉션을 담을 프로퍼티 이름
    • javaType : 컬렉션의 타입 (ArrayList, HashSet, …)
    • ofType : 컬렉션에 담길 객체의 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<mapper namespace="io.test.TeamDao">
    <resultMap id="teamResultMap" type="io.test.Team">
        <id property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="description" column="description"/>
        <collection property="members" javaType="java.util.ArrayList" ofType="io.test.Member">
            <id property="id" column="member_id"/>
            <result property="name" column="name"/>
        </collection>
    </resultMap>
    <select id="selectTest" resultMap="teamResultMap">
        SELECT
            t.id,
            t.title,
            t.description,
            m.id AS member_id,
            m.name
        FROM tbl_team t
        LEFT JOIN tbl_member m ON t.id = m.team_id
    </select>
</mapper>

mybatis ${} 와 #{} 차이

  • ${} : 문자열 치환할 때 사용한다.
  • #{} : 매개변수로 전달된 값을 바인딩할 때 사용한다.
구분${}#{}
사용처변수, 컬럼명
사용예${column}#{value}
원리Mybatis가 문자열로 치환Mybatis가 값에 대한 바인딩을 사용
주의SQL Injection 공격에 취약SQL Injection 공격에 안전
예시SELECT ${column} FROM tbl_testSELECT * FROM tbl_test WHERE id = #{id}

mybatis key-value map 동적으로 사용하기

  • 컬럼 목록을 코드에 고정할 수 없고 런타임에 결정해야 할 때(예: CSV 헤더를 그대로 테이블에 적재) 쓴다. 컬럼명은 ${}로 SQL에 심고, 값은 #{}로 바인딩한다.
  • #{row.${column}} 처럼 사용하면 row(map객체)의 column(key)에 해당하는 값(value)을 동적으로 읽어들일 수 있다. 즉 ${column}이 먼저 문자열로 치환돼 #{row.name} 같은 형태가 된 뒤, 그 값이 바인딩된다.

보안 주의: 여기서 ${tableName}${column}은 문자열 그대로 SQL에 이어 붙기 때문에, 사용자 입력값을 ${}에 직접 넣으면 SQL Injection에 그대로 노출된다. 테이블명·컬럼명처럼 ${}로 넣어야만 하는 값은 반드시 서버가 관리하는 화이트리스트(허용된 컬럼명 목록)와 대조해 검증한 뒤 사용한다. 값(value)은 예시처럼 #{}로 바인딩하면 안전하다.

  • header : List, row : Map<String, Object> 이고 map 객체에 header와 row를 담아서 파라미터로 전달할때 아래와 같이 사용할 수 있다. ```xml
INSERT INTO ${tableName} ${column} VALUES #{row.${column}}

```

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.