학습 목차
@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 |