Spring Framework/Spring

[Spring] 3. DI Annotation

hyomee2 2024. 9. 5. 14:15

예제 디렉토리 구조 및 공통 클래스

@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")