(책) 자바 웹을 다루는 기술

Chap 19 스프링 의존성 주입과 제어 역전 기능

EunaSon 2023. 9. 14. 16:41

19.1 의존성 주입하기 768p                     

19.2 의존성 주입 실습하기 774p             

19.3 회원 기능 이용해 DI 실습하기 787p


자바와 같은 객체 지향 프로그래밍 언어에서 클래스는 특정 기능을 수행하는 부품 역할을 함. 어떤 부품을 사용하다가 이상이 있거나 오래 되면 다른 부품으로 교체해서 사용하듯, 애플리케이션에서도 사용자의 요구사항에 따라 클래스 기능을 변경하거나 다른 클래스 기능으로 대체해야 하는 경우가 자주 생김. 이 상황을 수월하게 처리하기 위해 도입된 기능이 의존성 주입(DI)과 제어 역행(IoC)임.

 

19.1 의존성 주입하기

768p

여태까지는 한 클래스가 다른 클래스의 기능을 사용하려면 개발자가 직접 코드에서 사용할 클래스의 생성자를 호출해서 사용했음. 즉, 사용할 클래스와 사용될 클래스의 관계는 개발자에 의해 직접 코드로 부여됐음.

 

의존성 주입이란 이런 연관 관계를 개발자가 직접 코딩을 통해 컴포넌트(클래스)에 부여하는 것이 아니라 컨테이너가 연관 관계를 직접 규정하는 것.=> 코드에서 직접적인 연관 관계가 발생하지 않으므로 각 클래스들의 변경이 자유로워짐(loosely coupled, 약한 결합)

 

프로그램은 각각의 독립적인 기능들고 구성되어 있음. 서로 관련이 있는 기능들은 강하게 결합(tightly coupled)하고, 관련이 없는 기능들은 약하게 결합(loosely coupled)해야 좋은 프로그램임.

 

전체 애플리케이션은 각각의 기능을 담당하는 컴포넌트(클래스)들로 이루어짐. 각 컴포넌트들은 다시 세부 기능을 수행하는 클래스들로 이루어짐.

다른 클래스의 기능을 사용하기 위해 소스코드에서 다른 클래스의 생성자를 호출해서 사용 시 기능 구현 과정에서 다른 변경 사항이 발생하면 빠르게 대처하기 어려움(관련 있는 모든 클래스들의 소스 코드를 다시 수정해야 하기 때문)

=> 스프링 프레임워크에서는 각 클래스들의 연관 관계를 클래스들 사이에서 맺는 것이 아니라 스프링 프레임워크에서 설정을 통해 맺어줌으로써 클래스들이 연관 관계를 갖지 않게 구현했음.

 

1. 의존성 주입을 사용하기 전 게시판 기능

769p

BoardController - boardService = new BoardService(); // BoardService 객체를 코드에서 직접 생성해 사용함

BoardService - boardDAO = new BoardDAO(); // BoardDAO 객체를 코드에서 직접 생성해 DB와 연동함

BoardDAO - 오라클과 연동해 게시판 기능을 구현

=> 만약 중간에 오라클에서 MySQL로 DB를 변경한다고 가정하면, BoardDAO의 기능을 일일이 변경해주어야 함. 가끔은 BoardDAO를 사용하는 BoardService의 기능도 변경해야 할 수도 있음

=> 프로젝트 규모가 커질수록 직접 객체를 생성해서 사용하는 것(tightly coupled)은 복잡한 문제를 일으킬 수 있음

 

2. 인터페이스를 적용한 게시판 기능

771p

게시판 기능과 관련된 클래스들임. 각각의 클래스(Impl)가 인터페이스를 구현하는 구조를 이름

 

BoardServiceImpl - boardDAO = new BoardOracleDAOImpl();

// 인터페이스를 구현한 하위 클래스 객체를 생성 후 오라클과 연동

BoardOracleDAOImpl - 오라클과 연동작업

 

-> 개발 중 MySQL과 연동 기능 생겼다고 가정 시, 기존의 BoardOracleImpl을 변경할 필요 없이 BoardDAO 인터페이스를 구현한 또다른 BoardMySqlDAOImpl 클래스를 구현한 후 BoardServiceImple에서 사용하면 됨

 

BoardServiceImpl - boardDAO = new BoardMySqlDAOImpl();

: 인터페이스를 이용해 각 클래스를 구현한 후 각 클래스의 객체를 사용할 때에는 인터페이스 타입으로 선언한 참조변수로 접근해서 사용하면 됨. 이렇게 하면 완전하진 않지만 앞의 경우보다 클래스 간 의존관계가 훨씬 약해짐

but 인터페이스를 사용해도 BoardServiceImpl 클래스 자체는 여전히 소스 코드에서 직접 수정해야함

 

3. 의존성 주입을 적용한 게시판 기능

772p

이번에는 스프링의 의존성 주입 기능을 이용해서 각 클래스들 사이의 의존 관계를 완전히 분리해보자.

 

의존성 주입(DI, Dependency Injection)을 적용 시 장점

클래스들 간의 의존 관계를 최소화하여 코드를 단순화할 수 있음

애플리케이션을 더 쉽게 유지 및 관리할 수 있음

객체의 생성, 소멸, 객체 간의 의존관계를 컨테이너가 제어함

 

스프링에서 의존성 역전(DI)을 구현

: XML이나 애너테이션을 이용해 객체를 주입하여 객체들의 의존관계를 맺어줌

객체의 제어를 개발자가 아닌 스프링이 직접 담당하므로 제어의 역전(IoC)이라고 함. IoC의 종류는 여러 가지이며,

스프링에서는 DI/의존성 주입/로 IoC/제어 역전/의 기능을 구현하므로 IoC 보다 DI 라는 용어를 더 많이 사용함

 

▼ 생성자를 이용해 의존 객체 주입하는 방식

public class BoardServiceImpl implements BoardService {
    private BoardDAO boardDAO;
    public BoardServiceImpl(BoardDAO boardDAO) {
        this.boardDAO = boardDAO;
    }
}

의존하는 BoardDAOImpl 객체를 전달받기 위해서

생성자(BoardServiceImpl() )를 호출할 때 외부에서 객체를 주입받아 사용함.

BoardServiceImpl 생성자를 호출할 때 컨테이너에 의해 주입되는 객체로 boardDAO 변수를 초기화한 것.

=> 의존(dependency)하는 객체를 컨테이너 실행 시 주입(Injection)하기 때문에 DI라고 부름.

각 클래스 객체는 bean이라고 부르는데, 이는 의존관계를 설정하는 외부 XML 파일에서 각각의 객체를 <bean> 태그로 표시하기 때문임.

스프링에서 의존객체를 주입하는 방식에는 위의 '생성자를 이용하는 방식' 과 아래의 'setter를 이용하는 방식' 이 있음

 

▼ setter를 이용해서 의존 객체 주입하는 방식

public class BoardServiceImpl implements BoardService {
    private BoardDAO boardDAO;
    public void setBoardDAO(BoardDAO boardDAO){
        this.boardDAO = boardDAO;
    }
}

 

* 의존 객체 : 의존(받는, 되는) 객체.

위의 경우라면 BoardServiceImpl이 BoardDAOImpl한테 의존하고 있으므로

의존객체 BoardDAOImpl 이 BoardServlceImpl에 주입됨.

 

19.2 의존성 주입 실습하기

774p

1. setter 를 이용한 DI 실습

클래스 계층 구조 : TestServoce <- - - - TestServiceImpl

 

자바 프로젝트에서 DI 기능 실습을 위해서는 스프링 관련 라이브러리의 패스를 설정해줘야 함.

1. 이클립스 상단 - New > Project... 를 선택 후 Java Project를 선택 > Next

2. 프로젝트 이름으로 pro19를 입력, Finish를 클릭

3. 이클립스에서 자바 프로젝트를 생성함. 자바 프로젝트 생성 시 이클립스의 Perspective를 자바모드로 변경할지 물으면(Do you want to open this perspective now?) Remember my decision 체크박스에 체크하고, Open Perspective를 클릭

4. Project Explorer에서 자바 프로젝트가 생성된 것 확인할 수 있음

5. 프로젝트 pro19 아래 새 폴더 lib를 만든 후 이 책에서 제공하는 예제 소스에서 스프링 DI 관련 라이브러리를 복붙

6. pro19를 선택하고 우클릭 > Build Path > Configure Build Path... 를 선택

7. Libraries 탭에서 Classpath를 선택 > Add JARs... 를 클릭

8. 앞에서 미리 만든 lib 폴더의 라이브러리들을 모두 선택 후 OK를 클릭

9. Apply and Close를 클릭해서 이 내용을 적용함

- Classpath에

cglib-nodep-2.1_3.jar

commons-dbcp.jar

commons-logging.jar

commons-pool.jar

spring.jar 확인

 

10. Project Explorer의 Referenced Libraries에서 jar 파일들을 확인할 수 있음(위의 5개 jar파일)

11. 이제 DI 설정을 할 차례임. 스프링에서 DI 설정은 XML 파일에서 함. 따라서 빈을 설정하는 person.xml 파일을 생성해야 함. pro19 위에서 우클릭 > New > Other... > XML > XML File 선택 > Next

12. 파일 이름으로 person.xml을 입력하고 Finish 클릭

13. 프로젝트 이름 하위에 person.xml이 생성된 것 확인

 

다음은 XML 파일에서 빈을 생성하는 데 사용되는 <bean> 태그의 속성들을 살펴보자.

 

▼<bean> 태그에 사용되는 여러 가지 속성들

속성 이름 설명
id 빈 객체의 고유 이름. 빈 id를 이용해서 빈에 접근함.
name 객체의 별칭
class 생성할 클래스. 패키지 이름까지 입력해야 함.
constructor-arg 생성자를 이용해 값을 주입할 때 사용함
property setter를 이용해 값을 주입할 때 사용함

 

[ person.xml ]

<bean> 태그로 스프링실행 시 생성할 빈을 설정함.

빈은 스프링을실행할 때 사용하는 클래스 객체(인스턴스)

 

...
<beans>
    <bean id="personService" class="com.spring.ex01.PersonServiceImpl">
        <property name="name"> // 해당 빈의 name 속성(멤버변수)
             <value>홍길동</value> // 의 값을 설정함
        </property>
    </bean>
</beans>

실행 클래스 실행 시 <bean> 태그를 이용해 com.spring.ex01.PersonServiceImpl 클래스에 대한 빈을 생성함, 이 빈에 접근할 수 있는 빈 id를 personService로 지정한 후 <property> 태그를 이용해 PersonServiceImpl 클래스 객체의 name 속성에 <value> 태그의 값으로 초기화 함.

소스코드에서 new 를 이용해 직접 객체를 생성하던 것을 person.xml에서 대신 설정하는 것임.

 

pro19/src에 com.spring.ex01 패키지를 만들고, 클래스 파일들을 생성함

: PersonService.java, PersonServiceImpl.java, PersonTest.java

 

[ PersonService 인터페이스 ]

package com.spring.ex01;

public interface PersonService {
    public void sayHello(); // 추상 메서드를 선언
}

[ PersonServiceImpl 클래스 ]

package com.spring.ex01;

public class PersonServiceImpl implements PersonService {
    private String name;
    private int age;
    
    public void setName(Stirng name) // <value> 태그의 값을 setter를 이용해 설정함
    {
        this.name = name;
    }
    
    @Override
    public void sayHello() {
        System.out.println("이름 : " + name);
        System.out.println("나이 : " + age); // age는 xml에서 초기화되지않음(기본값 0으로 자동초기화)
    }
}

 

실행 클래스 - [ PersonTest 클래스 ]

라이브러리에서 제공하는 클래스를 이용해서 XML 파일을 읽어와 빈을 생성함

실행 클래스를 실행하면

스프링의 XmlBeanFactory 클래스를 이용해서 person.xml의 설정대로 PersonServiceImpl 빈을 메모리에 생성함,

이 빈에 대한 id인 personService로 접근하여 sayHello() 메서드를 호출함

package com.spring.ex01;

import org.springframeword.beans.factory.BeanFactory;
import org.springframeword.beans.factory.xml.XmlBeanFactory;
import org.springframeword.beans.core.io.FileSystemResource;

public class PersonTest {
    public static void main(String[] args) {
        // xml 파일 읽어와서 설정대로 빈을 메모리에 생성함
    	BeanFactory factory = new XmlBeanFactory(new FileSystemResource("person.xml"));
        PersonService person = (PersonService)factory.getBean("personService");
        person.sayHello();
    }
}

XmlBeanFactory - xml 파일 읽어와서 메모리에 설정한대로 빈을 생성함

getBean("빈 id") : 빈 id로 메모리에 생성된 빈에 접근하여 빈을 가져옴

위에서 person : person.xml을 읽어서 생성된 PersonServiceImpl 빈 객체임.

 

2. 생성자를 이용한 DI 기능 

784p

이번에는 생성자 주입 방식으로 DI를 실습해보자

 

com.spring.ex02 패키지를 만들고 PersonService.java, PersonServiceImpl.java, PersonTest2.java 클래스를 추가함

 

[ person.xml ]

빈을 생성,

<constructor -arg> 태그를 이용해 생성자 호출 시 생성자 매개변수로 <value> 태그의 값을 전달해서 속성을 초기화 함

<constructor -arg> 태그가 1개 -> 인자가 1개인 생성자 호출,

<constructor -arg> 태그가 2개 -> 인자가 2개인 생성자 호출

 

<beans>
    <bean id="personService1" class="com.spring.ex02.PersonServiceImpl">
        <constructor -arg value="이순신" />
    </bean>
    
    <bean id="personService2" class="com.spring.ex02.PersonServiceImpl">
        <constructor -arg value="손흥민" />
        <constructor -arg value="23" />
    </bean>
</beans>

인자가 1개(이순신)인 id=personService1인 빈을 생성함.

value인 이순신을 생성자로 전달해서 속성 name을 초기화함,

인자가 2개(손흥민, 23)인 id=personService2 인 빈을 생성함.

value인 손흥민, 23을 생성자로 전달해서 속성 name, age를 초기화함

 

[ PersonServiceImpl 클래스 ]

package com.spring.ex02;

public class PersonServiceImpl implements PersonService {
    private String name;
    private int age;
    
    public PersonServiceImpl(String name) { // person.xml에서 인자가 1개인 생성자 설정 시 사용됨
        this.name = name;
    }
    
    public PersonServiceImpl(String name, int age) { // person.xml에서 인자가 2개인 생성자 설정 시 사용됨
        this.name = name;
        this.ae = age;
    }
    
    @Override
    public void sayHello() {
        System.out.println("이름 : " + name);
        System.out.println("나이 : " + age);
    }
}

실행 클래스 - [ PersonTest2 클래스 ]

...

BeanFactory factory = new XmlBeanFactory(new FileSystemResource("person.xml"));

PersonService person1 = (PersonService) factory.getBean("personService1"); // id가 personService1인 빈을 가져옴

person1.sayHello(); // 이름만 초기화했음. 나이는 초기화하지 않아 기본값 0이 출력됨

 

PersonService person2 = (PersonService) factory.getBean("personService2"); // id가 personService2인 빈을 가져옴

person2.sayHello() // 이름과 나이 모두 초기화되어 손흥민, 23 모두 출력됨

 

 

19.3 회원 기능 이용해 DI 실습하기 

787p

앞에서는 설정파일(person.xml)에서 기본형 데이터를 빈의 속성값으로 주입했음

이번에는 의존 관계에 있는 다른 빈을 주입하는 경우를 살펴보자

 

Service 클래스는 db 연동을 위해 DAO 클래스에 의존하는 관계임

17장에서는 Service 클래스는 소스 코드에서 직접 DAO 객체를 생성해서 DAO에서 제공하는 메서드로 db와 연동했었음

 

person.xml과 같은 lib폴더에 member.xml을 생성하자

[ member.xml ]

<beans>
    <bean id="memberService" class="com.spring.ex03.MemberServiceImpl">
        <property name="memberDAO" ref="memberDAO" />
    </bean>
    <bean id="memberDAO" class="com.spring.ex03.MemberDAOImpl" />
</beans>

* ref : 주입되는 데이터가 기본형이 아니라 참조형일때는 <value> 태그 대신 ref 속성을 사용함

* ref 값으로는 주입될 빈의 id를 값으로 줌(bean id="memberDAO" -> ref="memberDAO")

=> setter 주입 방식으로 id가 memberDAO인 빈을 자신의 속성에 주입함

 

[ MemberServiceImpl 클래스 ]

...

private MemberDAO memberDAO; // 주입되는 빈을 저장할 MemberDAO 타입의 속성을 선언함

 

public void setMemberDAO(MemberDAO memberDAO)

// member.xml에서 memberDAO빈을 생성 후 속성 memberDAO에 주입함

{

    this.memberDAO = memberDAO;

}

 

@Override

public void listMembers()

{

    memberDAO.listMEmbers(); // 주입된 빈을 이용해서 listMembers() 메서드를 호출함

}

 

[ MemberDAOImpl 클래스 ]

...

@Override

public void listMembers()

{

    // 'listMembers 메서드 호출', ' 회원 정보를 조회합니다' 를 출력함

}

 

[ MemberTest1 클래스 ]

실행 클래스, member.xml을 읽어들인 후 빈을 생성함->setter 주입 방식으로 주입 후 빈id인 memberService로 접근해서 listMembers() 메서드를 호출함

 

BeanFactory factory = new XmlBeanFactory(new FileSystemResource("member.xml"));

// member.xml에 설정한대로 빈 생성 후 주입함

MemberService service = (MemberService) factory.getBean("memberService"); // 빈id가 memberService인 빈을 가져옴

service.listMembers();

 

=> 어떤 클래스 객체도 생성하지 않았음 오직 스프링의 DI 기능을 이용해 빈을 생성 & 의존 관계에 있는 빈을 속성에 주입하여 빈의 기능을 사용했음