백앤드(Back-End)/Spring Boot

[Spring Boot] 구조 분석 (2) - 스프링 컨테이너

RyanSin 2022. 12. 4. 21:26
반응형

- 지난 시간

안녕하세요. 지난 시간에는 @SpringBootApplication에 대해 알아봤습니다.

 

@SpringBootApplication에 대해 대한 아주 기본적인 용어와 많은 내용을 담고 있기 때문에 꼭 학습하고 오시는 걸 추천드리겠습니다.

[Spring Boot] 구조 분석(1) - @SpringBootApplication 이란?

 

[Spring Boot] 구조 분석(1) - @SpringBootApplication 이란?

- 지난 시간 안녕하세요. 지난 시간에는 아주 간단하게 Spring Boot API 서버를 만들어 봤습니다. 처음 Spring Boot를 접하시는 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다. [Spring Boo

any-ting.tistory.com

 

- 개요

이번 시간에는 말로만 듣던? 스프링 컨테이너에 대해 알아보겠습니다.

 

사실 어려운 개념이라기 보단 프레임워크에 대해서 깊이 있게 알고 싶다면 해당 내용을 아는 게 전 중요하다고 생각합니다.

내가 사용하는 기술에 대해서 겉핥기식 지식은 나중에 자신의 발목을 잡게 된다고 생각해요... 

 

아무튼 ㅎㅎ "알면 좋다" 그리고 너 어디 가서 Spring Boot 사용할 줄 아는구나라고 할 거예요. :)

1. 스프링 컨테이너란

스프링 컨테이너는 클래스 객체의 생명 주기를 관리하는 역할을 합니다.

 

객체의 생명주기?? 말 그대로 Spring Boot 서버가 시작하면 @Bean 어노테이션으로 선언한 객체를 미리 생성하고 소멸시키는 역할을 합니다.

 

여기서 말하는 @Bean은 바로 클래스 객체입니다. 또한 여기서는 Ioc와 DI의 원리가 적용됩니다.

 

Ioc와 DI에 대해 이해하지 못하신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

 

[OOP] 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)

 

[OOP] 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)

- 개요 안녕하세요. 이번 시간에는 제어의 역전 IoC와 의존관계 주입 DI에 대해 알아보겠습니다. 객체 지향 프로그래밍 공부를 하면 반드시 나오는 하나의 개념입니다. 이해하시는데 도움이 되면

any-ting.tistory.com

 

Java라는 언어는 클래스와 그 클래스를 생성한 객체를 다루는 언어입니다. 그렇기 때문에 개발자는 new 연산자를 통해 해당 클래스를 그때그때 생성합니다. 여기서 new 연산자를 통해 클래스를 생성할 때 많은 리소스가 사용됩니다.

 

그렇기 때문에 Spring 프레임워크에서 @Bean으로 설정한 부분을 통해 해당 클래스를 객체화해서 Spring Container에 등록하여 new 연산자를  대신에서 수행합니다.

 

 

위 그림은 AppConfig 클래스를 생성하고 @Configuration 어노테이션을 선언해 해당 클래스가 Spring Container가 관리하도록 설정한 것이다.

 

@Bean에 이름은 메서드 이름을 사용합니다. useRepository라고 메서드를 선언했기 때문에 이름이 useRepository입니다.

또한, 이름을 직접 설정할 수 있습니다.

 

@Bean(name="userMemoryRepository") 이런 식으로 직접 설정할 수 있습니다.

- 주의 사항!! -

Spring Container는 빈 이름을 항상 다르게 설정해야 합니다.

만약 이름이 동일하다면 Spring Container는 이름이 충돌난다고 알려줍니다.

 

지난 시간에 @Configuration 어노테이션에 대해서 설명을 했지만 다시 한번 더 설명하겠습니다.

 

@Configuration 어노테이션은 @Bean을 생성하고 Spring Container에 등록하게 하는 어노테이션입니다.

 

위 그림을 보면 UserMemoryRepository 클래스를 userRepository라는 이름으로 등록되는 걸 알 수 있습니다.

 

그렇다면 진짜 등록이 잘 됐는지 안됐는지 알 수 있는 방법은 무엇일까요? Spring Container가 무엇인지 어떤 코드가 Spring Container를 역할하는지 알아야겠죠?

 

테스트를 해보기 전에 아래 개념을 알아야 됩니다. 그렇기 때문에 개념 먼저 파악하겠습니다.

 

2. BeanFactory와 ApplicationContext

Spring Container는 BeanFactory와 ApplicationContext가 있습니다.

 

사실 실무에서는 실제 등록된 빈을 조회 하거나 조작하지 않기 때문에 어느 정도 이런 개념이 있다고 생각하시면 좋습니다!

 

이 두 인터페이스를 설명하는 이유는 우리가 Spring Boot 프로젝트를 생성했을 때 main 메서드가 실행되는 SpringApplication.run(...)

이라는 코드가 있습니다.

 

SpringApplication 클래스를 확인하면 최상의 주석에 이런 내용이 있습니다.

 

 Class that can be used to bootstrap and launch a Spring application from a Java main method. By default class will perform the following steps to bootstrap your
 
application: Create an appropriate {@link ApplicationContext} instance (depending on your classpath)

Java 주 메서드에서 Spring 응용 프로그램을 부트스트랩 하고 시작하는 데 사용할 수 있는 클래스입니다. 기본적으로 클래스는 부트스트랩을 위해 다음 단계를 수행합니다.

응용 프로그램: 클래스 경로에 따라 적절한 {@link ApplicationContext} 인스턴스 생성

 

즉, 우리가 생성한 서버는 결국 ApplicationContext를 참조해서 사용한다는 걸 알 수 있습니다. 그리고 각각의 @Bean을 등록합니다.

 

BeanFactory는 최상위 인터페이스이며 @Bean을 생성하고 관리합니다. 이 기능을 ApplicationContext는 상속받으며 다른 기능들이 필요하기 때문에 또 다른 인터페이스를 상속받습니다.

 

ApplicationContext

package org.springframework.context;

import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.lang.Nullable;

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
...
}

 

ApplicationContext 다이어그램

 

ApplicationContext 상속 받는 인터페이스

  1. ListableBeanFactory
  2. HierarchicalBeanFactory
  3. MessageSource
  4. EnvironmentCapable
  5. ApplicationEventPublisher
  6. ResourcePatternResolver

기본적으로 @Bean 관련 된 기능인 BeanFactory 인터페이스를 ListableBeanFactory, HierarchicalBeanFactory가 상속받으며

ApplicationEventPublisher는 이벤트를 담당하며 ResourcePatternResolver 설정 정보를 처리합니다.

 

결국 @Bean을 관리하는 기능만 가지고 있지 않고 정말 다양한 기능을 가지고 있다는 것을 알 수 있습니다.

 

 

AppConfig를 반영한 코드 수정

지난 시간에는 @Repository 어노테이션을 선언한 UserRepository 클래스를 만들어서 사용했습니다. 해당 코드를 수정해보겠습니다. 

 

UserRepositoryInterface 생성

사실 해당 내용과 연관은 없지만 습관처럼 추상화와 구현체를 분리하게 되는 것 같습니다. 그렇기 때문에 이 부분은 설명하지 않겠습니다.

 

package com.ryan.spring_boot_rest_api.repository;

import com.ryan.spring_boot_rest_api.domain.User;

import java.util.Collection;

public interface UserRepositoryInterface {
    int save(int id, String name);
    User findByUser(int id);
    Collection<User> findAllByUser();
    User update(int id, String name);
    Boolean delete(int id);
}

 

기본적으로 필요한 메서드를 선언했습니다.

 

UserRepository -> UserMemoryRepository로 변경 및 UserRepositoryInterface 상속

package com.ryan.spring_boot_rest_api.repository;

import com.ryan.spring_boot_rest_api.domain.User;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class UserMemoryRepository implements UserRepositoryInterface{
   ...
}

기존 기능은 그대로 사용하기 때문에 기능적으로는 특별한 부분은 없습니다. 하지만 @Repository 어노테이션을 선언하지 않았다는 부분이 있습니다.

 

@Repository 어노테이션은 @Componet 어노테이션을 선언하고 있기 때문에 스프링 컨테이너에 자동으로 등록됩니다.

하지만 우리는 AppConfig를 따로 만들어 해당 클래스를 스프링 컨테이너에 등록할 예정이기 때문에 제거했습니다.

 

AppConfig 파일 생성

 

config 폴더를 생성하고 AppConfig 파일을 생성합니다.

package com.ryan.spring_boot_rest_api.config;

import com.ryan.spring_boot_rest_api.repository.UserMemoryRepository;
import com.ryan.spring_boot_rest_api.repository.UserRepositoryInterface;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public UserRepositoryInterface userRepository(){
        return new UserMemoryRepository();
    }
}

 

여기서 @Configuraction 어노테이션과 @Bean 어노테이션을 활용해 우리가 방금 생성한 클래스를 추가합니다. 게시글 상단에 이미지와 비슷하죠?.

 

이제 Service 부분을 수정합니다.

 

package com.ryan.spring_boot_rest_api.service;

import com.ryan.spring_boot_rest_api.domain.User;
import com.ryan.spring_boot_rest_api.dto.CreatUserDto;
import com.ryan.spring_boot_rest_api.dto.SuccessResponse;
import com.ryan.spring_boot_rest_api.dto.UpdateUserDto;
import com.ryan.spring_boot_rest_api.repository.UserRepositoryInterface;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepositoryInterface userRepository;

    ...
}

 

private final UserRepository userRepository;으로 선언했던 코드를 private final UserRepositoryInterface userRepository로 변경했습니다.

 

그럼 실제 서버를 실행해 때 기존과 동일하게 동작하는지 확인해보겠습니다.

 

이런 코드를 넣으면 안 되지만 ㅋㅋ 증명하기 위해서 임시로 작성해서 실행하겠습니다.

 

AppConfig 클래스에 임시 코드를 설정합니다.

package com.ryan.spring_boot_rest_api.config;

import com.ryan.spring_boot_rest_api.repository.UserMemoryRepository;
import com.ryan.spring_boot_rest_api.repository.UserRepositoryInterface;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public UserRepositoryInterface userRepository(){

        UserMemoryRepository userMemoryRepository = new UserMemoryRepository();

        System.out.println("정말 등록하나요? =" + userMemoryRepository);
        return userMemoryRepository;
    }
}

서버가 실행될 때 해당 주석을 통해 등록되는 클래스에 메모리 주소를 확인합니다.

 

그리고 실제 호출하는 시점에 해당 클래스가 맞는지 확인합니다.

 

Service 클래스에 임시 코드를 설정합니다.

package com.ryan.spring_boot_rest_api.service;

import com.ryan.spring_boot_rest_api.domain.User;
import com.ryan.spring_boot_rest_api.dto.CreatUserDto;
import com.ryan.spring_boot_rest_api.dto.SuccessResponse;
import com.ryan.spring_boot_rest_api.dto.UpdateUserDto;
import com.ryan.spring_boot_rest_api.repository.UserRepositoryInterface;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepositoryInterface userRepository;
    /**
     * @author Ryan
     * @description 유저 생성 컨트롤러
     *
     * @path /user/create
     *
     * @return User Id
     */
    public SuccessResponse onCreateUser(CreatUserDto creatUserDto){

        System.out.println("동일한 Repository 인가요? = " + userRepository);

        int id = creatUserDto.getId();
        String name = creatUserDto.getName();

        int userId = this.userRepository.save(id, name);

        return new SuccessResponse(true, userId);
    }
    
    ...
    
}

 

그럼 실제 서버를 실행하고 호출해보겠습니다.

 

 

주석을 확인하면 동일한 메모리 주소를 참조하는 걸 알 수 있습니다.

 

Spring Container에 대한 개념은 정말 쉬우면서도 이해하기 좀 어려운 것 같았습니다. 꼭 실습을 통해 학습하시는 걸 추천드리겠습니다.

 

소스 코드

https://github.com/Ryan-Sin/Spring_Boot_Rest_API/tree/v2

 

GitHub - Ryan-Sin/Spring_Boot_Rest_API: Spring Boot RESTful API 프로젝트

Spring Boot RESTful API 프로젝트. Contribute to Ryan-Sin/Spring_Boot_Rest_API development by creating an account on GitHub.

github.com