Mappering File(XML 파일) - Query문 생성

Violet_Evgadn·2023년 4월 24일
0

DB연동

목록 보기
10/22

NameSpace

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

Namespace는 XML 파일(Mapping File)과 Mapper Interface를 연결하는 방법이다.

우리는 typeAlias를 활용해서 XML 파일 측에서 자바 객체를 활용할 수 있고, MapperLocation을 통해 Mapper 측에서 XML과 연동될 수 있음을 알았다.

그런데 "어떤 XML이 어떤 Mapper Interface와 연동되는가"에 대해서는 명확히 정한 것이 없다.

우리는 단지 특정 경로에 있는 XML 파일 전체가 Mapping File이 될 수 있다는 것만 명시하였을 뿐 "A XML이 B Mapper Interface와 연동된다"처럼 명확한 연결관계는 아직 설정하지 않은 것이다.

이런 명확한 연결 관계를 설정해놓은 것이 바로 "Namespace"라고 할 수 있겠다.

Namespace의 경로를 보면 Mapper Interface의 전체 Path를 저장해놨음을 알 수 있는데, 이런 방식으로 XML 파일에 기록된 Query가 Namespac로 지정한 Mapepr Interface의 메서드를 통해 실행될 수 있다는 것을 알려주는 것이다.

Namespace는 옛날 버전에서는 선택사항이었다. 하지만 Namespace 기능이 워낙 편하고 직관적이기 때문에 최근에는 Namespace를 필수로 활용하도록 업데이트되었다.

Annotation의 활용

MyBatis는 Namespace와 XML 파일을 이용하지 않고 Annotation(@Insert, @Update, @Delete, @Select 등)을 활용하여 Mapper Interface에 직접 Query를 저장할 수도 있다.

하지만 나는 몇 가지 이유로 이 방법을 그렇게 선호하진 않는다.

ORM과 달리 객체와의 연동도 어렵고, 직접 Query문을 입력해야 하며, 활용하는 DBMS(Oracle, MySQL)마다 해당 DBMS 문법에 맞게 Query문을 따로 입력해야 하는 귀찮은 SQL Mapper를 활용하는 이유가 무엇일까?

바로 "속도", 즉 성능이다.

MyBatis 같은 SQL Mapper는 직접 Query문을 짤 수 있기 때문에 가장 빠르게 Query문을 실행하는 방법을 직접 구현할 수 있는 것이다.

그런데 복잡한 Table에서 성능을 내기 위해 직접 Query문을 짜서 Path를 미리 정해 성능을 올리겠다는 SQL Mapper의 활용 이유를 생각한다면, Query문이 자연스럽게 길어질 것이라는 것도 예측할 수 있다.

만약 Annotation을 활용하면 Query문이 길어질수록 직관성이 떨어지고 동시에 유지보수도 하기가 힘들어진다. 그렇다고 짧은 Query문만 활용해 Annotation으로 SQL Mapper를 사용한다면 ORM이라는 좋은 대체제가 있는데 굳이 귀찮은 SQL Mapper를 활용할 이유가 없게 되는 것이다.

또한 SQL Mapper의 큰 장점 중 하나가 forEach나 if를 활용한 동적 SQL을 활용할 수 있다는 것인데, Annotation의 활용은 동적 SQL의 활용도를 확 떨어뜨려버린다.

따라서, MyBatis를 "진짜로" 활용하기 위해서는 XML 파일 기법을 잘 공부하고 Annotation은 활용할 수 있다는 정도만 아는 것을 추천한다.


파라미터 표기법

파라미터란?

SQL Mapper가 Query문을 직접 입력한다고는 하지만, 결국 "Java 객체"를 Input으로 받아 Query문을 자동으로 생성해준다는 개념은 바뀌지 않는다.

그렇다면, 어떻게 Java 객체(클래스)에서 값(멤버 변수의 값)을 뽑아낼 수 있을까? 바로 "파라미터"를 통해서이다.

예시를 한 번 보자.

<update id="updatePost" parameterType="Post">
    UPDATE ${sitePrefix}_POST SET
        PO_STICKY = #{poSticky}  	
        , PO_SELECTED1 = #{poSelected1}  	
        , PO_SELECTED2 = #{poSelected2}  	
        , PO_STICKY_SDATE = #{poStickySDate}
        , PO_STICKY_EDATE = #{poStickyEDate}
    WHERE ARC_ID = #{arcId}
</update>

위 Query는 "Post"라는 Parameter를 받아서 "${sitePrefix}_POST"라는 이름을 가진 Table을 Update 하는 Query문이다.

이때 Post 클래스는 아래와 같다

@ToString
@Getter
@Setter
public class Post {
	private Long arcId;
	private String sitePrefix;
    
	private boolean poSticky = false;	
	private boolean poSelected1 = false;
	private boolean poSelected2 = false;	
	private String poStickySDate;		
	private String poStickyEDate;		
	private Integer poStickyOrder;	
}

눈치 빠른 사람이라면 눈치챘을 것이다.

"어? Post 클래스에 있는 멤버 변수 이름들이 #{}이나 ${}에 둘러 쌓여 입력되어 있네?"

바로 이것이 파라미터 표기법이다.

MyBatis에서는 parameterType이 Java 기본 형일 경우 #{내가원하는이름}처럼 아무 값이나 지정해도 알아서 이를 처리한다. 원시 타입이나 간단한 데이터 타입은 데이터 형식이 어차피 정해져 있기 때문에, 즉 Property를 가지지 않기 때문에 Parameter 이름과 관계없이 값을 대체할 수 있게 되기 때문이다.

Parameter로 String 데이터가 들어온다고 가정하자. 이때 #{String}으로 지정하든 #{Wow}로 지정하든 결국 Parameter는 "String 데이터"로 들어옴을 MyBatis는 알고 있다. 따라서 이름(Property)에 관계없이 Query와 바로 매치시켜줄 수 있는 것이다.

하지만 복잡한 객체(대부분 우리가 생성한 POJO 객체) 같은 경우 여러 가지 멤버 변수(Property)를 가지고 있기 때문에 MyBatis 쪽에서는 이를 자동으로 처리하지 못한다.

위 클래스를 예시로 들어보자면 #{id}로 Parameter를 지정하면 이 값이 arcId 값을 저장해야 할지, sitePrefix 값을 저장해야 할지 모르게 되는 것이다.

따라서 개발자가 Query문에 들어가야 할 값을 #{}이나 ${} 안에 멤버 변수 이름(Property)으로 채워줌으로써 Input으로 들어온 Instance의 값이 적절한 위치에 Query문과 매핑되어 완전한 Query문을 형성할 수 있게 하는 것이다.

#{}, ${} 비교

위 예시에서 Parameter를 #{}과 ${}로 지정함을 알 수 있었다.

그렇다면 ${}와 #{}의 차이는 무엇일까?

#{}

#{}은 원래 Parameter Type이 무엇이든 String 형태로 변환되어 Query를 완성한다.

예를 들어 check = true이고 #{check}로 설정될 경우 CHECK="true"처럼 알아서 String형으로 변환되는 것이다.

아래서 설명하겠지만 #{}은 Query Injection 공격을 예방할 수 있어 보안 측면에서 유리하다.

#{}을 쓰면 무조건 String형으로 변환되니깐, String이 아닌 다른 데이터는 파라미터로 활용할 수 없게 되는 것은 아닐까?

이는 MyBatis 쪽에서 해결해주는 것이 아닌, DBMS에서 처리해준다.

Query문을 수행할 때는 Table명이나 Column명 같은 고정된 값이 아니라 사용자가 직접 넣을 수 있는 값, 즉 Query 검색 조건이나 저장할 값 등은 ' '(작은따옴표)를 붙여서 Query문을 수행해도 DBMS 측에서 자동으로 형 변환을 수행해주어 처리한 이후 DB에 넣어주게 된다.

예를 들어, INSERT Number(id) VALUES '1'로 입력해도 id라는 Column이 Integer를 저장한다면 1이 숫자로 변환되어 저장된다는 의미이다.

이 때문에 Query문에서 검색 조건이나 INSERT, UPDATE, DELETE문에서 활용되는 변숫값들은 #{}으로 지정해도 자동으로 형변환이 되기 때문에 정상 작동하는 것이다.

${}

${}은 Parameter가 바로 출력되는데, 이는 "컬럼의 자료형에 맞추어" 자동으로 파라미터의 자료형이 변경된다는 의미이다.

위 예시처럼 true 값을 Query문에 적용할 경우 CHECK=true처럼 값이 적용된다는 것이다.

물론 ${}도 Parameter 값을 전달하기 때문에 작동 자체는 정상적으로 될 것이다.

하지만 쿼리 주입을 예방할 수 없어 보안 측면에서는 불리하기 때문에 사용자의 입력을 전달할 때는 사용하지 않는 것이 좋다.

그렇다면 ${}은 왜 존재할까? 보안상 문제만 존재한다면 #{}만 있어도 충분했을 것이다.

이는 고정된 데이터인 Table이나 Column 명을 전달할 때 유용하게 활용되기 때문이다.

예를 들어 A라는 Case에서는 A_Department Table에서 데이터를 찾아야 하고 B라는 Case에서는 B_Department Table에서 데이터를 찾아야 한다고 가정하자.

물론 if(A) { A_Department Query}, if(B) {B_Department Query}로 직접 로직을 구현하는 방법도 존재할 것이다.

하지만, 만약 Case가 수백 개, 혹은 수만 개라면? 이 모든 Case를 대응하는 if문을 작성할 수는 없을 것이다.

이때 활용하는 것이 ${}이다. A, B라는 값을 tmp에 저장한 이후 Parameter로 전달해주어 ${tmp}_Department로 Table을 지정해준다면 tmp에 저장되는 값에 맞는 Table에서 Query문이 실행될 것이다.

만약 #{}을 활용한다고 가정하면, tmp에 저장된 값이 'A', 혹은 'B'라는 값으로 적용될 것이다.

이렇게 되면 'A'_Department Table을 찾게 되는데 고정된 값인 Table이나 Column명에 대해서는 DBMS에서 자동으로 처리해주지 않기 때문에 존재하지 않는 Table을 찾게 되며 에러가 발생하게 될 것이다.

이제 위에서 본 MyBatis Query를 다시 한번 뜯어보자

<update id="updatePost" parameterType="Post">
    UPDATE ${sitePrefix}_POST SET
        PO_STICKY = #{poSticky}  	
        , PO_SELECTED1 = #{poSelected1}  	
        , PO_SELECTED2 = #{poSelected2}  	
        , PO_STICKY_SDATE = #{poStickySDate}
        , PO_STICKY_EDATE = #{poStickyEDate}
    WHERE ARC_ID = #{arcId}
</update>

먼저 poSticky, poSelected1, poSelected2, poStickySDate, poStickyEDate, arcId 같은 User에게 직접 입력받을 수 있는 값이자 DBMS에서 자동으로 형 변환 처리해주는 값들에 대해서는 #{} Parameter를 활용했다

하지만 ${sitePrefix}_POST, 즉 고정된 값인 Table명에는 ${} Parameter를 활용해서 String형으로 변환되지 않고 Raw Data가 적용되도록 했다.(다른 말로, 따옴표가 붙지 않을 것이다)

결론적으로 보안상 #{}이 선호되지만 Column이나 Table명같이 'x'같이 따옴표가 붙어서는 안 되는 부분에서만 ${}을 활용한다로 알고 있으면 완벽한 이해일 것이다.

PreparedStatement, Statement()

#{}은 PreparedStatement를, ${}은 Statement를 활용한다.

이 때문에 #{}과 ${}에 차이가 생기게 되는 것이다. 즉, PreparedStatement와 Statement의 차이를 익힌다면 #{}과 ${}의 차이에 대해 더 잘 이해할 수 있게 될 것이다.

PreparedStatement는 DBMS에서 동일하거나 비슷한 DB Quey문을 높은 효율성을 가지며 반복적으로 실행하기 위해 사용되는 기능을 말한다.

PreparedStatement의 동작 방식은 아래와 같다.

먼저, Application은 Query문의 틀(Template)을 만들어 DBMS로 보내는데, 이 때 특정 값(주로 User에게 받아야 하는 조건값들)을 지정하지 않은 채 남기니다.

이후 DBMS는 Query Template만 Compile 하고 결과만 저장한 이후 Application이 Compile 된 Query문에 변수 값을 전달하면 애플리케이션은 변수 값만 추가로 작업하여 더욱 빠르게 Query문을 수행시킬 수 있게 되는 것이다.

Statemetnt는 매번 Query를 수행할 때마다 "연결 -> Query문 완성 -> Compile -> 실행"의 4단계를 거치지만 PreparedStatmetns는 첫 3단계를 한 번만 수행하고 캐시에 담아 재사용한다는 면에서 DB에 훨씬 적은 부하를 주며, 좋은 성능을 나타냄을 알 수 있다.

즉, #{}과 ${}은 Data 처리 방식도 차이가 존재하지만, Cahce를 사용하는가 여부도 달라지는 것이다.

이런 점에서 Cache에 따른 속도 차이도 존재하기 때문에 ${}를 사용할 이유는 더더욱 없어 보이지만, ${}가 Optimizer 수행 계획에 이점을 주는 경우도 존재한다고 한다.

예를 들어, 여러 직책에 대한 인원을 조사해야 하는데 "사장, 부장, 차장, 과장, 대리" Table이 존재한다고 가정하자.

이럴 때는 Table마다 Query를 생성해줘서, Table에 가장 적절한 OPtimizer 수행 계획을 세우도록 만들 수도 있다.

이렇게 Table이 다수일 경우 Optimizer 수행 계획을 잘 세워 데이터를 빨리 뽑아내는 것이 Query문의 Compile 하는 시간보다 더 큰 이점을 가지므로 이럴 때는 활용할 수 있을 것이다.

반대로, 1개 Table에 대해서만 Query를 수행하거나 Query를 수행할 Table 개수가 적을 경우 ${}보다는 #{}을 활용하는 것이 좋을 것이다.

또한 ${}과 #{}을 동시에 활용하면 결국 statement를 활용하는 것과 동일한 효과를 낼 것이므로 사실상 ${}만 있는 상황이라고 생각해도 좋을 것 같다

${}이 왜 위험할까?

위에서 ${}은 SQL Injection 공격의 위험성이 존재하기 때문에 유저의 입력을 받는 값에 대해서는 활용을 피해야 한다고 말했다.

정확한 의미를 알아보기 위해 아래 예시를 살펴보자

<select id="login" parameterType="UserDAO" resultType="UserVO">
    SELECT
        *
    FROM
        user
    WHERE
        id = ${id} AND password = ${password}
</select>

일단 parameterType과 resultType에 대해서는 고민하지 말고, 큰 그림만 보자.

이 Query에서는 id와 password값을 Parameter로 받아서 User Table에서 검색하는 로직을 가진다.

이렇게만 보면 아무런 문제없는 평범한 Query이다.

이때 어떤 악독한 유저가 id = 'admin' -- 값을 입력했다면, 아래와 같은 Query문이 완성될 것이다.

SELECT
    *
FROM
    user
WHERE
    id = 'admin' -- AND password =

SQL 문법에서 "--"은 주석 처리함을 의미한다.

이에 따라 "AND password =" 부분은 주석 처리되며, Query에서 Password까지 일치해야 로그인된다는 로직이 사라지는 것이다. 즉 id만 입력해도 해당 계정만 존재한다면 바로 로그인되는 것이다.

이렇게 User가 직접 SQL 구문을 입력하여 공격하는 방식을 SQL Injection이라고 한다.

그렇다면 #{}은 왜 SQL Injection에 대해 안전한 것일까? #{}을 활용했을 때 완성되는 Query를 살펴보자

SELECT
    *
FROM
    user
WHERE
    id = 'admin --' AND password =

즉, #{} 같은 경우는 알아서 ' '(작은따옴표)를 붙여 문자형 데이터로 변환하므로 SQL Injection에 충분히 대응할 수 있게 되는 것이다.


SELECT

<select id="selectPerson" parameterType="int" resultType="Member">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

SELECT문에 대해서 다룰 것이다.

SELECT문에 대해서만 제대로 알고 있다면 INSERT, UPDATE, DELETE문은 사용 방법이 살짝 다를 뿐 매우 유사하므로 쉬운 이해가 되기 때문에 꼭 집중해서 공부해보자!

먼저 이 Query가 SELECT문임을 알려야 하기 때문에 < select> 태그를 활용한다.

그리고 태그 사이에 Query문(SELECT문)을 입력함으로써 내가 원하는 SQL문을 Mapping File에 저장하여 활용할 수 있게 되는 것이다.

< select> 태그 사이에 존재하는 Query문에 대해서는 DB Mapping 보다는 SQL 문법에 대한 내용이기 때문에 따로 설명은 하지 않을 것이다. 다만, MyBatis를 활용해보며 DB를 사용하다 보니 SQL 문법에 대한 필요성을 느껴서 나중에 제대로 공부할 것 같다.

위 예시에서 볼 수 있듯 <select id="x" parameterType="y"...>처럼 < select> 태그 내에 사용할 수 있는 여러 개의 속성(Parameter)이 존재한다.

결국 MyBatis의 숙련도는 (SQL 구문을 완벽히 활용한다는 가정이 있다면) 속성의 활용 숙련도와 유사하기 때문에 잘 알아두자.

자주 활용하는 Parameter(개인적인 기준)

  • id : 구문을 찾기 위해 사용될 수 있는 Namespace 내 유일한 구분자
    • 어렵게 설명했지만, 결국 Namespace로 지정한 Mapper Interface에 지정한 메서드 이름 중 하나를 입력하면 됨
    • Namespace로 지정한 Interface 중 id와 일치하는 메서드가 연결되어 해당 메서드가 실행될 때 Query를 실행시킴
  • parameterType : 구문에 전달될 파라미터의 클래스명이나 별칭
    Query의 입력값
    • Mapper Interface 메서드의 입력값
  • resultType & resultMap : Query문에 의해 리턴되는 기댓값을 저장할 Class명이나 별칭(TypeAlias)

resultType과 resultMap

결국 resultType과 resultMap은 Query의 결과를 저장하는 객체(클래스)를 명시한 것이다.

그렇다면, 둘 사이에는 어떤 차이가 있어서 나눠져 있을까?

간단히 말하자면 "결과를 저장할 객체를 미리 설정해놨는가" 여부이다.

resultType에는 클래스명 전체 또는 alias를 입력한다. 즉, 매핑하려는 자바 객체의 전체 경로를 입력하는 것이다.

resultMap에는 미리 "어떤 Column에 어떤 Property가 저장되어야 한다"를 정의해놓고 미리 정의해 놓은 resultMap을 인자로 입력하는 것이다.

자 말로만 들으면 알쏭달쏭하다. 직접 코딩해보자.

먼저 MEMBER Table은 아래와 같은 형식을 가진다

  • Member Table

resultType

<select id="getUserData" parameterType="int" resultType="com.example.User">
	select * from MEMBER where num=#{id}
</select>

resultType으로는 "com.example.User", 즉 Query문의 Output을 받을 POJO 클래스의 Path가 정의되어 있다.

이렇게 resultType은 반환 타입이 될 클래스 자체를 가져와 참조하기 때문에 resultMap과는 달리 사전 정의가 필요 없다.

하지만 개인적으로 공부했을 때는 resultType을 많이 활용하지는 않는 것 같은데, 바로 "매칭" 때문이다.

매칭이라는 말을 좀 풀어서 이해해보자.

위 예시에서는 "select *" 구문을 수행하였기 때문에 (num, name, id, pwd, number) Column의 데이터가 모두 반환될 것이다. 그런데, 이 값이 어떻게 User의 멤버 변수와 매칭 될 수 있을까?

만약 User의 멤버 변수 이름이 (no, userName, userId, userPwd, userNumber)라면 어떤 Column의 데이터가 어떤 멤버 변수에 매칭 되어 저장돼야 할 것인가?

resultType을 통해서는 이 "매칭 관계"를 미리 정의해줄 수가 없다.

물론, 원하는 멤버 변수와 Column 값을 매칭 시키는 명명 로직이 있기는 한데, 이를 위해선 Column 명에 따라 멤버 변수명을 지어줘야 한다.

즉, 멤버 변수 이름은 Column 명에 종속되게 되며, 이는 좋은 개발이라고는 할 수 없을 것이다.

따라서, 내가 찾아봤을 때는 Java의 기본 자료형에 대해서만 resultType을 활용하는 것 같다.

예를 들어보면 아래와 같다

<select id="getUserNumber" resultType="int">
	select COUNT(*) from MEMBER
</select>

위 쿼리는 Member Table에 저장된 User 데이터의 개수를 반환하는 Query이다.

무조건 결과는 Integer, 즉 정수형일 것이다. 굳이 Integer형 데이터 하나를 처리하기 위해 UserNo라는 클래스를 만들어 Integer 데이터를 담을 POJO 객체를 만들 필요는 없다. 그냥 Mapper Interface 측에 "Integer 데이터가 반환됩니다!"라고만 알리면 되고, 이때 활용하는 것이 resultType이다.

resultMap

resultMap은 미리 선언하여 어떤 Property(멤버 변수)에 어떤 Column Data가 저장될지를 명시하고, 이렇게 미리 명시한 resultMap을 활용하는 것이다.

예시를 들어보자

<resultMap type="com.example.User" id="userResultMap">
    <id column="num" property="id" javaType="Integer"/>
    <result column="name" property="userName" javaType="String"/>
    <result column="id" property="userId" javaType="String"/>
    <result column="pwd" property="userPassword" javaType="String"/>
    <result column="number" property="userNumber" javaType="Integer"/>
</resultMap>

resultMap을 명시하는 방법은 위와 같다. 딱 봐도 어떤 Property가 어떤 Column 데이터를 저장할지 명확하다.

먼저 <resultMap>의 Parameter부터 확인하자.

type은 Query결과를 담을 객체를 지정해야 하는데, 위 예시처럼 "com.example.User"로써 객체의 전체 경로를 입력해줘야 한다.

id는 예상할 수 있듯, 아래에서 위 예시처럼 지정한 reusltMap을 활용할 때 사용하는 값이다. id를 userResultMap으로 지정했으니, 나중에는 resultMap="userResultMap"처럼 지정하여 위에서 지정한 ResultMap을 활용할 것임을 명시할 수 있다.

먼저 User 클래스에는 (id, userName, userId, userPassword, userNumber)라는 이름의 멤버 변수가 존재해야 한다.

<id>는 Table의 Primary Key, <result>는 Table의 Column들에 대한 매칭 관계를 설정하기 위한 Tag이다.

위 예시를 보면 알겠지만 매우 직관적인데, "num Column Data"는 "id"에, "name Column Data"는 "userName"에, "id Column Data"는 "userId" 멤버 변수에 저장될 것임을 알 수 있다.

또한 javaType을 통해 "어떤 형식의 데이터가 저장되어야 하는가"도 명시할 수 있고, 앞에서 설명했던 typeHandler를 적용할 수도 있을 것이다.(나중에 제대로 MyBatis를 활용할 때 활용해보자)

미리 <resultMap> 태그 안에 연결 관계를 설정해야 한다는 귀찮음이 존재하긴 하지만, 조금의 귀찮음으로 에러의 발생 확률을 확 낮출 수 있으며 유지 보수성도 좋아지고 연결 관계도 더욱 명확히 설정할 수 있는 등 많은 장점을 가지고 온다.

이제 이 resultMap을 활용해보자

<select id="getUserData" parameterType="int" resultMap="userResultMap">
	select * from MEMBER where num=#{id}
</select>

즉, Query의 결과 값이 Java 기본 자료형일 경우 resultType을, Query 결괏값을 개발자가 생성한 POJO 클래스에 매핑시키고 싶다면 resultMap 사용을 추천한다

많이 쓰이지는 않지만 사용할만한 Parameter

  • flushCache : true로 설정할 경우 구문이 호출될 때마다 Local 및 2nd 레벨 캐시가 지워짐
    • Default : false
    • 캐시를 사용하지 않기 위한 Setting
  • useCache : True일 경우 구문의 결과가 2nd레벨 캐시에 캐시 됨
    • Default : true
    • 캐시를 사용하지 않기 위한 Setting
    • useCache = "false"로 설정했다면 반드시 flushCache = "true"로 설정해야 함(반대도 마찬가지)
      • 생각해보면 당연하다. flushCache = "false"여서 캐시를 활용한다고 해놨는데 갑자기 useCache = "false"로 지정하면 Local 및 2nd 레벨 캐시를 활용할 것인데 구문 결과는 2nd 레벨 캐시에 저장하면 안 되는 이상한 상황이 발생할 것이기 때문이다.
  • timeout : DB 요청 결과를 기다리는 최대 시간
    • 드라이버에 따라 지원되지 않을 수도 있음
  • fetchSize : 결괏값 중 fetchSize에 해당하는 수만큼의 데이터만 반환
    • 드라이버에 따라 지원되지 않을 수도 있음

굳이 활용할까 싶은 Parameter

  • statementType : Query를 날리는 방식

    • STATEMENT : Statement와 동일
    • PREPARED : Default. PreparedStatement와 동일
    • CALLABLE : Parameter Type에 MODE라는 것이 존재하고, mode = "OUT"으로 설정된 Parameter들만 값이 전달되는 것이다. Function이나 Procedure에서 많이 활용한다고는 한다. CALLABLE은 parameterMap과 큰 연관 관계가 존재하고 parameterMap은 없어졌다. 내가 생각할 때는, 굳이 활용할 것 같지는 않다.
  • restulSetType : 검색 시 커서에 대한 설정. 지원하지 않는 드라이버가 존재할 수 있음

  • resultOrdered : true로 설정할 경우 내포된 결과를 가져오거나 새로운 주요 결과 레코드를 리턴할 때 함께 가져옴

    • Default : false

INSERT, UPDATE, DELETE

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

위 예시를 보면 알겠지만, <select> 태그가 <update>, <insert>, <delete>로 바뀌었을 뿐 나머지는 모두 동일하다.

즉, Query의 종류에 따라 태그 값만 바꾸면 MyBatis의 여러 Query를 바로 활용할 수 있게 되는 것이다.

3개 Tag에 대한 Parameter는 공통점이 많으므로 한꺼번에 설명하겠다.(SELECT문과 똑같은 Parameter도 다수 존재)

SELECT문과 동일한 Parameter

  • id
  • parameterType
  • resultType & resultMap
  • flushCache
  • timeout
  • statemntType

없어진 Parameter

  • parameterMap : 외부 parameterMap을 찾기 위한 방법.
    • parameterType으로 충분히 대체되기 때문에 Deprecated 되었다가 결국엔 없어졌다.

입력(Insert, Update)에만 적용할 수 있는 Parameter

  • useGeneratedKeys : 데이터베이스에서 내부적으로 생성한 Key값을 받을지 여부

    • Default : false
    • PK 등을 설정할 때 "AUTO_INCREMENT" 등으로 Key를 DB 측에서 자동으로 생성하게 하는 Column들이 있었을 것이다. 이때 대부분 NULL 값을 Input으로 넣어줘서 ID 값을 자동으로 생성하게 한다. 그런데, 이 PK 값을 바로 활용해야 할 경우가 있을 것이다. 이 때 useGeneratedKey = true일 경우 DB에서 자동으로 생성한 PK 값을 반환해준다
  • keyProperty : 자동 생성된 키 값을 저장할 Property 입력. 여러 개의 Property가 필요할 경우 콤마(,)를 구분자로 나열할 수 있음

  • keyColumn : DB에서 내부적으로 생성하는 Key값을 저장하는 Tabe Column

이렇게 하면 DBMS 측에서 자동으로 생성한 Key값을 바로 활용할 수 있는 것이다.

직접 예시를 들어보자.

"Survey"라는 테이블을 만들고, Survey ID 값을 Foreign Key값으로 활용하여 Questions라는 Table에 데이터를 넣어야 하는 Case라고 가정하자.

이 때 useGeneratedKey값을 활용할 경우 아래와 같은 과정이 벌어질 것이다.

  1. insert를 통해 Survey 테이블에 데이터를 저장
  2. Survey 테이블에서 검색을 통해 1에서 수행한 insert 구문을 통해 자동 생성된 Key값을 검색
  3. 2에서 구한 Key값으로 Foriegn Key를 설정

그런데 useGeneratedKey를 활용하면 2번 과정이 없어지는 것이다.

1 과정에서 Survey 테이블에서 데이터를 저장함과 동시에 자동 생성된 Key값을 Input으로 들어왔던 객체에 담아준다.

그리고, 이 때 keyProperty에 지정했던 멤버 변수에 자동 생성된 Key값을 저장하게 되는 것이다

직접 코드를 통해 활용 방식을 이해해보자

먼저 useGeneratedKey를 활용하지 않을 때 위에서 설명한 Survey와 Question을 저장하는 로직을 확인해보자

public void saveQuestion(Survey survey, Question question){
    surveyMapper.insertSurvey(survey);
    
    Survey tmpSurvey = surveyMapper.selectSurvey(survey);
    
    question.setSurId(tmpSurvey.getId());
    
    questionMapper.insertQuestion(question);
}

즉 DB에 총 3번 접근하게 되고, 만약 Survey Table에 데이터가 많다면 검색 시간도 (조금이겠지만) 오래 걸릴 것이다.

하지만 useGeneratedKey를 활용하면 아래와 같다

public void saveQuestion(Survey survey, Question question){
    surveyMapper.insertSurvey(survey);
    
    question.setSurId(survey.getId());
    
    questionMapper.insertQuestion(question);
}

자, selectSurvey(), 즉 Survey를 검색하는 과정이 줄었음을 알 수 있다.

결국 useGeneratedKey가 true라면 DB 측에서 자동으로 생성한 Key값을 Input으로 들어온 객체에 keyProperty로 지정한 멤버 변수에 저장할 것이다.

따라서, survey의 "id"멤버 변수에는 DB에서 자동 생성된 Key값이 저장되게 되는 것이다.

그렇다면 useGeneratedKey를 활용한 MyBatis의 Query도 알아보자

<insert id="insertSurvey" parameterType="Survey" useGeneratedKeys="true" keyProperty="id">
    insert into SURVEY(
     	quName, quNumber
    ) VALUES(
        #{quName}, #{quNumber}
    )
</insert>

keyProperty를 통해 "id" 멤버 변수에 자동생성된 값을 알렸고, useGeneratedKeys="true"로 설정하여 생성된 Key값을 받아오게 설정도 하였다. 생성된 Key값은 parameterType인 Survey의 id에 저장될 것이다.

selectKey

원래 DBMS에서는 Procedure나 Function 등을 활용해 Column에 저장될 값을 자동으로 생성할 수 있다.

그런데, DBMS에서 직접 이 과정을 지정하지 않고 MyBatis 측에서 지정할 수는 없을까?

예를 들어, 주민번호를 입력했는데 뒷번호 첫 글자가 "1"인 경우 남자로, "2"일 경우 여자로 저장하는 로직을 DBMS에서 짜지 말고, MyBatis XML 파일에서 직접 로직을 생성할 수는 없을까?

이를 위해 활용하는 것이 <selectKey> 태그이다.

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

원래는 id가 AUTO_INCREMENT였었지만, "랜덤한 ID값"으로 지정하고 싶을 수도 있을 것이다.

위 코드를 보면 <selectKey> 태그 사이에 어떤 select문이 존재하는데, 이는 랜덤한 값을 만들어주는 로직이다.

이렇게 만들어진 랜덤한 값을 keyProperty, 즉 "id"라는 Property에 저장하여 활용할 수 있게 되는 것이다.

즉, 랜덤한 값을 만드는 로직을 만들고, 이를 <selectKey> 태그 안에 넣어줌으로써 DBMS에 따로 Function이나 Procedure를 만들지 않아도 자동으로 Key값이 생성되는 것이다.

selectKey Property는 아래와 같다

  • keyProperty : selectKey 구문의 결과가 Setting 될 대상 Property(SQL에서 활용할 Property Name)
  • resultType : 결과의 타입
  • order : before, after로 설정할 수 있음
    • before : 태그 사이에 지정된 Query를 먼저 수행하고 insert 구문을 실행함
    • after : insert 구문이 먼저 수행된 이후 태그 사이에 지정된 Query를 수행함
  • statementType : STATEMETN, PREPARED, CALLABLE 지원. 위에서 설명

selectKey order="after"을 어디다 사용할까 싶었는데, 자동으로 생성된 Key값을 받아올 때 활용하는 것 같다.

하지만, useGeneratedKeys를 통해 쉽게 구현할 수 있기 때문에, 사실상 order="before"를 사용하여 내가 원하는 대로 Key값을 만든 이후 Insert 구문을 수행하게 하는 것이 주요 로직이 되지 않을까 싶다.


sql

생각해보자. 위에서 예시로 둔 Post라는 클래스는 멤버 변수가 매우 많았다.

이럴 경우 매번 select A = #{A}, B = #{B}...처럼 모든 Column을 입력해줘야 할까?

만약 실제로 이렇다면 너무 답도 없을 것이다. 이런 작업을 줄이기 위해 활용하는 것이 <sql> 태그이다.

<sql> 태그는 다른 SQL 구문에서 재사용할 수 있는 SQL 구문을 미리 정의하여 계속해서 태그 id 값을 통해 중복작업을 줄이기 위한 기술이다.

예를 들어보자.

<insert id="insertUser" parameterType = "User">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

위 구문에서 Author라는 Table의 Column은 id, username, password, email, bio가 존재함을 유추할 수 있다.

그런데 이 Column 명은 고정되어 있을 텐데 계속해서 입력해야 할 필요가 있을까?

Java 변수처럼 (id, username, password, email, bio)로 한 번에 묶어 활용할 수 없을까?

이를 위해 활용하는 것이 <sql> 태그인 것이다.

<sql> 태그를 활용한 예시를 들어보자

<sql id="userPlainColumns">
	id
    , username
    , password
    , email
    , bio
</sql>
<insert id="insertUser" parameterType = "User">
  insert into Author 
  	(<include refid="userPlasinColumns"/>)
  VALUES
  	(#{id},#{username},#{password},#{email},#{bio})
</insert>

먼저 <sql> 태그를 활용해 User의 Column 명을 한꺼번에 입력해 놓았다.

이후 <include refid="{sql id}">를 통해 위에서 지정한 <sql> 태그 값을 그대로 불러올 수 있게 되는 것이다.

이렇게 만드면 최종적으로 우리가 첫 번째에 봤던 INSERT 구문이 완성되며, Column 값을 모든 Query마다 일일이 입력해줘야 하는 번거로움이 확 줄어들 것이다.

<sql> 태그를 통해 SQL 구문(주로 Column명)을 미리 저장해 놓은 뒤, 이때 지정했던 id 값을 <include refid="X"> 태그에서 X위치에 입력함으로써 미리 지정했던 <sql> 태그 값을 활용할 수 있게 되는 것이다.

또한 <sql> 태그에서도 ${}를 활용해 변수 값을 정해서 로딩 시점에서 정적으로 parameter처럼 활용할 수 있다.

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

개인적인 생각으로는 이 활용방법은 구성이 너무 복잡해서 스파게티 코드가 되거나 유지보수성이 어려워지거나 코드의 가독성이 떨어진다고 느껴져서 일단 지식적으로만 알고 있어야겠다.

사실, ${}를 활용해 <sql>을 동적으로 만든다는 것도 큰 의미가 있는지 모르겠다. 왜냐하면 여러 value가 대응되야한다는 것은 value에 대응되는 값의 Column이 같거나 Table명이 같은 등 공통점이 존재해야 한다는 것인데 굳이 이걸 활용하기 위해서 Table의 Column명을 일일이 맞춘다..? 굳이 그렇게 어렵게 가다가 유지보수도 힘들어지고 에러도 발생하느니 그냥 맘 편하게 #{}을 통해 하드 코딩하는 게 더 쉬울 것 같다.

profile
혹시 틀린 내용이 있다면 언제든 말씀해주세요!

0개의 댓글