0. 스프링을 학습하기 전
스프링의 핵심 철학은, 객체지향 프로그래밍이 제공하는 가치를 활용하는 것이다.
그래서 스프링을 학습하기전에 객체 생성 -> 관계 형성 -> 소멸 까지의 흐름을 숙지하면 좋다.
또한 스프링을 사용하다보면 객체지향 설계/원칙을 자연스럽게 적용할 수 있도록 설계되어있다.
1.1.1 초난감 DAO
1
2
3
4
5
6
7
8
9
public class User {
String id;
String name;
String password;
// Getter 코드는 있다고 상상하기로 하자
// Setter 코드는 있다고 상상하기로 하자
}
여기서 기본 상식! 자바 Bean 이란?
간단하게 Bean 이라고도 불리는 자바 Bean은 다음 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다.
디폴트 생성자
자바 Bean은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 프레임워크에서 리플렉션을 통해 오브젝트를 생성하기 때문에 필요하다.
프로퍼티
자바 Bean이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 setter 혹은 getter로 수정/조회할 수 있다.
1.1.2 UserDAO
JDBC 기반의 DAO를 제작한다. 여기서 JDBC를 이용하는 작업의 일반적인 순서는 다음과 같다.
- DB 연결을 위한 Connection을 가져온다.
- SQL을 담은 Statement를 만든다.
- 만들어진 Statement를 실행한다.
- 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아, 정보를 저장할 객체에 옮긴다.
- 작업 중 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 해제한다.
- JDBC API 가 만들어내는 Exception은 직접 잡아서 처리하거나, 메소드 밖으로 던지도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class UserDAO {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/diger", "spring", "book");
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(1, user.getName());
ps.setString(1, user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/diger", "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;
}
}
복잡해보이지만 간단한 DAO 클래스를 만들었다. 근데 이 기능을 어떻게 테스트하지???
실제로 웹 애플리케이션을 만들고 서버를 배치해서 기능을 테스트하는건 너무 번거롭고 비용이 크다.
다음과 같이 테스트 해보자
1.1.3 main()을 이용한 DAO 코드 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[]args)throw ClassNotFoundException, SQLException{
UserDAO dao-new UserDAO();
User user=new User();
user.setId("diger")
user.setName("김도현");
user.setPassword("1009");
dao.add(user);
System.out.println("등록 성공");
User user2=dao.get(user.getId());
System.out.println(user2.getName());
System.out.println("조회 성공");
}
이 코드는 기능에 정말 충실하다. add, get 에 대한 책임을 분명히 지키고 있고 테스트 또한 정상적으로 동작한다.
근데 왜 이 코드가 초난감 DAO 라고 불리는 건가? -> 이에 대한 명확한 답은 정해진게 없지만 스프링에 대해 학습하게 되면서 깨닫게 될 것이다.
지금까지의 개인적인 생각으로는 DAO 자체가 특정 DBMS(예제에선 MySQL)에 종속적이다.
1.2.1. 관심사의 분리
스프링은 객체지향 극대화 하기 위해 도움을 줄 수 있는 프레임워크라고 이야기했다.
객체지향은 변화에 민감하며 변화에 대응하기 위해 존재한다. 여기서 뜻하는 “변화”란, 객체를 구성하는 필드가 변한다는 것이 아닌 요구사항으로 인해 객체의 설계와 구현 코드가 변한다는 것을 의미한다.
관심사의 분리는 변화에 대응할 수 있는 가장 좋은 방법은 변화의 폭을 최소한으로 줄여주는 것이다.
이게 무슨말이냐 하면, 다음과 같은 상황으로 변경에 대한 요청이 있을 때
사용자가 사용할 DBMS가 MySQL이 아닌 MariaDB로 변경 되었다고 해보자.
위에서 작성한 DAO 코드를 그대로 사용할 수 있는가? –> 아니다. 드라이버부터 시작하여 잘못하면 Statement 까지 손봐야한다.
또한 DB 접속용 암호를 변경하기 위해 DAO 클래스를 수백 개를 수정해야한다면? 이는 변경에 최악이다.
프로그래밍의 기초 개념 중에 관심사의 분리(Separation of Concerns) 라는게 있다.
1.2.2 커넥션 만들기의 추출
앞서 이야기한 문제점을 보완하고자 우선 DB 커넥션부터 분리해보자.
그리고 그 전에 UserDAO의 관심사항을 정리해보겠다.
UserDAO의 관심사항
- DB와 연결을 위한 커넥션을 어떻게 가져올 것인가? (어떤 드라이버를 사용, 어떤 로그인 정보를 사용, 커넥션 생성방법 등)
- 사용자 등록을 위한 DB에 보낼 Statement를 만들고 실행하는 것
- 작업이 끝났을 때 리소스를 반환하는 방법
현재는 위의 관심사 모두가 하나의 클래스에 묶여있다. 관심사가 여러개 묶여있다면 변경이 일어날 때 수백개의 클래스를 함께 수정해야하는 안좋은 상황이 발생하게 된다.
getConnection() 메서드 추출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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/diger","spring","book");
return c;
}
add get 이외의 메서드가 1000개가 넘는 상황에서 DB 연결 방식이 변경된 상황이 있다고 했을 때
위와 같이 Connection을 분리하면, 1000개에 대한 메서드를 직접 손댈 필요 없이 getConnection() 메서드만 수정하면 변경에 대응이 가능하다.
그런데 이렇게 리팩터링을 수행해도 여전히 문제점이 남아있다.
테스트할 때 발생하는 문제인데, main() 메서드를 여러 번 실행하면 두 번째 부터는 무조건 예외가 발생한다. 테이블의 기본 값인 id가 반복 되기 때문이다.
이는 점진적으로 해결해보도록한다.
1.2.3. DB 커넥션 만들기의 독립
위에서 작성한 DAO 코드가 여러 사용자에게 판매되었다고 가정해보자.
그리고 그 여러 사용자는 각기 다른 DBMS를 사용하고 있으며, DB 커넥션을 가져오는데 독자적인 방법을 사용하고 싶다는 요구사항이 있다고 하자.
UserDAO 소스코드를 변경하는 방법을 알려주지 않고 이 요구사항을 지킬 수 있는 방법이 있을까?
상속을 통한 확장으로 해결하자
기존 UserDAO 코드를 한 번 더 분리한다. 이 말인 즉슨 getConnection()을 추상 메서드로 만든다.
따라서 add(), get() 메서드에서 getConnection()메서드를 호출하는 코드는 그대로 유지할 수 있다.
그리고 이렇게 분리한 추상 클래스를 각 사용자에게 판매한다.
구매자(사용자)는 UserDAO 클래스를 상속하여, NUserDAO, DUserDAO 라는 서브클래스를 만든 후 DB Connection 에 관한 구문은 본인들이 원하는대로 작성하여 사용하면 된다.
추상 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 abstrcat Connection
getConnection() throws ClassNotFoundException, SQLException {
}
}
구매자 1. 상속 및 구현 클래스
1
2
3
4
5
6
7
public class NUserDAO extends UserDAO {
public Connection getConnection() throws ClassNotFoundException, SQLException {
// DB connection 생성 코드
}
}
구매자 2. 상속 및 구현 클래스
1
2
3
4
5
6
public class DUserDAO extends UserDAO {
public Connection getConnection() throws ClassNotFoundException, SQLException {
// DB connection 생성 코드
}
}
이렇게 슈퍼클래스에서 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환) 등을 반들고 그 기능의 일부를 추상 메서드나
오버라이딩 가능한 protected 메서드로 만든 뒤 서브클래스에서 사용에 알맞게 구현해서 사용하는 방법을 템플릿 메서드 패턴 이라고 한다.
이 방법은 관심사를 깔끔하게 분리하여 서로 독립적으로 변경/확장을 할 수 있게 해준다는 장점이 있지만
상속이라는 한계점이 많은 기능을 사용하여 이미 UserDAO가 다른 클래스를 상속받고 있는 요소가 있다면 다중상속을 지원하지 않는 Java 생태계에서는 문제가 발생하게 된다.
또한 DAO 클래스가 하나일 땐 문제가 되지 않지만, DAO 클래스가 여러개 일때도 무조건 상속을 받아서 구현을 해주며 getConnection 구현 코드가 중복된다는 문제도 가지고 있다.
1.3. DAO 확장
모든 객체는 변하지만 동일한 방식으로 변하는 건 아니다. 지금까지의 예제 상황에서는 다음 두가지로 관심사를 분리했다.
- 데이터 엑세스 로직을 어떻게 만들 것인가.
- DB 연결을 어떤 방법으로 할 것인가.
관심사 분리는 나쁘지 않게 했지만, 상속으로 해결한 상황이라 그리 좋다고는 못하는 상황이다. 다음 내용부터 이를 개선한다.
1.3.1. 클래스 분리
지금까지는 관심사 분리를 메서드 분리, 상하위 클래스로 분리로 수행해왔다. 이번에는 상속관계도 아닌 완전하게 독립적인 클래스로 만들어 본다.
DB 커넥션과 관련된 부분을 서브 클래스가 아닌, 아예 별도의 클래스에 담는 것이다.
그리고 이렇게 만든 클래스를 UserDAO가 이용하게 하면 되는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserDAO {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDAO() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
}
public void 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;
}
}
근데 이렇게 독립적으로 분리하면, 챕터 1.2. 에서 들은 상황에서 N과 D에 대한 UserDAO 클래스만 공급하고, 상속을 통해 DB 커넥션 기능을 확장해서 사용할 수 없게되었다.
왜 그런가?? –> UserDAO 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때처럼, UserDAO 코드의 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없는 것이다.
그니까, UserDAO만 가지고는 SimpleConnectionMaker 객체에 쓰여있는 Driver 이름 같은내용을 바꿀 수가 없게 되었다는 것이다!
이 문제의 근본적인 원인은 UserDAO가 바뀔 수 있는 정보, DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있는 것이다.
어떤 클래스가 쓰일지, 그 클래스에서 커넥션을 가져오는 수 많은 메서드 중 어떤 것을 사용해야할지 이름까지 일일히 알고 있어야한다.
이 문제를 어떻게 해결할 수 있을까?
1.3.2. 인터페이스 도입
클래스를 분리하면서도 이러한 문제를 해결할 수 있는 방법은 두 클래스 간 긴밀하게 연결되어 있지 않도록 하는 것이다.
즉, 중간에 추상적인 느슨한 연결고리를 만들어 주는 것인데 이 말이 뭔말이냐 함은.. 어떤 것들의 공통적인 성격을 뽑아내서 이를 따로 분리해내는 작업이다.
자바에서는 이 작업을 인터페이스라는 도구를 제공한다.
그럼 인터페이스를 적용하면 코드가 어떻게 되는지 한 번 알아보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLExecption;
}
public class DConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLExecption {
// D사의 Connection을 생성하는 코드가 있다고 상상하자.
}
}
public class NConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLExecption {
// N사의 Connection을 생성하는 코드가 있다고 상상하자.
}
}
public class UserDAO {
private ConnectionMaker connectionMaker;
public UserDAO {
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
}
public void get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
}
}
이 코드에서, UserDAO의 add, get 메서드 내에서, Connection()을 호출하는 구문은, 인터페이스에 정의된 메서드를 사용하므로 클래스가 바뀐다고 해서 메서드 이름이 변경될 이유가 없다.
그런데, 이 코드 역시 문제점이 있다. 인터페이스를 이용해서 메서드 이름따위를 외워서 사용해야할 필요는 없어졌지만
UserDAO를 생성할 때 특정 구현체를 생성하는 코드가 남아있는 것이다. 이래선 위의 예제와 마찬가지로 자유도가 너무 떨어진다. 생성자에서 특정 클래스에 직접적으로 결합되어있기 때문…
아니 근데 클래스 이름으로 객체를 생성하는게 아니면 어떻게 해야하지..?
1.3.3. 관계설정 책임의 분리
지금 왜 이런 상황이 발생했나? (이런상황 : UserDAO가 인터페이스 뿐만아니라 ConnectionMaker 구현체까지 알아야하는 상황)
UserDAO안에 분리되지 않은 또 다른 관심 사항이 존재하기 때문이다. (DConnectionMaker를 생성하는 또 다른 관심사가 생긴 것이다.)
UserDAO에는 어떤 구현체를 사용할지 결정하는 new DConnectionMaker() 라는 코드가 (new NConnectionMaker()도 가능) 있다.
UserDAO가 UserDAO가 사용할 ConnectionMaker의 구현체 사이의 관계를 설정해주는 관심, 이 관심을 분리하지 않으면 UserDAO는 독립적으로 확장 가능한 클래스가 될 수 없다.
그러면 UserDAO를 사용하기 전에 UserDAO가 어떤 ConnectionMaker의 구현 클래스를 사용할지를 결정하도록 만들어야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserDAO {
public UserDAO(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
}
public void get(String id) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
}
}
public class UserDAOTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDAO dao = new UserDAO(connectionMaker);
}
}
이렇게 UserDAO에서 생성자를 통해 클라이언트로부터 어떤 종류의 Connection을 사용할지 받아오는 것으로 관심사항을 클라이언트에게 넘겼다.
1.3.4 원칙과 패턴 (초난감 DAO 개선 내용 정리)
지금까지 개선해온 코드 내에서 사용된 객체지향 원칙/패턴을 소개하겠다.
개방 폐쇄 원칙
클래스나 모듈은 확장에는 열려있고, 변경에는 닫혀있어야 한다.
UserDAO는 DB 연결 방법이라는 기능을 확장하는 데에는 열려있다. (왜냐? 클라이언트에서 생성자에 DB연결 방법을 넣고싶은대로 넣어주면 되기 때문이다.)
또한 이 과정에서 UserDAO의 핵심 기능을 구현한 add, get 메서드는 전혀 건들지 않아도 되니 변경에는 닫혀있다는 것이다.
개방 폐쇄 원칙 초간단 요약 –> 확장 가능성이 있는 객체와의 상호작용은 생성자로 받자.
높은 응집도와 낮은 결합도
개방 폐쇄 원칙 == 높은 응집도와 낮은 결합도
응집도가 높다는 건 무슨말인가?? –> 하나의 모듈, 클래스가 하나의 책임 혹은 관심사에만 집중되어 있다는 것이다.
하나의 공통 관심사는 한 클래스에 모여있다. 또한 높은 응집도는 클래스 레벨 뿐 만 아니라 패키지/컴포넌트/모듈에도 일컫는다.
높은 응집도
응집도가 높다는 의미는, 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것이다.
이게 무슨말이냐면, 변경이 일어났을 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 하는 것이다.
만약 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체에서 어떤 부분이 바뀌어야 하는지 파악해야하며 그 변경으로 인해 바뀌지 않는 부분에는 다른 영향이 없는지 확인해야한다.
변화가 일어났을 때 모듈(쉽게 생각하면 클래스) 범위에 많은 부분이 변경되어야 한다.
낮은 결합도
느슨하게 연결된 형태를 유지하는 것이 바람직 하다는 것이다.
조금 더 풀어 말하자면, 객체간의 관계를 최소한의 방법으로 간접적인 형태로 제공하고, 나머지는 서로 알 필요 없이 만들어 버리는 것이다.
또한 이 낮은 결합도가 의미하는 것은, 하나의 변경이 발생할 때 관계가 있는 다른 클래스에 천파만파 영향이 전파되지 않아야 한다는 것이다.
지금까지 개선해온 UserDAO 클래스의 경우, 어떤 연결 종류에 관한 것인지에만 나타내는 관계가 있고, 그 구체적인 내용(구현체)은 신경 쓰지 않으며
NConnection 에서 DConnection으로 변경되었다고 하더라도, add, get 메서드를 철저하게 수행할 수 있는 코드이다.
전략 패턴
현재 UserDAOTest - UserDAO - ConnectionMaker 구조를 디자인 패턴 시각으로 보면 전략 패턴에 해당한다고 볼 수 있다.
전략 패턴은 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘(로직) 클래스를 필요에 따라 바꿔 사용할 수 있게 하는 디자인 패턴이다.
UserDAO는 전략패턴이라고 볼 수 있는데, UserDAO의 기능을 수행하는 데 필요한 기능 중 변경가능한 “DB 연결 방식” 이라는 알고리즘(로직, 책임)을 바꿔가면서 사용할 수 있게 분리했기 때문이다.
또한, 전략패턴은 USerDAOTest와 같은 클라이언트의 필요성이 포함되어있고, UserDAO(컨텍스트)를 사용하는 UserDAOTest(클라이언트)는 UserDAO가 사용할 전략(DConnectionMaker 혹은 NConnectionMaker)을 컨텍스트의 생성자 등을 통해 제공하는 것이 일반적이다.
스프링은 이 객체지향 원칙과 디자인 패턴의 장점을 자연스럽게 사용할 수 있도록 하는 프레임워크이다.
1.4. 제어의 역전(IoC (Inversion of Control))
지금까지 작성해온 내용은 초난감 DAO를 개선해나가는 작업이였다. 그런데 여기서 우리는 UserDAOTest를 넘겨짚듯이 작성했다.
이게 무슨말인가 하면 UserDAOTest는 기존 UserDAO가 직접 담당하던 기능, 어떤 ConnectionMaker 구현체를 사용할지를 결정하는 기능을 어쩌다보니 떠맡았다.
UserDAO가 ConnectionMaker 인터페이스를 구현한 특정 클래스로부터 완벽하게 독립할 수 있도록 UserDAOTest가 그 책임을 떠안게 된 것이다.
하지만 원래 UserDAOTest는 UserDAO의 기능이 잘 동작하는지 테스트하기 위해 만들었기 때문에, 또 다른 책임을 맡게 된 것이라 문제가 있다는 말이다.
1.4.1. 오브젝트 팩토리
UserDAO가 어떤 ConnectionMaker 구현체를 사용할지에 대한 분리를 책임지는 클래스를 만드는데 이를 팩토리라고 한다.
팩토리의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 객체를 돌려주는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DaoFactory {
public userDAO userDAO() {
ConnecitonMaker connecitonMaker = new DConnectionMaker();
UserDAO userDAO = new UserDAO(connecitonMaker);
return userDAO;
}
}
public class UserDAOTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDAO dao = new DaoFactory().userDao();
}
}
위와 같이 팩토리를 통해 UserDAOTest를 작성하면, UserDAO가 어떻게 만들어지는지, 어떻게 초기화가 되어있는지에 대한 신경을 쓸 필요가 없어진다.
여기서 조금 더 확장하자면, DaoFactory에 UserDAO가 아닌 다른 DAO 까지 생성하는 기능을 추가한다고 생각해보자.
1.4.2. 오브젝트 팩토리의 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
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());
}
}
다음과 같이 작성할 수 있을텐데 딱 봐도 뭔가 좀 불편하다.
new DConnecitonMaker() 라는 구문이 매 DAO 생성마다 중복되고 있는 것이다. 역시나 이러한 중복은 좋은 상황은 아니기 때문에 고쳐보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DaoFactory {
public userDAO userDAO() {
return new UserDAO(connectionMaker());
}
public AccountDAO accountDAO() {
return new AccountDAO(connectionMaker());
}
public MessageDAO messageDAO() {
return new MessageDAO(connectionMaker());
}
public ConnecitonMaker connectionMaker() {
return new DConnectionMaker();
}
}
DConnectionMaker 객체를 만들어주는 메서드를 추가하여, 각 DAO 생성 마다 해당 메서드를 추가하도록 변경하니 중복된 코드가 꽤나 사라졌다.
그리고 여기에는 제어의 역전이 적용되어 있다. 제어의 역전이라는 개념은 아래 파트에서 조금 더 자세히 말하니 일단 알아두자.
원래 ConnectionMaker의 구현클래스를 결정하고 객체를 만드는 제어권은 UserDAO에게 있었지만, 위의 코드로 개선한 결과 DaoFactory가 UserDAO의 Connection 객체를 결정한다.
따라서 UserDAO는 능동적으로 자신이 필요한 객체를 생성하고 사용하는 것이 아닌, 수동적으로 누군가에 의해 지정된 객체를 사용하도록 변화한 것이다.
이렇게 하면 자연스럽게 UserDAO에서 어떤 Connection을 사용해야하는지에 대한 관심을 분리할 수 있고, 확장에 유연함을 가져갈 수 있게 된 것이다.
1.4.3. 제어권의 이전을 통한 제어관계 역전
제어의 역전이 무슨소리인고…?
간단하게 말하면 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다. (너무 간단해서 그런가 뭔 말인지 모르겠다.)
덧붙이자면, 일반적인 프로그램의 흐름은 다음과 같다.
- main()메서드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 객체를 결정한다.
- 사용할 객체를 생성한다.
- 생성된 객체 내에 있는 메서드를 호출한다.
- 그 객체 메서드 내에서 1~3 과정을 능동적으로 수행한다.
초기의 UserDAO 코드를 살펴보면 테스트용 main()메서드는 UserDAO 객체를 직접 생성하고, 만들어진 UserDAO 객체의 메서드를 사용한다.
UserDAO 또한 자신이 사용할 ConnectionMaker의 구현체를 자신이 결정하고, 그 오브젝트가 필요한 시점에서 생성하며 각 메서드에서 이를 사용한다.
제어의 역전은 객체가 자신이 사용할 객체를 스스로 선택하지 않는다. 당연하게도 생성하지도 않는다.
또한 자신이 어디서 사용되고, 어떻게 만들어지는지 신경쓰지 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하는 것이다.
프로그램의 시작을 담당하는 main()메서드와 같이 특별한 엔트리 포인트를 제외하면 모든 객체는 이렇게 위임받은 제어 권한을 가진 특수 객체(팩토리)에 의해 결정되고 만들어진다.
여기서 좀 심오한 개념도 적용된다.
프레임워크는 제어의 역전 개념이 적용된 대표적인 기술이라고 말할 수 있는 것이다.
프레임워크는 라이브러리의 다른 이름이 아니다. 이를 이해하려면 라이브러리와 프레임워크가 어떻게 다른지 알아야한다.
라이브러리 vs 프레임워크
라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다.
–> Collections 라이브러리 사용할 때를 생각해보자. 컬렉션 라이브러리의 메서드가 직접 메서드 매개변수를 요구하고, 처리 결과를 반한다.
프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용된다.
–> 프레임워크 위에 개발한 클래스를 등록하고, 프레임워크가 흐름을 주도하는 도중, 개발자가 만들어 놓은 객체들을 사용하도록 하는 것이다.
프레임워크라고 불릴 수 있는 기술에서 가장 중요한 점은 제어의 역전 개념이 반드시 적용되어 있어야 한다는 것이다.
위에서 만들어본 DaoFactory는 IoC 컨테이너 혹은 IoC 프레임워크 까지로도 부를 수 있는 자격이 있는 내용이다.
IoC를 적용함으로써 설계가 깔끔해지고, 유연성이 증가하고, 확장성이 좋아지기 때문에 이런 장점이 필요할 땐 꼭 적용해보는 것이 좋다.
1.5. 스프링의 IoC
지금까지는 순수 자바로 IoC를 구현했다. 이 내용을 스프링에서 사용 가능하도록 변경해보자.
스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체를 빈(Bean) 이라고 부른다.
Java Bean 혹은 Enterprise Java Bean(EJB)에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 의미한다.
스프링에서는 Bean의 생성과 관계설정 등의 제어를 담당하는 IoC 객체를, Bean Factory라고 한다.
또한, 보통 Bean Factory보다는 이를 좀 더 확장한 Application Context라고 부른다.
Application Context는 IoC방식을 따라 만들어진 일종의 Bean Factory라고 생각하면 된다.
조금 더 구체적으로 용어를 정리하자면, Bean Factory는 Bean을 생성하고 관계를 설정하는 IoC의 기본 기능에 초점을 맞춘 것
Application Context라고 말할 때는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미가 더 부각된 것이다.
왜냐하면 Application Context는, 별도의 정보를 참고해서 Bean의 생성 + 관계설정 등의 제어 작업을 총괄하는데, Application Context는 앞서 언급한 별도의 정보들을 가지고 있지 않기 때문이다. (별도의 정보는 Bean 관계와 생성등의 설정정보를 이야기하고 어딘가에 따로 존재한다.)
DaoFactory를 사용하는 Application Context 만들기
DaoFactory를 Spring의 Bean Factory가 사용할 수 있는 본격적인 설정정보를 만들어야한다.
- Spring이 Bean Factory를 위한 객체 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration이라는 어노테이션을 추가한다.
- 객체를 만들어주는 메서드에는 @Bean이라는 어노테이션을 붙여준다.
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class DaoFactory {
@Bean
public UserDAO userDAO() {
return new UserDAO(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
그 다음, DaoFactory를 설정정보로써 사용하는 Application Context를 만든다.
Application Context는 ApplicationContext 타입의 객체로, ApplicatonContext를 구현한 클래스가 여러 가지가 있는데 @Configuration이 붙은 Java 코드를 설정정보로써 사용하기 위해선 AnnotationConfigApplicationContext를 사용하면 된다.
이때, ApplicationContext를 만들 때, 생성자 파라미터로 DaoFactory 클래스를 넣어 주면 된다.
1
2
3
4
5
6
public class UserDAOTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDAO dao = context.getBean("userDAO", UserDAO.class);
}
}
여기서 getBean() 메서드는 ApplicaitonContext가 관리하는 오브젝트를 요청하는 메서드이다.
getBean()의 파라미터인 “userDAO”는 ApplicationContext에 등록된 Bean의 이름이며, 이전 코드에서 @Bean이라는 어노테이션을 userDAO라는 메서드에 붙였기 때문에 이 메서드의 이름이 Bean의 이름이 되는 것이다.
이렇게 굳이 Bean의 이름을 지정하는 이유는, 같은 타입에 대하여 여러가지 메서드의 Bean을 등록할 수 있게 하기 위함이다.
getBean() 메서드는 기본적으로 Object 타입으로 반환하게 되어 있기 때문에 매번 캐스팅을 해줘야하는 불편함이 있지만
Java 5 이상의 Generic을 활용하여 getBean()메서드에 반환 타입을 지정하면(위의 코드의 DaoFactory.class), 캐스팅을 하지 않아도 된다.
지금까지의 내용으로는 Spring에서의 IoC를 적용한 것이나 Java에서의 IoC를 적용한 것이나 차이가 없어보이지만 분명한 차이가 있기 때문에 그에 대한 내용은 조금씩 살펴본다.
1.5.2. Application Context의 동작방식
일단 Spring에서 부르는 용어를 좀 정리하자면,
Application Context == IoC Container == DI Container == Spring Container 라고 생각하면 된다.
사실 IoC가 DI 보다 좀 더 큰 개념이긴하다. IoC 컨테이너를 활용하여 DI를 수행하는 것이기 때문이다.
또한, DI를 사용하지 않고 IoC를 구현하면 위에서 보았던 예제처럼 단순 템플릿 메서드 패턴에 속한다.
다시 본론으로 돌아와서, ApplicationContext는 ApplicationContext 인퍼테이스를 구현하는데, ApplicationContext는 Bean Factory가 구현하는 BeanFactory 인터페이스를 상속했으므로
ApplicaitonContext는 일종의 Bean Factory라고 생각할 수 있다.
또한 ApplicationContext가 스프링의 코어 객체인 이유로 ApplicationContext를 Spring이라고 부르는 사람이 있을 정도이다..
DaoFactory가 UserDAO 뿐 만아니라, DAO객체 생성, DB 생성 객체와 관계를 맺는 제한적인 역할을 하는 반면에
ApplicaitonContext는 Application 전반적으로 모든 객체의 생성,관계를 IoC를 적용하여 관리한다.
하지만, ApplicationContext에는 DaoFactory와 달리 직접 객체를 생성하고, 관계를 맺어주는 코드는 없고 별도의 설정 정보를 통해 얻는다고 말했다.
그 내용이 바로 @Configuration 어노테이션을 활용했을 때를 말하는 것이다. 이 어노테이션은 ApplicationContext가 활용하는 DaoFactory의 설정정보를 얻을 수 있게 하는 역할을 하는 것이다.
내부적으로는 ApplicationContext가 DaoFactory의 userDao() 메서드를 호출해서 객체를 가지고 있고, Client가 getBean()을 요청하면 그때 가지고 있는 객체를 전달하는 흐름인 것이다.
1.5.3. 스프링 IoC 용어 정리
Bean
Bean 혹은 Bean Object는 Spring이 IoC방식으로 관리하는 객체라는 뜻이다. Managed Object라고 부르기도 한다.
주의해야할 점은, Spring을 사용하는 Application에서 만들어지는 모든 객체가 전부 Bean은 아니라는 것이다.
Spring이 직접 그 생성과 제어를 담당하는 Object만을 Bean이라고 부른다.(스포하자면 @Component 어노테이션이 붙은 객체들이다.)
Bean Factory (Interface)
Spring의 IoC를 담당하는 핵심 컨테이너를 가리킨다. Bean 등록/생성/조회/반환 및 부가적인 Bean관리 기능을 수행한다.
보통은 이 BeanFactory를 곧바로 사용하진 않는다. (실제로 사용하기엔 조금 제한적인 부분이 있기 때문이다.)
그 대신 ApplicationContext를 사용하는데, 이는 앞선 단락에서 언급했듯이 BeanFactory를 상속하여 부가적인 기능을 구현한 구현체의 한 종류이다.
가장 기본적인 메서드는 getBean()과 같은 메서드가 있다.
ApplicationContext (Interface)
Bean Factory를 확장한 IoC 컨테이너이다. Bean 등록/관리 하는 기본적인 기능은 Bean Factory와 같지만 Spring이 제공하는 각종 부가 서비스를 추가로 제공한다.
Bean Factory라고 부를 때는 주로 Bean 생성/제어 관점에서 이야기 하는 것이고
ApplicationContext 라고 부를 때는 Spring 이 제공하는 부가기능을 모두 포함하기 때문에 Spring 환경에서는 이 용어를 더 자주 접하게 될 것이다.
Configuration Metadata
Spring 설정정보는 ApplicationContext, Bean Factory가 IoC를 적용하기 위해 사용하는 메타정보를 의미한다.
이 용어는 Application의 형상정보 라고도 하긴한다.
Container / IoC Container
IoC 방식으로 Bean을 관리한다는 의미에서 ApplicationContext, BeanFactory를 IoC 컨테이너라고도 한다.
“스프링에 빈을 등록하고 ~~~ 해보자” –> ApplicationContext(Spring Container)에 Bean을 등록 어쩌구 저쩌구한다는 것이다.
1.6. 싱글톤 레지스트리와 오브젝트 스코프
오브젝트의 동일성과 동등성
두 개의 오브젝트가 완전히 같은 (동일한)오브젝트이다. (동일성(Identity) 비교) 동일한 정보를 담고 있는 오브젝트이다. (동등성(Equality) 비교) 동일성 비교는 == 연산자로 비교한다.
동등성은 equals 메서드로 비교한다.
두 개의 오브젝트가 동일하다면 (동일성 비교에서 같다는 결과가 도출되면) 사실은 하나의 오브젝트만 존재하는 것이며
두 개의 오브젝트 레퍼런스 변수를 갖고 있을 뿐이다.
자바 클래스를 만들 때 equals 메서드를 따로 구현하지 않았다면, 최상위 클래스인 Object 클래스에 구현되어 있는 equals 메서드가 사용된다.
이펙티브 자바 Item 10 에서 자세한 설명이 나온다.
동일성/동등성 비교
1
2
3
4
5
String str1 = new String("ABC");
String str2 = new String("ABC");
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
쉽게 생각하면 다음과 같다.
동일성은 타입/자료형 및 그 내용까지 비교하는 것
동등성은 타입/자료형 을 비교하는 것
여기서 알아보고자 하는 것은 DaoFactory와 userDAO()를 여러 번 출력했을 때 동일한 오브젝트가 돌아오는 것에 대한 여부이다.
1
2
3
4
5
6
DaoFactory factory = new DaoFactory();
UserDAO dao1 = new UserDAO();
UserDAO dao2 = new UserDAO();
System.out.println(dao1);
System.out.println(dao2);
위 코드의 출력결과는 당연하게도 다른 객체를 가리키는 값이 나온다.
하지만 Spring이 첨가된다면 상반된 결과가 나오게 되는데 그 코드를 살펴보자.
1
2
3
4
5
6
7
8
9
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDAO dao3 = context.getBean("userDAO", UserDAO.class);
UserDAO dao4 = context.getBean("userDAO", UserDAO.class);
System.out.println(dao3);
System.out.println(dao4);
// 동일성 비교
System.out.println(dao3 == dao4);
위와 같은 코드로 실행하면 같은 객체를 가리키는 값과, TRUE 라는 결과가 떨어진다.
왜 그런것일까?
1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
ApplicationContext는 IoC 컨테이너다. 그러면서 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다.
스프링에서는 별도의 설정을 하지 않으면 내부에서 생성하는 Bean 객체를 모두 싱글톤으로 만든다.
여기서 다루는 싱글톤은, 디자인패턴에서 등장하는 싱글톤과 비슷한개념일 뿐 구현 방법은 다르다.
그러면 스프링에서 Bean을 싱글톤으로 만드는 이유는 뭘까?
스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.
조금 더 풀어 말하자면, 요청이 올 때마다 각 로직을 담당하는 50개의 오브젝트를 새로 만들어서 처리한다고 상상해보는 것이다.
초당 100개의 요청이 들어온다고 했을 때, 1초에 5000개의 객체를 생성하고 해제해야한다. 부하가 심할 것이다.
또한 스프링 이전에 사용하던 서블릿에서도 멀티스레드 환경에서 싱글톤으로 동작하여 서블릿 클래스 당 하나의 객체만 만든 후, 여러 쓰레드에서 이를 공유하여 처리한다.
싱글톤은 조금 껄끄러운 점이 있다.
- private 생성자를 갖고 있기 때문에 상속할 수 없다.
- 싱글톤은 테스트하기가 힘들다.
- 서버환경에서는 싱글톤이 하나만 만들어 지는 것을 보장할 수 없다.
- 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.