myBatis에서 제공하는 자동 매핑으로 해결이 어려운 경우를 위해 구조를 설계할 수 있도록 만들어진 도구이다.
ResultMap이 필요한 경우
ResultMap은 다음과 같은 데이터 구조를 불러올 때 적합하다.
계층형 데이터 구조
객관식 시험과 관련된 정보들을 데이터베이스에 추가한다면 다음과 같은 구조를 가지게 된다.
- 1번 시험문제
- 1번 보기
- 2번 보기
- 3번 보기
- 4번 보기
- 2번 시험문제
- 1번 보기
- 2번 보기
- 3번 보기
- 4번 보기
시험이라는 상위 entity와 보기라는 하위 entity로 나누어 생각해본다면 다음과 같이 이해할 수 있다.
물리 테이블은 다음과 같이 구성될 것이다.
문제 테이블(quiz)
문제번호(no)문제내용(text)
1
1번 문제
2
2번 문제
보기 테이블(choice)
보기번호(no)보기내용(text)문제번호(quiz)
1
1번 보기
1
2
2번 보기
1
3
3번 보기
1
4
4번 보기
1
5
1번 보기
2
6
2번 보기
2
7
3번 보기
2
8
4번 보기
2
위의 테이블 구조를 만약 join으로 불러온다면 어떻게 될까?
select
문제번호, 문제내용, 보기번호, 보기내용
from
문제 inner join 보기 on 문제.문제번호 = 보기.문제번호;
다음과 같은 결과가 나올 것이다. 문제번호문제내용보기번호보기내용
1
1번 문제
1
1번 보기
1
1번 문제
2
2번 보기
1
1번 문제
3
3번 보기
1
1번 문제
4
4번 보기
2
2번 문제
5
1번 보기
2
2번 문제
6
2번 보기
2
2번 문제
7
3번 보기
2
2번 문제
8
4번 보기
위의 결과를 가지고 다음 화면을 출력할 수 있는지 따져봐야 한다. join을 이용해서 위의 화면을 출력하는 것은 가능은 하지만 어렵다. 조건들이 많이 등장할 것이며, 코드는 지저분하게 표시된다. 다음 화면은 만들 수 있겠지만 우리가 원하는 화면은 아니다. 따라서 데이터를 화면과 같은 구조로 불러오고 싶은 경우 join 대신 ResultMap을 이용하면 좋다.
ResultMap을 위한 클래스 구성
- 1번 시험문제
- 1번 보기
- 2번 보기
- 3번 보기
- 4번 보기
- 2번 시험문제
- 1번 보기
- 2번 보기
- 3번 보기
- 4번 보기
위와 같은 구조에서 시험 문제의 입장으로 바라본다면 다음과 같이 구성되어 있다고 생각할 수 있다.
- 1번 시험문제
- List<보기>
- 2번 시험문제
- List<보기>
따라서 시험 문제와 보기의 관점에서 각각 저장할 클래스를 만들어 myBatis가 구조를 이해할 수 있도록 구성한다.
class Choice : 보기 클래스
public class Choice{
private int no;
private String text;
}
보기 클래스는 Choice라는 이름으로 작성하였으며, 보기 번호와 내용을 가질 수 있도록 구성하였다.
class Quiz : 문제 클래스
public class Quiz{
private int no;
private String text;
private List<Choice> choices;
}
문제 클래스는 Quiz라는 이름으로 작성하였으며, 퀴즈 번호와 내용, 그리고 보기 목록(List<Choice>)을 가질 수 있도록 구성하였다. 이 Quiz라는 형태를 myBatis에 ResultMap으로 등록하여 구문과 같이 연결해두면 계층형 데이터를 쉽게 불러올 수 있다.
ResultMap 설정
ResultMap은 mapper에 설정한다. quiz-mapper.xml이라는 파일을 만들고 내부에 다음과 같이 작성한다.
<!-- 기본 설정 생략 -->
<mapper namespace="quiz">
<resultMap type="com.hakademy.vo.Quiz" id="quiz">
<result column="no" property="no"/>
<result column="text" property="text"/>
<collection column="no" property="choices" javaType="java.util.List" ofType="com.hakademy.vo.Choice" select="getChoiceList">
</collection>
</resultMap>
<select id="getQuizList" resultMap="quiz">
select * from quiz order by no asc
</select>
<select id="getChoiceList" parameterType="int" resultType="com.hakademy.vo.Choice">
select * from choice where quiz = #{quiz}
</select>
</mapper>
해당 구문을 호출할 때에는 다음과 같이 작성한다.
List<Quiz> list = sqlSession.selectList("quiz.getQuizList");
ResultMap 설정 상세 설명
메인 select 구문
<select id="getQuizList" resultMap="quiz">
select * from quiz order by no asc
</select>
메인 select 구문이며, 실행 결과 형태는 미리 정의된 quiz라는 id를 가진 resultMap이 할당된다. 이 구문을 실행한 결과를 myBatis가 자동으로 quiz에 맞게 처리하게 된다.
Controller 메소드에 @ModelAttribute UserForm userForm 을 파라미터로 선언하고, jsp 단에 <form:spring modelAttribute="userForm"/> 스프링 폼태그를 사용해 본적이 있는가?
이때 컨트롤러단으로 요청이 들어오면 jsp에서 입력한 값이 modelAttribute 로 지정된 객체의 필드값에 매핑 저장되어 파라미터로 넘어오는 것을 볼 수 있다. 바로 여기서 들어온 요청에 대해 modelAttribute 로 선언된 객체의 필드값이 어떻게 매핑되는지 그 과정을 생각해 본 적이 있는 분!
.
.
.
.
.
메소드에 @ModelAttribute 를 선언했을 때 처리되는 과정은 다음과 같다.
1. 파라미터 타입의 객체를 하나 생성한다.
2. HTTP 요청에서 가져온 객체의 프로퍼티에 바인딩(값을 매긴다) 해준다. 이 과정에서 각 프로퍼티에 맞게 타입을 변환해준다. 만약 타입 변환 오류가 생기면 BindingResult 객체에 error 를 저장하여 컨트롤러로 넘겨준다.
(아~~ 너무 추상적이다..)
파라미터로 넘어온 친구들에 대해 프로퍼티 바인딩을 해준다. 즉, 각 프로퍼티에 맞게 타입을 변환해준다는 거군.
(그럼 '프로퍼티에 바인딩'한다는 건 머선 말이지..)
.
.
.
.
.
프로퍼티에 바인딩한다? - 2가지 방법
프로퍼티 바인딩이란 오브젝트의 프로퍼티(필드값)에 값을 넣는 행위를 말하는 것이다.
각각의 필드에 맞게 타입을 적절히 변환하고 프로퍼티의 커스텀메소드를 호출하는 것이다.
스프링에선 크게 두가지 프로퍼티 바인딩을 지원한다.
1. applicationContext.xml 의 설정파일로 Bean 을 정의할 때 사용한 <property> 태그이다.
PropertyEditor 는 스프링 API가 아니라 자바빈 표준에 정의된 API이다. GUI 환경에서 비주얼 컴포넌트를 만들 때 사용하도록 설계되었고, 기본적인 기능은 문자열과 자바빈 프로퍼티 사이의 타입 변환이다. 스프링은 이 PropertyEditor 를 문자열-오브젝트 상호변환이 필요한 XML 설정이나 HTTP 파라미터 변환에 유용하게 사용할 수 있다고 판단하여 이를 일찍부터 사용해왔다.
핵심은 이 디폴트 PropertyEditor들은 바인딩 과정에서 파라미터 타입에 맞게 자동으로 선정되어 사용된다는 것이다.
디폴트 프로퍼티 데이터에 등록되지 않은 타입을 파라미터로 사용하고 싶다면 직접 PropertyEditor 를 구현하여 등록하고 사용할 수 있다.
WebDataBinder
직접 구현한 에디터를 구현하기 전에 Controller 에서 메소드 바인딩하는 과정을 먼저 살펴보자.
AnnotationMethodHandlerAdapter 는 @RequestParam, @PathVariable, @ModelAttribute 와 같이 HTTP 요청을 변수에 바인딩하는 어노테이션을 만나면 먼저 WebDataBinder 라는 것을 만든다.
WebDataBinder 는 여기서 HTTP 요청 문자열을 파라미터로 변환하는 기능을 한다.
-> WebDataBinder (이 친구) 때문에 바로 이 글을 시작할 때 언급했던 ModelAttribute 를 선언한 메소드의 객체 필드에 매핑이 되는 것이다.
이때, 직접 구현한 PropertyEditor 를 사용하려면 이 WebDataBinder 에 직접 등록해줘야 한다.
근데 WebDataBinder 의 변환 과정이 외부로 노출되지 않으므로, 직접 등록해줄 방법은 없다.
그래서 스프링이 제공하는 WebDataBinder 초기화 메서드를 사용해야 한다.
.
.
.
.
@InitBinder
Controller 클래스에 아래와 같이 @InitBinder 어노테이션이 부여되고, WebDataBinder 를 매개변수로 받는 메소드를 하나 생성해 봅니다.
@Controller
public class Controller {
// 모든 요청이 들어올때마다 해당 method 를 거친다.
// 모든 컨트롤러 내에서 변환 하려면 ConfigurableWebBindingInitializer 를 설정해서 사용해야 한다.
// 특정 컨트롤러 내에서만 변환 하려면 컨트롤러에 @InitBinder가 붙은 메서드를 작성하여 사용하면 된다.
@InitBinder
private void initBinder(WebDataBinder binder) {
// , 구분자로 배열화하는 것을 방지한다.
binder.registerCustomEditor(String[].class , new StringArrayPropertyEditor(null));
}
.
.
@RequestMapping("json/binder.do")
public String binder(@RequestParam(value = "param", required = false) String[] param) {
//...
}
}
initBinder 메서드는 클래스내의 모든 메서드에 대해서 파라미터를 바인딩하기 전에 자동으로 호출된다. 바인딩 적용 대상은 @RequestParam, @PathVariable, @CookieValue, @RequestHeader, @ModelAttribute의 프로퍼티 이다.
기본적으로 PropertyEditor는 지정한 타입과 일치하면 항상 적용된다. 여기에 프로퍼티 이름을 추가 조건으로 주고, 프로퍼티 이름까지 일치해야만 적용되게 할 수 있다. 이러한 타입의 PropertyEditor는 이미 PropertyEditor가 존재할 경우 사용한다. WebDataBinder는 바인딩 시 커스텀 PropertyEditor가 있을 경우 이를 선적용하고, 없을 경우 디폴트 PropertyEditor를 적용하기 때문이다.
.
.
.
.
.
+) 추가 예시 설명
binder.registerCustomEditor(String[].class , new StringArrayPropertyEditor(null));
해당 customEditor 를 등록해주지 않은 경우
@RequestParam 에 배열타입으로(String[ ]) 들어오는 파라미터에 쉼표(,) 구분자가 있는 경우 디폴트(PropertyEditor)가 적용되어 무조건 쉼표(,) 구분자(쉼표가 default인 듯)에 의해 배열화 된다.
registerCustomEditor 인자로 String[].class , new StringArrayPropertyEditor(null) 값을 넣으면,
String [] 배열 타입에 대해 어떤 구분자로도 배열화하지 않겠다고 선언하는 것이다. (null 자리의 인자값은 seperator임)
필자가 원하는 값은 파라미터에 쉼표가 존재하더라도 하나의 문자열로 보고 구분하지 않도록 하는 것이었기 때문에 위와 같이 적용해주었다.
그러니까 본디 그 뜻을 보아하니.. 뭔가 연결시키고, 연관시키고, 하나로 꽉 묶고, 결합시키고 하는 건가보네?
개발적 언어의 의미
속성과 개체 사이 또는 연산과 기호 사이와 같은 연관이다. ( -_- 무슨 말? ) 즉, 바인딩(binding) 이란 프로그램의 어떤 기본 단위가 가질 수 있는 구성요소의 구체적인 값, 속성을 확정하는 것(줄로 꽉 묶는다는 뜻 연상)을 말한다. ( 오케이, 앞에거 모르겠고. 일단 느낌은 "값을 매긴다, 확정한다" 요거 구먼. )
2. 예를 들어보자면~
프로그램의 기본 단위인 변수를 예로 들면,
int num = 123;
여기서 int 는 변수의 자료형, num 은 변수 이름, 123 은 변수의 자료값이다.
즉, 데이터 타입이 int 라는 것으로 바인딩되고, a 라는 변수명에 바인딩 되고, 1 이라는 값이 바인딩 되는 것이다.
(아하 ~~ 너낌 와쒀. 한마디로 정리해서)
이름, 자료형, 자료값에 각각 num, int, 123 이라는 구체적인 타입, 이름, 값이 정해지고 메모리 할당하는 것 각각의 과정을 바. 인. 딩. 이라고 한다.
3. 두가지 바인딩
조금 더 깊이 들어가자면, 일반적으로 바인딩은 일어나는 시간에 따라 크게 정적 바인딩, 동적 바인딩으로 분류한다.
정적 바인딩(Static Binding)
컴파일 시간에 일어나며, 실행 중 변하지 않고 유지된다.
- 함수의 정적 바인딩은 컴파일 시간에 호출될 해당 함수의 주소가 결정되어 바인딩 된다. 즉, 실행 파일에 호출할 함수가 위치한 메모리 주소가 이미 확정 기록된 것이다.
동적 바인딩(Dynamic Binding)
- 실행시간 (run time) 중에 일어나며, 프로그램 실행 도중에 변경이 가능하다.
- 말그대로, 실행 파일을 만들때 호출할 함수의 메모리 주소가 확정되지 않고, 이후 실제로 실행되는 그 시간에 호출할 함수의 주소가 결정된다.
- 그렇기 때문에 이 주소를 저장할 공간을 미리 확보해둔다.
- 실행될지 안될지 확정되지 않았기에 일단, 해당 함수를 위해 저장공간을 할당해야한다는 점에서 메모리 관리에 비효율적이다.
(쏼라~쏼라~~ 메모리 관리에 효율적이냐 비효율적이냐를 말한것이군.. 아무튼 한줄 요약하자면~~)
실행 이전(컴파일될때)에 값이 확정되면 정적 바인딩 , 이후(진짜 코드가 실행될때)에 값이 확정되면 동적 바인딩인 것이다.
4. 결론
아주 쉽고 간단하게 말해서, 개발에서 말하는 바인딩은 값이 확정되어 최종적으로 값이 매겨지는 것을 말하는 것이다.