[Spring] 3. DI Annotation
예제 디렉토리 구조 및 공통 클래스
@Getter
@Setter
@ToString
@AllArgsConstructor
public class BookDTO {
private int sequence; //도서번호
private int isbn; //isbn
private String title; //제목
private String author; //저자
private String publisher; //출판사
private Date createdDate; //출판일
}
public interface BookDAO {
/* 도서 목록 전체 조회 */
List<BookDTO> selectBookList();
/* 도서 번호로 도서 조회 */
BookDTO selectOneBook(int sequence);
}
/* @Repository : @Component의 세분화 어노테이션의 한 종류로 DAO 타입의 객체에 사용한다. */
@Repository("bookDAO")
public class BookDAOImpl implements BookDAO {
private Map<Integer, BookDTO> bookList;
public BookDAOImpl() {
bookList = new HashMap<>();
bookList.put(1, new BookDTO(1, 123456, "자바의 정석", "남궁성", "도우출판", new Date()));
bookList.put(2,
new BookDTO(2, 654321, "칭찬은 고래도 춤추게 한다", "고래", "고래출판", new Date()));
}
@Override
public List<BookDTO> selectBookList() {
return new ArrayList<>(bookList.values());
}
@Override
public BookDTO selectOneBook(int sequence) {
return bookList.get(sequence);
}
}
@Autowired
@Autowired 어노테이션은 Type을 통한 DI를 할 때 사용한다.
스프링 컨테이너가 알아서 해당 타입의 Bean을 찾아서 주입해준다.
1. 필드(field) 주입
private BookDAO bookDAO = new BookDAOImpl();와 같이 필드를 선언하면
BookService 클래스는 BookDAOImpl 클래스의 변경에 직접적으로 영향받는 강한 결합을 가지게 된다.
직접적으로 객체를 생성하는 생성자 구문 대신에 필드에 @Autowired 어노테이션을 작성하면
객체 간의 결합을 느슨하게 할 수 있고,
스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다.
// section01\autowired\subsection01\field\BookService.java
// @Service: @Component의 세분화 어노테이션으로 Service 계층에서 사용한다.
@Service("bookServiceField")
public class BookService {
// BookDAO 타입의 빈 객체를 이 프로퍼티에 자동으로 주입해준다.
@Autowired // -> 필드 주입
private /* final */ BookDAO bookDAO;
// 도서 목록 전체 조회
public List<BookDTO> selectAllBooks() {
return bookDAO.selectBookList();
}
// 도서 번호로 도서 조회
public BookDTO searchBookBySequence(int sequence) {
return bookDAO.selectOneBook(sequence);
}
}
// section01\autowired\subsection01\field\Application.java
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section01");
/*applicationContext에서 BookService 타입의 이름이 bookServiceField인 빈을 가져온다.*/
BookService bookService = applicationContext.getBean("bookServiceField",BookService.class);
/*bean 사용하기*/
bookService.selectAllBooks().forEach(System.out::println);
System.out.println(bookService.searchBookBySequence(1));
/* 필드 주입의 경우 IoC 컨테이너 없이 테스트 하려고 하면 bookDAO 의존성 주입이 불가능해서
* 아래 코드 수행시 NullPointerException 발생한다. => 생성자 주입 권장 */
BookService bookService2 = new BookService(); // 빈을 수동으로 생성한다.
// 빈을 수동으로 생성하는 것은 피하자 -> Spring 컨텍스트를 통해 빈을 가져와야 의존성이 제대로 주입된다.
bookService2.selectAllBooks();
}
}
2. 생성자(constructor) 주입
스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다.
// section01\autowired\subsection02\constructor\BookService.java
// @Component의 세분화 어노테이션으로 Service 계층에서 사용한다.
@Service("bookServiceConstructor")
public class BookService {
private final BookDAO bookDAO;
//public BookService() {}
// 생성자 주입
// Spring 4.3 버전 이후부터 생성자가 1개 뿐이라면 어노테이션을 생략해도 자동으로 생성자 주입이 동작한다.
// 단, 생성자가 1개 이상인 경우에는 명시적으로 작성해주어야 한다.
//@Autowired
public BookService(BookDAO bookDAO) {
this.bookDAO = bookDAO;
}
public List<BookDTO> selectAllBooks() {
return bookDAO.selectBookList();
}
public BookDTO searchBookBySequence(int sequence) {
return bookDAO.selectOneBook(sequence);
}
}
// section01\autowired\subsection02\constructor\Application.java
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section01");
BookService bookService = applicationContext.getBean("bookServiceConstructor", BookService.class);
bookService.selectAllBooks().forEach(System.out::println);
System.out.println(bookService.searchBookBySequence(1));
/* IoC 컨테이너 없이 코드를 테스트 할 때 생성자를 통해 BookDAO 객체를 전달하므로
* 아래 코드는 문제 없이 테스트 될 수 있다. */
BookService bookService2 = new BookService(new BookDAOImpl());
bookService2.selectAllBooks();
}
(1) 생성자 주입의 장점
1) 객체가 생성될 때 모든 의존성이 주입되므로 의존성을 보장할 수 있다.
- 필드 주입이나 세터 주입은 의존성이 있는 객체가 생성되지 않아도 객체 생성은 가능하여 메소드가 호출되면(런타임) 오류 발생
- 생성자 주입은 의존성이 있는 객체가 생성되지 않으면 객체 생성이 불가능하여 어플리케이션 실행 시점에 오류 발생
2) 객체의 불변성을 보장할 수 있다.
- 필드에 final을 사용할 수 있고, 객체 생성 이후 의존성을 변경할 수 없어 안정성 보장
3) 코드 가독성이 좋다.
- 해당 객체가 어떤 의존성을 지니고 있는지 명확히 확인 가능
4) DI 컨테이너와의 결합도가 낮기 때문에 테스트하기 좋다.
- 스프링 컨테이너 없이 테스트 가능
3. Setter 주입
setter 메소드에 @Autowired 어노테이션을 작성하면,
스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다.
// section01\autowired\subsection03\setter\BookService.java
// @Component의 세분화 어노테이션으로 Service 계층에서 사용한다.
@Service("bookServiceSetter")
public class BookService {
private /* final */ BookDAO bookDAO;
// BookDAO 타입의 빈 객체를 setter에 자동으로 주입해준다.
@Autowired
public void setBookDAO(BookDAO bookDAO) {
this.bookDAO = bookDAO;
}
public List<BookDTO> selectAllBooks() {
return bookDAO.selectBookList();
}
public BookDTO searchBookBySequence(int sequence) {
return bookDAO.selectOneBook(sequence);
}
}
// section01\autowired\subsection03\setter\Application.java
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section01");
BookService bookService = applicationContext.getBean("bookServiceSetter", BookService.class);
bookService.selectAllBooks().forEach(System.out::println);
System.out.println(bookService.searchBookBySequence(1));
}
}
예제 디렉토리 구조 및 공통 클래스
public interface Pokemon {
void attack();
}
@Component
@Primary // @Autowired로 동일 타입의 여러 빈이 찾아진 경우 우선 시 할 타입을 설정
// 여기선 PokemonService의 생성자로 Pokemon 객체를 주입받으면 Charmander 빈 객체가 우선적으로 주입된다.
public class Charmander implements Pokemon {
@Override
public void attack() {
System.out.println("파이리 불꽃 공격");
}
}
@Component
public class Pikachu implements Pokemon {
@Override
public void attack() {
System.out.println("피카츄 백만볼트");
}
}
@Component
public class Squirtle implements Pokemon {
@Override
public void attack() {
System.out.println("꼬부기 물대포 발사");
}
}
@Primary
여러 개의 빈 객체 중에서 우선순위가 가장 높은 빈 객체를 지정하는 어노테이션이다.
// section02\annotation\subsection01\primary\PokemonService
@Service("pokemonServicePrimary")
public class PokemonService {
private Pokemon pokemon;
// @Autowired로 생성자 의존성 주입
@Autowired
public PokemonService(Pokemon pokemon) {
this.pokemon = pokemon;
}
public void pokemonAttack() {
pokemon.attack();
}
}
// section02\annotation\subsection01\primary\Application
public class Application {
public static void main(String[] args) {
// Charmander, Pikachu, Squirtle, PokemonService를 빈 스캐닝할 수 있도록 스프링 컨테이너 생성
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServicePrimary", PokemonService.class);
pokemonService.pokemonAttack();
}
}
만약 Charmander 클래스에 @Primary 어노테이션이 없다면 에러가 발생한다.
스프링 컨테이너 내부에 Pokemon 타입의 빈 객체가 charmander, pikachu, squirtle 총 3개가 있어서
1개의 객체를 PokemonService의 생성자로 전달할 수 없기 때문이다. -> 이럴 경우 @Primary 어노테이션 필요
@Primary 어노테이션을 설정하면
@Autowired로 동일한 타입의 여러 빈을 찾게 되는 경우 자동으로 연결 우선 시 할 타입으로 설정된다.
동일한 타입의 클래스 중 한 개만 @Primary 어노테이션을 사용할 수 있다.
@Qualifier
여러 개의 빈 객체 중에서 특정 빈 객체를 이름을 지정하는 어노테이션
@Primary와 같이 쓰인 경우 @Qualifier가 우선시 된다.
1. 필드 주입
// section02\annotation\subsection02\qualifier\PokemonService
@Service("pokemonServiceQualifier")
public class PokemonService {
// @Qualifier : 여러 개의 빈 객체 중 특정 빈 객체를 이름으로 지정하는 어노테이션
/* 1. 필드 주입의 경우 */
// @Qualifier 어노테이션을 사용하여 pikachu 빈 객체를 지정한다.
@Autowired
@Qualifier("pikachu")
private Pokemon pokemon;
public void pokemonAttack() {
pokemon.attack();
}
}
2. 생성자 주입
생성자 주입의 경우 @Qualifier 어노테이션은 메소드 파라미터의 앞에 기재한다.
마찬가지로 빈 이름을 통해 주입할 빈 객체를 지정한다.
// section02\annotation\subsection02\qualifier\PokemonService
@Service("pokemonServiceQualifier")
public class PokemonService {
// @Qualifier : 여러 개의 빈 객체 중 특정 빈 객체를 이름으로 지정하는 어노테이션
/* 2. 생성자 주입의 경우 */
@Autowired
public PokemonService(@Qualifier("pikachu") Pokemon pokemon) {
this.pokemon = pokemon;
}
public void pokemonAttack() {
pokemon.attack();
}
}
// section02\annotation\subsection02\qualifier\Application
// 위의 필드 주입 & 생성자 주입에서 모두 사용하는 Application 클래스
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServiceQualifier", PokemonService.class);
pokemonService.pokemonAttack();
}
}
Collection
같은 타입의 빈을 여러 개 주입받고 싶다면 Collection 타입을 활용할 수 있다.
Collection 타입으로 의존성 주입을 받게되면 해당 타입의 등록된 빈이 모두 주입된다.
1. List 타입
// section02\annotation\subsection03\collection\PokemonService
@Service("pokemonServiceCollection")
public class PokemonService {
/* 1. List 타입으로 주입 */
private List<Pokemon> pokemonList;
@Autowired // List<Pokemon> 타입 빈을 PokemonService에 주입
/*스프링 컨테이너가 초기화될 때 List<Pokemon> 빈이 생성되고, 이 리스트는 컨테이너에 등록된 모든 Pokemon 빈을 포함하게 된다.
PokemonService 빈이 생성될 때, 스프링은 이미 생성된 List<Pokemon> 빈을 주입한다.*/
public PokemonService(List<Pokemon> pokemonList) {
this.pokemonList = pokemonList;
}
public void pokemonAttack() {
pokemonList.forEach(Pokemon::attack);
}
}
// section02\annotation\subsection03\collection\Application
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServiceCollection", PokemonService.class);
pokemonService.pokemonAttack();
}
}
2. Map 타입
// section02\annotation\subsection03\collection\PokemonService
@Service("pokemonServiceCollection")
public class PokemonService {
// 빈 이름을 기준으로 사전순으로 map에 추가되어 모든 Pokemon 타입의 빈이 주입된다.
private Map<String, Pokemon> pokemons;
public PokemonService(Map<String, Pokemon> pokemons) {
this.pokemons = pokemons;
}
public void pokemonAttack() {
pokemons.forEach((k, v) -> {
System.out.println(k + " " + v);
v.attack();
});
}
}
@Resource
자바에서 제공하는 기본 어노테이션으로,
@Autowired와 같은 스프링 어노테이션과 달리 name 속성 값으로 의존성 주입을 할 수 있다.
필드 주입과 세터 주입은 가능하지만 생성자 주입은 불가능하다.
@Resource는 사용하기 전 라이브러리 의존성 추가가 필요하므로 build.gradle에 아래 코드를 추가해준다.
아래 코드는 Maven Repository에서 jakarta annotation을 검색하여 얻을 수 있다.
dependencies {
// https://mvnrepository.com/artifact/jakarta.annotation/jakarta.annotation-api
implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'
}
1. 이름으로 주입
@Resource 어노테이션의 name 속성에 주입할 빈 객체의 이름을 지정한다.
(1) 필드 주입
// section02\annotation\subsection04\resource\PokemonService
// Pokemon 타입의 객체를 의존성 주입받는 PokemonService 클래스 선언
@Service("pokemonServiceResource")
public class PokemonService {
// 필드 주입
@Resource(name = "pikachu")
private Pokemon pokemon;
public void pokemonAttack() {
pokemon.attack();
}
}
(2) 세터 주입
// section02\annotation\subsection04\resource\PokemonService
// Pokemon 타입의 객체를 의존성 주입받는 PokemonService 클래스 선언
@Service("pokemonServiceResource")
public class PokemonService {
// 세터 주입
@Resource(name = "pikachu")
public void setPokemon(Pokemon pokemon) {
this.pokemon = pokemon;
}
public void pokemonAttack() {
pokemon.attack();
}
}
// section02\annotation\subsection04\resource\Application
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServiceResource", PokemonService.class);
pokemonService.pokemonAttack();
}
}
2. 타입으로 주입
List<Pokemon> 타입으로 변경한 뒤 name 속성을 따로 기재하지 않고 동작시킬 수 있다.
(이 때는 @Resource라고만 어노테이션을 작성하면 된다.)
-> 기본적으로는 name 속성으로 주입하지만 name 속성이 없을 경우 type을 통해 의존성을 주입한다.
// section02\annotation\subsection04\resource\PokemonService
@Service("pokemonServiceResource")
public class PokemonService {
@Resource
private List<Pokemon> pokemonList;
public void pokemonAttack() {
pokemonList.forEach(Pokemon::attack);
}
}
// section02\annotation\subsection04\resource\Application
// @Resource 관련해서 계속 사용하는 Application
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServiceResource", PokemonService.class);
pokemonService.pokemonAttack();
}
}
@Inject
자바에서 제공하는 기본 어노테이션으로,
@Autowired 어노테이션과 유사하게 Type을 통해 의존성 주입을 하며 @Named을 통해 빈 이름을 명시할 수 있다.
필드, 생성자, 세터 주입 방식이 가능하다.
@Inject는 사용하기 전 라이브러리 의존성 추가가 필요하므로 build.gradle에 아래 코드를 추가해준다.
아래 코드는 Maven Repository에서 jakarta inject을 검색하여 얻을 수 있다.
dependencies {
// https://mvnrepository.com/artifact/jakarta.inject/jakarta.inject-api
implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}
1. 필드 주입
@Inject 어노테이션은 Type으로 의존성 주입을 하는데, 현재 3개의 동일한 타입의 빈이 있기 때문에 오류가 발생한다.
따라서 @Named 어노테이션을 함께 사용해서 빈의 이름을 지정하면 해당 빈을 의존성 주입할 수 있다.
필드, 생성자, 세터 주입 방식이 가능하다.
// section02\annotation\subsection05\inject\PokemonService
@Service("pokemonServiceInject")
public class PokemonService {
// 1. 필드 주입
@Inject
@Named("pikachu")
private Pokemon pokemon;
public void pokemonAttack() {
pokemon.attack();
}
}
2. 생성자 주입
// section02\annotation\subsection05\inject\PokemonService
@Service("pokemonServiceInject")
public class PokemonService {
private Pokemon pokemon;
// 2. 생성자 주입
// @Named 어노테이션은 메소드 레벨, 파라미터 레벨에서 모두 사용 가능하다.
@Inject
public PokemonService(@Named("pikachu") Pokemon pokemon) {
this.pokemon = pokemon;
}
public void pokemonAttack() {
pokemon.attack();
}
}
3. 세터 주입
// section02\annotation\subsection05\inject\PokemonService
@Service("pokemonServiceInject")
public class PokemonService {
private Pokemon pokemon;
/* 3. 세터 주입 */
@Inject public void setPokemon(@Named("pikachu") Pokemon pokemon) {
this.pokemon = pokemon;
}
public void pokemonAttack() {
pokemon.attack();
}
}
// section02\annotation\subsection05\inject\Application
// @Inject 관련해서 계속 사용하는 Application
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section02");
PokemonService pokemonService = applicationContext.getBean("pokemonServiceInject", PokemonService.class);
pokemonService.pokemonAttack();
}
}
정리
개발자는 객체 간의 의존성을 직접 관리하지 않고 스프링 컨테이너가 객체 간의 의존성을 주입해주는 방식으로 관리할 수 있고,
다양한 DI 어노테이션이 있다.
@Autowired | @Resource | @Inject | |
제공 | Spring | Java | Java |
지원 방식 | 필드, 생성자, 세터 | 필드, 세터 | 필드, 생성자, 세터 |
빈 검색 우선 순위 | 타입 -> 이름 | 이름 -> 타입 | 타입 -> 이름 |
빈 지정 문법 | @Autowired @Qualifier("name") |
@Resource(name="name") | @Inject @Named("name") |