ㅁ 스프링 특징 2
ㅁ DI (Dependency Injection)
- 의존성 주입.
- IoC와 연관되어 있는 기술이다.
- 개발자가 직접 객체를 생성하지 않고 스프링 컨테이너가 관리하고 있는 Bean 객체를 자동으로 주입하는 개념.
ㅁ 클래스 생성
- com.br.spring.di에 Phone 클래스 생성
package com.br.spring.di;
public class Phone {
private String name;
private String brand;
private int price;
private String releaseDate;
public Phone() {}
public Phone(String name, String brand, int price, String releaseDate) {
super();
this.name = name;
this.brand = brand;
this.price = price;
this.releaseDate = releaseDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getReleaseDate() {
return releaseDate;
}
public void setReleaseDate(String releaseDate) {
this.releaseDate = releaseDate;
}
@Override
public String toString() {
return "Phone [name=" + name + ", brand=" + brand + ", price=" + price + ", releaseDate=" + releaseDate + "]";
}
}
ㅁ
- 사실 스프링 레거시 프로젝트를 만드는 순간 ~이 자동으로 만들어진다.
- src/main/webapp/WEB-INF/spring에 root-copntext.xml
- 커피콩모양이 빈이다. s는 스프링이다. s와 커피콩이 같이 있으면 전에 만들었떤 appCtx.xml처럼 spring bean configuraiton file이다.
ㅁ root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Root Context: defines shared resources visible to all other web components -->
</beans>
- 이 파일은 서버 시작과 동시에 이 xml파일은 자동으로 바로 읽혀진다.
- 직접 컨테이터 객체를 만들 필요가 없다.
- 서버 우클릭 - add and remove로 지금 작업중인 프로젝트를 올린다.
- ~ web.xml파일이다.
ㅁ web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- 서버를 스타트하는 순간 web.xml 파일이 읽혀지는데 여기에 root-context.xml 파일이 적혀있어서 얘도 실행된다.
- 열어보면 기존의 동적 웹 프로젝트의 web.xml 파일과는 많이 다르다.
- <context-param> 태그와 <listener> 태그는 세트다. 둘 다 스프링 컨테이너와 관련된 태그다.
- ContextLoaderListener는 스프링 컨테이너를 생성해주는 역할의 클래스다.
- contextConfigLocation는 스프링 컨테이너를 생성할 때 읽어들일 파일의 경로다.
- root-context.xml에 등록되어있는 빈들이 어쩌구
※ <context-param> + <listener>
- ContextLoaderListener 클래스에 의해서 스프링 컨테이너 환경설정이 작성되어있는 root-context.xml 파일이 제일 먼저 읽혀지면서 Spring Container가 생성된다.
(1) 서버 start시 제일 먼저 web.xml 파일이 실행된다.
(2) web.xml 파일에 작성되어 있는 <context-param>, <listener> 태그에 의해서 root-context.xml 파일이 제일 먼저 읽혀진다. 이를 pre-loading이라고도 한다.
(3) root-context.xml (Spring Bean Configuration File)에 작성된 빈들이 스프링 컨테이너에 등록된다.
ㅁ 다시 root-context.xml 파일
<?xml version="1.0" encoding="UTF-8"?>
<bean class="com.br.spring.di.Phone" id="phone1">
<property name="name" value="아이폰 16 pro" />
<property name="brand" value="Apple" />
<property name="price" value="1700000" />
<property name="releaseDate" value="2024-09-28" />
</bean>
</beans>
ㅁ 클래스 생성
- com.br.spring.di에 PhoneController1 클래스 생성
package com.br.spring.di;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PhoneController1 {
@RequestMapping("/test1") // "localhost:8888/spring/test1" 라는 url 요청시 실행되는 메소드
public void diTest1() {
System.out.println("diTest1 메소드 작동됨");
}
}
- ~메소드 하나가 기존에 만들었다 서블릿 클래스 하나다.
- 프린트문 넣고 정상적으로 작동하는지 테스트한다.
ㅁ 서버 스타트
- 스프링부터 서버를 start할 때 읽어들이는 문서가 엄청 많다. 그 중 하나라도 문제가 있으면 서버 start가 안 된다.
ㅁ 오류
- public class PhoneController1 위에 @Controller라는 annotation이 있어야 한다.
- 이렇게 나오면 정상적으로 된거다.
ㅁ Container에 등록된 Bean을 가져오는 방법
- 가장 근본적인 방법이 빈들이 담겨있는 Spring 컨테이너 객체로부터 getBean 메소드로 가져오는 방법이다.
- 새로운 방법(DI 관련 어노테이션을 이용하는 방법)
@Inject : 등록된 Bean들 중 타입(class)이 일치하는 Bean 객체를 주입해주는 어노테이션
- 이름으로 찾으려면 @Qualifier("이름")를 직접 명시해야됨.
@Resource : 등록된 Bean들 중 이름(id)이 일치하는 Bean 객체를 주입해주는 어노테이션
@Autowired : 등록된 Bean들 중 타입(class)이 일치하는 Bean 객체를 주입해주는 어노테이션
- @Autowired는 @Qualifier이 내장되어있어서 타입이 여러개일 경우 이름으로 자동으로 찾아진다.
- @Autowired
- 실무에서는 거진 @Autowired를 사용한다.
※ @Autowired 사용방법
(1) 필드로 생성된 객체 주입
- 필드마다 매번 @Autowired 작성
(2) 생성자로 생성된 객체 주입
- 생성자의 매개변수에 스프링 컨테이너가 관리하는 Bean을 전달받아 주입.
- @Autowired 생략가능
- 실무에서 자주 쓰는 방법은 생성자를 이용하는 방법이다.
필드, 메소드는 각각 10개면 10번 다 써줘야 한다. @Autowired 생략도 가능하다.
(3) 메소드로 생성된 객체 주입
- 생성자의 매개변수에 스프링 컨테이너가 관리하는 Bean을 전달받아 주입.
- @Autowired 생략불가능
- 통상 setter 메소드의 형태로 작성. 이렇게 작성하지 않아도 @Autowired 붙어있으면 주입된다.
ㅁ 회사에선 그냥 new로 생성해서 쓸 때도 있다. 굳이 빈 등록하는게 더 번거로워서. 크게 문제가 되진 않다.
ㅁ PhoneController1 수정
package com.br.spring.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PhoneController1 {
// @Autowired
// (1) 필드에 바로 적용하는 방법
@Autowired
private Phone a; // 기본적으로 타입을 가지고 빈을 탐색한다.
@RequestMapping("/test1") // "localhost:8888/spring/test1" 라는 url 요청시 실행되는 메소드
public void diTest1() {
System.out.println(a);
}
// (2) 생성자를 이용해서 주입받기
// 필드를 초기화하는 매개변수 생성자 작성 후 해당 생성자 상단에 @Autowired 어노테이션 기술
// @Autowired 생성자 생략 가능.
private Phone b;
// @Autowired <- 생성자 주입 방식은 @Autowired 어노테이션 생략 가능
public PhoneController1(Phone b) { // 매개변수에 생성된 객체 주입
this.b = b;
}
@RequestMapping("/test2") // "localhost:8888/spring/test2" 라는 url 요청시 실행되는 메소드
public void diTest2() {
System.out.println(b);
}
// (3) 메소드를 이용하여 주입
// 객체를 전달받아서 필드에 대입을 시켜주는 용도의 메소드를 작성하고 상단에 @Autowired 기술 (생략불가)
// 생성자는 자동으로 실행돼서 생략이 됐던 거고, 메소드는 자동으로 실행이 안되니까.
// 반드시 메소드명을 setter 메소드처럼 작성할 필요는 없지만 통상적으로 setter 메소드처럼 작성한다.
private Phone c;
@Autowired // <- 메소드 방식은 @Autowired 생략 불가
public void setC(Phone c) {
this.c = c;
}
@RequestMapping("/test3") // "localhost:8888/spring/test2" 라는 url 요청시 실행되는 메소드
public void diTest3() {
System.out.println(c);
}
}
- http://localhost:8888/spring/test1, http://localhost:8888/spring/test2, http://localhost:8888/spring/test3를 각각 요청한 결과이다.
- 404뜨는 건 정상이다.
- 메소드 위의 @RequestMapping는 메소드당 1개여야만 한다. 같으면 오류남.
- 그런데 지금까지는 동일한 타입의 bean(Phone)이 하나였다.
ㅁ root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<bean class="com.br.spring.di.Phone" id="phone1">
<property name="name" value="아이폰 16 pro" />
<property name="brand" value="Apple" />
<property name="price" value="1700000" />
<property name="releaseDate" value="2024-09-28" />
</bean>
<bean class="com.br.spring.di.Phone" id="phone2">
<constructor-arg value="갤럭시 s24" />
<constructor-arg value="Samsung" />
<constructor-arg value="1600000" />
<constructor-arg value="2024-02-01" />
</bean>
</beans>
- phone2 빈을 만든다. 동일한 타입의 bean(Phone 객체)이 2개가 되었다.
ㅁ 서버를 스타트해본다.
- 서버를 스타트함과 동시에 오류가 발생한다.
- 마지막 오류를 보면 phone 객체가 2개가 ~
- @Autowired가 type으로 bean을 찾기 때문이다.
- 동일한 타입의 bean이 여러개 등록되어 있을 경우 @Autowired는 기본적으로 타입으로 빈을 탐색한다.
- 동일한 타입의 빈이 여러개일 경우 오류를 유발시킨다.
- 단, 해당 필드명을 내가 주입받고자하는 빈의 이름으로 작성시 이름으로 빈을 찾아서 주입한다.
- @Autrowired는 탐색순서가 기본적으로 먼저 타입(class)으로 찾고, 그 다음 이름(id)으로찾는다. 이름에 해당하는 것도 존재하지 않으면 오류가 발생한다.
ㅁ PhoneController1 수정
package com.br.spring.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PhoneController1 {
// (1) 필드
@Autowired
private Phone phone1;
@Autowired
private Phone phone2;
@RequestMapping("/test1")
public void diTest1() {
System.out.println(phone1);
System.out.println(phone2);
}
// (2) 생성자
private Phone a;
private Phone b;
// @Autowired <- 생략가능
public PhoneController1(Phone phone1, Phone phone2) {
this.a = phone1;
this.b = phone2;
}
@RequestMapping("/test2")
public void diTest2() {
System.out.println(a);
System.out.println(b);
}
// (3) 메소드
private Phone c;
private Phone d;
@Autowired
public void setC(Phone phone1) {
this.c = phone1;
}
@Autowired
public void setD(Phone phone2) {
this.d = phone2;
}
/* [이렇게도 가능]
@Autowired
public void setCandD(Phone phone1, Phone phone2) {
this.c = phone1;
this.d = phone2;
}
*/
@RequestMapping("/test3")
public void diTest3() {
System.out.println(c);
System.out.println(d);
}
}
- http://localhost:8888/spring/test1, http://localhost:8888/spring/test2, http://localhost:8888/spring/test3를 각각 요청한 결과이다.
ㅁ
- 컨트롤러 측에 필요한 필드는 사실 dto 필드가 아니라 서비스다.
==========================================================================================
ㅁ com.br.spring.di에 PhoneController2 클래스 생성
package com.br.spring.di;
import org.springframework.stereotype.Controller;
@Controller
public class PhoneController2 {
}
- 위에 @Controller를 붙인다.
ㅁ com.br.spring.di에 PhoneService 인터페이스 생성
package com.br.spring.di;
public interface PhoneService {
// 전체폰 조회
void selectList();
// 폰 등록
void insertPhone();
// 등등...
}
- 이렇게 서비스를 인터페이스, 그 인터페이스를 구현하는 클래스로 나눈 이유는
웹용 서비스 클래스와 모바일용 서비스 클래스를 다르게 ~
ㅁ com.br.spring.di에 PhoneServiceWebImpl 구현 클래스 생성
package com.br.spring.di;
public class PhoneServiceWebImpl implements PhoneService {
@Override
public void selectList() {
System.out.println("웹용 폰 전체조회 서비스 실행");
}
@Override
public void insertPhone() {
System.out.println("웹용 폰 등록 서비스 실행");
}
}
ㅁ com.br.spring.di에 PhoneServiceMobileImpl 구현 클래스 생성
package com.br.spring.di;
public class PhoneServiceMobileImpl implements PhoneService {
@Override
public void selectList() {
System.out.println("모바일용 폰 전체조회 서비스 실행");
}
@Override
public void insertPhone() {
System.out.println("모바일용 폰 등록 서비스 실행");
}
}
ㅁ PhoneController2 클래스 수정
- 스프링 사용 전에는 매번 서비스 객체를 생성해서 호출했다.
- private PhoneService pService = new PhoneServiceWebImpl(); -- 웹 개발 당시
- private PhoneService pService = new PhoneServiceMobileImpl(); -- 모바일 개발 당시
- 그런데 개발자가 직접 new로 객체 생성시 결합도가 높은 문제가 발생한다.
- 결합도(= 종속관계) : 연관관계에서의 클래스들 간에 각 클래스 수정시 서로에게 영향을 미치는 정도.
※ 연관관계의 클래스
public class A {
private B b = new B();
}
public class B {
}
- A 클래스에서 B 클래스를 필드로 사용할 경우 연관과계가 형성된다.
- B 클래스가 변경(클래스명, 생성자, 메소드 수정 등)될 경우 A 클래스도 일부 수정해야되는 상황이 발생한다.
- 이런 상황에서 두 객체간의 결합도가 높다, 강하다라고 표현한다.
- new로 생성하면 PhoneServiceWebImpl 클래스명이 변경되는 순간 이 클래스가 쓰이던
PhoneController2에 와서 ~도 수정해야할 수 있다.
- ~ B만 수정하면 된다. A 클래스는 수정하지 않아도 된다.
※ 스프링의 IoC + DI를 활용해서 해결 가능
(1) 개발에 사용할 서비스 클래스를 빈으로 등록 (xml 방식, 자바 방식, MVC용 어노테이션)
- 빈으로 등록하는 방법이 사실 하나 더 있다. 나중에 배울건데 MVC용 어노테이션
(2) DI를 통해 해당 객체를 주입받아 사용 (@Autowired)
- PhoneServiceWebImpl 의 위에 @Service를 붙이면 빈 등록이 끝난다.
package com.br.spring.di;
import org.springframework.stereotype.Service;
@Service
public class PhoneServiceWebImpl implements PhoneService {
@Override
public void selectList() {
System.out.println("웹용 폰 전체조회 서비스 실행");
}
@Override
public void insertPhone() {
System.out.println("웹용 폰 등록 서비스 실행");
}
}
- 여기에만 해본다.
package com.br.spring.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PhoneController2 {
@Autowired
private PhoneService pService; // 이러면 현재 서비스로 등록되어 있는 객체가 여기에 주입된다.
@RequestMapping("/list.ph")
public void selectList() {
pService.selectList();
}
@RequestMapping("/insert.ph")
public void insertPhone() {
pService.insertPhone();
}
}
- 일므을 바꿔도 상관없다.
- 사용할 객체의 클래스가 변경돼도 해당 객체를 참조하는 클래스에서 수정할 필요가 없다. 결합도가 낮아진 것이다.
ㅁ 기존 거는 주석처리하고 PhoneServiceMobileImpl 클래스의 위에 @Serivce를 붙인다.
package com.br.spring.di;
import org.springframework.stereotype.Service;
//@Service
public class PhoneServiceWebImpl implements PhoneService {
@Override
public void selectList() {
System.out.println("웹용 폰 전체조회 서비스 실행");
}
@Override
public void insertPhone() {
System.out.println("웹용 폰 등록 서비스 실행");
}
}
package com.br.spring.di;
import org.springframework.stereotype.Service;
@Service
public class PhoneServiceMobileImpl implements PhoneService {
@Override
public void selectList() {
System.out.println("모바일용 폰 전체조회 서비스 실행");
}
@Override
public void insertPhone() {
System.out.println("모바일용 폰 등록 서비스 실행");
}
}
- 어쩌구
===
ㅁ IoC + DI의 장점
(1) 메모리를 보다 효율적으로 사용할 수 있다.
- 직접 new로 생성할 경우 매번 메모리 영역에 새로 생성된다.
자주 사용될 객체, 동시에 다수가 사용할 객체라면 그만큼의 객체가 생성되었다가 소멸됨을 반복함(메모리를 빈번하게 사용함)
- 스프링의 IoC와 DI 개념이 적용되면 스프링 컨테이너가 해당 객체를 딱 한번만 생성해서 가지고 있다가,
필요할 때마다 해당 객체를 자동으로 주입해준다. (싱글톤 개념이 들어가 있음)
(2) 클래스간의 결합도를 해소할 수 있음 (결합도를 낮출 수 있음)
- 결합도가 높을 때 발생되는 문제 -> 특정 클래스 수정시 해당 클래스를 사용하고 있는 다른 클래스도 수정해야됨.
- 스프링의 IoC와 DI 개념이 적용되면 결합도가 낮아져서 소스코드의 수정을 최소화 할 수 있다.
- 개발할 때 결합도는 낮고 응집도는 높게 코딩하는 것이 좋다.