<< 학습 목표 >>
1. bind 태그를 활용할 수 있다.
2. AS 키워드 대신 resultMap 태그를 활용할 수 있다.
3. 계층적 데이터 구조를 이해하고 설계할 수 있다.
4. 계층적 데이터 구조인 데이터를 가져올 때 resultMap 태그를 활용할 수 있다.
이번에 배울 bind, resultMap 태그는 정적 쿼리든 동적 쿼리든 어디서든 활용할 수 있으므로 사용할 수 있는 상황을 제한하지 말자
| 태그명 | bind |
| 설명 | 쿼리에서 사용할 변수를 선언하는 태그 |
| 속성 | name 속성 : 변수의 이름 value 속성 : 변수의 값 |
SELECT 쿼리를 작성할 때 LIKE 가 필요하면 다음과 같이 LIKE 를 구성함
<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
SELECT * FROM member WHERE nickname LIKE "'%' + #{nickname} + '%'";
</select>
bind 태그를 사용하면 좀 더 가독성 있는 코드를 만들 수 있음
<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
<bind name="pattern" value="'%' + #{nickname} + '%'"/>
SELECT * FROM member WHERE nickname LIKE #{pattern};
</select>
bind 태그를 사용할 때 주의할 점은 select, insert, update, delete 태그 안에 사용해야한다는 점임
만약 여러 태그에서 똑같은 bind 태그를 여러번 사용한다면 아직까지는 그때 마다 bind 태그를 복사 해서 사용해야함
다음에 배울 sql, includes 를 배우면 똑같은 bind 태그를 여러번 사용해야할 때 bind 태그를 재사용하는 방법을 알 수 있을 것
| 태그명 | resultMap |
| 설명 | DB의 AS ( Alias ) 와 같은 역할을 하는 태그 또는 SELECT 결과가 계층적 데이터 구조로 되어있는 경우 결과를 저장하기 위해 사용하는 태그 |
| 속성 | type 속성 : 결괏값을 저장할 DTO의 데이터 타입 id 속성 : 이 구조의 이름 |
resultMap 태그는 어떻게 활용하느냐에 따라서 활용 방식이 배우 달라지는데 먼저, 가장 간단한 형태로 AS ( Alias ) 와 같은 역할을 하는 것으로 활용해보자
- AS 대신 사용
resultMap 태그 안에는 자식 태그로 id, result, association, collection, discriminator, constructor 태그를 가질 수 있음
AS 대신 resultMap 태그를 사용한다면 id, result 자식 태그만 사용하면 됨
member ( 회원 정보 ) 테이블의 column이 다음과 같은 상황임

MyBatis를 사용해서 회원 정보를 조회하는 쿼리를 만들자
<?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="com.study.chapter04.MemberDao">
// ...
<select id="selectMember" resultType="com.study.chapter04.ResultMapDto">
SELECT * FROM member;
</select>
// ...
</mapper>
결과를 저장할 DTO는 ResultMapDto 임
ResultMapDto 는 아래와 같음
package com.study.chapter04;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class ResultMapDto {
private int userIdx;
private String userId;
private String _pw;
private String nickName;
private String telNumber;
private LocalDateTime joinDateTime;
private boolean isDel;
}
이때 테이블의 칼럼명과 DTO의 멤버 변수명이 다르기 때문에 쿼리가 정상적으로 수행되도 결과를 받을 수 없음

결과를 제대로 받으려면 쿼리에 AS ( Alias ) 를 사용하면 됨

그러나 AS는 DBMS가 지원하는 키워드임
AS 대신 MyBatis가 지원하는 resultMap 태그를 사용해도 됨
<?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="com.study.chapter04.MemberDao">
// ...
<resultMap type="com.study.chapter04.ResultMapDto" id="newResultMapDto">
<id column="idx" property="userIdx" javaType="int" jdbcType="INTEGER" />
<result column="id" property="userId" javaType="String" jdbcType="VARCHAR" />
<result column="pw" property="_pw" javaType="String" jdbcType="VARCHAR" />
<result column="nickname" property="nickName" javaType="String" jdbcType="VARCHAR" />
<result column="tel" property="telNumber" javaType="String" jdbcType="VARCHAR" />
<result column="joinDateTime" property="joinDateTime" javaType="String" jdbcType="VARCHAR" />
<result column="isDel" property="isDel" javaType="boolean" jdbcType="VARCHAR" />
</resultMap>
<select id="selectMember" resultMap="newResultMapDto">
SELECT * FROM member;
</select>
// ...
</mapper>
<< 코드 설명 >>

(1). 칼럼명과 멤버 변수를 매치하기 위해 resultMap 태그 사용
(2). PK인 칼럼을 멤버 변수와 연결하기 위한 태그
column속성은 테이블의 칼럼명
property속성은 DTO의 멤버 변수명
javaType속성은 DTO의 멤버 변수의 데이터 타입
jdbcType속성은 테이블의 칼럼의 데이터 타입
(3). PK가 아닌 칼럼을 멤버 변수와 연결하기 위한 태그
속성들은 (2) 와 동일함
(4). resultMap 태그로 선언한 매칭 정보를 활용하기 위해서 resultMap 속성을 사용함
여기서 주의할 점 ! DTO를 그대로 사용할 때는 resultType 속성을 사용했지만 resultMap 태그를 사용할 때는 resultMap 속성을 사용함
resultMap 태그의 javaType, jdbcType은 생략 가능함
다음은 javaType, jdbcType 을 나열한 테이블임
| 자바의 데이터 타입 | javaType | DB의 데이터 타입 | jdbcType |
| int | int | int | INTEGER |
| double | double | float(실수) | DOUBLE |
| char | String | char | String |
| String | String | String | String |
이 표를 보는 방법은 (자바의 데이터 타입, javaType) , (DB의 데이터 타입, jdbcType) 임
자바의 데이터 타입에 맞게 javaType 속성을 사용하면 됨
DB의 데이터 타입에 맞게 jdbcType 속성을 사용하면 됨
javaType, jdbcType 속성을 생략해도 MyBatis가 적절히 매칭시켜줌
그러나 일반적으로 테이블의 칼럼명과 DTO의 멤버 변수명은 맞춰거나 DBMS의 AS 키워드를 사용하므로 resultMap을 AS 대신 사용하는 일은 없을 것
- SELECT 결과가 계층적 데이터 구조로 되어있는 경우 결과를 저장하기 위해 사용하는 태그
계층적 데이터 구조란 한 테이블 안에 상위 데이터와 하위 데이터가 함께 있는 구조를 말함
쇼핑몰의 카테고리가 대표적인 계층적 구조임
쿠팡의 카테고리를 보자

카테고리는 카테고리명, 차수(depth) 로 이뤄져있음
(1)인 패션의류/잡화, 뷰티, 출산/유아동 등에서 패션의류/잡화를 보면 카테고리명은 패선의류/잡화이고 차수는 1차임
뷰티의 카테고리명은 뷰티이고 차수는 역시 1차임
(2)인 여성패션, 남성패션, 남녀 공용 의류, 유아동패션은 1차 카테고리인 패션의류/잡화의 하위 카테고리임
그리고 카테고리이기 때문에 카테고리명과 차수로 이뤄져있음
또한 상위 카테고리가 패션의류/잡화임
따라서 여성패션의 카테고리명은 여성패션이고 차수는 2차, 상위 카테고리는 패션의류/잡화임
남성패션의 카테고리명은 남성패션이고 차수는 2차, 상위 카테고리는 패션의류/잡화임
마지막으로 (3)인 의류, 속옷/잠옷, 신발, 가방/잡화는 2차 카테고리인 여성패션의 하위 카테고리임
그리고 카테고리기 때문에 카테고리명과 차수로 이루어져있음
또한 상위 카테고리가 여성패션임
따라서 의류의 카테고리명은 의류이고 차수는 3차, 상위 카테고리는 여성패션임
속옷/잠옷의 카테고리명은 속옷/잠옷이고 차수는 3차, 상위 카테고리는 여성패션임
카테고리의 구조를 좀 더 시각적으로 나타내면 다음과 같음

이러한 구조의 데이터를 저장할 때는 아래와 같이 한 테이블에 저장할 수 있도록 테이블 구조를 설계함

<< 설명 >>
테이블명은 category 이고 4개의 칼럼을 가지고 있음
idx, name, order 칼럼은 특별한 점이 없지만 parent_idx 칼럼은 특별한 카테고리
parent_idx 카테고리가 참조하는 테이블은 category, 참조하는 칼럼은 idx
이 테이블의 ERD를 그리면 다음과 같음

category테이블에 데이터를 저장하면 다음과 같이 저장됨

1차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx IS NULL;
2차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx = 1;
-> SELECT * FROM category WHERE parent_idx = 2;
-> ...
3차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx = 4;
-> SELECT * FROM category WHERE parent_idx = 5;
-> SELECT * FROM category WHERE parent_idx = 6;
-> ...
1 ~ 3차 카테고리까지 한번에 찾는 쿼리
-> SELECT
depth1.name,
depth2.name,
depth3.name
FROM category depth1
LEFT JOIN category depth2
ON depth1.idx = depth2.parent_idx
LEFT JOIN category depth3
ON depth2.idx = depth3.parent_idx
WHERE depth1.parent_idx IS NULL;
( DBMS와 관련된 카테고리가 아니기 때문에 더 이상 자세한 설명은 생략함 )
1 ~ 3차 카테고리까지 한번에 찾는 쿼리를 사용하면 아래와 같은 데이터가 조회됨

이를 MyBatis, (DTO, DAO), 컨트롤러 로 가져오면 한 행에 하나씩 DTO에 저장됨
그리고 DTO를 사용하기가 어려울 수 있음
DTO를 사용하기 어려운 상황 중 하나를 재현해보자
컨트롤러에 접근했을 때 다음과 같이 각 차수별 카테고리와 그 하위 카테고리를 출력해보자

우선 카테고리 하나의 정보를 담을 수 있는 CategoryDto 를 추가하자
package com.study.chapter04;
import lombok.Data;
@Data
public class CategoryDto {
// 1차 카테고리 정보
private int depth1CategoryIdx;
private String depth1CategoryName;
// 2차 카테고리 정보
private int depth2CategoryIdx;
private String depth2CategoryName;
// 3차 카테고리 정보
private int depth3CategoryIdx;
private String depth3CategoryName;
}
<< 코드 설명 >>
카테고리가 최대 3차 카테고리까지 있으므로 3차 카테고리 정보까지 담을 수 있도록 멤버 변수를 선언해둠
이제 카테고리 정보를 조회해 DTO에 담아줄 쿼리를 만들자
프로젝트 -> src/main/resources -> mapper -> Category.xml 파일을 추가하고 아래 코드를 추가하자
<?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="com.study.chapter04.CategoryDao">
<select id="selectAllCategory" resultType="com.study.chapter04.CategoryDto">
SELECT
depth1.idx AS depth1CategoryIdx, depth1.name AS depth1CategoryName,
depth2.idx AS depth2CategoryIdx, depth2.name AS depth2CategoryName,
depth3.idx AS depth3CategoryIdx, depth3.name AS depth3CategoryName
FROM category depth1
LEFT JOIN category depth2
ON depth1.idx = depth2.parent_idx
LEFT JOIN category depth3
ON depth2.idx = depth3.parent_idx
WHERE depth1.parent_idx IS NULL;
</select>
</mapper>
<< 코드 설명 >>
조회 결과를 담기 위해 DTO의 멤버 변수 이름에 맞춰서 SELECT 하도록 지정함
이제 DAO 를 추가해 컨트롤러에서 쿼리를 실행하고 결과를 받아올 수 있도록 하자
package com.study.chapter04;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryDao {
List<CategoryDto> selectAllCategory();
}
마지막으로 컨트롤러를 추가해 시작 하면서 보여줬던 출력 예시처럼 출력하도록 만들자
이건 보지말고 여러분이 직접 만들어보자
package com.study.chapter04;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CategoryController {
@Autowired
CategoryDao categoryDao;
@GetMapping("/chatper04/categories")
public void printAllCategory() {
List<CategoryDto> categories = categoryDao.selectAllCategory();
String prevDepth1CategoryName = "";
String prevDepth2CategoryName = "";
String prevDepth3CategoryName = "";
for(CategoryDto category : categories) {
String nowDepth1CategoryName = category.getDepth1CategoryName();
if(!nowDepth1CategoryName.equals(prevDepth1CategoryName)) {
prevDepth1CategoryName = nowDepth1CategoryName;
System.out.println(nowDepth1CategoryName);
} // end if
String nowDepth2CategoryName = category.getDepth2CategoryName();
if(nowDepth2CategoryName != null) {
if(!nowDepth2CategoryName.equals(prevDepth2CategoryName)) {
prevDepth2CategoryName = nowDepth2CategoryName;
System.out.println("\t" + nowDepth2CategoryName);
}
String nowDepth3CategoryName = category.getDepth3CategoryName();
if(nowDepth3CategoryName != null) {
if(!nowDepth3CategoryName.equals(prevDepth3CategoryName)) {
prevDepth3CategoryName = nowDepth3CategoryName;
System.out.println("\t\t" + nowDepth3CategoryName);
} // end if
} // end if
} // end if
} // end for
}
}
이 컨트롤러를 완성했다고 해도 시간이 상당히 오래 걸렸을 것
그 이유는 카테고리는 우리가 본 형태는 계층적 데이터 구조이고 구현도 계층적 데이터 구조로 구현했음
그러나 자바로 가져올 때는 계층적 데이터 구조가 아닌 선형적 데이터 구조로 가져왔기 때문임
이렇게 계층적 데이터 구조를 그대로 가져오려면 resultMap 태그를 사용해야함
resultMap을 사용해 계층적 데이터 구조를 그대로 가져와보자
계층적 데이터 구조로 가져오기 위해 DTO를 새로 선언해야함
package com.study.chapter04;
import java.util.List;
import lombok.Data;
@Data
public class CategoriesDto {
private int idx;
private String name;
List<CategoriesDto> childCategories;
}
<< 코드 설명 >>

(1). n차 카테고리의 번호와 이름을 저장하기 위한 멤버 변수
(2). n차 카테고리의 하위 카테고리인 n+1차 카테고리의 정보를 저장하기 위한 멤버 변수
이제 쿼리에서 CategoriesDto 를 사용해 SELECT의 결과를 계층적 데이터 구조로 가져오도록 하자
<?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="com.study.chapter04.CategoryDao">
<resultMap type="com.study.chapter04.CategoriesDto" id="CategoriesDto">
<id column="depth1CategoryIdx" property="idx" jdbcType="INTEGER" />
<result column="depth1CategoryName" property="name" jdbcType="VARCHAR" />
<collection property="childCategories" ofType="com.study.chapter04.CategoriesDto">
<id column="depth2CategoryIdx" property="idx" jdbcType="INTEGER" />
<result column="depth2CategoryName" property="name" jdbcType="VARCHAR" />
<collection property="childCategories" ofType="com.study.chapter04.CategoriesDto">
<id column="depth3CategoryIdx" property="idx" jdbcType="INTEGER" />
<result column="depth3CategoryName" property="name" jdbcType="VARCHAR" />
</collection>
</collection>
</resultMap>
<select id="selectAllCategory" resultMap="CategoriesDto">
SELECT
depth1.idx AS depth1CategoryIdx, depth1.name AS depth1CategoryName,
depth2.idx AS depth2CategoryIdx, depth2.name AS depth2CategoryName,
depth3.idx AS depth3CategoryIdx, depth3.name AS depth3CategoryName
FROM category depth1
LEFT JOIN category depth2
ON depth1.idx = depth2.parent_idx
LEFT JOIN category depth3
ON depth2.idx = depth3.parent_idx
WHERE depth1.parent_idx IS NULL;
</select>
</mapper>
<< 코드 설명 >>

(1). SELECT 결과를 계층적 데이터 구조로 담기 위해 선언한 resultMap을 사용하도록 바꿈
(2). SELECT 결과를 계층적 데이터 구조로 담기 위해 resultMap 선언
이 구조를 제대로 이해하려면 SELECT의 결과와 함께 대조해야함

SELECT 결과를 보면 중복을 제거하고 보면 1차 카테고리가 3개로 나뉨
resultMap은 중복을 제거하고 1차 카테고리 1개당 DTO ( type 속성으로 지정한 com.study.chapter04.CategoriesDto ) 하나에 담에 총 3개의 1차 카테고리 정보를 담고 있는 List가 만들어짐

그 다음 1차 카테고리가 같은 2차 카테고리의 정보를 1차 카테고리 정보를 담고 있는 DTO의 childCategories 멤버 변수 안에 담음
2차 카테고리 역시 중복을 제거한 후 2차 카테고리 1개당 DTO 하나에 담음
- 1차 카테고리 idx가 1
- 1차 카테고리 name이 패션의류/잡화
- 자식 카테고리 4개 ( 여성패션, 남성패션, 남녀 공용 의류, 유아동패션 )
- 1차 카테고리 idx가 2
- 1차 카테고리 name이 뷰티
- 자식 카테고리 3개 ( 명품뷰티, 스킨케어, 클린/비건뷰티 )
- 1차 카테고리 idx가 3
- 1차 카테고리 name이 출산/유아동
- 자식 카테고리 3개 ( 유아동패션, 기저귀, 물티슈 )

마지막으로 2차 카테고리가 같은 3차 카테고리의 정보를 2차 카테고리 정보를 담고 있는 DTO의 childCategories 멤버 변수 안에 담음
3차 카테고리 역시 중복을 제거한 후 3차 카테고리 1개당 DTO 하나에 담음
- 2차 카테고리 idx가 4
- 2차 카테고리 name이 여성패션
- 자식 카테고리 4개 ( 의류, 속옷/잠옷, 신발, 가방/잡화 )
- 2차 카테고리 idx가 5
- 2차 카테고리 name이 남성패션
- 2식 카테고리 3개 ( 의류, 속옷/잡화, 신발, 가방/잡화 )
- 2차 카테고리 idx가 6
- 2차 카테고리 name이 남녀 공용 의류
- 2식 카테고리 3개 ( 티셔츠, 맨투맨/후드티, 셔츠 )
...
이제 컨트롤러에서 쿼리를 사용해 결과를 받을 수 있도록 DAO 도 수정하자
package com.study.chapter04;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryDao {
List<CategoriesDto> selectAllCategory();
}
마지막으로 계층적 구조로 담긴 모든 카테고리명을 출력하도록 컨트롤러를 수정하자
package com.study.chapter04;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CategoryController {
@Autowired
CategoryDao categoryDao;
private void printCategory(CategoriesDto category, int depth) {
String tab = "";
for(int i=1; i<depth; i++) {
tab = tab + "\t";
}
System.out.println(tab + category.getName());
List<CategoriesDto> childCategories = category.getChildCategories();
if(childCategories != null) {
for(CategoriesDto childCategory : childCategories) {
printCategory(childCategory, depth+1);
}
}
}
@GetMapping("/chatper04/categories")
public void printAllCategory() {
List<CategoriesDto> allCategory = categoryDao.selectAllCategory();
for(CategoriesDto category : allCategory) {
printCategory(category, 1);
}
}
}
<< 코드 설명 >>

(1). 출력할 카테고리 정보
(2). 출력할 카테고리의 차수
(3). 카테고리 차수에 맞춰 tab 을 추가
(4). 현재 카테고리 명 출력
(5). 현재 카테고리의 자식 카테고리가 있다면 재귀호출 방식으로 자식 카테고리 정보 출력
계층적 데이터 구조의 경우 데이터를 가져오는 것 자체는 선형적 데이터 구조로 가져오는 방법이 쉽지만 활용할 때는 굉장히 고생함
그러나 계층적 데이터 구조로 가져오기 위해서는 어떻게 가져올 지 를 굉장히 고민해야함
경험상 계층적 데이터 구조는 그대로 계층적 데이터 구조로 가져오는게 전체적으로 드는 시간과 노력이 절약됨
'Spring + Boot > Boot-Chapter04' 카테고리의 다른 글
| Chapter04. Spring Boot - DB / MyBatis 심화, cache, cache-ref 태그 (2) | 2023.05.31 |
|---|---|
| Chapter04. Spring Boot - DB / MyBatis 심화, sql, include, property 동적 쿼리 (1) | 2023.05.30 |
| Chapter04. Spring Boot - DB / MyBatis 심화, SET 동적 쿼리 (0) | 2023.05.28 |
| Chapter04. Spring Boot - DB / MyBatis 심화, WHERE 동적 쿼리 (0) | 2023.05.28 |
| Chapter04. Spring Boot - DB / MyBatis 심화, 동적 쿼리 (0) | 2023.05.28 |