일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 트러블슈팅
- redis
- 보따리
- 구슬
- 동적 SQL
- jscode
- 글또
- SQL
- 부꾸
- 코드트리
- Spring
- 테오의 스프린트
- Database
- 프로그래머스
- 후기
- dto projection
- 글또 #다짐
- 눈송이
- 체험
- jooq
- 모의면접
- DI
- 티스토리챌린지
- open contribution jam
- bean
- 부꾸러미
- spring context
- 사이드 프로젝트
- 오블완
- 북극곰
- Today
- Total
벤티의 개발 로그
[Spring] Spring Bean 2편 (의존성 주입) 본문
이 포스트는 글또 10기 내 독서 모임에 참여하여 읽기 시작한 '스프링 교과서'를 읽고 정리한 내용을 바탕으로 작성했습니다.

등록은 끝났다. 이제 어떻게 사용할까?
Spring Bean 1편에서는 Bean을 생성하고 추가하는 방법에 대해 작성했다. 2편에서는 Bean을 어떻게 사용하는지, 정확하게는 '어떻게 Spring이 필요한 곳에 Spring Context에 등록한 빈의 참조를 제공하는지'에 대해 작성했다.
Spring에서 하나의 Bean을 만드는데 온전히 그 안에서만 모든 것이 해결되는 경우는 거의 없다. 하나의 Bean은 다른 Bean을 참조하고 그 Bean에 있는 메서드를 활용하는 경우가 더 많다. 따라서 이런 경우 서로 다른 두 개의 Bean의 관계를 구현해서 Spring에 알려주는 것이 중요하다.
이 책은 총 3가지의 방법을 설명하고 있다.
Wiring과 Autowiring: Config 클래스에서 직접 해결하자
@RequiredArgsConstructor @Configuration @EnableRedisRepositories public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Bean public RedisConnectionFactory redisConnectionFactory() { // lettuce return new LettuceConnectionFactory(host, port); } @Bean public RedisTemplate<String, Object> redisTemplate() { // redis-cli 사용을 위한 설정 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); return redisTemplate; } }
위 코드는 예전에 진행했던 프로젝트에서 활용했던 코드로, Redis 관련 설정 클래스이다. 이 클래스는 Redis의 연결을 정의한다. 여기서 중요한 부분은 redisTemplate.setConnectionFactory(redisConnectionFactory());
이다.
첫 번째 메서드의@Bean
을 통해 Bean으로 등록한 ConnectionFactory를, 두 번째 메서드에서 setConnectionFactory()를 호출하고, 인자로 직접 넣어줌으로써 두 Bean 사이의 참조를 설정했다. 이렇게 직접 한 메서드에서 다른 메서드를 호출해서 Bean의 관계를 설정하는 방법을 Wiring이라고 한다.
Spring은 ConnectionFactory처럼 사용하려는 Bean이 이미 Spring Context에 존재한다면, Spring은 redisConnectionFactory()를 호출하는 대신 해당 Context에서 직접 인스턴스를 가져온다. 즉, 사용하려는 Bean이 없는 경우에만 메서드를 호출하며, 따라서 Spring Context에 ConnectionFactory 인스턴스는 두 개가 아닌 오직 하나만 존재한다.
책에서는 매개변수를 이용하여 Spring Context에서 Bean을 제공하도록 Spring에 지시하는 방법도 설명한다. 이런 방법을 Autowiring이라고 한다. 이 방법으로 Bean 간 관계를 설정할 경우, 위 코드는 redisTemplate()에 RedisConnectionFactory 타입의 매개변수가 생길 것이며, setConnectionFactory()의 인자로 이 매개변수를 그대로 대입하기만 하면 된다.
@Autowired
: 좀 더 유연하게
이 방법은 IoC 원칙을 기반으로 한 의존성 주입(Dependency Injection, DI)로 Bean 간 관계를 설정하는 방법이다. 실제로 코드를 작성할 때 이 방법을 훨씬 더 많이 사용했다. 앞 방법은 일반적으로 Config 클래스에서 많이 사용했고, 이 방법은 Service 클래스에서 더 많이 사용했다.
@Autowired
를 붙여 값을 주입할 수 있는 방법도 3가지가 있다. 이 중에서 공식 문서에 언급된, 권장되는 방법은 무엇일까?
1. 필드를 통해 주입한다.
@Component("fooFormatter") public class FooFormatter { public String format() { return "foo"; } } @Component public class FooService { @Autowired private FooFormatter fooFormatter; }
2. 생성자의 매개변수를 통해 주입한다.
public class FooService { private FooFormatter fooFormatter; @Autowired public FooService(FooFormatter fooFormatter) { this.fooFormatter = fooFormatter; } }
3. setter를 통해 주입한다.
public class FooService { private FooFormatter fooFormatter; @Autowired public void setFormatter(FooFormatter fooFormatter) { this.fooFormatter = fooFormatter; } }
Constructor-based or setter-based DI?
Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the
@Autowired
annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.
답은 '2번. 생성자의 매개변수를 통해 주입한다.'이다.
가장 큰 이유는 이 방법을 사용하면 필드를 final로 정의할 수 있어 초기화 후에는 필드 값이 바뀌지 않기 때문이다. 또 다른 이유는, 생성자를 호출할 때 값을 설정할 수 있어, 단위 테스트를 작성할 때도 훨씬 유리하기 때문이다.
실제 프로젝트에서는 아래처럼 Lombok에서 제공하는 @RequiredArgConstructor
를 Service나 Controller 같은 클래스에 붙여서 이 문제를 해결한다.
@Service @RequiredArgsConstructor @Slf4j public class AnswerService { private final AnswerRepository answerRepository; private final QuestionRepository questionRepository; private final MemberRepository memberRepository; // 이하 생략 }
2번 방법의 예제 코드와 비슷한 모양의 코드를 작성한 기억이 없어서 왜 그런가했더니, 이 이유 때문이었다. 덕분에 이번에 제대로 공부할 수 있었다!
같은 타입의 Bean들 사이에도 우선순위가 존재할까?
여기까지 읽고 궁금한 점이 생겼다. '동일한 타입의 인스턴스가 Spring Context에 여러 개가 등록될 경우, 참조 관계가 설정됐다고 했을 때, Spring에서는 어떤 인스턴스를 사용하는가?' 어느 한 Bean에 대해서 하나의 인스턴스만 등록해서 사용하면 좋겠지만, 그렇지 않은 경우가 많다. 책에 따르면, 매개변수명이 Spring Context에 등록된 Bean의 이름들 중 일치하는 것이 존재할 경우와 그렇지 않은 경우로 나뉜다.
우선, 존재할 경우는 이름이 일치하는 Bean을 선택한다. 그렇지 않다면? 이때 사용하는 것이 @Primary
와 @Qualifier
이다. 이름이 일치하는 Bean이 없음에도 불구하고 이 둘 중 무언가를 사용하지 않으면 예외가 발생한다.
@Primary
를 (Bean을 생성하는 메서드에) 사용하면 그 Bean이 기본(디폴트) Bean이 된다. @Qualifier
는 매개변수를 선언하는 부분에 사용할 수 있는데, (Coffee 타입의) Americano라는 이름의 Bean을 주입하려고 하는 경우 @Qualifier("Americano") Coffee coffee
처럼 value 속성으로 주입하려는 Bean을 지정할 수 있다.
그렇다면 위와 동일한 예제에서, 한 클래스 안에 @Primary
가 붙은 (Coffee 타입의) Latte라는 Bean이 존재하면 Spring은 Americano와 Latte라는 Bean 중 어느 Bean을 주입할까?
이 글에 따르면, 답은 Americano이다. 즉, @Qualifier
가 @Primary
보다 우선순위가 높다.
추상화를 사용하면서 의존성을 주입할 수는 없나요?
있다. Spring은 추상화도 이해하기 때문이다. 실제로 대부분의 프로젝트에서 추상화로 객체 간 의존성을 구현한다. Spring과 함께. 아래 코드는 과거 학부 수업에서 JdbcTemplate을 이용해 따릉이(자전거) 데이터를 관리했던 코드 중 일부이다.
public interface BikeRepository { void save(CreateBikeReq createBikeReq); void updateBikeStatus(String bikeId); Optional<Bike> findById(String bikeId); Bike findByBikeId(String bikeId); List<Bike> getListByBikeId(String bikeId); List<Bike> getListByBikeStatus(); List<Bike> findAll(); }
우선 이렇게 BikeRepository라는 클래스에 메서드를 선언한 뒤, 아래의 JdbcBikeRepository에서 구현했다. 여기서 봐야 하는 것은 Stereotype Annotation(아래의 경우는 @Repository)이 구현 클래스에만 붙어있다는 것이다.
@Slf4j @Repository public class JdbcBikeRepository implements BikeRepository { private final JdbcTemplate jdbcTemplate; public JdbcBikeRepository(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } @Override public void save(CreateBikeReq createBikeReq) { // 생략 } @Override public void updateBikeStatus(String bikeId) { // 생략 } // 이하 다른 메서드 생략 }
Spring Context에 객체를 추가하는 이유는 어디까지나 'Spring이 객체를 제어하고, 프레임워크가 제공하는 기능으로 객체를 관리하는 것'이기 때문에, 굳이 관리할 필요가 없는 인스턴스나 애초에 인스턴스를 만들 수 없는 인터페이스/추상 클래스에서는 의미 없이 Stereotype Annotation을 붙이지 않는다.
따라서, 특별한 경우가 아니라면 내가 해야 할 일은 Spring이 클래스의 인스턴스를 만들고 Spring Context에 Bean으로 추가할 수 있도록 Stereotype Annotation(아래의 경우는 @Service)을 붙이는 것뿐이다.
@Service public class BikeService { private final BikeRepository bikeRepository; private final MemberRepository memberRepository; @Autowired public BikeService(BikeRepository bikeRepository, MemberRepository memberRepository) { this.bikeRepository = bikeRepository; this.memberRepository = memberRepository; } public void createBike(CreateBikeReq createBikeReq) { // 생략 bikeRepository.save(createBikeReq); } // 이하 생략 }
이 예제에서는 앞에서 설명한 3가지 방법 중 가장 권장되는 방법인, 생성자의 매개변수를 통해 의존성을 주입하는 방법을 이용했다. 이렇게 사용하면, Spring은 BikeService의 생성자로 Bean을 생성한 후, Bean 인스턴스를 생성할 때 Spring Context에서 매개변수의 참조들(BikeRepository, MemberRepository)을 주입한다. 따라서, BikeService 객체와 의존성 인스턴스를 직접 생성할 필요도, 관계를 명시적으로 설정할 필요도 없다.
@Service와 @Repository 객체는 어떤 책임을 가지고 있을까?
1. @Service: 사용 사례 구현
2. @Repository: 데이터 지속성 관리
물론 하나의 인터페이스에 대한 구현 클래스가 하나가 아닌 경우도 많다. 이런 경우 앞과 마찬기지로 @Primary나 @Qualifier를 사용하면 된다. 예를 들어, JdbcBikeRepository를 BikeRepository의 구현 클래스들 중 기본으로 설정하고 싶다면 아래처럼 @Primary를 사용하면 된다.
@Slf4j @Repository @Primary // 이제 이 클래스는 BikeRepository의 구현 클래스들 중 최우선순위로 적용된다. public class JdbcBikeRepository implements BikeRepository { // 생략 }
만약 @Qualifier를 사용하여 여러 개의 구현 클래스를 구분해서 사용하고 싶다면, 앞의 @Qualifier("Americano") Coffee coffee 예제처럼 각 구현 클래스에 이름을 붙이고, 생성자의 매개변수로 이름이 일치하도록 의존성을 주입하면 된다.
후기
앞으로는 생성자의 매개변수를 통해 의존성을 주입해야겠다. 특히, 이 내용이 공식 문서에까지 나와 있는 것은 이번에 처음 알았다. (...) 다만, 선택적인 의존성(커스텀 의존성을 의미하는 것 같다.)의 경우에는 setter나 config 클래스의 메서드를 이용하라고 적혀있으니, 3가지 방법 모두 숙지해야겠다.