본문 바로가기
챕터정리방

[1장] 오브젝트와 의존관계

by 공부중중 2021. 12. 8.

스프링은 오브젝트를 어떻게 효과적으로 설계하고 구현하고 사용하고 이를 개선해나갈 것인가에 대한 명쾌한 기준을 마련해준다. 동시에 프레임워크를 제공한다.

 

1. 초난감 DAO

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

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스이며, 사용자 정보를 생성하고, 읽어오는 두 개의 메소드를 먼저 만들었다.

public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
   Class.forName("org.h2.Driver");
   Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "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();
 }
 public User get(String id) throws ClassNotFoundException, SQLException {
   Class.forName("org.h2.Driver");
   Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/test", "sa", "");

   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() 메소드를 사용해 스스로 검증하도록 하는 방법이다.

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

User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");

dao.add(user);

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

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

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

 

2. DAO의 분리

해당 UserDao 클래스 코드의 문제점은

   1. 예외상황이 존재하지 않는다.

   2. add() 메소드에 있는 DB 커넥션을 가져오는 코드와 동일한 코드가 get() 메소드에도 중복되어있다.

 

이 문제를 해결하기 위해 가장 먼저 할 일은

   1. 커넥션을 가져오는 중복된 코드를 분리한다.

   2. 중복된 DB 연결 코드를 getConnection()이라는 이름의 독립적인 메소드로 만들어둔다.

 

그 결과 로그인 정보가 변경돼도 앞으로는 getConnection()이라는 한 메소드의 코드만 수정하면 된다.

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();
    ...
  }
  
  private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
    	"jdbc:mysql://localhost/springbook", "spring", "book");
    return c;
  }
}

이런 작업을 리팩토링(refactoring)이라고 한다. 또한 위에서 사용한 getConnection()이라고 하는 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출기법이라고 부른다.

 

리팩토링은 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술이다.

이후 사용하는 DB자체를 바꾸려면 getConnection()을 abstract method로 수정하고, 그 구현부를 상속을 받아 구현하는 식으로 리팩토링할 수 있다.

 

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();
  ...
  }
  
  public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
public class NUserDao extends UserDao{
  private Connection getConnection() throws ClassNotFoundException, SQLException {
 	 // N 사 DB Connection 생성 코드
  } 
}

public class DUserDao extends UserDao{
  private Connection getConnection() throws ClassNotFoundException, SQLException {
 	 // D 사 DB Connection 생성 코드
  } 
}

이 코드의 장점은

   1. 클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업은 한층 용이해졌다.

   2. UserDao의 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 새롭게 정의한 클래스를 만들 수 있다.

   3. 새로운 DB 연결방법을 적용해야 할 때는 UserDao를 상속을 통해 확장해주기만 하면 된다.

 

이러한 방법은 템플릿 메소드 패턴이라고 한다.

템플릿 메소드 패턴은 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법이다.

 

그리고 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 한다.

 

3. DAO의 확장

위 코드의 단점은

   1. 만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면, 자바는 클래스의 다중상속을 허용하지 않기때문에 사용할 수 없다.

   2. 만약 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수 있다. 

 

위의 코드의 단점을 해결하기 위해서 상속관계도 아닌 독립적인 클래스를 만들었다. DB 커넥션과 관련된 부분을 서브클래스가 아니라, 아예 별도의 클래스에 담았고, 이렇게 만든 클래스를 UserDao가 이용하게 했다.

public class SimpleConnectionMaker {
  private SimpleConnectionMaker simpleConnectionMaker;
  
  public UserDao {
    simpleConnectionMaker = new SimpleConnectionMaker();
  }
  
  public void add(User user) throws ClassNotFoundException, SQLException {
    Connection c = simpleConnectionMaker.makeNewConnection();   
    ...
  }
  
  public User get(String id) throws ClassNotFoundException, SQLException {
    Connection c = simpleConnectionMaker.makeNewConnection();
    ...
  }
}
public class SimpleConnectionMaker {
  public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
    			"jdbc:mysql://localhost/springbook", "spring", "book");
    return c;
  }
}

이 코드의 문제는 UserDao가 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있다. 따라서 UserDao는 DB 커넥션을 가져오는 구체적인 방법에 종속되어 버린다.

 

이 문제를 해결하기 위해 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 Interface를 사용했다.

public interface ConnectionMaker {
  public Connection makeConnection() throws ClassNotFoundException, SQLException;
 }
 
 public class DConnectionMaker implements ConnectionMaker {
   public Connection makeConnection() throws ClassNotFoundException, SQLException {
   // D 사의 독자적인 방법으로 Connection을 생성하는 코드
   }
 }
 
 public class UserDao {
   private ConnectionMaker connectionMaker;
   
   public UserDao() {
   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();
   ...
   }
}

문제점 : 생성자에 있는 코드는 그대로 남아있다.

connectionMaker = new NConnectionMaker();

이유 : UserDao안에 아직 분리되지 않은 관심사항(본인이 사용할 ConnectionMaker의 특정 구현 클래스는 무엇인지)이 있기때문이다.

해결방법 : UserDao가 가지고 있던 관심사항의 책임을 클라이언트로 떠넘기는 작업을 해야한다.

public class UserDaoTest {
	public static void main(Stirng[] args) throws ClassNotFoundException, SQLException {
    	ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
    }
}

 

4. 원칙과 패턴

객체지향 설계 원칙(SOLID)

  • SRP(Single Responsibility Principle) : 단일 책임 원칙
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존관계 역전 원칙

높은 응집도

변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것이다. 즉 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 볼 수 있다.

낮은 결합도

하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도이다.

전략 패턴

자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부에 분리시키고, 이를 구현한 구체적인 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게하는 디자인 패턴이다.

 

4. 제어의 역전(loC)

현재 코드의 문제점은 UserDaoTest는 기존에 UserDao가 직접 담당하는 기능, 즉 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능을 갖고있다. 하지만 UserDaoTest는 UserDao의 기능이 잘 동작하는지를 테스트하는 메소드이다. 따라서 분리를 해야한다.

 

팩토리

팩토리는 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려줄 때의 일을 하는 오브젝트를 말한다.
public class DaoFactory {
  public UserDao userDao() {
    ConnectionMaker connectionMaker = new DConnectionMaker();
    UserDao userDao = new UserDao(connectionMaker);
  }
}
public class UserDaoTest {
	public static void main(Stirng[] args) thorws ClassNotFoundException, SQLException {
    UserDao dao = new DaoFacotry().userDao();
    ...
    }
}

UserDaoTest는 팩토리로부터 UserDao 오브젝트를 밭아다가, 자신의 관심사인 테스트를 위해 활용하면 된다.

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

 

제어의 역전

1. 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않으며 생성하지도 않는다.
2. 모든 제어 권한을 다른 대상에게 위임하기 때문에 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다.

라이브러리와 프레임워크

라이브러리를 사용하는 애플리케이션 코드가 애플리케이션 흐름을 직접 제어한다. 프레임워크는 애플리케이션 코드는 프레임워크에 의해 사용되기만 할 뿐, 흐름의 제어는 프레임워크가 한다.

 

5. 스프링의 loC

  • 빈(Bean) : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트이다.
  • 빈 팩토리(bean factory) : 빈의 생성과 관계설정 같은 제어를 담당한다.
  • 애플리케이션 컨텍스트(application context) : 별도의 정보를 참고해서 빈의 생성, 관계설정 등의 제어 작업을 총괄한다.

스프링 빈 팩토리가 사용할 설정정보를 담은 DaoFacotry 클래스

  • @Configuration : 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식한다.
  • @Bean : 오브젝트 생성을 담당하는 loC용 메소드라는 표시
@Configuration 
public class DaoFactory {
  @Bean
  public UserDao userDao() {
    return new UserDao(connectionMaker());
  }
  @Bean
  public ConnectionMaker connectionMaker() {
    return new DConnectionMaker();
  }
 }

애플리케이션 컨텍스트를 적용한 UserDaoTest

public class UserDaoTest {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		AnnotationConfigApplicationContext context 
      = new AnnotationConfigApplicationContext(DaoFactory.class);
		UserDao dao = context.getBean("userDao", UserDao.class);
        ...
  }
}
  • getBean() 메소드는 ApplicationContext가 관리하는 오브젝트를 요청하는 메소드이다.
  • getBean()의 파라미터인 "userDao"는 ApplicationContext에 등록된 빈의 이름이다.

따라서 DaoFacotry의 userDao() 메소드를 호출해서 그 결과를 가져온다.

 

애플리케이션 컨텍스트

  • ApplicationContext 인터페이스를 구현한다. 
  • 오브젝트와 관계를 맺어주는 제한적인 역할이 아닌, 애플리케이션에서 loC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다.

애플리케이션 컨텍스트의 장점

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다
  • 애플리케이션 컨텍스트는 종합 loC 서비스를 제공해준다.
  • 애플리케이션 컨테스트는 빈을 검색하는 다양한 방법을 제공한다.

스프링 loC의 용어 정리

  • 빈(bean) : 스프링이 loC 방식으로 관리하는 오브젝트이다.
  • 빈 팩토리(bean factory) : 스프링의 loC를 담당하는 핵심 컨테이너이다. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당한다.
  • 애플리케이션 컨텍스트(application context) : 빈 팩토리를 확장한 loC 컨테이너이다. 
  • 설정정보/설정 메타정보(congifuration metadata) : 애플리케이션 컨텍스트 또는 빈 팩토리가 loC를 적용하기 위해 사용하는 메타정보이다.
  • 컨테이터 또는 loC 컨테이너 : loC 방식으로 관리한다는 의미로 애플리케이션 컨텍스트나 빈 팩토리를 지정하는 말이다.
  • 스프링 프레임워크 : 스프링이 제공하는 모든 기능을 통틀어 말하는 것이다.

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

DaoFactory를 직접 사용하는 것과 @Configuration 애노테이션을 추가해서 스프링의 애플리케이션 컨텍스트를 통해 사용하는 것은 겉으로는 동일해보인다. 하지만 중요한 차이점이 있다.

 

오브젝트의 동일성과 동등성

동일성 비교(identity) : 두 개의 오브젝트가 완전히 같은 동일한 오브젝트이다. 동등성 비교(equality) : 동일한 정보를 담고 있는 오브젝트

DaoFactory의 userDao()를 여러 번 호출했을 때는 userDao 메소드를 호출할 때마다 new 연산자에 의해 새로운 오브젝트가 만들어진다.

애플리케이션 컨텍스트에 DaoFacotry를 설정정보로 등록하고 getBean() 메소드를 이용해 userDao라는 이름으로 등록된 오브젝트를 가져올때는 호출할때마다 가져오는 오브젝트는 동일하다.

 

그 이유는 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만들기 때문이다.

 

스프링이 싱글톤으로 빈을 만드는 이유는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.

싱글톤 패턴은 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴이다. 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.

자바에서 싱글톤을 구현하는 방법

  • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  • 스태틱 팩토리 메소드인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다. 
  • 한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getInstance() 메소드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.

싱글톤 패턴의 단점

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

그래서 스프링은 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그것을 싱글톤 레지스트리라고 한다.

 

싱글톤 레지스트리의 장점

  • public 생성자를 가질 수 있다.
  • 간단히 오브젝트를 생성해서 사용가능하기 때문에 테스트가 가능하다.
  • 싱글톤 패턴과 달리 객체지향적인 설계 방식과 원칙, 디자인패턴등을 적용하는 데 아무런 제약이 없다.

싱글톤을 사용할 떄에는 상태관리에 주의를 기울여야 한다. 

상태가 없는 방식으로 클래스를 만들었을 경우 DB나 서버의 리소스로부터 생성한 정보는 파라미터와 로컬 변수, 리턴 값 등을 이용하면 된다.

 

7. 의존관계 주입(DI)

스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불린다.

DI는 의존성 주입, 의존 오브젝트 주입이라고 부르기도 한다. DI는 오브젝트 레퍼런스를 외부로부터 제공받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다.

지금까지 작업해왔던 UserDao는 UserDao가 ConnectionMaker에 의존하고 있는 형태이다.

하지만 인터페이스에 대해서만 의존관계를 만들어주면 결합도가 낮아진다. 즉, 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워지는 셈이다.

 

의존관계 주입의 핵심은 설계 시점에서 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다. (인터페이스처럼)

 

의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말한다.

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

현재 관계설정 책임 분리 전의 생성자

public UserDao() {
	connectionMaker = new DConnectionMaker();
}

이 코드는 설계 시점에서 DConnectionMaker라는 구체적인 클래스의 존재를 알고 있다. 이미 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는것이 단점이다.

 

그래서 DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다.

 

의존관계 주입을 위한 코드

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

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼선스를 전달받는 과정이 마치 메소드를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다.

 

IoC 방법에는 의존관계 주입외에 의존관계 검색도 있다.

 

의존관계 검색

의존관계 검색이란 의존관계를 맺는 방법이 외부로부터 주입이 아니라 스스로 검색을 이용하는 방법을 쓰는 IoC방법이다. 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

의존관계 검색을 이용하는 UserDao 생성자

public UserDao() {
	AnnotationConfigApplicationContext context =
    	new AnnotationConfigApplicationContext(DaoFacotry.class);
    this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

이처럼 의존관계 검색 방법은 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타난다. 애플리케이션 컨텍스트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 그다지 바람직하지 않다. 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.

 

의존관계 검색과 의존관계 주입의 적용했을 때 중요한 차이점 

 

의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다. 어딘가에서 직접 new UserDao()해서 만들어서 사용해도 된다. 이때는 ConnectionMaker만 스프링의 빈이기만 하면 된다.
반면에 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다.

 

기능 구현의 교환

개발을 할때 일반적으로 로컬DB, 운영DB를 사용하는데, DI를 하지 않은 상태에서는 서버를 변경할 때 DB에서 연결할 때 필요한 코드를 모두 수정해야 한다.

 

반면 DI 방식을 적용해서 만들었다면, DAO는 생성시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로 제공받기 때문에 서버에서 사용할 DaoFacotry만 변경해주면 된다.

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

이 코드를 운영용 코드로 변경할때에는 

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

부가기능 추가

만약  DB연결횟수를 카운팅하고 싶다면, 모든 DAO의 makeConnection() 메소드를 호출하는 부분을 손대야 한다.

 

하지만 DI 컨테이너에서는 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면된다.

 

public class CountingConnectionMaker implements ConnectionMaker {
  int cnt = 0;
  private ConnectionMaker realConnectionMaker;
  
  public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
    this.realConnectionMaker = realConnectionMaker;
  }
  
  public Connection makeConnection() throws ClassNotFoundException, SQLException {
    ++this.cnt; 
    return realConnectionMaker.makeConnection();
  }
  
  public int getCounter() {
  	return this.cnt;
  }
}

CountingConnectionMaker 클래스는 ConnectionMaker 인터페이스를 구현했지만 내부에서 직접 DB 커넥션을 만들지 않는다. DAO가 DB 커넥션을 가져올 때마다 호출하는 makerConnection()에서 DB 연결횟수 카운터를 증가시킨다.

 

CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 DConnectionMaker를 호출하게 하도록 하는 코드

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

 

의존관계를 주입하는 방법

  • 수정자 메소드를 이용한 주입 
  • 일반 메소드를 이용한 주입

수정자 메소드는 외부로부터 제공받는 오브젝트 레퍼런스를 저장해뒀다가 내부의 메소드에서 사용하게 하는 DI방식에서 활용하기에 적당하다.

일반 메소드는 여러 개의 파라미터를 갖는 일반 메소드를 DI용으로 사용할 수 있다.

 

8. XML을 이용한 설정

XML은 DaoFactory와 같은 자바 클래스를 이용하는 것 외에도, 다양한 방법을 통해 DI 의존관계 설정정보를 만들 수 있는 방법 중 하나이다.

 

DI 정보가 담긴 XML 파일은 <beans>를 루트 엘리먼트로 사용한다.

 

하나의 @Bean 메소드를 통해 얻을 수 있는 빈의 DI 정보들

  • 빈의 이름 - getBean()에서 사용
  • 빈의 클래스 - 어떤 클래스를 이용할지 정의
  • 빈의 의존 오브젝트 - 빈의 생성자나 수정자 메소드를 통해 의존 오브젝트를 넣어준다.

클래스 설정과 XML 설정의 대응 항목

  자바 코드 설정정보 XML 설정정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean methodName() <bean id="methodName"
빈의 클래스 return new BeanClass(); class="a.b.c... BeanClass">

이 표를 참고해서 DaoFacotry의 @Bean 메소드에 담긴 정보를 1:1로 XML의 태그와 애트리뷰트로 전환해주기만 하면 된다.

 

userDao() 전환

XML에서는 <property> 태그를 사용헤 의존 오브젝트와의 관계를 정의한다. name과 ref라는 두 개의 애트리뷰트를 갖는데, name은 프로퍼티의 이름 ref는 수정자 메소드를 통해 주입해줄 오브젝트의 빈 이름이다.

 

userDao.setConnectionMaker(connectionMaker());

해당 코드의 각 정보를 <property> 태그로 전환했을 때

<property name="connectionMaker" ref="connectionMaker" />

완성된 userDao 빈을 위한 XML 정보이다.

<beans>
  <bean id="userDao" class="springbook.dao.UserDao">
      <property name="connectionMaker" ref="connectionMaker" />
  </bean>
</beans>

 

XML을 이용하는 애플리케이션 컨텍스트

 

애플리케이션 컨텍스트가 DaoFactory 대신 XML 설정정보를 활용하도록 한 코드이다.

 

XML 설정정보를 담은 applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
  <bean id="connectionMaker" class="package.MyConnectionMaker" />
  <bean id="userDao" class="springbook.user.dao.UserDao">
      <property name="connectionMaker" ref="connectionMaker" />
  </bean>
</beans>

 

DataSource 인터페이스를 적용한 userDao 코드이다.

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

DataSource() 타입의 dataSource()를 DI 받았을 때 사용한 코드

@Bean
public DataSource dataSource() {
  SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
  
  dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
  dataSource.setUrl("jdbc:mysql://localhost/springbook");
  dataSource.setUsername("spring");
  dataSource.setPassword("book");
  return dataSource;
}

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

 

XML 설정 방식

<bean id="dataSource"
	class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />

이 코드의 문제는 dataSource() 메소드에서 SimpleDriverDataSource 오브젝트의 수정자로 넣어준 DB 접속정보는 나타나 있지 않다.

 

XML에서 dataSource() 메소드에서처럼 DB 연결정보를 넣도록 설정을 해야 한다.

 

수정자 메소드에는 다른 빈이나 오브젝트뿐 아니라 스트링 같은 단순 값을 넣어줄 수도 있다. 이때 DI에서처럼 오브젝트의 구현 클래스를 다이내믹하게 바꿀 수 있게 해주는것보다는 변겅 가능한 정보를 설정해줄 수 있도록 만드는 정도이다.

<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/springbook" />
<property name="username" value="spring" />
<property name="password" value="book" />

이 코드에서 url, username, password는 모두 스트링 타입이니 value 애트리뷰트의 값을 사용하는 것은 문제 없다. 그런데 driverClass는 스트링 타입이 아니라 java.lang.Class 타입이다. 어떻게 "com.mysql.jdbc.Driver"라는 스트링 값이 Class 타입의 파라미터를 갖는 수정자 메소드에 사용될 수 있는 것일까? 

 

이 설정이 가능한 이유는 스프링이 프로퍼티의 값을, 수정자 메소드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해주기 때문이다.

 

내부적으로 진행되는 변환작업

Class driverClass = Class.forName("com.mysql.jdbc.Driver");
dataSource.setDriverClass(driverClass);

 

최종적으로 DataSource 인터페이스로서의 전환작업이 마쳐진 applicationContext.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
                           
       <bean id="dataSource"
       		class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
            <property name="driverClass" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost/springbook" />
            <property name="username" value="spring" />
            <property name="password" value="book" />
       </bean>
       
       <bean id="userDao" class="springbook.user.dao.UserDao">
       		<property name="dataSource" ref="dataSource" />
       </bean>
</beans>

 

'챕터정리방' 카테고리의 다른 글

[5장] 서비스 추상화  (0) 2021.12.29
[5장] 서비스 추상화  (0) 2021.12.26
[4장] 예외  (0) 2021.12.21
[3장] 템플릿  (0) 2021.12.17
[2장] 테스트  (0) 2021.12.11

댓글