본문 바로가기
공부/java & Spring

[Spring boot] IoC(Inversion of Control) 개인공부

by 고기 2023. 7. 2.

1. 용어 설명

2. 테스트 코드 작성

3. 왜 스프링에서는 의존성을 주입하는 방식을 사용해야 할까?


 

사실 나는 지금도 구체적으로 이해한 건 아니지만... 추상적으로나마 이해한 내용을 기록으로 적어둔다.

테스트 코드를 보면서 객체 생성(new)과 참조(주입)에 대한 차이를 알아보자.

 

Inversion of Control(IoC)와 Dependency Injection(DI).

이제 스프링 공부를 시작한... 또는 공부하고 있거나... 이미 공부했던 사람이라면 필연적으로 들어봤을 개념이다.

두 개념을 간단하게 설명하자면 이 정도로 정리할 수 있을 것 같다.

Dependency Injection(DI)는 IoC의 구현 방법 중 하나입니다. DI는 객체 간의 의존성을 외부에서 주입하는 방식으로 이루어집니다. 즉, 객체가 필요로 하는 의존성을 직접 생성하지 않고 외부에서 제공받는 것입니다. 이를 통해 객체 간의 결합도를 낮추고 유연한 구조를 유지할 수 있습니다.

DI는 IoC를 실제로 구현하는 방식 중 하나로서, IoC를 적용하면 객체의 생성, 의존성 관리, 의존성 주입 등을 프레임워크나 컨테이너가 담당하게 되어 개발자는 이러한 관리 작업에 집중하지 않고 비즈니스 로직에 집중할 수 있습니다.
Inversion of Control(IoC, 제어의 역전)는 개발자가 직접 객체의 생성과 관리를 담당하는 기존의 개발 방식과는 달리, 프레임워크 또는 컨테이너에 제어의 권한이 넘어가는 것을 의미한다. 기존의 개발 방식에서는 개발자가 직접 객체의 생성과 관리를 담당했지만, IoC 패턴을 적용하면 객체의 생성과 관리를 프레임워크 또는 컨테이너에게 위임합니다. 이를 통해 개발자는 객체의 의존성을 직접 해결하지 않고, 외부에서 필요한 의존성을 주입받을 수 있게 됩니다.

 

이 뭔 개소리지?

내가 알고 싶은건 얘네가 무엇인가? 가 아니라 왜 이렇게 사용하고 있는가? 인데

대부분은 이렇게 DI와 IoC가 무엇인가에 대한 것만 설명하고 있단 말이지.

개념을 아는 것은 좋지만... 정작 왜 이렇게 사용하는지 모르고 사용하면 의미가 없지 않나.

(물론 그 분들의 글은 나에게 많은 도움이 되었습니다...)

 

 

1. 용어 설명

코드를 작성하기 전에 설명할 때 사용할 용어들을 정리해보자.

빈을 주입하고 객체를 주입하고 인스턴스를 생성하고 객체를 생성하고 의존성이 어쩌고저쩌고

이런 용어들을 아무데나 집어넣어도 의미가 그럴듯하게 전달되니까 막 집어넣는단 말이야.

 

용어1. 스프링 애플리케이션이 실행되면 == 스프링 프로젝트가 실행되면

솔직히 IoC에 대해서 찾아다닐 정도면 스프링 프로젝트를 이미 어느정도 접해본 사람들이 아닐까.

그러니까 대충 다 같은 말이니까 찰떡같이 알아먹자.

 

용어2. 스프링 컨테이너 == 컨테이너

기본적으로 스프링 프로젝트를 생성하면 스프링 의존성들과 본인이 작성한 클래스들이 있겠지?

이것들이 스프링 애플리케이션이 실행되면 스프링 컨테이너에 bean으로 저장된다.

나머지는 아래 코드 보면서 다시 설명함! 지금은 스프링 컨테이너라는 놈이 있다는 것만 알면 된다.

 

용어3. 객체 생성 == new

java에서 new 키워드를 사용해서 객체를 생성하는 것을 말한다.

 

용어4. 객체 참조 == 생성자 주입

일반적으로 객체를 주입하는 방법은 4가지가 있다.

생성자 주입, 필드 주입, 세터 주입, 메서드 주입 4개가 있는데 이 방법들을 설명하려는건 아니고.

예제 코드에서 사용할 주입 방법은 생성자 주입이다. 공식 문서에서도 생성자 주입 방법을 권장하기도 하니까.

 

용어5. 빈(bean) == 인스턴스(instance) == 객체

대부분의 교재나 강의에서 빈에 대해 설명하기를 다음과 같이 설명한다.

"빈(bean)"이라는 용어는 스프링 프레임워크에서 사용되는 용어로, 객체의 인스턴스를 의미합니다. 스프링 컨테이너에 의해 생성되고 관리되는 객체를 "빈"이라고 부릅니다.

빈(bean)은 스프링 컨테이너에 의해 생성되고 관리되며, 필요한 곳에서 주입(Dependency Injection)받아 사용됩니다. 인스턴스와 빈은 개념적으로는 동일한 것을 의미하지만, 스프링에서는 "빈(bean)"이라는 용어를 사용하여 스프링 컨테이너에 등록된 객체의 인스턴스를 가리킵니다.

그러니 아래에서 설명하면서 튀어나올 빈이든 객체든 인스턴스든 같은 의미라고 생각하자.

 

2. 테스트 코드 작성

이제 테스트 코드를 작성해보자.

구조는 루트 디렉터리 아래 config, controller, service 3개의 패키지를 생성했다.

근데 사실 테스트라서 한 패키지 안에 몰아넣어도 상관없긴 함

 

2개의 controller를 작성하자.

하나는 객체를 생성하는 컨트롤러고 다른 하나는 객체를 참조하는 컨트롤러다.

그리고 RestController로 작성한 이유는 클라이언트 사이드랑 연결된 페이지가 없기 때문이다. (굳이 만들 필요도 없고.)

그냥 테스트용으로 log를 찍기위한 api를 만들었다고 생각하면 됨!

 

IoCTestController1은 객체를 참조하는 컨트롤러다.

이건 우리가 일반적으로 스프링에서 작성하는 방식으로 iocTestService 객체를 생성자 주입받고 있다.

이렇게 새로운 컨트롤러를 작성하면 스프링 컨테이너에 컨트롤러에 대한 bean이 등록된다.

package excel.mh.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import excel.mh.service.IoCTestService1;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
public class IoCTestController1 {

    private IoCTestService1 iocTestService;

    public IoCTestController1(IoCTestService1 iocTestService) {
        this.iocTestService = iocTestService;
    }

    @GetMapping("/ioc/test1")
    public void test1() {
        log.info("객체 참조 test!");
        iocTestService.iocTestService();
    }

}

 

그림은 첫 글자를 소문자로 변경한 ioCTestController1이라는 이름의 bean이 등록되는 과정이다.

(그림은 첫 글자를 소문자로 변경한 ... 이 부분은 스프링 내부에서 이루어지는 과정이니 신경쓰지 말자)

 

IoCTestController2는 객체를 생성하는 컨트롤러다.

여기서는 new 생성자를 통해 iocTestService 객체를 생성하고 있다.

그리고 마찬가지로 스프링 컨테이너에 새로운 컨트롤러에 대한 bean이 등록된다.

package excel.mh.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import excel.mh.service.IoCTestService2;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
public class IoCTestController2 {

    private IoCTestService2 iocTestService;

    public IoCTestController2() {
        this.iocTestService = new IoCTestService2();
    }

    @GetMapping("/ioc/test2")
    public void test2() {
        log.info("객체 생성 test!");
        iocTestService.iocTestService();
        iocTestService.destroy();
    }

}

 

ioCTestController2이라는 이름의 bean이 등록되는 과정이다.

 

여기까지... 작성된 게 객체 생성과 객체 참조 코드인 건 알겠는데... 이제 어쩌라고??

일단 코드를 마저 작성해보자.

 

먼저 서비스 인터페이스를 작성한 다음 작성한 인터페이스에 대해 생성과 참조 2가지 방법으로 구현할 것이다.

package excel.mh.service;

public interface IoCTestServiceI {

    public abstract void iocTestService();

}

 

IoCTestService1은 객체를 참조하는 컨트롤러에서 사용할 서비스다.

iocTestService 메서드를 호출하면 객체 참조 test라는 로그를 출력해준다.

package excel.mh.service;

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class IoCTestService1 implements IoCTestServiceI {

    public void iocTestService() {
        log.info("IoCTestService 객체 참조 test");
    }
}

 

IoCTestService2은 객체를 생성하는 컨트롤러에서 사용할 서비스다.

iocTestService 메서드를 호출하면 객체 생성 test라는 로그를 출력해준다.

객체 참조 서비스랑 다르게 destroy라는 메서드가 추가되었는데 이건 아래서 다시 설명하겠다.

package excel.mh.service;

import javax.annotation.PreDestroy;

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class IoCTestService2 implements IoCTestServiceI {

    public void iocTestService() {
        log.info("IoCTestService 객체 생성 test");
    }

    @PreDestroy
    public void destroy() {
        log.info("IoCTestService 소멸");
    }
}

 

이렇게 서비스를 작성하면 총 4개의 bean이 컨테이너에 저장되는 것을 예상해볼 수 있다.

 

bean이 생성된 것을 확인하기 위해 beanPrint.java를 작성하자.

ApplicationContext를 주입받아서 모든 빈의 이름을 출력하는 printBeanList() 메서드를 작성했다.

당연한 이야기지만 이 클래스 역시 스프링 컨테이너에서 빈으로 등록된다.

 

참고로 ApplicationContext는 스프링 컨테이너의 인터페이스로서, 빈들을 관리하고 제공하는 역할을 한다.

ApplicationContext의 getBeanDefinitionNames() 메서드를 사용해서 컨테이너에 저장된 빈을 조회하면 된다.

package excel.mh.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class beanPrint {
    private final ApplicationContext applicationContext;

    @Autowired
    public beanPrint(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void printBeanList() {
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            // log.info("bean:" + beanName); // 전체 bean 조회

            Object bean = applicationContext.getBean(beanName);
            if (bean.getClass().getPackage().getName().startsWith("excel.mh")) {
                log.info("bean:" + beanName); // 내가 생성한 클래스에 대한 bean 조회
            }
        }
    }
}

 

스프링 프로젝트를 생성하면 기본으로 생성되는 ~~Application.java에 내용을 추가하자

먼저 위에서 작성한 beanPrint를 주입 후 CommandLineRunner를 구현한다.

 

참고로 CommandLineRunner는 스프링이 실행될 때 특정한 작업을 수행하기 위한 인터페이스다.

해당 인터페이스를 구현하는 클래스는 run 메서드를 재정의해서 원하는 작업을 정의할 수 있다.

따라서 우리는 run() 메서드를 오버라이드 후 printBeanList() 메서드를 호출하도록 구현하면 된다.

package excel.mh;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import excel.mh.config.beanPrint;

@SpringBootApplication
public class MhApplication implements CommandLineRunner {
	@Autowired
	private beanPrint beanPrint;

	public static void main(String[] args) {
		SpringApplication.run(MhApplication.class, args);
	}

	@Override
	public void run(String... args) {
		beanPrint.printBeanList();
	}
}

 

이렇게 코드를 작성 후 애플리케이션을 실행하면 스프링 컨테이너에 등록된 모든 빈을 확인할 수 있다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.11)

2023-07-02 12:33:55.929  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : Starting MhApplication using Java 11.0.17 on mh with PID 20936 (C:\Users\nerin\Desktop\4. myproject\2. visual studio code\6. IoC test\bin\main started by nerin in C:\Users\nerin\Desktop\4. myproject\2. visual studio code\6. IoC test)
2023-07-02 12:33:55.930  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : No active profile set, falling back to 1 default profile: "default"
2023-07-02 12:33:56.130  WARN 20936 --- [  restartedMain] o.m.s.m.ClassPathMapperScanner           : No MyBatis mapper was found in '[excel.mh]' package. Please check your configuration.
2023-07-02 12:33:56.169  INFO 20936 --- [  restartedMain] o.s.b.w.e.t.TomcatWebServer              : Tomcat initialized with port(s): 443 (https)
2023-07-02 12:33:56.170  INFO 20936 --- [  restartedMain] o.a.c.c.StandardService                  : Starting service [Tomcat]
2023-07-02 12:33:56.170  INFO 20936 --- [  restartedMain] o.a.c.c.StandardEngine                   : Starting Servlet engine: [Apache Tomcat/9.0.74]
2023-07-02 12:33:56.191  INFO 20936 --- [  restartedMain] o.a.c.c.C.[.[.[/]                        : Initializing Spring embedded WebApplicationContext
2023-07-02 12:33:56.192  INFO 20936 --- [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 259 ms
2023-07-02 12:33:56.215  INFO 20936 --- [  restartedMain] o.s.s.w.a.c.ChannelProcessingFilter      : Validated configuration attributes
2023-07-02 12:33:56.216  INFO 20936 --- [  restartedMain] o.s.s.w.DefaultSecurityFilterChain       : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@74ae32a5, org.springframework.security.web.access.channel.ChannelProcessingFilter@7b91780a, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@40f5512, org.springframework.security.web.context.SecurityContextPersistenceFilter@73019b09, org.springframework.security.web.header.HeaderWriterFilter@5c806ea7, org.springframework.security.web.authentication.logout.LogoutFilter@3e906f8e, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@408411f7, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1f2eba16, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2baf4c35, org.springframework.security.web.session.SessionManagementFilter@4e920a83, org.springframework.security.web.access.ExceptionTranslationFilter@927423c, org.springframework.security.web.access.intercept.AuthorizationFilter@36b6653b]
2023-07-02 12:33:56.290  INFO 20936 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2023-07-02 12:33:56.321  INFO 20936 --- [  restartedMain] o.a.t.u.n.N.certificate                  : Connector [https-jsse-nio-0.0.0.0-443], TLS virtual host [_default_], certificate type [UNDEFINED] configured from [file:/C:/Users/nerin/Desktop/4.%20myproject/2.%20visual%20studio%20code/6.%20IoC%20test/sslWork/domain/keystore.p12] using alias [tomcat] and with trust store [null]
2023-07-02 12:33:56.325  INFO 20936 --- [  restartedMain] o.s.b.w.e.t.TomcatWebServer              : Tomcat started on port(s): 443 (https) with context path ''
2023-07-02 12:33:56.330  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : Started MhApplication in 0.445 seconds (JVM running for 1815.314)
2023-07-02 12:33:56.331  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.context.annotation.internalAutowiredAnnotationProcessor
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.context.annotation.internalCommonAnnotationProcessor
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.context.event.internalEventListenerProcessor
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.context.event.internalEventListenerFactory
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:mhApplication
2023-07-02 12:33:56.332  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:beanPrint
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:securityConfig
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestController1
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestController2
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestService1
2023-07-02 12:33:56.333  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestService2
2023-07-02 12:33:56.334  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration
2023-07-02 12:33:56.334  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:objectPostProcessor
2023-07-02 12:33:56.334  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
2023-07-02 12:33:56.334  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:authenticationManagerBuilder
...
...

 

내가 작성한 클래스에 대한 bean만 출력하도록 할 수도 있다. beanPrint.java의 주석을 참고하면 됨!

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.11)

2023-07-02 12:34:02.005  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : Starting MhApplication using Java 11.0.17 on mh with PID 20936 (C:\Users\nerin\Desktop\4. myproject\2. visual studio code\6. IoC test\bin\main started by nerin in C:\Users\nerin\Desktop\4. myproject\2. visual studio code\6. IoC test)
2023-07-02 12:34:02.006  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : No active profile set, falling back to 1 default profile: "default"
2023-07-02 12:34:02.219  WARN 20936 --- [  restartedMain] o.m.s.m.ClassPathMapperScanner           : No MyBatis mapper was found in '[excel.mh]' package. Please check your configuration.
2023-07-02 12:34:02.253  INFO 20936 --- [  restartedMain] o.s.b.w.e.t.TomcatWebServer              : Tomcat initialized with port(s): 443 (https)
2023-07-02 12:34:02.254  INFO 20936 --- [  restartedMain] o.a.c.c.StandardService                  : Starting service [Tomcat]
2023-07-02 12:34:02.255  INFO 20936 --- [  restartedMain] o.a.c.c.StandardEngine                   : Starting Servlet engine: [Apache Tomcat/9.0.74]
2023-07-02 12:34:02.271  INFO 20936 --- [  restartedMain] o.a.c.c.C.[.[.[/]                        : Initializing Spring embedded WebApplicationContext
2023-07-02 12:34:02.272  INFO 20936 --- [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 264 ms
2023-07-02 12:34:02.299  INFO 20936 --- [  restartedMain] o.s.s.w.a.c.ChannelProcessingFilter      : Validated configuration attributes
2023-07-02 12:34:02.300  INFO 20936 --- [  restartedMain] o.s.s.w.DefaultSecurityFilterChain       : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@5e6d7496, org.springframework.security.web.access.channel.ChannelProcessingFilter@52f97de0, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@476204c6, org.springframework.security.web.context.SecurityContextPersistenceFilter@4f19c873, org.springframework.security.web.header.HeaderWriterFilter@264ba9b1, org.springframework.security.web.authentication.logout.LogoutFilter@19d72be1, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3692231a, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d6f0258, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4fa6e5d3, org.springframework.security.web.session.SessionManagementFilter@2f0f4729, org.springframework.security.web.access.ExceptionTranslationFilter@6ff2d00a, org.springframework.security.web.access.intercept.AuthorizationFilter@22837aa6]
2023-07-02 12:34:02.378  INFO 20936 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2023-07-02 12:34:02.401  INFO 20936 --- [  restartedMain] o.a.t.u.n.N.certificate                  : Connector [https-jsse-nio-0.0.0.0-443], TLS virtual host [_default_], certificate type [UNDEFINED] configured from [file:/C:/Users/nerin/Desktop/4.%20myproject/2.%20visual%20studio%20code/6.%20IoC%20test/sslWork/domain/keystore.p12] using alias [tomcat] and with trust store [null]
2023-07-02 12:34:02.403  INFO 20936 --- [  restartedMain] o.s.b.w.e.t.TomcatWebServer              : Tomcat started on port(s): 443 (https) with context path ''
2023-07-02 12:34:02.406  INFO 20936 --- [  restartedMain] e.m.MhApplication                        : Started MhApplication in 0.449 seconds (JVM running for 1821.39)
2023-07-02 12:34:02.407  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:mhApplication
2023-07-02 12:34:02.407  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:beanPrint
2023-07-02 12:34:02.407  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:securityConfig
2023-07-02 12:34:02.408  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestController1
2023-07-02 12:34:02.408  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestController2
2023-07-02 12:34:02.408  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestService1
2023-07-02 12:34:02.408  INFO 20936 --- [  restartedMain] e.m.c.beanPrint                          : bean:ioCTestService2

 

 

3. 왜 스프링에서는 의존성을 주입하는 방식을 사용해야 할까?

그래서 객체를 생성하는거랑 참조하는 거랑 무슨 차이가 있는데?

bean이 어느 시점에 스프링 컨테이너에 등록되는지 알았지만 여전히 IoC를 왜 사용하는지는 모르겠단 말이야.

 

일단 나는 이 글을 작성하기 전까지 빈이 등록되는 것에 대해 조금 오해를 하고 있었다.

@Controller나 @Service 말고도 어노테이션의 종류는 많은데 일단 예제에서 쓴 두 개만 써봤다.

 

그러니까 객체를 생성하든 참조하든 iocTestService라는 클래스의 멤버변수가 bean으로 등록되는게 아니란 말이지.

bean으로 등록되는건 IoCTestService1과 IoCTestService2다. 생각해보면 당연한 거였는데 내가 멍청이였다.

public class IoCTestController1 {
    private IoCTestService1 iocTestService;
    public IoCTestController1(IoCTestService1 iocTestService) {
        this.iocTestService = iocTestService;
    }

    @GetMapping("/ioc/test1")
    public void test1() {
        log.info("객체 참조 test!");
        iocTestService.iocTestService();
    }
}


public class IoCTestController2 {
    private IoCTestService2 iocTestService;
    public IoCTestController2() {
        this.iocTestService = new IoCTestService2();
    }

    @GetMapping("/ioc/test2")
    public void test2() {
        log.info("객체 생성 test!");
        iocTestService.iocTestService();
        iocTestService.destroy();
    }
}

 

어쨌든... 다시 돌아와서 객체 생성과 참조 두 가지의 차이점에 대해서 얘기해보자.

스프링은 기본적으로 IoC 패턴을 사용해서 의존성을 주입하는(객체를 참조하는) 방식을 사용한다.

이것은 스프링에서 빈을 싱글톤으로 관리하기 때문이다.

 

그럼 누군가에게 "스프링에서 왜 싱글톤 패턴을 사용해서 빈을 관리하죠?" 라는 질문을 받으면 뭐라고 답해야할까?

먼저 스프링은 "웹" 애플리케이션이다.

한 사람이 사용하는 것이 아니라 여러 사람이 사용하는 것을 전제로 만들어야 한다.

 

극단적이지만 본인이 개발한 애플리케이션을 사용하는 사람이 "100만명"이라고 가정해보자.

어느날 운이 나쁘게도 100만명이 동시에 "/test" 엔드포인트에 액세스하면 어떻게 될까?

당연히 testService라는 객체가 100만개 생성될 거라는 것을 예상할 수 있다. (아무 처리도 안했을 때)

그렇게 되면 시스템 리소스 증가 및 메모리 부담이 될 수 밖에 없다.

@Controller
public class testController {
    private testService testService;
    
    public testController() {
        this.testService = new testService();
    }

    @GetMapping("/test")
    public void test() {
        log.info("test!");
        iocTestService.iocTestService();
    }
}

 

예시를 그림으로 표현하면 이렇다.

 

 

그래서 스프링에서는 기본적으로 의존성 주입 방법을 사용하고 컨테이너에 저장된 빈을 싱글톤으로 관리한다.

이 말이 무슨 말인지 코드를 살펴보자.

@Controller
public class testController {
    private testService testService;
    
    public testController(testService  testService) {
        this.testService = testService ;
    }

    @GetMapping("/test")
    public void test() {
        log.info("test!");
        testService.testService();
    }
}

 

위와 같이 코드를 작성한 경우 스프링 컨테이너에서는 의존성이 주입된 testService 객체를 싱글톤으로 관리한다.

마찬가지로 시스템에 100만명이 동시에 "/test"라는 엔드포인트로 요청을 보냈다고 가정해보자.

(testService는 예제로 주어지지 않았지만 작성되었다고 생각하자.)

 

예시를 그림으로 표현하면 이렇다.

 

어라... 뭐가 다른지 모르겠다고? 그러면 조금 더 디테일하게 그림을 수정해보겠다.

스프링에서는 여러 개의 쓰레드가 하나의 객체를 사용하는데, 각 쓰레드는 이 객체를 독립적으로 사용한다.

아까부터 계속 객체 의존성을 주입한다느니 싱글톤으로 빈을 관리한다느니 등의 모든 표현들이 이걸 가리킨다.

(여기서 하나의 객체라는건 스프링 컨테이너에 저장된 빈을 말한다.)

 

쓰레드가 사용할 하나의 객체는 스프링 컨테이너에 저장된 빈의 인스턴스 변수라고 생각해보자.

각 쓰레드의 객체는 객체의 메서드를 호출해서 작업을 수행한다.

그리고 그 과정에서 필요한 데이터를 인자로 전달하거나 객체의 지역 변수를 사용한다.

때문에 여러 개의 쓰레드가 동시에 실행되어도 객체의 상태를 독립적으로 유지하며 간섭이 일어나지 않는다.

...고 한다.

 

내가 자세한 내부 로직을 이해할 수는 없지만 그렇다네?

근데 여기까지도 잘 모르겠으면 뭐 적당히 그런가보다 하고 넘어갑시다.

어차피 천재 개발자 선배님들의 열띤 토론과 뛰어난 고찰을 통해 산출된 최적해일테니까.

그냥 감사합니다 하고 맘편하게 쓰는게 나을 듯? 감사합니다감사합니다감사합니다

 

참고로 지역 변수랑 비슷한 느낌이라고 생각할 수는 있지만 다른 개념이다.

지역변수는 여러 쓰레드에서 공유될 수 있지만 쓰레드의 객체는 그 쓰레드만의 객체이기 때문이다.

 

혹시 c를 조금 공부했던 사람은 포인터를 떠올릴 수도 있는데 그것과도 다른 개념이다.

객체를 가리키는 참조값을 변수에 저장할 뿐이지 직접적으로 메모리 주소를 참조하거나 수정하는게 아니기 때문이다.

 

아무튼 마지막으로 꼭 스프링에서 DI를 사용해야 하는가? 에 대해 생각해보자.

당연하지만 의존성을 주입하는 것이 필수는 아니라는거다.

그런데 누군가는 스프링을 개발하다보면 의존성 주입이 아니라 객체를 생성해야 할 때가 있다니까? 라고 한다.

근데 그 때가 언제인지 구체적으로 설명해주는 누군가는 왜 없을까...

 

나는 그게 무슨 경우인지 1년내내 궁금했었는데... 최근 운이 좋게도 개인 프로젝트를 진행하면서 알게 되었다.

과거의 내 질문에 무려 구체적인 예시를 들어서 답을 해보자.

 

※ 예시

python과 마찬가지로 java에서도 웹 스크래핑이 가능하다.

일반적으로 웹 스크래핑은 다수가 사용하는 웹보다 응용 프로그램에서 개인이 사용하는게 적절하지만...

아무튼 java에서 가능하다면 당연히 스프링에서도 구현이 가능하다.

 

웹 스크래핑 기능이 추가된 스프링 웹 애플리케이션을 만들어야 한다고 가정해보자.

사람들이 많이 사용하는 jsoup를 사용해서 간단히 구현하려고 했는데 큰 문제가 생겼다.

jsoup는 정적 페이지에 대해서만 데이터를 가져올 수 있기 때문이다.

 

이게 무슨 말인가 하면 보통 웹은 서버 사이드와 클라이언트 사이드로 구분되어 있다.

그리고 클라이언트 사이드에서는 서버로 데이터를 요청해서 받아온 데이터로 화면을 구성하는 경우가 있다.

우리가 흔히 알고 있는 ajax, axios 등 비동기 요청을 말한다.

 

https://1545154.tistory.com/93

 

[자바 크롤링] java jsoup의 문제점

프로젝트를 하다가 곤란한 상황을 만났다. jsoup를 사용해서 신나게 크롤링 코드를 짰는데 jsoup는 html element가 추가되는 경우, 그 값을 가져올 수 없었다. 그리고 아무래도 문제를 해결하기 위해서

1545154.tistory.com

 

비동기 요청 데이터를 가져오기 위해서는 selenium을 사용해야한다.

selenium을 사용해보지 않은 사람이라면 어떻게 사용하는지 알아보는게 먼저겠지만...

간단하게 말해서 크롬 드라이버를 사용해서 웹 스크래핑을 한다.

 

위에서 스프링은 기본적으로 의존성 주입을 통해 싱글톤으로 객체를 관리한다고 했다.

그러면 그림이 이렇게 되겠지.

 

우선 서버 사이드를 실행시키면 크롬 드라이버가 실행되고 개발자용 크롬 페이지가 생성된다.

크롬 페이지가 열리면 셀레니움 명령어를 사용해서 창을 조작하는 방식이다.

https://1545154.tistory.com/97

 

[스프링에서 크롤링 데이터 수집하기] java selenium (feat. 메이플스토리 랭킹정보 수집)

0. 하려는거 1. 크롬드라이버 다운로드 2. build.gradle 작성 3. 컨트롤러 작성 4. 서비스 작성 4.1 웹 드라이버 핸들러 4.2 핸들러 세팅 4.3 데이터 파싱1 4.4 데이터 파싱2 4.5 데이터 파싱3 4.6 세션 종료 5.

1545154.tistory.com

 

그런데 이게 의존성 주입을 사용해서 하나의 크롬드라이버 객체를 여러 쓰레드에서 사용하게 되면 안된다.

뭐? 분명 위에서 쓰레드의 각 객체는 개별로 관리된다고 했는데 왜 이것이 문제가 되는가?

 

일단 대전제로 크롬 드라이버로 조작할 수 있는 개발자용 페이지는 고유의 ID를 가진다.

크롬 드라이버를 조작하는 객체를 빈으로 등록하면 당연히 그 빈을 참조하는 크롬 드라이버의 ID는 동일하겠지.

즉, 하나의 개발자용 크롬 페이지에서 여러 사람이 동시에 요청하게 된다.

 

만약 셀레니움을 사용해서 google에 검색하고 검색한 내용의 이미지들을 반환하는 프로그램을 만들었다고 해보자.

프로세스는 크게 2개로 나뉠 수 있다.

 

"검색어"를 입력하면 보여지는 페이지에서 "이미지" 탭을 누른다.

"이미지"탭에서 보여지는 이미지들을 반환한다.

 

이 프로그램에서 만약 A사용자는 "하이"를, B사용자는 "헬로"를 동시에 검색하면 어떻게 될까?

"동시에"라고 해도 어차피 A사용자의 요청과 B사용자의 요청 2개 중 하나를 먼저 처리하게 되겠지.

(0.001초의 오차없이 보내지는 못했을테고 네트워크 속도도 있고 뭐...그런것들 때문에...)

 

A 요청을 먼저 처리한다고 해보자.

구글에 "하이" 검색 > 페이지 렌더링 > 이미지 탭 누름 > 페이지 렌더링 > 이미지 반환 

과정을 보면 알겠지만 이 과정은 순식간에 일어나지 않는다.

 

그리고 B요청도 동시에 처리되어야 한다.

위에서 말했던 것처럼 서버에 빈으로 등록된 크롬드라이버는 하나이다.

따라서 속도의 차이는 있겠지만 이렇게 될 것을 예상할 수 있다.

구글에 "하이" 검색 > "하이" 페이지 렌더링 > "하이" 이미지 탭 누름 > 구글에 "헬로" 검색 > "헬로" 페이지 렌더링 > "헬로" 이미지 반환

이럴경우 A와 B는 둘 다 "헬로"이미지를 반환받게 된다.

 

아무튼 뭐... 이런 이슈가 있어서 객체 생성을 해야한다는 거다.

 

보는 사람은 없겠지만 만약 보게되면 적당히 걸러서 보시길.

틀린 설명이 있으면 어쩌지 싶긴한데 뭐 내 개인공부용으로 작성하는거니까!

지식공유자가 있다면 지적 감사합니다...

 

끝!

댓글