과정을 즐기자

오브젝트와 의존관계 본문

Spring

오브젝트와 의존관계

320Hwany 2023. 3. 25. 10:07

이 글은 토비의 스프링을 읽고 정리한 내용입니다.

스프링이 자바에서 가장 중요하게 가치를 두는 것은 객체지향 프로그래밍이 가능한 언어라는 점입니다.

스프링이 가장 관심을 많이 두는 대상은 오브젝트입니다.

스프링은 오브젝트를 어떻게 효과적으로 설계하고 구현하고, 사용하고, 이를 개선해나갈 것인가에 대한 

명쾌한 기준을 마련해줍니다.

 

여러가지 관심사항을 가지고 있는 UserDao를 개선해나가는 과정을 통해 스프링에 대해서 알아보겠습니다.

 

UserDao

public void add(User user) throws ClassNotFoundException, SQLException {
    Class.forName("org.h2.Driver");
    Connection c = DriverManager.getConnection(
            "jdbc:h2:tcp://localhost/~/toby", "sa", "");

    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();
}

DB연결, SQL 전달, 공유 리소스를 시스템에 돌려주는 이러한 여러가지 관심사항을 가지고 있습니다.  

 

1. 중복 코드의 메소드 추출

 

먼저 여러가지 관심사항을 가지고 있는 UserDao를 getConnection이라는 메소드를 만들어

메소드 추출을 통해 분리하였습니다. add뿐만 아니라 다른 메소드에서도 반복되어 나타났었는데 

중복을 제거하였고 이제 DB연결 부분을 변경하고 싶으면 전체 메소드를 일일이 변경하지 않아도 됩니다.

그 관심이 집중되는 부분의 코드만 수정하면 됩니다.  

 

2. 상속을 통한 확장

 

UserDao의 메소드가 다형성을 가지기 위해서 상속을 통해 확장할 수 있습니다. 

UserDao를 추상 클래스로 만들고 이를 구현하는 구현 클래스를 만들어서 확장하는 방식입니다.  

이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한

protected 메소드 등으로 만든뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하는 방법을

디자인 패턴에서 템플릿 메소드 패턴이라고 합니다.

 

하지만 이러한 상속을 통한 확장은 문제점이 있습니다. 

(1) 자바는 클래스의 다중 상속을 허용하지 않습니다. 

(2) 상속관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용합니다.

(3) DB 커넥션을 생성하는 코드를 다른 Dao 클래스에 적용할 수 없습니다.  

 

이러한 문제점은 '이펙티브 자바 아이템 20 : 추상 클래스보다 인터페이스를 우선하라'에도 잘 나와있습니다.  

인터페이스를 통한 구현은 확장에 초점을 맞추었고 추상 클래스의 상속은 여러 클래스를 하나의 성격으로 

묶는 것에 초점을 두었습니다. 따라서 확장성있는 구조로 만들려면 추상 클래스보다는 인터페이스를 사용해야합니다.  

 

3. 인터페이스의 도입

 

UserDao를 추상 클래스로 만들지 않고 UserDao는 그대로 두고 다른 인터페이스를 만들 수 있습니다.  

 

ConnectionMaker

public interface ConnectionMaker {

    Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

UserDao

public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao() {
        this.connectionMaker = new DConnectionMaker(); // 이 부분은 아직 문제
    }

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

        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();
    }
    
    ...
    
}

 

이렇게 자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고

이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게하는 디자인 패턴이 전략패턴입니다.

 

이제 인터페이스를 도입하여 상속을 통한 확장 시의 문제점을 해결하였지만 아직 UserDao에서 구체 클래스의

정보를 알 수 있습니다. 구체 클래스 정보를 UserDao에서 결정하는 방식이 아니고 외부에서 결정하도록 해야 합니다. 

UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 해야합니다.

 

4. 제어의 역전 (IoC)

UserDao에서 다시 분리될 기능은 UserDao와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것과

그렇게 만들어진 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺어주는 것입니다. 

 

DaoFactory

public class DaoFactory {

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

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

이렇게 DaoFactory에 기능을 분리하였습니다. 이것이 제어의 역전이 적용된 것이라고 볼 수 있습니다.  

프로그램의 제어 흐름 구조가 뒤바뀐 것입니다.  

 

모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고 언제 어떻게 그 오브젝트를 만들지를 스스로

관리하던 방식에서 모든 제어 권한을 자신이 아닌 다른 대상에게 위임한 것입니다.

 

5. 스프링의 IoC

글의 도입부에서 스프링은 오브젝트를 어떻게 관리할 것인지에 큰 관심을 두고 있다고 했습니다.  

스프링은 애플리케이션 컨텍스트, 스프링 컨테이너라고 불리는 곳에서 오브젝트를 관리합니다. 

 

지금까지 만들었던 DaoFactory를 설정 정보로 참고하여 스프링 컨테이너가 생성과 관계설정,

사용 등을 제어해줍니다. 이렇게 스프링 컨테이너 안에서 만들어진 오브젝트를 스프링 빈이라고 합니다.  

 

DaoFactory

@Configuration
public class DaoFactory {

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

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

 

 

스프링 컨테이너는 DaoFactory를 설정 정보로 사용합니다. 이 메소드의 이름이 빈 이름이 됩니다. 

클라이언트가 구체적인 클래스를 알 필요없이 인터페이스에만 의존할 수 있게하고 오브젝트가 만들어지는 방식,

시점과 전략, 자동 생성, 후처리, 정보의 조합, 설정방식의 다변화, 인터셉팅 등 종합 IoC서비스를 제공합니다.  

 

이렇게 스프링 컨테이너로 관리되는 스프링 빈들은 중요한 특징이 있습니다. 

바로 싱글톤으로 관리된다는 것입니다. 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는

서버환경이기 때문에 서블릿 클래스당 하나의 오브젝트만 만들어두고 사용자의 요청을 담당하는 여러 스레드에서

하나의 오브젝트를 공유해 동시에 사용합니다.  

 

직접 싱글톤을 구현하면 상속을 할 수 없고 테스트하기 어렵다는 등 여러 문제가 있습니다. 

스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하고 이것을 싱글톤 레지스트리라고 부릅니다.  

다만 싱글톤을 사용할 때 동시성문제가 발생할 수 있으므로 개별적으로 바뀌는 정보는 스레드끼리 공유하지 않는

로컬 변수로 정의하거나, 파라미터로 주고 받으면서 사용해야 합니다.

 

6. 의존관계 주입 (DI)

 

의존관계 주입이라는 것은 3가지 조건을 만족해야 합니다.

(1) : 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다.

        그러기 위해서는 인터페이스에만 의존해야한다.

(2) : 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.

(3) : 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 주입해줌으로써 만들어진다.  

 

IoC와 DI는 같이 묶이는 개념입니다. IoC는 제어권을 제 3자에게 넘겼다는 것에 초점을 두는 것이고

DI는 이렇게 넘긴 제어권으로 외부에서 주입을 해주는 것에 초점을 맞춰 클라이언트 코드를 변경하지 않고

런타임 시점의 의존관계를 변경할 수 있다는 개념입니다.  

 

7. DataSource 인터페이스로 전환

 

지금까지 ConnectionMaker를 인터페이스를 구현하는 방식으로 DB 커넥션을 가져왔지만

자바에서는 이미 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로

사용할 수 있게 만들어진 DataSource라는 인터페이스를 제공합니다.  

 

DaoFactory

@Configuration
public class DaoFactory {

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

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

        dataSource.setDriverClass(org.h2.Driver.class);
        dataSource.setUrl("jdbc:h2:tcp://localhost/~/toby");
        dataSource.setUsername("sa");
        dataSource.setPassword("");

        return dataSource;
    }
}

 

지금까지 UserDao 코드를 리팩토링하면서 스프링의 핵심원리 중 오브젝트를 관리하는 방식에 대해 알아봤습니다. 

스프링이란 '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크'입니다.

 

출처 : 토비의 스프링 3.1 Vol.1 스프링의 이해와 원리