<< 학습 목표 >>

1. XX

2. XX


이번에는 코드를 더 발전시켜보자

프로젝트 -> com.study.chapter03 -> Ex05 소스 파일을 추가하고 아래 코드를 추가하자

package com.study.chapter03;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Ex05 {
	
	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone = (GalaxyS23) ctx.getBean("galaxyS23");
		
		System.out.println("<< phone >>");
		phone.printBatteryInfo();
	}

}

 

그리고 실행시켜보면 문구가 이상하다는걸 알 수 있음

스마트폰의 배터리를 다 썼다고해서 교체하는 사람은 없을 것

스마트폰의 배터리를 다 쓰면 충전하는게 일반적

 

우리가 만든 스마트폰은 베터리가 건전지였을 때는 베터리의 정보를 출력하면 "교체하세요." 문구가 적절했지만 이제 리튬 베터리로 바뀌어서 "교체하세요." 대신 "충전하세요." 로 문구를 바꿔야함

 

스마트폰 베터리는 나중에라도 더 효율적인 베터리로 업그레이드가 될 가능성이 있으므로 가장 적은 노력으로 문구를 바꾸는 방법은 자바에서 배운 클래스, 인터페이스, 상속 등을 활용하는 것

다형성, 클래스, 인터페이스에 대한건 기초인 자바에서 배우는 것들이므로 여기서는 설명하진 않겠음

 

프로젝트 -> com.study.chapter03 -> BatteryType 인터페이스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public interface BatteryType {
	void printBatteryInfo();
}

 

프로젝트 -> com.study.chapter03 -> NormalBattery 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class NormalBattery implements BatteryType {

	@Override
	public void printBatteryInfo() {
		System.out.println("배터리는 건전지입니다.");
		System.out.println("배터리를 모두 사용했다면 건전지를 교체하세요.");
	}

}

 

프로젝트 -> com.study.chapter03 -> LithiumBattery 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class LithiumBattery implements BatteryType {

	@Override
	public void printBatteryInfo() {
		System.out.println("배터리는 리튬 배터리입니다.");
		System.out.println("배터리를 모두 사용했다면 배터리를 충전하세요.");
	}

}

 

BatteryType 인터페이스는 베터리가 되기 위한 클래스는 반드시 구현해야하는 인터페이스로 정의했음

이 인터페이스를 통해 배터리에 다형성을 적용할 수 있게 됐음

그 후 건전지를 표현한 NormalBattery 클래스와 리튬 배터리를 표현한 LithiumBattery 클래스를 선언했음

NormalBattery 클래스는 건전지에 맞는 배터리 정보를 출력하도록 printBatteryInfo 메서드를 구현했음

LithiumBattery 클래스는 리튬 배터리에 맞는 배터리 정보를 출력하도록 printBatteryInfo 메서드를 구현했음

 

 

이제 각 스마트폰에서는 문자열 대신 인터페이스를 활용해 다형성이 적용된 베터리 타입을 갖고(1) 베터리 유형에 맞는 정보가 출력(2)되도록 했음

 

스마트폰을 생성하고 의존 주입을 해주는 Config 컨테이너의 코드도 상황에 맞게 바꾸자

여기서는 우선 스마트폰의 베터리를 건전지로 지정했음

package com.study.chapter03;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {
	private BatteryType batteryType = new NormalBattery();
	
	@Bean
	public GalaxyS23 galaxyS23() {
		return new GalaxyS23(batteryType);
	}
	
	@Bean
	public GalaxyFlip4 galaxyFlip4() {
		return new GalaxyFlip4(batteryType);
	}
	
	@Bean
	public IPhone14 iPhone14() {
		return new IPhone14(batteryType);
	}
}

<< 코드 설명 >>

(1). 스마트폰이 의존 하고 있는 의존 객체 생성 / 이때 의존 객체로 건전지 정보를 갖고 있는 배터리를 생성했음

(2), (3), (4). 건전지 정보가 주입 된 빈을 컨테이너에 등록

 

 

다시 Ex05 소스 파일로 돌아가 프로그램을 실행시켜보자

당연히 "건전지를 교체하세요" 문구가 뜸

package com.study.chapter03;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Ex05 {

	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone = (GalaxyS23) ctx.getBean("galaxyS23");
		
		System.out.println("<< phone >>");
		phone.printBatteryInfo();
	}

}

 

 

스마트폰의 배터리를 리튬 배터리로 교체해야한다면 잘 알고 있듯 이와 같이 배터리를 교체하면 됨

 

지금까지 @Configuration 애너테이션과 @Bean 애너테이션을 사용해 Spring Framework가 제공하는 DI, IoC를 사용했음

@Configuration 애너테이션을 사용해 컨테이너를 생성하고 @Bean 애너테이션을 사용해 프로젝트에 필요한 빈 객체를 등록했음

 

특히, 컨테이너에 의존 객체를 생성하도록 직접 코드를 썼고 의존 주입이 된 빈을 등록하도록 직접 코드를 썼음 


Spring Framework의 장점 중 하나는 직접 입력해야 할 코드를 애너테이션으로 대체할 수 있다는 점

 

현재 우리는 세 종류의 스마트폰(GalaxyS23, GalaxyFlip4, IPhone14)를 컨테이너에 빈으로 등록하고 있는데 이렇게 컨테이너에 빈으로 등록하고 싶은 클래스가 있다면 클래스 명 위에 @Component 애너테이션(1)을 붙이면 더 간편하게 빈으로 등록할 수 있음

package com.study.chapter03;

import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@Data
public class GalaxyS23 {
	private BatteryType batteryType;
	
	public GalaxyS23(BatteryType batteryType) {
		this.batteryType = batteryType;
	}

	public void printBatteryInfo() {
		batteryType.printBatteryInfo();
	}
}

<< 코드 설명 >>

클래스 명 위에 @Component 애너테이션을 붙이면 컨테이너에 빈 객체를 등록할 필요가 없어짐

@Component 애너테이션은 클래스 명 위에 붙이는 것으로 클래스명 위에 만 있으면 되므로 @Data 애너테이션 위이든 아래이든 상관 없음

 

GalaxyFlip4와 IPhone14 클래스에도 @Component 애너테이션을 붙이자

 

@Component 애너테이션을 붙이면 컨테이너에 빈이 등록됨

이때 빈의 이름은 클래스 이름을 따라감

 

 

여기서 드는 의문!

"빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)은 BatteryType에 의존하고 있는데 의존 주입은 어떻게 되는거지?"

그렇다. 빈 객체에 의존 객체가 있다면 의존 객체의 클래스명 위에도 @Component 애너테이션을 붙여줘야함

 

 

이번에는 스마트폰이 의존하고 있는 의존 객체에도 @Component 애너테이션을 붙이자

 

의존 객체에 @Component 애너테이션을 사용하면 의존 주입을 하기 위해 의존 객체를 생성할 필요가 없어짐

여기서 각 스마트폰에 사용한 @Component 애너테이션과 각 베터리에 사용한 @Component 애너테이션이 다름

(1). 각 스마트폰에 사용한 @Component 애너테이션은 ( ) 가 없었음

(2). 각 베터리에 사용한 @Component 애너테이션은 ( ) 안에 문자열이 들어있음

 

 

@Component 애너테이션에 ( ) 가 없으면 해당 빈을 컨테이너에 등록할 때 클래스 이름이 빈의 이름이 됨

@Component 애너테이션의 ( ) 안에 문자열이 있으면 해당 빈을 컨테이너에 등록할 때 지정한 문자열이 빈의 이름이 됨

 

(1)의 빈 색깔과 (2)의 빈 색깔이 다른 이유는 컨테이너 입장에서는 모두 빈이지만 개발자 입장에서는 GalaxyS23, GalaxyFlip4, IPhone14는 필요한 곳에서 꺼내서 쓰기 위한 빈이고 nb, lb 빈은 GalaxyS23, GalaxyFlip4, IPhone14이 의존하고 있는 의존 객체이기 때문에 색깔을 다르게 지정했음

 

 

우선 여기까지 빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)이 주입 받아야할 의존 객체를 생성했음

빈 객체들에 의존 주입을 받으려면 빈 객체들이 갖고 있는 의존 객체에 @Autowired 애너테이션을 붙어야함

package com.study.chapter03;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@Data
public class GalaxyS23 {
	@Autowired
	@Qualifier("nb")
	private BatteryType batteryType;
	
	public void printBatteryInfo() {
		batteryType.printBatteryInfo();
	}
}

 

잠깐! 이 코드에는 굉장히 중요한 점이 있음!

지금까지 빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)이 생성자를 갖고 있었던 이유는 우리(개발자)가 직접 의존 주입을 하기 위해서였음

이제는 우리가 직접 의존 주입을 하지 않고 애너테이션을 통해서 Spring Framework가 알아서 의존 주입을 하도록 할 것이므로 생성자 방식 또는 setter 방식의 의존 주입 코드 는 지워야함

 

@Qualifier 애너테이션은 의존 주입 받을 빈의 이름임

따라서 GalaxyS23은 배터리가 건전지임

 

나머지 빈 객체는 리튬 배터리로 의존 주입을 하자

 

여기까지 컨테이너에 코드 한 줄 안 쓰고 @Component, @Autowired, @Qualifer 애너테이션으로 빈 객체를 등록하고 의존 주입을 했음

 

 

현재 상황에서 빈 객체를 갖고 있는 컨테이너는 누굴까?

그 컨테이너는 바로 우리가 프로젝트를 만들었을 때 자동으로 들어있던 코드 중 하나임

 

지금은 시간이 꽤 흘러 기억이 안나겠지만 우리가 처음 프로젝트를 만들었을 때부터 있던 코드가 있음

그 코드는 com.study 패키지 안에 들어있음

 

이 중에서 (프로젝트명)Application.java 인 StudyProjectApplication.java 소스 파일을 보자

 

이 소스 파일에는 굉장히 중요한 @SpringBootApplication 애너테이션이 있음

이 애너테이션은 여러 애너테이션이 하나로 합쳐진 애너테이션임

 

애너테이션 위에 마우스를 올려두면 어떤 애너테이션들이 하나로 합쳐졌는지 알 수 있음

 

이 애너테이션들은 또 다시 여러 애너테이션이 하나로 합쳐진 애너테이션임

즉, @SpringBootApplication 애너테이션은 굉장히 많은 애너테이션이 하나로 합쳐진 애너테이션임

합쳐진 애너테이션 중 @Configuration, @EnableAutoConfiguration, @ComponentScan 애너테이션이 있는데 이 애너테이션이 컨테이너를 생성하고 컨테이너에 의존 주입된 빈 객체를 등록하는 역할을 함

 

하나 하나 자세히 설명하면 또 다른 얘기로 많이 넘어가야하므로 Spring Boot 난이도에 맞춰서 대략적으로만 이해하고 있자

( 하나 하나 자세히 이해하고 싶다면 Spring Framework 를 공부해야함 )

@Configuration - Confing 컨테이너에 붙였던 애너테이션으로 컨테이너를 생성할 때 사용하는 애너테이션

@ComponentScan - 컨테이너에 등록할 빈 객체를 자동으로 찾아주는 애너테이션으로 @Component 애너테이션이 붙은 클래스를 컨테이너에 빈 객체로 등록함

@EnabledAutoConfiguration - 생성된 Config 컨테이너를 불러와 ctx에 저장해 사용했는데 이 애너테이션을 사용하면 컨테이너를 불러 올 필요 없이 컨테이너 내 빈 객체를 꺼낼 수 있음

 

 

@SpringBootApplication 애너테이션 덕분에 @Component, @Autowired, @Qualifer 애너테이션으로 빈 객체를 등록하고 의존 주입을 했다면 컨테이너를 만들 필요 없이 빈 객체를 꺼낼 수 있음

 

빈 객체를 꺼내보기 전에 빈 객체의 의존 주입 방식이 완전히 바뀌었기도 하고 Config 컨테이너는 필요 없으므로 Config 클래스는 삭제하자

(1). 컨테이너를 생성할 때 사용할 설정 클래스인 Config 클래스를 삭제했으므로 더 이상 컨테이너를 직접 생성할 수 없음

       이 코드도 삭제 해야함

(2). 빈 객체 생성과 의존 주입을 우리가 직접 한게 아니라 애너테이션을 사용해서 Spring Framework가 대신 해주도록 했으므로 더 이상 main 메서드 안에서는 빈 객체를 사용할 수 없음

main 메서드 이기 때문이 아니라 main 메서드가 static 메서드이기 때문임

 

이제 main 의 역할을 대신할 컨트롤러를 만들어야함

프로젝트 -> com.study.chapter03 -> Ex06 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Ex06 {
	@Autowired
	private GalaxyS23 phone1;
	@Autowired
	private GalaxyFlip4 phone2;
	@Autowired
	private IPhone14 phone3;
	
	@GetMapping("/chapter03/diNioc/phone1")
	public void diAndIocTest1() {
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
	}
	
	@GetMapping("/chapter03/diNioc/phone2")
	public void diAndIocTest2() {
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
	}
	
	@GetMapping("/chapter03/diNioc/phone3")
	public void diAndIocTest3() {
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
	}
}

<< 코드 설명 >>

(1). @Controller 애너테이션이 붙은 클래스는 SpringFramework가 컨테이너로 만들어줌

즉, 컨테이너도 우리가 직접 생성하는게 아니라 애너테이션으로 생성한 것

앞에서는 우리가 직접 설정 클래스를 사용해서 ctx 라는 컨테이너를 생성했지만 이제는 @Controller 애너테이션 한 줄을 통해서 SpringFramework가 대신 컨테이너를 생성한 것

(2). 컨테이너에 빈 객체 등록

컨테이너에 빈 객체를 등록할 때도 @Autowired 애너테이션을 붙임

(3). GET 방식으로 http://서버주소:포트번호/chapter03/diNioc/phone1 경로에 접근했을 때는 diAndIocTest1 메서드가 호출되도록 diAndIocTest1 메서드에 GetMapping 애너테이션을 지정했음

diAndIocTest1 메서드가 호출되면 sts 내 콘솔창에는

 

<< phone1 >>

배터리는 건전지입니다.

배터리를 모두 사용했다면 건전지를 교체하세요.

 

가 출력될 것

 

이제 sts의 [ Boot Dashboard ] 에서 서버를 실행 시키고 컨트롤러를 호출해보자

 

여기서 URL에 사용한 diNioc는 DI와 IoC임


이렇게 Spring Framework는 컨트롤러가 컨테이너의 역할을 하며 사용자의 요청을 받고 사용자의 요청에 맞는 서비스 메서드를 호출함

 

서비스 메서드에는 DAO에 의존해 DB와 통신할 때 DAO를 통해 통신함

 

여기까지 Spring Framework에서 굉장히 중요한 DI, IoC 에 대해서 배웠음

DI, IoC는 굉장히 중요한 요소이므로 여러번 복습해서 꼭 내것으로 만들어야함

728x90
LIST

<< 학습 목표 >>

1. 컨테이너를 생성하고 빈 객체를 등록할 수 있다.

2. 의존 주입을 한 빈 객체를 컨테이너에 등록할 수 있다.

3. 컨테이너의 생성 과정을 설명할 수 있다.


전 글 ( https://codingaja.tistory.com/111 ) 에서 의존, 의존 주입을 배웠고 의존 주입을 대신 해주는 요소에 대해 배웠음

그러면서 우리가 직접 자바만 사용해 의존 주입을 대신 해주는 요소(SmartPhoneFactory)를 만들었음


Spring Framework의 경우 xml을 이용한 의존 주입, 직접 의존 주입, 애너테이션을 이용한 의존 주입 / 이렇게 3가지 의존 주입 기법을 제공함

 

특히, 우리처럼 Spring Boot 로 생성한 Spring Framework 프로젝트의 경우 일반적으로는 애너테이션을 이용한 의존 주입을 하도록 권장하고 있음


이번에는 Spring Framework가 제공하는 의존 주입 기법 중 직접 의존 주입에 대해서 알아보자

SPring Framework가 권장하는 방식인 애너테이션을 이용한 의존 주입 방법은 다음 글 ( ) 에서 알아 볼 예정


<< 직접 의존 주입 하는 방법 >>

전 글의 SmartPhoneFactory 역할을 할 Configuration 클래스가 필요함

프로젝트 -> com.study.chapter03 -> Config 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {
	private String batteryType = "리튬 폴리머 배터리";
	
	@Bean
	public GalaxyS23 galaxyS23() {
		return new GalaxyS23(batteryType);
	}
	
	@Bean
	public GalaxyFlip4 galaxyFlip4() {
		return new GalaxyFlip4(batteryType);
	}
	
	@Bean
	public IPhone14 iPhone14() {
		return new IPhone14(batteryType);
	}
}

<< 코드 설명 >>

왼쪽에 SmartPhoneFactory 는 자바만 사용해 IoC, DI 를 적용한 클래스이고 왼쪽에 Config 는 Spring Framework가 제공하는 IoC, DI 를 적용한 클래스임

 

같은 역할을 하는 두 클래스의 코드를 비교 해보자

(1). 의존 주입을 하기 위한 공통 분모 생성

(2), (3), (4). 객체를 생성하며 의존 주입 후 반환

 

여기서 사용된 @Configuration 애너테이션과 @Bean 애너테이션을 분석해보자

 

@Configuration 애너테이션은 빈(Bean) 들을 생성해 가지고 있고 이 Bean들이 필요한 곳에 제공을 해주는 클래스에 애너테이션을 붙임

@Configuration 애너테이션이 붙은 클래스를 컨테이너(Container) 라고 부름

실행활에서 컨테이너를 직접 보지못했어도 어떤 역할을 하는지 충분히 알고 있을 것

컨테이너는 물건들을 옮길 때 사용하는 것으로 컨테이너에 물건들을 넣고 뺄 수 있음

 

Spring Framework의 컨테이너( @Configuration 애너테이션이 붙은 클래스 ) 도 이와 같은 역할을 함

Spring Framework의 컨테이너에 넣고 빼는 물건을 빈(Bean) 이라고 부름

 

빈(Bean)은 자바에서 객체, 인스턴스로 부르는 것으로 SmartPhoneFactory 클래스에서 각 if문이 return 해주는 인스턴스를 SpringFramework의 용어로 빈(Bean)이라고 부름

 

이 빈들을 Spring Framework의 컨테이너에 넣어두면 빈이 필요한 곳에서 컨테이너에 들어있는 빈을 필요할 때 꺼낼 수 있음

Spring Framework의 컨테이너에 빈을 넣어두려면 @Bean 애너테이션이 붙은 메서드가 필요한대 아래 (1), (2), (3) 메서드가 컨테이너에 빈을 넣어두는 @Bean 애너테이션이 붙은 메서드임

 

Config 클래스에 @Configuration 애너테이션이 붙어있으므로 Config 클래스는 컨테이너가 됨

그리고 Config 컨테이너에 @Bean 애너테이션이 붙은 메서드를 선언했으므로 메서드가 return 해주는 빈들이 Config 컨테이너에 등록되는데 이때 빈의 이름은 메서드이름이 됨

 

이제 빈이 필요한 곳에서 Config 컨테이너를 불러온 다음 빈의 이름을 사용해 각 빈을 꺼낼 수 있음

 

 

프로젝트 -> com.study.chapter03 -> Ex02 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Ex02 {
	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone1 = (GalaxyS23) ctx.getBean("galaxyS23");
		GalaxyS23 phone2 = (GalaxyS23) ctx.getBean("galaxyS23");
		GalaxyFlip4 phone3 = (GalaxyFlip4) ctx.getBean("galaxyFlip4");
		IPhone14 phone4 = (IPhone14) ctx.getBean("iPhone14");
		IPhone14 phone5 = (IPhone14) ctx.getBean("iPhone14");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}
}

<< 코드 설명 >>

(1). Config 컨테이너를 불러와 ctx 객체에 저장

(2). Config 컨테이너에 있는 빈을 꺼내 각 객체에 저장

  이때 빈의 이름은 메서드 이름이라는 점을 기억하자

  또한 getBean 메서드로 어떤 빈을 꺼낼지 모르기 때문에 getBean 메서드는 반환 타입이 Object 라는점

  그래서 getBean으로 빈을 꺼낸 다음 적절한 형태로 형변환해야함


우리가 직접 만든 컨테이너인 SmartPhoneFactory 컨테이너와 Spring Framework가 제공하는 컨테이너의 사용 방식을 보면 다를게 없음

SmartPhoneFactory는 컨테이너를 흉내낸 우리가 직접 만든 컨테이너로 실무에서 활용할 수 있는 수준의 컨테이너가 되려면 굉장히 많은 기반 지식과 개발 경험이 있어야함

SmartPhoneFactory 처럼 이렇게 개발에 필요한 부분을 처음부터 끝까지 다 만들려면 상당한 개발 지식, 경험이 필요하고  개발자가 관리해야할 코드가 많아지고 개발 기간이 상당히 길어짐

또한 그것을 직접 만든 개발자가 퇴사를 하거나 어떤 일로 자리를 비우면 프로젝트에 마비가 올 것

 

프레임워크를 사용하면 개발에 필요한 상당 부분을 프레임워크의 도움을 받을 수 있고 개발 기간이 단축될 수 있음

또한 특정한 개발자가 직접 만든게 아니기 때문에 프레임워크가 제공하는 것들을 사용해 개발했다면 개발자가 퇴사를 하거나 어떤 일로 자리를 비우더라도 프레임워크를 알고 있는 다른 개발자가 대신 할 수 있으므로 프로젝트에 마비가 오지 않음


이제 Spring Framework가 제공하는 컨테이너에 대해서 좀 더 깊게 들어가보자

 

다시 한번 우리가 직접 만든 컨테이너와 Spring Framework가 제공하는 컨테이너를 비교해보자

아래와 같이 두 방식 모두 두 대의 핸드폰을 생성했고 각 핸드폰의 배터리 타입을 바꿨음

위와 같이 프로젝트 -> com.study.chapter03 -> Ex03 소스 파일을 추가하고 위에서 왼쪽에 있는 코드를 입력하자

또한 프로젝트 -> com.study.chapter03 -> Ex04 소스 파일을 추가하고 위에서 오른쪽에 있는 코드를 입력하자

 

 

우리가 직접 만든 컨테이너를 사용한 코드에서는 우리가 알고 있는대로 각 객체의 배터리 타입을 바꿨으므로 phone1, phone2의 배터리 타입이 다르게 출력됨

 

 

그러나 Spring Framework가 제공하는 컨테이너를 사용한 코드에서는 우리가 알고 있는 것과 다르게 두 핸드폰의 배터리 타입이 모두 리튬 폴리머 배터리로 바꼈음

 

왜이럴까??

이는 Spring Framework 컨테이너의 생성 과정과 동작 과정을 보면 어렵지 않게 이해할 수 있음

 

Spring Framework 프로젝트를 실행시키면 @Configuration 애너테이션이 붙은 컨테이너를 생성함

컨테이너의 이름은 @Configuration 애너테이션이 붙은 클래스의 이름을 따라감

 

 

바로 이어서 빈들(galaxyS23, galaxyFlip4, iPhone14)이 의존하고 있는 의존 객체인 batteryType을 생성함

 

 

그 후 @Bean 애너테이션이 붙은 메서드들이 차례대로 호출됨

 

메서드들이 호출되면서 컨테이너에 빈 객체가 등록되는데 메서드의 이름이 빈 객체의 이름이 됨

 

빈은 프로젝트에 필요한 인스턴스를 갖고 있는데 그 인스턴스는 메서드가 반환해주는 인스턴스임

메서드가 인스턴스를 생성할 때 우리가 지정한 방식인 생성자를 통해 의존 주입을 하고 반환함

따라서 빈은 의존 주입이 된 프로젝트에 필요한 인스턴스를 갖고 있음

 

나머지 빈 객체들도 마찬가지로 의존 주입이 된 인스턴스를 갖고 있음

 

이렇게 의존 주입이 된 빈 객체를 갖고 있는 컨테이너가 생성됨

 

Ex04에서 사용할 컨테이너가 생성되는 과정을 다시 한번 알아봤음

다시 Ex04 코드를 보자

 

(1). 이렇게 생성된 Config 컨테이너를 불러와 ctx에 저장함

 

(2). ctx를 통해 Config 컨테이너에 저장되어있는 galaxyS23 빈을 가져와 phone1 객체에 저장함

 

여기서 주의할 점은 빈을 가져온다는건 빈을 꺼낸다와는 다름

 

실실행활에서 컨테이너에서 물건을 가져온다는건 꺼낸다는 것이고 컨테이너에서 물건을 가져오면 그 컨테이너에는 가져온 물건이 빠짐

Spring Framework의 컨테이너에서 빈을 가져온다는건 빈을 꺼낸다가 아님

galaxyS23 빈은 GalaxyS23 클래스의 인스턴스가 저장된 주소를 갖고 있는 것

( 이는 자바의 기초적인 내용이니 빈이 왜 인스턴스가 저장된 주소를 갖고 있는지에 대한 설명은 하지 않겠음, 만약 왜? 라는 생각이 들면 자바의 클래스를 다시 처음부터 공부하고 오자 )

 

그래서 getBean 메서드로 빈을 가져오면 빈이 가지고 있는 의존 주입된 GalaxyS23 클래스의 인스턴스 주소를 반환함

phone1 객체는 이 인스턴스의 주소를 갖고 있게되는 것

 

결국 phone1 객체는 아래와 같이 GalaxyS23 클래스의 인스턴스를 참조하게 됨

 

 

(3). 이번에도 (2)와 마찬가지로 ctx를 통해 Config 컨테이너에 저장되어있는 빈을 가져옴

따라서 phone2 객체도 phone1 객체와 마찬가지로 GalaxyS23 클래스의 인스턴스를 참조함

 

(4). 이런 상태에서 phone1, phone2 객체의 setter 를 사용해 의존 객체를 수정하면 같은 인스턴스의 의존 객체를 수정하는 것이므로 GalaxyS23 클래스의 인스턴스의 의존 객체는 "리튬 폴리머 배터리"가 됨

 

여기까지 의존 주입이 적용된 컨테이너를 생성해봤고 IoC를 활용해 빈이 필요한곳에서 빈을 꺼내봤음

 

DI, IoC는 Spring Framework에서 굉장히 중요한 부분이므로 이 글에서 설명하는 것들을 이해할 수 있을 때까지 여러번 반복해서 읽어보고 여러번 반복해서 읽었는데도 이해가 안되면 다른 블로그를 더 찾아보고서라도 반드시 이해해야함

728x90
LIST

<< 학습 목표 >>

1. 의존에 대해서 설명할 수 있다.

2. 의존 객체에 대해서 설명할 수 있다.

3. 의존 주입에 대해서 설명할 수 있다.


우선 프로젝트 -> com.example.de.chapter03 패키지를 추가하자

 

 

Spring Framework에서 중요한 개념인 DI (Dependency Injection) 에 대해서 알아보자

DI는 우리말로 의존 주입이라고 함

DI는 Spring Framework에서 사용되는 용어가 아닌 개발에서 일반적으로 사용하는 말이고 일반적으로 사용되는 기법임

 

먼저 의존에 대해서 알아보자

우리 말에서 "A가 B에게 의존한다" 라면 "A는 B에게 전적으로 도움을 받고 있다" 로 해석할 수 있음

B가 없으면 A가 스스로 무언가를 할 수 없을 것

 

좀 더 현실의 예를 들어보자

대부분이 가전제품은 전기에 의존하고 있음

컴퓨터는 전기에 의존하고 있기 때문에 전기가 없다면 컴퓨터를 켤 수 없음

스마트폰은 배터리에 의존하고 있기 때문에 배터리가 없다면 스마트폰을 켤 수 없음

시계는 건전지에 의존하고 있기 때문에 건전지가 없다면 시계는 동작하지 않음

 

이런것처럼 프로그래밍에서도 의존 현상이 발생하고 이는 중요한 개발 기법 중 하나임

Spring Framework으로 의존 기법을 설명하면 좋겠지만 난이도가 굉장히 어려워지니 자바로 의존 기법을 이해하자

Spring Framework는 자바 언어를 사용하는 프레임워크이므로 자바로 의존 기법을 이해했다면 Spring Framework의 의존, 의존 주입 등을 이해할 수 있음


우선 캡슐화부터 시작해서 의존, 의존 주입(DI), 제어 역전(IoC)까지 천천히 그리고 확실하게 알아보자

상속까지 들어가야 할 수도 있지만 상속까지 끼게되면 이해하는데 더 복잡해지므로 상속은 제외하고 보자

 

위에서 언급한대로 잠깐 여기서는 Spring Framework는 접어두고 자바로만 알아보자

 

아래와 같이 스마트폰을 구현한 클래스들이 있음

이 클래스들을 프로젝트 -> com.study.chatper03 패키지에 추가하자

 

이번에는 프로젝트 -> com.study.chapter03 -> Ex01 클래스를 아래와 같이 추가하자

Ex01 클래스는 스마트폰들을 생성하고 스마트폰들의 정보를 출력하는 역할임

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23();
		phone1.setBatteryType("건전지");
		
		GalaxyS23 phone2 = new GalaxyS23();
		phone2.setBatteryType("건전지");
		
		GalaxyFlip4 phone3 = new GalaxyFlip4();
		phone3.setBatteryType("건전지");
		
		IPhone14 phone4 = new IPhone14();
		phone4.setBatteryType("건전지");
		
		IPhone14 phone5 = new IPhone14();
		phone5.setBatteryType("건전지");
		
		System.out.println("<< phone1 >>");
		System.out.println("배터리는 " + phone1.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone1.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone2 >>");
		System.out.println("배터리는 " + phone2.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone2.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone3 >>");
		System.out.println("배터리는 " + phone3.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone3.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone4 >>");
		System.out.println("배터리는 " + phone4.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone4.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone5 >>");
		System.out.println("배터리는 " + phone5.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone5.getBatteryType() + " 를 교체하세요.");
	}

}

 

현재 모든 스마트폰의 배터리가 건전지인 상태라고 하자

그래서 스마트폰 5대를 생성했고 모든 스마트폰의 배터리를 건전지로 설정한 상태임

그리고 스마트폰의 배터리 정보를 출력하고 있음

 

이때 배터리 정보를 출력하는 문구가 조금이라도 바뀌면 모든 Sysout에서 코드를 바꿔야하므로 메서드를 활용하면 스마트폰의 배터리 정보를 좀 더 쉽게 출력할 수 있고 좀 더 쉽게 수정할 수 있음

각 스마트폰 클래스에 다음과 같이 배터리 정보를 출력하는 메서드를 추가하자

 

이제 바뀐 방식대로 배터리 정보를 출력하도록 바꿔보자

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23();
		phone1.setBatteryType("건전지");
		
		GalaxyS23 phone2 = new GalaxyS23();
		phone2.setBatteryType("건전지");
		
		GalaxyFlip4 phone3 = new GalaxyFlip4();
		phone3.setBatteryType("건전지");
		
		IPhone14 phone4 = new IPhone14();
		phone4.setBatteryType("건전지");
		
		IPhone14 phone5 = new IPhone14();
		phone5.setBatteryType("건전지");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

 

여기서 스마트폰은 배터리(batteryType 멤버 변수)에 의존하고 있음

따라서 배터리는 스마트폰의 의존 객체임

그리고 main이 스마트폰의 의존 객체를 setter 통해서 넣어주고 있음

 

스마트폰이 의존 하고 있는 batteryType 멤버 변수에 건전지 문자열을 직접 넣는게 아니라 main이 setter 통해 넣어주고 있는데 이를 의존 주입이라고 함

다시 설명하면 본인이 아닌 제 3자가 의존 객체를 넣어주는걸 의존 주입(Dependency Injection) 이라고 함

 

의존 주입을 하는 방법은 두 가지가 있음

- 생성자를 통한 의존 주입

- setter를 통한 의존 주입

 

지금 우리는 setter를 통한 의존 주입이므로 다른 방식의 의존 주입도 해보자

아래와 같이 생성자를 통한 의존 주입으로 바꿔보자

 

변경된 의존 주입 방식을 적용해 스마트폰에 배터리를 지정하자

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23("건전지");
		GalaxyS23 phone2 = new GalaxyS23("건전지");
		GalaxyFlip4 phone3 = new GalaxyFlip4("건전지");
		IPhone14 phone4 = new IPhone14("건전지");
		IPhone14 phone5 = new IPhone14("건전지");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

 

의존 주입에 좋은 방식, 더 나은 방식은 없음

그저 두 가지 방식이 있을 뿐이고 상황에 따라서 필요한 또는 하고 싶은 방식으로 의존 주입을 하면 됨

 

여기까지 의존이 무엇이고 의존 주입이 무엇인지 설명을 했음


지금 의존 주입을 하나 하나 일일히 해주고 있음

이렇게 했을 때 생길 수 있는 문제점은 의존 객체가 반드시 바뀌어야할 때 개발자가 직접 바꿔줘야하는 의존 객체들이 너무 많기 때문에 개발자의 실수로 의존 객체가 바뀌지 않을 수 있다는 것

 

개발자의 실수로 의존 객체가 바뀌지 않는 상황을 알아보자

 

 

이제 시대가 발전해서 스마트폰이 건전지 대신 리튬이온 배터리로 바꼈음

그렇다면 main에서 스마트폰에 건전지를 주입해주는 대신 리튬이온 배터리를 주입해주도록 해야함

 

중간에 바로 보이지만 의존 객체가 반드시 바뀌어야하는데 의존 객체를 바꿔줘야 할 스마트폰들이 너무 많기 때문에 실수로 어떤 객체의 의존 객체를 바꾸지 못할 수도 있다는 것

 

이 문제를 해결할 수 있는 첫 번째 방법은 스마트폰이 의존 객체를 직접 주입하는 것

아래와 같이 스마트폰의 생성자를 통해서 의존 주입을 받는게 아닌 스마트폰이 직접 필요한 의존 객체(batteryType 멤버 변수)를 주입하면 됨

아래와 같이 따라 입력할 필요는 없고 보기만 하면 됨

 

바뀐 의존 주입 방법대로 스마트폰들을 생성해보자

 

이렇게 하면 개발자의 실수를 없앨 수 있는것처럼 보임

기존에는 생성된 스마트폰 마다 의존 객체를 바꿔줘야했으므로 5대의 스마트폰이 있다면 5번 의존 객체를 바꿔줘야하고 100대의 스마트폰이 있다면 100번 의존 객체를 바꿔줘야했지만 스마트폰이 의존 객체를 직접 생성하도록 바꿈으로써 스마트폰의 대수에 상관 없이 세 종류의 스마트폰의 생성자를 수정하면 되므로...

 

그러나 바로 위에서 언급한것처럼 없앨 수 있는 것처럼 보이는거지 없앤건 아님

스마트폰의 종류가 늘어나면 어떻게 될까?

결국 100대의 스마트폰이 있는 상황과 100종류의 스마트폰이 있는 상황은 똑같은 상황이 됨

 

스마트폰이 직접 의존 객체를 생성하는 방식을 강한 결합 이라고 부르고 스마트폰이 의존 객체를 주입 받는걸 약한 결합이라고 부름

강한 결합은 좋지 못한 코드를 만들 확률이 높아지게 만들고 약한 결합은 좋은 코드를 만들 확률이 높아지게 만듬

 

다시 원래대로 생성자 또는 setter 를 사용해 의존 주입을 받는 약한 결합으로 되돌아가자

어차피 되돌아갈 것이므로 따라 입력하지는 말라고 했던 것


다시 원점으로 돌아와서 의존 객체를 주입하는 약한 결합이 좋은데 약한 결합을 하면 개발자의 실수로 의존 객체를 반드시 바꿔야하는데 바꾸지 못하는 상황이 생김

 

이를 해결할 수 있는 좋은 방안은 없을까?

 

당연히 있음

 

이를 해결할 수 있는 좋은 방안은 main에서 스마트폰을 직접 생성하지 말고 스마트폰을 생성해주는 공장을 만드는 것

프로젝트 -> com.study.chapter03 -> SmartPhoneFactory 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class SmartPhoneFactory {
	public Object createSmartPhone(String smartPhoneType) {
		String batteryType = "리튬이온 배터리";
		
		if(smartPhoneType.equals("GalaxyS23")) {
			return new GalaxyS23(batteryType);
		} else if(smartPhoneType.equals("GalaxyFlip4")) {
			return new GalaxyFlip4(batteryType);
		} else if(smartPhoneType.equals("iPhone14")) {
			return new IPhone14(batteryType);
		} else {
			System.out.println("생산할 수 없는 스마트폰입니다.");
			return null;
		}
	}
}

위와 같이 스마트폰을 만들고 필요한 의존 주입까지된 스마트폰을 반환해주는 공장이 있다면 개발자의 실수를 상당히 줄 일 수 있음

 

<< 코드 설명 >>

(1). 반환 타입이 Object 인건 스마트폰 공장에서 여러 종류의 스마트폰을 만들 수 있으므로 모든 종류의 스마트폰을 반환 할 수 있도록 하기 위함

혹시 반환 타입이 Object인 메서드 또는 매개변수 타입이 Object인 메서드에 대해서 잘 모른다면 자바 쪽이 약한것이므로 [ 반환 타입이 Object인 메서드 ], [ 매개변수 타입이 Object인 메서드 ] 로 검색해서 찾아보자

 

(2). 스마트폰에 필요한 의존 객체 생성

(3). 필요한 스마트폰을 생성해 의존 주입 후 반환

(4). 오타나 어떤 실수로 잘못된 스마트폰 타입을 전달했다면 이를 알려주는 else 문

 

 

이제 main에서는 스마트폰을 직접 생산하는게 아니라 SmartPhoneFactory 를 사용해서 필요한 스마트폰을 생산하면 됨

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		SmartPhoneFactory sf = new SmartPhoneFactory();
		
		GalaxyS23 phone1 = (GalaxyS23) sf.createSmartPhone("GalaxyS23");
		GalaxyS23 phone2 = (GalaxyS23) sf.createSmartPhone("GalaxyS23");
		GalaxyFlip4 phone3 = (GalaxyFlip4) sf.createSmartPhone("GalaxyFlip4");
		IPhone14 phone4 = (IPhone14) sf.createSmartPhone("iPhone14");
		IPhone14 phone5 = (IPhone14) sf.createSmartPhone("iPhone14");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

<< 코드 설명 >>

(1). 스마트폰을 만들기 위한 공장 생성

(2). 공장을 통해 필요한 스마트폰을 생성하는데 공장에서는 Object 타입으로 반환하므로 필요한 적절한 형태로 형변환

 

특히, 이제 main이 직접 스마트폰을 생성하는 방식에서 SmartPhoneFactory가 대신 스마트폰을 생성해주므로 이를 제어의 역전 ( IoC / Inversion Of Controll ) 이라고 부름


이제 코드가 상당히 완전해졌음

시대가 더 발전해 모든 스마트폰의 배터리를 리튬이온 배터리에서 리튬 폴리머 배터리로 바꿔야한다면 간단하게 스마트폰 생산 공장에서 배터리 타입만 바꿔주면 됨

 

 

main을 보면 처음(왼쪽)에는 스마트폰을 직접 생산하고 직접 의존 주입을 해줬지만 이제(오른쪽)는 스마트폰 공장에서 의존 주입까지 완료된 스마트폰을 받는 방식으로 바꼈음

이를 제어 역전(IoC / Inversion Of Control) 이라고 함

 

Spring Framework 를 배우다 갑자기 의존, 의존 주입(Dependency Injection / DI) 를 하는 이유는 Spring Framework의 핵심 기능 중 하나가 ( IoC, DI )임

Spring Framework에서 의존, 의존 주입, 제어 역전은 굉장히 중요한 키워드이니 반드시 내것으로 만들자

 

프로젝트를 개발하다 보면 컨트롤러와 서비스, 서비스와 DAO 등 많은 요소들이 서로 의존하며 동작하게됨

이때 마다 개발자가 직접 의존 주입을 하는게 아닌 SmartPhoneFactory 처럼 Spring Framework의 DI 를 사용해 의존 주입 된 요소를 받아서 사용하면 개발자의 실수도 줄어들고 개발도 편리해짐

 

이 글에서 만든 SmartFactory 를 컨테이너라고 부름

컨테이너는 프로젝트에 필요한 인스턴스를 생성해 반환해주는 역할을 함

또한 생성할 인스턴스에 의존 주입이 필요하면 의존 주입까지 해줌

 

이 글에서는 컨테이너를 우리가 직접 만들었지만 다음 글 ( https://codingaja.tistory.com/112 ) 에서는 Spring Framework의 DI를 사용해보자


의존, 의존 주입과 관련되서 다른 시각으로 이해하고 싶다면 아래 블로그를 더 보자

 

의존관계 주입(Dependency Injection) 쉽게 이해하기

이번 글에서는 DI(의존성 주입, 의존관계 주입)의 개념을 설명한다.

tecoble.techcourse.co.kr

 

 

의존성 주입이란 무엇이며 왜 필요한가?

목표 의존성 주입이 무엇인지 이해한다. 의존성 주입이 왜 필요한지 이해한다. 의존성 주입이란? 의존성 주입이란 클래스간 의존성을 클래스 외부에서 주입하는 것을 뜻한다. 더 자세하게는 의

kotlinworld.com

 

728x90
LIST