DI는 IoC라는 원칙을 구현하기 위해서 사용되는 방법 중 하나. Dependency Injection 즉, 번역하면 외존성 주입이라고 표현할 수 있음.
위의 코드는 실제 MemberService
라는 객체에서 MemberRepository
라는 객체에 의존성을 가지고 있음. 아래 구현한 세가지 메스들은 모두 MemberRepository
객체에서 구현한 메서드를 사용하고 있음. 이러한 상황에서 MemberRepository
라는 객체를 다른 MockRepository
라는 객체로 교체하는 경우, 새롭게 의존관계를 설정해준 MockRepository
라는 객체에 의존하지 않으면 해당 코드도 모두 변경되어야 함. 그리고 객체간의 관계가 변경될 때마다 직접 해당 코드를 찾아 수정하고 문제점이 없는지 살펴보는 과정을 거쳐야 함. 위에서 발생하는 문제점들은 의존성 주입을 통해 해결할 수 있음.
위 그림에서 작성된 코드와 같이 생성자를 통해 의존성을 주입받음으로써 객체가 생성되는 순간 의존관계를 설정할 수 있음. 생성자 뿐만 아니라 다른 방식으로도 의존성 주입이 가능하지만, 일반적으로는 생성자를 통한 의존관계 주입을 사용함. 생성자를 통해 의존성을 주입하게 되면 실제로 스프링에서 의존성 주입을 도와주게 됨. 이 방법은 스프링에서 공식적으로 추천하는 방법임.
위와 같이 코드를 수정하게 된다면 MemberTest.java
파일에서 MemberService
객체를 생성할 때 오류가 발생하게 됨. 직접 해당 파일에서 MemberRepository
객체를 생성해 생성자를 통해 넣어주면 해결이 가능하지만, 그렇게 되면 객체를 생성할 때마다 직접 주입할 객체를 직접 작성해야 함 -> 의존성 주입을 사용하는 의미 퇴색.
MemberService
와 같이 CoffeeService
객체도 생성자 주입을 통한 코드로 변경 후 새로운 파일을 생성함.
이 후 DependencyConfig.java
파일을 생성 후, 코드를 입력함.
해당 파일을 통해서 의존관계가 이루어지는 부분을 모두 관리할 수 있음. 하지만 현재 작성된 코드는 같은 객체를 new를 통해 두 번 생성함.
명확한 역할을 구현 부분을 나누기 위해 코드를 수정함.
package com.codestates.section2week4;
import com.codestates.section2week4.coffee.CoffeeRepository;
import com.codestates.section2week4.coffee.CoffeeService;
import com.codestates.section2week4.member.MemberRepository;
import com.codestates.section2week4.member.MemberService;
public class DependencyConfig {
public MemberService memberService() {
return new MemberService(memberRepository());
}
public MemberRepository memberRepository() {
return new MemberRepository();
}
public CoffeeService coffeeService() {
return new CoffeeService(coffeeRepository());
}
public CoffeeRepository coffeeRepository() {
return new CoffeeRepository();
}
}
이렇게 역할을 명확하게 나누어 작성해준다면 더 이상 서비스 내부에서 객체 주입 관련해서 수정 코드가 발생하는 것이 아닌 DependencyConfig.java
에서 구현 부분만 수정해주면 됨.
MemberService
와 CoffeeService
입장에서는 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없으며 알 필요도 없음MemberService
와 CoffeeService
는 오로지 실행에만 집중함MemberTest.java
와 CoffeeTest.java
두가지 파일에서 서비스 객체를 생성하는 부분에서 오류 발생MemberTest.java
코드
package com.codestates.section2week4.member;
import com.codestates.section2week4.DependencyConfig;
public class MemberTest {
public static void main(String[] args) {
DependencyConfig dependencyConfig = new DependencyConfig();
MemberService memberService = dependencyConfig.memberService();
Member member = new Member(0L, "lucky@codestates.com", "KimLucky", "010-1234-5678");
memberService.createMember(member);
Member currentMember = memberService.getMember(0L);
System.out.println("회원 가입한 유저 : " + member.getName());
System.out.println("현재 첫번째 유저 : " + currentMember.getName());
if(member.getName().equals(currentMember.getName())) {
System.out.println("새롭게 가입한 사용자와 현재 사용자가 같습니다.");
}
memberService.deleteMember(0L);
if(memberService.getMember(0L) == null) {
System.out.println("회원 삭제가 정상적으로 완료되었습니다.");
}
}
}
CoffeeTest.java
코드
package com.codestates.section2week4.coffee;
import com.codestates.section2week4.DependencyConfig;
public class CoffeeTest {
public static void main(String[] args) {
DependencyConfig dependencyConfig = new DependencyConfig();
CoffeeService coffeeService = dependencyConfig.coffeeService();
Coffee coffee = new Coffee(0L, "바닐라 라떼", "vanilla latte", 5000);
coffeeService.createCoffee(coffee);
if(coffeeService.getCoffee(0L).getKorName().equals(coffee.getKorName())) {
System.out.println("바닐라 라떼가 등록되었습니다.");
}
coffeeService.editCoffee(0L, "바닐라 라떼", 3000);
if(coffeeService.getCoffee(0L).getPrice() == 3000) {
System.out.println("바닐라 라떼의 금액이 정상적으로 변경되었습니다.");
}
coffeeService.deleteCoffee(0L);
if(coffeeService.getCoffee(0L) == null) {
System.out.println("바닐라 라떼가 정상적으로 삭제되었습니다.");
}
}
}
프레임워크에서 사용하는 방식이 아닌 단순 컨테이너를 활용해 객체의 의존관계를 주입하는 방식.
스프링 컨테이너는 스프링 프레임워크의 핵심 컴포넌트임. 스프링 컨테이너는 내부에 존재하는 애플리케이션 빈의 생명주기를 관리함.
ApplicationContext
를 스프링 컨테이너라고 하고 인터페이스로 구현되어 있음 (다형성 적용)
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResroucePatternResolver {
XML, 애너테이션 기반의 자바 설정 클래스
로 만들 수 있음new 생성자
를 썼어야 함new AnnotationConfigApplicationContext (구성정보.class)
로 스프링에 있는 @Bean의 메서드를 등록함// Spring Container 생성
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(DependencyConfig.class);
ApplicationContext
인터페이스의 구현체임파라미터로 넘어온 설정 클래스 정보를 참고해서 빈의 생성, 관계 설정 등의 제어작업을 총괄하는 컨테이너
Bean Factory
Application Context
ApplicationContext 생성자에게 제공된 위치 경로 또는 경로는 컨테이너가 로컬 파일 시스템, Java CLASSPATH 등과 같은 다양한 외부 리소스로부터 구성 메타데이터를 로드할 수 있도록 하는 리소스 문자열
// Annotation
Application context = new
AnnotationConfigApplicationContext(DependencyConfig.class);
스프링 컨테이너에 의해 관리되는 재사용 소프트웨어 컴포넌트
- Spring 컨테이너가 관리하는 자바 객체를 의미하며 하나 이상의 빈을 관리함
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("servfes.xml", "daos.xml");
// retrieve configured instance
PetStareService service = context.getBean("memberRepository", memberRepository.class);
//use configured instance
List<String> userList = service.getUsernameList();
getBean
을 사용하여 bean의 인스턴스를 가져올 수 있음@Bean
이나 <bean>
당 각 1개씩 메타 정보가 생성됨Bean definition을 만들 때 해당 bean definition에 의해 정의된 클래스의 실제 인스턴스를 만들기 위한 레시피를 만듦. (빈이 존재할 수 있는 범위를 의미)
Scope | Description |
---|---|
singleton | (Default) 각 Srping컨테이너에 대한 단일 객체 인스턴스에 대한 단일 bean definition의 범위를 지정함 |
prototype | 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프 |
request | 웹 요청이 들어오고 나갈 때까지 유지되는 스코프 |
session | 웹 세션이 생성되고 종료될 때까지 유지되는 스코프 |
application | 웹 서블릿 컨텍스와 같은 범위로 유지되는 스코프 |
websocket | 단일 bean definition 범위를 WebSocket의 라이프사이클까지 확장함. Spring ApplicationContext의 컨텍스트에서만 유효함. |
클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴임.
자바 기반 설정의 가장 중요한 애너테이션 2가지
// DependencyConfig 클래스
컨텍스트를 인스턴스화 할 때
@Configuration
public class DependencyConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
AnnotationConfigApplicationContext
@Configuration 클래스를 입력으로 사용 (DependencyConfig.class)
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfig.class);
MyService mySerivce = ctx.getBean(MyService.class);
myService.doStuff();
}
@Component 또는 JSR-330 주석이 달린 클래스는 다음과 같이 생성자에 입력으로 사용
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImp.class, Dependency1.class);
MyService myService = ctx.getBean(MySerive.class);
myService.doStuff();
}
@Autowired - MySerivceImpl, Dependency1, Dependency2에서 스프링 의존성 주입 애너테이션을 사용한 예제
@Bean은 메서드-레벨 애너테이션이며, 에서 제공하는 일부 속성을 지원함
빈 선언
@Bean 애너테이션을 메서드에 추가해서 Bean으로 정의(선언)할 수 있음
@Configuration
public class DependenctConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
public interface BaseConfig{
@Bean
default TransferServiceImpl transferService() {
return new TransferSerivceImpl();
}
}
@Configuration
public class DependencyConfig implements BaseConfig{
}
빈 의존성
@Bean 애너테이션이 추가된 (@Bean-annoted) 메서드는 빈을 구축하는데 필요한 의존성을 나타내는데 매개 변수를 사용할 수 있음
@Configuration
public class DependecyConfig{
@Bean
public TransferSerivce transferService(AccountRepository accountRepositry) {
return new TransferServiceImpl(accountRepository);
}
}
Bean 사이에 의존성 주입
빈이 서로 의존성을 가질 때, 의존성을 표현하는 것은 다른 bean 메서드를 호출하는 것처럼 간단함
beanOne
은 생성자 주입을 통해 beanTwo
에 대한 참조를 받음@Configuration
public class DependencyConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
@Configuration
public class DependencyConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientSerivceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientSerivce clientService2() {
ClientSerivceImpl clientSerivce = new ClientSerivceImpl();
clientService.setClientDao(clientDao());
return clientSevice;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()
메서드는 clientSerivce1()
과 clientService2()
메서드에서 한 번씩 호출됨clientDaoImpl
의 새 인스턴스를 만들고 이를 반환하므로 일반적으로 두 개의 인스턴스(각 서비스마다 하나씩)이 있어야 함Spring의 자바 기반 구성 기능 특징인 애너테이션을 사용해 구성의 복잡성을 줄일 수 있음
@Import
애너테이션@Configuration
public class DependencyConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(DependencyConfigA.class)
public class DependencyConfigB {
@Bean
public B b() {
return new B();
}
}
컨텍스르트를 인스턴스화할 때 DependencyConfigA.class와 DependencyConfigB.class 모두 지정하는 대신 DepencyConfigB만 제공하면 됨
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfigB.class);
// now both beans A and B will be available
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.calss);
}
Import(DependencyConfigA.class)
받은 DependencyConfigB.class
사용으로 인해 ctx.getBean(A.class)
가 가능해짐.문제점
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImp(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everthirng wires up across configuration classes
TransferSerivce transferService = ctx.getBean(TransferSeriver.class);
transferService.transfer(100.00, "A123", "C456");
}
해결방법
@Configuration
public class ServiceConfig {
@Autowired
private AccountRespository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRespository);
}
}
@Configuration
public class RespositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSoruce);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new dataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everyhing wires up acrros configuration classes
TransferService transferService = ctx.getBean(TransferService.class);
trasferService.trasfer(100.00, "A123", "C456");
}
스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공함
지금까지는 스프링 빈을 등록할 때 자바 코드의 @Bean or XML의 등의 설정 정보에 등록한 스프링 빈들을 직접 작성하였음. 이렇게 수작업으로 등록하게 되면 설정 정보가 커지고 누락하는 등 다양한 문제가 발생할 수 있음. @ComponentScan
은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록해주기 때문에 설정 정보에 붙여주면 됨
의존관계도 자동으로 주입하는 @Autowired 기능도 제공함.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan
public class AutoDepdencyConfig {
}
기존에 작성하던 DependencyConfig와 비교한다면 @Bean으로 등록한 클래스를 볼 수 없음.
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
기존에 작성한 AppConfig가 있다면 정상적인 작동이 되지 않음.
새 프로그젝트로 진행할 경우엔 문제가 되지 않음
DependenctConfig등 @Configuration 설정이 된 파일이 있을 시 아래 코드 추가: @ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
탐색할 패키지의 시작 위치를 지정하고, 해당 피키지부터 하위 패키지 모두 탐색함.
@SpringBootApplication
을 이 프로젝트 시작 루트 위치에 두는 것을 추천함@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
생성자를 통해서 의존 관계를 주입받는 방법임. 생성자에 @Autowired를 하면 스프링 컨테이너에 @Component로 등록된 빈에서 생성자에 필요한 빈들을 주입함.
특징
NullPointerException
을 방지할 수 있음final
로 선언 가능함예제
@Component
public class CoffeeService {
private final MemberRespository memberRepository;
private final CoffeeRepository coffeeRepository;
@Autrowired
public CoffeeService(MemberRepository memberRepository, CoffeeRespository coffeeRespository) {
this.memberRespository = memberRepository;
this.coffeeRepository = coffeeRespository;
}
}
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존 관계를 주입하는 방법
특징
예제
@Component
public class CoffeeService {
private MemberRepository memberRepository;
private CoffeeRepository coffeeRepository;
@Autowired
public void SetMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setCoffeeRepository(CoffeeRepository coffeeRepository) {
this.coffeeRepository = coffeeRepository;
}
}
set필드명
메서드를 생성하여 의존 관계를 주입하게 됨필드에 @Autowired 붙여서 바로 주입하는 방법임
특징
예제
@Component
public class CoffeeService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private CoffeeRepository coffeeRepository;
}
일반 메서드를 사용해 주입하는 방법
특징
주입할 스프링 빈이 없을 때 동작해야 하는 경우가 있음
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않게 함org.springwork.lang.@Nullable
: 자동 주입할 대상이 없으면 null
이 입력됨Optional<>
: 자동 주입 대상이 없으면Optional.empty
가 입력됨과거에는 수정자, 필드 주입을 많이 사용했지만 최근에는 대부분 생성자 주입 사용을 권장함
NPE(Null Point Exception)
이 발생하는데 의존관계 주입이 누락되었기 때문에 발생함final 키워드
사용 가능final 키워드
를 사용할 수 있음java: variable (데이터 이름) might not have been initialized
BeanCurrentlyInCreationException
이 발생함