Backend

[MyBatis] 동작원리, 사용방법 정리

연_우리 2022. 1. 27. 02:36
반응형

목차

     

    MyBatis 등장배경

     

    [JDBC] 사용방법

    JDBC : JAVA DataBase Connectivity 기존 자바에서는 DB를 조작하기 위해서 JDBC API를 사용했다. JDBC는 데이터베이스 종류에 상관없이 JDBC만 알면 어떤 데이터베이스를 사용하더라도 일관된 코드로 작성할

    lotuus.tistory.com

    기존의 JDBC API를 보면 DBMS와 연동하고 결과를 얻어오기위한 준비운동에 대한 코드가 절반이다.

    여기서 비즈니스 로직이 추가된다면 코드가 더 복잡해지는 것은 물론이고,

    한 파일 안에서 너무 많은 역할을 해내야한다 (DB연결.. DB결과값 처리... ResultSet과 객체 맵핑.. 객체를 가지고 비즈니스로직 구현...)

    또한 이 연결하는 코드가 다른 파일에서도 중복되어 나타나게된다. 핵심은 DB에서 가져온 값으로 비즈니스 로직을 구현하는 것인데..

    위와 같은 문제를 해소하고자 나온 것이 MyBatis이다.

     

    MyBatis는

    1. SQL문을 조작하기 전, 후의 Connection 및 객체 맵핑 등을 대신해주어 코드의 중복과 작성을 생략할 수 있게해준다.

    2. SQL문을 코드와 분리하여 유지보수성을 높여준다.

    3. SQL 실행 결과를 VO로 변환해주는 자동 Mapping을 지원해준다.

     

     


     

    MyBatis 동작원리

    1. 애플리케이션 시작 시 SqlSessionFactoryBuilder가 설정파일을 참고하여 SqlSessionFactory 생성

    2. 애플리케이션에서 DB작업 시 SqlSessionFactory가 SqlSession객체 생성

    3. 생성된 SqlSession을 참고해서 mapper인터페이스 호출 (SqlSession에 mapper정보가 내장되어있다)

    5. mapper가 SqlSession을 호출하여 SQL실행

     

     

     

     * DataSource

    JDBC 사용 시에는 데이터베이스에 접근할때마다 Connection을 연결하고 끊어주었다.

    이 Connection 작업을 줄이기 위해 미리 Connection을 생성해서 Connection Pool 이라는 곳에 저장하기로했고

    데이터베이스에 접근할때마다 Connection Pool에서 미리 생성된 Connection을 제공받고 다시 돌려주기로 했다.

    DataSource는 자바에서 Connection Pool을 지원해주는 인터페이스이다.

     

    Spring에서 DataSource는 application.properties파일로 설정 가능하다.

    spring.datasource.driver-class-name = 드라이버명
    spring.datasource.url = url입력
    spring.datasource.username = 유저명
    spring.datasource.password = 비밀번호

     

     * SqlSession

    MyBatis의 컴포넌트로 SQL을 실행하고 트랜잭션을 제어해준다.

    Spring Boot에서는 Spring-Boot-Starter-MyBatis를 dependency해주면 별다른 설정 없이 바로 사용 가능하다

     

     

     * Mapper.xml Or Mapper Interface

    실제 SQL구문 작성하는 곳이다. 아래에서 자세히 살펴보자

     

     


     

    MyBatis 연동 및 사용방법 (Spring Boot 기준)

    1. build.gradle에 mybatis-spring-boot-starter와 자신이 사용할 DMBS의 의존성을 추가한다.

    (maven repository에서 확인 가능)

    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.1'
    
    implementation 'org.mariadb.jdbc:mariadb-java-client:2.7.5'

     

    2. application.properties 파일에 DataSource 설정을 추가한다.

    spring.datasource.driver-class-name = org.mariadb.jdbc.Driver
    spring.datasource.url = jdbc:mariadb://localhost:3306/test
    spring.datasource.username = root
    spring.datasource.password = 1234
    
    mybatis.type-aliases-package = com.example.demo.domain
    # xml mapper에서 resultType에 Member만 적을 수 있게해준다.
    # type-aliases-package를 작성하지 않으면 
    # com.example.demo.domain.Member 이런식으로 패키지 경로를 전부 써주어야한다.
    
    mybatis.mapper-locations = mybatis/**/*.xml
    # resources 폴더 밑에 mybatis라는 폴더가 있는데
    # mybatis 폴더 하위 레벨에 상관없이 모두, 어떤 이름을 가진 xml파일도 모두 가져올 수 있다.

     

    3. 테스트용 테이블 생성

     

    4. 테스트용 domain 생성

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public Member(Long id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
        // 생성자에 대해서 할말이 많다..ㅜㅜ 맵핑부분 확인하자
    
    }

     

    5. 테스트용 repository, mapper.xml 생성

    package com.example.demo.repository;
    
    import com.example.demo.domain.Member;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Insert("insert into member(name, age) values(#{name}, #{age})")
        @Options(useGeneratedKeys = true, keyProperty = "id")
        void insertAnno(Member member);
    
        @Select("select * from member where id=#{id}")
        Member selectAnno(Long id);
    
    	//mapper참고
        void insertXml(Member member);
    
    	//mapper참고
        Member selectXml(Long id);
    
    }
    /resources/mybatis/MemberMapper.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.example.demo.repository.MemberRepository">
        <insert id="insertXml" useGeneratedKeys="true" keyProperty="id">
            insert into member(name, age) values(#{name}, #{age})
        </insert>
    
        <select id="selectXml" resultType="Member" parameterType="Long">
            select * from member where id = #{id}
        </select>
    </mapper>

     

     

    6. 테스트용 컨트롤러 생성

    package com.example.demo.controller;
    
    import com.example.demo.domain.Member;
    import com.example.demo.repository.MemberRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class MemberController {
        @Autowired private MemberRepository memberRepository;
    
        @GetMapping("/member")
        public String memberTest(){
            Member memberA = new Member("Lisa", 20);
            Member memberB = new Member("Alice", 22);
    
            memberRepository.insertAnno(memberA);
            memberRepository.insertXml(memberB);
    
            Member memberC = memberRepository.selectAnno(memberA.getId());
            Member memberD = memberRepository.selectAnno(memberB.getId());
            System.out.println(memberC);	//Member(id=1, name=Lisa, age=20)
            System.out.println(memberD);	//Member(id=2, name=Alice, age=22)
    
            return "Ok";
        }
    }

     

     

    MyBatis 간략한 사용방법

    mapper는 2가지 방식이 있다. @Mapper를 이용한 어노테이션 방식, Xml을 이용한 방식

     

    1. @Mapper를 이용한 어노테이션 방식

    @Mapper는 @Repository와 함께 작성하면되고 interface로 선언해야한다.

    그러면 스프링부트에서 자동으로 Mapper로 인식해서 다른 설정 없이도 DBMS 조작이 가능하다.

     

    @Select(), @Insert(), @Update(), @Delete() 각각 알맞는 SQL문을 작성하면된다.

    파라미터를 넘길때 #{파라미터명}은 ' '이 붙고, ${파라미터명}은 ' '이 붙지 않는다.

     

    @Insert문은 @Options(useGeneratedKeys = true, keyProperty = "id")와 같이 쓰일 때가 많은데

    keyProperty의 id는 데이터베이스에서 AUTO_INCREMENT로 지정해두었기때문에 데이터베이스에 삽입되어야만 생성되는 값이다. 이 값을 가져오려면 삽입하고 다시 select해야하나?? 라는 생각도 드는데 useGeneratedKeys를 true로 설정하면 select 하지 않아도 Member에 자동으로 id값이 들어가게해준다.

     

    2. XML을 이용한 방식

    XML파일에 <mapper>태그를 작성하고 namespace에 Repository의 패키지경로를 적는다.

     

    <select>, <insert>, <update>, <delete> 태그 안에 알맞는 SQL문을 작성하면된다.

    파라미터는 어노테이션 방식과 동일하게 #, $를 사용한다.

     

    주로 간단한 SQL문은 어노테이션 방식으로, 동적인 처리가 이루어져야하는 SQL문은 XML에 작성하기때문에

    연관관계 맵핑에 해당하는 <resultMap>, <association>, <collection>

    동적으로 쿼리를 조작하는 <trim>, <if>, <choose>, <where> 등을 사용하게된다.


    동적 SQL

     

    MyBatis – 마이바티스 3 | 동적 SQL

    동적 SQL 마이바티스의 가장 강력한 기능 중 하나는 동적 SQL을 처리하는 방법이다. JDBC나 다른 유사한 프레임워크를 사용해본 경험이 있다면 동적으로 SQL 을 구성하는 것이 얼마나 힘든 작업인지

    mybatis.org


     

    MyBatis는 어떻게 컬럼을 맵핑할까?

    (사실 내가 엄청 헤맨 내용이다..)

     

    나는 Member객체를 수정할 수 없게 만들고 싶다.

    생성 시에 필요한 name, age에 대한 생성자만 만들어놓고 setter도 없는 상태이다.

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.ToString;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    package com.example.demo.repository;
    
    import com.example.demo.domain.Member;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Select("select * from member")
        Member selectAnno(Long id);
    }

    이 상태에서 selectAnno를 실행해보자

     

    아래와 같은 오류가 발생한다. DB에서 SQL문을 실행했을 땐 결과가 잘 나온다.. ㅎㅎ 왜 맵핑이 안되는걸까

    Error attempting to get column 'name' from result set. Cause: java.sql.SQLException: Out of range value for column 'name' : value Lisa ; Out of range value for column 'name' : value Lisa; nested exception is java.sql.SQLException: Out of range value for column 'name' : value Lisa
    org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'name' from result set.  Cause: java.sql.SQLException: Out of range value for column 'name' : value Lisa
    ; Out of range value for column 'name' : value Lisa; nested exception is java.sql.SQLException: Out of range value for column 'name' : value Lisa
    
    ..... 중략 ....
    
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.15.jar:5.3.15]
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.15.jar:5.3.15]
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.15.jar:5.3.15]
    	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-5.3.15.jar:5.3.15]
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.15.jar:5.3.15]
    	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.15.jar:5.3.15]
    	at com.sun.proxy.$Proxy110.selectAnno(Unknown Source) ~[na:na]
    	at com.example.demo.controller.MemberController.memberTest(MemberController.java:25) ~[main/:na]

     

    나도 바보같았던게ㅜ 단순히 select할때 id, name, age 모두 받아오니 필드에 맵핑되서 객체 짠! 하고 나올줄알았는데

    조금만 생각해보면 기본 생성자도, 모든 필드가 있는 생성자도 아닌 생성자가 버젓이 있는데 객체가 나오는것도 이상하다..

     

    MyBatis는 컬럼을 아래와 같이 맵핑한다.

    1. 클래스에 setter가 있으면 setter를 호출한다.

    2. setter가 없다면 필드 이름으로 맵핑한다.

    3. 직접 정의한 생성자(모든 필드가 있는 생성자 포함)는 DB 출력 컬럼순서와 생성자에 정의된 파라미터 순서가 같아야한다.

    4. 기본 생성자 또는 순서를 맞춘 모든 필드가 있는 생성자를 반드시 생성해주자

     

     


     

    MyBatis 1:1 관계 맵핑

    xml에서 <resultMap>안에 <Association>  태그를 사용하자.

     

    member 테이블

    address 테이블

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.Collections;
    import java.util.List;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
        private Address address;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
    //    public Member(Long id, String name, int age, Address address) {
    //        this.id = id;
    //        this.name = name;
    //        this.age = age;
    //        this.address = address;
    //    }  
        // No constructor found in com.example.demo.domain.Member matching [java.lang.Long, java.lang.String, java.lang.Integer, java.lang.Long, java.lang.Long, java.lang.String]
    
        public Member() {
        }
        //OK
    }
    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.ToString;
    
    @Getter @ToString
    public class Address {
        private Long memberId;
        private Long postcode;
        private String detailAddress;
    
        public Address(Long postcode, String detailAddress) {
            this.postcode = postcode;
            this.detailAddress = detailAddress;
        }
    
    //    public Address(Long memberId, Long postcode, String detailAddress) {
    //        this.memberId = memberId;
    //        this.postcode = postcode;
    //        this.detailAddress = detailAddress;
    //    } 
    
        public Address() {
        }
        //OK
    }
    package com.example.demo.repository;
    
    import com.example.demo.domain.Member;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    import java.util.Map;
    
    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Select("select member.*, address.* " +
                "from member join address on member.id=address.memberid " +
                "where member.id = #{id}")
        @ResultMap("MemberAddress")
        Member selectAnno(Long id);
    
    }
    <?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.example.demo.repository.MemberRepository">
    
        <resultMap id="Address" type="Address">
            <result property="memberId" column="memberid"></result>
            <result property="postcode" column="postcode"></result>
            <result property="detailAddress" column="detailaddress"></result>
        </resultMap>
        <resultMap id="MemberAddress" type="Member">
            <id property="id" column="id"></id>
            <result property="name" column="name"></result>
            <result property="age" column="age"></result>
            <association property="address" resultMap="Address"></association>
        </resultMap>
    
    </mapper>

    <association>은 특이하게 모든 필드의 생성자가 있으면 작동하지 않았다

    Member, Address 둘다 모든 필드의 생성자가 있으면 오류가 발생했고

    Member, Address 둘다 기본 생성자만 있어야지 오류가 발생하지 않는다.

     

    흠 어차피 member와 address에 겹치는 필드명이 없으니까 resultmap을 안써도 되지않을까?! 했는데

    address가 null로 나온다.

     

     


    MyBatis 1:N 관계 맵핑

    xml에서 <resultMap>안에 <Collection> 태그를 사용하자.

    <Collection>에 대응되는 @Results 어노테이션은 사용하지말자! select를 지정하면서 N+1 문제가 발생할 수 있다.

     

    객체 안에 기본타입 리스트일때 ( List<String> )

    member 테이블

    book 테이블

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.Collections;
    import java.util.List;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
        private List<String> bookname;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public Member() {
        }
        //★★★★★
    }
    package com.example.demo.repository;
    
    import com.example.demo.domain.Member;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    import java.util.Map;
    
    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Select("select member.*, book.bookname " +
                "from member join book on member.id=book.memberid " +
                "where member.id = #{id}")
        @ResultMap("MemberBookList")
        Member selectAnno(Long id);
    
    }
    <?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.example.demo.repository.MemberRepository">
    
        <resultMap id="MemberBookList" type="Member">
            <id property="id" column="id"></id>
            <result property="name" column="name"></result>
            <result property="age" column="age"></result>
            <collection property="bookname" ofType="String" javaType="List">
                <result column="bookname"></result>
            </collection>
            
            <!-- <collection property="bookname" column="bookname" ofType="String" javaType="List"></collection> -->
            <!-- 위에처럼도 가능함 -->
        </resultMap>
    
    </mapper>

     

     

    여기서 이것저것 해보다가 이런 오류가 났다

    No constructor found in com.example.demo.domain.Member matching [java.lang.Long, java.lang.String, java.lang.Integer, java.lang.String]

     

    그래서 혹시나 하는 마음에 해봤더니..

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.Collections;
    import java.util.List;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
        private List<Book> bookname;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public Member(Long id, String name, int age, String bookname) {
            this.id = id;
            this.name = name;
            this.age = age;
            //엇..? 된다
        }//★★★★★★
    }

    이렇게 해도 리스트를 받아올 수 있었다 (?!)

    생성자는 String으로 bookname을 받지만 값을 저장하지는 않는다.

    마이바티스를 속이는 꼼수를 알아낸느낌..?ㅋㅋㅋㅋ

     

    객체 안에 객체타입 리스트일때 ( List<Object> )

    member 테이블

    book 테이블

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.Collections;
    import java.util.List;
    
    @Getter @ToString
    public class Member {
        private Long id;
        private String name;
        private int age;
        private List<Book> books;
    
        public Member(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public Member() {
        }
        //★★★★★
    
    }
    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.ToString;
    
    @Getter @ToString
    public class Book {
        private Long memberId;
        private String bookName;
        private int price;
    
        public Book(String bookName, int price) {
            this.bookName = bookName;
            this.price = price;
        }
    
        public Book() {
        }//★★★★★
    
    }
    package com.example.demo.repository;
    
    import com.example.demo.domain.Member;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    import java.util.Map;
    
    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Select("select member.*, book.* " +
                "from member join book on member.id=book.memberid " +
                "where member.id = #{id}")
        @ResultMap("MemberBookList")
        Member selectAnno(Long id);
    
    }
    <?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.example.demo.repository.MemberRepository">
    
        <resultMap id="BookList" type="Book">
            <result property="memberId" column="memberid"></result>
            <result property="bookName" column="bookname"></result>
            <result property="price" column="price"></result>
        </resultMap>
        
        <resultMap id="MemberBookList" type="Member">
            <id property="id" column="id"></id>
            <result property="name" column="name"></result>
            <result property="age" column="age"></result>
            <collection property="books" resultMap="BookList"></collection>
        </resultMap>
    
    </mapper>

    둘다 기본 생성자가 있을 때 받아올 수 있다! 그렇지 않으면 book에 오류가 있어도 member에서 오류가 걸려버리더라..

     

    그리고 이것도 꼼수로 할 수 있는 것같다

    package com.example.demo.domain;
    
    import lombok.Getter;
    import lombok.ToString;
    
    @Getter @ToString
    public class Book {
        private Long memberId;
        private String bookName;
        private int price;
    
        public Book(String bookName, int price) {
            this.bookName = bookName;
            this.price = price;
        }
    
    //    public Book(Long a, String b, int c, Long d, String e, int f) {
    //
    //    }
        //No constructor found in com.example.demo.domain.Book matching [java.lang.Long, java.lang.String, java.lang.Integer, java.lang.Long, java.lang.String, java.lang.Intege
        //가짜 생성자..
    }

    이 방법은 웬만해서는 사용하지 않는게 좋을 것 같다

     


    MyBatis를 쓰면서 헷갈리던 것들을 쭉 정리해봤다

     

    요약하자면, 

    1:1 맵핑 시에는 <association>

    연관되는 객체 모두에게 빈 생성자만(!!) 있어야 한다.

    모든 필드가 있는 생성자는 오류가 발생한다

     

    1:N 맵핑 시에는 <collection>

    연관되는 객체 모두에게 빈 생성자 또는 모든 필드가 있는 생성자가 있어야 한다.

     

     

    마음 편하게 빈 생성자는 꼭 선언해주자..!

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기