스프링 프레임워크에서 마이바티스를 사용하기 위해 필요한 것은 크게 4가지이다 :
이번에는 스프링 부트에서 H2 데이터베이스에 마이바티스를 사용해보기로 했다. 이 경우 필요한 의존성은 다음과 같다 (Gradle) :
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
spring-boot-starter-jdbc
추가com.h2database:h2
추가mybatis-spring-boot-starter:3.0.2
추가마이바티스는 스프링부트에서 버전관리를 하지 않기 때문에 의존성을 추가할 때 뒤에 버전 정보를 붙여주어야 한다. 나는 스프링부트 3.1 버전이기 때문에 마이바티스는 3.0 버전을 사용했다. (호환 버전은 🔗 마이바티스 매뉴얼 참고)
마이바티스는 커넥션 풀을 사용한다. 따라서 DataSource
를 설정해주어야 한다.
DataSource
: 애플리케이션이 커넥션 풀링을 통해 데이터베이스와 연결을 확보할 때 사용하는 인터페이스 (이전 포스팅 참고 : 🔗 DataSource 개념 및 설정)레거시 프로젝트의 경우 DB 연결 정보와 커넥션 풀 설정을 담당하는 HikariConfig
빈과, HikariConfig
를 참고하여 커넥션 풀을 만드는 HikariDataSource
빈을 직접 등록해주어야 했다. (이전 포스팅 참고 : 🔗 MyBatis 연결)
스프링 부트에서는 application.yml
만 작성하면 자동으로 필요한 스프링 빈을 등록해준다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;IGNORECASE=TRUE
username: sa
password: sa
참고로 스프링부트는 JDBC 드라이버를 JDBC URL을 통해 자동 인식하며, 가장 우선적으로 HikariCP를 탐색하기 때문에 hikari
프로퍼티는 생략해도 된다. (이전 포스팅 참고 : 🔗 H2 데이터베이스)
<!-- 기존 namespace 아래 정보 추가 -->
<beans xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring">
<beans xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<!-- BoardMapper interface의 구현클래스 생성
SqlSessionFactoryBean(SQL을 실행하는 API) -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Mapper interface들을 메모리에 올리는 방법(scan) -->
<mybatis-spring:scan base-package="org.example.mapper"/>
레거시 프로젝트의 경우 root-context.xml
에 네임스페이스, SqlSessionFactoryBean
, 스캔 경로를 설정해주어야 한다.
스트링부트의 MyBatis-Spring-Boot-Starter
는 다음과 같은 작업을 자동화해준다 :
DataSource
조회SqlSessionFactoryBean
에 DataSource
를 전달하고 SqlSessionFactory
인스턴스를 생성 및 등록SqlSessionFactory
에서 가져온 SqlSessionTemplate
인스턴스를 생성 및 등록SqlSessionTemplate
에 연결하고, 매퍼를 빈에 주입할 수 있도록 스프링 컨텍스트에 등록한다.SqlSessionTemplate
은 트랜잭션 관리, 세션의 생명주기 관리를 담당하며 마이바티스 예외를 스프링의 DataAccessException
로 변환해주는 스프링 마이바티스 연동 모듈의 핵심이다.
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
SqlSessionTemplate
의 구현인 sqlSession
인스턴스는 sqlSessionFactory
에 의해 생성된다.
SqlSessionFactoryBean
은 스프링 연동모듈에서 SqlSessionFactoryBuilder
대신 사용되는 것으로, DataSource
를 통해 SqlSessionFactory
를 생성한다.
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}
}
SqlSessionFactoryBean
에서 getObject()
로 얻은 SqlSessionFactory
이다.SqlSessionFactoryBean
은 DataSource
를 참조한다.mybatis:
type-aliases-package: com.example.board.domain
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
스트링부트의 자동설정 덕분에 개발자는 application.yml
에 간단한 설정만 하면 된다.
type-aliases-package
: 도메인 객체가 있는 패키지를 지정해주면 매퍼에서 타입 정보를 명시할 때 패키지 이름을 생략할 수 있다(prefix).mapper-locations
: 매퍼 XML 파일의 위치map-underscore-to-camel-case
: 관례적으로 RDB에서는 스네이크 케이스를 사용하고 자바는 카멜 케이스를 사용하기 때문에 이러한 불일치를 보완해주는 설정이다. (예를 들면 컬럼과 필드를 매핑할 때 user_name과 userName을 동일한 것으로 간주한다.)더 많은 설정은 다음 사이트에서 참고한다. (🔗 MyBatis 매뉴얼)
마이바티스 연동 테스트를 위해 매퍼 간단하게 매퍼 XML 없이 매퍼 인터페이스를 작성해보았다.
@Mapper
public interface PostMapper {
@Insert("INSERT INTO post (post_title, post_content) VALUES (#{postTitle}, #{postContent})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void save(Post post);
@Select("SELECT * FROM post WHERE id = #{id}")
Post findById(Long id);
}
save(Post post)
는 post
를 저장한 뒤 데이터베이스에 의해 auto-increment된 id
값을 가져와야 하기 때문에 @Option
을 사용했다.
void
또는 int
이어야 한다. int
로 반환하는 경우 쿼리로 인해 영향받은 로우 수를 반환한다. 단, 일반적인 인터페이스의 경우와 달리 매퍼 인터페이스를 구현한 클래스가 메서드를 오버라이딩하는 경우 리턴 타입을 변경할 수 있다. (🔗 관련 포스팅 : Mapper method has an unsupported return type 오류)@Configuration
@RequiredArgsConstructor
public class MyBatisConfig {
private final PostMapper postMapper;
@Bean
public PostRepository postRepository() {
return new MyBatisPostRepository(postMapper);
}
}
추후에 리포지토리를 JPA로 변경할 예정이기 때문에 매퍼를 바로 사용하지 않고 리포지토리 인터페이스를 만든 뒤 그 구현체에서 매퍼를 주입하여 사용했다.
@Repository
@RequiredArgsConstructor
public class MyBatisPostRepository implements PostRepository {
private final PostMapper postMapper;
@Override
public Post save(Post post) {
postMapper.save(post);
return post;
}
@Override
public Post findById(Long id) {
return postMapper.findById(id);
}
}
마이바티스를 사용하는 리포지토리 인터페이스의 구현 클래스이다.
save()
의 리턴 타입을 void
에서 Post
로 오버라이딩했다. 따라서 save()
는 로직을 처리한 뒤 파라미터로 전달되었던 post
객체를 그대로 반환하는데, post
의 id
필드는 쿼리 실행 후 데이터베이스에서 auto-increment된 id
값이 새롭게 바인딩된다. 매퍼에서 save()
에 @Options(useGeneratedKeys = true, keyProperty = "id")
를 사용했기 때문이다.
@Slf4j
@Transactional
@SpringBootTest
public class MyBatisPostRepositoryTest {
@Autowired
PostRepository postRepository;
@Test
void save() {
//given
Post post = new Post("title 1", "Lorem Ipsum");
//when
Post savePost = postRepository.save(post);\
//then
Post findPost = postRepository.findById(savePost.getId());
assertThat(findPost).isEqualTo(savePost);
}
}