본문 바로가기
Framework/Spring

[토비의 스프링] IoC에 대한 이해

by 코딩하는 랄로 2023. 11. 20.
728x90

토비의 스프링을 공부하면서 이해한 내용을 요약하고 정리하기 위한 포스팅이다. 이번 글에서는 스프링의 핵심 개념 중 하나인 IoC(제어의 역전 : Inversion of Control)에 대해서 알아보겠다. 이 포스팅에서 사용되는 예제는 이전 포스팅이 "객체 지향에 대한 이해"와 이어지는 예제이다.

 

 

 

오브젝트 팩토리

이전 글에서 작성된 예제는 처음에는 객체 지향과는 거리과 먼 코드였지만, 객체지향의 개념을 하나 하나 적용해나가면서 객체지향적인 코드로 바뀔 수 있었다. 이번 글에서는 객체 지향 코드로 만들기 위해 이전 글에서 그냥 무심코 넘어갔던 부분을 되짚으면서 제어의 역전에 대한 이해의 실마리를 찾아보겠다.

 

이전 예제에서 Test를 위한 코드를 먼저 살펴보자.

public class UserTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        DBConnection dbConn = new SDBConnection();
        //의존성 주입
        UserDAO us = new UserDAO(dbConn);

        User user = new User();
        user.setId(1);
        user.setName("USER1");
        user.setNickName("Spring");

        us.add(user);

        System.out.println(user.getId() + " 등록 성공!!");

        User user2 = us.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getNickName());

        System.out.println(user2.getId() + " 조회 성공!!");
    }
}

 

DB connection에 대한 책임을 테스트 코드에게 넘겨줘서 사용하는 것을 볼 수 있다. 제어의 역전에 대한 실마리를 찾기 위해서는 해당 코드에서 한 발짝 더 나아가야 한다. 테스트 코드는 말 그대로 테스트라는 책임을 위한 코드이다. 하지만 위의 코드는 관계 설정이라는 책임 또한 지고 있는 것을 볼 수 있다.

 

이러한 책임을 분리하고자 객체의 관계를 설정하는 클래스를 따로 만들어주자.

package com.chapter1.spring.dao;

public class UserDaoFactory {
    public UserDAO userDAO() {
        DBConnection dbConn = new SDBConnection();
        UserDAO us = new UserDAO(dbConn);
        return us;
    }
}

 

위의 클래스처럼, 객체의 생성 방법을 결정하고 객체와의 연결 등을 담당하는 클래스를 오브젝트 팩토리라고 한다. 이렇게 팩토리 클래스를 선언해줌으로 객체를 사용하는 책임과 객체를 생성하는 책임이 분리될 수 있다.

 

해당 클래스를 이용하여 테스트를 진행하는 코드는 아래와 같이 사용할 수 있다.

public class UserTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDAO us = new UserDaoFactory().userDAO();

        User user = new User();
        user.setId(1);
        user.setName("USER1");
        user.setNickName("Spring");

        us.add(user);

        System.out.println(user.getId() + " 등록 성공!!");

        User user2 = us.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getNickName());

        System.out.println(user2.getId() + " 조회 성공!!");
    }
}

 

이제 UserTest 코드는 userDAO가 어떤 DB connection과 연결되어 초기화되고 생성되는지는 신경쓰지 않고 오로지 사용하는데에만 초점을 둘 수 있다.

 

 

 

팩토리에 대한 이해

위에서 사용되는 팩토리에 대해서 이해하기 위해서는 위에서 동작하는 객체들이 어떠한 관계에 있는지를 살펴봐야 한다.

클라이언트(테스트)는 UserDAO를 사용하고 UserDAO는 DB Connection과의 관계를 가진다. 하지만, UserDAO와 DB connection의 생성과 관계 설정은 Factory 클래스에서 처리된 후 클라이언트가 사용할 수 있게 해준다.

 

이를 간단하게 그림을 통해 나타내면 다음과 같다.

위의 그림을 토대로 코드의 흐름을 보면, 애플리케이션에서 비즈니스 로직을 담당하는 컴포넌트들은 다른 컴포넌트에 영향을 받지 않고(유연하고) 동작하는 것을 볼 수 있다. 그 사이에서 UserDaoFactory 클래스, 즉 factory 역할을 하는 컴포넌트는 비즈니스 로직을 담당하는 컴포넌트 사이의 관계를 정의하고 애플리케이션의 구조를 정의하는 역할을 하는 것을 볼 수 있다.

 

이처럼 팩토리는 어떤 객체가 어떤 객체를 사용해 놓는지를 지정하여 애플리케이션 전체에 걸쳐 일어나는 컴포넌트의 의존관계에 대해 설계를 하는 설계도 역할을 맡는 클래스인 것이다. 간단하게 비즈니스 로직을 수행하는 컴포넌트들이 애플리케이션이라는 건물을 짓는데 필요한 재료라면, 팩토리는 해당 재료들로 건물을 짓기 위한 설계도인 것이다.

 

 

 

이해를 넘어선 활용

이해를 넘어선 활용이라는 거창한 소제목을 붙였지만, 사실 지금까지 배운 것을 팩토리 클래스를 활용할 때에도 써먹는 것에 불과하다. 지금까지의 예제에서는 팩토리 클래스가 단순히 UserDAO의 관계 설정만을 해주었지만, 애플리케이션이 커질수록 더 많은 관계를 설정해야 한다. 

 

아래의 예제를 살펴보자.

public class UserDaoFactory {
    public UserDAO userDAO() {
        return new UserDAO(new SDBConnection());
    }
    
    public MenuDAO menuDAO() {
        return new MenuDAO(new SDBConnection());
    }
    
    public PaymentDAO paymentDAO() {
        return new PaymentDAO(new SDBConnection());
    }
}

 

만약, 위의 코드에서 문제점을 찾지 못하였다면, 아직 객체 지향에 대한 이해가 부족한 것이다. 위의 코드에서의 문제점은 만약 SDBConnection이 다른 DB connection으로 변경된다면, 해당 코드를 가진 메소드를 모두 수정해야하는 번거로움이 발생하게 된다.

 

이러한 번거로움이 발생하지 않도록 해당 중복된 코드를 분리하여 새로운 메소드로 선언해주어야 한다.

public class UserDaoFactory {
    public DBConnection dbConnection() {
        return new SDBConnection();
    }
    
    public UserDAO userDAO() {
        return new UserDAO(dbConnection());
    }
    
    public MenuDAO menuDAO() {
        return new MenuDAO(dbConnection());
    }
    
    public PaymentDAO paymentDAO() {
        return new PaymentDAO(dbConnection());
    }
}

 

이제 DB connection이 바뀌더라도 연결을 담당하는 메소드만 수정하기만 하면 된다. 다른 메소드도 DB 연결 메소드가 변하든 변하지 않든 신경쓰지 않을 수 있게 된 것이다.

 

 

 

IoC : 제어의 역전

제어의 역전에 대해서 쉽게 이해하기 위해서는 지금의 예제 코드가 어떤 식으로 흘러왔는지를 이해하는 것이 중요하다. 이러한 이해를 바탕으로 제어의 역전이 무엇인지에 대해 알아보자.

 

제어의 역전은, 이름 그대로의 의미인 제어의 흐름이 뒤바뀌었음을 의미한다. factory 클래스를 도입하기 이전에 코드들을 살펴보면, 원하는 객체의 기능을 사용하기 위해서는 해당 객체를 new 생성자를 통해 생성하는 모습을 많이 볼 수 있다. 이렇게 기존의 애플리케이션은 객체의 생성과 사용에 보다 능동적(직접적)으로 관여한다.

 

제어의 흐름을 객체를 사용하는 애플리케이션이 내부적으로 통제를 하는 것이다. 이러한 흐름을 뒤튼다라는 것은 객체를 생성하고 어떤 객체와 관계를 맺고 하는 등의 통제를 객체를 사용하는 애플리케이션이 아닌 다른 대상에게 위임을 하는 것을 의미한다. 기존의 제어의 흐름이 뒤바뀐 것이다.

 

이게 무슨 말인지 이해가 안 된다면, 위의 팩토리를 사용한 예제 코드를 살펴보자. 팩토리를 사용하기 이전에는 테스트 코드가 객체의 생성과 관계 설정 책임을 맡고 있었다. 하지만 이러한 책임을 팩토리에게 위임하여, 테스트 코드는 어떻게 객체가 생성되고 관계가 설정되는지는 신경쓰지 않고 동작할 수 있게 된 것을 확인할 수 있었다. 이 과정이 바로 제어의 역전이라는 개념이 적용된 예제인 것이다!!

 

즉, 제어의 역전은 스프링만이 가지고 있는 독창적이고 혁신적인 개념이 아닌 언제든지 사용하고 적용할 수 있는 프로그래밍 모델인 것이다. IoC를 사용하면, 유연성이 좋아지고 확장성이 좋아지기 때문에 스프링이 아니더라도 필요할 때에 해당 개념을 적용하여 코드를 설계하면 된다. ( 스프링은 이러한 IoC를 극한까지 적용하고 있기 때문에 왠만하면 스프링과 같은 IoC 프레임워크를 사용하자... )

 

 

 

프레임워크 VS 라이브러리 

흔히 많은 분들이 프레임워크와 라이브러리의 차이라고 하면 프레임워크는 제어의 역전 개념이 적용된 것이다라고 말한다. 사실 스프링을 처음 공부하고 제어의 역전에 대한 이해가 부족한 사람이 듣기에는 그 차이가 명확하게 무엇이며 어떠한 가치를 지니고 있는지 와닿지 않는다.( 저 또한... )

 

하지만, 이제는 제어의 역전에 대해 알게 되었으므로, 해당 차이점이 지니는 가치를 알 수 있게 된 것이다. 라이브러리는 애플리케이션 코드가 능동적으로 라이브러리를 사용한다. 해당 라이브러리의 기능이 필요할 때마다 애플리케이션 코드 내에서 직접 호출하여 사용하는 것이다.

 

하지만, 프레임워크는 애플리케이션의 코드가 프레임워크에 의해 사용된다. 우리가 작성한 코드가 프레임워크가 짜놓은 틀안에서 프레임워크가 짜놓은 흐름대로 수동적으로 동작하는 것이다. 짜놓은 틀에 맞춰 코드를 작성하기만 하면 되기 때문에 쉽게 애플리케이션을 제작할 수 있는 것이다.

 

 

 

 

 

참조 :  이일민, 토비의 스프링 3.1 - 스프링의 이해와 원리, 에이콘, 2012

728x90