본문 바로가기
Spring Framework

제어의 역전(IoC)과 의존성 주입(DI)

by mozzi329 2022. 8. 16.
728x90

 

     

    스프링 프로젝트의 모든 Bean을 관리하는 스프링 컨테이너

     

    📌 제어의 역전(IoC)

    제어의 역전(IoC, Inversion of control)은 애플리케이션 흐름의 주도권을 개발자가 아닌 프레임워크(혹은 서블릿 컨테이너)가 갖는 것을 말한다.

    앞서 라이브러리와 프레임워크를 설명할 때 프레임워크는 애플리케이션 흐름의 주도이 프레임워크에게 있다고 설명했다. 여기서 말하는 애플리케이션 흐름의 주도권이 뒤바뀐 것을 바로 제어의 역전(IoC, Inversion of Control)이라고 한다.

     

    public class Example {
    	public static void main(String[] args) {
        	System.out.println("Hello IoC!");
        }
    }

    일반적으로 자바 애플리케이션을 실행하기 위해선 main() 메서드가 있어야 한다. main() 메서드를 통해 객체를 선언하거나 해당 클래스의 메서드를 실행시킬 수 있다. 이렇게 개발자가 작성한 코드를 순차적으로 실행하는 것이 우리가 많이 봐왔던 애플리케이션의 일반적인 제어 흐름이라고 할 수 있다.

     

    서블릿 컨테이너의 서블릿 호출

    자바 애플리케이션에서 IoC가 적용되는 서블릿 컨테이너의 예시를 들어보자.

     

    서블릿 컨테이너의 경우, 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직이 서블릿을 직접 실행시켜 주기 때문에 별도의 main() 메서드가 필요 없다.

     

    이 경우, 서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에게 있다. 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)의 개념이 적용되어 있는 것이다.

     

    스프링에서는 IoC가 DI(Dependency Injection)를 통해 적용되고 있다.

     

    📌 의존성 주입(DI)

    의존성 주입(Dependency Injection)은 IoC의 개념을 더욱 구체화시킨 것으로, 하나의 객체가 다른 객체의 의존성을 제공하는 방법 혹은 기술이다. 

    ✔️ 의존한다?

    A는 B에 의존한다.

    그림과 같이 MenuController(A) 클래스 내부에서 MenuService(B) 클래스를 호출하거나 해당 메서드를 사용하고 있다면 A는 B에 의존한다라고 할 수 있다.

     

    public class MenuController {
    	public static void main(String[] args) {
        
        	// (1) MenuService(B)를 호출했기에 의존관계가 된다.
        	MenuService menuService = new MenuService(); 
            List<Menu> menuList = menuService.getMenuList();    
        }
    }
    
    
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuService {
    	public List<Menu> getMenuList() {
        	return null;
        }
    }

    코드를 보자면 다음과 같다. MenuContoroller 내부에 MenuService를 생성했기 때문에 MenuController 클래스는 MenuService 클래스를 의존하게 된다.

     

    ✔️ 의존성 주입 그거 어떻게 하는 건데?

    의존성 주입은 생각보다 간단하다. 바로 생성자의 파라미터로 관련 클래스 객체를 전달하기만 하면 된다.

     

    위의 코드를 조금 수정해보자. 다음은 의존성 주입이 일어나는 예이다.

    public class CafeClient {
    	public static void main(String[] args) {
        	MenuService menuService = new MenuService(); 
            
            // (2) MenuController의 파라미터로 menuService 객체를 주입한다.
            MenuController controller = new MenuController(menuService); 
            List<Menu> menuList = controller.getMenus();    
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuController {
    	private MenuService menuService;
        
        public MenuController(MenuService menuService) { // 의존성 주입
        	this.menuService = menuService;
        }
    
    	public List<Menu> getMenus() {
        	return menuService.getMenuList();
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuService {
    	public List<Menu> getMenuList() {
        	return null;
        }
    }

     

    이전에 코드에서 MenuService 기능을 사용하기 위해 MenuController에서 MenuService의 객체를 new 키워드로 직접 생성한 반면에 위 코드에서는 MemberController 생성자로 MenuService의 객체를 전달받고 있다.

     

    이처럼 생성자를 통해서 외부의 객체를 전달 받는 것을 의존성 주입이라고 표현한다. 

     

    그런데 의존성 주입을 사용할 때, 항상 염두해 두어야 하는 부분이 있다. 바로 new 키워드를 사용할지 말지의 여부를 결정하는 것이다. new 키워드를 사용할 경우 객체지향 설계의 관점에서 중요한 문제가 발생할 수 있다.

     

    ✔️ 강한 결합(Tight Coupling)

    메뉴에서 API 통신이 잘 되는지 확인하기 위해 별도의 응답을 위한 MenuServiceStub로 교체해야 하는 상황이 발생했다고 가정해보자. 개발자는 별도의 API 메뉴 데이터를 조회하기 위해 코드 구성을 다음과 같이 수정했다.

    public class CafeClient {
    	public static void main(String[] args) {
        	MenuServiceStub menuService = new MenuServiceStub(); // MenuService를 MenuServiceStub로 변경
            MenuController controller = new MenuController(menuService); 
            List<Menu> menuList = controller.getMenus();    
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuController {
    	private MenuServiceStub menuService;
        
        public MenuController(MenuServiceStub menuService) { // 의존성 주입
        	this.menuService = menuService;
        }
    
    	public List<Menu> getMenus() {
        	return menuService.getMenuList();
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuServiceStub {
    	public List<Menu> getMenuList() {
        	return List.of( // API 통신 조회를 위한 Stub
                new Menu(1, "아메리카노", 2500),
                new Menu(2, "카라멜 마끼야또", 4500),
                new Menu(3, "바닐라 라떼", 4500)
            );
        }
    }

    메뉴 목록 조회 API로 Stub를 제공하기 위해 MenuServiceStub 클래스를 사용하는 것으로 변경되었다.

     

    그런데 MenuServiceStub 클래스를 사용하려고 보니, 이 MenuService 클래스를 의존하고 있는 CafeClient와 MenuController에서 MenuService를 MenuServiceStub 클래스로 변경해야 하는 상황히 불가피하게 발생했다. 코드가 한, 두 줄이라면 변경하는데 그리 오래걸리지 않지만 만약 MenyServiceStub 클래스를 사용해야할 대상이 수십, 수백군데라면 어떻게 해야할까? (야근 확정) 결국 관련된 모든 코드를 일일히 수정해야만 한다.

     

    이처럼 new 키워드를 사용해서 의존 객체를 생성할 때, 클래스들 간에 강하게 결합(Tight Coupling)되어 있다고 표현한다. 결론적으로 의존성 주입을 하더라도 의존성 주입의 혜택을 보기 위해서는 클래스들 간의 강한 결합은 피하는 것이 좋다.

     

    ✔️ 느슨한 결합(Loose Coupling)

    기껏 사용한 의존성 주입의 혜택을 보기 위해서는 클래스간의 의존성 관계가 느슨한 결합이 되어야 한다. Java에서 클래스들 간의 관계를 느슨하게 만드는 대표적인 방법은 바로 인터페이스(Interface)를 사용하는 것이다.

    인터페이스를 사용한 클래스 간의 느슨한 결합

    다음과 같은 클래스 다이어그램에 따르면 MenuCotroller가 MenuService 클래스를 직접적으로 의존하는 것이 아닌, 클래스 이름은 같지만 인터페이스를 의존하고 있는 것을 볼 수 있다.

     

    MenuController는 MenuService를 의존하고 있지만 MenuService의 구현체는 MenuServiceImpl인지 MenuServiceStub인지 알지 못한다. 

     

    이처럼 어떤 클래스가 인터페이스와 같이 일반화된 구성 요소에 의존하고 있을 때, 클래스들 간에 느슨하게 결합(Loose Coupling)되어 있다고 표현한다. 코드를 수정하자면 다음과 같다.

    public class CafeClient {
    	public static void main(String[] args) {
        	MenuService menuService = new MenuServiceStub();
            MenuController controller = new MenuController(menuService); 
            List<Menu> menuList = controller.getMenus();    
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuController {
    	private MenuService menuService;
      
        public MenuController(MenuService menuService) { // MenuService 인터페이스를 주입받고 있다.
        	this.menuService = menuService;
        }
    
    	public List<Menu> getMenus() {
        	return menuService.getMenuList();
        }
    }
    
    public interface MenuService {
    	List<Menu> getMenuList();
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuServiceStub implements MenuService{ // MenuService 인터페이스를 상속받아 getMenuList 메서드를 구현했다. 
    	public List<Menu> getMenuList() {
        	return List.of( // API 통신 조회를 위한 Stub
                new Menu(1, "아메리카노", 2500),
                new Menu(2, "카라멜 마끼야또", 4500),
                new Menu(3, "바닐라 라떼", 4500)
            );
        }
    }

    MenuServiceStub는 MenuService 인터페이스를 상속받아 getMenuList를 구현하고 있다.

     

    CafeClient의 인터페이스 타입 MenuService는 해당 인터페이스 구현 객체(MenuServiceStub)를 할당할 수 있는데 이를 업캐스팅(Upcasting)이라고 한다. 업캐스팅을 통한 의존성 주입으로 인해 MenuController와 MenuService는 느슨한 결합 관계를 유지하고 있다.

     

    그러나 클래스들 간의 관계를 느슨하게 만들기 위해서는 new 키워드를 사용하지 않아야 하는데, CafeClient 클래스의 코드를 보면 MenuServiceStub의 객체와 MenuController 객체를 생성하기 위해 여전히 new 키워드를 사용하고 있다. 

     

    그렇다면 이 new 키워드는 어떻게 하면 제거하고 의존 관계를 느슨하게 만들 수 있을까? 바로 이 의존 관계를 Spring이 대신해준다.

    public class CafeClient {
    	public static void main(String[] args) {
    		GenericApplicationContext context =
            	new AnnotationConfigApplicationContext(Config.class);
            MenuController controller = context.getBean(MenuController.class);
            
            List<Menu> menuList = controller.getMenus();
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    public class MenuController {
        private MenuService menuService;
      
        @Autowired
        public MenuController(MenuService menuService) { 
        	this.menuService = menuService;
        }
    
        public List<Menu> getMenus() {
            return menuService.getMenuList();
        }
    }
    
    ///////////////////////////////// 코드 구분선 ///////////////////////////////////
    
    @Configuration
    @ComponentScan(basePackageClasses = CafeClient.class)
    public class Config { 
        @Bean
        public MenuService getMenuService() {
        	return new MenuServiceStub();
        }
        
        @Bean
        public MenuController getMenuController(MenuService menuService) {
        	return new MenuController(menuService);
        }
    }

    @Configuration, @ComponentScan, @Bean과 같은 애노테이션은 아래에서 설명하겠다. 다만 Config 클래스에서 Menucontroller 객체 생성을 정의해두면 AnnotationConfigApplicationContext를 통해 객체를 애플리케이션 코드에서 사용할 수 있다.

     

    다시 말하자면 Config 클래스에 정의해둔 MenuController 객체를 Spring의 도움을 받아 CafeClient 클래스에게 제공하고 있는 것이다. 이처럼 Spring 기반의 애플리케이션에서는 Spring이 의존 객체들을 주입해주기 때문에 애플리케이션 코드를 유연하게 구성할 수 있다. 

     

    위의 코드에서 서비스의 변경이 일어난다고 한다면 Spring FrameWork 영역(Config)에 있는 getMenuSerivce에서 리턴되는 서비스를 변경된 서비스로 단 한번만 변경해주면 된다. 이와 같이 Spring에서는 애플리케이션 코드에서 이루어지는 의존성 주입(DI)을 Spring에서 대신해준다.

     

    이제 스프링 프레임워크의 핵심 프로그레밍 모델 중 하나인 의존성 주입(DI)에 필요한 내용들에 대해 알아보자.

     

    📌스프링 컨테이너

    스프링 컨테이너(Spring Container)는 Spring 프로젝트 내부에 존재하는 애플리케이션 빈의 생명주기(생성, 관리, 제거 등)를 관리하는 역할을 한다.

    ✔️ 컨테이너의 의미

    소프트웨어 개발 용어의 관점에서 컨테이너란 내부에 또 다른 컴포넌트를 가지고 있는 어떤 컴포넌트를 의미한다.

    • 컨테이너는 먼저 객체를 생성하고 객체를 서로 연결한다.
    • 객체를 설정하는 단계를 지나 마지막으로 생명주기 전반을 관리한다.
    • 컨테이너는 객체의 의존성을 확인해 생성한 뒤 적절한 객체에 의존성을 주입한다.

    스프링 컨테이너(ApplicationContext) 인터페이스

    ✔️ 스프링 컨테이너의 특징

    • 스프링 컨테이너는 XML 혹은 애너테이션 기반의 자바 설정 클래스로 만들 수 있다.
      예전에는 개발자가 xml을 통해 모두 설정해줬지만, 이러한 복잡한 부분들을 Spring Boot를 사용하면서 거의 사용하지 않게 되었다.

    • 스프링 빈의 인스턴스화, 구성, 제거 등 전체 생명 주기를 처리한다.
      컨테이너는 개발자가 정의한 빈을 객체로 만들어 관리하고 개발자가 필요로 할 때 제공한다.

    • 스프링 컨테이너를 원하는 만큼 많은 객체를 가질 수 있다.

    • 의존성 주입을 통해 애플리케이션의 컴포넌트를 관리한다.
      → 서로 다른 빈을 연결해 애플리케이션의 빈을 연결하는 역할을 한다.
      → 개발자는 모듈 간에 의존 및 결합으로 인해 발생하는 문제로부터 자유로울 수 있다.
      → 메서드가 언제, 어디서 호출되어야 하는지, 메서드 호출을 위해 필요한 매개 변수를 준비해서 전달해준다.

    ※ 스프링 빈(Spring Bean) : 스프링 컨테이너가 관리하는 자바 객체를 말한다. @Bean 애노테이션 혹은 xml 설정을 통해 등록이 가능하다.

     

    ✔️ 스프링 컨테이너는 왜 사용할까?

    이전에 객체를 사용하기 위해서 new 생성자를 사용해야 했다. 이러한 이유로 애플리케이션의 무수한 객체들이 서로를 참조해야만 했다. 서로의 참조가 심할수록 의존성이 높다고 표현한다. 이는 낮은 결합도와 높은 캡슐화가 핵심인 객체 지향 프로그래밍을 지키는데 걸림돌이 되었다. 이러한 부분에서 객체 간의 의존성을 낮추기 위해 스프링 컨테이너가 사용되게 되었다.

     

    ✔️ 스프링 컨테이너가 해결법이 되는 이유

    기존의 방식으론 새로운 정책이 생기게 될 경우 변경 사항들을 수작업으로 수정해야만 했다. 서로 의존이 많이 되어있지 않는 작업 초반부에는 하나 하나 수정할 수 있었지만, 점점 서비스의 코드가 거대해지면서 객체 간의 의존도가 높아짐에 따라 코드의 변경이 많이 필요하게 되었다. 후에 이에 대한 개선 방법으로 스프링 컨테이너가 사용되면서 구현 클래스에 있는 의존을 제거하고 인터페이스에만 의존하도록 설계할 수 있게 되었다.

     

    📌 스프링 컨테이너의 생성

     스프링 컨테이너의 설정 방식으로 Java 기반의 애너테이션 기반 설정과 XML 방식의 설정 방식이 존재한다.

    스프링 컨테이너 생성 과정

    스프링 컨테이너는 Configuration Metadata(작성된 AppConfig.class 혹은 application.xml 등)를 사용한다. 컨테이너에 파라미터로 넘어온 설정 클래스 정보를 이용해서 스프링 빈을 스프링 컨테이너에 등록한다.

     

    애플리케이션 클래스는 구성 메타데이터와 결합되어 ApplicationContext 생성 및 초기화된 후 완전히 구성되고 실행 가능한 시스템 또는 애플리케이션을 갖게 된다.

     

    스프링 빈은 모든 자바 객체의 최고 부모인 Object 타입으로 모든 스프링 빈을 조회하며, 조회에서 상속관계가 있을 시 부모타입으로 조회 시 자식 타입도 함께 조회된다. 

     

    ✔️ 애너테이션 기반 설정(AppConfig.class)

    // Spring Container 생성
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    
    
    ////////////////////////////////// 코드 구분선 ////////////////////////////////
    
    @Configuration
    public class AppConfig {
        @Bean
        public UserService userService() {
            return new UserServiceImpl(userRepository());
        }
        @Bean
        public UserRepository userRepository() {
            return new UserRepositoryImpl();
        }
        @Bean
        public OrderService orderService() {
            return new OrderServiceImpl(userRepository(), discountInfo());
        }
        @Bean
        public DiscountInfo discountInfo() {
            return new CurrentDiscountInfo();
        }
    }
    • AppConfig와 같은 별도의 클래스를 만들어 @Configuration 애노테이션으로 구성정보 클래스를 등록한다.
    • 생성자 메서드는 @Bean 애노테이션을 사용하여 메서드를 등록한다.
    • 등록된 메서드는 최종적으로 new AnnotationConfigApplicationContext(AppConfig.class)를 통해 스프링 컨테이너에 등록된다.

    ※ ApplicationContext 생성자에 제공된 위치 경로 또는 경로는 컨테이너가 로컬 파일 시스템, Java CLASSPATH 등과 같은 다양한 외부 리소스로부터 구성 메타데이터를 로드할 수 있도록 하는 리소스 문자열이다.

     

    ✔️ XML 기반 설정(xml)

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd">
            
        <bean id="..." class="...">  
            <!-- collaborators and configuration for this bean go here -->
         </bean>
            
         <bean id="..." class="...">
             <!-- collaborators and configuration for this bean go here -->
         </bean>
            
         <!-- more bean definitions go here -->
            
    </beans>
    • <beans />에 필요한 값들을 설정한다.
    • <bean id="..."> : 빈 정의를 식별하는데 사용되는 문자열이다.
    • <bean class="..."> : 빈의 유형을 정의하고 사용되는 클래스 이름을 작성한다.

     

    ✔️ 스프링 컨테이너의 종류

    스프링 컨테이너의 최상위 인터페이스(BeanFactory)

    BeanFactory

    스프링 컨테이너의 최상위 인터페이스이다.

    • 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 한다.
    • getBean() 메서드를 통해 빈을 인스턴스화할 수 있다.
    • @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용해 빈을 등록한다.

     

    ApplicationContext

    BeanFactory의 기능을 상속받아 BeanFactory의 기능과 그 외의 기능을 제공한다.

    • MessageSource
      메세지 다국화를 위한 인터페이스
    • EnvironmentCapable
      개발 및 운영에 대해 환경변수 등으로 나누어 처리하고, 애플리케이션 구동 시 필요한 정보들을 관리하는 인터페이스
    • ApplicationEventPublisher
      이벤트 관련 기능을 제공하는 인터페이스
    • ResourceLoader
      파일, 클래스 패스, 외부 등 리소스를 편리하게 조회하는 기능을 제공하는 인터페이스

     

    📌 빈(Bean)

    스프링 컨테이너가 관리하는 자바 객체를 의미하며, 하나 이상의 빈을 관리한다.
    • 빈(Bean)은 인스턴스화된 객체를 의미한다.
    • 스프링 컨테이너에 등록된 객체를 스프링 빈(Spring Bean)이라고 한다. 
    • @Bean이 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
    • 클래스의 등록정보(생성자), 게터/세터 메서드를 포함한다.
    • 컨테이너에 사용되는 설정 메타데이터(XML, 자바 애너테이션, 자바 코드)로 생성되며, 설정 메타데이터는 컨테이너의 명령과 인스턴스화, 설정, 조립할 객체를 정의한다.

     

    ✔️ 빈 접근 방법

    ApplicationContext를 사용하여 빈(Bean) 정의를 읽고 액세스할 수 있다.

    // create and configure beans
    ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
    
    // retrieve configured instance
    PetStoreService service = context.getBean("cmarket", cmarketService.class);
    
    // use configured instance
    List<String> userList = service.getUsernameList();
    • getBean을 사용하여 빈의 인스턴스를 가져올 수 있다.
      단, 응용 프로그램 코드에서는 getBean() 메서드로 호출하여 사용하면 안된다. 개발자가 빈을 호출하여 사용한다는 점에서 제어의 역전(IoC)에 대해 위배되기 때문이다.
    • ApplicationContext 인터페이스에는 빈을 가져오는 여러가지 방법이 존재한다.

     

    ✔️ BeanDefinition

    스프링은 다양한 설정 형식을 BeanDefinition이라는 추상 인터페이스를 통해 제공한다.

    • Bean은 BeanDefinition(빈 설정 메타정보)으로 정의되고 BeanDefinition에 따라 활용 방법이 달라지게 된다.
    • 속성에 따라 컨테이너가 빈을 어떻게 생성하고 관리할지를 결정한다.
    • @Bean or <bean> 당 각 1개 씩의 메타 정보가 생성된다.
    • Spring이 설정 메타정보를 BeanDefinition 인터페이스를 통해 관리하기 때문에 컨테이너 설정을 XML, Java 등 다양하게 설정할 수 있다.
    • BeanDefinition을 통해 어떤 설정 형식이 오던지 일관된 기능을 제공한다.

     

    ❗️구성 메타데이터

    • 패키지 수식 클래스 이름
      일반적으로 정의되는 빈의 실제 구현 클래스이다.
    • 빈 동작 구성 요소
      컨테이너에서 빈이 어떻게 동작해야하는지를 설명한다. (범위, 수명 주기 콜백 등)
    • 다른 빈에 대한 참조
      빈이 작업을 수행하는데 필요한 다른 빈에 대한 참조
    • 기타 구성 설정
      새로 만든 개체에 설정할 기타 구성 설정들이다.

     

    ❗️메타데이터는 각 빈 정의를 구성하는 속성 집합으로 변환된다.

    • BeanClassName
      생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
    • factoryBeanName
      팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
    • factoryMethodName
      빈을 생성할 팩토리 메서드 지정, 예) userService
    • Scope
      싱글톤(기본값)
    • lazyInit
      스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리 하는지 여부
    • InitMethodName
      빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
    • DestoryMethodName
      빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
    • Constructor arguments, Properties
      의존관계 주입에서 사용한다. (자바 설정처럼 팩터리 역할의 빈을 사용하면 없음)

     

    📌 빈 스코프

    빈 스코프(Bean Scope)란 빈이 존재할 수 있는 범위를 의미한다.
    • 특정 빈 정의에서 생성된 개체에 연결할 다양한 의존성 및 구성 값뿐만 아니라 생성된 개체의 범위도 제어할 수 있다.
    • Spring Framework는 6개의 범위를 지원하며, 그 중 4개는 ApplicationContext를 사용하는 경우에만 사용할 수 있다.
    • 빈은 여러 범위 중 하나에 배치되도록 정의할 수 있다.
    • 구성을 통해 생성하는 개체의 범위를 선택할 수 있기 때문에 강력하고 유연하다.
    • 사용자 정의 범위를 생성할 수도 있다.
    Scope Description
    singleton (Default) 각 스프링 컨테이너에 대한 단일 객체 인스턴스에 대한 단일 빈 정의의 범위를 지정한다.
    prototype 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
    request 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
    session 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    application 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다.
    websocket 단일 bean definition 범위를 WebSocket의 라이프사이클까지 확장한다.
    Spring ApplicationContext의 컨텍스트에서만 유효하다.

     

    ✔️ 스프링 컨테이너에서 사용되는 싱글톤

    싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스가 단 하나만 생성되고 해당 인스턴스만을 관리하는 디자인 패턴이다.

    싱글톤 패턴(Singleton Pattern)

    • 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다.
    • 싱글톤 빈의 하나의 공유 인스턴스만 관리하게 된다.
    • 단일 인스턴스는 싱글톤 빈의 캐시에 저장된다.
    • 싱글톤 스코프의 스프링 빈은 여러번 호출하더라도 모두 같은 인스턴스 참조 주소 값을 가진다.
    • 해당 bean definition와 일치하는 ID 또는 ID를 가진 빈에 대한 모든 요청은 스프링 컨테이너에서 해당 특정 빈 인스턴스를 반환다.
    • 스프링 컨테이너 종료시 소멸 메서드도 자동으로 실행된다.

     

    ✔️ 싱글톤 패턴의 문제점

    • 싱글톤 패턴을 구현하는 코드 자체가 많다.
    • 의존관계상 클라이언트가 구체 클래스에 의존한다.
    • 지정해서 가져오기 때문에 테스트하기 어렵다.
    • private 생성자를 사용하여 자식 클래스를 만들기 어렵기 때문에 유연성이 떨어진다.
    • 멀티쓰레드 환경에서 싱글톤 객체의 속성은 여러 쓰레드에 의해 바뀔 수 있다.
    • A 쓰레드에선 속성 값을 x로 바꾸고 출력하는 과정에서 B 쓰레드가 속성 값을 y로 바꾸면 쓰레드 A에선 예상하지 못한 값이 나올 수 있다. (1개의 인스턴스에서 속성 값을 공유하기 때문에 발생하는 문제점)
    • 가급적 읽기만 가능해야 한다. (특정 클라이언트가 값을 변경할 수 있으면 안된다.)
    • Application 초기 구동 시 인스턴스 생성
      싱글톤 빈은 기본적으로 애플리케이션 구동 시 생성되므로 싱글톤 빈이 많을수록 구동 시간이 증가할 수 있다.

     

    ✔️ 싱글톤 컨테이너

    객체 인스턴스를 싱글톤으로 관리하며 위의 싱글톤 패턴의 문제점들을 싱글톤 컨테이너가 해결해준다.

    • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.
    • 싱글톤 객체로 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
    • 스프링 컨테이너의 위 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하며 객체를 싱글톤으로 유지할 수 있다.

     

    ✔️ 싱글톤 컨테이너 방식의 주의점

    싱글톤 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태로 설계해야 한다.

    • 특정 클라이언트가 값을 변경할 수 있으면 안된다.
    • 읽기만 가능해야 합니다.
    • 스프링 빈의 공유 값을 설정하면 장애가 발생할 수 밖에 없다.

     

    📌 Java 기반 컨테이너 설정

    ✔️ 스프링 컨테이너를 인스턴스화 하는 방법

    • Spring 3.0에 도입된 AnnotationConfigApplicationContext 이다.
    • ApplicationContext 구현은 아래와 같은 애너테이션이 달린 클래스로 파라미터를 전달 받고 있다.
      • @Configuration 클래스
      • @Component 클래스
      • JSR-330 메타데이터
    • @Configuration 클래스가 입력으로 제공되면
      @Configuration 클래스 자체가 빈 정의로 등록되고 클래스 내에서 선언된 모든 @Bean 메서드도 빈 정의로 등록된다.
    public static void main(String[] args) {
    
        // @Configuration 클래스를 입력으로 사용(AppConfig.class)
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        MyService myService = ctx.getBean(MyService.class);
        myService.doStuff();
    }
    • @Component 클래스와 JSR-330 클래스가 제공되면
      빈 정의로 등록되며 필요한 경우 해당 클래스 내에서 @Autowired 또는 @Inject와 같은 DI 메타데이터가 사용되는 것으로 가정한다.
    public static void main(String[] args) {
        // @Component 또는 JSR-330 주석이 달린 클래스를 생성자에 입력으로 사용
        ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
        MyService myService = ctx.getBean(MyService.class);
        myService.doStuff();
    }

     

    ✔️ @Bean

    @Bean은 메서드 레벨의 애너테이션이며 <bean />에서 제공하는 일부 속성을 지원한다. 

    • 메서드의 초기화
    • 메서드의 소멸
    • Autowiring

     

    ❗️빈의 선언

    • 해당 메서드를 빈으로 정의한다.
    @Configuration
    public class AppConfig {
    
        @Bean
        public TransferServiceImpl transferService() {
            return new TransferServiceImpl();
        }
    }

     

    • 빈 정의가 있는 인터페이스를 구현하여 bean configuration을 설정할 수 있다.
    public interface BaseConfig {
    
        @Bean
        default TransferServiceImpl transferService() {
            return new TransferServiceImpl();
        }
    }
    
    @Configuration
    public class AppConfig implements BaseConfig {
    
    }

     

    • 빈 의존성
      @Bean 애너테이션이 추가된 메서드는 빈을 구축하는데 필요한 의존성을 나타내기 위해 매개 변수를 사용할 수 있다.
    @Configuration
    public class AppConfig {
    
        @Bean
        public TransferService transferService(AccountRepository accountRepository) { // 매개변수(객체) 주입
            return new TransferServiceImpl(accountRepository);
        }
    }

     

    ✔️ @Configuration

    @Configuration는 클래스 레벨의 애너테이션이며 해당 객체가 bean definitions의 소스임을 나타내는 애너테이션이다.

    • @Configuration는 @Bean-annoted 메서드를 통해 빈을 선언한다.
    • @Configuration 클래스의 @Bean 메서드에 대한 호출을 사용하여 빈 사이의 의존성을 정의할 수도 있다.
    @Configuration
    public class AppConfig {
    
        @Bean
        public BeanOne beanOne() {
            // beanTwo 메서드(빈 메서드)를 호출하여 빈 사이의 의존성을 정의
            return new BeanOne(beanTwo());
        }
    
        @Bean
        public BeanTwo beanTwo() {
            return new BeanTwo();
        }
    }
    • 모든 @Configuration 클래스는 시작 시 CGLIB를 사용하여 하위 클래스로 분류된다.
    • 스프링에서 CGLIB라는 바이트코드 조작 라이브러리를 사용하는 것이다.
    • AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록하는 것이다.
      (AppConfig → AppConfig@CGLIB)
    • 같은 메서드를 호출할 때 이미 스프링 컨테이너에 등록되어 있으면 기존에 만들어진 걸 반환해준다.
    • 스프링 컨테이너에 등록되어 있지 않을 때에만 필요한 Bean을 생성하고 스프링 컨테이너에 등록한다.
    • @Configuration 클래스를 사용할 때, 자바 컴파일러는 구성 모델에 제약을 두며, 다른 빈에 대한 참조는 유효한 자바 구문이어야 한다.

     

    📌 Spring DI

    스프링에서 대표적인 의존성 주입 방법으로 생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입이 있다.

    ✔️ 생성자 주입

    생성자를 통해서 의존 관계를 주입받는 방법이다.

    생성자에 @Autowired를 하면 스프링 컨테이너에 @Component로 등록된 빈에서 생성자에 필요한 빈들을 자동으로 주입한다.

     

    ❗️특징

    • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변과 필수 의존 관계에 사용된다.
    • 생성자가 1개만 존재하는 경우에는 @Autowired를 생략해도 자동 주입된다.
    • NullPointerException을 방지할 수 있다.
    • 주입받을 필드를 final로 선언 가능하다.
    @Component
    public class OrderServiceImpl implements OrderService {
        private final UserRepository userRepository;
        private final DiscountInfo discountInfo;
    
        @Autowired
        public OrderServiceImpl(UserRepository userRepository, DiscountInfo discountInfo) {
            this.userRepository = userRepository;
            this.discountInfo = discountInfo;
      }
    }

     

    ✔️ 수정자 주입

    setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존 관계를 주입하는 방법이다.

     

    ❗️특징

    • 선택과 변경 가능성이 있는 의존 관계에 사용된다.
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
    @Component
    public class OrderServiceImpl implements OrderService {
        private UserRepository userRepository;
        private DiscountInfo discountInfo;
    
        @Autowired
        public void setUserRepository(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        @Autowired
        public void setDiscountInfo(DiscountInfo discountInfo) {
            this.discountInfo = discountInfo;
        }
    }
    • 생성자 주입과 차이점은 생성자 대신 set필드명 메서드를 생성하여 의존 관계를 주입하게 된다.
    • 수정자의 경우 @Autowired를 입력하지 않으면 실행이 되지 않는다.
      @Component가 실행하는 클래스를 스프링 빈으로 등록한다.
      스프링 빈으로 등록한 다음 의존 관계를 주입하게 되는데 @Autowired가 있는 것들을 자동으로 주입하게 된다.

    ※ 생성자 주입은 1개일 때 @Autowired가 없어도 작동이 되는 이유

    스프링이 해당 클래스 객체를 생성하여 빈에 넣어야 하는데 생성할 때 생성자를 부를 수 밖에 없게 된다. 그래서 빈을 등록하면서 의존 관계 주입도 같이 발생하게 되는 것이다.

     

    ✔️ 필드 주입

    필드에 @Autowired를 바로 붙여서 주입하는 방법이다.

     

    ❗️특징

     

    • 코드가 간결해서 예전에 많이 사용된 방식이지만, 외부에서 변경이 불가능하여 테스트하기 힘들다는 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 실제 코드와 상관 없는 특정 테스트를 하고 싶을 때 사용할 수 있다.
    • 하지만 정상적으로 작동되게 하려면 결국 setter가 필요하게 되서 수정자 주입을 사용하는게 더 편리해진다.
    @Service
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private UserRepository userRepository;
        @Autowired
        private DiscountInfo discountInfo;
    }

     

     

    ✔️ 일반 메서드 주입

    일반 메서드를 사용해 주입하는 방법이다.

     

    ❗️특징

    • 한번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 사용되지 않는다.

     

    ✔️ 옵션 처리

    주입할 스프링 빈이 없을 때 동작해야하는 경우 옵션 설정을 통해 처리해주어야 한다.

    • @Autowired만 사용하는 경우 required 옵션 기본값이 true로 사용되어 자동 주입 대상이 없으면 오류가 발생할 수 있다.
    • 스프링 빈을 옵셔널하게 해둔 상태에서 등록이 되지 않고, 기본 로직으로 동작하게 하는 경우가 있다.

     

    ❗️자동 주입 대상 옵션 처리 방법

    • @Autowired(required=false)
      자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않게 된다.
    • org.springframework.lang.@Nullable
      자동 주입할 대상이 없으면 null이 입력된다.
    • Optional<>
      자동 주입할 대상이 없으면 Optional.empty가 입력된다.

     

    ✔️ 생성자 주입을 권장

    과거에는 수정자, 필드 주입을 많이 사용했지만, 최근에는 대부분 생성자 주입 사용을 권장한다.

     

    ❗️생성자 주입을 사용하는 이유

    • 불변
      → 의존 관계 주입은 처음 애플리케이션이 실행될 때 대부분 정해지고 종료 전까지 변경되지 않고 변경되서는 안된다.
       수정자 주입 같은 경우에는 이름 메서드를 public으로 열어두어 변경이 가능하기 때문에 적합하지 않다.
       누군가 실수로 변경할 수도 있고, 애초에 변경하면 안되는 메서드가 변경할 수 있게 설계하는 것은 좋은 방법이 아니다.
       생성자 주입은 객체를 생성할 때 최초로 1번만 호출되고 그 이후에는 다시는 호출되는 일이 없기 때문에 불변하게 설계할 수 있다.
    • 누락
       호출했을 때는 NPE(Null Point Exception)이 발생하는데 의존관계 주입이 누락되었기 때문에 발생한다.
       생성자 주입을 사용하면 주입 데이터 누락 시 컴파일 오류가 발생한다.
    • final 키워드 사용 가능
      → 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
      → 생성자에서 값이 설정되지 않으면 컴파일 시점에서 오류를 확인할 수 있다.
      → java: variable (데이터 이름) might not have been initialized
      → 생성자 주입을 제외한 나머지 주입 방식은 생성자 이후에 호출되는 형태이므로 final 키워드를 사용할 수 없다.
    • 순환 참조
       순환 참조를 방지할 수 있다. (A → B를 참조하고, B → A를 참조)
       개발하다보면 여러 컴포넌트 간에 의존성이 생기게 된다. 
       필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에 애플리케이션이 어떠한 오류와 경고 없이 구동된다. 이는 실제 코드가 호출될 때까지 문제를 알 수 없다. 생성자를 통해 주입하게 되면 BeanCurrentlyInCreationException이 발생하게 된다.

     

    📌 Component 스캔

    스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

    지금까지는 스프링 빈을 등록할 때 자바 코드의 @Bean or XML 등의 설정 정보에 등록할 스프링 빈들을 직접 장성해야 했다. 이렇게 수작업으로 등록하게 되면 설정 정보가 커지고, 누락하는 등 다양한 문제가 발생할 수 있다.

     

    @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록해주기 때문에 설정 정보에 붙여주면 된다. 또한 의존 관계도 자동으로 주입하는 @Autowired 기능도 제공한다.

     

    주의할 점은 @Configuration에도 @Component 애너테이션이 붙어있기 때문에 컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록된다. 만약 기존에 작성한 AppConfig가 있다면 정상적인 작동이 되지 않는다.

     

    AppConfig 등 @Configuration 설정이 된 파일이 있을 시 아래 코드 추가하자.

    @ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))

     

    ✔️ basePackages

    탐색할 패키지의 시작 위치를 지정하고, 해당 패키지로부터 하위 패키지를 모두 탐색한다.

    • @ComponentScan의 매개변수로 basePackages = “”를 줄 수 있다.
      ex) @ComponentScan(basePackages ="cmarket")

    • 지정하지 않으면, @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
      설정 정보 클래스의 위치를 프로젝트 최상단에 두고 패키지 위치는 지정하지 않는 방법이 가장 편하다.

    • 스프링 부트를 사용하면 @SpringBootApplication를 프로젝트 시작 루트 위치에 두는 것을 추천한다.
      @SpringBootApplication 구성요소에 @ComponentScan이 포함되어 있기 때문이다.

     

    ✔️ 컴포넌트 스캔 기본 대상

    아래의 애너테이션들의 소스 코드에는 @Component를 포함하고 있다. 

    • @Component
      컴포넌트 스캔에서 사용된다.
    • @Controller & @RestController
      스프링 MVC 및 REST 전용 컨트롤러에서 사용된다.
    • @Service
      스프링 비즈니스 로직에서 사용된다.
      특별한 처리는 하지 않고 개발자들이 핵심 비즈니스 로직이 여기에 있다는 것을 보여줌으로써 비지니스 계층을 인식하는데 도움을 준다.
    • @Repository
      스프링 데이터 접근 계층에서 사용된다.
      스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
    • @Configuration
      스프링 설정 정보에서 사용된다.
      스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

     

    ✔️ 필터

    • includeFilters
      컴포넌트 스캔 대상을 추가로 지정한다.
    • excludeFilters
      컴포넌트 스캔에서 제외할 대상을 지정한다.

     

    ❗️ FilterType 옵션

    • ANNOTATION
      기본값, 애너테이션으로 인식해서 동작한다.
    • ASSIGNABLE_TYPE
      지정한 타입과 자식 타입을 인식해서 동작한다.
    • ASPECTJ
      AspectJ 패턴을 사용한다.
    • REGEX
      정규 표현식을 나타낸다.
    • CUSTOM
      TypeFilter라는 인터페이스를 구현해서 처리한다.

    댓글