티스토리 뷰
spring 을 공부하면 가장 먼저 배우는 키워드 중 하나가 DI 이다. DI 란 무엇인가? 의존성을 직접 해결하지 않고 외부로부터 주입받는 것이다. 더군다나 주입받는 대상을 구현 클래스가 아닌 인터페이스로 지정하면 확장에 열려있는 설계가 되며, DI 는 스트레티지 패턴에 기초하고 있다는걸 공부하게 된다.
그럼 spring 을 사용하고 있는 우리는 공부한걸 잘 이해하고, spring 으로 애플리케이션을 개발할때 다형성을 잘 이용하고 있을까?
@Component
public class DataGetter {
public String get() {
// SQL 을 이용해 데이터 조회
}
}
@Component
public class DataAggregator {
@Autowired
private DataGetter dataGetter;
public void method() {
String data = dataGetter.get();
// ...
}
}
DataGetter 라는 구현클래스를 spring bean 으로 등록하고, DataAggregator 라는 spring bean 에서 해당 객체를 주입받도록 했다. 이런 코드는 실무에서도 흔히 볼 수 있는 코드이다. 요구사항이 추가되어 DataGetter 가 구하는 데이터를 캐시해놓아야한다면 어떻게 할 수 있을까?
@Component
public class DataGetter {
public String get() {
// 분기를 사용해 캐시가 있으면 바로 리턴, 없으면 SQL 조회
}
}
DataGetter 내부 코드를 변경했다. 가장 간단한 방법이지만 다형성을 논하는 이번 글의 주제랑은 거리가 먼 방법이다. 하지만 현재 상태에서 이 방법이 마냥 나쁘다고만 생각하지는 않는다.
추가요구사항이 들어왔다. Aggregator 가 하나 더 필요한데 여기서는 캐시된 데이터를 조회하면 안되고 항상 DB 를 조회한 데이터가 필요하다. 이럴땐 어떻게 할 수 있을까?
public interface DataGetter {
String get();
}
@Component
public class DbDataGetter implements DataGetter {
@Override
public String get() {
// SQL 을 이용해 데이터 조회
}
}
@Component
public class CacheDataGetter implements DataGetter {
@Autowired
private DataGetter dataGetter;
@Override
public String get() {
// dataGetter 를 이용해 조회 후 캐시
}
}
@Component
public class CacheDataAggregator {
@Autowired
private DataGetter dataGetter;
public void method() {
String data = dataGetter.get();
// ...
}
}
@Component
public class DbDataAggregator {
@Autowired
private DataGetter dataGetter;
public void method() {
String data = dataGetter.get();
// ...
}
}
DataGetter 에 추상화를 도입해 각기 목적에 따른 구현체를 갖게했다. 오 이제 뭔가 다형성을 이용한 멋진 코드가 나온듯한 느낌이다. 이제 이렇게만 해놓으면 각 Aggregator 들을 목적에 맞게 사용할 수 있을까?
spring 기반 애플리케이션을 개발하는 개발자라면 모두 알겠지만 위 코드는 컴파일은 될지언정 spring 컨테이너가 정상적으로 올라오지 못하고 에러와 함께 종료된다. 그 이유는 DataGetter 타입의 spring bean 이 두개이기 때문에 어떤 DataGetter 를 주입해야할지 결정하지 못하기 때문이다. 이 문제는 너무나도 유명한 문제라 해결방법은 다들 알거라고 생각한다.
// 1. 의존하는 타입을 인터페이스가 아니라 구현 클래스로 변경
@Component
public class CacheDataAggregator {
@Autowired
private CacheDataGetter dataGetter;
}
// 2. 필드명을 주입받고자 하는 spring bean 이름에 맞게 변경
@Component
public class CacheDataAggregator {
@Autowired
private DataGetter cacheDataGetter;
}
// 3. spring bean 이름으로 주입하는 애노테이션 사용
@Component
public class CacheDataAggregator {
@Autowired
@Qualifier("cacheDataGetter")
private DataGetter dataGetter;
}
spring 애플리케이션을 개발하다보면 누구나 최소 한번이상 이 문제에 직면했을거라 생각한다. 그리고 위 3가지 방법 중 하나로 해결할것이다. 저 중에 어떤 방식이 가장 나을까?
1번 방식은 기껏 다형성 쓰겠다고 인터페이스 만들었는데 결국 다시 구현체에 의존하게 된다. 썩 마음에 들지 않는다.
2, 3 중에 하나를 선택해야할 것 같은데 3번은 굳이 애노테이션을 하나 더 달아야한다. 필드명을 바꿀 수 없는 이유가 있는게 아닌 이상 2번이 낫지 않을까? 2번이든 3번이든 이 둘중 하나를 선택한다면 구현 클래스에 의존하지 않으니 다형성을 이용해서 문제를 잘 해결했다고 할 수 있지 않을까?
여기서 내가 가지는 의문은 2,3 번 방식이 꼭 1 번보다 낫다고 할 수 있을까? 라는 것이다. 물론 의존하는 타입이 구현 클래스가 아니라 인터페이스라는건 알겠지만 그렇다고해서 CacheAggregator 가 CacheDataGetter 라는 구현체에 대한 의존을 완전히 끊었다고 할 수 있을까? 2, 3번 상태에서 CacheDataGetter 의 클래스명을 바꾸면 어떻게 될까? CacheDataAggrator 는 변경의 영향에서 벗어날 수 있을까? 결과적으로 현재 상태에서 byName 주입방식은 의존관계는 없애지 못한채 컴파일러에 의한 체크만 못하게 만드는 방식이라고 생각한다. DataGetter 에 대한 요구사항이 많아짐에 따라 추상화를 도입했지만 DataGetter 에 의존하는 DbDataAggregator, CacheDataAggregator 는 오롯이 인터페이스에 대한 의존을 하지 못한채 타입이든 이름이든 구현체에 대한 정보가 필요하다는 것이다.
그럼 이 문제는 왜 일어나고, Aggregator 들과 DataGetter 의 구현체에 대한 의존을 완전히 끊을 수 있는 방법이 있을까?
# ComponentScan
spring boot 는 존재하지도 않던 태초의 시절 spring bean 을 만들고 bean 간에 의존관계를 맺어주는 방식은 xml 을 이용했었다. 예전에는 spring 책을 사면 기본으로 배우는 방식이었는데 요즘 spring 책에도 이 방식이 나와있을지 잘 모르겠다. 요즘엔 제 3자에 의한 spring bean 주입을 할 상황이 생기더라도 xml 보다는 자바코드를 이용한 주입이 대중적이라 xml 로 spring bean 을 만들고, 주입하는 경우는 거의 없다.
여튼 xml 에 의존관계를 명시함으로써 spring bean 을 만들다가 ComponentScan 이라는 기능이 추가됐고 이는 가히 혁신적이었다. 너무나도 당연히 사용하는 @Component, @Controller, @Service, @Repository 와 같은 애노테이션들이 ComponentScan 기능과 같이 나타난것이다. 클래스를 만들고 spring bean 을 만들때마다 xml 이나 자바코드에 명시하는 방식은 부가작업을 일으키기 때문에 편하게 클래스에 애노테이션 붙여서 spring bean 등록을 하는게 대중적이 되었다.
ComponentScan 을 이용한 spring bean 생성과 의존관계 주입은 너무나도 편리하지만 이 방식의 맹점은 의존관계를 컴파일 타임에 확정지어야 한다는 것이다. 객체지향과 다형성은 컴파일 타임이 아니라 런타임에 의존관계를 확정짓기 때문에 인터페이스를 활용해서 구현 클래스에 대한 정보없이 개발할 수 있는건데 컴포넌트 스캔에 의한 방식은 컴파일 타임에 의존관계를 확정짓는다는 것이다. 컴파일 타임에 의존관계를 확정짓기 때문에 위처럼 Aggregator 클래스에 타입이든 이름이든 구현 클래스에 대한 무언가의 정보가 없어질 수가 없는것이다.
그리고 또 하나의 문제가 있는데 클래스당 spring bean 을 하나밖에 만들지 못한다는 점도 있다. 위 예제에선 DataGetter 를 중점적으로 설명했지만 DbDataAggregator, CacheDataAggregator 도 각각 구현 클래스가 분리되어있다. 만약 이 두 클래스이 로직이 같고, DataGetter 만 서로 다른 구현체를 주입받아야한다면 굳이 Aggregator 가 두개가 될 필요가 있을까? Aggregator 는 하나만 두고 각 DataGetter 를 주입받는 인스턴스를 두개를 만드는게 훨씬 효율적이고, 객체지향적일 것이다. 그리고 이는 spring 을 사용하는게 아니라면 사실 너무나도 당연한 방식이다.
public class DataAggregator {
private DataGetter dataGetter;
public DataAggregator(DataGetter dataGetter) {
this.dataGetter = dataGetter;
}
}
// 인터페이스에 의존하고 있기에 상황별로 다른 전략객체를 전달
DataAggregator dbDataAggregator = new DataAggregator(new DbDataGetter());
DataAggregator cacheDataAggregator = new DataAggregator(new CacheDataGetter());
이 방식이 우리가 공부한 스트레티지 방식에 더 가까울 것이다. DataAggregator 는 구현체에 대한 그 어떤 정보도 없이 인터페이스에 의존하고, 의존관계에 대한 해결은 런타임에 일어나게 된다. 하지만 spring bean 으로 만드려는 순간 DataAggregator 도 DataGetter 의 구현체만큼 필요해진다.
# 각기다른 구현체를 주입받는 DataAggregator
그럼 이 문제는 어떻게 해결할 수 있을까? spring bean 을 이용할 수는 없는걸까? 위에서 이미 이야기를 하고 내려왔는데, 의존관계를 컴파일 타임에 해결하는게 아니라 런타임에 해결해주는 제3의 존재가 필요하다.
@Configuration(proxyBeanMethods = false)
class DataAggregatorConfiguration {
@Bean
public DataAggregator dbDataAggregator() {
return new DataAggregator(new DbDataGetter());
}
@Bean
public DataAggregator cacheDataAggregator() {
return new DataAggregator(new CacheDataGetter());
}
}
ComponentScan 을 이용하지 않고, 의존관계를 해결해주는 존재를 도입하면 각기 다른 구현체를 주입받는 spring bean 을 만들 수 있다. 그리고 좀 더 객체지향적이고 다형성을 활용하는 의존관계를 만들어 사용할 수 있다.
# ComponentScan 을 쓰지 말자는거야?
ComponentScan 은 이렇게 대중화된 이유가 분명할 정도로 편리한 방식이다. 그래서 ComponentScan 을 쓰지않을 생각도 없고, 쓰지말자고 이야기하고 싶지도 않다. 하지만 코드를 작성할때 ComponentScan 방식에만 매몰돼서 그 안에서 문제를 해결하려는 모습들을 많이 봐왔다. 그 안에서 문제를 해결하고자 하니까 다형성을 제대로 활용하지도 못하고 거의 동일한 코드의 클래스를 두개 이상 만드는 모습도 많이 봐왔다. 우리의 애플리케이션 코드는 spring 이 정해놓은 방식을 따라가는게 아니라 우리가 정의한 방식에 spring 을 활용하는 형태로 가야하는데 이게 뒤바뀐 것이다. 특히나 객체지향에 관심이 많아 추상화를 도입해놓고도 결국 ComponentScan 으로 인해 구현 타입을 사용해야하는 상황이 오면 객체지향이 정말 좋은건지 이게 의미가 있는건지 고민이 될때도 많았다. 내 나름대로의 고민과 그 해결법을 글로 남겨놓는다.
'Java > spring' 카테고리의 다른 글
spring boot 3 migration#04 spring boot 3 resilience4j 버전이 안올라간다면 (1) | 2024.01.28 |
---|---|
spring boot 3 migration#03 @ConstructorBinding 스펙 변경 (0) | 2023.11.07 |
spring boot 3 migration#02 WebSecurityConfigurerAdapter (0) | 2023.05.14 |
spring boot 3 migration#01 ListenableFuture (1) | 2023.05.14 |
spring boot 에서 ObjectMapper 확장하기 (1) | 2023.03.24 |
- Total
- Today
- Yesterday
- DesignPattern
- java8
- backend개발환경
- Kotlin
- go-core
- JavaScript Core
- MySQL
- db
- toby
- programming
- EffectiveJava
- Spring
- mariadb
- frontcode
- servlet
- Jackson
- spring cloud
- TEST
- OOP
- java
- javascript
- clean code
- http
- code
- generics
- Design Pattern
- 정규표현식
- JPA
- Git
- frontend개발환경
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |