@Qualifier
가 안먹힐 때(with 롬복)스프링에서 사용할 수 있는 스프링 빈 주입방식은 크게 3가지가 있다.
@Autowired
, @Inject
, @Resource
)이 선언된 필드(field) 주입이 중에서 권장되는 방식은 “생성자 주입“이다. 객체를 생성하는 단계에서 필요한 스프링 빈을 주입할 수 있어서 누락되는 것을 피할 수 있다.
스프링 환경에서 생성자 주입이 작동되려면 클래스에는 생성자가 하나만 선언되어 있어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Service
public class BootService {
private ExampleProperties properties;
public BootService(ExampleProperties properties) {
this.properties = properties;
}
@PostConstruct
public void init() {
log.debug("Injected properties: {}", this.properties);
}
}
해당 클래스를 테스트할 때 테스트용 스프링 애플리케이션컨텍스트를 구동하지 않고 원하는 코드로 바꿔치기도 가능하다.
자바 개발환경에서는 반복적으로 작성하게 되는 접근자/설정자(Getter(get
)/Setter(set
)), toString()
과 equals()
등을 애노테이션으로 대체할 수 있는 롬복(lombok, https://projectlombok.org/) 이 널리 사용된다(개발자에 따라서 호불호가 갈린다. 애노테이션 사용이 남발되고 있다고…).
가끔 동일한 타입의 스프링 빈을 다른 이름으로 사용해야 하는 상황이 생긴다. 그럴 때면 다음과 같이 스프링 빈의 이름을 각각 다르게 선언하여 사용하기도 한다. 아래코드는 서로다른 데이터베이스 설정으로 구성된 DataSource
를 primaryDataSource
와 secondaryDataSource
라는 이름으로 가지는 스프링 빈을 선언하고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Component;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class MigrationBatchConfiguration {
@Qualifier("primaryJdbcTemplate")
private final JdbcTemplate primaryJdbcTemplate;
@Qualifier("secondaryNpJdbcTemplate")
private final NamedParameterJdbcTemplate secondaryNpJdbcTemplate;
public void migrate() {
List<MigrationDto> sources = primaryJdbcTemplate.query("SELECT name, age FROM primary_person", new MigrationDtoRowMapper());
SqlParameterSource[] parameterSources = generateParameterSources(sources);
secondaryNpJdbcTemplate.batchUpdate("INSERT INTO secondary_person(name, age) VALUES(?, ?)", parameterSources);
}
private SqlParameterSource[] generateParameterSources(List<MigrationDto> sources) {
MapSqlParameterSource[] sqlParameterSources = new MapSqlParameterSource[sources.size()];
for (int i = 0; i < sources.size(); i++) {
sqlParameterSources[i] = new MapSqlParameterSource()
.addValue("name", sources.get(i).getName())
.addValue("age", sources.get(i).getAge());
}
return sqlParameterSources;
}
@Getter
public static class MigrationDto {
private String name;
private Integer age;
public MigrationDto(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public static class MigrationDtoRowMapper implements RowMapper<MigrationDto> {
@Override
public MigrationDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return new MigrationDto(rs.getString("name"), rs.getInt("age"));
}
}
}
위 DatabaseConfig
클래스에서 선언한 서로 다른 primary~
와 secondary~
빈을 사용하기 위해 다음과 같은 코드를 사용했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@SpringBootTest
class MigrationBatchConfigurationTest {
@Autowired
@Qualifier("primaryJdbcTemplate")
private JdbcTemplate primaryJdbcTemplate;
@Autowired
@Qualifier("secondaryJdbcTemplate")
private JdbcTemplate secondaryJdbcTemplate;
@Autowired
@Qualifier("secondaryNpJdbcTemplate")
private NamedParameterJdbcTemplate secondaryNpJdbcTemplate;
@BeforeEach
void setUp() {
primaryJdbcTemplate.execute("DELETE FROM primary_person");
secondaryJdbcTemplate.execute("DELETE FROM secondary_person");
}
@Test
@DisplayName("기본실행")
void test01() {
// 생략
}
}
그런데 내 의도와는 다르게 @Qualifier("secondaryNpJdbcTemplate")
에 primaryNpJdbcTempate
가 주입되었다.
@Priamry 선언을 했기 때문에 그런 것인가?
하고 고민을 했다. 그런데 이 클래스를 테스트하기 위해 다음과 같은 코드를 작성했을 때는 정상적으로 주입되는 것을 확인했다. 즉, 기본 구성은 정상적으로 주입받아 사용했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@SpringBootTest
class MigrationBatchConfigurationTest {
@Autowired
@Qualifier("primaryJdbcTemplate")
private JdbcTemplate primaryJdbcTemplate;
@Autowired
@Qualifier("secondaryJdbcTemplate")
private JdbcTemplate secondaryJdbcTemplate;
@Autowired
@Qualifier("secondaryNpJdbcTemplate")
private NamedParameterJdbcTemplate secondaryNpJdbcTemplate;
@BeforeEach
void setUp() {
primaryJdbcTemplate.execute("DELETE FROM primary_person");
secondaryJdbcTemplate.execute("DELETE FROM secondary_person");
}
@Test
@DisplayName("기본실행")
void test01() {
// 생략
}
}
디버거를 이용해서 MigrationBatchConfiguration
를 살펴봤을 때는 계속 primaryNpJdbcTemplate
빈이 주입되는 상황이 발생했다. 생각을 정리하니,
다음과 같이 ‘
@Qualifier("secondaryNpJdbcTemplate")
이 안먹는다.’ 는 결론에 도달했다. 롬복@RequiredArgsConstructor
가@Qualifier
를 무시하고 있다는 생각을 하게 되었다.
1
2
3
4
5
6
7
8
@RequiredArgsConstructor
public class MigrationBatchConfiguration {
@Qualifier("primaryJdbcTemplate")
private final JdbcTemplate primaryJdbcTemplate;
@Qualifier("secondaryNpJdbcTemplate")
private final NamedParameterJdbcTemplate secondaryNpJdbcTemplate;
//생략
인터넷 검색을 시작한다. ‘lombok constructor Qualifier annotation not work’, 그리고 답을 찾았다.
문서를 살펴보면 프로젝트 루트에 롬복 구성파일 lombok.config
를 생서앟고 다음과 같은 코드를 추가하면 된다.
1
2
# see https://projectlombok.org/features/constructor lombok.copyableAnnotations
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
이렇게 추가해주면 롬복 애노테이션 프로세서(AnnotationProcessor)는 생성자(필드에 선언된 애노테이션 포함)를 생성는데 사용할 필드에 선언된 @Qualifier
를 복사한다.
@Qualifier
애노테이션은 정상적으로 작동한다.@Qualifier
애노테이션은 적용되지 않는다.