오늘은 스프링에서 스프링부트로 전환하면서 마이바티스에 대해 알게된 내용에 대해 공유하려고 한다.
spring:
datasource:
url: jdbc:${url}:${port}/${db_name}
driver-class-name: org.postgresql.Driver
username: ${id}
password: ${password}
mybatis:
// 실제 sql문이 저장된 xml파일들 위치이다.
// 나의 경우는 리소스 폴더에 매퍼 디렉토리안에 각 도메인별로 디렉토리를 만들고
// 그 안에 그 도메인에해당하는 sql 파일들을 ~~~_SQL.xml형식으로 등록해서 아래처럼 등록해줬다.
// 각자 위치에 맞춰서 설정해주면 된다.
mapper-locations: classpath:mapper/**/*_SQL.xml
사실 스프링부트에서 mybatis를 사용하려면 마이바티스 스타터 라이브러리를 추가해줘야 되는데,
이걸 사용하면 자동으로 세션 팩토리랑 세션 템플릿 등의 빈을 만들어주기 때문에 빈을 꼭 등록해줄 필요는 없다.
기본설정을 변경하고 싶은 경우에 참고하면 된다. 그 전에 세션 팩토리, 세션 템플릿이 뭔지 간단하게 알아보자
세션 팩토리와 템플릿은 mybatis에서 데이터베이터와 연결 관련 작업을 처리하기 위해 사용된다.
데이터베이스와의 연결을 관리한다. 디비 연결 정보를 설정하고 마이바티스와 디비 간의 연결을 생성, 관리한다.
@Configuration
//해당 위치에 있는 @Mapper가 붙은 인터페이스를 스캔하고 실제 SQL문과 연결되게 해줌
@MapperScan(basePackages = {"com.example.mapper"})
public class MybatisConfig {
// 데이터소스 빈은 스프링부트에서 자동으로 만들어줌
private final DataSource dataSource;
public MybatisConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Bean
public SqlSessionTemplate postgreSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public SqlSessionFactory postgreSqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
return sessionFactory.getObject();
}
}
기본적으로는 이런식으로 만들어주면 된다.
추가할 설정이 있으면 그에 맞게 커스텀해주면 된다.
그리고 @MapperScan이거 할 때 주의점이 있다. 사실 이걸 공유하고 싶어서 포스팅을 했다.
기존 프로젝트에서는 매퍼 인터페이스가 여러군데에 산재해있어서
처음에 나는 basePakages를 루트 디렉토리로 설정했다. 그랬더니 이상한 데서 빈 중복에러가 났다.
구체적으로는 어떤 서비스 인터페이스를 만들고 그를 구현한 구현체를 만들고
그 위에 @Service어노테이션으로 자동 빈 등록을 해줬다.
그러면 당연히 구현체만 빈으로 등록되고 인터페이스는 등록되지 않는 게 정상이다.
하지만 내가 마주친 에러는 인터페이스와 구현객체가 둘다 빈으로 등록되어서 빈이 중복된다는 것이었다.
대체 왜 인터페이스가 빈으로 등록되었는가에 대한 이유를 몇시간을 고민한 것 같다.
그런데 나중에 알고보니까 매퍼스캔 문제였다.
왜냐면 위에 적힌것처럼 매퍼스캔은 매퍼 '인터페이스'를 스캔해서 실제 SQL문과 연결한다.
나는 @Mapper가 붙은 인터페이스만 해당하는 줄 알았더니 그냥 모든 인터페이스를 스캔해서 빈으로 등록해버린 것이다.
그래서 기존 패키지 구조를 버리고 mapper 디렉토리를 만들어서 매퍼 인터페이스를 다 옮기고 거기만 스캔하도록 했다.
아, 그리고 추가로 매퍼스캔이 명시적으로 설정돼있지 않으면 루트부터 자동으로 매퍼 인터페이스만 자동으로 찾아서 등록해준다. 하지만 명시적으로 @MapperScan을 해준다면 딱 그 패키지 아래에 있는 모든 인터페이스를 매퍼로 인식해서 등록한다. 그러니까 매퍼스캔을 명시적으로 넣어주지 않는다면 위와 같은 문제는 일어나지 않는다.
하지만 나는 꼭 매퍼스캔을 사용해야하는 이유가 있었어서 이렇게 해결했다.
위의 설정을 그대로 따라가면서 예시를 들어보겠다.
com.example.mapper가 매퍼스캔 시작 위치니까 그 아래 적당히 user 디렉토리를 만들고
아래 아래처럼 매퍼를 만들어준다.
@Mapper
public interface UserMapper {
User findById(int id);
List<User> findAll();
void insert(User user);
void update(User user);
void delete(int id);
}
그리고 여기에 대응되는 실제 SQL문을 xml 파일에 작성해준다.
위치는 위에 mapper-locations에 설정한 위치에 맞춰서 생성해주면 된다.
<mapper namespace="com.example.mapper.UserMapper">
<select id="findById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="findAll" resultType="User">
SELECT * FROM users
</select>
<insert id="insert" parameterType="User">
INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>
<update id="update" parameterType="User">
UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="delete" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
이렇게 하면 마이바티스가 xml과 매퍼 인터페이스를 연결해준다.
즉, 매퍼 인터페이스의 finById를 호출하면 xml에서 id=finById를 부분을 찾아서 실행해주고
resultType이 User니까 실행결과를 User 객체로 매핑해준다.
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(int id);
@Select("SELECT * FROM users")
List<User> findAll();
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
void insert(User user);
@Update("UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}")
void update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
void delete(int id);
}
참고로 xml파일 없이 이렇게 해도 된다.