-토비의 스프링 3.1 vol.1의 1장을 정리 및 실습한다.
1장은 스프링 관심 대상인 오브젝트 설계와 구현, 동작원리 등을 dao 예제를 통해 설명하려고 한다. 개인적으로 토비의 스프링은 시도할 때마다 새롭게 뭔가 보이는게 있어서 좋은 것 같다. 디자인패턴을 조금 공부하고 나니(아직 맛보기 수준이지만..) 좀 더 이해가 깊어진 것 같기는 하다. 이 책은 간단하게 요약하면 나중에 봤을 때 항상 뭔 흐름인지 헷깔리고 실제로 너무 좋은 내용이 많아서 쌩요약보다는 좀 길더라도 적절하게 요약했다. 나중에 책 대신에 차근차근 빠르게 읽을 수 있을 정도로만 요약헀다.
1.1 초난감 DAO
사용자 정보를 JDBC API를 통해 DB에 저장하고 조회하는 간단한 DAO를 아래와 같이 만든다.
1.1.1 User
DAO는 자바빈 규약(setter,getter 존재)을 따르는 방식으로 작성한다.
User.java
package springbook.user.domain;
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;
}
}
DB에 User라는 이름의 테이블을 만든다.
create table users (
id varchar(10) primary key,
name varchar(20) not null,
password varchar(10) not null
)
1.1.2 UserDao
사용자 정보를 DB에 넣고 관리하는 DAO를 다음과 같이 만든다. 우선 사용자 정보 생성(add)과 읽기(get) 메소드를 만든다.
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException{
//DB연결을 위한 커넥션 가져오기
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook","spring","book");
//SQL을 담을 PreparedStatement 만들기
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
//Statement 실행
ps.executeUpdate();
//리소스 닫아주기
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException{
//DB연결을 위한 커넥션 가져오기
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook","spring","book");
//SQL을 담을 PreparedStatement 만들기
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
//실행결과를 ResultSet에 담기
ResultSet rs = ps.executeQuery();
rs.next();
//정보를 User객체에 담기
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.1.3 main()을 이용한 DAO 테스트 코드
main메소드를 만들어 테스트 해본다.
Main.java
package springbook.user;
import java.sql.SQLException;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new UserDao();
//저장할 정보를 User 객체에 set
User user = new User();
user.setId("superman2");
user.setName("슈퍼맨");
user.setPassword("batman1234");
//셋팅한 User 객체를 add 인자로 넘겨 메소드 호출
dao.add(user);
System.out.println(user.getId() + "등록성공");
//get메소드 호출하여 정상적으로 DB 데이터를 가져오는지 확인
User user2 = dao.get("superman2");
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
실행하기 전 mysql connection 라이브러리(mysql-connector-java -5.1.7 -bin.jar)를 클래스패스에 넣어줘야 한다. mysql connector download 등으로 검색하면 mysql 홈페이지의 아카이브에서 다운로드 받을 수 있다.
1.2 DAO의 분리
1.2.1 관심사의 분리, 1.2.2 커넥션 만들기의 추출
관심사의 분리란 관심이 같은 것끼리는 하나의 객체 또는 친한 객체로 모이게 하고, 관심이 다른 것은 따로 최대한 떨어뜨려서 서로 영향을 주지 않도록 분리하는 것을 말한다.
UserDao의 관심사항
UserDao의 관심사항을 분류해보면 다음과 같다.
- DB와 연결을 위한 커넥션을 어떻게 가져올 것인가에 대한 관심
- 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것에 대한 관심
- 작업 끝난 후 리소스 닫기에 대한 관심
UserDao의 문제점은 다음과 같다.
- 메소드에 DB 커넥션 가져오기, SQL문장 만들기, 리소스 닫기 등의 관심사가 혼합되어 있음
- add()와 get()에 중복된 DB 커넥션 코드가 있음
위와 같은 문제로 인하여 메소드가 수백개라면 중복코드가 수백개 발생하고 DB 커넥션이 변경되야 한다면 수백개를 수정해줘야 한다.
중복 코드의 메소드 추출
중복된 DB 연결 코드를 getConnection() 메소드로 만들어 다음과 같이 호출해 사용한다.
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException{
Connection c = getConnection(); //메소드 추출로 만든 함수 호출
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
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{
Connection c = getConnection(); //메소드 추출로 만든 함수 호출
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;
}
//DB 연결과 관련된 관심사 코드 분리
private Connection getConnection() throws ClassNotFoundException, SQLException{
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook","spring","book");
return c;
}
}
위와 같이 메소드 추출을 하면, DB관심사 코드가 변경되야 할 때 getConnection()만 수정해주면 된다. 메소드 추출이란 공통된 기능을 담당하는 메소드로 중복된 코드를 뽑아내어 리펙토링 하는 기법이다.
변경사항에 대한 검증:리펙토링과 테스트
리펙토링이란 외부의 동작 방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업, 기술을 말한다. 코드를 더 이해하기 쉬워지고 변화에 효율적 대처가 가능해진다. 메소드 추출은 리펙토링 기법 중 하나이다.
1.2.3 DB 커넥션 만들기의 독립
UserDao가 N사,D사 등에 판매되는데 각각 다른 DB 커넥션 방식을 사용한다고 가정한다. 또한 이후 DB커넥션 방식이 변경될 가능성이 있다고 가정한다. 고객들이 각자 원하는 DB커넥션 생성 방식을 적용시키기 위해 UserDao를 변경해야 한다.
상속을 통한 확장(추상클래스 사용)
메소드 추출한 getConnection()을 추상 메소드로 만든 후 UserDao를 상속하여 각자 getConnection을 원하는 방식대로 구현할 수 있다.
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException{
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
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{
Connection c = getConnection();
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;
}
//DB 연결과 관련된 관심사 코드 분리 - 추상메소드로 변경
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
NUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class NUserDao extends UserDao{
public Connection getConnection() throws ClassNotFoundException, SQLException{
//N사의 DB connection 생성 코드
}
}
DUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class DUserDao extends UserDao{
public Connection getConnection() throws ClassNotFoundException, SQLException {
//D사의 DB connection 생성 코드
}
}
위와 같이 상속을 활용하여 USerDao의 코드 변경 없이 DB 커넥션 코드를 각자의 회사에서 수정할 수 있게 되었다. 여기서 템플릿 메소드 패턴과 팩토리 메소드 패턴이 활용 되었다.
템플릿 메소드 패턴이란 슈퍼 클래스에 기본적인 로직의 흐름을 담은 템플릿 메소드(add,get)를 만듥 일부 기능은 추상메소드 등으로 만들어 서브클래스에서 구현하여 사용하는 것을 말한다. getConnection()이라는 일부 기능을 NUserDao와 DUserDao에서 확장해서 사용하였다.
팩토리 메소드 패턴은 서브클래스가 구체적인 오브젝트 생성 방법을 결졍하게 하는 패턴이다. getConnection() 메소드를 통해 오브젝트를 어떻게 생성할 것인지를 결정한다.
위와 같이 상속을 활용하면 간단하게 문제를 해결할 수 있다. 그러나 UserDao가 이미 다른 목적을 위해 상속을 사용하고 있다면 곤란해진다. 자바는 다중상속을 허용하지 않기 때문이다. 그리고 상속을 사용하면 슈퍼클래스 변경에 서브클래스가 수정되야 할 수도 있다. 즉, 변화에 불리해진다.
1.3 DAO의 확장
모든 오브젝트는 지속해서 변화한다. UserDao는 JDBC API를 사용할 것인가, 어떤 SQL을 만들 것인가, DB에서 꺼낸 정보를 어떻게 저장할 것인가 등에 대한 관심사를 갖고 있고 이런 관심사가 바뀌면 그때 변경이 일어난다. 이런 경우 DB 커넥션에 관심을 갖는 NUserDao나 DUserDao는 코드가 변경되지 않는다. 그 반대도 마찬가지이다. 이런 변화에 대응하기 위해 상속을 사용하여 슈퍼클래스와 서브클래스를 나누었다. 그러나 다중상속 문제 등이 있기 때문에 좀 더 수정이 필요하다.
1.3.1 클래스의 분리
아예 DB 커넥션 관심을 갖는 부분을 클래스로 만들어 분리해본다.
SimpleConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
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.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker; //DB 커넥션 관심을 아예 클래스로 분리하여 사용
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException{
Connection c = simpleConnectionMaker.makeNewConnection(); //DB 커넥션 관심을 아예 클래스로 분리하여 사용
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
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{
Connection c = simpleConnectionMaker.makeNewConnection(); //DB 커넥션 관심을 아예 클래스로 분리하여 사용
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;
}
//DB 연결과 관련된 관심사 코드 분리
//public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
추상클래스로 된 UserDao를 일반클래스로 변경 후 SImpleConnectionMaker라는 DB 커넥션 관심만 갖는 클래스를 따로 분리해 사용했다. 상속을 사용 안 하게되는 되었지만 UserDao클래스가 SimpleConnectionMaker 클래스에 종속되어 버렸다. UserDao 생성자에서 SimpleConnectionMaker 인스턴스를 생성해주기 때문에 N사와 D사는 각각의 방식으로 DB 커넥션 방식의 사용이 곤란해졌다. 또한 어떤 고객사가 openConnection()이라는 이름의 메소드로 사용하면 모든 add,get 같은 메소드를 변경해야 하는 문제가 있다.
1.3.2 인터페이스의 도입
UserDao와 각 고객사의 DB 커넥션 클래스가 느슨하게 연결되도록 추상적인 연결고리를 만들어주면 되는데 이를 위해 인터페이스를 사용한다. 추상화란 어떤 것들의 공통적 성격을 뽑아내어 따로 분리하는 작업이다.
ConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public interface ConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}
DConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class DConnectionMaker implements ConnectionMaker {
@Override
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
//D사 전용 DB 커넥션 코드
return null;
}
}
NConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class NConnectionMaker implements ConnectionMaker {
@Override
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
//N사 전용 DB 커넥션 코드
return null;
}
}
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private ConnectionMaker connectionMaker; //DB 커넥션 관심은 인터페이스로 추상화하여 사용
public UserDao() {
connectionMaker = new NConnectionMaker(); //DB 커넥션 관심을 인터페이스로 추상화했지만 초기화 때 클래스 종속 문제 발생
}
public void add(User user) throws ClassNotFoundException, SQLException{
Connection c = connectionMaker.makeNewConnection(); //DB 커넥션 관심은 인터페이스로 추상화하여 사용
PreparedStatement ps = c.prepareStatement("insert into users(id,name,password) value(?,?,?)");
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{
Connection c = connectionMaker.makeNewConnection(); //DB 커넥션 관심을 아예 클래스로 분리하여 사용
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;
}
//DB 연결과 관련된 관심사 코드 분리
//public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
인터페이스를 사용하여 관심사별로 클래스를 분리했지만 생성자에 초기화 코드가 종속되어 있다. 이 관계를 설정해주는 코드를 분리해야 한다.
1.3.3 관계설정 책임의 분리
UserDao와 DB 커넥션 관심사 클래스의 관계설정 책임을 UserDao를 사용하는 클라이언트에게 넘긴다. UserDao의 생성자는 다형성을 활용하여 ConnectionMaker타입의 파라미터를 받도록 변경하고 new DConnectionMaker()와 같은 직접적으로 종속되는 초기화 코드를 없앤다. 다음과 같이 변경된다.
UserDao.java 일부
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker; //UserDao 생성시 파라미터로 넘어온 ConnectionMaker 타입의 클래스를 넣어줌
}
UserDaoTest.java
package springbook.user.dao;
public class UserDaoTest {
public static void main(String[] args) {
ConnectionMaker connectionMaker = new DConnectionMaker(); //UserDao가 사용할 ConnectionMaker 구현 클래스
UserDao dao = new UserDao(connectionMaker); //위의 구현 클래스를 생성자 인자로 넣어줌
}
}
위의 UserDaoTest가 클라이언트의 역할을 하고 관계설정 책임을 갖게 된다. 이제 UserDao의 변경 없이 클라이언트에서 고객사별로 사용할 DB 커넥션 클래스를 다형성을 활용하여 변경하면 된다. 아래는 인터페이스를 사용한 방식으로 변경한 UserDao,ConnectionMaker와 클라이언트의 관계 및 구조이다.
이제 다른 Dao가 만들어져도 관심사를 인터페이스로 분리해두었기 때문에 클라이언트를 통해 관계설정 후 사용하기만 하면 된다. 또한 ConnectionMaker 구현 클래스들이 DB 커넥션을 가져오는 방식을 변경하더라도 UserDao의 코드는 아무런 영향을 받지 않는다.
1.3.4 원칙과 패턴
개방 폐쇄 원칙(OCP, Open-Closed Principle)
클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다는 원칙이다. UserDao의 경우 인터페이스를 통해 확장 포인트는 자유롭게 열려있다. 반면 인터페이스를 이용하는 UserDao 자체는 변경이 굳이 필요없이 폐쇄되어있다. 인터페이스를 사용해 확장 기능을 정의한 API들 대부분이 OCP원칙을 따르고 있다.
높은 응집도와 낮은 결합도
개방 폐쇄 원칙을 설명하는 고전적인 원리이다.
높은 응집도는 해당 모듈에서 변하는 부분이 크다는 것이다. 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높은 것이다. 무엇을 변경할지 명확하며 다른 곳의 수정을 요구하지 않게 된다. DB 연결 방식 테스트를 하려면 DB 커넥션 관심사가 응집되어 있는 NConnectionMaker만 테스트해도 충분하다.
낮은 결합도는 책임,관심사가 다른 오브젝트끼리는 느슨하게 연결되어 있다는 것이다. 이러면 확장이 편해지고 변화에 대응하는 속도가 높아진다.
UserDao는 그 자체로 자신의 책임에 대한 응집도가 높다. 또한 UserDao와 ConnectionMaker의 관계는 인터페이스를 통해 느슨하게 연결되어 낮은 결합도를 유지하고 있다.
전략 패턴
변경이 필요한 알고리즘(책임)을 인터페이스를 통해 통째로 외부로 분리시키고 이를 구현한 알고리즘 서브클래스를 필요에 따라 변경해 사용하는 패턴이다. UserDao는 전략패턴의 컨테스트(Context)이다. 클라이언트(UserDaoTest)의 역할은 컨텍스트가 사용할 전략(ConnectionMaker 구현 클래스)을 컨텍스트의 생성자, setter 등을 통해 제공해주는 것이다.
1.4 제어의 역전(IoC)
1.4.1 오브젝트 팩토리
UserDaoTest는 테스트용 클래스인데 관계설정 관심사까지 맡게 되었다. 관계설정 관심사 부분을 따로 뗴어 클래스로 만든다.
팩토리
객체 생성 방법을 결정하고 오브젝트를 돌려주는 일을 하는 오브젝트를 팩토리(factory)라고 한다. DaoFactory로 만든다.
DaoFactory.java
package springbook.user.dao;
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
UserDaoTest.java
package springbook.user.dao;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new DaoFactory().userDao();
//테스트
User user = new User();
user.setId("kim1234");
user.setName("kimminsu");
user.setPassword("1234");
dao.add(user);
User getUser = dao.get("kim1234");
System.out.println(getUser.getId());
}
}
UserDaoTest에서는 DaoFactory 인스턴스를 만들어 userDao()를 호출하여 DB 커넥션이 셋팅된 UserDao를 리턴받아 사용한다.
설계도로서의 팩토리
UserDao와 ConnectionMaker같이 실질적 로직을 담은 클래스들은 '컴포넌트'라고 볼 수 있다. 반면 DaoFactory는 컴포넌트의 관게설정을 하는 설계도 역할을 한다. 각 고객사에서 원하는 DB 커넥션을 적용하려면 DaoFactory만 수정하면 된다.
1.4.3 제어권의 이전을 통한 제어관계 역전
일반적 프로그램 흐름은 사용하는 쪽에서 오브젝트를 생성, 호출하며 제어한다. main() 메소드 같이 프로그램 시작점에서 사용할 오브젝트를 생성하고, 그 오브젝트에서 사용할 오브젝트를 또 생성하여 사용하는 등 사용하는 쪽에서 제어한다. 이전 UserDao에서도 DConnectionMaker를 직접 생성하는 등 제어권을 갖고 있었다.
제어의 역전은 이런 흐름을 뒤집는 것을 말한다. 오브젝트가 자신이 사용할 오브젝트를 직접 제어하지 않고 다른 대상이 생성하고 관리한다. 서블릿도 제어의 역전 개념이 적용되어 있다. 서블릿을 서버 배포 후 직접 개발자가 실행하지 않는다. 서블릿 컨테이너가 적정 시점에 서블릿 클래스 오브젝트를 만들고 그 안의 메소드를 호출한다.
프레임워크도 제어의 역전 개념이 적용된 기술이다. 라이브러리가 수동적으로 애플리케이션에 의해 사용되는 반면, 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임워크에는 제어의역전(IoC)개념이 적용되어 있어야 한다. 예제의 DaoFactory가 제어의 역전이 적용되어 있다. 스프링은 IoC 기술을 기반으로 삼고 있으며 이 기술을 극한까지 적용하고 있다.
1.5 스프링의 IoC
1.5.1 오브젝트 팩토리를 이용한 스프링 IoC
애플리케이션 컨텍스트와 설정정보
스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전 개념이 적용된 오브젝트이다. 빈팩토리는 스프링 빈의 생성, 관계설정같은 제어를 담당하는 IoC 오브젝트이다. 이를 확장한 것이 애플리케이션 컨텍스트이다. 애플리케이션 컨텍스트는 설정정보(DaoFactory)를 참고하여 빈 생성, 관계 설정 등의 제어작업을 총괄한다.
DaoFactory를 사용하는 애플리케이션 컨텍스트
다음과 같이 어노테이션을 적용하면 애플리케이션 컨텍스트의 설정정보로 활용 가능하다. 그 전에 필요한 라이브러리들을 클래스패스에 설정해줘야 한다.
- com.springssource.net.sf.cglib-2.2.0.jar
- com.springsource.org.apache.commons.logging-1.1.1.jar
- org.springframework.asm-3.0.7.RELEASE.jar
- org.springframework.beans-3.0.7.RELEASE.jar
- org.springframework.context-3.0.7.RELEASE.jar
- org.springframework.core-3.0.7.RELEASE.jar
- org.springframework.expression-3.0.7.RELEASE.jar
DaoFactory.java
package springbook.user.dao;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
@Configuration은 스프링 설정 정보로 사용하겠다는 어노테이션이다. @Bean은 IoC 전용 메소드라는 표시이다.
UserDaoTest.java
package springbook.user.dao;
import java.sql.SQLException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); //DaoFactory 설정정보를 생성자의 인자로 줌
UserDao dao = context.getBean("userDao", UserDao.class); //getBean(메소드명, 리턴타입)
}
}
위와 같이 AnnotationConfigApplicationContext를 사용하면 DaoFactory 설정정보를 활용한 어플리케이션 컨텍스트를 사용할 수 있다. getBean을 통해 @Bean이 붙은 메소드의 리턴값을 가져올 수 있다.
1.5.2 애플리케이션 컨텍스트의 동작방식
- @Configuration 붙은 설정정보(기존 오브젝트 팩토리)를 ApplicationContext에 등록
- @Bean이 붙은 메소드 이름을 가져와 빈 목록을 만들어 둠
- 클라이언트가 getBean() 요청시 자신의 빈 목록에서 요청한 이름이 있는지 찾은 후 오브젝트 생성하여 클라이언트에게 전달
DaoFactory를 직접 오브젝트 팩토리로 사용했을 때보다 애플리케이션 컨텍스트를 사용했을 때 얻는 장점은 다음과 같다.
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
- 관계설정뿐만 아니라 오브젝트가 만들어지는 방식, 시점과 전략 변경, 오브젝트 후처리, 설정방식 변경, 인터셉팅 등 다양한 기능을 제공한다.
- 빈을 검색하는 다양한 방법을 제공한다. getBean()뿐만 아니라 특별한 애노테이션 설정이 되어 있는 빈도 찾을 수 있다.
1.6 싱글톤 레지스트리와 오브젝트 스코프
DaoFactory로 userDao()를 호출하여 UserDao를 생성하면 생성할 때마다 매번 새로운 오브젝트가 만들어진다. 반면 @Configuration을 사용하여 스프링 애플리케이션 컨텍스트에서 사용하여 UserDao를 getBean()으로 생성하면 매번 같은 오브젝트가 만들어진다. 스프링은 기본적으로 빈을 싱글톤으로 만든다.
1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
애플리케이션 컨텍스트는 IoC이면서 싱글톤을 저장,관리하는 싱글톤 레지스트리이다.
서버 애플리케이션과 싱글톤
스프링은 자바 엔터프라이즈 기술을 사용하는 서버 환경이 주 사용대상이기 때문에 싱글톤으로 빈을 생성한다. 클라이언트 요청이 올 때마다 오브젝트를 새로 만들어서 사용하면 부하가 심해지기 때문이다.
자바에서 싱글톤 구현법은 다음과 같다.
- 클래스 밖엣 오브젝트 생성을 할 수 없도록 생성자를 private로 선언
- 싱글톤 오브젝트 생성시 오브젝트를 저장할 자신과 같은 타입의 스태틱 필드 정의
- 스태틱 팩토리 메소드인 getInstance()를 만든 후 최초 호출시 한번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스태틱필드에 저장됨
- 이후 또 호출될 때에는 이미 만들어진 스태틱 필드의 오브젝트를 넘겨줌
싱글톤 패턴의 문제점은 다음과 같다.
- private 생성자로 인하여 상속 불가능하다. 객체지향의 장점인 상속과 이를 이용한 다형성 적용이 불가능하다.
- 싱글톤은 테스트가 어렵다.
- 서버 환경에서는 싱글톤이 하나의 오브젝트만 만들어진다는 보장이 없다. 클래스 로더 구성에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다.
- 싱글톤은 사용하는 클라이언트가 정해져 있지 않아 전역 상태(global state)를 만들 수 있다. 여러 객체가 접근하여 수정, 공유의 위험이 있다.
스프링은 이런 문제들을 해결해준다. 싱글톤 레지스트리로서 일반적인 자바 클래스를 싱글톤으로 사용하게 해준다.
1.6.2 싱글톤과 오브젝트의 상태
싱글톤은 멀티스레드 환경에서 무상태(stateless) 방식으로 만들어야 한다. 스레드들이 싱글톤 오브젝트의 인스턴스 변수를 수정하면 위험하기 때문에 상태유지(stateful)로 만들지 않는다. UserDao에서는 User, Connection 등의 오브젝트를 로컬변수로 선언해서 사용했다. 상태를 가질 수 있는 이런 오브젝트들은 로컬변수로 선언하면 매번 새로운 공간이 만들어지기 때문에 값을 덮어쓸 일이 없다. 반면 ConnectionMaker 같은 변수는 인스턴스 변수로 선언해도 된다. 변수에 초기화 후에 수정되지 않기 때문에 가능하다.
1.6.3 스프링 빈의 스코프
스프링 빈의 생성과 존재, 적용되는 범위에 대한 것을 스코프라고 한다. 기본적으로는 스프링 컨테이너에 싱글톤으로 한 개의 오브젝트만 만들어진다. 경우에 따라서는 프로토타입(prototype) 스코프를 만들 수 있다. 컨테이너에 빈을 요청할 때마다 매번 새롭게 오브젝트를 만든다. 또한 웹을 통해 HTTP 요청이 생길때마다 생성되는 요청(request) 스코프가 있으며 웹의 세션과 유사한 세션(session) 스코프도 있다.
1.7 의존관계 주입(DI)
1.7.1 제어의 역전(IoC)과 의존관계 주입
제어의 역전(IoC) 방식의 의도를 좀 더 명확히 드러내는 용어가 의존관계 주입(DI)이다. IoC방식의 특징 중 하나라고 볼 수도 있을 것 같다.
1.7.2 런타임 의존관계 설정
의존관계
A가 B에 의존하고 있는 경우, B가 변경되면 A도 영향을 받는다. 반면 A가 바뀐다고 B가 영향받지는 않는다. UserDao는 ConnectionMaker에 의존하고 있다. ConnectionMaker가 변경되면 UserDao가 영향을 받는다. 의존관계는 UML에서 설계 모델의 관점에서 이야기 한다. 반면, 런타임 의존관계는 설계시점의 의존관계가 실체화 된 것이다. UserDao는 ConnectionMaker는 설계시점에 의존하고 있지만, NCoonectionMaker를 사용할지 DConnectionMaker를 사용할지는 런타임 시점에만 알 수 있다. 여기서 ConnectionMaker와 같이 실제 사용대상 오브젝트를 의존 오브젝트라고 한다.
의존관계 주입(DI)는 구체적인 의존 오브젝트와 이를 사용하는 주체(클라이언트) 오브젝트를 런타임 시에 연결해주는 작업을 말한다. 스프링에서는 스프링 애플리케이션 컨텍스트라는 제3의 존재가 오브젝트의 관계를 결정한다. 그래서 스프링 컨테이너를 DI 컨테이너라고도 부른다.
1.7.3 의존관계 검색과 주입
런타임 시 의존관계를 맺을 오브젝트를 결정하는 것, 오브젝트의 생성 작업 등은 외부 컨테이너에게 IoC로 맡기지만 이를 가져올 때 메소드나 생성자를 통한 주입이 아니라 스스로 컨테이너에게 요청하는 방법이다. 의존관계 검색을 사용하여 UserDao의 ConnectionMaker 관계 설정은 다음과 같이 할 수 있다.
UserDao.java 일부
public UserDao(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
의존관계 검색은 코드 안에 오브젝트 팩토리 클래스, 스프링 API가 나타나게 되어 컴포넌트 성격의 클래스가 컨테이너와 같은 성격이 다른 오브젝트에 의존하게 되어 DI가 더 추천된다. 그러나 UserDaoTest처럼 테스트코드에서 애플리케이션 기동 시점에 한 번은 의존관계를 사용하여 오브젝트를 가져와야 한다. 서버에서도 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번은 의존관계 검색 방식을 사용해 가져와야 한다.
의존관계 검색과 의존관계 주입의 가장 큰 차이점은 다음과 같다.
- 의존관계 검색 방식은 검색하는 오브젝트 자신이 스프링 빈일 필요가 없다. 위의 UserDao가 굳이 스프링 빈이 아니더라도 의존관계 검색을 사용할 수 있다.
- 의존관계 주입 방식은 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 둘 다 스프링 빈이어야 한다.
1.7.4 의존관계 주입 응용
기능 구현의 교환
로컬DB를 사용하다가 개발이 끝나고 운영DB로 DB 커넥션을 변경해야 하는 경우가 있을 수 있다. DI 적용 전인 경우, DAO가 100개면 100개의 LocalDBConnection() 코드를 ProdDBConnection() 코드로 바꿔야 한다. 유지보수의 문제가 발생한다. 반면 DI를 사용하면 아래와 같이 사용할 수 있다.
DaoFactory.java 일부
@Bean
public ConnectionMaker localConnectionMaker(){
return new LocalDBConnectionMaker(); //로컬 사용시
//return new ProductionDBConnectionMaker(); //운영 사용시(로컬을 주석처리 후 이 부분 주석 제거)
}
부가기능 추가(DB 커넥션 카운팅 기능 추가)
add(),get() 등의 메소드를 호출할 때마다 커넥션과 관련된 메소드가 호출되는데, 이때마다 카운트를 하는 기능을 추가하려고 한다. 호출하는 부분에 count를 셀 수도 있지만, dao를 직접 수정하지 않고 DI를 이용하여 기능을 구현한다. 기존에 UserDao가 connectionMaker 인터페이스를 구현한 클래스를 사용하고 있는데, 중간에 카운팅 전용 클래스를 사용한다.
중간에 끼어들어간 카운트 전용 클래스는 실제 DB 커넥션을 담당하는 ConnectionMaker를 DI받은 후 사용한다. 아래와 같다.
CountingConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.SQLException;
public class CountingConnectionMaker implements ConnectionMaker{
int counter = 0;
ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
this.realConnectionMaker = realConnectionMaker; //실제 DB 커넥션 인스턴스 DI
}
@Override
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
counter++; //카운팅
return realConnectionMaker.makeNewConnection(); //DI받은 DB 커넥션의 커넥션 메소드 호출
}
public int getCounter() {
return this.counter;
}
}
새롭게 만든 DaoFactory 설정 정보는 CountingDaoFactory라는 이름으로 아래와 같이 만든다.
CountingDaoFactory.java
package springbook.user.dao;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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();
}
}
위와 같은 DaoFactory 설정정보를 구성하면 런타임시 userDao의 bean을 가져올 때 UserDao는 CountingConnectionMaker를 주입받고, CountingConnectionMaker는 DConnectionMaker라는 실제 DB 커넥션 클래스를 주입받아 사용한다. 아래와 같이 테스트를 해볼 수 있다.
UserDaoConnectionCountingTest.java
package springbook.user.dao;
import java.sql.SQLException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import springbook.user.domain.User;
public class UserDaoConnectionCountingTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(CountingDaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
//테스트용 데이터 셋팅
User user = new User();
user.setId("countMan2");
user.setName("kimMinSu");
user.setPassword("1234");
//dao 함수 호출
dao.add(user); //카운팅 1개 추가
dao.get("countMan2"); //카운팅 1개 추가
//counting 하기
CountingConnectionMaker ccm = context.getBean("connectionMaker",CountingConnectionMaker.class);
System.out.println("Connection counter : " + ccm.getCounter()); //Connection counter : 2
}
}
1.7.5 메소드를 이용한 의존관계 주입
DI 방법은 생성자 말고도 수정자 메소드 이용 방법과 일반 메소드 이용 방법이 더 있다. 수정자 메소드(setter)를 많이 사용한다.(XML 설정방식에서도 편함)
예를 들어 아래와 같이 사용한다.
UserDao.java 일부
public class UserDao{
private ConnectionMaker connectionMaker;
//생성자 방식 DI
//public UserDao(ConnectionMaker connectionMaker){
// this.connectionMaker = connectionMaker;
//}
//수정자 메소드 방식 DI
public void setConnectionMaker(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
}
설정정보인 DaoFactory도 아래와 같이 변경해준다.
DaoFactory.java 일부
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}
1.8 XML을 이용한 설정
의존관계 설정정보를 만드는 방법은 DaoFactory 같이 자바방식으로 만들 수도 있지만 XML로 만들 수도 있다. 장단점이 있는데 XML은 따로 빌드가 필요없고 이해가 쉽다.
위의 DaoFactory.java를 XML 매칭시키면 다음과 같다.
DaoFactory.java
@Configuration //이부분은 XML의 <beans></beans>라는 루트 엘리먼트로 매칭
public class DaoFactory {
@Bean //이부분은 <bean>이란 엘리먼트로 매칭
public UserDao userDao() { //메소드명은 id로 매칭-> <bean id="userDao"...>로 사용
return new UserDao(connectionMaker()); //<bean></bean> 안에서 <property name="connectionMaker" ref="connectionMaker"/>로 사용
}
@Bean //이부분은 <bean>이란 엘리먼트로 매칭
public ConnectionMaker connectionMaker() { //메소드명은 id로 매칭-> <bean id="connectionMaker"...>로 사용
return new DConnectionMaker(); //이부분은 <bean class="springbook...DConnectionMaker"/> 방식으로 사용
}
}
정리하면 아래와 같은 모양이 된다.
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="springbook.user.dao.DConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connectionMaker" ref="connectionMaker"/>
</bean>
</beans>
흐름을 설명하자면 <beans></beans>라는 루트 엘리먼트 내부에 DaoFactory의 @bean이 붙은 메소드들을 <bean>으로 만든 것이다. id는 메소드명에 매칭, class는 리턴타입에 매칭한다.(풀패키지 경로를 써줘야 함)
<property>는 수정자 메소드에 매칭된다. name을 connectionMaker라고 적었는데, 실제 자바의 setConnectionMaker에 매칭된다. ref는 참고할 bean의 id를 적어준다. ref로 DConnectionMaker를 주입받는 것이다.
※스프링 XML 설정파일의 문서 구조 정의 방법은 DTD방식과 스키마 방식이 있다. 구조 정의에 맞게 선언을 넣어주면 <beans>나 <bean>같은 엘리먼트를 사용할 수 있다. 다른 태그들도 사용하려면 스키마를 사용하는 것을 더 추천한다.
그리고 아래와 같이 애플리케이션컨텍스트 초기화 시에 new GenerixXmlApplicationContext()를 사용한다.
UserDaoTest.java
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//자바설정파일을 사용한 애플리케이션컨텍스트 초기화
//ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); //DaoFactory 설정정보를 생성자의 인자로 줌
//UserDao dao = context.getBean("userDao", UserDao.class); //getBean(메소드명, 리턴타입)
//XML설정파일을 사용한 애플리케이션컨텍스트 초기화
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao",UserDao.class);
//테스트용 데이터 셋팅
User user = new User();
user.setId("xmlMan");
user.setName("kimMinSu");
user.setPassword("1234");
//dao 함수 호출
dao.add(user);
dao.get("xmlMan");
System.out.println("add and get 성공");
}
}
1.8.3 DataSource 인터페이스로 변환
DB 커넥션을 위해 ConnectionMaker를 정의하여 사용하였는데, 자바에서는 DataSource라는 인터페이스를 제공한다. DB커넥션 기능 외에 풀링(pooling) 기능까지 제공한다. DI받을 클래스는 DataSource 구현 클래스인 SimpleDriverDataSource를 사용한다. 이를 위해 라이브러리 org.springframework.jdbc-3.0.7.RELEASE.jar를 추가한다.
UserDao는 다음과 같이 수정한다.
UserDao.java 일부
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws ClassNotFoundException, SQLException{
Connection c = dataSource.getConnection();
...
}
}
설정정보 파일도 바꿔야 한다. java와 xml 모두 다음과 같이 바꿀 수 있다.
DaoFactory.java
package springbook.user.dao;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setDataSource(dataSource());
return userDao;
}
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");
return 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>
dataSource에 설정해줄 설정값은 property에 value에 정의해준다. String값 외에도 com.mysql.jdbc.Driver와 같이 class로 자동변환도 해준다.
1장 정리를 마친다. 2장도 할 수 있을까..
참고자료 : 토비의 스프링 3.1 Vol.1
'개발자 일지 > Spring' 카테고리의 다른 글
[자바, 스프링]인프런 김영한 로드맵1, 스프링 입문 강의 정리4 (0) | 2022.01.10 |
---|---|
[자바, 스프링]인프런 김영한 로드맵1, 스프링 입문 강의 정리3 (0) | 2022.01.01 |
[자바, 스프링]인프런 김영한 로드맵1, 스프링 입문 강의 정리2 (0) | 2021.12.12 |
[자바, 스프링]인프런 김영한 로드맵1, 스프링 입문 강의 정리1 (0) | 2021.12.06 |
[자바,스프링]서블릿 컨테이너와 스프링 컨테이너 (0) | 2021.11.09 |
[토비의 스프링 vol.1]4장 예외 (0) | 2021.06.11 |
[토비의 스프링 vol.1]3장 템플릿 (0) | 2021.05.16 |
[토비의 스프링 vol.1]2장 테스트 (0) | 2021.05.12 |