kkkkkkkkkkkk
kkkkk
kkkkkkkkkkkk
전체 방문자
오늘
어제
  • 분류 전체보기
    • CS & OS
    • Algorithms
    • Laguage
    • Book
      • 객체지향의 사실과 오해
      • Effective Java
      • Spring boot 와 AWS로 혼자 구현하는 ..
      • 도메인 주도 계발 시작하기
    • DB
    • Spring
    • Spring Boot
    • JPA
    • Git
    • Clean Code
    • HTTP

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 설계 원칙
  • 응집도
  • 결합도
  • 객체지향 프로그래밍
  • 책임
  • 역할

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
kkkkkkkkkkkk

kkkkk

[Spring Boot] Spring Dependency Injection 동작 원리와 Ioc Container를 알아보자
Spring Boot

[Spring Boot] Spring Dependency Injection 동작 원리와 Ioc Container를 알아보자

2022. 3. 2. 19:02

학습 목차

@Component 애노테이션을 사용하면 왜 싱글톤 인스턴스가 생성이 되지?

reflection 기능을 이해해보자

애노테이션을 사용해보자

Dependency Injection 을 만들어보자

Spring IOC Container 가 무엇인지?


@Component 애노테이션을 사용하면 왜 싱글톤 인스턴스가 생성이 되지?

BookRepository 의 의존성을 가진 BookService 가 있습니다

public class BookRepository {
}
public class BookService {
    public BookRepository bookRepository;
}

테스트 코드를 사용하여 BookService의 인스턴스를 생성하지 않고 null 인지 테스트를 해보겠습니다.

BookService bookService;
BookRepository bookRepository;

@Test
@DisplayName("Spring Dependency Injection Test")
void dependencyInjectionTest() {
    // given & then
    assertThat(bookService).isNull();
}

테스트 결과는 null 이 맞습니다.

spring 의 @Component 의 기능을 가진 애노테이션을 사용하여 테스트를 다시 해봅시다.

@Repository
public class BookRepository {
}
@Service
public class BookService {
    @Autowired
    public BookRepository bookRepository;
}
BookService bookService;
BookRepository bookRepository;

@Test
@DisplayName("Spring Dependency Injection Test")
void BookServiceTest() {
    // then
    assertThat(bookService).isNotNull();
    assertThat(bookService.bookRepository).isNotNull();
}

테스트 결과는 null 이 아니여서 성공입니다.

그런데 어째서 인스턴스화를 하지 않았는데 @Component 기능을 가진 애노테이션을 사용하면 인스턴스가 생성이 될까요???


reflection 기능을 이해해보자

😃  reflection

클래스의 정보를 가지고 접근 지시자 상관없이 클래스 메서드를 통해서 메서드나 필드, 생성자에 정보를 읽어 올 수 있고 값을 변경 할 수도 있고 인스턴스를 생성할 수 있습니다.

Serializable 인터페이스를 구현한 Name 클래스가 있고 이를 상속한 Book 클래스가 있다고 가정해봅시다.

public interface Serializable {
}
public class Name implements Serializable {
    private Long id;
    private String name;
}
public class Book extends Name {

    private int price;
    private String serialNumber;
    private int quantity;
    public String company;

    public Book() {
    }

    private Book(int price) {
        this.price = price;
    }

    protected Book(int price, String serialNumber) {
        this.price = price;
        this.serialNumber = serialNumber;
    }

    Book(int price, String serialNumber, int quantity, String company) {
        this.price = price;
        this.serialNumber = serialNumber;
        this.quantity = quantity;
        this.company = company;
    }
}

😍  클래스의 인스턴스를 가져올 수 있습니다.

😭  첫번째

해당 인스턴스를 알면 getClass() 메서드로 인스턴스를 가져올 수 있습니다.

@Test
@DisplayName("Book 인스턴스를 가져오는 테스트")
void getInstance() {
    // given
    String packageName = "com.example.book_system.dependency.Book";
    // 해당 인스턴스를 알고 있을 때!
    Book book = new Book();

    // when
    Class<? extends Book>aClass = book.getClass();

    // then
    assertThat(aClass.getName()).isEqualTo(packageName);
}

🤩  두번째

타입을 알면 .class 로 클래스 타입의 인스턴스를 가져옵니다.

@Test
@DisplayName("Book 인스턴스를 가져오는 테스트")
void getInstance1() {
    // given & when
    String packageName = "com.example.book_system.dependency.Book";
    // 해당 인스턴스를 알고 있을 때!
    Class<Book>bookClass = Book.class;

    // then
    assertThat(bookClass.getName()).isEqualTo(packageName);
}

🥳  세번째

풀패키지명을 알면 “풀패키지명” 을 인자로 주고 Class.forName() 메서드로 인스턴스를 가져옵니다.

@Test
@DisplayName("Book 인스턴스를 가져오는 테스트")
void getInstance2()throws ClassNotFoundException{
    // given & when
    String packageName = "com.example.book_system.dependency.Book";
    // 풀패키지명을 알고 있을 때!
    Class<?>aClass = Class.forName(packageName);

    // then
    assertThat(aClass.getName()).isEqualTo(packageName);
}

🥰  클래스에 접근 후 필드의 정보를 가져올 수 있습니다.

getFields() 메서드라는 메서드를 사용해서 해당 클래스의 필드를 가져올 수 있습니다.

하지만 이 방법은 접근지시자 public 인 것만 가져옵니다.

@Test
@DisplayName("클래스에 접근 후 필드의 정보를 가져오는 테스트")
void getFields() {
    // given
    Class<Book>bookClass = Book.class;
    // when
    Field[]fields = bookClass.getFields();
    // then
    Arrays.stream(fields).forEach(field -> {
            assertThat(field).isNotNull();
            assertThat(field.getName()).isEqualTo("company");
    });
}

해당 필드가 접근지시자 private 인 경우 getDeclearedFields() 메서드를 사용해봅시다.

getDeclearedFields() 메서드는 private, public, protected, default 접근지시자를 전부 가져옵니다.

@Test
@DisplayName("클래스에 접근 후 필드의 정보를 가져오는 테스트")
void getFields1() {
    // given
    Class<Book>bookClass = Book.class;
    // when
    Field[]fields = bookClass.getDeclaredFields();
    // then
    Arrays.stream(fields).forEach(field -> {
        assertThat(field).isNotNull();
    });
    assertThat(fields.length).isEqualTo(4);
}

접근이 불가능 한 필드를 접근이 가능하도록 하려면 setAccessible() 메서드의 인자값을 true 로 설정해야합니다.

해당 테스트는 Book 클래스의 private 생성자를 접근하여 테스트 하였습니다.

@Test
@DisplayName("Book 인스턴스를 가져오는 테스트")
void getInstance3()throws NoSuchMethodException{
    // given & when
    String packageName = "com.example.book_system.dependency.Book";
    Class<Book> bookClass = Book.class;
    bookClass.getDeclaredConstructor(int.class).setAccessible(true);
    // then
    assertThat(bookClass.getName()).isEqualTo(packageName);
}

많은 Class 의 메서드들이 있으니 천천히 살펴봅시다...

😘  클래스에 접근 후 생성자와 상위 클래스나 인터페이스의 정보를 가져올 수 있습니다.

getSuperClass() 메서드를 사용하여 상위 클래스의 정보를 가져옵니다.

Book클래스는 Name클래스로 상속하여 생성했습니다.

@Test
@DisplayName("상위 클래스정보 가져오기")
void superClass() {
    // given
    Class<Book>bookClass = Book.class;

    // when
    Class<? super Book>superclass = bookClass.getSuperclass();
    // then
    assertThat(superclass.getName()).isEqualTo("com.example.book_system.dependency.Name");
}

getInterfaces() 메서드를 사용하여 상위 인터페이스의 정보를 가져옵니다.

 


애노테이션을 사용해보자

간단한 애노테이션 사용법입니다. java 애노테이션 정리된 글을 보면 이해가 될겁니다.

🥴  애노테이션

클래스 로딩 과정에서 애노테이션만 빼고 클래스 파일을 읽어 옵니다. 그래서 애노테이션을 유지시키려면 @Retention(RestionPolicy.RUNTIM)을 달아줘야합니다.

기본값은 RestionPolicy.CLASS 입니다.

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

애노테이션을 사용할 수 있는 위치를 설정 할 수 있습니다.

@Target 을 사용하여 타입, 필드, 메서드 등 에 설정할 수 있습니다.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

기본적인 값들을 프리미티브 타입으로 가질 수 있습니다.

String name(); 이런식으로 정의 해야합니다.

기본 값을 설정 할수 있습니다.

String name() defalut “스폰지밥”;

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String name() default "스폰지밥";
}

상속관계가 있는 클래스 하나에 애노테이션을 붙이고 해당 애노테이션 정보를 가지고 올 수 있습니다. @Ingerited 애노테이션을 사용하면 됩니다.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyAnnotation{
    String name()default "스폰지밥";
}

애노테이션을 참조하여 애노테이션의 정보들의 값을 가지고 올 수 있습니다.

@Test
@DisplayName("상위 타입에 붙어있는 애노테이션 정보 가져오기 테스트")
void BookTest() {
    // given
    Class<Book>bookClass = Book.class;
    // when
    Annotation[]annotations = bookClass.getAnnotations();
    // then
    for(Annotation annotation : annotations) {
        System.out.println(annotation);
    }
}

@com.example.book_system.dependency.MyAnnotation(name="스폰지밥") 결과가 나올 것입니다.


Dependency Injection 을 만들어보자

spring의 Ioc Container 의 동작 원리 처럼 dependency Injection을 하는 기능을 만들어 보겠습니다.

우리는 BookRepository 클래스와 BookService 클래스가 있다고 가정해봅시다.

앞에서 보다시피 의존성 주입을 해도 위의 클래스는 null 것입니다.

BookService bookService;
BookRepository bookRepository;

@Test
@DisplayName("Spring Dependency Injection Test")
void dependencyInjectionTest() {
    // given & then
    assertThat(bookService).isNull();
}

하지만 Spring의 @ComponentScan 이라는 기능을 모방하여 의존성 주입을 가능하게 만들어봅시다.

@AutoWired 애노테이션은 의존성을 주입하게 만들어주는 기능인데 이 애노테이션 대신에 @Injection 애노테이션을 만들겠습니다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Injection{
}

BookService 와 BookRepository 클래스를 생성하고 의존성 주입을 해줍니다. Ioc Container의 기능을 만들지 않았으니 아직까진 null 이 나오겠죠?

public class BookRepository {
}
public class BookService {
    @Injection
    BookRepository bookRepository;
}

지금부터 container의 기능을 구현해보겠습니다. containerService 클래스를 생성합니다.

public class ContainerService{

    public static <T> T getObject(Class<T> classType) {
        T instance = createInstance(classType);
        Arrays.stream(classType.getDeclaredFields()).forEach(field ->{
            if(field.getAnnotation(Injection.class)!= null) {
                Object fieldInstance = createInstance(field.getType());
                field.setAccessible(true);
                try{
                    field.set(instance, fieldInstance);
                }catch(IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        return instance;
}

    private static <T> T createInstance(Class<T> classType) {
        try{
            return classType.getConstructor().newInstance();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
}

인자로 classtype 을 받아와 reflection 을 사용하여 받아 온 인자를 createInstance() 메서드를 사용해 인스턴스 생성을 하고 생성된 인스턴스로부터 필드를 얻어와 해당 필드에 @Injection 이 붙어 있는지 확인 후 붙어 있으면 해당 필드의 인스턴스를 생성하는 로직입니다.

테스트 코드를 작성하여 검증을 해봅시다.

@Test
@DisplayName("dependency injection test")
void ContainerServiceTest(){
    // given & when
    BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
    BookService bookService = ContainerService.getObject(BookService.class);

    // then
    assertThat(bookRepository).isNotNull();
    assertThat(bookService).isNotNull();
}

 


Spring IOC Container 가 무엇인지?

앞에 dependency Injection의 기능을 만들어보고 의존성 주입의 동작 원리를 배웠는데요. 이제는 DI를 해주고 Bean들을 관리해주는 Spring Ioc Contatiner에 대해 배워보고 @ComponentScan 이라는 기능을 배워보겠습니다.

🥶  Spring IOC Container 가 뭔데요??

Spring IOC Container 는 우리가 직접 객체들을 관리해주는 것이 아니라 spring 이 객체를 container 에 담아서 관리하는 방식이라 설명할 수 있습니다.

inversion of control 이란 제어권이 역전됐다 라는 말인데요, 객체의 관리 제어권이 spring 에게 넘어 갔다 라고 이해하면 편하실겁니다.

🥲 그러면 Bean 이 뭔데요???

bean 이란 Spring IOC Container 가 관리하는 객체들을 bean 이라고 불립니다.

bean 들어 제어하는 역할을 하는 것을 beanFactory 와 applicationContext 가 있는데 applicationContext 는 beanFactory 를 구현하여 확장시킨 인터페이스입니다.

이 2개를 통틀어서 Spring IOC Container 라고 부릅니다.

🥲  bean 들은 누가? 어떻게? 등록을 해요??

ComponetScan 기능으로 @Componet, @Service, @Repository, @RestController, @Controller, @Confinguration 이라는 애노테이션들이 붙어 있는 클래스나 인터페이스 등 해당 타입의 이름을 가져와 Spring IOC Container 등록을 해줍니다.

@Service
public class BookService{
}
@Repository
public class BookRepository{
}
@Controller
public class BookController{
}
@Configuration
public class BookConfigu{
// @bean 생략
}

또한 @Configuration 을 사용하여 bean 등록 을 해주는 방법이 있습니다.

@Configuration
public class BookConfigu{

    @Bean
    public BookRepository bookRepository() {
        return new BookRepository();
    }

    @Bean
    public BookService bookService() {
        return new BookService();
    }
}

😝  테스트를 진행하며 빈이 등록이 됬는지 검증을 해봅시다.

@Configuration으로 bean을 등록한 경우 new AnnotationConfigApplicationContext() 를 생성하고 @Configuration 애노테이션이 붙어있는 클래스 타입을 인자 값으로 준 다음 applicationContext 로 getBean() 를 사용해 봅시다.

@Test
@DisplayName("Configuration 으로 bean 등록 테스트")
void BookServiceTest() {
    // given
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BookConfigu.class);
        
    // when
    BookService bookService = applicationContext.getBean(BookService.class);
    BookRepository bookRepository = applicationContext.getBean(BookRepository.class);
        
    // then
    assertThat(bookRepository).isNotNull();
    assertThat(bookService).isNotNull();
}

여기서 한가지 테스트를 더 해볼 수 있습니다. Spring IOC Container 가 관리하는 bean들은 모두 singleton Scope 을 가지는데요. 테스트를 진행해 보겠습니다.

@Test
@DisplayName("SingleTon 검증")
void singleTonTest() {
    // given
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BookConfigu.class);

    // when
    BookService bookService = applicationContext.getBean(BookService.class);
    BookService bookService1 = applicationContext.getBean(BookService.class);

    BookRepository bookRepository = applicationContext.getBean(BookRepository.class);
    BookRepository bookRepository1 = applicationContext.getBean(BookRepository.class);

    // then
    assertThat(bookService.equals(bookService1)).isTrue();
    assertThat(bookRepository.equals(bookRepository1)).isTrue();
}

🤬  싱글톤을 보장하는 것을 알수가 있습니다.

dependency Injection의 기능 을 만들때 getObject() 라는 메서드가 있었습니다. 이 메서드의 기능이 getBean() 의 역할을 한다는 것을 발견할 수 있습니다.

다음으로는 @Autowired 를 사용하여 의존성 주입을 해보겠습니다.

@Autowired 애노테이션은 Spring IOC Container 에 등록되어 있는 bean중에 필요로 하는 의존성을 주입 해주는 기능입니다.

@Autowired
BookService bookService;

@Autowired
BookRepository bookRepository;
@Test
@DisplayName("@Autowired로 Dependency Injection Test")
void getBeanTest() {
    // given & when & then
    assertThat(bookRepository).isNotNull();
    assertThat(bookService).isNotNull();
}

이렇듯 의존성 주입은 잘 됩니다.

마지막으로 그림을 보며 이해해봅시다.

 

'Spring Boot' 카테고리의 다른 글

@Builder 사용시 초기화 필드는 어떻게 될까?  (0) 2022.09.05
[Spring boot] 순환 참조 이슈  (0) 2022.08.12
[Spring Boot] Spring 에서 비동기 처리 방식은 어떻게 하고 왜 사용해야 할까?  (0) 2022.07.31
[Spring Boot] Interceptor 는 어떻게 사용하고 왜 사용해야 할까?  (0) 2022.07.30
[Spring Boot] Filter를 왜 사용해야하고 어떻게 사용하는 걸까?  (0) 2022.07.30
[Spring Boot] Validation을 왜 해야하고 어떻게 할까?  (0) 2022.07.29
[Spring Boot] 예외처리를 왜 해야하고 어떻게 처리할까?  (0) 2022.07.29
ModelMapper  (0) 2022.07.20
    'Spring Boot' 카테고리의 다른 글
    • [Spring Boot] Filter를 왜 사용해야하고 어떻게 사용하는 걸까?
    • [Spring Boot] Validation을 왜 해야하고 어떻게 할까?
    • [Spring Boot] 예외처리를 왜 해야하고 어떻게 처리할까?
    • ModelMapper
    kkkkkkkkkkkk
    kkkkkkkkkkkk

    티스토리툴바