[스프링 프레임워크] 스프링 의존성 주입과 제어 역전 기능

hoonak·2023년 10월 12일
0

자바와 같은 객체 지향 프로그랭밍 언어에서 클래스는 특정 기능을 수행하는 부품 역할을 함. 우리가 살아가는 현실 세계에서도 어떤 부품을 사용하다가 이상이 있거나 오래 되면 다른 부품으로 교체해서 사용함.

마찬가지로 애플리케이션에서도 사용자의 요구 사항에 따라 클래스 기능을 변경하거나 다른 클래스 기능으로 대체해야 하는 경우가 자주 생김. 이러한 사황에서 좀 더 수월하게 대체할 수 있게 도입된 기능이 바로 의존성 주입(DI)과 제어 역행(IoC)임.

의존성 주입하기

지금까지 우리가 프로그래밍을 할 때는 어떤 한 클래스가 다른 클래스의 기능을 사용하려면 당연히 개발자가 직접 코드에서 사용할 클래스의 생성자를 호출해서 사용했음. 즉, 사용할 클래스와 사용될 클래스의 관계는 개발자에 의해 직접 코드에서 부여됨.

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

Note
강합 결합과 약한 결합
현실에서 우리는 자동차의 에어컨이 고장 나면 당연히 에어컨만 수리하거나 교체하면 됨. 하지만 만약 에어컨 기능이 자동차 엔진과 관련 있게 설계되었다면 어떨까. 에어컨은 작은 문제라도 생기면 자동차 엔진까지 손을 봐야 하는 상황이 됨. 즉 자동차의 부품은 같은 기능끼리는 강하게 결합하고, 큰 관련이 없는 기능과는 서로 영향을 주지 않게 만들어야 좋은 자동차라고 할 수 있음. 프로그램도 마찬가지임. 프로그램은 각각의 독립적인 기능들로 구성되어 있음. 쇼핑몰의 경우 크게 상품 관리, 주문 관리, 회원 관리, 게시판 관리 등으로 구성됨. 각 기능들은 또 세부 기능을 하는 여러 클래스들로 이루어 짐. 그런데 부품 기능을 하는 클래스에 변경 사항이 발생했을 때 그 클래스의 기능과 관련이 없는 다른 클래스까지 손봐야 한다면 자동차의 예처럼 여러 가지 문제가 발생할 수 있음. 따라서 서로 관련이 있는 기능들은 강하게 결합(tightly coupled)하고, 관련이 없는 기능들은 약하게 결합(loosely coupled)해야 좋은 프로그램인. 그 반대가 되면 안됨.

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

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

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

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

  • BoardController.java

각 클래스들의 기능을 보면 다른 클래스의 기능을 사용하기 위해 소스 코드에서 직접 다른 클래스 객체를 생성한 후 메서드를 호출하여 연동함.

@WebServlet("/board/*")
public class BoardController08 extends HttpServlet {
	
	BoardService boardService;
	ArticleVO articleVO;

	public void init(ServletConfig config) throws ServletException {
		
        //BoardService 객체를 코드에서 직접 생성해서 사용함.
        boardService = new BoardService();
		articleVO = new ArticleVO();
	}
    ...
  • BoardService.java
public class BoardService {
	BoardDAO boardDAO;

	public BoardService() {
    	
        // BoardDAO 객체를 코드에서 직접 생성해 데이터베이스와 연동함.
		boardDAO = new BoardDAO();
	}
    ...
  • BoardDAO.java
public class BoardDAO {
	private DataSource dataFactory;
	Connection conn;
	PreparedStatement pstmt;

	public BoardDAO() {
		try {
			Context ctx = new InitialContext();
			Context envContext = (Context) ctx.lookup("java:/comp/env");
			dataFactory = (DataSource) envContext.lookup("jdbc/oracle");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
    ...

현재 BoardDAO 클래스에서는 오라클과 연동해 게시판 기능을 구현하고 있음. 그런데 만약 중간에 오라클에서 Mysql로 데이터베이스를 변경한다고 가정해 보자. 지금과 같은 경우는 BoardDAO 클래스의 기능을 일일이 변경해 줘야 함. 그뿐만 아니라 경우에 따라서는 BoardDAO 클래스를 사용하는 BoardService 클래스의 기능도 변경해야 할 수도 있음.

이제까지는 클래스를 사용하려면 자바에서 배웠듯이 자바코드에서 클래스 생성자를 호출해 객체를 생성했음. 하지만 지금처럼 프로젝트 규모가 점점 커지는 상황에서 이렇게 자바 코드에서 직접 객체를 생성해서 사용하는 것은 복잡한 문제를 일으킬 수도 있음.

다른 클래스의 변경 사항이 연속적으로 다른 부분에 영향을 미친다면 이 방법(자바 코드에서 직접 객체를 생성해 사용하는 것)은 좋은 방법이 아님.

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

그럼 이번에는 앞에서 다룬 게시판 기능 구현 시의 문제점을 인터페이스를 사용해서 해결해 보겠음.

다음은 게시판 기능과 관련된 클래스들의 계층 구조를 나타낸 것임. 각각의 클래스가 인터페이스를 구현하는 구조를 이룸.

  • 오라클과 연동하는 게시판 클래스 계층 구조

[BoardController] <--- [BoardControllerImpl]

[BoardService] <--- [BoardServiceImpl]

[BoardDAO] <--- [BoardOracleDAOImpl]

다음은 각 클래스들이 상위 인터페이스를 구현한 후 오라클 데이터베이스와 연동하는 기능을 구현하는 소스임.

  • BoardServiceImpl.java
public class BoardServiceImpl implements BoardService {
	BoardDAO boardDAO;
    
    public BoardService(){
    	// 인터페이스를 이용해 하위 클래스 객체를 생성한 후 오라클 데이터베이스와 연동함.
    	boardDAO = new BoardOracleDAOImpl();
    }
}
...
  • BoardOracleDAOImpl.java
public class BoardOracleDAOImpl implements BoardDAO {
	private DataSource dataFactory;
    Connection conn;
    PreparedStatement pstmt;
    
    public BoardDAO() {
    	try {
        	Context ctx = new InitialContext();
            Context envContext = (Context)ctx.lookup("java:/comp/env");
            dataFactory = (DataSource)envContext.lookup("jdbc/oracle");
        }catch(Exception e) {
        	e.printStackTrace();
        }
    }
}
...

이번에는 개발 중에 MySQL과 연동하는 기능이 생겼다가 가정함. 지금처럼 인터페이스로 구현한 경우에는 기존의 BoardOracleDAOImpl 클래스를 변경할 필요가 없음.


이미지처럼 BoardDAO 인터페이스를 구현한 또 다른 BoardMySqlDAOImpl 클래스를 구현한 후 다음 코드처럼 BoardServiceImpl에서 사용하면 됨.

  • BoardServiceImpl.java
public class BoardServiceImpl implements BoardService {
	BoardDAO boardDAO;
    
    public BoardService() {
    // boardDAO = new BoardOracleDAOImpl();
    // 인터페이스를 이용해 하위 클래스 객체를 생성한 후 Mysql 데이터베이스와 연동함.
    boardDAO = new BoardMySqlDAOImpl();
    }
}

인터페이스를 이용해 각 클래스를 구현한 후 각 클래스의 객체를 사용할 때는 인터페이스 타입으로 선언한 참조 변수로 접근해서 사용하면 됨. 그러면 완전하지는 않아도 앞의 경우보다 훨씬 클래스들 간 의존관계가 약해짐. 그러나 인터페이스를 사용해도 BoardServiceImpl 클래스 자체는 여전히 소스 코드에서 직접 수정해야 함.

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

앞의 게시판 예제를 통해 클래스들의 의존 관계가 강하게 결합되어 있으면 여러 가지 문제가 발생 할 수 있음을 알았음. 이번에는 스프링의 의존성 주입 기능을 이용해 각 클래스들 사이의 의존 관계를 완전히 분리하는 작업을 실습해 보겠음.

  • 의존성 주입을 적용했을 때 얻을 수 있는 장점들
  1. 클래스들 간의 의존 관계를 최소화하여 코드를 단순화할 수 있음.

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

  3. 기존 구현 방법은 개발자가 직접 코드 안에서 객체의 생성과 소멸을 제어했음. 하지만 의존성 주입은 객체의 생성, 소멸과 객체 간의 의존 관계를 컨테이너가 제어함.

스프링에서 의존성 주입(이하 di)을 구현하려면 xml이나 애너테이션을 이용해 객체를 주입하여 객체들의 의존관계를 맺어주면 됨. 즉, di를 사용하여 각 객체들 간의 의존 관계를 최소화함으로써 코드를 단순화하고 유지보수를 쉽게 할 수 있음.

di는 객체의 생성, 소멸, 의존 관계를 개발자가 직접 설정하는 것이 아니라 xml이나 애너테이션 설정을 통해 경량 컨테이너에 해당하는 스프링 프레임워크가 제어함. 따라서 기존 코드에서는 개발자가 직접 객체를 제어했지만 스프링 프레임워크에서는 객체의 제어를 스프링이 직접 담당하므로 제어의 역전(이하 ioc)이라고 하는 것임.

ioc의 종류도 여러 가지이며, 일반적으로 스프링에서는 di로 ioc의 기능을 구현하므로 ioc보다는 di라는 용어를 더 많이 사용함.

다음 코드는 di를 적용해 게시판 기능을 구현한 것.

  • BoardServiceImpl.java
public class BoardServiceImpl implements BoardService {
	private BoardDAO baordDAO;
    
    // DI 적용 예시.
    public BoardServiceImpl(BoardDAO boardDAO) {
    	this.boardDAO = boardDAO;
    }
...

BoardServiceImpl 클래스는 의존하는 BoardDAOImpl 객체를 전달받기 위해 new 키워드를 사용해 객체를 생성하지 않고 생성자를 호출할 때 외부에서 객체를 주입 받아 사용했음. 이것이 바로 di를 적용한 예임.

소스 코드에서 new를 사용해 객체를 생성하는 것이 아니라 BoardServiceImpl 생성자를 호출할 때 컨테이너에 의해 주입되는 객체로 boardDAO 변수를 초기화한 것.

di를 사용하면 객체의 의존 관계를 외부에서 설정한다고 했음. 외부 설정에 의해 각 의존 객체가 어떻게 주입되는지 과정을 아래에 나타냈음.

이미지처럼 스프링에서는 의존하는 개체를 컨테이너 실행 시 주입하기 때문에 di라고 부름. 여기서 각 클래스 객체를 bean(이하 빈)이라고 부르는데, 이는 의존 관계를 설정하는 외부 xml 파일에서 각각의 객체를 bean 태그로 표시하기 때문임.

이번에는 스프링에서 의존 객체를 주입하는 방식을 알아보겠음. 스프링의 의존 객체 주입 방식은 생성자를 이용해서 주입하는 방식과 setter를 이용해서 주입하는 방식이 있음.

위에 코드는 생성자를 이용한 주입 방식이고 아래는 setter를 이용한 의존 객체 주입 방식임.

  • BoardServiceImpl.java
public class BoardServiceImpl implements BoardService {
	private BoardDAO boardDAO;
    
    // setter를 이용해 컨테이너에서 생성된 BoardDAOImpl 객체를 주입.
    public void setBoardDAO(BoardDAO boardDAO) {
    	this.boardDAO = boardDAO;
    }
...
profile
Hello World!

0개의 댓글