1장. 오브젝트와 의존관계

지하나·2021년 8월 17일
0

토비의 스프링 v1

목록 보기
1/6
post-thumbnail
  • 출처: 토비의 스프링 3.1 vol.1 스프링의 이해와 원리

스프링이 가장 관심을 두는 대상은 오브젝트다. 스프링을 이해하려면 먼저 오브젝트에 깊은 관심을 가져야 한다. 애플리케이션에서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 진지하게 생각해볼 필요가 있다. 더 나아가서 오브젝트는 어떻게 설계돼야 하는지, 어떤 단위로 만들어지며 어떤 과정을 통해 자신의 존재를 드러내고 등장해야 하는지에 대해서도 살펴봐야 한다. -본문 53p

스프링은 객체지향 설계와 구현에 관해 특정한 모델과 기법을 억지로 강요하지는 않는다. 하지만 오브젝트를 어떻게 효과적으로 설계하고 구현하고, 사용하고, 이를 개선해나갈 것인가에 대한 명쾌한 기준을 마련해준다. -본문 53p


1.1 초난감 DAO

DAO란?
Data Access Object. DB를 사용해 데이터를 조회화거나 조직하는 기능을 전담하도록 만든 객체

사용자 정보를 담는 DB가 있고, 이 데이터를 가져오고 관리하는 DAO 클래스인 UserDao가 있다고 하자.
그러면 JDBC를 이용하는 작업의 일반적인 프로세스는 다음과 같을 것이며 UserDao에 조회 결과를 담아주게 될 것이다.

1. DB 연결 connection을 만듦
2. SQL 쿼리문을 만듦
3. 쿼리문 실행함
4. 조회문일 경우, 결과 데이터를 받아와 객체에 넣어줌
5. 생성된 리소스를 close해줌
6. 예외 처리 
public class UserDao {
    public void add(User user) throws Exception {
    	Class.forName("com.mysql.jdbc.Driver");
    	Connection c = DriverManager.getConnection(
    			"jdbc:mysql://localhost/springbook", "spring", "book");

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

    	ps.close();
    	c.close();
    }
    
    public User get(String id) throws Exception {
    	Class.forName("com.mysql.jdbc.Driver");
    	Connection c = DriverManager.getConnection(
    			"jdbc:mysql://localhost/springbook", "spring", "book");

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

1.2 DAO의 분리

(언제나 그랬듯..) 개발자는 설계 시 미래의 변경 가능성을 염두에 두어야 한다. 그리고 변경에 대비하는 가장 좋은 대책은 변경의 폭을 최소한으로 줄여주는 것이다. 따라서 분리와 확장을 고려한 설계를 해야한다.

즉, 관심사의 분리가 필요하다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이다.

이를 UserDao에 적용해보자. 위에서 정리한 프로세스를 보면 UserDao가 가지는 관심사항을 3가지로 분리할 수 있는 것이다.

  • DB와의 연결 connection : DB와의 연결 문제. 어떤 DB를 쓰고 어떤 드라이버를 사용할 것인지 등
  • SQL 쿼리문의 실행 : 파라미터로 넘어온 사용자 정보를 쿼리문에 넣어서 DB를 통해 실행시키는 과정
  • 리소스 close

중복 코드의 메소드 추출

그러면 위에서 중복된 DB 연결 코드를 메소드로 뽑아서 getConnection() 메소드로 리팩토링할 수 있다. (이를 메소드 추출, extract method 기법이라고 함) 후에 DB 연결과 관련된 부분에 변경이 일어나도 관심 내용이 독립적으로 존재하므로 수정이 간단해진다.

public class UserDao {
    public void add(User user) throws Exception {
        Connection c = getConnection();
        // 이하 생략 
    }

    public User get(String id) throws Exception {
        Connection c = getConnection();
        // 이하 생략 
    }

    private Connection getConnection() throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }
}

DB 커넥션 만들기의 독립

그런데 만약 이번에는 DB 커넥션을 만드는 메소드 자체에 변경이 있다면? 예를 들어 N사 방식, D사 방식으로 구분된 메소드를 구현해야 한다면?
쉽게 생각하면 메소드의 구현 코드를 제거하고 추상 메소드로 만들어서 상속을 받아 확장하도록 설계할 수 있을 것이다.

public abstract class UserDao {
    public void add(User user) throws Exception {
        Connection c = getConnection();
        // 이하 생략 
    }

    public User get(String id) throws Exception {
        Connection c = getConnection();
        // 이하 생략 
    }

	// 구현부 없이 추상 메소드로 만듦
    public abstract Connection getConnection() throws Exception 
}

public Class NUserDao extends UserDao {
	public Connection getConnection() throws Exception {
    	// 상속을 통해 확장된 메소드 구현 
    }
}

위와 같이 구현하여 UserDao에는 코드의 수정 없이 DB 연결 기능을 정의할 수 있게 되고, 새로운 DB 연결 방법을 적용할 시, 상속을 통해 손쉽게 확장할 수 있다. 이러한 디자인 패턴을 템플릿 메소드 패턴이라고 한다.


1.3 DAO의 확장

클래스의 분리

하지만 사실 이렇게 상속을 통한 확장 패턴에는 제약이 있다. 상속의 관계는 꾀나 의존적이기 때문이다.
이번에는 DB 커넥션과 관련된 부분을 위와 같은 상속을 받아서 서브클래스에서 확장하는 방식이 아니라, 아예 별도의 클래스에 담아보자.

public class UserDao {
	private SimpleConnectionMaker simpleConnectionMaker;
    
    public UserDao() {
    	simpleConnectionMaker = new SimpleConnectionMaker();
    }
    
    public void add(User user) throws Exception {
        Connection c = simpleConnectionMaker.makeNewConnection();
    }
}

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }
}

위의 코드로 구현 시 문제가 있는데 바로,, DB 연결을 예전처럼 독립적으로 변경할 수 없고, 변경 시 UserDao에 아래 코드를 수정해야 한다.
simpleConnectionMaker = new SimpleConnectionMaker();
또한 DB 커넥션을 제공하는 클래스를 SimpleConnectionMaker로 아예 박아놓았기(?) 때문에 이 또한 변경할 수 없게 된다. 예를 들어 SimpleConnectionMaker의 메소드에 변경사항이 생기면 같이 바꾸어줘야 한다.

이는 근본적으로 UserDao가 DB 커넥션을 가져오는 클래스와의 의존성이 높다는 것을 보여준다. 그럼 어떻게 클래스로 분리하면서도 이런 문제를 해결 할 수 있을까?

인터페이스의 도입

UserDao와 SimpleConnectionMaker 두 클래스 사이의 연결을 추상화를 통해 의존성을 낮춰주는 방법이 있다.
추상화란? 공통적인 성격을 봅아내고 이를 따로 분리해내는 작업이다.
자바에서 인터페이스를 이용하면 이를 사용하는 객체(UserDao) 입장에서는 사용할 객체(SimpleConnectionMaker)가 무엇인지 자세히 몰라도 되기 때문에 느슨한 결합이 된다.

public interface ConnectionMaker {
    public Connection makeConnection() throws Exception
}

public class DConnectionMaker implements ConnectionMaker {
    public Connection makeConnection() throws Exception {
        // 구현 코드 
    }
}

public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public UserDao() {
    	connectionMaker = new DConnectionMaker();
    }
    
    public void add(User user) throws Exception {
        Connection c = connectionMaker.makeConnection();
        // 인터페이스로 가져오기 때문에 여기서는 클래스가 바뀌어도 
        // 같은 인터페이스 출신의 클래스라면 코드를 바꿔주지 않아도 된다!
    }
}

관계 설정 책임의 분리

그런데 위에는 여전히 문제점이 있는데.. UserDao가 ConnectionMaker 구현 클래스의 어떤 객체를 가져올지를 알고 있다는 점이다.
connectionMaker = new DConnectionMaker();

즉, UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계 설정에 대한 관심을 여전히 들고 있다.
그럼 이 부분을 분리해주려면 어떻게 해야 할까?

간단히 생각하면.. 오브젝트가 다른 오브젝트를 가져올 때, 즉 생성 패턴에는 위처럼 new 키워드로 가져올 수도 있지만 이를 확장하는 방법으로 팩토리 패턴이 있다. 객체의 생성 동작을 별도 클래스로 분리하여 처리하는 것이다.

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

public class UserDaoTest {
    public static void main(String[] args) throws Exception {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
    }
}

그러면 이제 UserDao를 생성할 때 인자로 넣어주면 UserDao는 DB 커넥션 객체에 대해 독립적이게 된다. UserDao에는 전혀 손대지 않고도 DB 연결 기능의 확장이 가능해졌다.

원칙과 패턴

위와 과정에서 사용한 주요 패턴들을 가볍게 훑고 넘어가면..

  • 개방 폐쇄 원칙
    객체 지향 설계 원칙 (SOLID) 중 하나. 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

  • 전략 패턴
    필요에 따라 변경이 필요한 기능은 인터페이스를 통해 외부로 분리시키고, 구현 클래스를 바꿔서 사용할 수 있게 하는 디자인 패턴


1.4 제어의 역전(IoC)

오브젝트 팩토리

그러면 이번엔 위에서 만든 UserDaoTest를 보자. 이름에서 보면 UserDao가 잘 동작하는지를 테스트하는 객체인데 DB 커넥션을 만들고, UserDao 생성하는 책임까지 수행하고 있다. 이를 앞에서 했던 방식처럼 UserDao, ConnectionMaker 생성 로직을 분리하고 UserDao에서는 외부에서 객체를 단순히 가져와서 테스트하도록 수정하자.

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

public class UserDaoTest {
    public static void main(String[] args) throws Exception {
        UserDao dao = new DaoFactory().userDao();
    }
}

이렇게 분리하면 후에 UserDao 외에 다른 DAO (예를 들어, AccountDao, MessageDao 등) 추가되었을 때마다 아래와 같이 DaoFactory에서 이를 관리해줄 수 있다는 점이다.

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

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

설계도로서의 팩토리

이렇게 분리를 하면 오브젝트들간의 역할과 관계가 확연히 분리되는 것을 볼 수 있다. 애플리케이션의 실질적인 로직을 담당하는 컴포넌트(UserDao, ConnectionMaker)와 이러한 컴포넌트의 구조와 관계를 정의하는 설계도 같은 역할(DaoFactory)로 구분할 수 있다.

다시 말하면, 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다.

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

맨 처음에 초난감 UserDao 코드에서의 제어 흐름을 보면.. 모든 오브젝트가 자신이 사용할 클래스를 직접 결정하고 직접 생성해서 언제 어떻게 만들지를 관장했다. 작업의 흐름이 사용하는 쪽에서 제어하는 구조다.

그런데 DaoFactory를 적용한 코드를 보면.. 어떤 ConnectionMaker 구현 클래스를 가져올지에 대한 결정, UserDao를 생성하고 가져오는 역할이 DaoFactory가 관장하고 UserDao는 이제 수동적으로 사용되는 입장이 되었다. UserDaoTest는 DaoFactory가 만들어서 주는대로 받아서 사용하는 입장이 되었다. 제어의 흐름이 역전된 것이다.

제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집은 것이다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다. 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특벽한 오브젝트에 의해 결정되고 만들어진다. -본문 93p

제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계 설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다. (생략) 스프링은 IoC를 모든 기능의 기초가 되는 기반기술로 삼고 있으며, IoC를 극한까지 적용하고 있는 프레임워크다. -본문 94p


1.5 스프링의 IoC

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

  • 빈(bean)이란?
    스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트. 즉, 컨테이너가 생성과 관계 설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트

  • 빈 팩토리(bean factory)란?
    빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트. 스프링 IoC를 담당하는 핵심 컨테이너. 보통 빈 팩토리를 바로 사용하지 않고 이를 확장한 애플리케이션 컨텍스트를 이용한다

  • 애플리케이션 컨텍스트(application context)란?
    빈 팩토리를 좀 더 확장한 개념으로 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업 범위 개념. IoC 컨테이너 또는 스프링 컨테이너라고도 부른다.

빈 팩토리라고 하면 주로 빈의 생성과 제어의 관점에서의 이야기이고, 애플리케이션 컨텍스트라고 하면 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함하는 것을 뜻한다.

DaoFactory를 사용하는 애플리케이션 컨텍스트

애플리케이션 컨텍스트는 별도의 정보를 참고해서 빈을 생성하고 관계를 설정해주는데 이 때 별도의 설정 정보는 스프링의 어노테이션을 통해 만들 수 있다.

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

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

public class UserDaoTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);
    }
}

@Configuration 어노테이션이 붙은 클래스를 설정 정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 되고, getBean() 메소드를 통해 ApplicationContext가 관리하는 오브젝트를 요청하여 "userDao"라는 이름으로 등록된 빈을 불러올 수 있다.

만약 클래스는 같지만 생성 방식이나 구성이 다른 빈을 가져온다면 getBean("userDao2", UserDao.class)와 같이 호출할 수 있게 되는 것이다.

사실 여기서 ApplicationContext는 BeanFactory를 상속한 것이다.

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

애플리케이션 컨텍스트는 애플리케이션 전반에 걸쳐 IoC를 적용하여 관리할 모든 오브젝트에 대한 생성과 관계설정을 관장한다. 이때 직접 DaoFactory 때처럼 코드로 직접 관리하기 보다 @Configuration 어노테이션으로 DaoFactory 클래스를 설정 정보로 등록해두고 @Bean이 붙은 메소드들로 빈 목록을 미리 만들어둔다. 후에 클라이언트가 해당 빈을 호출하면 자신의 빈 목록에서 요청한 빈을 찾아 오브젝트를 생성한 후 반환한다.

이처럼 IoC 기능을 컨텍스트 차원으로 확장하면 얻을 수 있는 장점은 무엇인가?

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다
    클라이언트 입장에서 어떠한 오브젝트를 사용하려면 어떤 팩토리 클래스를 써야하는지에 대해 몰라도 된다.

  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다
    오브젝트 생성과 관계 설정 외에도, 오브젝트가 만들어지는 방식, 시점과 전략을 빈마다 다르게 가져갈 수 있고, 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트 활용성이 커진다.

  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다


1.6 싱글톤 레지스트리와 오브젝트 스코프

그런데 앞에서 만든 DaoFactory와 스프링 애플리케이션 컨텍스트의 동작방식이 얼핏 같아 보이지만.. 같은 오브젝트를 여러번 생성해보면 사실 다르게 작동하는 것을 알 수 있다.

DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

system.out.println(dao1 == dao2);	// false
system.out.println(dao3 == dao4);	// true

싱글톤 레지스트리로서의 애플리케이션 컨텍스트

이것은 애플리케이션 컨텍스트가 별도의 설정이 없으면 빈 오브젝트를 모두 싱글톤으로 만들기 때문이다. 싱글톤으로 만드는 이유는 애플리케이션이 복잡한 레이저로 나누어진 복잡한 애플리케이션의 경우를 상상해보면 이해하기 쉽다. 매번 요청마다 새로 만든다면....흠;

그런데 또 싱글톤 패턴은 그 나름의 단점이 있다.

  • private 생성자를 갖고 있기 때문에 상속할 수 없다
  • 테스트하기가 어렵다
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다
  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다

이러한 문제점 때문에 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하는데 이를 싱글톤 레지스트리(Singleton registry)라고 한다.

싱글톤 레지스트리는 private 생성자와 static 메소드 방식이 아니라, 평범한 자바 클래스를 싱글톤으로 관리해준다. 이것이 가능한 것은 스프링에서 오브젝트 생성에 관한 모든 역할을 IoC 컨테이너가 전담하기 때문이다. 그래서 스프링에서는 오브젝트가 public 생성자를 가질 수 있고, 테스트 환경에서 자유롭게 오브젝트를 만들 수도, 목(mock)으로 대체도 가능하다.

싱글톤과 오브젝트의 상태

싱글톤은 멀티스레드 환경에서 상태 관리에 특히 주의해야 한다. 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지 방식으로 만들지 않고, 파라미터와 메소드 내에서의 로컬 변수 등으로 저장해 정보를 구분해주도록 한다.


1.7 의존관계 주입(DI)

제어의 역전(IoC)와 의존관계 주입

사실 개발자들이 사용하는 스프링 IoC의 개념이 모호한데 이를 좀 더 분명하게.. 스프링 IoC 기능의 대표적인 동작 원리를 의존성 주입(dependency Injection)이라 한다. 이는 스프링이 다른 프레임워크와 차별화돼서 제공해주는 기능을 분명하게 뜻하기 위해 사용된다.

그럼 의존 관계라는 건 무엇인가? 의존하고 있다는 건 무슨 뜻일까?

예를 들어 A가 B의 메소드를 사용하고 있다면 B에 변경 사항이 생길 경우 A에도 영향을 미칠 수 있다. 이런 걸 A가 B에 의존하고 있다고 말한다.

의존 관계에는 방향성이 존재한다. A가 B에는 의존하고 있지만 B는 A에게 의존하고 있지 않는 것처럼. 즉, 의존하지 않는다는 말은 변화의 영향을 받지 않는다는 뜻이다.

위의 UserDao 코드에서 본다면 UserDaoConnectionMaker 인터페이스에 의존하고 있는 구조였다. ConnectionMaker 인터페이스에 변화가 생긴다면 UserDao에도 영향을 줄 수 있다. 하지만 Connection 인터페이스의 구현 클래스(DConnectionMaker)에 변화가 생겨도 UserDao에는 영향을 주지 않는다. 인터페이스를 통해 의존관계를 제한하여 결합도를 낮춘 것이다.

런타임 의존관계 설정

의존 관계에는 위처럼 모델링 시점에서 드러나는 의존관계 외에, 런타임 시에 생기는 의존 관계도 있다. 이를 런타임 의존 관계, 오브젝트 의존 관계라고 한다.

위에서 처럼 인터페이스를 통해 설계 시점에 느슨한 의존 관계를 맺고 있기 때문에 UserDao 오브젝트는 런타임 시에 자신이 사용할 오브젝트가 어떤 클래스로 만든 것인지 알지 못한다. 의존 관계 주입은 구체적인 의존 오브젝트와 그것을 사용하는 오브젝트를 런타임 시에 연결해주는 작업을 말한다. 여기서 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 것이다.

이게 위의 경우에서는 DaoFactory가 되는 것이다. DaoFactoryUserDao를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 DConnectionMaker의 오브젝트를 전달한다. 정확히는 오브젝트의 레퍼런스를 전달한다. 그리고 이러한 제 3의 존재가 앞에서 이야기한 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너라고 볼 수 있다.

정리하면 의존 관계 주입은 다음의 조건을 만족해야 한다.

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

DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다. 스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다. -본문117

의존관계 주입의 응용

이번에는 DAO의 DB 커넥션 사용 횟수를 카운팅하는 기능을 추가한다고 해보자. UserDaoConnectionMaker 인터페이스하고만 의존관계이므로 ConnectionMaker 인터페이스를 받아와서 기능을 확장시키면 된다.

일단 위에서의 기존 코드를 보면..

public interface ConnectionMaker {
    public Connection makeConnection() throws Exception
}

public class DConnectionMaker implements ConnectionMaker {
    public Connection makeConnection() throws Exception {
        // 구현 코드 
    }
}

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

public class UserDaoTest {
    public static void main(String[] args) throws Exception {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
    }
}

여기에 ConnectionMaker 인터페이스를 받아와서 CountingConnectionMaker를 만들고..

public class CountingConnectionMaker implements ConnectionMaker {
    private int count = 0;
    private CounnectionMaker realConnectionMaker;
    
    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }
    
    public Connection makeConnection() throws Exception {
        this.count++;
        return realConnectionMaker.makeConnection();
    }
    
    public int getCounter() {
        return this.count;
    }
}

UserDaoDConnectionMaker 대신 CountingConnectionMaker으로 바꿔준다.

그러면 UserDao가 생성될 때마다 CountingConnectionMaker.makeConnection()이 실행되고 카운팅이 될 것이다. 그리고 다시 실제 사용할 DB 커넥션인 DConnectionMakerCountingConnectionMaker에도 주입해주면 메소드 내의 실제 DB 커넥션을 가져오는 부분인 realConnectionMaker.makeConnection()에서 DConnectionMaker.makeConnection()가 실행될 것이다.

@Configuration
public class CountingDaoFactory {
    @Bean 
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }
    
    @Bean 
    public ConnectionMaker connectionMaker() {
        return new CountingConnectionMaker(dConnectionMaker());
    }
    
    @Bean
    public ConnectionMaker dConnectionMaker() {
    	return new DConnectionMaker();
    }
}

의존성 주입으로 UserDao는 코드의 수정이 없었다는 점, 그리고 오브젝트의 주입을 DaoFactory에서 CountingDaoFactory로 바꾸는 변경으로 해결되었다는 점을 기억하자.

메소드를 이용한 의존관계 주입

위에서 UserDao의 의존관계 주입은 생성자를 사용하는 방식이었는데, 일반 메소드를 사용하는 방법도 존재한다. 당연히 이 경우, DaoFactory에서 DI를 주입하는 방식에도 변경이 필요하다.

// 생성자 주입 방식
public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public UserDao(ConnectionMaker connectionMaker) {
    	this.connectionMaker = connectionMaker;
    }
}

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

// 수정자 메소드 DI 방식 
public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public void setConnectionMaker(ConnectionMaker connectionMaker) {
    	this.connectionMaker = connectionMaker;
    }
}

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

1.8 XML을 이용한 설정

DataSource 인터페이스로 변환

지금까지 이야기했던 ConnectionMaker는 DB 커넥션을 생성하면서 IoC와 DI개념을 이해하기 위해 정의해본 인터페이스였다. 자바에서는 사실 이와 같이 DB 커넥션을 가져오는 오브젝트의 기능을 추상화한 인터페이스인 DataSource가 존재한다.

public interface DataSource extends CommonDataSource, Wrapper {
    Connection getConnection() throws SQLException;
}

여태까지 만들었던 코드로 보면 ConnectionMaker.makeConnection()과 동일한 메소드로 작용한다. 따라서 위의 UserDao에서 ConnectionMakerDataSource로 치환하면, 다음과 같다.

public class UserDao {
    private DataSource dataSource;
    
    public void setConnection(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public void add(User user) throws Exception {
        Connection c = dataSource.getConnection();
    }
}

그럼 위에서 ConnectionMaker의 구현체를 빈으로 등록해주었던 것과 마찬가지로 DataSource의 구현체를 빈으로 등록해주면 되겠다. 이 외에도 DB 연결과 풀링 기능을 갖춘 많은 DataSource 구현클래스가 존재한다.


"개인적으로 공부하면서 정리한 자료입니다. 오타와 잘못된 내용이 있을 수 있습니다."

profile
개발은 즐겁게🎶

0개의 댓글