출처 : http://www.javajigi.net/pages/viewpage.action?pageId=535
IoC(Inversion of Control)란 무엇인가?
Spring 프레임워크를 이해하기 위해서는 제일 먼저 IoC개념에 대하여 이해해야한다. IoC 개념은 Spring 프레임워크에서 처음 등장한 것이 아니라 서블릿 컨테이너와 EJB 컨테이너에서 이미 사용하고 있는 개념이다. 단지 우리 개발자들이 그 동안 이 같은 개념에 대하여 몰랐던 것 뿐이다. 최근에 다양한 Light Weight 프레임워크들이 등장하면서 모두들 IoC 컨테이너 기능을 지원하고 있다고 이야기 한다. 이와 같이 IoC 컨테이너 개념이 최근에 이슈처럼 등장하고 있는 이유는 그 동안 서블릿과 EJB 컨테이너가 가지고 있던 제약사항을 극복하고 새로운 대안의 IoC 컨테이너를 제공하자는 것이다.
우리들이 자바를 처음 시작하고 JSP + Java Beans를 이용하여 개발을 시작했을 때를 생각해보자. 모든 Java Beans에 대한 생성 권한은 누구에게 있는가? 다음의 예를 보면 극명하게 들어날 것이다. 새로운 사용자를 추가, 수정, 삭제할 수 있는 기능을 지원하는 Manager클래스를 만들어보자.
package net.javajigi.user; import java.sql.SQLException; import java.util.List; /** * 사용자 관리 API를 사용하는 개발자들이 직접 접근하게 되는 클래스. * UserDAO를 이용하여 데이터베이스에 데이터 조작 작업이 가능하도록 하며, * 데이터베이스의 데이터들을 이용하여 비지니스 로직을 수행하는 역할을 한다. * 비지니스 로직이 복잡한 경우에는 비지니스 로직만을 전담하는 클래스를 * 별도로 둘 수 있다. */ public class UserManager { private static UserManager instance = null; private UserManager() { } public static UserManager instance() { if ( instance == null ) { return new UserManager(); } else { return instance; } } public int create(User user) throws SQLException, ExistedUserException { if (getUserDAO().existedUser(user.getUserId())) { throw new ExistedUserException(user.getUserId() + "는 존재하는 아이디입니다."); } return getUserDAO().create(user); } public int update(User user) throws SQLException { return getUserDAO().update(user); } public int remove(String userId) throws SQLException { return getUserDAO().remove(userId); } public User findUser(String userId) throws SQLException, UserNotFoundException { User user = getUserDAO().findUser(userId); if (user == null) { throw new UserNotFoundException(userId + "는 존재하지 않는 아이디입니다."); } return user; } public List findUserList(int currentPage, int countPerPage) throws SQLException { return getUserDAO().findUserList(currentPage, countPerPage); } public boolean login(String userId, String password) throws SQLException, UserNotFoundException, PasswordMismatchException { User user = findUser(userId); if (!user.isMatchPassword(password)) { throw new PasswordMismatchException("비밀번호가 일치하지 않습니다."); } return true; } private UserDAO getUserDAO() { return new UserDAO(); } }
위 예제는 일반적으로 우리들이 많이 사용하는 Manager 클래스의 한 형태이다. 이 클래스를 보면 사용자 데이터의 CRUD를 담당하고 있는 UserDAO에 대한 생성을 UserManager 클래스가 담당하고 있는 것을 확인할 수 있다. 또한 UserManager는 Singleton 패턴을 지원하기 위하여 하나의 인스턴스만을 생성하도록 지원하고 있다.
그렇다면 이 UserManager 클래스를 사용하는 클라이언트 소스는 어떨까? 아마도 다음과 같을 것이다.
..... public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { //UserForm클래스를 얻는다. UserForm userForm = (UserForm) form; User user = new User(); //UserForm에 저장되어 있는 사용자 정보를 User클래스에 복사한다. PropertyUtils.copyProperties(user, userForm); //모델과 통하여 새로운 사용자를 생성. UserManager manager = UserManager.instance(); manager.create(user); return mapping.findForward("user_list"); } .....
위 클래스를 보면 InsertAction 클래스에서 새로운 사용자를 추가하기 위하여 UserManager 인스턴스를 직접 생성하는 것을 볼 수 있다. 위 예제 소스처럼 지금까지 대부분의 프로젝트에서는 위와 같은 방법으로 사용하고자 하는 자바 인스턴스를 우리 개발자들이 개발하는 소스내에서 인스턴스를 직접 생성한 다음, 사용하고, 반환하는 방법으로 애플리케이션을 개발하였다.
위와 같은 개발 방법으로 개발을 진행할 경우의 장,단점에 대해서는 추후에 논의해 보도록 하겠다.
자바가 성장하는 초반에는 위와 같이 논리적인 레이어를 두고 개발하지 않았다. JSP에서 직접 데이터베이스에 접근하는 로직을 가지는 경우도 많았으며, 비지니스 로직을 포함하는 경우까지도 종종 있었다. 이와 같이 체계화된 아키텍처 없이 개발을 진행하던 중 등장한 것이 EJB였다. EJB는 이 같은 레이어를 물리적으로 분리함으로서 개발자들이 멀티티어 구조로 개발을 진행하지 않을 수 없도록 강제시켰다. 또한 EJB 컨테이너가 등장하면서 사용되는 개념이 IoC 개념이었다.
우리들이 EJB를 개발하면서 EJB에 대한 생명주기를 우리들이 직접 Control할 수 있는가? 위에서 본 예제 코드처럼 우리가 직접 EJB를 생성한 다음, 사용하고 폐기처분 할 수 있는가? 그 동안 인스턴스에 대한 Control을 개발자들이 직접 담당해 왔는데 EJB가 등장하면서 이에 대한 Control을 컨테이너라는 놈한테 빼았겨 버렸다. 그 만큼 우리 개발자들은 EJB의 스펙에 충실하게 Business Layer(흔히 세션빈이 담당)와 Persistence Layer(원래 엔티티빈이 이 영역이지만 실행 속도의 문제로 세션빈이 대부분 담당)를 구현하지 않을 수 없게 되었다.
우리 개발자들의 자유는 그 만큼 사라지게 되었으며 우리들이 개발하는 EJB 기반의 애플리케이션은 점점 더 EJB 스펙과 EJB 컨테이너는 지원하는 외국 대형 벤더들에게 종속되어 가고 있다. 특히 EJB를 개발하기 위해서는 EJB 스펙에서 규정하는대로 EJB를 개발하고 배포해야 한다. 이는 여간 짜증나고 개발 속도의 저하를 가져오지 않을 수 없게 되었다. 또한 개발자들의 학습 곡선이 상당히 높아 짐으로서 EJB에 대한 진입 장벽 또한 발생하게 되었다.
이와 같이 인스턴스들의 생명주기를 Control하는 권한이 자바 개발자에게서 컨테이너로 넘어가게 된 것을 IoC라 일컫는다. 즉, 인스턴스 Control의 역전 현상. 인스턴스에 대한 생명주기의 Control 권한이 누구에게 있는가가 뭐그리 중요한가? 단순하게 생각하면 뭐 그리 중요할 것도 없다. 그러나 EJB의 이 같은 IoC 개념은 개발자들에게서 자유를 빼았았지만 추가적으로 개발자들이 고민해야 할 부분을 해결해주었다.
각 컴포넌트에 대한 Transaction 처리를 지금까지는 개발자들이 직접 하드코딩을 통하여 해결해왔다면 EJB 스펙에서는 IoC 개념을 가진 컨테이너가 담당하게 된다. 복잡한 애플리케이션에서 Transaction 처리는 상당히 중요한 작업이 아닐 수 없다. 이 같이 중요한 작업을 하드 코딩이 아닌 EJB를 생성할 때의 설정파일에서 선언적으로 정의함으로서 손쉽게 처리할 수 있게 되었다. 각각의 메써드에 대한 접근권한의 관리, 인스턴스 풀링등의 기능을 컨테이너가 직접 지원하게 되었다.
IoC 개념의 등장으로 인해 그 동안 Enterprise 애플리케이션을 개발하기 위하여 개발자들이 고민해야 했던 상당한 부분들을 EJB 컨테이너가 지원하게 되었다. 이 같은 기능을 지원할 수 있도록 한 것이 EJB 컨테이너가 IoC 개념을 가지고 있었기 때문이다.
EJB 컨테이너외에도 Servlet 컨테이너를 보면 비슷하다는 것을 확인할 수 있다. 우리들이 Servlet을 개발하면 개발자들이 직접 Servlet을 Control할 수는 없다. Servlet에 대한 모든 Control 권한은 Servlet 컨테이너가 가지고 있는 것이다. 그렇다면 Servlet 컨테이너 IoC 컨테이너의 일종이다라고 생각할 수 있다.
그렇다면 Spring 프레임워크가 IoC 컨테이너 기능을 가지고 있다는 것은 Spring 프레임워크도 인스턴스에 대한 Control 기능을 가지고 있다는 것이 된다. EJB 컨테이너는 EJB의 생명주기를, Servlet 컨테이너는 Servlet의 생명주기를..? 그럼 Spring 컨테이너는 무엇의 생명주기를 관리할까? Spring 컨테이너는 POJO(Plain Old Java Object)에 대한 생명주기를 관리하며, 이 POJO 클래스에 Transaction, Security, Pooling과 같은 서비스들을 지원하고 있다.
그런데 이 POJO 클래스의 생명주기를 관리하는 것이 기존의 EJB와 무엇이 다르다는 것인가? 어차피 EJB이건 POJO이건 같은 서비스를 제공한다면 굳이 새롭게 배워야하는 기술을 만들어서 개발자들을 혼란스럽게 만들 필요가 없지 않은가? 그렇다. 물론 이 둘 사이에 큰 차이가 없다면 새롭게 Spring 프레임워크가 제공하는 IoC 컨테이너 기능을 이해하지 않아도 된다. 그러나 이 둘 사이에는 개발속도, 실행속도, 테스트의 용이성등 애플리케이션 개발의 전방위 걸쳐서 큰 차이점을 가지고 있다.
Spring 프레임워크 기반하에서 애플리케이션을 개발할 때 앞에서 살펴본 UserManager를 보면 다음과 같다.
/* * Created on 2005. 5. 3. */ package net.javajigi.security.service.impl; import java.util.List; import net.javajigi.security.dao.UserDAO; import net.javajigi.security.model.User; import net.javajigi.security.service.UserManager; /** * @author 박재성 */ public class UserManagerImpl implements UserManager { private UserDAO userDAO = null; /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#setUserDAO(net.javajigi.user.dao.UserDAO) */ public void setUserDAO(UserDAO newUserDAO) { this.userDAO = newUserDAO; } /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#addUser(net.javajigi.user.model.User) */ public void addUser(User newUser) { userDAO.addUser(newUser); } /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#updateUser(net.javajigi.user.model.User) */ public void updateUser(User newUser) { userDAO.updateUser(newUser); } /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#removeUser(java.lang.String) */ public void removeUser(String userId) { userDAO.removeUser(userId); } /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#findUser(java.lang.String) */ public User findUser(String userId) { return userDAO.findUser(userId); } /* * (non-Javadoc) * * @see net.javajigi.user.service.UserManager#findUserList() */ public List findUserList() { return userDAO.findUserList(); } }
위 예제에서 눈여겨 볼 부분은 UserDAO를 생성하는 소스이다. 앞의 예제에서는 UserDAO를 UserManager내에서 직접 생성하였다. 그러나 위 예제를 보면 UserDAO를 생성하는 소스가 보이지 않는다. 그런데 UserManagerImpl 클래스를 보면 setter로 전달된 UserDAO를 모든 메써드 내에서 사용하고 있는 것을 볼 수 있다.
이것이 바로 IoC 개념의 핵심이다. 인스턴스에 대한 생성을 컨테이너에서 담당하기 때문에 각각의 클래스 내에서 사용할 클래스들을 직접 생성할 필요가 없이 컨테이너에서 생성된 인스턴스를 사용하기만 하면 된다. 각 클래스에서 필요한 인스턴스에 대한 정보는 Spring 프레임워크 설정파일에서 관리하게 된다.
Spring 프레임워크에서 어떻게 이 같은 일이 가능한지에 대한 구체적인 설명은 다음 절에서 다루도록 하겠다. 이 절에서는 기존에 컨테이너 없이 개발을 진행했을 때와 컨테이너의 등장으로 인해 애플리케이션 개발이 어떻게 변경되었으며, EJB 컨테이너와 Spring 프레임워크가 가지고 있는 IoC 컨테이너 개념에 대하여 이해하는 것이 우선이다.
Template Method 패턴에 의한 IoC
오늘 아침 지하철에서 "J2EE 설계와 패턴" 책을 읽다가 새로운 내용을 알게 되었다. Template Method 패턴을 이용하여 애플리케이션을 구축할 경우 이 또한 IoC의 한 유형이라는 것이다. 이를 이해하기 위해서는 Template Method 패턴을 이해해야 한다.
Template Method 패턴은 비지니스 로직(워크 플로우)을 제어하는 부분과 이 비지니스 로직 내에서 구체적으로 실행되어야 하는 부분들을 분리하는 것을 말한다. 사실 이처럼 아무리 설명한다고 할 지라도 이해하기 힘들 것이다. 나도 Template Method 패턴에 의하여 구현되어 있는 예제를 보기 전까지는 명확하게 피부에 와닿지 않았다. "J2EE 설계와 패턴" 책에 나와 있는 예제를 바탕으로 이 부분을 설명하도록 하겠다.
Template Method 패턴의 Base가 되는 클래스는 AbstractOrderEJB클래스로 고객이 주문 한도를 초과하지 않는지 체크하고, 대량 주문에 대해 할인 적용하는 비지니스 로직을 구현하고 있다. AbstractOrderEJB클래스의 이 같은 기능을 하는 placeOrder() 메써드를 보면 다음과 같다.
public final Invoice placeOrder(int customerId, InvoiceItem[] items) throws NoSuchCustomerException, SpendingLimitViolation { int total = 0; for( int i=0; items.lenght;i++ ) { total = getItemPrice(items[i]) * items[i].getQuantity(); } if( total > getSpendingLimit(customerId) ) { getSessionContext().setRollbackOnly(); throw new SpendingLimitViolation(total, limit); } else if ( total > DISCOUNT_THRESHOLD ) { // 합계에 할인율 적용 } int invoicId = placeOrder(customerId, total, items); return new InvoiceImpl(invoiceId, total); }
위 예제 소스에서 getItemPrice(), getSpendingLimit(), placeOrder()의 세개 메써드는 protected abstract "template method"로 구현되어 있는 메써드를 호출한다. 이 세개의 메써드들은 AbstractOrderEJB 클래스의 하위 클래스에서 구현되어야 한다. AbstractOrderEJB에서 정의되어 있는 위 세개의 메써드는 다음과 같다.
protected abstract int getItemPrice(InvoiceItem item); protected abstract int getSpendingLimit(int customerId) throws NoSuchCustomerException; protected abstract int placeOrder(int customerId, int total, InvoiceItem[] items);
이 메써드들은 AbstractOrderEJB를 상속하는 하위 클래스에서 구현하게 된다. 이와 같이 분리할 경우 상위 클래스는 비지니스 로직에 집중할 것이며, 하위 클래스는 기본적인 구현에 집중할 수 있을 것이다. 위 예제의 경우에는 JDBC와 같은 저수준 API를 이용하여 데이터베이스와의 커뮤니케이션을 담당하게 될 것이다.
그런데 이 같이 구현하는 것이 IoC이다. 이해가 되지 않는 부분이다. 이 책에서는 다음과 같이 설명하고 있다. 사용자 코드가 라이브러리 코드를 호출하는 전통적인 클래스 라이브러리들과 달리, 이 방식에서는 상위 클래스내의 코드가 하위 클래스인 사용자 코드를 호출한다.
이것이 무엇을 의미하는지를 한참을 생각했다. 한참을 고민하다가 느낀 부분은 일반적으로 우리가 상속 개념을 통해서 구현하는 방식을 어떨까를 생각해보았다. 일반적으로 상속을 하는 이유는 상위 클래스에 구현되어 있는 부분을 하위 클래스에서 사용하기 위한 것이다. 그러나 위와 방식의 경우에는 이것이 역전된 것을 볼 수 있다. 하위 클래스가 상위 클래스의 구현 메써드를 호출하는 것이 아니라 상위 클래스가 하위에 구현되어 있는 메써드를 호출하는 방식이다. 이와 같은 형태를 IoC라고 Rod Johnson은 이야기하고 있다.
또 다시 발생하는 고민거리..그럼 앞에서 이야기한 컨테이너 개념에서의 IoC와는 어떤 관계를 가지고 있을까? 아직까지 이에 대한 해결책은 찾지 못했다. 이 둘의 개념을 별개로 생각해야 할 것인지 하나로 통일해서 정리해도 될 것인지는 좀 더 고민해야 될거 같다.
'Freamwork > Explain' 카테고리의 다른 글
의존성주입의 의미와 이점 (0) | 2017.06.24 |
---|---|
프로그래밍에서 의존성이란? (0) | 2017.06.24 |
AOP 프로그래밍 이란? (0) | 2017.06.24 |
vo, dao, dto란 무엇인가 (0) | 2015.11.23 |