반응형

1. 개요

  - 토비의 스프링 1주차 스터디

  - 1장 오브젝트와 의존관계 (54p ~ 101p)

 


2. 스프링이란?

 EJB 프레임워크에 반발하여 만든 POJO 방식의 프레임워크이다. EJB에 반발, POJO 방식이란 뭘까? 이는 자바 프로그래밍의 역사와 관련이 있다.

 EJB는 Enterprise Java Bean의 약자로 엔터프라이즈급 어플리케이션 개발을 위한 사용되던 프레임워크이다. 비즈니스 로직을 엔티티 빈이라는 곳에 넣어두고 JSP에서 이를 실행시키는 서버 컴포넌트이다.

 EJB의 큰 특징은 비지니스 로직 개발에 초점을 두어 객체지향적인 프로그래밍을 하지 않았다는 것인데, 이때문에 객체가 가진 기능이 많아지고, 책임이 많아짐에 따라 객체가 '무거워지게' 되었다. 이러한 EJB의 객체지향적이지 않은 개발 방식에 반발심을 가져 POJO 라는 말이 등장했다.

 POJO란 Plain Old Java Object 가볍고 오래된 방식의 자바 오브젝트라는 뜻이다. 자바 객체는 객체 지향적으로 설계해야 하며 가벼워야한다는 자바 프로그래밍의 기본 철학을 되새기기 위해 나온 단어이다.

 EJB 기술의 혼란속에서 잃어버렸던 객체 지향 기술의 가치를 회복하고 기본으로 돌아가기 위해 만들어진 프레임워크가 바로 스프링이다.

 

3. 초난감 DAO 예제

 초난감 DAO 예제를 두번 작성하였다. 처음 작성할 때에는 관심사를 구분하는 것에 초점을 맞췄고, 두번째 작성할 때에는 스프링의 IoC와 어떻게 연결되는지에 초점을 맞췄다.

 객체지향적인 설계를 위한 첫걸음은 관심사를 분리하는 것이다. 아래 초난감 DAO인 UserDao에 대해 관심사를 분리하는 방법을 Step By Step으로 알아보았다.

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {

        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/spring","xxx","xxx");

        // 실행시킬 쿼리 생성
        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());

        // execute : 수행 결과를 Boolean 타입으로 반환
        // executeQuery : select 구문을 처리할 때 사용하며, 수행 결과를 ResultSet 타입으로 반환
        // executeUpdate : INSERT, UPDATE, DELETE 구문을 처리할 때 사용하며, 반영된 레코드 수를 int 타입으로 반환.
        ps.executeUpdate();

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

    public User get(String id) throws ClassNotFoundException, SQLException{

        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/spring","xxx","xxx");

        // 쿼리 및 파라미터 셋팅
        PreparedStatement ps = c.prepareStatement(
                "select * from users where id = ?");
        ps.setString(1, id);

        // executeQuery 메서드를 사용하여 실행 결과를 ResultSet으로 반환
        ResultSet rs = ps.executeQuery();

        // ResultSet 값이 존재하는지 확인하며 존재할 경우 해당 레코드(다음 레코드)로 이동
        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;
    }

}

 이 클래스에는 현재 두가지 관심사가 있다. 첫째는 DB와 연결을 위한 Connection을 어떻게 가져올 것인가. 둘째는 사용자 등록 및 조회를 위해 DB에 보낼 SQL을 어떻게 작성할 것인가. 셋째는 작업이 끝나면 사용한 리소스는 어떻게 반환할 것인가. 이번 예제에서는 첫째 관심사인 DB와 연결을 분리하는 것을 목표로 하였다. 

 

3.1. 중복 코드에 대한 메서드 추출

 get, add 메서드에 Connection을 가져오는 로직이 중복되어 있다. 중복된 로직은 메서드로 추출하였다. 이로써 얻을 수 있는 이점은 Connection 정보가 변경될 경우 생성한 메서드의 코드만 변경하면 된다.

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {

        Connection c = getConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException{


        Connection c = getConnection();

        ...

        return user;
    }
    
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost/spring","xxx","xxx");
    }

}

 

3.2. 상속을 통한 분리

 메서드 추출만으로 변화에 유연하게 대처할 수 있으나, 변화를 반길 순 없다. 변화를 반기는 DAO, 즉 어떤 변화에도 끄떡없는 DAO를 만들어보자.

 교재에서 가정한 변화의 예시는 다음과 같았다.

 

필자가 구현한 UserDao가 업계에 알려지면서 N사와 D사에서 사용자 관리를 위해 UserDao를 구매하겠다는 주문이 들어왔다. 그런데 납품 과정에서 문제가 발생했다. N사와 D사가 각기 다른 종류의 DB를 사용하고 있고, DB 커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용싶어한다는 점이다. 더 큰 문제는 UserDao를 구매한 후에도 DB 커넥션을 가져오는 방법이 변경될 가능성이 있다는 점이다. 
 이 경우 UserDao의 코드를 고객에게 제공해주고, 변경이 필요하면 getConnection() 메서드를 수정해서 사용하라고 할 수 있으나, 고객에게 소스를 직접 공개하고 싶지 않아 컴파일된 클래스 파일만 제공하려 한다. UserDao 소스코드를 N사와 D 사에게 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?
 

이 경우 UserDao 코드를 한단계 더 분리한다. UserDao를 추상 클래스로 만들고 getConnection() 을 추상메서드로 만드는 방법이다. 그럼 각자 UserDao를 상속받아 getConnection 메서드만 각자 회사의 독자적인 기술로 만들기만 하면 된다. 이를 상속을 통한 분리라고 한다.

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {

        Connection c = getConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException{


        Connection c = getConnection();

        ...

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

public class DUserDao extends UserDao{

    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // D사 Connection 생성 코드
    }
}


public class NUserDao extends UserDao{

    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N사 Connection 생성 코드
    }
}

 

 N사, D사에서는 각각 NUserDao, DUserDao 클래스를 만들고 UserDao를 상속받아 getConnection을 정의하면 된다. 이로써 DAO의 핵심 기능인 '어떻게 데이터를 등록하고 가져올 것인가'라는 관심을 담당하는 UserDao와, 'DB 연결 방법은 어떻게 할 것인가' 라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 분리되었다. 클래스 단위로 분리되면서 변경 작업은 한층 용이해졌다. 이제는 UserDao의 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 클래스를 만들어 사용할 수 있다.

 또한 이번 리팩토링으로 인해 UserDao와 구현 클래스는 템플릿 메서드 패턴과 팩토리 메서드 패턴이 적용되었다고 말할 수 있다.

 

템플릿 메서드 패턴
 이처럼 슈퍼클래스에 기본적인 로직의 흐름(get, add)을 만들고, 그 기능의 일부(getConnection)를 추상 메소드나 오버라이딩이 가능한 메소드로 만든 뒤 서브 클래스에서 구현하도록 하는 방법을 디자인 패턴에서 템플릿 메서드 패턴이라고 한다.
팩토리 메서드 패턴
 UserDao의 서브클래스의 getConnection() 메서드는 Connection 오브젝트를 어떻게 생성할 것인지를 결정하는 방법이라고도 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메서드 패턴이라고 한다.

 

 하지만 이 방식은 상속을 사용했다는 단점이 있다. 상속은 한계가 있다. 바로 다중 상속을 허용하지 않는다는 점이다. 만약 NUserDao와 DUserDao가 다른 목적을 위해 다른 클래스를 상속하고 있다면 UserDao를 상속받지 못하게 된다.

 또 다른 문제는 상속을 통한 상하위 클래스의 관계는 밀접하다는 점이다. 서브 클래스는 슈퍼 클래스의 기능을 직접 사용할 수 있다. 그래서 슈퍼 클래스 내부의 변경이 있을 때 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다.

 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 것도 단점이다. Dao 클래스들이 계속 만들어진다면 상속을 통해 만들어진 getConnection() 의 구현 코드가 매 DAO 클래스마다 중복될 것이다.

 

3.3. 클래스의 분리

 DB 커넥션과 관련된 부분을 서브 클래스가 아니라, 아예 별도의 클래스로 만들고 이렇게 만든 클래스를 UserDao에서 생성하여 사용하게 만든다. 이럴 경우 상속을 통한 한계점을 극복할 수 있고, 다른 DAO 클래스에도 해당 클래스를 사용하게 만든다면 구현코드의 중복도 막을 수 있다.

 

public class UserDao {

    private ConnectionFactory connectionFactory;

    public UserDao(){
        connectionFactory = new ConnectionFactory();
    }
    public void add(User user) throws ClassNotFoundException, SQLException {

        Connection c = connectionFactory.makeNewConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException{

        Connection c = connectionFactory.makeNewConnection();
        
		...
    }
}

public class ConnectionFactory {

    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost/spring","xxx","xxx");
    }

}

 

 ConnectionFactory 클래스를 만들고 UserDao에서는 ConnectionFactory 필드를 추가한다. N사에서 ConnectionFactory 방식으로 Connection을 가져오고 있다고 가정한다. 이 후 기본 생성자 주입을 통해 ConnectionFactory를 받아 add(), get() 메서드에서 사용하도록 한다.

 성격이 다른 코드를 클래스로 분리하였으나 N사와 D사에서 상속을 통해 DB 커넥션 기능을 사용했던 게 불가능해졌다. 현재 UserDao가 N사의 커넥션 생성 방식인 ConnectionFactory 클래스에 종속되어 있기 때문에  D 사에서 다른 커넥션 생성 방식, 예를들면 SimpleConnectionFactory 클래스를 만들어 사용한다고 할 경우 UserDao를 그에 맞게 또 변경해줘야하기 때문이다. 결국 처음으로 돌아와버린 격이다.

 

 이렇게 클래스를 분리한 경우에도 자유로운 확장이 가능하게 하려면 구체적인 방법에 종속되지 않도록 해야한다. 즉, ConnectionFactory, SimpleConnectionFactory와 같은 클래스와 UserDao를 이어주는 추상적인 연결고리를 만들어주는 것이다.

 

추상화
 추상화란 어떤 것들의 공통적인 성격을 뽑아내서 분리해내는 작업을 말한다. 자바가 추상화를 위해 제공하는 가장 유용한 도구는 인터페이스이다.

 

 즉, UserDao에서 사용하는 커넥션 생성 방식에 대한 객체를 만드려면 구체적인 클래스 하나를 선택해야겠지만, 인터페이스를 사용할 경우 구체적인 클래스가 정확히 무엇인지 몰라도 된다. 인터페이스를 도입해야한다.

 

3.4. 인터페이스 도입

 ConnectionMaker라는 인터페이스를 정의하고 DB Connection을 가져오는 메소드 이름을 makeConnection이라고 정한다. 이 인터페이스를 사용하는 UserDao는 ConnectionMaker 인터페이스 타입의 오브젝트라면 어떤 클래스로 만들어졌는지 상관없이 makeConnection() 메서드를 호출하기만 하면 Connection 타입의 오브젝트를 만들어서 돌려줄 것이라 기대할 수 있다.

public interface ConnectionMaker {

    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

public class DConnectionMaker implements ConnectionMaker{

    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // D사의 Connection 생성 코드
    }
}

public class NConnectionMaker implements ConnectionMaker {

    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // N사의 Connection 생성 코드
    }
}

public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao(){
        this.connectionMaker = new DConnectionMaker();
    }
    public void add(User user) throws ClassNotFoundException, SQLException {

        Connection c = connectionMaker.makeConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException{

        Connection c = connectionMaker.makeConnection();

        ...
    }
}

UserDao에서는 ConnectionMaker의 인터페이스와 인터페이스 메서드인 makeConnection()을 사용하도록 하고 N사와 D사에서는 ConnectionMaker의 인터페이스 구현체를 각자 생성하도록 하였다. 하지만 문제가 하나 있다. UserDao의 기본 생성자에 DConnectionMaker라는 클래스를 생성하여 connectionMaker에 주입하고 있다. 이렇게 된 이유는 어떤 ConnectionMaker 구현 클래스를 사용할지를 UserDao에서 결정하고 있기 때문이다. UserDao가 사용할 ConnectionMaker의 구현 클래스를 설정해주는 관심이 UserDao에 남아있다. 이러한 설정을 관계 설정이라고 한다. 이 관계설정이라는 관심을 분리해야한다.

 

3.5. 관계설정 분리

 관계를 설정하는 클래스를 만들고 그 클래스의 관심사를 ConnectionMaker 구현체를 결정하는 것으로, 즉 관계 설정 책임을 담당하는 클래스로 사용한다. 필자는 Main 이라는 클래스를 생성하고 main 메서드에서 관계를 설정해주었다.

 N사에 대한 ConnectionMaker 방식을 사용하고자 할 경우 UserDao 생성자 파라미터로 NConnectionMaker 객체를, D사에 대한 ConnectionMaker 방식을 사용하고자 할 경우 DConnectionMaker 객체를 넘겨주면 된다. 그럼 UserDao의 코드 변경 없이 확장까지 가능한 UserDao 클래스가 구현되었다.

public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
    public void add(User user) throws ClassNotFoundException, SQLException {

        Connection c = connectionMaker.makeConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException{

        Connection c = connectionMaker.makeConnection();

        ...
    }
}

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        User user = new User();
        user.setId("test");
        user.setName("테스트");
        user.setPassword("1234");

        userDao.add(user);

        ...
    }
}

 


4. 초난감 DAO 예제의 목적

 그럼 초난감 DAO 의 목적은 무엇이었을까. 필자가 생각했을 땐 객체지향 설계의 기본 원칙과 패턴들이 이런것이다 라는 것을 상기시켜주기 위함이 아닐까 생각한다.

 관심사를 분리시키다보니 객체 지향적으로 코드가 리팩토링됐다. 이 과정에서 템플릿 메서드 패턴, 팩토리 메서드 패턴, 전략 패턴이 적용되었고, 객체지향 설계 원칙 중 하나인 개방폐쇄 원칙이 적용되었다. 이렇게 리팩토리된 코드는 높은 응집도낮은 결합도를 가졌다고 표현한다.

 

개방 폐쇄 원칙
개방 폐쇄 원칙(OCP)은 객체지향 5대 원칙(SOLID) 중 하나로 '클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다'라는 원칙이다.

 

 UserDao는 DB 연결 방법 기능을 확장하는 데는 열려있다. 그에반에 UserDao의 핵심 기능, 즉 주요 관심사인 등록과 조회는 DB 연결 방법의 변화에는 영향을 받지 않기때문에 변경에는 닫혀있다라고 할 수 있다. 다시말하면 인터페이스를 통한 확장 포인트는 확장을 위해 열려있고, 인터페이스를 사용하여 주요 관심사를 수행하는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 굳게 닫혀되어 있다.

 

전략 패턴
 전략 패턴은 자신의 기능 중 필요에 따라 변경이 필요한 로직을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 로직을 필요에 따라 바꿔 사용할 수 있게 하는 디자인 패턴이다.

 

 개선한 Main - UserDao - ConnectionMaker 구조를 디자인 패턴의 시각으로 보면 전략 패턴이라고 한다. 전략 패턴의 정의를 우리가 구현한 클래스에 대입하면 다음과 같다.

 UserDao 클래스에서 변경이 필요한 DB Connection을 가져오는 로직을 ConnectionMaker라는 인터페이스를 사용해 외부로 분리시키고, 이를 구현한 구체적인 로직인 DConnectionMaker, NConnectionMaker를 필요에 따라 Main 클래스에서 바꿔 사용할 수 있게 하는 것이다.

 

높은 응집도
 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도를 가진다. 응집도가 높다는 건 하나의 클래스가 하나의 관심사에 집중되어 있다는 뜻한다. 관심사에 대한 변경이 일어날 때 해당 클래스에서 많은 변경이 일어난다면 응집도가 높고, 다른 클래스에서도 변경이 일어난다면 응집도가 낮다는 것이다.

 

 UserDao 클래스는 사용자의 데이터를 처리하는 관심사에 집중되어 있고, ConnectionMaker의 구현체는 자체적인 Connection을 생성하는 관심사에 집중되어 있다. 이렇듯 하나의 관심사가 하나의 클래스에만 응집되어 있으므로 응집도가 높다라고 할 수 있다.

 

낮은 결합도
 결합도가 낮다는 건 관심사가 다른 클래스와 느슨하게 연결된 것을 뜻한다. 여기서 결합도란 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도를 말한다.

 

 ConnectionMaker 인터페이스의 도입으로 인해 DB 연결 기능을 구한한 클래스가 바뀌더라도 UserDao의 코드는 변경될 필요가 없다. ConnectionMaker와 UserDao의 결합도가 낮다는 뜻이다. 

 


5. IoC

 IoC는 Inversion Of Controller의 약자로 제어의 역전을 뜻한다. 갑자기 이 용어가 튀어나온 이유는 작성한 예제에도 IoC가 적용되어 있기 때문이다.

 UserDao라는 클래스에서 사용하는 ConnectionMaker의 구현체는 UserDao 본인이 결정한게 아니다. Main 클래스에서 ConnectionMaker의 구현 객체를 제어하고 있다. 이를 제어의 역전이라고 한다. 제어의 역전이 적용되면 특정 오브젝트는 자신이 사용할 오브젝트를 스스로 선택하지 않게 된다. 생성하지도 않으며 자신도 어떻게 만들어지는지 알 수 없다.


6. 오프젝트 팩토리

  자신이 사용할 오브젝트를 다른 클래스에서 제어한다는 IoC의 개념을 되새기며 UserDao 코드를 리팩토링 해보자.

 현재 Main 클래스는 UserDao의 기능이 잘 동작하는지에 대한 관심 UserDao 클래스에 구현 객체를 제어하는 IoC 기능에 대한 관심까지 갖고 있다. 관심사가 하나가 아니므로 응집도가 낮고 결합도가 높은 상황이다. IoC 기능에 대한 관심사 분리를 해보자.

 분리시킬 기능을 담당하는 클래스를 만들어야하는데, 이 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것(IoC의 기능)이다. 이런 일을 하는 오브젝트를 팩토리 또는 오브젝트 팩토리라고 부른다.  

 

 팩토리 역할을 맡을 클래스를 DaoFactory라고 하고 UserDao, ConnectionMaker에 대한 생성 작업을 DaoFactory로 옮긴다.

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

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao userDao = new DaoFactory().userDao();
        User user = new User();
        user.setId("test");
        user.setName("테스터");
        user.setPassword("1234");

        userDao.add(user);

        ...
    }
}

 

 DaoFactory의 userDao 메서드를 호출하면 DConnectionMaker를 사용해 DB 커넥션을 가져오도록 한 UserDao 오브젝트를 반환한다. Main 클래스는 UserDao가 어떻게 만들어지고 초기화되는지에 대한 관심사에서 분리되었다.

 만약 DaoFactory에 UserDao가 아닌 다른 DAO의 생성 기능을 넣으면 어떻게 될까? 그럼 아래와 같이 ConnectionMaker의 구현체를 생성하여 관계를 맺어주는 코드가 중복될 것이다. DAO가 많을 경우 ConnectionMaker의 구현체를 변경해야 한다면 모든 메서드를 수정해야하는 문제가 생긴다.

public class DaoFactory {
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new DConnectionMaker(); // 중복
        return new UserDao(connectionMaker);
    }

    public AccountDao accountDao(){
        ConnectionMaker connectionMaker = new DConnectionMaker(); // 중복
        return new AccountDao(connectionMaker);
    }

    public MessageDao messageDao(){
        ConnectionMaker connectionMaker = new DConnectionMaker(); // 중복
        return new MessageDao(connectionMaker);
    }
}

 

 중복 문제 해결을 위해 메서드를 분리하는 방법을 사용하면 아래와 같이 DAO 팩토리 메서드가 많아져도 중복코드로 인한 문제가 발생하지 않는다.

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

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

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

 

 여기서 IoC 개념을 다시 한번 생각해보자. UserDao 클래스는 자기가 사용할 ConnectionMaker의 구현체를 자기가 결정하는 게 아니라 DaoFactory 클래스가 결정해주고 있다. UserDao 자신도 팩토리에 의해 수동적으로 만들어지고 자신이 사용할 오브젝트도 팩토리가 주는대로 사용하는 입장이 되었다. 바로 이게 제어의 역전 즉, IoC이다.

 IoC를 이토록 강조한 이유는 IoC를 기본 배경으로 사용하는 대표적인 프레임워크가 바로 스프링이기 때문이다. 이제 이 스프링에서 사용하는 IoC를 적용해볼 차례이다.


7. 스프링 IoC

 스프링 IoC 컨테이너에서 관리되는 자바 객체를 (bean)이라고 하며, 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(bean Factory)라고 한다. 보통 빈 팩토리보다는 이를 좀 더 확장한 용어인 애플리케이션 컨텍스트를 주로 사용한다. 빈 팩토리라는 용어는 빈을 생성하고 관계를 설정하는 IoC의 기본 기능에 초점을 맞춘 것이고, 애플리케이션 컨텍스트는 애플리케이션 전반에 걸치는 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미로 사용한다.

 DaoFactory를 스프링의 빈 팩토리가 사용할 수 있는 설정정보로 만들어야 한다. @Configuration 어노테이션을 DaoFactory에 추가하고, 오브젝트를 만들어주는 메서드에 @Bean 어노테이션을 추가한다.

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao(){
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
    }
}

 

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어야한다. 어플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트이며 @Configuration 어노테이션이 붙은 클래스를 설정정보로 사용하려면 AnnotationConfigApplicationContext 생성자를 사용하고 생성자 파라미터에 넣어주면 된다.

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        
        ApplicationContext applicationContext = 
        	new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao userDao = applicationContext.getBean("userDao", UserDao.class);
        User user = new User();
        user.setId("test");
        user.setName("테스터");
        user.setPassword("1234");

        userDao.add(user);

        ...
    }
}

 

getBean 메서드는 ApplicationContext가 관리하는 빈을 요청하는 메서드이다. userDao는 ApplicationContext에 등록된 빈의 이름이며, DaoFactory에서 @Bean 어노테이션이 붙은 메서드이다. 메서드 이름이 바로 빈의 이름이 되는것이다.

 애플리케이션의 동작 방식은 다음과 같다.

토비의 스프링 Application Context 구조 참고

 1) 애플리케이션 컨텍스트 객체 생성 시 DaoFactory 클래스를 설정정보로 등록해두고, @Bean이 붙은 메소드의 이름을 가져와 빈 목록을 만들어둔다.

 2) 클라이언트가 애플리케이션 컨텍스트의 getBean() 메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾는다.

3) 있다면 빈을 생성하는 메서드를 호출해서 오브젝트를 생성시킨 후 클라이언트에게 반환한다. 

 

 * 이 시점에 생성된 오브젝트는 스프링 컨테이너에 생성된 오브젝트 즉 빈(bean)이며, 스프링 컨테이너 빈의 스코프는 기본적으로 싱글톤이다. 때문에 똑같이 getBean() 메서드를 한번 더 호출할 경우 오브젝트를 생성하지 않고 이미 등록된 빈을 리턴한다.

 

참고로 추가한 스프링 라이브러리는 다음과 같다. (gradle)

    implementation 'cglib:cglib:3.3.0'
    implementation 'org.apache.commons:com.springsource.org.apache.commons.logging:1.1.1'
    implementation 'org.springframework:spring-asm:3.1.4.RELEASE'
    implementation 'org.springframework:spring-beans:3.1.4.RELEASE'
    implementation 'org.springframework:spring-context:3.1.4.RELEASE'
    implementation 'org.springframework:spring-core:3.1.4.RELEASE'
    implementation 'org.springframework:spring-expression:3.1.4.RELEASE'

 


8. 회고

 스프링을 어느정도 써봤고, 스프링에 대한 용어를 어느정도 알고 있는 상태에서 이 책을 한번 더 읽으니 첫번째 읽었을 때보다 이해되고 공감되는 부분이 많았다. 복잡하고, 불필요하게만 느껴졌던 내용들도 글쓴이가 무슨 의도로 이러한 말을 했는지 이해하게 되었다.

 분량이 50페이지 정도이고 처음 부분이라 몇 시간정도면 끝낼 줄 알았던 게 이틀이나 걸려 당황스러웠다(그만큼 배운게 많았다 ㅎㅎ). 2주차 스터디는 여유있게 시간을 더 투자하여 많은 내용들을 머릿속에 재정립할 수 있도록 해야겠다.

 

반응형

+ Recent posts