DI, Ioc, Singleton, Bean 등 Spring을 공부하는 사람이라면 한 번쯤은 들어봤을 내용입니다. 저 또한 Spring을 처음 공부할 때, 블로그나 강의에서 많이 듣곤 하였습니다. Spring boot를 사용할 때 이러한 개념들이 헷갈려서 정리하려고 합니다.
이 글에서는 엄청 자세하게 이야기를 하지 않으려고 합니다. 제가 공부하면서 헷갈렸던 부분이나, 연관성이 어떻게 있는지에 대해서 풀어보려고 합니다. 자세한 내용이 궁금하시면 '김영한님의 스프링 핵심원리 기본편'을 들으시는 것을 추천드리겠습니다.
Spring Container
Spring Container는 쉽게 말해 객체를 관리해주는 컨테이너라고 생각하면 됩니다. 아무 객체나 관리해주는 것이 아니라, 당연히 개발자가 해당 객체를 관리해달라고 컨테이너에게 요청합니다. 객체를 Spring container에 등록하여 spring container가 관리하는 자바 객체, 즉 spring에 의하여 생성되고 관리되는 자바 객체를 Spring bean이라고 합니다.
그럼 여기서 의문점이 생깁니다. 객체가 필요할 때 생성해서 사용하는 게 낫지.. 왜 객체의 생성부터 관리까지 Spring container에게 맡기는 것일까요?
제 생각에 그 이유는 Spring이 생겨난 이유와도 연관이 있다고 생각합니다. Spring이 나오기 전에도 java를 사용하여 개발을 많이 해왔지만 상당히 코드도 복잡하고 어려웠다고 합니다. Spring은 Java의 가장 큰 특징인 객체 지향 언어의 특징을 살려내는 프레임워크 입니다.
흔히 말하는 좋은 객체 지향의 원칙이라고 하면 SOLID가 먼저 생각납니다. SOLID 원칙은 전부 중요합니다. 5가지 원칙 중에서 Spring container와 가장 밀접한 관련이 있다고 생각하는 것은 OCP와 DIP입니다. 간략하게 알아보겠습니다.
OCP(Open Closed Principle)
OCP는 한국어로 번역하면 '개방 폐쇄의 원칙' 입니다. 무엇에 개방되어야 하고 무엇에 폐쇄되어야 할까요? 기능을 추가하는 면에서는 개방되어야 하고 수정하는 부분에서는 폐쇄되어 있어야 합니다. 쉽게 말해 기존의 코드를 변경하지 않고 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻입니다.
DIP(Dependency Inversion Principle)
DIP는 '의존관계 역전 원칙'입니다. 상위 모듈은 하위모듈의 구현에 의존하면 안 되고 추상화에 의존해야 한다는 이야기입니다. Spring에서 예를 들면 controller 객체가 service의 구현체에 의존하면 안 되고 추상화에 의존해야 한다는 이야기입니다.
예시 코드를 통해서 좀 더 알아보겠습니다.
// TestController.java
public class TestController {
private TestService testService = new MysqlTestService(); // 변경 전
private TestService testService = new PostgresqlTestService(); // 변경 후
}
위의 예시 코드는 MysqlTestService를 사용했다가 데이터베이스가 바뀌어 PostgresqlTestService로 바꾸려고 하는 코드입니다. 이 코드는 OCP, DIP 원칙을 전부 어겼습니다. 구현하려고 하는 객체를 변경하려고 할 때, controller의 코드를 변경해야만 합니다. 또한 추상화에 의존하는 것이 아닌, 구현체에 의존하고 있습니다.
스프링은 OCP, DIP를 가능하게 해 줍니다. Spring Container가 객체를 관리하고 직접 객체를 주입해주기 때문에 위의 코드처럼 직접 구현체를 주입해줄 필요가 없습니다. 이렇게 의존성(객체)을 주입해주는 것을 DI(Dependency Injection)라고 합니다. 또한 제어 흐름을 직접 제어하는 것이 아닌 외부(Spring container)에서 관리하기에 IOC(Inversion Of Control)라고도 합니다. 따라서 Spring container를 DI container, IOC container라고도 부르기도 합니다.
객체를 등록해야 Spring container가 알아서 주입도 해주고 관리를 해주기에 Spring container에 spring bean으로 등록하는 방법에 대해서 알아보겠습니다.
직접 등록
첫 번째, 방법으로는 직접 등록하는 방법이 있습니다. 직접 등록이라고해서 복잡하게 넣어주고 그러는 것이 아니고 @Configuration과 @Bean을 이용해서 직접 등록하는 방식입니다. 이 방법이 직접 등록하는 방법이라고 하는 이유는 나중에 나오는 @Component를 사용하는 방법보다 복잡하고 말 그대로 @Bean을 이용해서 등록을 해주기 때문입니다.
Bean annotation의 경우 다음과 같이 되어있습니다. @Target을 보면 @Bean은 method나 annotation에만 붙여줄 수 있습니다.
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
```
```
}
이제 코드를 통해 보겠습니다.
// TestConfiguration.java
@Configuration
public class TestConfiguration {
@Bean
public Person person() {
return new Person();
}
}
// Person.java
public class Person {
private String username;
private int age;
private void canSpeak() {
System.out.println("can Speak");
}
private void canEat() {
System.out.println("can Eat");
}
public Person() {}
}
위의 코드처럼 @Configuration 을 붙여주고 method에 @Bean을 붙여주면 해당 객체가 bean으로 등록이 됩니다. 이름의 경우 메소드 이름을 그대로 갖고 가는데 @Bean의 name 속성을 이용하여 이름을 변경해줄 수 있습니다.
Bean으로 등록이 되었는지 두 번째 방법까지 얘기하고 한 번에 확인해보겠습니다.
@Component 사용
@Component 어노테이션을 사용해서 등록하는 방법입니다. 프로젝트 규모가 커질수록 직접 bean을 등록하기 어렵고 부담스러워집니다. 대신 Component 어노테이션을 사용하면 쉽게 등록할 수 있습니다. 그렇다고 항상 @Component만을 사용하는 것은 아니고 외부 라이브러리 등 @Component를 사용 못하는 경우에는 직접 등록해서 사용합니다. 어떻게 Component 어노테이션만 있다고 Spring이 인식을 할 수 있을까요?
그 이유는 @ComponentScan 어노테이션을 이용하여 Spring container가 Component 어노테이션이 붙은 객체들을 전부 Spring bean으로 등록 시키기 때문입니다.
Spring boot 코드를 한 번 보겠습니다.
// TestApplication.java
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
위 코드에 있는 @SpringBootApplication 어노테이션을 들어가서 보면 @ComponentScan 어노테이션이 있는 것을 볼 수 있습니다.
또한 @Controller, @Service, @Repository, @Configuration 어노테이션 안에 @Component 어노테이션이 있기에, 따로 Component 어노테이션을 안 줘도 됩니다.
예시 코드는 다음과 같습니다.
// TestController.java
@Controller
@RequestMapping("/test")
@RequiredArgsConstructor
public class testController {
@GetMapping
public void getTest() {
System.out.println("test for get method");
}
}
그럼 해당 bean이 잘 등록 되었는지 확인해 보겠습니다.
Bean 객체 확인
Bean이 잘 등록되었는지 확인을 해 보겠습니다. Spring boot를 실행할 때 Spring container가 @Componenet가 붙어 있는 class와 @Bean이 붙어 있는 method등을 Bean으로 등록을 합니다. 이렇게 바로 실행시킬 때 등록하는 방법을 eager loading이라고 하는데, 바로 등록시키지 않고 사용할 때 등록시키게끔 lazy loading으로 변경할 수 있습니다. 자세한 방법은 구글링하면 설명을 잘 해주는 글들이 많습니다!!
// TestApplication.java
@SpringBootApplication
public class TestApplication implements CommandLineRunner {
@Autowired
private ApplicationContext appContext; // import org.springframework.context.ApplicationContext;
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
String[] beans = appContext.getBeanDefinitionNames(); // 등록된 bean 객체의 이름을 받아온다.
for(String bean : beans) {
Object beanDetail = appContext.getBean(bean); // bean 이름으로 bean 객체를 받아온다.
System.out.println(bean);
System.out.println(beanDetail);
}
}
}
Application 코드를 위와 같이 바꾸면 등록되어 있는 bean들을 확인할 수 있습니다.
코드를 실행시키면 위와 같이 Bean들이 등록되는 것을 볼 수 있습니다. Bean을 어떻게 등록하고 관리하는지에 대해 알아 보았습니다.
이제, 앞에서 언급한 DI(의존성 주입)에 대해서 알아보겠습니다.
의존성 주입
Spring container에 Bean 객체를 등록시키면, 알아서 관리를 해주고 객체들을 주입해준다고 하였습니다. 어떻게 주입을 시켜주는지 알아보겠습니다.
의존성을 주입 시키는 방법에는 생성자 주입, 필드 주입, Setter 주입 등이 있습니다. 주입을 할 때는 @Autowired annotation을 사용하여 Spring container에게 Bean을 주입해 달라고 요청합니다. 위의 3가지 주입 방식 중에서 일반적으로 생성자 주입을 많이 사용하고 권장하고 있습니다. 각 주입의 장단점을 찾아보면 자세하게 설명한 글들이 많습니다.
코드로 살펴보겠습니다. TestController.java 파일을 밑의 코드처럼 변경해주겠습니다.
// TestController.java
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class testController {
private final Person person1;
private final Person person2;
@GetMapping
public void getTest() {
System.out.println("person1 = " + person1);
System.out.println("person2 = " + person2);
}
@PostMapping
public void JacksonTest1(@RequestBody TestOneRequestDto testOneRequestDto) {
System.out.println(testOneRequestDto.getTest());
}
}
위와 같이 코드를 변경하고 Postman 또는 api tester 등을 이용해서 요청을 보내보겠습니다.
요청을 하고 터미널을 확인해보면 이와 같이 등록된 Bean 객체를 주입해주는 것을 알 수 있습니다. 위의 코드에서 왜 @Autowired를 사용하지 않았는데도 Spring container가 알아서 주입을 해주는지 궁금증이 생길 수도 있습니다. 생성자 주입의 경우 생성자가 1개만 존재할 때 @Autowired를 해주지 않아도 Spring container가 알아서 @Autowired를 해주게 됩니다. 위의 코드에서는 lombok annotation @RequiredArgsConstructor를 이용하여 생성자를 한 개만 만들어주었기에 알아서 주입이 되었습니다.
api 요청을 계속 보내보겠습니다.
결과를 확인해보면 새로운 요청을 보내도 매번 똑같은 객체를 주입해 준다는 것을 알 수 있습니다. 어떻게 이런게 가능할까요?
그 이유는 기본적으로 Spring container는 bean 객체를 Singleton으로 관리하기 때문입니다. Bean 객체의 Scope 즉 빈이 사용되는 범위는 singleton, prototype, request 등이 있지만, Spring container는 디폴트로 singleton으로 관리를 합니다.
Singleton으로 관리를 하면 매번 요청을 할 때마다 객체를 새로 생성하지 않아도 되기에 메모리나 성능면에서 효율적입니다. 하지만 단점도 있기에 주의하여 사용해야합니다.
이번 글에서는 Spring container, DI, IOC, Bean 등 Spring의 기본 개념에 대해서 적어보았습니다. 주입 종류, Bean 생명주기, 스코프 등 자세한 내용은 다루지 않았습니다. 궁금한 내용은 구글링을 하면 자세히 설명해주는 블로그들이 많아서 직접 찾아보시기 바랍니다.
※ 궁금한 사항이나 잘못된 점 댓글 부탁드립니다.
참고자료
https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot
'Spring' 카테고리의 다른 글
[Jackson] 왜 field가 1개인 DTO는 기본 생성자가 필요할까?(1) (0) | 2022.12.15 |
---|---|
[Lombok] Lombok에서 생성자 Annotation에 대해 알아보자 (0) | 2022.10.29 |