Spring IoC(1) Spring Container, IoC, DI 개념 및 실습

Yeppi's 개발 일기·2022년 6월 5일
0

Spring&SpringBoot

목록 보기
2/16

1. Spring Container

1) 동작

프리로딩

  • lazy-loading
    • 클라이언트의 요청이 들어올 때까지 객체 생성을 늦춤
    • Servlet
  • pre-loading
    • 컨테이너가 생성될 때 XML 에 등록된 클래스의 객체를 생성
    • Filter, Listener

동작 원리

  1. xml 파일을 읽자마자(LOADING) 컨테이너 생성

    • xml 파일로 컨테이너를 조절 및 설정
    • Spring Container는 bean 에 등록된 순서대로 메모리에 띄움
    • <beans> <bean>
  1. 컨테이너가 fruit.red 객체를 생성

  2. 클라이언트가 컨테이너에게 id가 apple 인 객체를 요청

  3. 컨테이너는 id가 applefruit.red 객체를 return

  • 서블릿과 마찬가지로 디폴트 생성자만 호출 할 수 있음
    • 디폴트 생성자는 자바 파일이 컴파일되는 순간, 자동으로 제공
  • Lockup이란?
    • 스프링 컨테이너로부터 객체를 획득하는 것


❤️정리❤️

  • 스프링 컨테이너는(GenericXmlApplicationContext)는 XML에 등록된 객체를 Pre-loading

  • 스프링 컨테이너는 객체를 생성할 때 기본 생성자만 호출

  • 스프링 컨테이너는 XML에 등록된 순서대로 객체를 생성



2) 종류

GenericXmlApplicationContext

  • 파일 시스템이나 클래스 경로에 있는 XML 설정 파일을 로딩하여 구동하는 컨테이너
  • IoC, AOP

XmlWebApplicationContext

  • 웹 기반의 스프링 애플리케이션을 개발할 때 사용하는 컨테이너


3) 파일 생성

  • 소스 폴더 생성
  • 생성 후, 해당 폴더에서 new-other 누른 후

applicationContext.xml

  • 스프링 설정 파일
  • STS : 스프링 전용 개발 도구. 자동완성 가능
<?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 class="패키지명.클래스명"></bean>

</beans>

스프링 컨테이너 생성

  • 개발자가 직접 생성 new → 서블릿 컨테이너보다 좀 더 명확
  • new GenericXmlApplicationContext("applicationContext.xml");

자세한 건 아래 실습을 참고하자



2. Spirng IoC

1) 개념

제어의 역전 =역제어

  • IoC (Inversion of Control)
  • 어플리케이션 운용에 필요한 객체의 생성과 객체들 간의 의존관계를
    자바 코드가 아닌 컨테이너가 모두 처리/관리하는 것
  • GenericXmlApplicationContext 사용
    자바 코드 수정 없이, 실행 객체를 교체할 수 있음

  • 일반적인 Java 객체를 new 로 생성하여 개발자가 관리하는 것 ❌
    객체에 대한 제어를 Spring Container에게 관리를 전부 맡김 ⭕

👉 개발자에서 프레임워크로 제어의 객체 관리 권한이 넘어감
👉 스프링의 가장 큰 특징



2) 순제어

  • 역제어 ↔ 순제어

  • 객체에 대한 제어를 개발자가 자바코드로 일일히 제어

  • 유지보수가 힘듦

  • 전통적인 자바 프로그래밍 방식

  • TV tv = new SamsungTV();



3) 장점

IoC 가 좋은 이유 → 컨테이너가 모든 것을 해줌

  1. xml 을 이용해서 컨테이너를 프리로딩함

  2. xml 파일을 수정하면 실행되는 객체가 변경

    • 자바코드는 수정하지 않음

👉 이때 왜 굳이 JAVA를 사용할까?
👉 IoC 를 제공하기 때문, 유지보수할 때 자바 코드를 수정하지 않음



4) xml 설정 및 태그

네임 스페이스

  • applicationContext.xml
  • 해당 “네임 스페이스” 에 등록된 태그만 쓸 수 있다고 지정

👉 스프링 컨테이너는 약속된 태그만 사용할 수 있다
👉 컨테이너가 해당 태그에 해당하는 클래스를 찾아서 객체를 프리로딩


  • 이제 네임스페이스를 체크하여, 더 다양한 태그를 활용해보자
  • applicationContext.xml 파일에 네임스페이스가 자동으로 추가 되어 사용할 수 있는 context. 태그가 보여짐
        

👉 XML 파일에 새로운 네임스페이스를 추가하는 것
👉 = 스프링 컨테이너에게 전혀 다른 종류의 작업을 지시할 수 있음을 의미한다


  • import
    • applicationContext.xml 하나만 로딩하면? 나머지 3개의 xml 파일도 같이 로딩할 수 있음
    • 사실 잘 안쓰임


<bean>

  • Spring 컨테이너에서 관리되는 객체들
  • 특정 클래스가 new 로 새로 생성된 객체
  • id 는 옵션
  • class 는 필수
  • 별도의 id 를 지정하지 않으면? 자동으로 아래처럼 id 가 부여(#)
    ⇒ 복잡하기 때문에 내가 기억하기 쉬운 id 속성을 지정하는 것이 좋음

  • <bean> 태그에서 id는 생략 가능하다. 하지만 class 속성은 필수❗

  • id 를 통해 생성된 객체를 식별할 수 있어야 하기 때문에
    id 속성 값을 반드시 전체 객체에서 유일해야 한다❗

👉 id 는 유니크해야한다 → 아이디로 객체 구분


  • id 속성 값은 자바의 변수명 규칙을 적용
    • 원래는 변수명 규칙을 적용했는데, spring 버전올라가면서 안해도 됨!
    • 그래도 사용하길 권장 → 기억하기 쉬움으로
      ex. t 0000000000v → 0이 몇 개…?


📌실습📌

  • User.java
// 1. 스프링 컨테이너 생성/구동
GenericXmlApplicationContext container = new GenericXmlApplicationContext("applicationContext.xml"); // 객체 생성되는 시점


// 2. 스프링 컨테이너로부터 id가 "tv"인 TV 타입의 객체를 획득(=Lockup)

TV tv = (TV) container.getBean("tv"); // 컨테이너에게 id가 "tv"인 객체 return해달라고함
tv.powerOn();
tv.volumeUp();
tv.volumeDown();
tv.powerOff();

// 3. 스프링 컨테이너를 종료
container.close();
  • applicationContext.xml
<bean id="tv" class="polymorphism.SamsungTV" init-method="멤버변수초기화" destroy-method="자원해제"></bean>
  • init-method
    • 프리로딩하자마자 “멤버변수초기화” 하도록 지정
    • 서블릿 init() 메서드와 동일
    • 객체가 호출된 직후에
  • destroy-method
    • 객체가 삭제되기 직전에 호출

👉 모든 컨테이너는 컨테이너가 종료되기 직전에
👉 컨테이너가 생성한 객체들을 먼저 메모리에서 제거하는 작업을 수행



init-method 속성

laza-init

  • lazy-init="true"
    특정 객체를 Pre-loading이 아닌 Lazy-loding으로 생성하도록 함
  • lazy-init="false"
    디폴트 값
  • 메모리 낭비 방지 기능
    • true 로 지정한 객체는 메모리에 로딩되지 않음
  • 서블릿 객체는 메모리에 하나만 뜬다
    → 브라우저와 멀티스레드로 동작 = 스프링도 똑같음❗

scope

  • scope="prototype”
    • 특정 객체를 요청(getBean()) 할 때마다 새로운 객체를 생성하여 return
  • scope="singleton”
    • 디폴트 설정 값
    • 하나의 객체만 유지
    • 위 사진에서 주소 값이 동일


3. DI

1) 개념

Dependency Lookup

  • Lookup
  • 위에서 다룬 내용
  • 컨테이너 외부에서 클라이언트가
    컨테이너가 생성한 객체 중 필요한 객체를 찾아달라고 요청

Dependency Injection

의존성 주입

  • 의존 관계
  • 가장 중요한 개념 중 하나
  • 의존적인 객체를 직접 생성 및 관리 ❌
    의존 관계에 있는 두 객체간의 관계를 외부에서 연결 ⭕


2) 장점

  • 의존성으로부터 격리 → 코드 테스트에 용이

  • DI 를 통해, 불가능한 상황을 Mock와 같은 기술로 안정적인 테스트 가능

  • 코드 확장 또는 변경 시 → 영향을 최소화 및 추상화

  • 모듈의 결합도 감소 + 유연성 증가

  • 순환참조 막기 가능

  • 외부에서 내가 사용하는 객체를 주입시켜 주는 것



3) 인젝션 사용하기

의존 관계 시, NullpointerException 발생하는 경우

  • GoogleTV 는 스피커에 의존적
  • GoogleTV.java
    public class GoogleTV extends TV {
    	private SonySpeaker speaker;
    	
    	
    	public GoogleTV() {
    		System.out.println("===> GoogleTV 생성");
    	}
    	
    	public void powerOn() {
    		System.out.println("GoogleTV---전원 키기");
    	}
    
    	public void powerOff() {
    		System.out.println("GoogleTV---전원 끄기");
    	}
    	
    	public void volumeUp() {
    		// speaker = new SonySpeaker(); // 순제어 방식
    		speaker.volumeUp();
    	}
    	
    	public void volumeDown() {
    		speaker.volumeDown();
    	}
    }
  • SonySpeaker.java
    public class SonySpeaker {
    	public SonySpeaker() {
    		System.out.println("===> SonySpeaker 생성");
    	}
    	
    	public void volumeUp() {
    		System.out.println("SonySpeaker---소리 올리기");
    	}
    	
    	public void volumeDown() {
    		System.out.println("SonySpeaker---소리 내리기");
    	}
    }
  • volumeUp(), volumeDown()NullpointerException 발생

NullpointerException 해결하려면?

  • 순제어 방식 사용

    • GoogleTV의 volumeUp(), volumeDown()speaker = new SonySpeaker();
  • 문제점

    1. 메모리에 여러개가 생성
    2. 의존관계가 바뀜

xml 파일에서 constructor-arg 로 해결(아래)


<방법 1> 생성자 인젝션

  • GoogleTV.java
    • 디폴트 생성자가 호출되지 않음
    • Speaker 객체를 매개변수로 받아서 → 멤버변수를 초기화할 수 있음
    • NullpointerException 발생하지 않음!
    public class GoogleTV implements TV {
    	private Speaker speaker;
    	private int price;
    	
    	public GoogleTV() {
    		System.out.println("===> GoogleTV(1) 생성");
    	}
    	
    	public GoogleTV(Speaker speaker) {
    		System.out.println("===> GoogleTV(2) 생성"); // 출력
    		this.speaker = speaker;
    	}
    	
    	. . .
    
    ]
  • applicationContext.xml
    <bean id="speaker" class="polymorphism.SonySpeaker"></bean>
    <bean id="tv" class="polymorphism.GoogleTV">
    			<constructor-arg ref="speaker"></constructor-arg>
    			<constructor-arg value="1500000"></constructor-arg>
    </bean>
  • constructor-arg
    • ref → 생성자인자로 idspeaker 인 객체를 넘기는 것
    • value → 정수, 실수, 문자열같이 고정된 값을 할당 할 때

  • DI 기능을 하는 인터페이스, Extract Interface 만들기
    • Alt+Shift+T
    • 의존성을 주입할 파일에서 우클릭-Refator
  • 자바에서 다형성 적용되는 개념과 동일
    • SonySpeakerAppleSpeakerSpeaker 인터페이스를 상속받음
    • 자바코드 수정 없이 xml 수정만으로 사용 가능
    • Speaker.java
      public interface Speaker {
      	void volumeUp();
      	void volumeDown();
      }

<방법 2> Setter 인젝션

  • 위와 마찬가지로 Extract Interface 로 만들기
  • TV.java
    public interface TV {
    	public void powerOn();
    	public void powerOff();
    	public void volumeUp();
    	public void volumeDown();
    }
  • GoogleTV.java
    public class GoogleTV implements TV {
    	. . .
    	
    	// setter 인젝션
    	public GoogleTV(Speaker speaker, int price) {
    		System.out.println("===> GoogleTV(3) 생성");
    		this.speaker = speaker;
    		this.price = price;
    	}
    
    	public void setSpeaker(Speaker speaker) {
    		System.out.println("---> setSpeaker() 호출");
    		this.speaker = speaker;
    	}
    
    	public void setPrice(int price) {
    		System.out.println("---> setPrice() 호출");
    		this.price = price;
    	}
    
    	. . .
    }
  • applicationContext.xml
    <!-- Setter 인젝션 설정 -->	
    		<bean id="speaker" class="polymorphism.AppleSpeaker"></bean>
    		<bean id="tv" class="polymorphism.GoogleTV">
    			<property name="speaker" ref="speaker"></property>
    			<property name="price" value="100000"></property>
    		</bean>
  • 출력결과

p 네임스페이스 사용

  • 아래 두 코드의 결과는 동일
  • 기존 setter
    <bean id="speaker" class="polymorphism.AppleSpeaker"></bean>
    		<bean id="tv" class="polymorphism.GoogleTV">
    			<property name="speaker" ref="speaker"></property>
    			<property name="price" value="100000"></property>
    		</bean>
  • p 사용
    <bean id="speaker" class="polymorphism.SonySpeaker"></bean>
    <bean id="tv" class="polymorphism.GoogleTV" p:speaker-ref="speaker" p:price="100000"></bean>
    • 간결
    • 가독성은 떨어짐

🍑 생성자 인젝션 VS 세터 인젝션 🍑

  • 생성자 인젝션
    <constructor-arg> 태그를 이용하여 생성자를 호출

  • 세터 인젝션
    <property> 태그를 이용하여 세터 메소드를 호출

  • 실제 개발 시, 세터만 많이 사용
  • 생성자 인젝션을 많이 사용하지 않는 이유
    • 관리하기 힘들기 때문
    • 매번 매개변수 초기화를 위한 새로운 생성자를 만들기 힘듦


4. 📌실습📌

1) 일반적인 encoding

  • 기본적인 인코딩방식 : url 인코딩 방식

  • 아래 코드처럼 필요할 때마다 계속해서 추가하면서 사용해야함
    ⇒ 매우 불편한 방식, 아래 실습에서 분리시켜보자

		String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

        // Base 64 encoding
        Encoder encoder = new Encoder();
        String result = encoder.encode(url);
        System.out.println(result);
        
        // url encoding
        UrlEncoder urlEncoder = new UrlEncoder();
        String urlResult = urlEncoder.encode(url);
        System.out.println(urlResult);


2) 추상화를 적용시킨 encoding

  • Base64Encoder.java, UrlEncoder.java 의 공통적인 부분을
    IEncoder.java 인터페이스로 추상화시켜 사용
  • Encoder.java 내부에 의존성이 있는 객체(현. 인터페이스 IEncoder)를 받을 변수를 만들고,
    객체들을 처리하는 로직을 작성
  • main 에서 Encoder 객체를 생성하고, 필요한 인코딩 방식을 적용

👉 즉 Encoder 내부를 직접적으로 변경하지 않고, 외부(현. main)에서 주입하는 방식
👉 가독성, 편리성, 효율성 증가


  • Base64Encoder.java
public class Base64Encoder implements IEncoder{
    public String encode(String message) {
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}
  • UrlEncoder.java
public class UrlEncoder implements IEncoder{
    public String encode(String message) {
        try {
            return URLEncoder.encode(message, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 인터페이스 IEncoder.java
public interface IEncoder {
    String encode(String message);
}
  • DI 역할을 하는 Encoder.java
public class Encoder {
    private IEncoder iEncoder**; // 의존성이 있는 객체
    public Encoder(IEncoder iEncoder) { // 의존성이 있는 객체를 주입 받아서 처리(DI)
        // this.iEncoder = new Base64Encoder();
        // this.iEncoder = new UrlEncoder();

        // DI
        this.iEncoder = iEncoder;
    }
    public String encode(String message) {
        return iEncoder.encode(message);
    }
}
  • main.java
String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

// 추상화
// new Encoder(외부에서 주입)
// Encoder 내부는 변경 안함

// Base 64 encoding
Encoder encoder = new Encoder(new Base64Encoder()); 
String result = encoder.encode(url);
System.out.println(result);

// url encoding
Encoder urlEncoder = new Encoder(new UrlEncoder());
String result2 = urlEncoder.encode(url);
System.out.println(result2);
  • 출력결과


3) DI + IoC 활용

어노테이션 개념 및 은 Spring 시리즈 3번 을 참고해주세요

Base64, URL

  • Base64Encoder, UrlEncoder 파일에 @Component 를 붙여서, 제어의 역전이 일어나도록 함
// Spring 에 객체로 관리해달라는 요청/권한 넘김 -> Bean으로 만들어서 관리
@Component
public class Base64Encoder implements IEncoder{
	. . .
}
// Spring 에 객체로 관리해달라는 요청/권한 넘김 -> Bean으로 만들어서 관리
@Component
public class UrlEncoder implements IEncoder{
	. . .
}

  • ApplicationContextProvider.java
    • text를 읽어들여서 주입시키는 클래스를 생성
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextProvider  implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // (ApplicationContext applicationContext) 알아서 주입
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
        return context;
    }
}

  • Encoder.java
    • Spring 으로 객체관리를 넘길 때, 객체를 주입 받을 수 있는 장소를 생성
    • set 메서드로 진행해야함
.
.
.
		// Spring 을 주입 받을 수 있는 장소 -> 변수 생성자, setMethod
    public void setIEncoder(IEncoder iEncoder) {
        this.iEncoder = iEncoder;
    }

  • main
SpringApplication.run(SpringIocApplication.class, args);
ApplicationContext context = ApplicationContextProvider.getContext();

Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);

Encoder encoder = new Encoder(base64Encoder);
String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

String result = encoder.encode(url);
System.out.println(result);

encoder.setIEncoder(urlEncoder);
result = encoder.encode(url);
System.out.println(result);
  • 실행결과


Encdoer에도 IoC 적용

  • @Component 가 위 실습에서 이미 적용되어 있는 상태에서, Encoder에도 적용하려면?

    그러면 여러개의 @Component 가 만들어 지기 때문에
    Spring은 어떤 컴포넌트를 매칭해야 할 지 결정 못함
  • @Qualifier("urlEncoder") 로 어떤 컴포넌트를 쓸지 지정해주면 됨
    • 클래스의 앞글자를 소문자로 바꿔서

  • 만약 클래스의 이름을 따로 지정해서 사용하고 싶다면?
    • Encoder.java

      public Encoder(@Qualifier("**base74Encoder**") IEncoder iEncoder) { . . . }
    • Base64Encoder.java

      **@Component**("**base74Encoder**")
      public class Base64Encoder implements IEncoder{ . . . }

  • main
SpringApplication.run(SpringIocApplication.class, args);
ApplicationContext context = ApplicationContextProvider.getContext();

Encoder encoder = context.**getBean(Encoder.class)**;
String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);
  • 출력결과
  • Base64Encoder.java의 내용이 잘 출력
d3d3Lm5hdmVyLmNvbS9ib29rcy9pdD9wYWdlPTEwJnNpemU9MjAmbmFtZT1zcHJpbmctYm9vdA==


4) Bean 사용

Bean 확인

  • main 클래스에서 해당 아이콘을 찾아보자(현. intellij)
  • 현재 Bean 목록을 확인할 수 있다


여러개 Bean 등록

  • Encoder.java 를 @Component 로 등록하지않고, Bean 으로 직접 등록하는 방법
// @Configuration -> 한 클래스에서 여러개 Bean 등록
@Configuration
class AppConfig{

    // Base64Encoder 를 Spring 으로부터 주입받기
    @Bean("base64Encode")
    public Encoder encoder(Base64Encoder base64Encoder) {
        return new Encoder(base64Encoder);
    }

    // UrlEncoder 를 Spring 으로부터 주입받기
    @Bean("urlEncode")
    public Encoder encoder(UrlEncoder urlEncoder) {
        return new Encoder(urlEncoder);
    }
}
  • main
// bean 여러개 등록된 클래스 불러오기
Encoder encoder = context.getBean("urlEncode", Encoder.class);
String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);


❤️ 정리 ❤️

  • IoC
    Spring 컨테이너가 관리하는 것

  • DI
    객체를 주입받는 것

  • 실제 프로젝트에서
    객체(클래스)를 새로 만들지 않고, Spring의 context를 가져오는 형태로 사용

  • 생성자, setMethod 등으로 직접 객체를 받아와서 사용

profile
imaginative and free developer. 백엔드 / UX / DATA / 기획에 관심있지만 고양이는 없는 예비 개발자👋

0개의 댓글