본문 바로가기

개발기술/Spring

스프링 도입의 핵심 - Application Context, DI/IOC/Bean

 

스프링의 핵심(1) : DI(Dependency Injection) 의존관계주입

  1. 의존성: 한 클래스가 기능을 수행하기 위해 다른 클래스의 메서드나 데이터에 의존하는 관계를 말합니다.
  2. 주입: 의존성이 필요한 객체를 직접 만들지 않고, 생성자, 메소드, 또는 프로퍼티를 통해 외부에서 받는 과정을 말합니다.

  A가 B에 직접적으로 의존한다면 A는 B의 변화에따라 덩달아 변하게된다. 그런 의존성을 A 혹은 B 클래스 코드 내에서 직접 연결시키지않고, Configuration이라는 외부에서 실제구현 객체와 Spring Context객체를 만들어서 연결성(의존성)을 지정(주입)해준다. 

 

  자바개발이 찰흙으로 물건을 만드는 거라면, 스프링은 Container라는 판 위에서 레고로 조립을 하는 것과 같다. bean이라는 규격에 맞춰 클래스를 만들어 주기만 한다면, 스프링이 만들어둔 interface 코드들을 끼워넣어서 사용할 수 있고 서로 유기적으로 작동할 수 있게된다. 

  애플리케이션실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을의존관계 주입이라 한다. 객체 인스턴스를 생성하고,그 참조값을 전달해서 연결된다.

  의존성 주입은 처음에는 XML이라는 별도의 파일에서 클래스들의 정보를 hardcoding하여 각 클래스의 의존성을 설정하였음. 그 후,  XML component scan을 통해서 각 클래스에 달려있는 annotation을 인식하여 의존성을 설정하도록 발전하였음.

스프링의 핵심(2) : IOC 제어의 역전

  심플 자바에서는 구현객체가 스스로 필요한 의존 객체를 생성하여 프로그램의 제어의 흐름을 조정하였다. 그러나 Configuration이라는 객체를 도입함으로써, 각 객체들은 다른 객체를 생성하는 역할은 제외하고 자신의 코드를 실행하는 역할로 변경되었음. 그리고 제어의 흐름은 객체 간의 의존성을 정해주는, 즉, 제어의 흐름을 담당하는 Configuration이라는 객체가 담당한다. 제어의 흐름이 어디에 소재하고있느냐가 프레임워크와 라이브러리의 차이이다. 

  Open/Close 원칙을 위해서 Case에 따라사용 구현체 Class를 별도로 추가하였고, 해당 클래스들의 통일성을 유지하기위해서 Interface를 implement하게 만들었다. interface도입은 코드에 형식의 제약을 가함으로서 통일성을 유지하고 이로 인해 변경과 교체가 쉽도록 하였다. (무한히 자유로운 코드보다는 어느정도 제약이 있는 코드가 좋은 코드임)

  동일한 원리로, 개발자가 모든 코드를 처음부터 만들어 무한한 자율성을 갖는 것보다는, 공통된 개발 로직(http protocol 호출 등)에 대해서는 잘짜여진 프레임워크 코드를 사용하여 제어의 흐름을 개발자 자신에서 프레임워크로 전이시킨다. 

  스프링은 자유도를 주는 것이아니라 제약을 주는 툴이다. 사용자가 직접 class의 객체를 new로 생성하는 것이 아니라 프레임워크가 클래스를 제어하도록 함. 

순수 Java Configuration에서 Spring 도입

https://bsh6226.tistory.com/60

 

스프링 도입배경 - 객체지향적 코드개선

원본 코드 현상황 분석 : 결제서비스(Class1)가 결제를 위해 머니어뎁터(Class2)를 호출해서 사용하는 단순한 형태였는데,  PayMethod라는 parameter 값에 따라서  Class2(머니어뎁터)를 쓰던지 Class3(카

bsh6226.tistory.com

 

 

  스프링 도입전 객체지향원리에 따라 코드개선을 해보았고, 최종적으로 ApplicationConfiguration이라는 설정클래스 도입을 통해 인터페이스에 어떤 구현체를 주입할지 결정을 하는 기능을  분리하였다. 바로 이 ApplicationConfiguration이 스프링의 컨테이너와 동일한 역할을 하는 클래스이며, 이를 스프링 내에서 Application Context라고 부름.

 

IoC 컨테이너, DI 컨테이너

AppConfig 클래스처럼 대상객체 외부에서 객체를 생성,관리하면서 의존관계를 연결해 주는 것을  IoC 컨테이너 또는 DI 컨테이너라 한다.또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다. Spring에서는 ApplicationContext가 해당 역할을 하며 이를 스프링 컨테이너라한다.

 

  기존에는 개발자가 `AppConfig` 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다. Spring은 annotation을 사용하여 해당 메소드가 스프링프레임워크 내에서 어떤 역할을 하는 메소드인지 메타데이터를 알려준다. 스프링 컨테이너는 `@Configuration` 이 붙은 `AppConfig` 를 bean으로 등록하고, 해당 bean이 다른 bean을 생성하는 설정(구성) 정보로 사용한다. config class 내에 `@Bean`이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

  스프링 컨테이너는 applicationContext 라는 인터페이스 변수로 표현하고, 코드를 호출하는 Client 측에서  new applicationContext구현체(Config.class 혹은 설정파일)을 통해서 스프링 개체를 생성한다. 

  즉, 이전에는 개발자가 필요한 객체를 `AppConfig` 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통 해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 `applicationContext.getBean()` 메서드를 사용해서 찾을 수 있다. ApplicationContext라는 레고판(컨테이너)에 레고블럭(빈)들을 형식에 맞추어 생성해주고 있음.

 

 //설정클래스로 context생성
    ApplicationContext applicationContext
            = new AnnotationConfigApplicationContext(ApplicationConfig.class);
// 설정클래스의 메소드를 사용하여 bean을 생성 (interface에 하부 클래스 대입)
    ConveniencePayService conveniencePayService
            = applicationContext.getBean("conveniencePayService",
            ConveniencePayService.class);

스프링 DI설정 방법들

 기존에는 개발자가 `AppConfig` 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.스프링 컨테이너는 `@Configuration` 이 붙은 `AppConfig` 를 설정(구성) 정보로 사용한다. 여기서 `@Bean`이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  ApplicationContext 역시, 인터페이스로 
 코드를 호출하는 Client 측에서, 

 

ApplicationContext applicationContext =
        new  AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService =
        applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService",
        OrderService.class);

 

 

1. XML을 통한 빈등록(과거) 

  모든 클래스의 구조와 위치 이름 등을 하드코딩으로 입력해야하여 불편함. 레거시 코드의 경우 XML이 있을 수 있음. 빈이 300~400개 이상되면 어려움이 많음.

<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="conveniencePayService" class="com.zerobase.convpay.service.ConveniencePayService">
        <constructor-arg name = "paymentInterfaceSet">
            <set>
                <ref bean = "moneyAdapter"/>
                <ref bean = "cardAdapter"/>
            </set>
        </constructor-arg>
        <constructor-arg name = "discountInterface" ref = "discountByConvenience"/>
    </bean>

    <bean id="cardAdapter" class="com.zerobase.convpay.service.CardAdapter"/>
    <bean id="moneyAdapter" class="com.zerobase.convpay.service.MoneyAdapter"/>
    <bean id="discountByConvenience" class="com.zerobase.convpay.service.DiscountByConvenience"/>
    <bean id="discountByPaymethod" class="com.zerobase.convpay.service.DiscountByPaymethod"/>
</beans>

 

2. XML ComponentScan을 통한 빈등록(과거)

ComponentScan방식은 bean으로 instance를 생성하고자하는 class에 @Component를 붙임. 다만, instance 생성시 생성자에 다른 instance가 필요할때 자동으로 @component로 등록된 class instance를 대입하는데, interface변수가 개입되고 구현체가 2개인 경우 에러가난다. 그럴때는 혼동이 없도록 inteface변수를 구현체 이름과 동일하게 하여 혼동을 없앤다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zerobase.convpay"/>
</beans>

 

3. javaConfig를 통한 bean 등록

xml기반에서 벗어나서 javaConfig라는 class를 별도생성하고 여기서 해당 class를 @Configuration으로 표기하고, 클래스내 instance를 생성하는 method들을 @bean로 표기함. 

@bean : 객체를 반환하는 메소드에 표기하여 반환된 객체를 빈으로 등록함

 

4. 의존관계자동주입 :  ComponentScan을 통한 빈 자동 등록(현사용)

 

기존 방식에서는 JavaConfig라는 별도의 클래스를 통해서 bean을 생성하고 의존관계를 명시하는 역할을 했다면, 구현클래스에서 바로 빈을 생성하는 방식으로 @Component와 @ComponentScan방식을 사용한다. 단, applicationContext를 호출하는 방식은 기존과 동일하다. @SpringBootApplication은 @ComponentScan을 포함하고 있기때문에 별도로 붙여주지 않아도 된다.

 

@ComponentScan : 해당 어노테이션이 붙은 클래스는 @Component가 붙은 클래스를 탐색하여 빈으로 등록한다

@Component (or other stereotype annotations like @Service, @Repository, @Controller) : 빈으로 등록할 객체클래스를 표시

 

  Bean으로 등록할 class마다 @Component (or other stereotype annotations like @Service, @Repository, @Controller)으로 마킹을하고 javaConfig라는 class에 @Configuration @ComponentScan으로 Scan할 범위를 설정해주면 된다.

  Configuration class에서 bean을 생성하지 않기때문에 의존관계를 명시하지 않는데 의존관계 명시를 위해서 각 @Component가 붙은 클래스의 생성자에 @Autowired를 통해서 생성자 인자의 타입에 맞는 Bean을 찾아서 의존관계를 맺어준다. (마치 getbean의 역할을 하는 것) 

  여기서 더 나아가, 생성자가 하나만 존재한다면 @Autowired를 생략해도 괜찮고, 필드 변수를 private final로 사용한후 lombok의 @requiredArgsConstructor을 사용하면 생성자를 자동으로 만들어준다.

@Configuration
@ComponentScan(basePackageClasses = ConvpayApplication.class) // package는 해당 package 하위의 클래스를 스캔, clas는 해당 class하위의 클래스를 스캔
public class ApplicationConfig {
}
@SpringBootApplication
public class AccountApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }
}



 

빈의 구현체가 여러개인 경우 주입법

ComponentScan을 완료하면 여러가지 bean이 생성되는데 어떤 빈을 어떤 interface variable에다가 주입하는지 결정을 해주어야한다. getbean이후 후속적으로 연계되어있는 interface 변수에는 variable type에 맞도록 자동적으로 주입되게 되어있음.

 

1. @Primary  : 해당 빈을 최우선으로 주입 ; 빈으로 만들 클래스 위에 위치

2. @Qualifier("beanname") : beanname으로 지정된 빈을 최우선주입; 구현체가 주입되는 변수 위에 위치

3. set 혹은 List로 모두 다 받기

4. 프로퍼티 이름을 빈과 동일하게 하기 : 일반적인 기법 (Type Matching first, Name Matching second)

 

빈과 빈의 연결법

1. final field and @RequiredArgsConstructor : part of the Dependency Injection (DI) pattern and the Constructor Injection strategy. This approach ensures that all required dependencies are provided at the time of object creation, making the class immutable and easier to test.

 

2. autowire : @Autowired is an annotation used to inject dependencies provided by Spring's container into your beans automatically. This annotation can be used on constructor parameters, setters, properties, and even methods with arbitrary names

  In modern versions of Spring, specifically starting with Spring Framework 4.3, you can omit the @Autowired annotation on constructors when a class has only one constructor.

빈의 스코프

일반적으로 하나의 객체를 생성해서 돌려쓰는 싱글톤 방식이지만, 경우에 따라서 bean 내부의 속성값을 별도로 부여해야하는 경우 요청에 따라 생성한다. 매번 요청에 따라 다른 객체가 생서됨.

Prototype :

Request : Session : WebSocket :

 

빈의 환경설정

특정환경에서만 동작하도록 빈을 설정하는 방법이 있음. 인텔리제에서 환경변수를 설정하고 그 환경변수가  @profile("variable") 프로파일의 값과 일치할때만 빈이 동작함.

 

스프링어플리케이션 첫 동작시켜보기 :

1. 처음시작하면 @SpringBootApplication으로 main class에 표기되어있고 그 안의 psvm에서 SpringApplication의 static method run을 실행시킴 

2. SpringApplication.run()은 springapplication을 constructor을 통해서 실행시키며 field 값을 생성함.

3. 함의 : 스프링의 동작을 따라가보면, 스프링이라는 것이 다른게 아니라 매우 복잡하지만 결국은 코드의 흐름으로 이루어진 프로그램임을 알 수 있음. 오류가 발생할때도 스프링의 흐름을 따라가면서 문제를 이해하고 해결할 수 있음. 

4. 스프링 접근법 : 모든 것을 이해하려고하지말고, 필요에 따라서만 선택적으로 공부할 것.