Spring 03 : 의존 자동 주입

LeeWonjin·2022년 8월 1일

2022 백엔드스터디

목록 보기
13/20

환경
윈도우즈 10 / 이클립스 2022-06 (4.24.0) / Java SE 18 / 메이븐 3.8.6 / spring-context 5.3.22

교재
책 : 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 챕터 4
영상 : 예제로 배우는 스프링 입문(개정판)

의존 자동 주입 방법

설정코드(e.g. @Configure가 포함된 AppCtx.java)에서 각 Bean객체에 직접 의존주입 하는 대신, 스프링이 의존객체에 맞는 타입의 Bean객체를 찾아 자동으로 주입

구현에는 아래 요소 사용

  • @Autowired, @Qualifier, @Nullable 어노테이션
    ---> 의존객체 필드, 메소드에 사용 가능
  • Optional<T> 타입

의존을 수동으로 주입하는 예시

자동 주입이 없는 경우 아래와 같이 작성할 수 있음 (모두 같은 패키지라고 가정)

  • 클래스 목록
    • Gorani : 관리할 item
    • GoraniDao : 고라니 객체들을 관리하는 데이터 접근관리자
    • Person : 고라니를 저장, 삭제하는 메소드, say()메소드를 가진 사람
    • Staff : Person상속. say()메소드를 재정의하는 고라니 관리직원
    • Mouse : makeSound(String)메소드를 가진 사람의 입(mouse)
  • 시나리오
    • Person, Staff 모두 할 수 있는 일 : GoraniDao를 통한 Gorani의 구매/판매 + Mouse를 통한 발화
    • Staff만 할 수 있는 일 : GoraniDao를 통한 목록 확인
  • 의존 관계
    • Person, StaffGoraniDao, Mouse에 의존
// AppCtx.java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
@Configuration
public class AppCtx {
	@Bean
	public GoraniDao goraniDao() {
		return new GoraniDao();
	}
	
	@Bean
	public Mouse mouse() {
		return new Mouse();
	}
	
	@Bean
	public Person person() {
		Person person;
		person = new Person(mouse()); // 생성자로 의존주입
		person.setGoraniDao(goraniDao()); // setter로 의존주입
		
		return person;
	}
	
	@Bean
	public Staff staff() {
		Staff staff = new Staff(mouse()); // 생성자로 의존주입
		staff.setGoraniDao(goraniDao()); // setter로 의존주입
		return staff;
	}
}

// Main.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
	public static ApplicationContext ctx;
	public static void main(String[] args) {
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		
		Person person = ctx.getBean("person", Person.class);
		person.buyGorani(1, "wonjin");
		person.buyGorani(2, "hyungyu");
		person.buyGorani(3, "comgong");
		person.sellGorani(2);
		person.say(); // I want to go home
		
		Staff staff = ctx.getBean("staff", Staff.class);
		staff.say();
		// I have these goranis
		// [Gorani] #1 : wonjin
		// [Gorani] #3 : comgong
		
		staff.sellGorani(3);
		staff.say();
		// I have these goranis
		// [Gorani] #1 : wonjin
	}
}

// Gorani.java
public class Gorani {
	private int id;
	private String name;
	
	public Gorani(int id, String name) {
		this.id = id;
		this.name = name;
	}
	
	public int getId() { return id; }
	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
	
	@Override
	public String toString() {
		return "[Gorani] #" + id + " : " + name;
	}
}

// GoraniDao.java
import java.util.HashMap;
public class GoraniDao {
	private HashMap<Integer, Gorani> list = new HashMap<Integer, Gorani>(); // Database
	
	public void add(Gorani gorani) { 
		list.put(gorani.getId(), gorani);
	}
	
	public void remove(int id) {
		list.remove(id);
	}
	
	public void printAll() {
		(list.values()).forEach(item -> System.out.println(item));
	}
}

// Person.java
public class Person {
	GoraniDao goraniDao;
	Mouse mouse;
	
	// 생성자 DI
	public Person(Mouse mouse) { this.mouse = mouse; }
	// setter DI
	public void setGoraniDao(GoraniDao goraniDao) { this.goraniDao = goraniDao; }
	
	// 메소드 (buy, sell, say)
	public void buyGorani(int id, String name) {
		goraniDao.add(new Gorani(id, name));
	}
	public void sellGorani(int id) {
		goraniDao.remove(id);
	}
	public void say() {
		mouse.makeSound("I want to go home");
	}
}

// Staff.java
public class Staff extends Person {
	public Staff(Mouse mouse) {
		super(mouse);
	}
	
	@Override
	public void say() {
		mouse.makeSound("I have these goranis");
		goraniDao.printAll();
	}
}

// Mouse.java
public class Mouse {
	public void makeSound(String str) {
		System.out.println(str);
	}
}

@Autowired

AppCtx.java에서 의존주입 문장 삭제
Person.java에서 @Autowired 두 가지 방법으로 사용
Staff.java에서 super()문장 삭제

// AppCtx.java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
@Configuration
public class AppCtx {
	@Bean
	public GoraniDao goraniDao() {
		return new GoraniDao();
	}
	
	@Bean
	public Mouse mouse() {
		return new Mouse();
	}
	
	@Bean
	public Person person() {
		return new Person();
	}
	
	@Bean
	public Staff staff() {
		return new Staff();
	}
}

// Person.java
import org.springframework.beans.factory.annotation.Autowired;
public class Person {	
	// [ 방법 1 ] 필드에 @Autowired 사용
	// 필드에 직접 자동주입 하는 경우. 자동으로 Bean객체 Mouse가 등록됨
	@Autowired 
	Mouse mouse; 
	
	// [ 방법 2 ] setter에 @Autowired 사용
	// setter를 거쳐 자동주입 되는 경우. setter의 매개변수에 자동으로 Bean객체 GoraniDao가 넘어옴
	GoraniDao goraniDao; 
	
	@Autowired
	public void setGoraniDao(GoraniDao goraniDao) { this.goraniDao = goraniDao; }
	
	// 메소드 (buy, sell, say)
	public void buyGorani(int id, String name) {
		goraniDao.add(new Gorani(id, name));
	}
	public void sellGorani(int id) {
		goraniDao.remove(id);
	}
	public void say() {
		mouse.makeSound("I want to go home");
	}
}

// Staff.java
public class Staff extends Person {	
	@Override
	public void say() {
		mouse.makeSound("I have these goranis");
		goraniDao.printAll();
	}
}

한정자

Bean객체는 자신을 식별할 수 있는 수단으로 Bean이름에 더불어 한정자를 가짐
자동주입 수행 시 타입한정자 사용

기본 한정자

한정자를 별도로 지정하지 않으면 아래와 같이 기본 한정자 적용

  • 설정파일에 등록된 Bean객체 : Bean 이름
  • 자동 주입 받는 필드 : 필드 식별자
  • 자동 주입 받는 매개변수 : 매개변수 식별자

상기 <@Autowired> 섹션의 코드 에서 자동주입 가능 여부를 살펴보면 다음과 같음

  1. 아래 두 대상의 타입, 한정자가 일치 → 자동주입 가능
  2. 한정자, 식별자중 하나가 다르더라도 타입을 기준으로 자동주입 가능
파일대상(식별자)타입한정자
AppCtx.javaBean객체 mouseMousemouse
Person.java필드 mouseMousemouse

@Qualifier

기본 한정자만으로 자동주입을 수행할 수 없는 경우 @Qualifier어노테이션으로 한정자를 변경/지정

// 설정파일에서의 사용 예
@Configuration
public class AppCtx {
	@Bean
    @Qualifier("IamDao")
	public GoraniDao goraniDao() {
		return new GoraniDao();
	} // goraniDao Bean객체의 한정자는 IamDao
}

// 의존객체를 주입받는 Bean객체에서의 사용 예
public class Person {
	@Autowired 
    @Qualifier("specialMouse")
	Mouse mouse; // 한정자가 specialMouse, 타입이 Mouse인 객체를 주입받음
	
	GoraniDao goraniDao; 
	@Autowired
    @Qualifier("IamDao")
	public void setGoraniDao(GoraniDao goraniDao) { 
      this.goraniDao = goraniDao; 
    } // 한정자가 IamDao, 타입이 GoraniDao인 객체를 주입받음
}

상기 <@Autowired> 섹션의 코드 를 아래와 같이 변경한 뒤 자동주입 가능 여부를 살펴보면 다음과 같음

  1. Person객체는 필드 Mouse mouse를 자동주입 받음
  2. 같은 타입의 Bean객체 mouse_1mouse_2가 주입 후보
  3. 타입은 결정기준이 될 수 없어 한정자로 주입객체 결정 시도하였으나, 일치하는 한정자가 없음
  4. 따라서 자동주입에 실패하고 아래 예외 발생
WARNING: Exception encountered during context initialization  - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'person': 
Unsatisfied dependency expressed through field 'mouse'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'chap04practice.Mouse' available: 
expected single matching bean but found 2: mouse_1,mouse_2
// AppCtx.java
  ... 생략 ...
public class AppCtx {
	@Bean
	public GoraniDao goraniDao() { 생략 }
	
	@Bean
	public Mouse mouse_1() { return new Mouse(); }
	@Bean
	public Mouse mouse_2() { return new Mouse(); }
	
	@Bean
	public Person person() { 생략 }
	@Bean
	public Staff staff() { 생략 }
}

// Person.java
(수정없음)

정상적으로 자동주입 되도록 수정하는 방법 중 세가지를 뽑아보면 아래와 같음

---------- 방법 1 : 설정파일의 Bean과 주입받는 필드 양쪽에 한정자 지정
// 설정파일
@Configuration
public class AppCtx {
	@Bean
    @Qualifier("wonjin")
	public Mouse mouse_1() { return new Mouse(); }
	
	@Bean
	public Mouse mouse_2() { return new Mouse(); }
}

// 의존객체를 주입받는 클래스
public class Person {
	@Autowired 
    @Qualifier("wonjin")
	Mouse mouse; 
}

---------- 방법 2 : 주입받는 필드의 한정자를 설정파일Bean의 기본한정자로 지정
// 설정파일
@Configuration
public class AppCtx {
	@Bean
	public Mouse mouse_1() { return new Mouse(); }
	
	@Bean
	public Mouse mouse_2() { return new Mouse(); }
}

// 의존객체를 주입받는 클래스
public class Person {
	@Autowired 
    @Qualifier("mouse_1")
	Mouse mouse; 
}

---------- 방법 3 : 주입받는 필드의 식별자를 설정파일Bean의 기본한정자로 변경
// 설정파일
@Configuration
public class AppCtx {
	@Bean
	public Mouse mouse_1() { return new Mouse(); }
	
	@Bean
	public Mouse mouse_2() { return new Mouse(); }
}

// 의존객체를 주입받는 클래스
public class Person {
	@Autowired
	Mouse mouse_1; 
}

자동주입의 필수 여부

어떤 필드가 의존객체를 반드시 넘겨받지 않아도 되도록 설정 가능

아래 코드에서 Person은 의존객체 GoraniDao를 필수적으로 요구.
이를 주입받아도, 받지 않아도 되도록 처리하기 위해 세 가지 기법 사용가능
(아래 코드는 goraniDao가 Bean객체로 등록되지 않았으므로 런타임예외 발생)

// AppCtx.java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
@Configuration
public class AppCtx {
	@Bean
	public Mouse mouse() {
		return new Mouse();
	}
	
	@Bean
	public Person person() {
		return new Person();
	}
}

// Main.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
	public static ApplicationContext ctx;
	public static void main(String[] args) {
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		
		Person person = ctx.getBean("person", Person.class);
		person.buyGorani(1, "wonjin");
		person.buyGorani(2, "hyungyu");
		person.buyGorani(3, "comgong");
		person.sellGorani(2);
		person.say(); // I want to go home
	}
}

// Person.java
import org.springframework.beans.factory.annotation.Autowired;
public class Person {
	@Autowired
	Mouse mouse; 
	
	@Autowired
	GoraniDao goraniDao;
	
	// 메소드 (buy, sell, say)
	public void buyGorani(int id, String name) {
		if(hasDao())
			goraniDao.add(new Gorani(id, name));
		else
			System.out.println("I have no DAO");
	}
	public void sellGorani(int id) {
		if(hasDao())
			goraniDao.remove(id);
		else
			System.out.println("I have no DAO");
	}
	public void say() {
		mouse.makeSound("I want to go home");
	}
	
	
	public boolean hasDao() {
		return goraniDao != null;
	}
}

// Gorani.java
public class Gorani {
	private int id;
	private String name;
	
	public Gorani(int id, String name) {
		this.id = id;
		this.name = name;
	}
	
	public int getId() { return id; }
	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
	
	@Override
	public String toString() {
		return "[Gorani] #" + id + " : " + name;
	}
}

// Mouse.java
public class Mouse {
	public void makeSound(String str) {
		System.out.println(str);
	}
}

@Autowired(required = false)

기존에 사용하던 @Autowired에 인수로 required = false를 줄 수 있음

GoraniDao객체에 대해

  • Bean객체로 등록 된 경우 → 의존 주입 수행
  • Bean객체로 등록되지 않은 경우 → 주입 하지 않음 (Person.java클래스의 goraniDao에 대입 자체를 하지 않음)
// Person.java
import org.springframework.beans.factory.annotation.Autowired;
public class Person {
	@Autowired
	Mouse mouse; 
	
	@Autowired(required = false)
	GoraniDao goraniDao;
	
    ... 생략 ...
}

@Nullable

의존주입 대상에 null을 허용함을 뜻하는 @Nullable 어노테이션을 붙임

GoraniDao객체에 대해

  • Bean객체로 등록 된 경우 → 의존 주입 정상수행
  • Bean객체로 등록되지 않은 경우 → null 대입
// Person.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
public class Person {
	@Autowired
	Mouse mouse; 
	
	@Autowired
    @Nullable
	GoraniDao goraniDao;
	
    ... 생략 ...
}

Optional<T>

java기본 패키지에서 제공하는 Optional로 인수를 받아
의존객체 또는 null 대입

// Person.java
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
public class Person {
	GoraniDao goraniDao;
	
	@Autowired
	Mouse mouse; 
	
	@Autowired
	public void setGoraniDao(Optional<GoraniDao> opt) {
		this.goraniDao = (opt.isPresent())
				? opt.get()
				: null;
	}
	
	// 메소드 (buy, sell, say)
	public void buyGorani(int id, String name) {
    ... 생략 ...
}
profile
노는게 제일 좋습니다.

0개의 댓글