본문 바로가기
실습/리눅스 서버 + 스프링 부트

Spring Boot + Mybatis

by 이민우 2023. 7. 2.
728x90
반응형

현재 회사에서 주로 하는 프로젝트는 빅데이터 혹은 사물인터넷 관련이다. 그러다보니 주로 데이터 저장에는 NoSQL을 사용하게 되고 RDB를 사용하면 동적으로 DDL을 생성해서 테이블을 create 하는 방법을 사용하고 있다. (빅데이터나 IoT는 데이터 형식에 따라 동적으로 테이블 구조가 결정되기 때문)

 

첫 프로젝트에서는 JPA를 사용해 개발을 진행했었다. 그 때 JPA의 편리한 사용법에 매료됐었지만, 아쉽게도 그 이후로는 JPA를 쓸 일이 없었다. 이유라면 위에서 언급했듯, 동적으로 DDL을 생성해서 테이블을 관리하기에 사전에 테이블 구조가 정해져 있어야 하는 JPA는 사용을 할 일이 없었다. 그래서 대신 MyBatis를 주로 사용했고, 오늘은 Spring Boot에서 MyBatis의 사용법을 포스팅 해볼까 한다.

 

MyBatis

MyBatis는 자바 개발자가 DB와의 상호작용을 쉽게 처리할 수 있게 해주는 오픈 소스 SQL 매핑 프레임워크이다. 개발자가 직접 SQL을 작성할 수 있게 해줌으로써, 각기 다른 DB 벤더와 상호작용하는 방법을 추상화하고 코드 중복을 최소화한다고 한다.

 

MyBatis 사용에 따른 장점과 단점은 아래와 같다.

  1. 개발자가 직접 SQL을 작성할 수 있기에 복잡한 쿼리 및 최적화가 필요한 상황에서 유리하다.
  2. 비교적 배우기 쉽고 단순하다.
  3. DB 테이블과 자바 객체간의 매핑을 유연하게 다룰 수 있다.

 

  1. 개발자가 직접 SQL을 작성해야 하기에 SQL에 익숙하지 않은 개발자라면 이 부분에서 2번의 장점이 사라질 수 있다.
  2. JPA와 비교했을 때 DB 연산에 대한 추상화 수준이 낮다.
iBatis vs MyBatis

처음 MyBatis를 썼을 때 구글링을 하던 중 iBatis라는 것이 있다는 것을 발견했고, 이게 뭐지? 라는 생각을 했었다. 그냥 단순하게 MyBatis(JDK 1.5 이상)는 iBatis(JDK 1.4 이상)의 다음 버전이다.

 

MyBatis 프로젝트 설정

MyBatis를 사용하는 방법은, 일단 내가 사용하는 방법을 기준으로 두 가지이다.

  • Mapper xml 파일에 SQL 을 작성해서 사용
  • SqlProvider 클래스를 만들어 Java 내에서 동적으로 쿼리를 생성해서 사용

오늘은 이 두 가지를 전부 다 실습해볼까 한다.

 

우선 스프링 부트 프로젝트를 생성한다.

 

프로젝트가 생성되는 동안 DB에 테스트를 위해 사용할 아래 테이블을 생성한다.

create table user (
	id varchar(20) not null primary key,
	age int null,
	birthday timestamp null default now()
);

 

프로젝트와 테이블 모두 생성 완료되었으면 우선 application.properties 파일에 DB 관련 설정을 해놓는다.

spring.datasource.url=jdbc:mariadb://localhost:3306/testdb
spring.datasource.username=root
spring.datasource.password=0000

 

그리고 아래와 같이 /src/main/resource 아래에 mapper 폴더를 생성한다. 앞서 말한 방법 중 xml을 이용한 방법에서 sql이 적힌 mapper 파일은 전부 이곳에 생성한다.

 

xml을 이용한 MyBatis 사용법

먼저 해볼 것은 xml을 이용한 방법이다.

먼저 dto 클래스를 작성한다.

 

UserDto.java

package com.mwlee.mybatis.user.dto;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
	private String id;
	private int age;
	private Date birthday;
}

그리고 Mapper XML을 작성한다.

Mapper XML은 아래 !DOCTYPE이 반드시 추가되어 있어야 한다.

 

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

 

 

이제 Mapper를 만들어보자.

 

UserMapper.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">
        
 <!-- 아래 namespace는  -->
 <mapper namespace="com.mwlee.mybatis.user.dao.UserDaoXml">
 
	<!-- parameterType : 파라미터 타입 지정 -->
 	<insert id="insertUser" parameterType="com.mwlee.mybatis.user.dto.UserDto">
 		INSERT INTO user
 		(id
 		<!-- null (int는 0)이 아니면 하나씩 추가 -->
 		<if test="user.age != null">
 			,age
 		</if>
 		<if test="user.birthday != null">
 			,birthday
 		</if>
 		)
 		VALUES
 		( 
 			#{user.id}
 			<if test="user.age != null">
 				,#{user.age}
 			</if>
 			<if test="user.birthday != null">
 				,#{user.birthday}
 			</if>
 		)
 	</insert>
 	
 	<!-- 만약 여러 개를 insert 하는 상황이라면 -->
 	<insert id="insertManyUser" parameterType="java.util.List">
 		INSERT INTO user
 		(id, age, birthday)
 		VALUES
 		<foreach collection="users" item="user" separator=",">
 			(
 				#{user.id}
 				<choose>
 					<when test="user.age != null">
 						, #{user.age}
 					</when>
 					<otherwise>
 						, null
 					</otherwise>
 				</choose>
 				<choose>
 					<when test="user.birthday != null">
 						, #{user.birthday}
 					</when>
 					<otherwise>
 						, null
 					</otherwise>
 				</choose>
 			)
 		</foreach>
 	</insert>
 	
 	<!-- 만약 ddl을 할 경우에도 그냥 update로 지정하면 됨. -->
 	<update id="updateUser" parameterType="com.mwlee.mybatis.user.dto.UserDto">
 		UPDATE user
 		SET
 			<if test="user.age != null">
 				age = #{user.age}
 			</if>
 			<if test="user.age != null and user.birthday != null">
 				<!-- 쉼표 추가 -->
 				,
 			</if>
 			<if test="user.birthday != null">
 				birthday = #{user.birthday}
 			</if>
 		WHERE
 			id = #{user.id}
 	</update>
 	
 	<delete id="deleteUser" parameterType="java.util.Map">
 		DELETE FROM user
 		WHERE
 			1=1
 			<if test="target.id != null">
 				AND id = #{target.id}
 			</if>
 			<if test="target.age != null">
 				AND age = #{target.age}
 			</if>
 			<if test="target.birthday != null and target.comparator != null">
 				<!-- #{} 사용 시 양쪽에 ' 가 삽입되지 않은 채로 나옴 -->
 				AND birthday ${target.comparator} #{target.birthday}
 			</if>
 			
 	</delete>
 
 	<!-- resulType : 반환 타입 지정 -->
 	<select id="selectUniqueUser" parameterType="java.util.Map" resultType="com.mwlee.mybatis.user.dto.UserDto">
 		SELECT *
 		FROM user
 		WHERE
 			1=1
 			<if test="target.id != null">
 				AND id = #{target.id}
 			</if>
 			<if test="target.age != null">
 				AND age = #{target.age}
 			</if>
 			<if test="target.birthday != null and target.comparator != null">
 				<!-- ${} 사용 시 양쪽에 ' 가 삽입되지 않은 채로 나옴 -->
 				AND birthday ${target.comparator} #{target.birthday}
 			</if>
 	</select>
 	
 	<select id="selectSeveralUser" parameterType="java.util.Map" resultType="com.mwlee.mybatis.user.dto.UserDto">
 		SELECT *
 		FROM user
 		WHERE
 			1=1
 			<if test="target.age != null">
 				AND age = #{target.age}
 			</if>
 			<if test="target.birthday != null and target.comparator != null">
 				<!-- ${} 사용 시 양쪽에 ' 가 삽입되지 않은 채로 나옴 -->
 				AND birthday ${target.comparator} #{target.birthday}
 			</if>
 	</select>
 
 </mapper>

 

간단하게 개별 create, 다중 create, update, delete, 개별 select, 다중 select를 구현했다.

참고해야 할 사항은 아래와 같다.

  • namespace는 반드시 해당 매퍼를 인터페이스로 만든 클래스의 경로와 일치해야 한다.
  • ${} 사용 시에는 양쪽에 ' 가 삽입되지 않은 상태로 출력된다.

 

그러면 이제 위에서 생성한 mapper를 인터페이스로 만든다.

 

UserDaoXml.java

package com.mwlee.mybatis.user.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.mwlee.mybatis.user.dto.UserDto;

@Mapper
public interface UserDaoXml {
	
	public int insertUser(@Param("user") UserDto user);
	public int insertManyUser(@Param("users") List<UserDto> users);
	public int updateUser(@Param("user") UserDto user);
	public int deleteUser(@Param("target") Map<String, Object> target);
	public UserDto selectUniqueUser(@Param("target") Map<String, Object> target);
	public List<UserDto> selectSeveralUser(@Param("target") Map<String, Object> target);
	
}

 

그리고 테스트 코드를 작성하고, 모든 로직이 정상 작동되는지 확인하면 끝이다.

	@Autowired UserDaoXml userDao;
	
	@Test
	void test() throws ParseException {
		SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");

		// 개별 insert 테스트
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("INSERT 테스트 : A가 추가되어야 함.");
		UserDto a = new UserDto("A", 29, date.parse("1995-08-28"));
		
		userDao.insertUser(a);
		// 전체 조회
		List<UserDto> users = userDao.selectSeveralUser(new HashMap<>());
		
		showUsers(users);
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		// 다중 insert 테스트 (하는김에 null 넣어서.)
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("INSERT 테스트 : B와 C가 추가되어야 함.");
		UserDto b = new UserDto("B", null, date.parse("1994-09-20"));
		UserDto c = new UserDto("C", 19, null);
		users = new ArrayList<>();
		users.add(b);
		users.add(c);
		
		userDao.insertManyUser(users);
		users = userDao.selectSeveralUser(new HashMap<>());
		
		showUsers(users);
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		// update 테스트
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("UPDATE 테스트 : B와 C가 NULL값이 없어야 함.");
		b.setAge(30);
		c.setBirthday(date.parse("2000-01-02"));
		
		userDao.updateUser(b);
		userDao.updateUser(c);
		
		users = userDao.selectSeveralUser(new HashMap<>());
		
		showUsers(users);
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		// delete 테스트
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("DELETE 테스트 : C가 없어져야 함.");
		
		Map<String, Object> target = new HashMap<>();
		target.put("birthday", date.parse("1999-01-01"));
		target.put("comparator", ">");
		
		userDao.deleteUser(target);
		users = userDao.selectSeveralUser(new HashMap<>());
		
		showUsers(users);
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		// select 테스트
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("SELECT 테스트 : B만 나와야 함.");
		
		target = new HashMap<>();
		target.put("age", 30);
		
		log.info(userDao.selectUniqueUser(target).toString());
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		// 전체 삭제
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		log.info("DELETE 테스트 : 전부 사라져야 함.");
		userDao.deleteUser(new HashMap<>());
		
		users = userDao.selectSeveralUser(new HashMap<>());
		showUsers(users);
		log.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@");
		
		
	}
	
	public void showUsers(List<UserDto> users) {
		for(UserDto user : users) {
			log.info(user.toString());
		}
	}

다행히 전부 정상 동작한다.

 

 

sqlProvider를 활용한 MyBatis 사용법

사실 이 부분을 저장해놓고 싶어서 이 포스팅을 작성했다.

XML을 이용한 방법에서 XML에 사전에 코드를 저장해놓았듯, SqlProvider는 Java 클래스 파일 내에서 코드를 저장해놓고 활용하는 방법이다.

 

뭐가 더 편한지는 사람마다 다르겠지만, 개인적으로 xml이 더 편하긴 하다.

하지만 그렇다고 프로젝트에서 내 마음대로 xml만을 사용할 수는 없기에 SqlProvider를 활용한 방법을 기재해놓는다.

 

앞서 테이블과 DTO는 이미 작성된 상태이니, 우선 SqlProvider를 생성한다. 그리고 xml에서 작성했던 함수들을 그대로 작성한다.

 

sqlProvider 생성 방법은 StringBuffer를 이용해도 되고, org.apache.ibatis.jdbc.SQL을 이용해도 된다. 다만 StringBuffer를 이용하는 방법은 굳이 저장할 필요가 없으므로 SQL을 이용한 방법으로 작성했다.

 

UserSqlProvider.java

package com.mwlee.mybatis.user.sqlprovider;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.jdbc.SQL;

import com.mwlee.mybatis.user.dto.UserDto;

public class UserSqlProvider {
	
	private final String TABLE_NAME = "user";

	public String insertUser(UserDto user) {
		// { 이 두 개 임을 명심한다.
		return new SQL() {{
			// SQL 종류와 테이블 이름 지정
			INSERT_INTO(TABLE_NAME);
			
			// value 지정
			VALUES("id", "#{user.id}");
            if (user.getAge() != null) {
                VALUES("age", "#{user.age}");
            }
            if (user.getBirthday() != null) {
            	VALUES("birthday", "#{user.birthday}" );
            }
			
		}}.toString();
	}
	
	public String insertManyUser(List<UserDto> users) {
		
		// 여러 개를 insert하기 위해서는 VALUE에 #{user.id} 같은 걸 할 수가 없음.
		// 그래서 SQL로 INSERT INTO ~ VALUES 까지만 만들고, 뒤에 나오는 건 StringBuffer로 해결
		SQL sql = new SQL() {{
			INSERT_INTO(TABLE_NAME);
			
			INTO_COLUMNS("id", "age", "birthday");
		}};
		
		SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		StringBuffer sqlAppend = new StringBuffer();
		for(int i=0; i<users.size(); i++) {
			UserDto user = users.get(i);
			
			sqlAppend.append("('").append(user.getId()).append("', ");
			if(user.getAge() != null) {
				sqlAppend.append("'").append(user.getAge()).append("', ");
			}
			else {
				sqlAppend.append("null, ");
			}
			if(user.getBirthday() != null) {
				sqlAppend.append("'").append(date.format(user.getBirthday())).append("')");
			}
			else {
				sqlAppend.append("null)");
			}
			
			if(i != users.size() - 1) {
				sqlAppend.append(", ");
			}
		}
		
		return sql.toString() + " VALUES " + sqlAppend.toString();
    }
	
	public String updateUser(final UserDto user) {
        return new SQL() {{
            UPDATE(TABLE_NAME);
            
            if (user.getAge() != null) {
            	SET("age = #{user.age}");
            }
            if (user.getBirthday() != null) {
            	SET("birthday = #{user.birthday}");
            }
            
            // WHERE 지정 : WHERE은 하나의 String으로 작성한다.
			WHERE("id = #{user.id}");
        }}.toString();
    }
	
	public String deleteUser(final Map<String, Object> target) {
        return new SQL() {{
            DELETE_FROM(TABLE_NAME);
            
            // WHERE 지정
            // 파라미터가 map이면 이상하게 map안에 map이 들어감.
            if (((Map<String, String>)target.get("target")).get("id") != null) {
    			WHERE("id = #{target.id}");
            }
            if (((Map<String, String>)target.get("target")).get("age") != null) {
            	WHERE("age = #{target.age}");
            }
            if (((Map<String, String>)target.get("target")).get("birthday") != null && ((Map<String, String>)target.get("target")).get("comparator") != null) {
        		WHERE("birthday ${target.comparator} #{target.birthday}");
            }
        }}.toString();
    }
	
	public String selectUniqueUser(final Map<String, Object> target) {
        return new SQL() {{
            SELECT("*");
            FROM("user");
            if (((Map<String, String>)target.get("target")).get("id") != null) {
            	WHERE("id = #{target.id}");
            }
            if (((Map<String, String>)target.get("target")).get("age") != null) {
            	WHERE("age = #{target.age}");
            }
            if (((Map<String, String>)target.get("target")).get("birthday") != null && ((Map<String, String>)target.get("target")).get("comparator") != null) {
        		WHERE("birthday ${target.comparator} #{target.birthday}");
            }
        }}.toString();
    }

    public String selectSeveralUser(final Map<String, Object> target) {
        return new SQL() {{
            SELECT("*");
            FROM(TABLE_NAME);
            
            if (((Map<String, String>)target.get("target")).get("age") != null) {
            	WHERE("age = #{target.age}");
            }
            if (((Map<String, String>)target.get("target")).get("birthday") != null && ((Map<String, String>)target.get("target")).get("comparator") != null) {
        		WHERE("birthday ${target.comparator} #{target.birthday}");
            }
        }}.toString();
    }
}

만들면서 느낀건데, 어떤 건 쉼표(,)를 쓰고, 어떤 건 안쓰는데 참 구분이 어렵다.

그냥 INSERT의 VALUES에서만 쉼표를 쓴다고 보면 될 것 같다.

 

이제 다음과 같이 DAO를 만든다.

 

UserDaoProvider.java

package com.mwlee.mybatis.user.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;

import com.mwlee.mybatis.user.dto.UserDto;
import com.mwlee.mybatis.user.sqlprovider.UserSqlProvider;

@Mapper
public interface UserDaoProvider {
	
	@InsertProvider(type = UserSqlProvider.class, method="insertUser")
	public int insertUser(@Param("user") UserDto user);

	@InsertProvider(type = UserSqlProvider.class, method="insertManyUser")
	public int insertManyUser(@Param("users") List<UserDto> users);
	
	@UpdateProvider(type = UserSqlProvider.class, method="updateUser")
	public int updateUser(@Param("user") UserDto user);
	
	@DeleteProvider(type = UserSqlProvider.class, method="deleteUser")
	public int deleteUser(@Param("target") Map<String, Object> target);
	
	@SelectProvider(type = UserSqlProvider.class, method="selectUniqueUser")
	public UserDto selectUniqueUser(@Param("target") Map<String, Object> target);

	@SelectProvider(type = UserSqlProvider.class, method="selectSeveralUser")
	public List<UserDto> selectSeveralUser(@Param("target") Map<String, Object> target);
	
}

 

마지막으로 앞서 만들었던 테스트에서 UserDaoXml로 선언된 userDao를 UserDaoProvider로 바꿔주고 테스트를 진행해본다.

@Autowired UserDaoProvider userDao;

 

다 잘 나온다.

728x90
반응형