Coding/개발 서적

[토비의 스프링 3.1 Vol 1] 01장. 오브젝트와 의존 관계

kangplay 2025. 1. 23. 14:23

들어가며..

'들어가며'는 토비의 스프링 3.1 Vol 1 을 어떤 것에 집중하여 읽어나가야하는지 설명해준다.

토비의 스프링 3.1 Vol 1은 스프링의 핵심 가치와 원리에 대한 이해를 중심으로 다룬다. 스프링에는 가장 중요한 핵심 가치와 그것이 가능하도록 도와주는 세 가지 핵심 기술이 있다. 또한 스프링이 강조하는 중요한 프로그래밍 모델이 있다. 이를 자세히 공부하고 스프링을 일관된 방식으로 이해할 수 있는 눈을 갖도록 하는 것이 이 책의 목표이다.

 

스프링을 통해 얻게 되는 두 가지 중요한 가치는, 단순함과 유연성이다. 이 가치를 위해 클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작하게 만든다. 따라서 스프링을 사용하려면 먼저 스프링 컨테이너를 다루는 방법과 스프링 컨테이너가 애플리케이션 오브젝트를 사용할 수 있도록 설정 정보를 작성하는 방법을 알아야한다.

(참고로, 스프링 컨테이너의 역할은 크게 스프링 빈 생성 및 등록과 의존 관계 주입이다.)

 

또한, 스프링은 다음과 같은 세 가지 핵심 프로그래밍 모델을 지원한다.

  1. IoC/DI: 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델로, 스프링 프레임워크에서 동작하는 코드는 IoC/DI 방식을 따라서 작성돼야 스프링이 제공하는 가치를 제대로 누릴 수 있다.
  2. 서비스 추상화: 스프링을 사용하면 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있다.
  3. AOP: AOP는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델로, 스프링은 AOP를 이용해서 다양한 엔터프라이즈 서비스를 적용하고도 깔끔한 코드를 유지할 수 있게 해준다.

스프링에서 가장 관심을 많이 두는 대상은 오브젝트다. 1장은 오브젝트 기술적인 특징과 사용 방법을 넘어서 오브젝트 설계와 구현, 동작원리에 집중한다. 객체지향 설계의 기초와 원칙을 비롯해서 재활용 가능한 설계 방법인 디자인 패턴, 지속적으로 개선해나가는 작업인 리팩토링, 효과적으로 검증하는 데 쓰이는 단위테스트가 포함된다.

1-1. 초난감 DAO

DAO(Data Access Object)는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.

먼저, 사용자 정보를 저장할 User 클래스를 만든다.

public class User {
    String id;
    String name;
    String password;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

그 다음, User 오브젝트에 담긴 정보가 실제로 보관될 DB 테이블을 하나 만든다.

마지막으로, 사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만든다.

💡 JDBC에 대한 설명
애플리케이션 서비스는 "1.커넥션 연결 2.SQL 전달 3.결과 응답"의 과정을 통해 데이터베이스를 사용한다. 각각의 데이터베이스마다 이 과정을 하는 방법이 모두 다른데, 이를 인터페이스로 표준화한 것이 JDBC(Java Database Connectivity)이다. 

JDBC를 이용하는 작업의 일반적인 순서는 다음과 같다.
1. DB 연결을 위한 Connection을 가져온다.
2. SQL을 담은 statement(SQL문을 DB에 전송하는 방법을 정의한 인터페이스)를 만들고 실행한다.
3. SQL 실행 결과를 ResultSet(SQL 실행 결과를 조회할 수 있는 방법을 정의한 인터페이스)으로 받아서 저장할 오브젝트(User)에 옮겨준다.
4. 작업 중 생성된 리소스를 닫아주고, 예외를 처리한다.
public class UserDao {

    public void add(User user) throws SQLException, ClassNotFoundException {
	// 사용할 DBMS 정의
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";
		
    // 커넥션 연결
        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

	// SQL문 전달
        PreparedStatement ps = c.prepareStatement(
                "insert into users(id, name, password) values (?, ?, ?)"
        );
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();
		
        // 리소스 정리
        ps.close();
        c.close();
    }

    public User get(String id) throws SQLException, ClassNotFoundException {
    // 사용할 DBMS 정의
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

	// 커넥션 연결
        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );
		
    // SQL문 전달
        PreparedStatement ps = c.prepareStatement(
                "select * from users where id = ?"
        );
        ps.setString(1, id);
		
    // 결과 응답
        ResultSet rs = ps.executeQuery();
        rs.next();

        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
		
        // 리소스 정리
        rs.close();
        ps.close();
        c.close();

        return user;
    }    
}

그런데 이 클래스가 제대로 동작하는지 어떻게 확인할 수 있을까?

main 메소드를 만들고 그 안에서 UserDao의 오브젝트를 생성해서 add()와 get() 메소드를 검증해보자.

public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao dao = new UserDao();

        User user = new User();
        user.setId("1");
        user.setName("제이크");
        user.setPassword("jakejake");

        dao.add(user);

        System.out.println(user.getId() + " register succeeded");

        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());

        System.out.println(user2.getId() + " query succeeded");
    }

지금 만든 UserDao 클래스 코드에는 여러 가지 문제가 있다. 초난감 DAO 코드를 객체지향 기술의 원리에 충실한 스프링 스타일의 코드로 개선해보겠다.

1.2 DAO의 분리

1.2.1 관심사의 분리

개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 

관심사의 분리를 통해 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있다.

즉, 관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 괌심에 효과적으로 집중할 수 있게 만들어주는 것이다.

1.2.2 커넥션 만들기의 추출

UserDao의 add 메소드에서 다음과 같은 세 가지 관심 사항을 발견할 수 있다.

  1. DB와 연결을 위한 커넥션을 어떻게 가져올까
  2. DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
  3. 작업이 끝나면 공유 리소스를 시스템을 돌려주는 것

첫 번째 관심사를 보면, 현재 다른 관심사와 섞여서 add() 메소드에 담겨 있다. 또한, get()에 동일한 코드가 중복되어 있다.

커넥션을 가져오는 중복된 코드를 분리해보자. 

public void add(User user) throws SQLException, ClassNotFoundException {
        // 중복 코드의 메소드 추출
        Connection c = getConnection();

        PreparedStatement ps = c.prepareStatement(
                "insert into users(id, name, password) values (?, ?, ?)"
        );
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

// 커넥션 가져오기 관심사
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );
    }

메소드 추출만으로도 변화에 유연하게 대처할 수 있는 코드를 만들었다.

1.2.3 DB 커넥션 만들기의 독립

만약, 서로 다른 2개의 DB 커넥션을 이용하고, DB 커넥션을 가져오는 데 있어 다른 방법을 적용하고 싶다면 어떻게 될까. 또한, DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다면?

 

기존 UserDao 코드를 한 단계 더 분리하면 된다. 바로 상속을 통해 서브 클래스로 분리해버리는 것이다. 

public abstract class UserDao {
    public void add(User user) throws SQLException, ClassNotFoundException {
        Connection c = getConnection();

        // 코드..
    }

    public User get(String id) throws SQLException, ClassNotFoundException {
        Connection c = getConnection();

        // 코드 ..
    }

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class NUserDao extends UserDao{
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

        return c;
    }
}

UserDao는 Connection 인터페이스 타입의 오브젝트라는 것외에 어떤 식으로 Connection 기능을 제공하는지에 관심을 두지 않는다. 서브 클래스가 서버의 DB 커넥션 풀에서 가져올 수도 있고, 드라이버를 직접 이용해 새로운 DB 커넥션을 만들 수도 있다. 즉, 관심사항을 분리해 상하위 클래스에 나눠 담도록 한 것이다.

 

이렇게 슈퍼 클래스(UserDao)에서 기본적인 로직의 흐름(커넥션 가져오기, SQL 전달, 결과 응답)을 만들고, 그 기능의 일부를 서브 클레스에서 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메소드 패턴이라고 한다. 

또는, 팩토리 메소드 패턴으로도 볼 수 있는데 알고리즘 구조의 정의를 슈퍼 클래스에서 하는지 여부에만 차이가 있다. 

 

하지만 이 방법은 상속을 사용했다는 단점이 있다. 

일단, 자바는 클래스의 다중상속을 허용하지 않는다. 따라서, 후에 다른 목적으로 UserDao에 상속을 적용하기 힘들다.

두 번째로, 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다. 서브 클래스는 슈퍼 클래스의 기능을 직접 사용할 수 있다. (슈퍼클래스 변경 시 서브 클래스에 영향이 간다.)

세 번째로, 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다.

1.3 DAO의 확장

관심사가 다르다는 것은, 변화의 성격이 다르다는 것이고, 이는 변화의 이유와 시기, 주기 등이 다르다는 것이다. 

1.2 장에서의 리팩토링은, 변화의 성격이 다른 것을 분리해서 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 하지만, 상속을 사용했다는 단점이 있다.

1.3.1 클래스의 분리

이번에는 아예 상속 관계도 아닌 완전히 독립적인 클래스로 만들어보겠다. (DB 커넥션과 관련된 부분을 서브 클래스가 아니라, 아예 별도의 클래스에 담는다.)

생성자에서 SimpleConnectionMaker의 오브젝트를 만들어두고, add/get 메소드에서 이를 이용해 DB 커넥션을 가지고 오면 된다.

public class UserDao {

    SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        this.simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws SQLException, ClassNotFoundException {
        Connection c = simpleConnectionMaker.makeNewConnection();

        // 코드
    }

    public User get(String id) throws SQLException, ClassNotFoundException {
        Connection c = simpleConnectionMaker.makeNewConnection();

        // 코드
    }
}

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "iwaz123!@#";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

        return c;
    }
}

이번에는, 다음과 같은 문제가 생긴다.

  • 클래스의 메소드 명이 변경됐다면, UserDao 내에 있는 add(), get() 메소드의 커넥션을 가져오는 코드를 일일이 변경해야한다. 
  • UserDao가 DB 커넥션을 제공하는 클래스가 어떤 것인지를 구체적으로 알고 있어야한다.

1.3.2 인터페이스의 도입

이 문제를 해결하기 위해서, 두 개의 클래스가 서로 긴밀하게 연결되어있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주면 된다. 자바가 추상화(공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업)를 제공하는 가장 유용한 도구는 바로 인터페이스이다.

public interface ConnectionMaker {
    Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

public class UserDao {
	private ConnectionMaker connectionMaker;
}
    public UserDao() {
        this.connectionMaker = new DConnectionMaker();
    }
}

하지만, 초기에 한 번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 제거되지 않고 남아있다.

1.3.3 관계설정 책임의 분리

여전히 UserDao에는 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 관심사를 담고 있다.

즉, 클래스 사이에 관계가 만들어진 것인데, 관심사를 분리하기 위해서는 클래스가 아니라 오브젝트와 오브젝트 사이의 관계를 설정해줘야 한다. 

 

오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다. 직접 생성자를 호출해서 오브젝트를 만드는 방법도 있지만 외부에서 만들어준 것을 메소드 파라미터 등을 이용해 가져오는 방법도 있다. 

코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있고, 이는 오브젝트 사이에 다이내믹한 관계가 만들어지는 것이다.

바로 객체지향 프로그램에서는 다형성이라는 특징이 있는 덕분이다.

클라이언트는 자기가 UserDao를 사용해야할 입장이기 때문에 UserDao의 세부 전략이라고도 볼 수 있는, ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 오브젝트를 생성해서 UserDao와 연결해줄 수 있다.

1.3.4 원칙과 패턴

앞서 변화한 코드는 사실 잘 알려진 원칙과 패턴을 이용한 것이다.

  • 개방 폐쇄 원칙(OCP, Open-Closed Principle)
    • 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀있어야한다.
    • 인터페이스를 통해 제공되는 확장 포인트는 확장을 위해 활짝 개방되어 있는 반면, 인터페이스를 이용하는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 굳게 폐쇄되어 있다.
  • 높은 응집도와 낮은 결합도
    • 응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어있다는 뜻이다. 즉, 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수도 있다. (관심사가 같으면 변화의 성격이 같다는 뜻이므로)
    • 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결된 형태를 유지하는 것, 즉 변경이 전파되지 않는 상태가 낮은 결합도이다. (여기서 결합도란, 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도이다.
  • 전략 패턴
    • 전략 패턴은, 자신의 기능 맥락에서 필요에 따라 변경이 필요한 부분을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
    • 컨텍스트(UserDao)를 사용하는 클라이언트(UserDaoTest)는 컨텍스트가 사용할 전략(ConnectionMaker를 구현한 클래스)을 컨텍스트 생성자 등을 통해 제공해주는 게 일반적이다.

1.4 제어의 역전(IoC)

1.4.1 오브젝트 팩토리

클라이언트인 UserDaoTest는 UserDao의 기능이 잘 동작하는지 테스트하려고 만든 클래스인데, 지금 ConnectionMaker 구현 클래스를 결정하는 기능을 맡고 있다. 따라서 분리시킬 기능을 담당할 기능을 하나 만들어주어야 한다.

 

객체 생성 방법을 결정하고, 그렇게 만들어진 오브젝트를 돌려주는 역할을 하는 오브젝트를 흔히 팩토리라고 부른다. (디자인 패턴에서 말하는 추상 팩토리 패턴이나 팩토리 메소드 패턴과는 다르니 혼동하지 말자.)

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }
}

public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {

        UserDao dao = new DaoFactory().userDao();

        User user = new User();
        user.setId("1");
        user.setName("제이크");
        user.setPassword("jakejake");

        dao.add(user);
    }
}

이렇게, UserDaoTest는 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경쓰지 앟고 팩토리로부터 UserDao 오브젝트를 받아다가 자신의 관심사인 테스트에 활용하면 그만이다!

애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리

1.4.2 오브젝트 팩토리의 활용

다른 DAO의 생성 기능을 넣는다면, 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능이 중복돼서 나타난다.

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(new DConnectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(new DConnectionMaker());
    }
}

따라서, 이를 메소드 추출 기법을 활용해 별도의 메소드로 분리해내자.

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(getConnectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(getConnectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(getConnectionMaker());
    }

    private DSimpleConnectionMaker getConnectionMaker() {
        return new DConnectionMaker();
    }
}

1.4.3 제어권 이전을 통한 제어관계 역전

초기 코드를 보면, 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 어제 어떻게 그 오브젝트를 만들지를 스스로 관장한다. 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조다.

 

제어의 역전이란, 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다. 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 위임 받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.

자신이 어떻게 만들어지고 어떻게 사용되는지 알 수 없다가 무슨 소릴까?

 

서블릿과 프레임워크도 제어의 역전 개념이 적용된 기술이다. 서블릿을 개발자가 직접 제어할 수 없고, 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메소드를 호출한다. 애플리케이션 코드가 직접 애플리케이션 흐름을 제어하는 라이브러리와 다르게 애플리케이션 코드가 프레임 워크에 의해 사용된다. 

템플릿 메소드 패턴은 왜 제어의 역전인가?

 

UserDao는 이제 DaoFactory가 정해주는 구현체를 이용해 지정된 행위를 수행하는 수동적인 클래스가 되었다. UserDaoTest도 DaoFactory가 공급해주는 DAO를 사용할 수 밖에 없다. DAO와 ConnectionMaker의 구현체를 생성하는 책임은 모두 DaoFactory에 있다. 이것이 제어의 역전이 일어난 상황이다.

 

제어의 역전에서는 프레임 워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계 설정, 사용, 생명 주기 관리 등을 관장하는 존재가 필요하다. 단순한 적용이라면 DaoFactory와 같이 IoC 제어권을 가진 오브젝트를 분리해서 만드는 방법이면 충분하겠지만, IoC를 애플리케이션 전반에 적용하려면 스프링과 같은 IoC 프레임워크의 도움을 받는 편이 훨씬 유리하다.

1.5 스프링의 IoC

1.5.1 오브젝트 팩토리를 이용한 스프링 IoC

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하느 오브젝트를 빈(Bean)이라고 부른다. 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 부르고, 이를 확장한 애플리케이션 컨텍스트를 주로 사용한다. 

 

애플리케이션 컨텍스트는 별도의 정보를 참고해서 빈의 생성, 관계 설정 등의 제어 작업을 총괄한다.

DaoFactory를 스프링의 빈 팩토리가 사용할 수 있는 설정 정보로 만들어보자.

  • 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration이라는 애노테이션을 추가한다.
  • 오브젝트를 만들어주는 메소드에는 @Bean이라는 애노테이션을 추가한다.
@Configuration 
public class DaoFactory {

    @Bean 
    public UserDao userDao() {
        return new UserDao(getConnectionMaker());
    }

    @Bean 
    public DConnectionMaker getConnectionMaker() {
        return new DConnectionMaker();
    }
}

이제 DaoFactory를 설정 정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.

ApplicationContext를 구현한 클래스는 여러 가지가 있는데 DaoFactory처럼 @Configuration이 붙은 자바 코드를 설정 정보로 사용하려면 AnnotaionConfigApplicationContext를 이용하면 된다.

public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(DaoFactory.class);
                
	// getBean의 두 번째 파라미터에 리턴 타입을 주면, 
        // 매번 리턴되는 오브젝트에 다시 캐스팅을 해줘야 하는 부담이 없다.
        UserDao userDao = applicationContext.getBean("userDao", UserDao.class);
  
        User user = new User();
        user.setId("2");
        user.setName("제이크2");
        user.setPassword("jakejake");

        userDao.add(user);
    }
}

지금까지만 보면, 굳이 스프링을 사용하지 않고 DaoFactory 같은 오브젝트 팩토리를 만들어서 사용하면 되는 게 아닐까? 물론 그렇지 않다. 스프링은 지금 우리가 구현했던 DaoFactory를 통해서는 얻을 수 없는 방대한 기능과 활용 방법을 제공해준다.

1.5.2 애플리케이션 컨텍스트의 동작방식 

DaoFactory가 UserDao를 비롯한 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 하는 데 반해, 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계 설정을 담당한다. 대신, ApplicationContext 에는 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻는다. 

🌱 컨테이너 또는 IoC 컨테이너
IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너 라고도 한다. 애플리케이션 컨텍스트는 그 자체로 ApplicationContext 인터페이스를 구현한 오브젝트를 가르키기도 하는데, 애플리케이션 컨텍스트 오브젝트는 하나의 애플리케이션에서 보통 여러 개가 만들어져 사용되어, 이를 통틀어서 스프링 컨테이너라고 부를 수 있다.

1.7 의존관계 주입(DI)

1.7.2 런타임 의존관계 설정

의존한다는 건 의존대상, 즉 여기서는 B가 변하면 그것이 A에 영향을 미친다는 뜻이다.

의존 관계란 한 쪽의 변화가 다른 쪽에 영향을 주는 것이므로, 인터페이스를 통해 의존 관계를 제한해주면 그만큼 변경에서 자유로워진다.

 

의존 관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체를 런타임 시에 연결해주는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존 관계가 드러나지 않는다. (대신 컴파일 타임 의존성) 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계를 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

따라서, 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제3의 존재라고 할 수 있다.

스프링 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다.

ApplicationContext 초기화 → 빈 등록 → 의존성 주입(DI) → 애플리케이션 실행