Back-End/Spring

[Spring] Spring Bean과 의존 관계(Dependency Injection)

유자맛바나나 2021. 7. 9. 02:10

 

 

❑ 의존 관계가 필요한 상황

@Controller
public class MemberController {

    private final MemberService memberService = new MemberService();
}

public class MemberService {

}

위와 같이 MemberController가 MemberService 클래스를 사용(의존)하고 있다고 가정하자. MemberController는 새롭게 초기화되는 MemberService가 아닌 1개만 생성되어 사용할 객체가 필요하다면 Spring Dependency Injection(의존성 주입) 기능을 통해 의존관계를 설정해주는 것이 좋다.

 

 

 컴포넌트 스캔과 자동 의존관계 설정

Spring Bean과 의존 관계 설정

@Controller
public class MemberController {

//    private final MemberService memberService = new MemberService();

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}
@Service
public class MemberService {

}
  • Spring 컨테이너와 Spring Bean 등록(컴포넌트 스캔)
    • @Controller, @Service 어노테이션을 클래스 위에 써주면 Spring이 처음 동작할 때 MemberController, MemberService 객체를 생성해 Spring 컨테이너라는 통에 넣어 관리한다. 이를 Spring Bean(콩)으로 등록했다고 말한다.
    • 정확히는 @Component 어노테이션이 있으면 컴포넌트 스캔을 통해 자동으로 Spring Bean으로 등록하는데 @Controller, @Service는 @Component의 특수 케이스이기 때문이다.
      (@Controller, @Service 내부로 가면 @Component를 달고 있음)
    • Spring 컨테이너에 생성되는 각 클래스 별 객체(Bean)는 Singleton으로 등록되어 공유한다(설정으로 Singleton이 아니게 할 수 있지만 특별한 경우를 제외하고는 Singleton 사용).
  • Spring Dependency Injection
    • MemberController가 사용할 MemberService를 넣어줄 때는(=의존성을 주입할 때는) 생성자에 @Autowired (자동으로 묶어준다는 뜻) 어노테이션을 사용하면 연관된 객체를 스프링 컨테이너에서 찾아 넣어준다.
    • 이렇게 객체 의존관계를 외부(Spring)에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다
    • 주입하려는 의존 클래스에 @Service 어노테이션을 써주지 않으면 Spring 컨테이너에 객체가 생성되지 않아 Spring이 찾을 수 없으므로 주의해야 한다.

 

@Service
public class MemberService {
//    private final MemberRepository memberRepository = new MemoryMemberRepository();

    private MemberRepository memberRepository;

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

@Repository
public class MemoryMemberRepository implements MemberRepository {

}

이번에는 MemberService에 MemberRepository객체를 주입해줬다(의존성 주입). @Repository 어노테이션을 사용하면 마찬가지로 컨테이너에 Spring Bean으로 등록되는데 이는 @Repository는 @Component의 특수한 케이스이기 때문이다.

 

[참고] @Controller, @Service, @Repository

  • @Controller, @Service, @Repository가 Spring의 키워드로 사용되는 것은 그만큼 세 가지 구조로 개발하는 것이 정형화된 것을 뜻한다.
  • View를 제어하는 Controller, 비즈니스 로직이 구현된 Service, 정보를 저장하고 있는 Repository로 이해할 수 있다

[주의] 컴포넌트 스캔 시작점

package com.hypeople.HYPEOPLE;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HypeopleApplication {

	public static void main(String[] args) {
		SpringApplication.run(HypeopleApplication.class, args);
	}
}
  • @Component 어노테이션을 찾는 컴포넌트 스캔을 할 때 Spring의 default 스캔 시작점은 @SpringBootApplication 어노테이션이 있는 클래스의 패키지부터 하위 패키지를 스캔한다(별도의 설정을 하면 가능함).
  • 위와 같이 코드가 작성되었을때 Spring은 com.hypeople.HYPEOPLE 패키지에서부터 컴포넌트 스캔을 시작하기 때문에 Spring Bean으로 등록하려고 하는 클래스가 컴포넌트 스캔 범위에 있는지 확인해야 한다.

Spring Bean 등록과 의존관계

그림1. Spring Bean 등록과 의존관계 (출처: 김영한님 인프런 스피링 입문 강의 '스프링 빈과 의존관계')

 

 Java 코드로 직접 Spring Bean 등록하기

@Controller, @Service 등의 어노테이션을 이용해 컴포넌트 스캔으로 Spring Bean에 등록할 수도 있지만 Java코드로 직접 Spring Bean에 등록하는 코드를 작성하여 관리해야 하는 경우도 있다.

@Controller
public class MemberController {

//    private final MemberService memberService = new MemberService();

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

public class MemberService {

    private MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

public class MemoryMemberRepository implements MemberRepository {

}

위에서 MemberService, MemoryMemberService 클래스는 @Service, @Repository 어노테이션을 제거했기 때문에 자동으로 Spring Bean에 등록되지 않는다.

@Configuration 어노테이션을 사용하면 각 클래스를 Java 코드로 직접 Spring Bean에 등록할 수 있다

 

SpringConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

}
  • @Configuration
    Spring 환경설정(Configuraion) 정보를 작성할 SpringConfig 클래스를 만들고 @Configuration 어노테이션을 작성하면 Spring은 SpringConfig 클래스를 환경정보가 담긴 것으로 인식한다
  • @Bean
    @Bean을 쓰면 Spring Bean을 등록한다는 뜻이다.  따라서 @Bean이 달린 memberService() 메서드와 memberRepository() 메서드를 차례대로 호출해 MemberService, MemoryMemberRepository 객체를 Spring Bean에 등록한다.

위와 같이 작성하면 그림1. Spring Bean 의존관계와 동일하게 작동한다.

 

[참고]

  • 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.
  • 정형화 되지 않거나, 상황에 따라 구현 클래스(Implementaio)를 변경해야 하면 설정(@Configuration)을 통해 스프링 빈으로 등록한다.

 

 @Autowired 위치에 따른 3가지 의존성 주입 방법

@Controller
public class MemberController {
    
    /**
     * 의존성 주입 방법1. 필드 주입
     */
    @Autowired
    private MemberService memberService;
    
    // Overload 생성자 있을 시 기본 생성자 정의 필요
    public MemberController() {}
    
    /**
     * 의존성 주입 방법2. 생성자 주입
     */

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }    

    /**
     * 의존성 주입 방법3. Setter 주입
     */
    @Autowired
    public void setMemberService(MemberService memberService) {
    	this.memberService = memberService;
    }
}
  1. 필드 주입
    필드 주입은 잘 사용되지 않는 방법이다. 외부 클래스를 통해 주입되는 방법이 없기 때문에 외부에서 의존성 주입이 불가능하다.
    필드 주입은 기본 생성자(Parameter가 없는)를 호출하면서 주입된다.
    만약 생성자 주입처럼 Parameter가 있는 Overload 생성자가 있다면 컴파일러가 기본 생성자를 따로 만들어주지 않아 컴파일 에러가 나므로 기본 생성자를 정의해줘야 한다.
  2. 생성자 주입
    현업에서 주로 사용되는 방법이다. Application 로딩 시점에 생성자를 통해 의존성 주입 후 변경할 수 없도록 닫혀있다.
  3. Setter 주입
    과거에는 사용하였으나 최근에는 Setter 주입 역시 거의 사용하지 않는다. 이유는 Setter를 public으로 공개하므로 중간에 의존성이 변경될 수 있는 여지가 생기기 때문이다. 거의 모든 상황에서 Application 로딩 후 Runtime에 의존성을 동적으로 변경하는 경우는 없다.