ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [spring] 의존성 주입 (DI) _ 어노테이션 없이 xml로 설정 (생성자, 수정자)
    Spring 2024. 7. 31. 19:17

    ▤ 목차

       
       
       
      모든 객체지향 프로그래밍에 통용되는 개념이다. 
      java를 배우기 때문에 spring을 기준으로 정리한다.

      ✔ 의존성 주입 (Dependency Injection)

      ✨ IoC(Inversion of Control)

       
      의존성 주입을 얘기하기 전에 먼저 알고 가야 하는 개념이 있는데, 바로 IoC이다.
      흔히 제어의 역전이라고 말한다.
      의미는 번역 그대로 메소드나 객체의 호출 작업을 개발자가 결정하는 것 아니라 외부에서 결정하는 것을 의미한다.
       
      Spring에서 ApplicationContext는 IoC 컨테이너를 의미한다.
       
      스프링 프레임워크를 예로 들면, Controller 같은 객체들의 동작 구현은 개발자가 하지만 해당 객체들이 어느 시점에 호출할지는 프레임워크가 제어한다.
       

      개발자가 프레임워크의 규칙대로 구현한다면, 프레임워크가 해당 객체들을 가져다 생성하고 호출하고 소멸시킨다.

       
       

      ✏️프레임워크


      어쩌면 "제어의 역전" 이라는 단어는  '프레임워크'의 개념을 알게 되면 자연스러운 일이라고 생각된다.
      프레임워크를 잘 해석해보자면 틀을 가지고 일하다는 의미로 미리 구조를 만들고  일을 하는 것이다.
      많은 사람들이 시행착오를 겪으면 '이 틀을 기준으로 작성하면 오류가 덜 나고 효율적이야!'라며 만들어놓은 틀이다.
       
       + 각각 다른 사람이 코드를 짜고 공유할 때 상대방의 스타일을 이해하는데 시간이 덜 걸린다.
      개발 스킬의 차이가 큰 경우에도 이해하기 힘든 경우가 생기는데, 이러한 문제를 개발 표준(명명 규칙, 작성 규칙, 디렉터리 구조 등)을 정의하고 표준에 맞춰 개발될 수 있도록 한다.
       + 자주 사용하는 기능이나 사이트 전체와 관련된 기능(로그인, 세션관리, 권한관리 등)을 공통으로 관리하여 재사용성을 높일 수 있다.
       
       

      장점

      1) 효율적이다.
      제로베이스에서 코드를 작성하는 것보다 시간과 비용이 훨씬 절약된다.

      2) Quality가 향상된다.
      다수의 개발자가 사용하며 수정하니 검증된 코드이기에 반복 작업에서 실수하기 쉬운 부분을 처리해준다.

      3) 유지 보수에 좋다.
      정해진 프레임워크를 사용하면 코드가 체계적이기에 해당 개발 담당자가 바뀌어도 유지보수에 안정적으로 대응할 수 있다.
       

      단점

      1) 해당 프레임워크를 학습해야 한다.
      프레임워크는 틀이 정해져 있기에 개발자가 습득하고 이해하는데 오랜 시간이 걸린다.

      2) 개발자의 자유로운 개발에 한계가 있다.
      틀에 맞춰 개발해야 하기 때문에 자유롭고 유연하게 개발하는데 한계가 있다.
       
      java에서 프레임워크 종류에는 spring, struts, 전자정부 프레임워크 등이 있다.
       
       
       

      🪄의존성 주입 (Dependency Injection)


      IoC를 구현하기 위해 사용하는 디자인 패턴 중 하나이다.
      객체 이름 그대로 의존 관계를 외부(객체 기준 외부를 의)에서 주입시키는 패턴이다.
      DI를 통해 코드의 결합도를 낮추고 재사용 가능한 코드를 작성할 수 있다.
       
      자바에서 일반적으로 인터페이스를 이용해서 객체들이 상호작용할 때 규칙을 따르고 있어야 할지 정의한다.
      예를 들어, 게임의 캐릭터들이 "달리기 능력이 있어"라고 설계하면, 새로운 캐릭터를 추가할 때 해당 규칙에 따르면서 쉽게 달리기를 구현할 수 있는 것을 말한다.
       

      😉 핵심 개념

      의존성(Dependency)
       하나의 객체가 다른 객체 없이 기능을 제대로 수행할 수 없는 상태를 말한다.
      예 ) 서비스 객체가 DAO(DB접근객체)에 의존하면, 서비스 객체는 데이터베이스에 접근하는 기능을 DAO 객체를 통해 사용할 수 있다.
       
      주입(Injection)
      객체가 의존하는 다른 객체를 직접 생성하거나 관리하지 않고 외부에서 받는 것을 말한다.
      (주입이란 단어가 와닿지 않는다면, 주사로 주입하는 것을 생각해라. 주입은 '외부'가 존재할 수밖에 없다.)
       
       

      👏 장점

      • 결합도 감소 
      • 객체 간의 의존 관계를 외부에서 설정하기 때문에 각 객체는 약결합이 되어있다. 
      • => 코드의 유지보수성이 높아지고 재사용, 확장성이 높아진다.
      • 테스트 용이성
      • 의존 객체를 모의 객체로 대체하기 쉽다. 단위 테스트를 쉽게 작성할 수 있다.
      • 재사용성
      • 객체의 의존 관계가 외부에서 설저오디기 때문에 동일한 객체를 여러 곳에 사용할 수 있다. 

       

      🔷 코드로 보기

      + interface가 있어야 하지만 예시 코드에서는 사용하지 않겠다.
      공통 코드

      더보기
      package model;
      
      public class DataDao{
      	public void selectData() {
      		System.out.println("DB 연동 후 자료 읽기 처리");
      	}
      }

       

      package controller;
      
      import model.DataDao;
      
      public class ProcessServiceImpl{
      	private DataDao dataDao;
      	
      //	public ProcessServiceImpl(){ //기본 생성자를 쓰는 것을 추천
      //	}
      	
      	public ProcessServiceImpl(DataDao dataDao){
      		this.dataDao = dataDao;
      	}
      	
      	@Override
      	public void selectProcess() {
      		System.out.println("selectProcess 처리 시작");
      		dataDao.selectData(); // model 영역의 클래스가 수행되고 있다.
      		System.out.println("selectProcess 처리 끝");
      	}
      }

       

      +interface를 사용해서 해당값을 넘겨받는다.

       

       

       
       

      🔹전통적인 방법

      package controller;
      
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      
      import model.DataDaoImpl;
      
      public class ServiceMain {
      
      	public static void main(String[] args) {
      		// DB 처리 객체를 생성
      		DataDaoImpl impl = new DataDaoImpl();
      		
      		//BL 관련 객체 생성
      		ProcessServiceImpl serviceImpl = new ProcessServiceImpl(impl); //DataDaoImpl의 주소를 넣어준다
      		ProcessService processServiceImpl = serviceImpl;
      		processServiceImpl.selectProcess();
      	}
      
      }

       
      위의 코드를 보면 개발자가 직접 생성자를 생성하여 필드를 초기화하고 있다.
       
      즉, 객체 생명주기나 메서드의 호출을 개발자가 직접 제어하고 있다.
       
       
       

      🔹Spring 방법 (생성자 주입)

      외부에서 관리하는 방법은 아래와 같다.
       

      package controller;
      
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      
      import model.DataDao;
      import model.DataDaoImpl;
      
      public class ServiceMain {
      
      	public static void main(String[] args) {
      		//Spring 방법 사용
      		ApplicationContext context = new ClassPathXmlApplicationContext("init.xml");
      		ProcessService processService2 = (ProcessService)context.getBean("serviceImpl");
      		processService2.selectProcess();
      	}
      
      }

       

      <?xml version="1.0" encoding="UTF-8"?>
      ...
      <bean name="dataDaoImpl" class="model.DataDaoImpl" />
          <bean id ="serviceImpl" class="controller.ProcessServiceImpl">
          	<constructor-arg>
          		<ref bean="dataDaoImpl"/>
          	</constructor-arg>
          </bean>

       
       + name은 객체 변수이름을 여러 개 줄 수 있다 id는 중복이 불가능하다.
       
      지금 여기에서 신경 써야 하는 것은 xml 파일과 main 메서드에서 직접 new를 하지 않는다는 점이다.
      xml파일에서 bean 요소를 사용해서 정의하고 있다. 
      Spring 프레임워크에서 관리하는 객체를 bean이라고 한다. messageImpl 클래스가 bean으로 등록되어 있다.
       
       
      의존성 주입은 3가지 방법
      1. 생성자 주입
      2. 필드 주입
      3. 수정자 주입
       


       
       
       

      ✔ 생성자 주입

      🧐 생성자 주입(constructor injection)이란? 

      생성자 주입은 필수적으로 의존성을 주입받아야만 객체를 생성할 수 있다.
      객체가 생성될 때 의존성이 누락되지 않도록 보장한다.
       
      생성자 주입을 사용하면 객체의 불변성을 유지하기 쉽다.
      한번 생성된 객체는 주입된 의존성을 변경할 수 없기 때문에 객체의 상태를 예측 가능하게 만들어 준다. 생성자를 통한 주입은 한 번만 이뤄진다.
      또한 생성자 주입은 객체 생성 시점에 모든 의존성이 명시적으로 주입된다.
      따라서 순환 참조*가 발생하지 않도록 할 수 있다.

      더보기

      *순환 참조

      두 개 이상의 객체가 서로를 직접적이거나 간접적으로 의존하고 있어서 발생하는 상황을 말한다.

      예를 들어

      A클래스가 B에 의존하고 동시에 B 클래스가 A클래스에 의존하는 경우가 있다.

      A를 생성할 때 B클래스가 필요하고 B를 생성할때 A필요하므로 두 객체 간에 의존성 주입을 계속해서 순환하게 된다.

      이런 순환 참조를 피하는 방법이 객체 간의 의존성을 재구성하거나 주입하는 방식을 변경하여 의존성을 관리하는 것이다.

       

      💻 코드로 보기

      model은 로직을 처리한다.

      package pack.model;
      
      public class Animal {
      
      	public String eat() {
      		return "냠냠";
      	}
      	
      }

       
      controller로, 매개역할을 수행한다.

      package pack.service;
      
      import pack.model.MyInfoInter;
      import pack.other.OutFileInter;
      
      public class Message {
      	private String message1, message2 ="";
      	
      	public MessageImpl(String message1,String message2) {
      		this.message1 = message1;
      		this.message2 = message2;
      	}
      	
      	@Override
      	public void say() {
      		String msg = "Message class, say method call: ";
      		msg += "\n" + message1 +"  " + message2;
      		
      		System.out.println(msg); //console 출력
      
      	}
      
      }

       
      실행할 main 메서드

      public class Main {
      	public static void main(String[] args) {
      		ApplicationContext context = new ClassPathXmlApplicationContext("classpath:tot.xml");
      	
      		Message impl1 = (Message)context.getBean("mImpl");
      		impl1.sayHi();
      		Message impl2 = (Message)context.getBean("mImpl");
      		impl2.sayHi();
          }

       
       
      외부 주입 (tot.xml)

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
      ...
      
      	 <bean id="mImpl" class="pack.service.Message" scope="prototype">
           <constructor-arg index="1" type="java.lang.String">
             		<value>안녕하세요</value>
           </constructor-arg>
           <constructor-arg index="0" type="String">
             		<value>잘있어요</value>
           </constructor-arg>
      </beans>


      👏 scope

      객체의 범위를 나타낸다.
      기본적으로 singleton이 설정되어 있다. 즉, 객체는 하나만 생성한다.
      종류는 singleton, prototype가 있다.

      • singleton
      • : 객체를 1개만 설정한다.
      • prototype
      • : 객체를 계속 만든다. (힙 메모리에 만들어진다!)

       

      👏 constructor-arg

      생성자의 argument에 값을 주는 요소이다.
      지정해 준 bean의 생성자를 찾아가 값을 주입한다.
       

      👏 index

      위의 코드와 같이 같은 타입의 arg를 2개 이상 가지고 있다면 index를 사용할 수 있다.
      index는 0부터 시작해서 처음 arg에 index="0"을 주면 해당 값이 첫번째 위치의 arg에 들어간다.
       
       
       
       


       
       

      ✔ 수정자 주입

       
      setter의 전반적인 흐름은 다음과 같다.
       

      💻 핵심 코드

       
      setter를 통한 주입의 핵심적인 부분이다.

      setter 메서드의 파라미터로 변경할 값을 주입한다. 의존성을 어디에서든 설정할 수 있기에 유연하다는 장점이 있다.
      때문에 단위 테스트 시 유용하다.
      이 부분은 장점이기도 하지만 단점이기도 하다.

      setter 메서드를 외부에서 호출하여 null을 넣어줄 수 있다. 즉, 객체의 의존성이 바뀐다면 객체가 전혀 예상하지 못하는 값이 주입될 수 있다. 

      의존성을 재설정하거나 다시 주입하는 특수한 상황에서 setter 주입을 고려하자.
       


       
       

      😊정리

      기본 생성자 VS setter , 파라미터가 있는 생성자 VS setter 이렇게 2가지의 생성자가 있다면?
      둘 다 setter가 마지막에 주입이 된다.

       
      다음에는 어노테이션을 사용하면 위와 같은 과정들을 @를 사용해서 내부적으로 처리가 된다.
      그러나 어노테이션만 알게 되면 헷갈리는 경우가 있으니 과정을 알고 가는 것을 추천한다.
      주입을 해주는 어노테이션은 @Autowired이다.
      spring에서 권장하는 방법은 생성자 주입이다.
      실무에서는 간편하기에 필드 주입을 많이 사용한다.
       
      Ioc를 이야기하려면 DI가 빠질 수없고 DI를 설명하려면 IoC가 빠질 수 없다. 

      'Spring' 카테고리의 다른 글

      MVC 모델  (0) 2024.07.26
    Designed by Tistory.