본문 바로가기
Spring

[Spring] DI(의존성 주입)

by moca7 2024. 10. 15.

 

 

ㅁ 스프링 특징 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"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"

    <!-- 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 개념이 적용되면 결합도가 낮아져서 소스코드의 수정을 최소화 할 수 있다. 

- 개발할 때 결합도는 낮고 응집도는 높게 코딩하는 것이 좋다.

 

 

 

'Spring' 카테고리의 다른 글

[Spring] MVC (2) resources 등록  (0) 2024.10.16
[Spring] MVC (1)  (0) 2024.10.16
[Spring] IOC(제어의 역전)  (0) 2024.10.14
[Spring] 첫번째 프로젝트 생성, pom.xml  (2) 2024.10.14
[Spring] 스프링 기초  (0) 2024.10.14