윤개발

스프링의 객체 주입 & 동일한 객체를 추가로 생성하여 주입하는 경우 본문

백엔드/스프링

스프링의 객체 주입 & 동일한 객체를 추가로 생성하여 주입하는 경우

DEV_SJ 2021. 6. 5. 01:27

1. 스프링의 싱글톤

스프링은 기본적으로 Bean으로 등록된 객체를 싱글턴 방식을 사용하여 관리합니다.

예를 들어 @Bean, @Controller, @Component 등의 어노테이션이 붙은 Bean들은 스프링 컨테이너가 실행 시점에 등록된 Bean, 컴포넌트 스캔을 하여 딱 1번만 생성됩니다. 이후에는 요청이 들어와도 새로 객체를 생성하지 않고 공유해서 쓰게 됩니다.

왜 싱글톤일까요?

이는 객체를 매번 새로 생성하면 메모리 낭비가 심하고 요청이 완료되면 객체의 참조가 끊어지며 더 빈번하게 Gabage Collecting이 일어나기 때문입니다. 예를 들어 서버로 10000명의 사용자가 요청을 보낼 시 싱글톤이 아니라면 10000개의 객체를 생성해야 하니 낭비가 심하겠죠?

 

당연히 싱글톤을 이용하면서의 주의점도 있습니다. 싱글톤으로 공유가 되기에 상태를 유지하지 않도록 코드를 작성을 해야 하는데요. 이를 무상태성(Stateless) 이라 합니다. 공유되는 객체인 만큼 여러 스레드가 접근하게 되면 예상치 못한 결과가 일어날 수 있습니다.

 

2. 객체 자동 주입

스프링은 앞서 싱글톤으로 생성했던 객체를 자동으로 주입해줍니다. 자주 사용하는 의존 관계 주입방법을 알아보겠습니다.

생성자 주입 (추천!)

@Component
public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;

	@Autowired // 생략 가능
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}

코드와 같이 생성자 주입은 OrderServiceImpl이 생성되는 시점에 딱 1번만 호출되는 것이 보장됩니다.

불변이고 필수인 의존관계에 사용되며 생성자가 하나만 있으면 @Autowired를 생략할 수 있습니다. 

요즘은 Lombok을 많이 이용하기 때문에 @RequiredArgsConstructor를 사용해서  생성자 자체를 생략할 수도 있습니다.

수정자 주입 (setter 사용)

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
    
}

수정자 주입은 setter에 @Autowired를 붙여서 사용합니다. 그러면 스프링 컨테이너가 해당 Bean을 찾아서 setter에 자동으로 주입해줍니다.  그렇다면 실수로 @Autowired를 붙이지 않으면 어떻게 될까요?

- 실수한 케이스

//    @Autowired 빠트렸다!
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

실험 결과 스프링이 실행되는 시점에는 오류가 발생하지 않습니다. 하지만 해당 객체 접근 및 사용시점에는 NullpointerException이 발생합니다. 당연하게도 의존관계가 주입되지 않았기 때문입니다. 이처럼 수정자 주입은 애플리케이션 실행 시점에는 오류를 체크할 수가 없습니다. 추가적으로 순환 참조가 일어나는 경우도 실행 시점에는 체크가 불가능합니다!

 

생성자 주입이 불변, 필수에 주입된다면 수정자 주입은 주로 선택, 변경 가능성이 있는 의존관계에 사용할 수 있습니다.

필드 주입 (비추!)

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private DiscountPolicy discountPolicy;
    
}

필드 주입은 @Autowired를 붙여서 주입됩니다. 생성자 주입, 수정자 주입보다 코드가 정말 간단하죠?

그러나 이 방법은 추천하지 않습니다. 제가 비추하는 것이 아니라 인텔리제이에서 그림과 같이 노란 줄이 그어집니다!

코드는 깔끔하고 좋은데 어떤 경우에 문제가 될까요?

 

순수한 자바 테스트 코드를 작성하는 경우에는 당연히 @Autowired가 동작하지 않습니다. @SpringBootTest처럼 의존관계를 주입해줄 스프링 프레임워크가 필요합니다.  Spring이 아니면 해당 필드에 Injection을 할 수 있는 방법이 없습니다. 그렇다면 Setter를 써야 할 텐데... 그럴 거면 수정자 주입을 사용하는 것이 좋겠지요?

 

그래서 요즘에는 안 쓰는 게 좋다고 합니다. BUT! 애플리케이션과 관계없는 스프링 테스트 코드에서는 편하게 사용할 수 있습니다. 어디서 가져다 쓰는 게 아니라 문제 되지 않으니까요.

 

정리하면 필수적인 의존성에서는 Constructor Injection을, 선택적인 의존성에서는 Setter Injection를 사용하라고 합니다. 

자 이제 제가 말하려던 같은 객체가 2개 있을 땐 어떻게 주입하지?라는 본론으로 넘어가 보겠습니다. 

3.  2개가  필요했던 경우

동일한 객체이지만 다른 설정을 가지는 2개의 Bean을 스프링은 어떻게 관리할까요? 또 이런 경우는 언제 일어날까요?

최근에 업무를 하면서 일어났던 경우에 대해 알려드리겠습니다.

 

내용은 기존 Gitlab 프로젝트를 새로운 Gitlab으로 Migration 하는 것이었는데요.

처음에는 간단하게 Migration 요청이 들어온 프로젝트를 쉘 스크립트로 Clone 받아서 신규 서버에 밀어 넣을 계획이었습니다. 하지만 이렇게 되면 기존 커밋 이력은 넘어가지 않고 소스코드만 넘어가게 돼서 고민이었는데

역시나 java gitlab api가 maven으로 이미 만들어져 있더군요.. ㅎ_ㅎ

https://github.com/gitlab4j/gitlab4j-api#importexportapi

 

gitlab4j/gitlab4j-api

GitLab4J API (gitlab4j-api) provides a full featured Java client library for working with GitLab repositories via the GitLab REST API - gitlab4j/gitlab4j-api

github.com

 

 

좀 더 상세히 설명드리면 그림과 같습니다. RabbitMQ에 마이그레이션 요청이 들어온 ID를 빼와서 

기존 Gitlab Export => 신규 Gitlab Import를 하는 과정입니다. 기존 Gitlab, 신규 Gitlab 뭔가 동일한 객체가 2개 필요할 것 같지 않으신가요?

 

// Create a GitLabApi instance to communicate with your GitLab server
GitLabApi gitLabApi = new GitLabApi("http://your.gitlab.server.com", "YOUR_PERSONAL_ACCESS_TOKEN");

java gitlab api를 확인해보면 gitlab server와 access token으로 객체를 생성하고 있습니다. 앞서 말씀드렸다시피 요청이 들어올 때마다 객체를 생성하면 낭비가 심하기 때문에 저는 GitlabApi를 Bean으로 관리하기로 하여 아래와 같이 Configuration을 작성하였습니다.

@Configuration
public class GitlabApiConfiguration {

	// 생략

    @Bean
    public GitLabApi oldGitLabApi(){
        return new GitLabApi(oldGitlabUrl, oldGitlabKey);
    }

    @Bean
    public GitLabApi newGitLabApi(){
        return new GitLabApi(newGitlabUrl, newGitlabKey);
    }
}

어.. 근데 스프링이 기존 거랑 신규를 어떻게 구별해서 주입해줄까요...?  기존 꺼에선 Export, 신규에선 Import를 해야 해서 2개가 다 필요한데요 ㅜ_ㅜ;;  지금까지 개발을 하면서 항상 한 가지 Bean에 대한 경우만 주입했던 것 같습니다.

@Service
public class GitlabMigrationService {

    private final GitLabApi oldGitLabApi;

    private final GitLabApi newGitLabApi;

    public GitlabMigrationService(GitLabApi oldGitLabApi, GitLabApi newGitLabApi) {
        this.oldGitLabApi = oldGitLabApi;
        this.newGitLabApi = newGitLabApi;
    }
    
}

Bean을 주입받아 사용할 Service에서 어떻게 구분하지..! 사실 토비님의 스프링 책 또는 김영한 님의 스프링 강좌를 들으셨다면 @Qualifier의 존재에 대해서 알 것입니다.

4. @Qualifier

몇 번 보기는 했지만 처음 써보는 어노테이션이므로 알아보고 사용하도록 합시다. 먼저 당당하게 소스코드를 까 보았습니다만.. 역시 뭐가 없습니다. value만 선언이 되어있네요.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
    String value() default "";
}

 

그래서 spring docs에서 검색한 결과 아래와 같이 설명되어 있습니다.

 

Fine-tuning Annotation-based Autowiring with Qualifiers

@Primary is an effective way to use autowiring by type with several instances 
when one primary candidate can be determined. 
When you need more control over the selection process, 
you can use Spring’s @Qualifier annotation.

@Primary 어노테이션은 몇몇의 후보 빈이 있을 때 주입할 빈을 결정한다고 합니다. 만약 더 많은 결정이 필요하다면 @Qualifier를 사용하라고 되어있네요. 즉 여러개의 빈중에 하나를 주입하려면 @Primary를 사용하고 여러개의 빈을 모두 사용하려면 @Qualifier를 사용해야하는 합니다. 예제 코드로 알아보도록 하겠습니다.

 

간단한 Student 객체

public class Student {
   private Integer age;
   private String name;
   // 생략
}

student1과 student2를 빈으로 생성

   <!-- Definition for student1 bean -->
   <bean id = "student1" class = "com.Student">
      <property name = "name" value = "Zara" />
      <property name = "age" value = "11"/>
   </bean>

   <!-- Definition for student2 bean -->
   <bean id = "student2" class = "com.Student">
      <property name = "name" value = "Nuha" />
      <property name = "age" value = "2"/>
   </bean>

원하는 Bean을 명시

public class Profile {
   @Autowired
   @Qualifier("student1")
   private Student student;
   
   @Autowired
   @Qualifier("student2")
   private Student student;
}

생각보다 사용법이 굉장히 단순합니다. 여러 개의 빈을 생성했어도 빈의 이름을 써주면 잘 주입받네요.

테스트를 위해 @Autowired를 사용했는데 아래 저의 프로젝트와 같이 생성자 주입 시에도 어노테이션 활용이 가능합니다.

 

그러면 프로젝트에 적용시켜보겠습니다. 원래의 Configuration Bean에 다음과 같이 이름을 붙여줍니다.

    @Bean
    @Qualifier("oldGitlab")
    public GitLabApi oldGitLabApi(){
        return new GitLabApi(oldGitlabUrl, oldGitlabKey);
    }

    @Bean
    @Qualifier("newGitlab")
    public GitLabApi newGitLabApi(){
        return new GitLabApi(newGitlabUrl, newGitlabKey);
    }

그리고 생성자 주입받는 Service을 아래와 같이 수정합니다.

@Service
public class GitlabMigrationService {

    private final GitLabApi oldGitLabApi;

    private final GitLabApi newGitLabApi;

    public GitlabMigrationService(@Qualifier("oldGitlab")GitLabApi oldGitLabApi,
                                  @Qualifier("newGitlab")GitLabApi newGitLabApi) {
        this.oldGitLabApi = oldGitLabApi;
        this.newGitLabApi = newGitLabApi;
    }
    
}

이렇게 같은 객체임에도 두 개의 다른 Bean을 등록하였고 저는 성공적으로 마이그레이션을 할 수 있었습니다.

 

 

5. 마치며

전에 공부하면서 이런 경우는 언제 쓰일까 했는데 결국 언젠간 쓰이게 되네요. 스프링 주입에 대해서도 다시 공부하는 좋은 시간이었던 것 같습니다.

 

 

참고:

https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-setter-injection
https://sightstudio.tistory.com/20

https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

https://www.tutorialspoint.com/spring/spring_qualifier_annotation.htm

 

Comments