Spring - DI Annotation

제훈·2024년 8월 12일

Spring

목록 보기
5/18

DI Annotation

이번에는 어노테이션을 활용한 DI에 대해 알아보자.

@Autowired : Type을 통한 DI를 할 때 사용한다. 스프링 컨테이너가 알아서 해당 타입의 Bean을 찾아서 주입해준다.

기본적으로 작성해두고 사용할 클래스들

BookDTO

import lombok.*;

import java.util.Date;

@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter @ToString
public class BookDTO {
    private int sequence;
    private int isbn;
    private String title;
    private String author;
    private String publisher;
    private Date createdDate;
}

BookDAO 인터페이스

import java.util.List;

public interface BookDAO {
    List<BookDTO> findAllBook();

    BookDTO searchBookBySequence(int sequence);
}

BookDAOImpl

import java.util.*;

@Repository
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, 222222, "와! 칭찬! 고래춤!", "whale", "흰수염", new Date()));
    }

    @Override
    public List<BookDTO> findAllBook() {
        /* HashMap은 ArrayList로 쉽게 바꿀 수 있다. HashMap -> Collection -> ArrayList */
        return new ArrayList<>(bookList.values());
    }

    @Override
    public BookDTO searchBookBySequence(int sequence) {
        // 실제로는 select 쿼리를 작성해야하지만 일단은 이렇게 두자.
        return bookList.get(sequence);
    }
}

Impl은 인터페이스의 구현체인데 그럼 Service -> BookDAO -> BookDAOImpl 방향으로 의존이 돼 있을까?

아니다. Service -> BookDAO <- BookDAOImpl 이런 방향으로 돼 있다.
그 이유는 컴파일 시점에는 BookDAO는 Impl이 자신의 메소드를 구현하고 있는지 모른다.
자식이지만 존재를 모르는.. 느낌...

그럼 서비스 코드도 작성해본다.

이 코드 또한 여러 방식이 있는데

의존성 주입 방식

1. 필드 주입 방식

  • 장점
    - 1. 개발할 때는 편하다.

      1. 그래서 테스트 코드에서는 쓸만 하다.
  • 단점
    - 1. 자바의 reflection 기술을 마음대로 쓰면 캡슐화가 적용이 안 될 수 있기 때문에 위험하다. (안티 패턴 중 하나)
    - 2. 너무 @Autowired 어노테이션을 남발하게 된다.

BookService

import com.jehun.section01.common.BookDAO;
import com.jehun.section01.common.BookDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookService {
    /*
    이전까지는 아래처럼 초기화해줬지만 @Autowired 어노테이션을 사용할 것이다.
    BookDAO bookDAO = new BookDAOImpl();
    */

    @Autowired
    private BookDAO bookDAO;

    public List<BookDTO> findAllBook() {
        return bookDAO.findAllBook();
    }

    public BookDTO searchBookBySequence(int sequence) {
        return bookDAO.searchBookBySequence(sequence);
    }
}

Application

import com.jehun.section01.autowired.subsection01.BookService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.jehun.section01");

        BookService bookService = context.getBean("bookService", BookService.class);

        /* 설명. 전체 도서 목록 조회 후 출력 확인 */
        bookService.findAllBook().forEach(System.out::println);

        /* 설명. 도서 번호로 검색 후 출력 확인 */
        System.out.println(bookService.searchBookBySequence(1));
        System.out.println(bookService.searchBookBySequence(2));
    }
}

실행결과

실행했을 때의 흐름

  1. Application 클래스 속 "com.jehun.section01" 중에 스프링 컨테이너를 생성 -> @Repository, @Service등의 어노테이션이 작성 된 클래스가 빈 스캐닝을 통해 잘 등록 되었는지 확인하러 감
  2. @Service 어노테이션을 보고 BookService의 기본 생성자를 보는데, @Autowired 어노테이션을 보게 된다. -> 해당 타입을 찾으러 감
  3. BookDAOImpl에도 @Repository 어노테이션이 있기 때문에 스프링 컨테이너가 해당 타입(BookDAOImpl)을 뜻하는 것이라는 것을 알게 되며 의존성 자동 주입을 해준다.

2. setter를 이용한 주입 방식

시작하기 전 필드 방식과 다르게 하기 위해서 패키지를 새로 만들어서 정의할건데 서비스에서 BookService 클래스 이름이 같아서 안 된다.

같은 component-scan 범위 안에 같은 타입에 같은 이름으로 2개 이상의 bean이 공존할 수 없기 때문이다.

그래서 이전 BookService에서 @Service 어노테이션을 @Service("bookServiceField") 하고, 이번 BookService는 Setter를 사용할 것이기 때문에 아래처럼 만든다.

BookService (2번째)

import com.jehun.section01.common.BookDAO;
import com.jehun.section01.common.BookDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("bookServiceSetter")
public class BookService {

    @Autowired
    private BookDAO bookDAO;

    public BookService() {
    }

	/* BookDAO 타입의 빈 객체를 setter에 자동으로 주입해준다. */
    @Autowired
    public void setBookDAO(BookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }

	...
}

setter로 주입했을 때의 문제점 또한 있다.
다른 객체임에도 주소가 같아서 같은 객체라고 여기게 되는 문제가 생길 수도 있다.
그리고 setter로 인해 무조건 public 접근 지정자로 만들어야하기 때문에 캡슐화에 대해 문제가 생길 수 있다.

필드와 setter 주입 방식의 가장 큰 단점
상수를 넣었을 때 의존성 주입에서 에러가 생긴다.

그래서 생성자 주입을 더 권장하는 것이다.


3. 생성자 주입 방식

BookService

import com.jehun.section01.common.BookDAO;
import com.jehun.section01.common.BookDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("bookServiceConstructor")
public class BookService {

    private BookDAO bookDAO;

    public BookService() {
    }

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

생성자 주입 방식을 권장하는 이유도 위에 적은 것들에 추가적인 것도 알아보자.

  1. 필드에 final 키워드를 추가해도 에러가 나지 않는다. 즉, 사용할 수 있다.
  2. 순환 참조를 방지할 수 있다. => 필드 주입 방식과 setter주입 방식은 컴퓨터를 키자마자 판별해주지 않기도 하지만, 생성자는 아니다.
  3. 필드 주입과 Setter 주입의 단점 : @Autowired 를 남발하게 될 수도 있다. -> 자바의 reflection 기술을 통해 캡슐화 적용이 불가능하다.
  4. 테스트 코드 시에 생성자를 통해 편하게 테스트할 수 있다. (Mock 객체 생성 불필요하다.)

추가적인 DI 어노테이션

@Primary

@Primary : 여러 개의 빈 객체 중에서 우선순위가 가장 높은 빈 객체를 지정하는 어노테이션이다.

포켓몬 예제로 만들어보자. 나에게는 피카츄, 파이리, 꼬부기가 있으며, 셋 중 우선순위가 가장 높은 객체를 피카츄로 정해서 해볼 것이다.

Pokemon 인터페이스

public interface Pokemon {
    void attack();
}

Pikachu, Charmander, Squirtle

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class Pikachu implements Pokemon {

    @Override
    public void attack() {
        System.out.println("피카츄 공격");
    }
}

클래스에 코드 내용을 똑같다. (파이리 공격, 꼬부기 공격 출력문과 이름만 다름)
차이점으로는 피카츄에만 @Component 옆에 @Primary 어노테이션을 추가해줬다.

PokemonService

import com.jehun.section02.annotation.common.Pokemon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("pokemonServicePrimary")
public class PokemonService {

    private Pokemon pokemon;

    @Autowired
    public PokemonService(Pokemon pokemon) {
        this.pokemon = pokemon;
    }

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

}

Application

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.jehun.section02");

        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName: beanNames) {
            System.out.println("beanName = " + beanName);
        }

        PokemonService pokemonService = context.getBean("pokemonServicePrimary", PokemonService.class);
        pokemonService.pokemonAttack();
    }
}

이렇게 했을 때 사실 @Primary 어노테이션을 지우고 실행하면 같은 우선순위인 bean 객체가 피카츄, 파이리, 꼬부기 3개가 겹치면서 에러가 발생한다.

그 문제점을 이제 우선순위가 가장 높은 빈 객체를 지정해 해결한 것이다.

실행결과


@Qualifier

@Qualifier : 여러 개의 빈 객체 중에서 특정 빈 객체를 이름으로 지정하는 어노테이션

context.getBean("pokemonServiceQualifier") 으로 변경해보면서 Service도 수정해봤다.

Application

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ohgiraffers.section02");

        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName: beanNames) {
            System.out.println("beanName = " + beanName);
        }

        PokemonService pokemonService = context.getBean("pokemonServiceQualifier", PokemonService.class);
        pokemonService.pokemonAttack();
    }
}

PokemonService

import com.jehun.section02.annotation.common.Pokemon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service("pokemonServiceQualifier")
public class PokemonService {

    private Pokemon pokemon;

    @Autowired
    public PokemonService(@Qualifier("squirtle") Pokemon pokemon) {
        this.pokemon = pokemon;
    }

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

아까는 피카츄였지만 지금은 꼬부기다..

실행결과


Collection

Collection 타입 : 같은 타입의 빈을 여러 개 주입 받고 싶다면 사용할 수 있다.

List 타입

같은 타입의 Bean을 List 형태로 생성자 주입을 해본다.

PokemonService

import com.jehun.section02.annotation.common.Pokemon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("pokemonServiceCollection")
public class PokemonService {

    private List<Pokemon> pokemonList;

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

    public void pokemonAttack() {
        pokemonList.forEach(Pokemon::attack);
    }
}

그럼 List에는 어떻게 요소들이 추가될까??
-> 알파벳순이다.

Application

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ohgiraffers.section02");

        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName: beanNames) {
            System.out.println("beanName = " + beanName);
        }

        PokemonService pokemonService = context.getBean("pokemonServiceCollection", PokemonService.class);
        pokemonService.pokemonAttack();
    }
}

실행결과


Map 타입

PokemonService

import com.jehun.section02.annotation.common.Pokemon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service("pokemonServiceCollection")
public class PokemonService {

    private Map<String, Pokemon> pokemonMap;
    
    @Autowired
    public PokemonService(Map<String, Pokemon> pokemonMap) {
        this.pokemonMap = pokemonMap;
    }

    public void pokemonAttack() {
        pokemonMap.forEach((k, v) -> {
            System.out.println("key : " + k);
            System.out.println("value : " + v);
            v.attack();
        });
    }
}

Application

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.jehun.section02");

        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName: beanNames) {
            System.out.println("beanName = " + beanName);
        }

        PokemonService pokemonService = context.getBean("pokemonServiceCollection", PokemonService.class);
        pokemonService.pokemonAttack();
    }
}

실행결과


다른 것들은 이제

  • @Resource : @Autowired와 같은 스프링 어노테이션과 다르게 name 속성 값으로 의존성 주입을 할 수 있다.
    - 의존성 추가 필요
    			dependencies {
    				implementation("javax.annotation:javax.annotation-api:1.3.2")
    			...생략
    			}
  • @Inject : @Autowired 어노테이션과 같이 Type 으로 빈을 의존성 주입한다.
    - 의존성 추가 필요
    
    			dependencies {
    				implementation("javax.inject:javax.inject:1")
    			...생략
    			}

정리

@Autowried@Resource@Inject
제공SpringJavaJava
지원 방식필드, 생성자, 세터필드, 세터필드, 생성자, 세터
빈 검색 우선 순위타입 → 이름이름 → 타입타입 → 이름
빈 지정 문법@Autowired
@Qualifier(”name”)
@Resource(name=”name”)@Inject
@Named(”name”)

복습하면서 확실하게 알게 된 내용

이 내용은 하브루타를 하면서 궁금증이 생긴 내용을 찾아보면서 알게 된 내용이다.

@Component, @Service, @Repository, @Controller, @Configuration 등 이러한 역할을 하는 것들의 상위 개념이다.

근데 보니까 앞의 4개는 @Bean이라고 해서 스프링 컨테이너에 객체나 메소드를 등록하지 않고 사용하며, @Configuration@Bean 으로 등록을 하고 있길래 왜 그럴까 생각을 해봤다..

@Configuration 이 아니라고 해도 @Bean을 등록하는 경우들이 있어서 공부를 해보고 알게된 것을 작성하려고 한다.

알게된 점

일단 전부 다 클래스에 붙기 때문에 스프링 컨테이너에 등록이 된 채로 사용한다.
@Component 를 제외하고 얘기하겠다. (가장 상위 개념이기에)

@Controller는 일단 MVC 패턴과 관련이 있다.

Spring MVC 패턴의 요청 처리 과정 링크

Dispathcer Servlet이 Handler mapping을 통해 요청에 맞는 컨트롤러를 찾아주는데, 그 때 Service는 자동으로 의존성 주입이 되고 어차피 비즈니스 로직을 통해 Repository도 의존성 주입이 된다. (생성자로 @Autowired 를 하거나 하면서 자동으로 된다.)
@Service, @Repository 는 결국 스프링 컨테이너에 등록만 돼 있다면 자동으로 의존성 주입을 통해 내부 메소드를 사용할 수 있게 되는 것이다.

하지만 @Configuration은 설정 파일로써 만약에 안에 있는 객체들을 사용한다면 그 때 @Bean 으로 등록하는 것이다.

profile
백엔드 개발자 꿈나무

0개의 댓글