스프링의 핵심 철학은 객체지향 프로그래밍이므로, 오브젝트에 초점을 맞춘다.
따라서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 고려해야만 한다.
(오브젝트 설계)
다양한 목적을 위해 재활용 가능한 디자인 패턴,
좀 더 깔끔한 구조가 되도록 개선해나가는 리팩토링,
오브젝트가 제대로 작동하는지에 대한 단위 테스트가
필요로 된다.
1.1 초난감 DAO
DAO는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.
자바빈?
JSP에서 객체를 가져오기 위한 기법은 DTO라고도 한다.
( getter, setter로 구성 )
DAO를 통해서 사용자 정보를 관리한다.
JDBC는 다음과 같은 순서로 작동한다.
DB 연결을 위한 Connection을 가져온다.
SQL을 담은 Statement를 만든다.
만들어진 Statement를 실행한다.
조회의 경우 SQL 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
작업 중에 생성된 Conncetion, Statement,ResultSet 같은 리소스는 반드시 작업 종료 후 close한다.
JDBC API가 만든 예외를 직접 처리하거나 throws를 선언해서 예외가 발생하면 메소드 밖으로 던진다.
public class UserDao{
// Exception은 Dao가 처리하지 말고, 딴 애가 처리한다.
public void add(User user) throws ClassNotFoundException, SQLException{
...}
public User get(String id) throws ClassNotFoundException, SQLException{
...}
}
오브젝트 스스로 자신을 검증하도록 만드는 것이 가장 좋은 테스트 방법이다.
(main 메서드에서 실행)
하지만, 1.1.2에서 한 방식의 코드는 너무 복잡해서 수정이 필요하다.
1.2 DAO의 분리
애플리케이션은 끊임없이 시간에 따라 변화를 한다.
따라서 미래의 변화에 대비해서 어떻게 설계를 할지가 중요하다.
가장 좋은 대책은 변화의 폭을 최소한으로 줄이는 것이다.
관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.
UserDAO에서 DB 커넥션을 가져오는 코드는 다른 관심사(DB에 데이터 INSERT)와 섞여서 add()메서드에 담겨 있다.
가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다.
DB 연결 코드를 getConncection()이라는 독립적인 메소드로 만들어둔다.
각 DAO는 getConncetion()을 호출함으로써
DB 커넥션을 가져올 수 있다.
public void add(User user) throws ClassNotFound...{
Conncetion c = getConnection();
}
pubilc User get(String id) ... {
Conncetion c= getConnection();
}
이런식으로 하면 DB수정이 필요할 때, getConnection만 수정하면 된다.
getConncetion()이라고 하는 공통의 기능을 담당하는 메서드로 중복된 코드를 뽑아내는 것을
리팩토링에서 "메서드 추출 기법"이라고 부른다.
리팩토링이란?
기존의 코드를 외부의 동작방식에는 변화 없이
내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.
중복된 코드가 매우 흔하게 발생이 되면,
반드시 리팩토링으로 수정을 해야만 한다.
Ex)
N사와 D사가 각기 다른 종류의 DB를 사용하고 있고,
DB 커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다.
그리고 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다.
UserDao 소스코드를 N사와 D사에 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDAO을 사용하게 어떻게 하나?
UserDao의 구현 코드를 제고하고, getConncetion()을 추상 메서드로 만든다.

이렇게 슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성,실행,반환)을 만들고,
그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤,
서브 클래스에서 필요에 맞게 구현해서 사용하도록 하는 방법을
UserDao의 getConnection()메소드는Connection 타입 오브젝트를 생성한다는 기능을 정의한 추상 메소드이다.
그리고 UserDao의 서브클래스의 getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정할 수 있다.

위와 같이 세부적인 사항을 잘게 쪼갠 후( 관심사항이 다른 코드를 분리하고), 템플릿으로 만든다.
그러면 하위 상속받은 클래스는 템플릿을 커스터마이징해서 입맛대로 사용하면 된다.
( 자바는 다중상속을 허용하지 않는다. )
단지, 커넥션 객체를 가져오는 방법을 분리하기 위해 상속구조를 만들면, 후에 다른 목적으로 UserDao에 상속을 적용하기 힘들다.
또한 상속은 상하위 클래스의 관계가 생각보다 밀접해진다.
만약 UserDao 외의 DAO 클래스들이 계속 만들어지면 그때는 상속을 통해서 만들어진 getConnection()의 구현 코드가 매 DAO 클래스마다 중복된다.
여태껏 독립된 메소드를 만들어서 분리했고,
상하위 클래스로 분리했다.
이번에는 아예 상속관계도 아닌 독립적인 클래스로 만든다.

UserDAO는 데이터를 다루는 역할을
SimpleConnectionMaker는 Connection을 담당한다.
(SimpleConnectionMaker는 오브젝트를 만들어두고 재사용을 하는 편이 낫다.)
public class UserDao{
private SimpleConnectionMaker simpleconnectionMaker;
public UserDao(){
simpleConnectionMaker = new SimpleConnectionMaker();
}
}
public class SimpleConnectionMaker{
...
}
위와 같이 클래스를 분리한 경우에도 상속을 이용했을 때와 같이 자유로운 확장을 하려면,
SimpleConnectionMaker의 메소드 문제
만약 D사에서 openConncetion()이라는 메소드를 사용했다면,
우리가 만든 connection()과 호환이 안됨.
DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다.
이런 문제가 발생한 이유는 UserDao가 DB 커넥션을 가져오는 구체적인 방법에 종속이 되었기 때문이다.
(인터페이스가 없다.)
긴밀한 연결이 아니라 느슨한 연결고리를 만들어준다.
추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보는 모두 감춰버린다.
인테페이스로 추상화를 함으로써 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다.
( 호환성과 이식성이 뛰어나다 )

인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다.
(구현 방법은 알 수 없다.)
인터페이스를 도입함으로써
UserDao는 Connection 타입의 오브젝트를 반환 받을 것이라고 기대할 수 있다.
(구체적인 작동방식, 타입은 몰라도 된다)
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(){
// conncectionMaker에는 MySQL, ORACLE 등 가능하다
connectionMaker = new DConnectionMaker();
}
public void add(User user) ...{
// 인터페이스로 만들어졌기 때문에, UserDao는 문제없이 그냥 가져다 사용하면 된다.(호환성이 우수)
Connection c = connectionMaker.makeConncetion();
}
}
그러나 생성자의 코드는 제거되지 않고 남아 있다.
(클래스 이름을 넣어서 오브젝트를 만들어야 한다!)
앞서 UserDao는 구체적인 클래스까지 알아야 하는 문제가 발생한다.
( new DConnectionMaker()로 인해서 발생 )
그래서, UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것이 중요하다.
내부에서 Connection을 생성하는 것보단
외부에서 주입을 받는 편이 훨씬 낫다
(어떤 것이든지 호환이 가능하다.)
메소드 파라미터나, 생성자 파라미터를 이용해서 주입을 받으면 된다.
여기서 클래스 사이의 관계와 오브젝트 사이의 관계를 이해를 해야하는데,
오브젝트 사이 관계를 맺음으로써, 특정 클래스에 대해서는 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면,
그 클래스를 사용할 수 있다.
(클래스 사이 관계는 객체를 직접 주입하는 것인데, 이렇게 하면 관계가 너무 구속된다.)
그래서 사등뫄 같이 수정
public UserDao(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
클래스나 모듈은 "확장"에는 열려 있어야 하고
"변경"에는 닫혀 있어야 한다.
Ex)
UserDao는 DB 연결 방법이라는 기능을 "확장"하는 데 열려 있고,
자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로,
"변경"에는 닫혀 있다.
인터페이스를 사용해 확장 기능을 정의한 대부분의 API는 "개방 폐쇄 원칙을 잘 따른다."
객체지향 설계 원칙(SOLID)
SRP(The Single Responsiblity Principle) : 단일 책임 원칙
OCP(The Open Closed Principle) : 개방 폐쇄 원칙
LSP(The Liskov Substitution Principle) : 리스코프 치환 원칙
ISP(The Interface Segregation Principle) : 인터페이스 분리 원칙
DIP(The Dependency Inversion Principle) : 의존관계 역전 원칙
응집도가 높다 = 하나의 모듈(클래스)은 하나의 책임만 진다.
(불필요한 외부 관심과 책임은 얽혀 있지 않으며, 하나의 공통 관심사는 한 클래스에 모여 있다.)
변경이 일어날 때 모듈의 많은 부분이 함께 바뀔 수 있다.
( 일부분만 바뀌더라도 전체에 영향을 미칠 수 있다. )
인터페이스를 사용할 경우, 그 인터페이스를 구현한 구체적인 클래스만
갈아 끼우면 된다.
느슨한 연결은 관계를 유지하는 데 꼭 필요한 최소한의 방법만 제공하고,
나머지는 서로 독립적이고 알 필요가 없다.
결합도가 낮으면 변화에 빠르게 대응할 수 있고, 확장하기 편하다.
낮은 결합도는 하나의 변경이 발생할 때 여타 모듈과 객체로 변경에 대한
요구가 전파되지 않는 상태를 말한다.
(그 모듈을 사용하는 클라이언트는 인터페이스에 명시된 설명서대로 사용하기만 하면 된다.)
꼭 필요한 관계만 인터페이스를 통해 낮은 결합도로 최소한으로 연결된다.
(이를 통해서 해당 인터페이스가 변경,확장을 하더라도
그 인터페이스를 사용하는 클라이언트는 기능 사용에 문제가 없다.)
필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고,
이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
클라이언트는 인터페이스를 통해서 전략을 바꿔가면서 사용할 수 있다.
성격이 다른 책임이나 관심사는 분리하는 것이 주요한 작업이자 핵심이다.
객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 반환을 한다.
(단지 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하는 것이 목적이다.)
pulbic class DaoFactory{
public UserDao userDao(){
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
// 이런식으로 클라이언트 객체가 구체적인 오브젝트를 알아야 하는 경우, 매우 경직되어 있다.

UserDao는 구체적인 클래스,
ConncetionMaker는 인터페이스,
DaoFactory는 위 두 모듈을 이용해서 객체를 반환하는 역할을 담당한다.
핵심은 애플리케이션의 컴포넌트 역할을 하는 오브젝트와
애플리케이션의 구조를 결정하는 오브젝트를 분리했다는데 가장 의미가 있다.
// 문제의 코드
// new DConnectionMaker()가 중복이 된다.
public class DaoFactory{
public UserDao userDao(){
return new UserDao(new DC...())
}
public AccountDao accountDao(){
return new AccountDao(new DC...())
}
...
}
위 코드로에서 DC...가 아니라 다른 것으로 바뀔 경우,
일일히 전부 수정을 해야하는 문제가 발생한다.
그래서, 중복 문제를 해결하는 방법은 분리하는 것이다.
public class DaoFactory{
public UserDao(){
return new UserDao(connectionMaker());
}
public AccountDao accountDao(){
return new AccountDao(connectionMaker());
}
...
// 아래와 같이 주입 메소드를 따로 분리한다.
// 수정이 필요할 경우 한군데만 변경을 하면 전역적으로 수정이 가능함.
public ConnectionMaker conncetionMaker(){
return new DC...();
}
}
기존
main()메소드와 같은 프로그램 시작 지점에서 오브젝트를 직접 생성하고, 만들어진 오브젝트의 메소드를 사용한다.
또한 그 오브젝트를 필요한 시점에서 생성해두고, 각 메소드에서 이를 사용한다.
(모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장한다.)
제어의 역전(IoC)
모든 오브젝트는 제어 권한을 자신이 아닌 다른 대상에게 위임한다.
(모든 오브젝트는 자신의 생성, 사용을 알 수 없다.)
Ex)
일반적으로 main() 메소드에서 시작해서 개발자가 미리 정한 순서대로 오브젝트가 생성되고 실행이 된다.
하지만, 서블릿 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고
그 안의 메소드를 호출한다.
템플릿 메소드 패턴에서도 슈퍼클래스에서 add(), get() 등
필요할 때 하위 클래스의 기능을 호출한다.
라이브러리는 애플리케이션 흐름을 직접 제어한다.
(단지 동작하는 중에 필요한 기능이 있을 떄 능동적으로 라이브러릴 사용한다.)
반면에, 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용된다.
프레임워크 위에 개발한 클래스를 "등록"해두고,
프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식이다.
프레임워크와 라이브러리는 제어의 역전이 되었느냐의 차이이다.
(프레임워크가 짜놓은 틀에서 수동적으로 동작을 해야한다.)
DaoFactory의 경우도 UserDao는 팩토리에 의해서 수동적으로 만들어지고 자신이 사용할 오브젝트도 DaoFactory가 공급해주는 것을
수동적으로 사용해야 할 입장이다.
또한 UserDao와 ConnectionMaker의 구현체를 생성하는 책임도 DaoFactory가 담당한다.
스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트 = 빈(Bean)
빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트 = 빈 팩토리(bean factory)
applicationContext는 별도의 정보를 참고해서
빈의 생성, 관계설정 등의 제어 작업을 총괄한다.
applicationCotext는 어떻게 사용할지에 대한 정보가 없지만,
별도의 설정정보를 이용하는 IoC엔진이라고 보면 된다.
@Configuration
빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스
@Bean
오브젝트를 만들어주는 메소드
위 두가지 설정만으로도 IoC기능을 사용할 수 있다.
// 빈등록
@Configuration // applicationContext가 사용할 설정정보
public class DaoFactory{
@Bean // 오브젝트 생성을 담당하는 IoC용 메소드
public UserDao userDao(){
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker(){
return new DConnectionMaker();
}
}
public class UserDaoTest{
public static void main(String args[]) ...{
ApplicationContext context=
new AnnotaionConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao",UserDao.class)
}
}
getBean으로 applicationContext가 관리하는 오브젝트를 요청할 수 있다.
"userDao"를 통해서 이에 해당 하는 빈을 가져온다.
(클래스는 같으나 이름만 다르게 해서 여러개의 빈을 생성할 수 있다.)
applicationContext = IoC 컨테이너 = 스프링 컨테이너 = 빈 팩토리
applicationContext는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 성성, 관계설정을 담당한다.
(직접 오브젝트를 생성하고 관계를 맺어주는 코드는 없고,
그런 생성정보와 연관관계 정보를 별도의 설정정보를 통해서 얻는다.)

ApplicationContext는 @Configuration이 붙은 클래스들을 설정 정보로 등록해두고, @Bean이 붙은 메소드의 이름으로 빈 목록을 생성한다.
클라이언트가 해당 빈을 요청한다.
ApplicationContext는 자신의 빈 목록에서 요청한 이름이 있는지 찾는다.
ApplicationContext는 설정 클래스로부터 빈 생성을 요청하고, 생성된 빈을 돌려준다.
클라이언트는 필요한 오브젝트를 가져오려면 어떤 팩토리 클래스를 사용해야 할지 알아야 하고,
필요시 팩토리 오브젝트를 생성해야 하는 번거로움이 있다.
applicationContext를 통해서 오브젝트 팩토리가 아무리 많아도 이를 다 알 필요가 없고,
일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
오브젝트 생성, 관계설정, 만들어지는 방식, 시점 및 전략을 다르게 가져갈 수 있고,
기타 다양한 기능을 제공한다.
getBean()메소드를 통해서 빈의 이름을 이용해서 빈을 찾을 수 있다.
스프링이 IoC 방식으로 관리하는 오브젝트.
오직 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.
스프링의 IoC를 담당하는 핵심 컨테이너.
빈을 등록, 생성, 조회, 반환, 부가적인 기능까지 담당.
주로 applicationContext를 이용
빈 팩토리를 확장한 IoC 컨테이너(기본적으로 빈 팩토리와 동일)
applicationContext를 적용하기 위해 사용하는 메타정보.
configuration이라고도 함.
applicationContext = 빈 팩토리 = (IoC)컨테이너
스프링 프레임워크 = IoC 컨테이너 + application 컨테이너