[Spring Core] DI Annotation

컨테이너·3일 전
0

SpringFramework

목록 보기
14/15
post-thumbnail

1. @Autowired 기본 개념

@Autowired = “이 타입의 Bean 좀 넣어줘”

  • 스프링 컨테이너 안에서 타입 기준으로 Bean을 찾아서 주입해 준다.
  • BookService 입장에서 보면
    • “나 BookDAO 필요해요”
    • 그러면 컨테이너가 BookDAO 타입 Bean을 찾아 넣어주는 구조.

예시 구조:

public interface BookDAO { ... }

@Repository("bookDAO")
public class BookDAOImpl implements BookDAO { ... }

@Service("bookServiceField")
public class BookService {

    @Autowired
    private BookDAO bookDAO;   // 여기로 BookDAOImpl이 들어옴

    public List<BookDTO> selectAllBooks() { ... }
}

포인트

  • new BookDAOImpl() 이런 코드가 서비스에 없어야 스프링 DI를 제대로 쓰는 것. BookService에서BookDAO를 사용하기 위해 BookDAOImple() 을 여기서 new 한다면, 결합도가 높아지는 것.
  • 의존성은 “필드 선언 + @Autowired” 로 주입 받는다.

2. @Autowired 주입 방법 3가지

2-1. 필드 주입

@Service("bookServiceField")
public class BookService {

    @Autowired
    private /*final*/BookDAO bookDAO;

    ...
}

BookDao 타입 의 bean 객체를 이 프로퍼티에 자동으로 추가한다.

장점

  • 코드가 짧고 편하다.

단점

  • 테스트/리팩토링이 불편하다.
    • 순수 자바 코드로 바로 new 해서 테스트하기 힘들다.
  • 필드 주입에는 final키워드를 사용할 수 없다.

→ 실무에서는 점점 덜 쓰는 추세, 하지만 학습용 예제나 단순 서비스에서 많이 본다.


2-2. 생성자 주입

@Service("bookServiceConstructor")
public class BookService {

    private final BookDAO bookDAO;

    @Autowired   // 생성자가 하나면 생략 가능
    public BookService(BookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }
    
    public List<BookDto> selectAllBooks() {
    return bookDao.selectBookList();
		}
		public BookDto selectBookById(int sequence) {
		    return bookDao.selectOneBook(sequence);
		}
}

장점이 매우 많다:

  1. 의존성이 없으면 객체가 아예 못 만들어진다.
    • 앱 실행 시점에 바로 에러 발견 → 버그 조기 발견
  2. 불변성 보장 (final 사용 가능)
    • 생성 후에 다른 객체로 갈아끼울 수 없음
    • 코드 안정성 증가
  3. 테스트하기 좋다
    • 스프링 없이도
      new BookService(new FakeBookDAO());
      
      이런 식으로 단위 테스트 가능
  • @Autowired 는 생성자가 하나면 spring4.3 버전 이후로 생략해도 자동으로 어노테이션을 주입해준다. 단, 생성자가 1개 이상이라면 명시적으로 작성해야 한다.

→ 스프링 공식 문서와 요즘 코드 스타일은 “생성자 주입 + final 필드” 패턴을 가장 많이 쓴다.

실행파일 예시

public static void main(String[] args) {
    ApplicationContext context =
            new AnnotationConfigApplicationContext("com.ohgiraffers.section01");
    String[] beanDefinitionNames = context.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        System.out.println("beanDefinitionName = " + beanDefinitionName);
    }
    // IoC 컨테이너 확인
    BookService bookService = context.getBean("bookServiceConstructor", BookService.class);
    System.out.println("bookService = " + bookService.selectBookById(1));
    List<BookDto> books = bookService.selectAllBooks();
    for (BookDto book : books) {
        System.out.println(book);
    }
}

IoC 컨테이너 없이 코드를 테스트할 때는 아래와 같이도 간단하게 사용한다.

/* IoC 컨테이너 없이 */
BookService bookService2 = new BookService(new BookDaoImpl());
System.out.println("bookService2 = " + bookService2.selectBookById(1));

2-3. Setter 주입

@Service("bookServiceSetter")
public class BookService {

    private BookDAO bookDAO;

    @Autowired
    public void setBookDAO(BookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }
}

특징:

  • 의존성을 나중에 갈아끼울 수 있다 → “선택적인 의존성”, “테스트용 교체” 등에 가끔 사용
  • 하지만 필수 의존성에는 생성자 주입이 더 적합하다.

2-4. @Autowired(required= true/false)

  • 의존성 주입이 옵션으로 수행될 수 있도록 처리하는데 유용하다.

@Autowired(required = true/false)

스프링이 이 의존성을 반드시 주입해야 하는지 여부를 설정하는 옵션이다.

아주 간단하게 정리하면:

1. required = true (기본값)

“무조건 이 타입의 Bean 있어야 해. 없으면 앱 실행하면 안 돼.”

  • 스프링이 빈을 못 찾으면 애플리케이션 실행 자체가 실패한다.
  • 즉, 스프링이 주입 필수 의존성이라고 판단한다.

예:

@Autowired(required = true)
private BookDAO bookDAO;

BookDAO 타입 Bean이 없으면:

NoSuchBeanDefinitionException

발생하고 프로그램이 안 뜬다.


2. required = false

의미 : bean 이 없어도 무방. 있으면 넣고, 없으면 null로 설정”

@Autowired(required = false)
private BookDAO bookDAO;

이 경우:

  • BookDAO Bean이 있으면 넣어준다.
  • BookDAO Bean이 없어도 오류 없이 null로 남겨둔다.

그래서 다음 같은 설계가 가능하다:

  • 어떤 기능은 Bean이 있을 때만 활성화
  • 개발 환경/운영 환경에서 Bean 존재 여부가 다를 때
  • 외부 모듈이 optional일 때

3. 주의점

required = false 라도 필드를 그대로 사용하면 NPE 발생 가능

bookDAO.selectAllBooks(); // bookDAO null이면 NullPointerException 발생

그래서 선택적 의존성에 보통 아래 방식이 많이 쓰인다.

Optional 로 감싸기

@Autowired(required = false)
private Optional<BookDAO> bookDAO;

또는

생성자 주입에서는 사용하기 불편

스프링 부트 + 현대 스프링에서는 아래 조합이 더 추천됨:

  • Optional
  • @Conditional
  • Profile 기반 분기(@Profile)

4. 한 줄 정리

  • required = true빈이 반드시 있어야 한다(없으면 에러)
  • required = false빈이 없어도 된다(없으면 null로 둠)

3. @Primary, @Qualifier

여러 bean 중 하나를 고르는 Annotation

Pokemon 예제를 기준으로 보면 쉽게 이해된다.

public interface Pokemon { void attack(); }

@Component
public class Charmander implements Pokemon { ... }

@Component
public class Pikachu implements Pokemon { ... }

@Component
public class Squirtle implements Pokemon { ... }

지금 Pokemon 타입 Bean이 3개다.

@Service
public class PokemonService {
    @Autowired
    private Pokemon pokemon;   // 어느 걸 넣어야 할까?
}

이러면 에러가 난다.

“Pokemon 타입 Bean이 3개인데 하나만 고를 수가 없다.”

이때 쓰는 게 @Primary@Qualifier.


3-1. @Primary – 기본 우선순위 지정

  • Primary 는 Bean 객체 중 비어있는 객체에 우선순위를 지정해주는 것이다.
@Component
@Primary
public class Charmander implements Pokemon { ... }
  • Pokemon 타입을 주입해야 할 때
  • 아무 지정이 없으면 항상 Charmander가 기본 선택
@Service("pokemonServicePrimary")
public class PokemonService {
    private final Pokemon pokemon;

    @Autowired
    public PokemonService(Pokemon pokemon) {
        this.pokemon = pokemon;   // 자동으로 Charmander
    }
}

규칙:

  • 같은 타입 여러 개 → @Primary 붙은 것 1개를 “기본값”으로 사용
  • 같은 타입에 @Primary를 2개 붙이면 안 된다.

3-2. @Qualifier – 이름으로 정확히 지정

@Service("pokemonServiceQualifier")
public class PokemonService {

    @Autowired
    @Qualifier("pikachu")
    private Pokemon pokemon;  // pikachu Bean을 넣어라

    public void pokemonAttack() {
        pokemon.attack();
    }
}
  • @Qualifier("빈이름") 으로 정확히 Bean을 지정할 수 있다. 여러 개의 빈 객체 중 특정 빈 객체를 이름으로 지정하는 어노테이션 (@Primary 보다 우선시 된다.)

결과

피카츄 ⚡⚡⚡ 백만 볼트 ⚡⚡⚡

@Primary 가 Charmender에 붙어 있지만 @Qualifier 를 “pikachu” 로 지정하여, pokemon 객체에 pikachu가 들어가 있다.

  • 생성자 주입에서도 사용 가능:
@Autowired
public PokemonService(@Qualifier("squirtle") Pokemon pokemon) {
    this.pokemon = pokemon;   // Squirtle 주입
}

우선순위:

  1. @Qualifier 지정 있으면 → 그 이름 먼저
  2. 없으면 → @Primary 지정된 Bean
  3. 둘 다 없고 여러 개면 → 에러

4. 같은 타입 여러 개를 한 번에 주입: Collection

main

public static void main(String[] args) {
    ApplicationContext context =
            new AnnotationConfigApplicationContext("com.ohgiraffers.section02");
    String[] beanDefinitionNames = context.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        System.out.println("beanDefinitionName = " + beanDefinitionName);
    }
    /* 1. List */
    System.out.println("========== LIST =============");
    // 해당 타입 전부를 싹 긁어오는거임
    PokemonServiceList pokemonService = context.getBean("pokemonServiceCollectionList", PokemonServiceList.class);
    pokemonService.pokemonAttacks();
    /* 2. Map */
    System.out.println("============ MAP ==============");
    PokemonServiceMap pokemonServiceMap = context.getBean("pokemonServiceCollectionMap", PokemonServiceMap.class);
    pokemonServiceMap.pokemonAttacks();
}

4-1. List

@Service("pokemonServiceCollectionList")
public class PokemonServiceList {

    private final List<Pokemon> pokemonList;

    @Autowired
    public PokemonServiceList(List<Pokemon> pokemonList) {
        this.pokemonList = pokemonList;
    }

    public void pokemonAttack() {
        pokemonList.forEach(Pokemon::attack);
    }
}
  • 스프링이 Pokemon 타입 Bean들을 전부 찾아 List에 넣어준다.
  • Bean 이름(아이디) 기준으로 사전 순 정렬되어 들어간다.

main

    /* 1. List */
    System.out.println("========== LIST =============");
    // 해당 타입 전부를 싹 긁어오는거임
    PokemonServiceList pokemonService = context.getBean("pokemonServiceCollectionList", PokemonServiceList.class);
    pokemonService.pokemonAttacks();

4-2. Map<String, Pokemon>

@Service("pokemonServiceCollectionMap")
public class PokemonServiceMap {
    private Map<String, Pokemon> pokemons;
    public PokemonServiceMap(Map<String, Pokemon> pokemons) {
        this.pokemons = pokemons;
    }
    public void pokemonAttacks() {
        pokemons.forEach((name, v) -> {
            System.out.println("Pokemon : "  + name);
            System.out.print("공격 : ");
            v.attack();
        });
    }
}
  • key = Bean 이름
  • value = Bean 객체

이 패턴은 “플러그인 여러 개, 전략 패턴 여러 개”를 한 번에 주입하고 싶을 때 유용하다.

main

/* 2. Map */
System.out.println("============ MAP ==============");
PokemonServiceMap pokemonServiceMap = context.getBean("pokemonServiceCollectionMap", PokemonServiceMap.class);
pokemonServiceMap.pokemonAttacks();

4-3. main

public static void main(String[] args) {
    ApplicationContext context =
            new AnnotationConfigApplicationContext("com.ohgiraffers.section02");
    String[] beanDefinitionNames = context.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        System.out.println("beanDefinitionName = " + beanDefinitionName);
    }
    /* 1. List */
    System.out.println("========== LIST =============");
    // 해당 타입 전부를 싹 긁어오는거임
    PokemonServiceList pokemonService = context.getBean("pokemonServiceCollectionList", PokemonServiceList.class);
    pokemonService.pokemonAttacks();
    /* 2. Map */
    System.out.println("============ MAP ==============");
    PokemonServiceMap pokemonServiceMap = context.getBean("pokemonServiceCollectionMap", PokemonServiceMap.class);
    pokemonServiceMap.pokemonAttacks();
}

결과

========== LIST =============
파이리 불꽃 공격 🔥🔥
피카츄 ⚡⚡⚡ 백만 볼트 ⚡⚡⚡
꼬부기 물대표 공격 🚿🚿
============ MAP ==============
Pokemon : charmander
공격 : 파이리 불꽃 공격 🔥🔥
Pokemon : pikachu
공격 : 피카츄 ⚡⚡⚡ 백만 볼트 ⚡⚡⚡
Pokemon : sqiuretle
공격 : 꼬부기 물대표 공격 🚿🚿

5. @Resource – 이름 우선, 자바 표준

@Service("pokemonServiceResource")
public class PokemonService {

    @Resource(name = "pikachu")
    private Pokemon pokemon;

    public void pokemonAttack() {
        pokemon.attack();
    }
}

특징:

@Resource : 자바 진영에서 제공하는 DI Annotation

@Autowired와 달리 name 속성 값으로 의존성 주입을 할 수 있고 필드 주입, 세터 주입만 가능하다.

  • 자바 표준(JSR) 어노테이션
  • 기본 규칙
    • name 으로 의존성을 주입한다.
    • name 속성을 쓰면: 이름 → (없으면) 타입 순으로 찾는다.
    • name 없이 쓰면: 필드 이름 → 타입 순으로 찾는 형태로 동작
@Resource
private List<Pokemon> pokemonList;   // 타입으로도 주입 가능
  • List<Pokemon> 타입이라면, name 속성을 생략하면 3개의 bean이 담긴다. 규칙은 위에와 동일하다.

주의:

  • 필드 주입, 세터 주입만 가능
  • 생성자 주입은 안 된다. (두 가지 타입 모두)

스프링만 쓸 거면 보통 @Autowired + @Qualifier 조합을 더 자주 사용한다.


6. @Inject – 타입 우선, 자바 표준

@Service("pokemonServiceInject")
public class PokemonService {

    @Inject
    @Named("pikachu")
    private Pokemon pokemon;

    public void pokemonAttack() {
        pokemon.attack();
    }
}
  • @Inject 자체는 @Autowired와 비슷하게 타입 기준 주입
  • Java 진영에서 제공하는 DI Annotation
  • 여러 개 Bean 있을 때는 @Named("빈이름") 으로 선택

결과

꼬부기 물대표 공격 🚿🚿

사용 가능한 위치:

  • 필드
  • 생성자
  • 세터

예: 생성자 주입

@Service("pokemonServiceInject")
public class PokemonService {

    private final Pokemon pokemon;

    @Inject
    public PokemonService(@Named("pikachu") Pokemon pokemon) {
        this.pokemon = pokemon;
    }
}

정리

  • @Autowired = 타입으로 의존성 주입
  • @Primary = 타입 여러 개일 때 “기본값”
  • @Qualifier = 그 중에서도 “이름으로 정확히 지정”
  • @Resource = 이름 우선(자바 표준)
  • @Inject = 타입 우선(자바 표준)

profile
백엔드

0개의 댓글