티스토리 뷰

이전 포스팅에서 Spring Boot와 MyBatis를 사용해 다중 데이터소스를 설정하고 패키지로 구분하는 방법을 다뤘습니다. 이번에는 @Qualifier 를 이용하여 Mapper 경로를 공유하면서도 다중 데이터소스를 사용하는 방법을 알아보겠습니다.

이전 포스팅 보러가기: [SpringBoot] Mybatis 다중 Datasource 적용하기


1. @Qualifier 의존성 주입 순서

  1. 타입으로 빈 검색: @Autowired가 붙은 필드나 생성자의 타입과 일치하는 빈을 찾습니다.
    • 1개일 경우: 해당 빈을 바로 사용합니다.
    • 2개 이상일 경우: 추가 조건으로 좁혀갑니다.
  2. @Qualifier가 명시된 경우: @Qualifier("이름")에 지정된 이름과 일치하는 빈을 찾습니다.
    • 일치하는 빈이 있으면: 그 빈을 사용합니다.
    • 없으면: 다음 단계로 넘어가지 않고 바로 예외 발생(NoSuchBeanDefinitionException).
  3. @Qualifier가 없는 경우: 타입이 같은 빈이 2개 이상일 때, 변수명(필드명) 또는 파라미터명과 이름이 일치하는 빈을 찾습니다.
    • 일치하는 빈이 있으면: 그 빈을 사용합니다.
    • 없으면: 예외 발생
  4. 모두 실패 시: NoUniqueBeanDefinitionException 또는 NoSuchBeanDefinitionException이 발생하며 컨테이너가 종료됩니다.

2. 간단한 예제

- application.yml

spring:
  datasource:
    primary:
      jdbc-url: jdbc:h2:mem:primaryDB
      username: sa
      password:
      driver-class-name: org.h2.Driver
    secondary:
      jdbc-url: jdbc:h2:mem:secondaryDB
      username: sa
      password:
      driver-class-name: org.h2.Driver

mybatis:
  configuration:
    map-underscore-to-camel-case: true
  • 해당 예제에서는 H2 인메모리 데이터베이스를 사용하지만, 실제 프로젝트에서는 원하는 DB로 대체하면 됩니다.

 

- PrimaryDataSourceConfig.java

@Configuration
public class PrimaryDataSourceConfig {

    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(
        	new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
        );
        return factoryBean.getObject();
    }

    @Primary
    @Bean(name = "primarySqlSessionTemplate")
    public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

 

- SecondaryDataSourceConfig.java

@Configuration
public class SecondaryDataSourceConfig {

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(
        	new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
        );
        return factoryBean.getObject();
    }

    @Bean(name = "secondarySqlSessionTemplate")
    public SqlSessionTemplate secondarySqlSessionTemplate(@Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
  • Mapper 경로를 classpath:mapper/*.xml로 통일합니다.

 

- Mapper 인터페이스와 XML

@Mapper
public interface UserMapper {
    User findById(Long id);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <select id="findById" resultType="com.example.demo.model.User">
        SELECT id, name FROM users WHERE id = #{id}
    </select>
</mapper>
  • Mapper 인터페이스는 하나만 정의하고, 두 데이터소스에서 공유합니다.
- UserService.java
@Service
public class UserService {

    private final UserMapper primaryUserMapper;
    private final UserMapper secondaryUserMapper;

    public UserService(
    	@Qualifier("primarySqlSessionTemplate") SqlSessionTemplate primarySqlSessionTemplate,
    	@Qualifier("secondarySqlSessionTemplate") SqlSessionTemplate secondarySqlSessionTemplate
    ) {
        this.primaryUserMapper = primarySqlSessionTemplate.getMapper(UserMapper.class);
        this.secondaryUserMapper = secondarySqlSessionTemplate.getMapper(UserMapper.class);
    }

    public User getUserFromPrimary(Long id) {
        return primaryUserMapper.findById(id);
    }

    public User getUserFromSecondary(Long id) {
        return secondaryUserMapper.findById(id);
    }
}
  • SqlSessionTemplate.getMapper(): 각 데이터소스에 맞는 SqlSessionTemplate에서 동일한 UserMapper 인터페이스를 가져옵니다.
  • @QualifierprimarySqlSessionTemplatesecondarySqlSessionTemplate을 구분해 주입받습니다.

 

- UserController.java

@RestController
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/primary/users/{id}")
    public User getPrimaryUser(@PathVariable Long id) {
        return userService.getUserFromPrimary(id);
    }

    @GetMapping("/secondary/users/{id}")
    public User getSecondaryUser(@PathVariable Long id) {
        return userService.getUserFromSecondary(id);
    }
}

마무리

Mapper 경로를 공유하면 코드가 간결해지고 유지보수가 쉬워지는 장점이 있습니다. 특히 동일한 쿼리를 여러 데이터소스에서 실행해야 하는 경우에 유리합니다. 반면, 데이터소스별로 쿼리가 크게 다르다면 경로를 분리하는 게 더 나을 수도 있으니. 프로젝트 요구사항에 따라 유연하게 선택하세요. 감사합니다.
최근에 올라온 글
Total
Today
Yesterday