토비의 스프링 3
1장 오브젝트와 의존관계
스프링이 자바에서 가장 중요하게 가치를 두는 것은 바로 객체지향 프로그래밍 언어라는 점이다. 자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 객체지향 프로그래밍 기본으로 돌아가자는 것이 핵심 철학이다.
스프링이 가장 관심을 많이 두는 대상은 오브젝트 이다. 애플리케이션에서 오브젝트가 생성되고, 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 진지하게 생각해볼 필요가 있다.
1.1 초난감 DAO
DAO
dao(Data Access Object)는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 만든다.
1.1.1 USER
package user.domain;
public class User {
String id;
String name;
String password;
public String getId() {
return id;
}
public void setId() {
this.id = id;
}
public String getName() {
return name;
}
public void setName() {
this.name = name;
}
public String getPassword(){
return password;
}
public Svoid setPassword(String password){
this.password = password;
}
}
1.1.3 main()을 이용한 DAO 테스트 코드
public static void main(String []args) throws ClassNotFoundException, SQLException{
UserDao dao = new UserDao();
User user = new User();
user.setId("~~");
user.setName("~~");
user.setPassword("~~");
dao.add(user);
System.out.println("등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");
}
한심한 코드이다. 하지만, ==기능적으로 잘동작 한다. 왜 이 코드에 문제가 많다고 하는 것일까? DAO를 개선했을 때의 장점은 무엇인가? 그런 장점들이 당장에, 또는 미래에 주는 유익은 무엇인가? 또, 객체지향 설계의 원칙과는 무슨 상관이 있을까? 이 DAO를 개선하는 경우와 그대로 사용하는 경우, 스프링을 사용하는 개발에서 무슨 차이가 있을까?==
1.2 DAO의 분리
1.2.1 관심사의 분리
객체지향 설계와 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다.
변경이 일어났을 때는 필요한 작업을 최소화하고, 변경이 다른 곳에 문제를 일으키지 않게 할 수 있었을까? 분리와 확장 을 고려한 설계가 있었기 때문이다.
예를 들면, ==DB 접속용 암호를 변경하려고 DAO클래스를 수백 개를 수정해야 한다면? 트랜잭션 기술을 다른 것으로 바꼈다고 비즈니스 로직이 담긴 코드를 모두 변경해야 한다면? 또는 다른 개발자가 개발한 코드에 변경이 일어날 때마다 내가 만든 클래스도 함께 수정을 해줘야 한다면?==
끔직한 일이다.
프로그래밍의 기초 개념 중에 관심사의 분리(Separation of Concerns)라는 게 있다. 객체지향에 적용해보면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라고 생각할 수가 있다.
관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어주는 것이다.
1.2.2 커넥션 만들기의 추출
UserDao 구현 메소드를 살펴보면 add() 메소드 하나에서만 적어도 세가지 관심사항을 확인할 수가 있다.
- 첫째는 DB와 연결을 위해 커넥션을 어떻게 가져올까라는 관심이다. 더 세분화해서 어떤 DB를 쓰고, 어던 드라이버를 쓰고, 어떤 로그인 정보를 쓰는데, 그 커넥션을 생성하는 방법은 또 어떤 것이다 라는 것까지 구분해서 볼 수도 있다.
- 둘째는 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것이다. 여기서의 관심은 파라미터로 넘어온 사용자 정보를 Statement에 바인딩 시키고, Statement에 담긴 SQL을 DB를 통해 실행시키는 방법이다.
- 셋째는 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것이다.
스파게티 코드가 되는 것을 없애기 위해서, 중복코드의 메소드 추출
1.2.3 DB 커넥션 만들기의 독립
N사와 D사에서 사용자 관리를 위해 이 UserDao를 구매하겠다는 주문이 들어왔다. 문제는 N사와 D사가 각기 다른 종류의 DB를 사용하고 있고, DB커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 하지만, UserDao를 공개하고 싶지 않아서 컴파일된 바이너리 파일만 제공하고 싶다. 과연 이런 경우에 UserDao 소스코드를 N사와 D사에 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하는 방법
상속을 통한 확장
public abstract Class UserDao {
public void add(User user) throws SQLException {
Connectopn c = getConnection();
...
}
public void get(String id) throws SQLException {
Connectopn c = getConnection();
...
}
public abstract Connection getConnection() throws SQLException;
}
public class NUserDao extends UserDao {
public Connection getConnection() throws SQLException{
//N사 DB Connection 생성코드
}
}
public class DUserDao extends UserDao {
public Connection getConnection() throws SQLException{
//D사 DB Connection 생성코드
}
}
이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 디자인 패턴에서 템플릿 메소드 패턴(template method pattern) 이라고 한다.
그리고, UserDao의 서브크를래스의 getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정하게 하는 것을 팩토리 메소드 패턴 (factory method pattern)이라고 부르기도 한다.
1.3 DAO의 확장
서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 그러나 여러가지 단점이 많은, 상속이라는 방법을 사용했다는 사실이 불편하게 느껴진다.
1.3.1 클래스의 분리
관심사가 다르고, 변화의 성격이 다른 이 두가지 코드를 좀 더 화끈하게 분리를 해볼 생각이다.
public class UserDao{
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
...
}
public void get(String id) throws SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
...
}
}
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws SQLException {
Class.forName("com.mySql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql:/localhost/springbook", "spring", "book");
return c;
}
}
성격이 다른 코드를 분리하는 것은 잘한 것 갔지만, N사와 D사에 UserDao 클래스만 공급하고, 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 게 다시 불가능해졌다. 왜냐하면 UserDao의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때처럼 UserDao코드의 수정없이 DB 커넥션 생성 기능을 변경할 방법이 없다.
1.3.2 인터페이스의 도입
public class UserDao{
private ConnectionMaker connectionMaker;
public UserDao {
//여기에 클래스 이름이 나오는 문제...
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws SQLException {
Connection c = connectionMaker.makeNewConnection();
...
}
public void get(String id) throws SQLException {
Connection c = connectionMaker.makeNewConnection();
...
}
}
public interface ConnectionMaker {
Connection makeNewConnection() throws SQLException;
}
public class SimpleConnectionMaker implements ConnectionMaker {
public Connection makeNewConnection() throws SQLException {
Class.forName("com.mySql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql:/localhost/springbook", "spring", "book");
return c;
}
}
필요할 때마다 UserDao의 생성자 메소드를 직접 수정하라고 하지 않고는 고객에게 자유로운 DB 커넥션 확장 기능을 가진 UserDao 를 제공할 수가 없다.
1.3.3 관계설정 책임의 분리
클래스 사이에 불필요한 의존관계를 가지고 있는 구조이기 때문에,
public UserDao(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
public class UserDaoTest {
public static void main(String []args) throws SQLException{
ConnectionMaker connectionMaker = new DConnectionMaker();
/*
1. userDao 생성
2.사용할 ConnectionMaker 타입의 오브젝트 제공, 결국 두 오브젝트 사이의 이존관계 설정 효과
*/
UserDao dao = new UserDao(connectionMaker);
}
}
1.3.4 원칙과 패턴
개방 폐쇄 원칙(OCP, Open-Closed Principle)
확장에는 열려있고, 변경에는 닫혀있다.
객체지향 설계 원칙(SOLID)
로버트 마린이 정리한 객체지향 설계 원칙인 SOLID에 대한 소개 웹사이트(http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)에서 찾아볼 수 있다.
높은 응집도와 낮은 결합도
높은 응집도
응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수 있다.
낮은 결합도
낮은 결합도란 결국 하나의 변경이 발생할 때마다 모듈과 객체로 변경에 대한 요구가 전파되지 않은 상태를 말한다.
전략 패턴
UserDaoTest-UserDao-ConnectionMaker 구조를 디자인 패턴의 시각으로 보면 전략 패턴에 해당한다고 볼 수 있다. 전략패턴은 디자인 패턴의 꽃이라고 불릴 만큼 다양하게 자주 사용되는 패턴이다.
1.4 제어의 역전
IOC라는 약자로 많이 사용되는 제어의 역전(Inversion Of Control)이라는 용어가 있다.
1.4.1 오브젝트 팩토리
팩토리
분리시킬 기능을 담당할 클래스를 하나 만들어보겠다. 이 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것이다. 이런 일을 하는 오브젝트를 흔히 팩토리라고 부른다.
public class DaoFactory{
public UserDao userDao(){
ConnectionMaker connectionMaker = new DConnectionMaker();
userDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
UserDaoTest는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경쓰지 않고 팩토리로부터 UserDao 오브젝트를 받아자가, 자신의 관심사인 테스트를 위해 활용하기만 하면 그만이다.
public class UserDaoTest{
public static void main(String[] args) throws SQLException{
UserDao dao = new DaoFactory().userDao();
...
}
}
1.4.2 오브젝트 팩토리의 활용
DaoFactory에 UserDao가 아닌 다른 DAO의 생성 기능을 넣으면 어떻게 될까? AccountDao, MessageDao 등을 만들었다고 가정해보자.
//ConnectionMaker 구현 클래스를 선정하고 생성하는 코드의 중복
public Class DaoFactory {
public UserDao userDao() {
return new UserDao(new DConnectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(new DConnectionMaker());
}
public MessageDao messageDao() {
return new MessageDao(new DConnectionMaker());
}
}
중복 문제를 해결하려면 역시 분리해내는 게 가장 좋은 방법이다.
public Class DaoFactory {
public UserDao userDao() {
return new UserDao(connectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(connectionMaker());
}
public MessageDao messageDao() {
return new MessageDao(connectionMaker());
}
}
//분리해서 중복을 제거한 ConnectionMaker타입 오브젝트 생성코드
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
1.4.3 제어권의 이전을 통한 제어관계 역전
제어의 역전이라는 것은 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다.
일반적으로 프로그램의 흐름을 보면 main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 오브젝트 메소드 안에서 다음에 사용할 것을 결정하는 식 작업이 반복된다.
제어의 역전이란 이런 제어 흐름의 개념을 거꾸러 뒤집는 것이다. 제어의 역전에서는 오브젝트가 자신의 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는 지를 알수가 없다.
제어의 역전의 예 서블릿을 생각해보면, 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고, 그 안의 메소드를 호출한다.
프레임워크는 분명한 제어의 역전 개념이 적용되어 있어야 한다. 애플리케이션 코드는 프레임워크가 짜놓은 틀에서 수동적으로 동작해야 한다.
우리가 만든, UserDao와 DaoFactory 에도 제어의 역전이 적용되어 있다. 원래 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 제어권은 UserDao에게 있었다. 그런데 지금은 DaoFactory에게 있다. ConnectionMaker 구현 클래스를 만들고 사용할지를 결정할 권한을 DaoFactory에 넘겼으니, UserDao는 이제 능동적인게 아니라 수동적인 존재가 됬다.
제어의 역전에서도 프레임워크 또는 컨테이너와 깉이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리등을 관장하는 존재가 필요한다.
1.5 스프링의 IOC
1.5.1 오브젝트 팩토리를 이용한 스프링 IoC
애플리케이션 컨텍스트와 설정정보
스프링에서는 스프링의 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈이라고 부른다. 스프링에서는 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 부른다. 보통 빈 팩토리보다는 이를 좀더 확장한 애플리케이션 컨텍스트(application context)를 주로 사용한다. 두가지 동일하다고 보면 된다.
DaoFactory를 사용하는 애플리케이션 컨텍스트
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
@Configuration
public Class DaoFactory{
//오브젝트 생성을 담당하는 IoC용 메소드라는 표시
@Bean
public UserDao userDao(){
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new DConnectionMaker();
}
}
DaoFactory를 설정정보를 사용하는 애플리케이션 컨텍스트를 만들어보자. 애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트다.
public class UserDaoTest {
public static void main(String[] args) throws SQLException{
AplicationContext conext =
new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
}
}
스프링을 적용하긴 했지만, DaoFactory를 만든 것보다 좀 더 번거로운 준비 작업과 코드가 필요하다. 그렇다면 굳이 스프링을 사용하지 않고, DaoFactory와 같이 오브젝트 팩토리를 만들어서 사용하면 되는 게 아닐까? 스프링은 DaoFactory를 통해서 얻을 수 없는 방대한 기능과 활용방법을 제공해준다.
애플리케이션 컨텍스트의 동작방식
클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
애플리케이션 컨텍스트는 종합 IOC 서비스를 제공해준다.
오브젝트 관계설정 말고도, 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.
애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
getBean() 메소드는 빈의 이름을 이용해서 빈을 찾아준다. 타입만으로 빈을 검색하거나 특별한 어노테이션 설정이 되어 있는 빈을 찾을 수도 있음
1.6 싱글톤 레지스트리와 오브젝트 스코프
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();
System.out.println(dao1);
System.out.println(dao2);
springbook.dao.UserDao@118f375 springbook.dao.UserDao@117a8bd
ApplicationContext conext = new
AnnotationConfigApplicationConext(DaoFactory.class);
UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);
System.out.println(dao3);
System.out.println(dao4);
springbook.dao.UserDao@ee22f7 springbook.dao.UserDao@ee22f7
스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다는 것이다. 단순하게 getBean()을 실행할 때마다 UserDao() 메소드를 호출하고 매번 new에 의해 새로운 UserDao가 만들어지지 않는다.
왜 그럴까?