1.1 DAO
- DAO(Data Access Object): DB를 사용해서 데이터를 조회하거나 조작하는 기능만 하도록 만드는 오브젝트을 말한다.
1.1.1 User
<hide/>
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;
}
}
cf) 자바빈(JavaBean)
- 자바빈(JavaBean): 원래 비주얼 툴에서 조작 가능한 컴포넌트를 말한다.
- 자바의 주력 개발 플랫폼이 웹 기반의 엔터프라이즈 방식으로 바뀌면서 비주얼 컴포넌트로서 자바빈의 인기는 떨어짐
- 하지만, 자바빈의 몇 가지 코딩 관례는 JSP 빈, EJB같은 표준 기술과 자바 빈 스타일의 오브젝트를 사용하는 오픈 소스 기술을 통해 계속 이어져왔다.
- 지금은 "자바빈"이라고 하면 비주얼 컴포넌트라기 보다는 다음 두 관례를 따르는 Object를 가리킨다. "빈"이라고도 한다.
- 1) 디폴트 생성자: 자바빈은 기본 생성자(@NoArgsconstructor)가 필수
- 2) 프로퍼티: 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. (멤버 변수, 필드)
1.1.2 UserDao
<hide/>
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
// db connection
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobispring", "root", "4250");
// add user
PreparedStatement ps = connection.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();
// connection close
ps.close();
connection.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
// db connection
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobispring", "root", "4250");
// get user
PreparedStatement ps = connection.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"));
// connection close
rs.close();
ps.close();
connection.close();
return user;
}
}
- getConnection() 안에는 로컬에서 설정한 MySQL DataBase 의 id와 비번을 입력해준다.
JDBC를 이용하는 작업의 순서
- DB 연결을 위한 Connection 가져온다.
- SQL을 담은 Statement(또는 PreparedStatement)을 만든다.
- Statement 실행
- SQL 쿼리의 실행 결과를 ResultSet에 담아서 Object에 옮겨준다.
- 작업 중 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후, 반드시 닫아준다. (닫아 주지 않으면 문제가 생긴다.)
- JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나 throws를 선언해서 메서드 밖으로 던진다.
1.1.3 main() 함수를 통한 DAO 테스트 코드
class.forname( com.mysql.cj.jdbc.driver ) error
- 오류: ClassNotFoundException 이 발생한다.
- 아래의 main 함수를 실행하면 add()의 첫 줄 Class.forname() 부터 오류가 발생한다.
- 원인: 해당 프로젝트 폴더에 mysql 라이브러리를 다운 받지 않아서 Class를 찾을 수 없다는 오류가 발생한다.
- 해결: 다음 jar 파일을 다운 받아서 프로젝트를 진행하는 폴더 안에 넣어준다.
- 다음과 같이 외부 라이브러리에 해당 라이브러리가 추가돼있어야한다.
Ex)
- 다음과 같이 MySQL 워크벤치로 데이터베이스와 table을 생성해준다.
- main 함수
<hide/>
UserDao userDao = new UserDao();
User user = new User();
user.setId("ran");
user.setName("고라니");
user.setPassword("0987");
userDao.add(user);
System.out.println(user.getId() + " 등록 성공");
User getUser = userDao.get(user.getId());
System.out.println(getUser.getName());
System.out.println(getUser.getPassword());
System.out.println(getUser.getId() + " 조회 성공");
Note) 실행 결과
1.2 DAO의 분리
1.2.1 관심사의 분리 (Seperation of Concerns)
- 객체 지향은 실세계에 가장 가깝게 모델링할 수 있기 때문에 의미가 있다.
- 개발자가 객체를 설계할 때 가장 중요한 건 미래의 변화를 어떻게 대비할 것인가이다.
- 미래를 준비하는 건 변화에 어떻게 대비할 것인가에 대한 문제이다. 변화의 폭을 줄이는 것이 가장 좋은 대책이다.
- 분리와 확장을 고려해서 설계해야 변경 작업을 최소화할 수 있다.
- 관심사의 분리: 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이도록 하고 관심이 다른 것은 떨어뜨려서 서로 영향을 주지 않도록 분리하는 것을 말한다.
1.2.2 커넥션 만들기의 추출
UserDao의 관심 사항
- DB와 연결을 위한 커넥션을 어떻게 가져올 것인가?
- DB, 드라이버 종류, 커넥션 생성 방법, 등...
- 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행한다.
- 작업이 끝나면 사용한 리소스(Statement, Connection)를 닫아줘서 공유 리소스를 시스템에 돌려준다.
- 실무에서 close() 해주지 않으면 큰 문제가 생길 수 있다.
UserDao의 문제점
- 예외 처리가 없다.
- DB 연결을 위해 Connection을 가져오는 부분이 get(). add() 메서드에 중복으로 들어가있다. (가장 큰 문제)
- 하나의 관심사가 방만하게 중복되며 흩어져서 다른 관심 대상과 얽혀 있으면 코드를 변경할 때 큰 문제가 생긴다.
UserDao 개선 방법
- 1) 중복 코드의 메서드 추출: 중복 코드를 분리한다. DB 연결 코드를 독립적인 메서드로 따로 뺀다.
- 아래 코드 참고
- 리팩토링에서 메서드 추출 기법이라고 한다.
- 2) 변경 사항에 대한 검증: 리팩토링과 테스트
- 1) 메서드 추출: 다음과 같이 getConnection() 을 따로 빼준다.
- 만약 DAO 클래스 안에 지금 2개 있는 것과 다르게 메서드가 많은 경우에 수정 사항이 생긴 다면 getConnection() 안에만 수정하면 되기 때문에 훨씬 간단하고 효율적이다.
<hide/>
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobispring", "root", "4250");
return connection;
}
- 2) 변경 사항에 대한 검증
- 기존에 Dao 잘 동작하는 건 테스트해봤는데 코드를 수정한 다음에도 기능에 문제가 없는지는 보장할 수 없다.
- 확인하려면 ? main() 한 번 더 실행하면 된다.
- 그런데 다시 실행하면 아래와 같은 SQLIntegrityConstraintViolationException 예외가 터진다.
- 테이블의 PK로 설정한 id 필드명이 중복되기 때문이다.
cf) 리팩토링(Refactoring)
- 기존의 코드를 외부의 동작 방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업이나 기술을 말한다.
- 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고 변화에 효율적으로 대응 가능하다.
- 코드의 품질 올라가고 유지 보수가 용이하고 견고하면서 유연한 제품을 개발 가능하다.
1.2.3 DB 커넥션 만들기의 독립
- 앞에서 리팩토링했으나 더욱 품질 좋은 코드를 만드려면 어떻게 해야할까?
- 만약 여러 고객사의 DB 종류가 다를 수도 있다.
- 그리고 고객사에서 독자적으로 만든 방법을 적용해서 DB 커넥션을 가져오고 싶은 경우도 있을 것이다.
- 이런 경우라면 고객사에 UserDao의 소스코드를 아예 제공해주고 변경이 필요하면 getConnection()을 사용하라고 할 수도 있다.
- 그런데 비밀 기술이 적용된 Dao 클래스라면 ?고객에게 소스 코드를 직접 공개하지 않고 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶은 상황이라면?
- 이런 경우에 고객 스스로 원하는 DB 커넥션 생성 방식을 적용하면서 UesrDao 를 사용할 수 있을까?
상속을 통한 확장
- UserDao 에서 메서드의 구현 코드를 제거해서 getConnection() 를 추상 메서드로 만든다. (즉, UserDao 는 추상 클래스)
- 그러면 get(), add() 메서드에서 getConnection()을 호출하는 코드는 그대로 유지 가능하다.
- 그러고 나서 UserDao를 상속해서 각각 NaverUserDao , DaumUserDao 라는 자식 클래스를 만든다.
- 그러면 getConnection() 를 원하는 대로 확장한 다음에 UserDao의 기능과 함께 쓸 수 있다.
Ex)
- UserDao 클래스
<hide/>
public abstract class UserDao {
private abstract Connection getConnection() throws ClassNotFoundException, SQLException ;
}
}
- NaverUserDao
<hide/>
public class NaverUserDao extends UserDao {
private Connection getConnection() throws ClassNotFoundException, SQLException {
// 고객사 naver의 DB connection 생성 코드
return null;
}
}
Note)
- UserDao 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 새롭게 정의할 클래스를 만들 수 있다.
- 새로운 DB 연결을 적용해야한다면 UserDao 상속을 통해 확장하면 된다.
디자인 패턴(Design Pattern)
- 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션을 디자인 패턴이라고 한다.
- 모든 패턴은 의도와 해결책이 있다.
- 주로 객체 지향적 설계 원칙을 이용해서 문제를 해결한다.
- 여러 디자인 패턴의 설계 구조가 비슷한 이유가 여기에 있다.
- 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 대부분 두 가지로 정리 되기 때문이다.
- 1. 클래스 상속
- 2. Object 합성
- 디자인 패턴은 목적과 의도가 가장 중요
- 디자인 패턴은 설계 전략이자 편리한 커뮤니케이션 수단이다.
템플릿 메소드 패턴(Template Method Pattern)
- 앞서 본 예제와 같이 상위 클래스에 기본적인 로직의 흐름(커넥션 가져오기, sql 실행, 반환, ...)을 만들고 그 기능의 일부를 추상 메서드 또는 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤 자식 클래스에서 필요에 맞게 따라서 구현해서 사용하도록 하는 방법을 말한다.
- 상속을 통해 부모 클래스의 기능을 확장할 때 사용하는 가장 대표적 방법이다.
- 변하지 않은 기능은 부모 클래스에, 자주 변경되거나 확장할 기능은 자식 클래스에 만들도록 한다.
- 부모 클래스에 디폴트 기능을 정의해두고 이를 활용해서 코드의 기본 알고리즘을 담고 있는 템플릿 메서드를 만든다.
- 템플릿 메서드 안에는 자식 클래스에서 오버라이드할 여러 메서드를 모아둔다.
- 자식 클래스에서 선택적으로 오버라이드할 수 있도록 만든 메서드를 훅(hook) 메서드라고 한다.
- 추상 메서드는 자식 클래스에서 반드시 구현해야하지만 훅 메서드는 선택사항이다.
- 훅 메서드는 추상 메서드(구현부 없음)와 다르게 구현부 안이 비어 있다. => "{ }"
팩토리 메소드 패턴(Factory Method Pattern)
- 자식 클래스인 NaverUserDao의 getConnection() 메서드는 Connection 클래스의 Object를 어떻게 생성할 것인지를 결정하는 방법이다.
- 이렇게 자식 클래스에서 구체적인 오브젝트 생성 방법을 결정하는 것을 팩토리 메서드 패턴이라고 한다.
- 주로 인터페이스 타입으로 Object를 반환하므로 부모 클래스는 자식 클래스의 해당 메서드의 반환형을 알 수 없다.
- cf) 자바에서 종종 Object 생성하는 기능을 가진 메서드를 팩토리 메서드라고도 한다. 팩토리 메서드 패턴과 구분해야한다.
- 템플릿 메소드 패턴과 팩토리 메서드 패턴의 공통점과 차이점
- 차이점
- 템플릿 메소드 패턴은 오버라이드한 "메서드의 새로운 구현"에 초점을 맞춘다면 팩토리 메서드 패턴은 메서드를 반환해서 얻은 객체, 즉, "메서드의 반환형(return type)"에 초점을 맞춘다고 생각했다.
- 공통점
- 상속을 통해 기능을 확장한다.
- 차이점
상속의 문제점
- 만약 UserDao가 다른 목적을 위해 사용하고 있다면?
- 자바는 다중 상속을 허용하지 않는다.
- 1) 커넥션 객체를 가져오는 방법을 분리하기 위해 상속 구조로 만들어버리면 후에 다른 목적으로 UserDao 상속을 적용하기 힘들다.
- 2) 상속을 통한 상하위 클래스의 관계는 아주 밀접하다.
- 자식 클래스는 부모 클래스의 기능을 직접 사용 가능하다. 즉, 부모 클래스 내용이 변경되면 이에 따라 자식 클래스도 수정하는 경우가 생길 수 있다.
1.3 DAO의 확장
- 모든 Object는 변한다.
- 관심사에 따라 Object를 분리하는데 각 Object는 독특한 변화의 특징이 있다.
- 앞에서 데이터 엑세스 로직을 어떻게 만들 것인지, DB 연결을 어떤 방법으로 할 것인가 두 가지의 관심을 살펴봤는데 이 두 가지 관심은 변화의 성격이 다르다.
- 어떤 관심사가 바뀔 때 변화가 일어나는 것이다.
- 추상 클래스를 만들어서 상속한 자식 클래스에서 각기 다르게 쓸 수 있도록 만든 이유가 바로 여기 있다.
- 변화의 성격이 다른것을 분리해서 서로 영향을 주지 않으면서 각각 필요한 시점에 독립적으로 변경할 수 있도록 하기 위함이다.
- 그러나 상속이라는 방법에 불편함이 있다.
1.3.1 클래스의 분리
- 앞서 살펴본 두 개의 관심사를 본격적으로 독립시키면서 동시에 쉽게 확장하려면 어떻게 해야할까?
- 상속도 없애고 완전히 독립적인 클래스로 만들어보려한다.
- 다음과 같이 SimpleConnectionMaker라는 클래스는 DB 연결 기능을 가지고 있다.
<hide/>
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/tobispring", "root", "4250");
return connection;
}
}
- UserDao
- 이렇게 바꿀 수 있다.
<hide/>
public class UserDao {
// private Connection getConnection() throws ClassNotFoundException, SQLException {
// Class.forName("com.mysql.cj.jdbc.Driver");
// Connection connection = simpleConnectionMaker.makeNewConnection();
//
// }
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
// db connection
Connection connection = simpleConnectionMaker.makeNewConnection();
;
// add user
PreparedStatement ps = connection.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();
// connection close
ps.close();
connection.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
// db connection
Connection connection = simpleConnectionMaker.makeNewConnection();
// get user
PreparedStatement ps = connection.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"));
// connection close
rs.close();
ps.close();
connection.close();
return user;
}
}
Note)
- 기능은 변함 없지만 내부 설계를 변경해서 개선시켰다.
- 성격이 다른 코드를 완전히 분리했다.
- 그런데 여기서, 네이버와 다음에서 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 게 불가능해졌다.
- UserDao 의 코드가 SimpleConnectionMaker 에 종속되어있기 때문에 상속을 했을 때처럼 UserDao 코드의 수정없이 DB 커넥션 생성 기능을 변경할 방법이 없다.
- 다른 방식으로 DB 커넥션 을 제공하는 클래스를 사용하기 위해서는 다음 코드를 변경해야한다.
simpleConnectionMaker = new SimpleConnectionMaker();
- 그럼 이제 UserDao의 소스 코드를 함께 제공하지 않고는 DB 연결 방법을 바꿀 수 없다는 처음 문제로 다시 되돌아온다.
- 클래스를 분리한 후에도 상속을 이용할 때 처럼 자유로운 확장이 가능하도록 하려면? 두 가지 문제를 해결해야한다.
- 1) SimpleConnectionMaker 안에 메서드가 문제다.
- makeNewConnection()으로 DB 커넥션을 가져오게 했는데
- 만약에 고객사 Daum에서 만든 커넥션 제공 클래스는 openConnection()이라는 이름을 사용했다면 ??
- add(), get() 메서드의 커넥션을 가져 오는 코드를 다음과 같이 일일이 변경해야한다. (makeNewConnection() => openConnection())
- 개수가 많아지면 아주 번거로워진다.
- 2) DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야한다는 점
- UserDao에 SimpleConnectionMaker 라는 클래스 타입의 인스턴스 변수까지 정의해놓고 있으니 고객사 네이버에서 어쩔 수 없이 UserDao 자체를 다시 수정해야한다.
- 근본적 원인: UserDao가 바뀔 수 있는 정보인 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다.
- 따라서 UserDao 는 DB 커넥션을 가져오는 구체적 방법에 종속되어 버린다.
- 고객사가 DB 커넥션 정보를 가져오는 방법을 자유롭게 확장하기 어려워졌다.
- 1) SimpleConnectionMaker 안에 메서드가 문제다.
1.3.2 인터페이스 도입
- 클래스를 분리하면서 위 문제를 해결하려면?
- 두 클래스가 긴밀하게 연결되어 있지 않도록 중간에 느슨한 연결고리를 만들어주는 것이다.
- 추상화는 어떤 것들의 공통적인 성격을 뽑아 이를 분리해내는 작업을 말한다.
- 인터페이스가 바로 추상화를 위한 가장 좋은 도구이다.
- 인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다.
- ConnectionMaker 인터페이스
- makeConnection(): DB 커넥션을 가져온다.
- 납품할 때 UserDao 클래스와 함께 ConnectionMaker 인터페이스도 전달한다.
<hide/>
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
- DConnectionMaker
- Daum 기업의 개발자라면 다음 클래스를 만들고 자신들의 DB 연결을 통해 가져오도록 메서드를 작성하면 된다.
<hide/>
public class DConnectionMaker implements ConnectionMaker{
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
// 커넥션 생성 코드
return null;
}
}
- 인터페이스를 사용하도록 개선한 UserDao
- 인터페이스만 사용하도록 구성했다.
- 따라서, 여러 고객사에서 DB 접속용 클래스를 다시 만든다고 해도 UserDao의 코드를 고칠 일은 없어진다.
<hide/>
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
// db connection
Connection connection = connectionMaker.makeConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
// db connection
Connection connection = connectionMaker.makeConnection();
...
}
Note) 실행 결과
- 그러나 아직도 new DConnectionMaker() 라는 이름이 보인다.
- 초기에 한 번 어떤 클래스의 오브젝트를 사용할 지 결정하는 생성자 코드는 제거되지 않고 남아있다.
- .. 다시 또 원점...
- 결론: 필요할 때마다 UserDao의 생성자 메서드를 직접 수정하라고 하지 않고는 고객에게 자유로운 DB 커넥션 확장 기능을 가진 UserDao 를 제공할 수가 없다는 뜻이다.
1.3.3 관계 설정 책임의 분리
- 앞에서 UserDao 와 ConnectionMaker 라는 두 개의 관심을 인터페이스를 이용해서 분리했는데 여전히 UserDao는 여전히 인터페이스뿐만 아니라 구체적 클래스까지 알아야한다.
- 왜 이런 문제가 생길까?
- UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 있다.
- 이 코드는 기존 Dao의 관심사의 JDBC API와 User 오브젝트를 사용해서 DB에 정보를 넣고 빼는 게 아니고 ConnectionMaker 인터페이스로 대표되는 커넥션을 어떻게 가져올 것인가에 대한 관심사도 아니다.
- new DConnectionMaker() 는 짧고 간단하지만 충분히 독립적인 관심사를 담고 있다
- UserDao 가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다.
- Dao와 Dao가 사용할 ConnectionMaker 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.
- 이 관심사를 담은 코드를 Dao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장 가능한 클래스가 될 수 없다.
- 오브젝트 사이의 관계 만들기
- 일단 만들어진 오브젝트가 있어야한다.
- 생성자 호출 또는 외부에서 만들어준 것을 가져오는 방법도 있다.
- 생성자 호출: connection = new DConnectionMaker();
1.3.4 원칙과 패턴
개방 폐쇄 원칙(Open Closed Principle)
- 클래스나 모듈은 확장에 열려 있어야하고 변경에는 닫혀 있어야한다.
- ex) 앞에서 살펴본 UserDap는 DB 연결 방법이라는 기능을 확장하는 데는 열려있다.
- 맨 앞에서 살펴봤던 초난감 DAO는 개방 폐쇄 원칙을 잘 따르지 못한 설계라고 할 수 있다.
높은 응집도와 낮은 결합도
- 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 고전적 원리로 설명가능하다.
- 높은 응집도
- 하나의 모듈, 클래스가 하나의 책임이나 관심사에만 집중돼있다는 뜻이다. 클래스 레벨, 패키지, 컴포넌트, 모듈에 이르기까지 그 대상의 크기가 달라도 동일한 원리로 적용된다.
- 응집도가 높으면 변화가 일어날 때, 해당 모듈에서 변하는 부분이 크다는 뜻이다.
- 낮은 결합도
- 결합도: 하나의 오브젝트가 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도를 말한다.
- 즉, 하나의 변경이 발생할 때, 다른 모듈이나 객체로 변경에 대한 요구가 전파되지 않는 상태를 말한다.
- 느슨하게 연결된 형태를 유지해야한다.
- 느슨한 연결은 관계를 유지하는데 꼭 필요한 방법만 간접적인 형태로 제공하고 나머지는 독립적이고 서로 알 필요도 없게 만든다.
- 결합도가 낮아지면 변화에 대응하는 속도가 빨라지고 구성이 깔끔해진다.
- 높은 응집도보다 더 민감한 원칙이다.
전략 패턴(Strategy Pattern)
- 전략 패턴은 자신의 기능 맥락(context)에서 필요에 따라 변경이 필요한 알고리즘 인터페이스를 통해 통째로 외부로 분리시키고 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라서 바꿔 사용할 수 있게 하는 디자인 패턴이다.
- 여기에서 쓰는 알고리즘은 독립적으로 책임으로 분리 가능한 기능을 말한다.
- 이를 대체 가능한 전략이라고 보므로 "전략 패턴"이라는 이름이 붙었다.
- 디자인 패턴의 꽃이라고 할만큼 자주 쓰인다.
- 앞에서 만들었던 UserDao는 전략 패턴의 컨텍스트에 해당한다. UserDao라는 컨텍스트는 자신의 기능을 수행하는데 필요한 기능 중에서 변경 가능한 DB 연결 방식이라는 알고리즘을 ConnectionMaker라는 인터페이스로 정의하고 이를 구현한 클래스(전략)을 바꿔가면서 사용할 수 있도록 분리한다.
1.4 제어의 역전(IoC, Inversion Of Control)
1.4.1 오브젝트 팩토리
- 팩토리(factory)란?
- 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 역할을 하는 오브젝트를 보통 팩토리라고 한다.
- 오브젝트를 생성하는 쪽과 사용하는 쪽의 역할과 책임을 분리하기 위한 목적으로 사용한다.
<hide/>
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
- Userao의 생성 책임을 맡은 팩토리 클래스
- DaoFactory: 팩토리 역할을 맡는 클래스이다. UserDaoTest에 담겨 있던 UserDao, ConnectionMaker 관련 생성 작업을 DaoFactory로 옮기고 UserDaoTest에서는 DaoFactory에 요청해서 미리 만들어진 UserDao 오브젝트를 가져와서 사용하게 만든다.
- 팩토리 메서드는 UserDao타입의 오브젝트를 어떻게 만들고 준비시킬지 결정한다.
<hide/>
public class UserDaoTest {
public static void main(String[] args) {
UserDao userDao = new DaoFactory().userDao();
}
}
- 팩토리를 사용하도록 수정된 UserDaoTest
- UserDaoTest는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지 신경쓰지 않고 팩토리로부터 UserDao오브젝트를 받아다가 자신의 관심사인 테스트만 하도록 활용된다.
설계도로서의 팩토리
- UserDao, ConnectionMaker: 애플리케이션의 핵심적인 데이터 로직과 기술 로직을 담당한단. 실질적인 로직을 담당하는 컴포넌트
- DaoFactory: 이런 애플리케이션의 오브젝트를 구성하고 그 관계를 정의하는 책임을 맡고 있다. 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할이다.
1.4.2 오브젝트 팩토리의 활용
- DaoFactory에 UesrDao 가 아닌 DAO의 생성 기능을 넣으면 어떻게 될까?
- AccountDao, MessageDao 등을 만든다고 가정하자.
- 이 경우에 UserDao를 생성하는 userDao()를 복사해서 accountDao(), messgeDao() 메서드로 만든다면 새로운 문제가 발생한다.
- ConnectionMaker 구현 클래스의 오브젝트를 생성하는 코드가 메서드마다 반복되는 것이다.
- 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능이 중복돼서 나타나는 것이다.
1.4.3 제어권의 이전을 통한 제어관계 역전
- 제어의 역전이란 쉽게 말해 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다.
- 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선책하지 않는다.
- 모든 권한을 다른 대상에게 위임하기 때문이다. 모든 것을 사용하는 쪽에서 제어하는 구조이다.
- main() 과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.
- 일반적인 자바 프로그램은 main() 메서드에서 시작해서 개발자가 미리 정한 순서를 따라서 오브젝트가 생성, 실행된다.
- 그런데 서블릿을 개발해서 서버에 배포할 수는 있지만 그 실행을 개발자가 제어할 수는 없다.
- 서블릿에 대한 제어 권한을 가지고 있는 컨테이너가 적절한 시점에 오브젝트를 만들고 그 안의 메서드를 호출한다.
- 스프링은 IoC를 모든 기능의 기초가 되는 기반 기술로 삼고 있다.
1.5 스프링의 IoC
1.5.1. 오브젝트 팩토리를 이용한 스프링 IoC
애플리케이션 컨텍스트와 설정 정보
- 빈(bean): 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트이다.
- 자바빈 또는 엔터프라이즈 자바빈(EJB)에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말한다.
- 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계 설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트이다.
- 빈 팩토리(bean factory): 빈의 생성과 관계 설정같은 제어를 담당하는 IoC 오브젝트를 말한다.
- 애플리케이션 컨텍스트(application context): 빈 팩토리를 확장한 개념이다. IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 볼 수 있다. (동일하다고 생각하자. )
- 빈 팩토리: 빈 생성, 관계 설정하는 IoC 기본 기능에 초점
- 애플리케이션 컨텍스트: 모든 구성 요소의 제어 작업을 담당하는 IoC 엔진의 의미가 부각
DaoFactory를 사용하는 애플리케이션 컨텍스트
- @Configuration을 붙이면 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있다.
- 오브젝트 생성을 담당하는 IoC용 메서드에는 @Bean을 붙인다.
<hide/>
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao(new DConnectionMaker());
return userDao;
}
}
- 두 가지 애너테이션을 붙여서 스프링 프레임워크의 빈 팩토리 또는 애플리케이션 컨텍스트가 IoC 방식의 기능을 제공할 때 사용할 설정 정보가 된다.
Ex) ApplicationContext 적용하기
- ApplicationContext 를 구현한 클래스는 여러 가지가 있다.
- DaoFactory처럼 @Configuration가 붙은 코드를 설정 정보를 사용하려면 AnnotationConfigApplicationContext를 사용하면 된다.
- 생성자의 파라미터로 DaoFactory 클래스를 넣어준다.
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao = context.getBean("userDao", UserDao.class);
Note)
- getBean("빈 이름", 해당 메서드의 반환 형태.class): ApplicationContext 가 관리하는 오브젝트를 요청하는 메서드이다. getBean() 안에 들어가는 파라미터는 ApplicationContext 에 등록된 빈의 이름이다.
- 앞에서 DaoFactory 안에 선언한 메서드 userDao()는 메서드 이름이 곧 빈 이름이 된다.
- 빈 을 호출해서 그 결과인 객체를 가져온다고 생각하면 된다.
- cf) getBean() 의 두 번째 파라미터에 리턴 타입을 주면 지저분한 캐스팅 코드를 사용하지 않아도 된다.
1.5.2 애플리케이션 컨텍스트의 동작 방식
- 오브젝트 팩토리 이용 방식과 스프링의 애플리케이션 컨텍스트 사용 방식은 뭐가 다를까?
오브젝트 팩토리
- DaoFactory는 UserDao 같은 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계 맺어주는 제한적인 역할을한다.
애플리케이션 컨텍스트
- 애플리케이션 컨텍스트는 IoC 컨테이너라고도 한다. ( = 스프링 컨테이너 모두 같은 뜻이다. 또는 빈 팩토리 라고도 한다. )
- ApplicationContext는 빈 팩토리가 구현하는 BeanFactory 인터페이스를 상속했기 때문에 애플리케이션 컨텍스트는 일종의 빈 팩토리인 셈이다.
- (애플리케이션 컨텍스트를 스프링이라고 부르기도 한다. )
- 제한적인 역할을 하는 DaoFactory와 다르게 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계 설정을 담당한다.
- ApplicationContext 는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고 별도의 설정 정보를 통해서 생성 정보와 연관 관계 정보를 얻는다.
- 애플리케이션 컨텍스트는사용하면 범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해 사용한다.
애플리케이션 컨텍스트의 장점 (오브젝트 팩토리 보다 좋은 점)
- 클라이언트는 구체적인 클래스를 알 필요가 없다.
- 종합 IoC 서비스를 제공
- 빈을 검색하는 다양한 방법을 제공: getBean()
1.5.3 스프링 IoC의 용어 정리
Note
- 빈(Bean, 빈 오브젝트): 스프링이 IoC 방식으로 관리하는 오브젝트이다. 스프링이 직접 생성, 제어를 담당하는 오브젝트를 빈이라고 한다.
- 빈 팩토리(Bean Factory): IoC를 담당하는 핵심 컨테이너를 말한다.
- BeanFactory와 같이 붙여쓰면 빈 팩토리가 구현하고 있는 가장 기본적인 인터페이스 이름이 된다.
- 이 인터페이스 안에 getBean()이 정의되어 있다.
- 애플리케이션 컨텍스트(ApplicationContext): 빈 팩토리를 확장한 IoC 컨테이너이다.
- ApplicationContext는 BeanFactory를 상속한다.
- 빈 등록하고 관리하는 기능은 빈 팩토리와 동일하지만 애플리케이션 컨텍스트는 스프링이 제공하는 부가 서비스를 추가로 제공한다.
- 보통 빈 팩토리보다는 이를 확장한 애플리케이션 컨텍스트를 이용한다.
- 설정 정보 / 설정 메타 정보(configuration metadata)
- 설정 정보(configuration)는 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다.
- 컨테이너(Container) 또는 IoC 컨테이너
- IoC 방식으로 빈을 관리한다는 의미로 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너 라고 한다.
- 컨테이너라는 말은 애플리케이션 컨텍스트보다 추상적인 표현이다.
- 스프링 프레임워크
- IoC컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 "스프링 프레임워크"를 사용한다.
- 줄여서 스프링이라고도 한다.
1.6 싱글톤 레지스트리와 오브젝트 스코프
오브젝트의 동일성과 동등성 - Java
- 동일성(identity): 두 개의 오브젝트가 완전히 같다. '=='로 비교한다.
- 두 개의 오브젝트가 동일하다면? 사실 하나의 오브젝트만 존재하는 것이다. 두 개의 레퍼런스 변수를 갖고 있을 뿐이다.
- 동일하면 항상 동등하다. (반대는 항상 참이라고 볼 수는 없다. )
- 동등성(equality): 동일한 정보를 담고 있는 오브젝트, 'equals'로 비교한다.
- 두 개의 오브젝트가 동일하지 않지만 동등한 경우에는? 두 개의 각기 다른 오브젝트가 메모리 상에 존재하는데 오브젝트의 동등성 기준에 따라 두 오브젝트의 정보가 동등하다고 판단하는 것이다.
Ex) 비교
<hide/>
UserDao userDao1 = factory.userDao();
UserDao userDao2 = factory.userDao();
System.out.println(userDao1);
System.out.println(userDao2);
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
System.out.println(context.getBean("userDao", UserDao.class));
System.out.println(context.getBean("userDao", UserDao.class));
Note) 실행 결과
- 스프링은 여러 번에 걸쳐 빈을 요청하더라고 매번 동일한 오브젝트를 돌려준다.
- 오브젝트 팩토리는 매번 new에 의해서 새로운 오브젝트를 생성한다.
com.example.toby.ch01.UserDao@357246de
com.example.toby.ch01.UserDao@28f67ac7
com.example.toby.ch01.UserDao@6f10d5b6
com.example.toby.ch01.UserDao@6f10d5b6
1.6.1. 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
- 싱글톤 레지스트리(singleton registry)
- 애플리케이션 컨텍스트는 앞에서 만들었던 오브젝트 팩토리와 비슷한 방식으로 동작하는 IoC 컨테이너이다.
- 동시에 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 하다.
서버 애플리케이션과 싱글톤
- 스프링이 싱글톤으로 빈을 만드는 이유는?
- 스프링이 주로 적용되는 환경이 자바 엔터프라이즈 기술을 사용하는 서버 환경이기 때문이다.
- 스프링으로 PC 등에서 동작하도록 윈도우 프로그램 따위를 만들수도 있으나 실제로는 드물다.
- 애초에 스프링은 자바 엔터프라이즈 시스템을 위해 고안된 기술이므로 서버 환경에서 사용될 때 가치가 있다.
- 스프링이 처음으로 설계됐던 대규모의 엔터프라이즈 서버 환경은 서버 하나당 최대 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 처리하도록 높은 성능이 요구되는 환경이었다.
- 그런데 매번 클라이언트에서 요청이 올 때마다 오브젝트를 만들어서 사용한다면 ?
- 아무리 가비지 컬렉션의 성능이 좋아졌다고 하더라도 서버가 감당하기 힘들다.
- 그래서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 오래전부터 사용했다.
- "서블릿"은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다.
- 서블릿은 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들고 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해서 동시에 사용한다.
- 이렇게 제한된 (보통 한 개의) 오브젝트를 만들어서 사용하는 게 싱글톤 패턴의 원리이다.
싱글톤 패턴(Singleton Pattern)
- 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스의 개수, 이름처럼 주로 하나만 존재하게끔 강제하는 패턴을 말한다.
- 하나만 만들어지면 애플리케이션 내에서 전역적인 접근이 가능하다.
- 가장 자주 활용되지만 가장 비판을 받는 패턴이기도 하다.
- 디자인 패턴을 쓴 저자 GoF도 싱글톤 패턴을 매우 조심해서 사용하거나 피하라고 했다.
싱글톤 패턴의 한계
- 자바에서 싱글톤 구현 방법
- 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private로 만든다.
- 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다.
- static 팩토리 메서드인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다.
- 한번 오브젝트가 싱글톤으로 만들어지고 난 다음에는 getInstance() 를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.
Ex) UserDao를 싱글톤 패턴을 이용해서 만들면 다음과 같이 될 것이다.
<hide/>
public class UserDao {
private static UserDao INSTANCE;
private UserDao(ConnectionMaker connectionMaker) {
connectionMaker = new DConnectionMaker();
}
public static synchronized UserDao getInstance(){
if(INSTANCE == null){
INSTANCE = new UserDao();
}
return INSTANCE;
}
}
- 싱글톤을 위한 코드를 추가하고나니까 코드가 지저분해졌다.
- 게다가 private로 바뀐 생성자는 외부에서 호출할 수가 없기 때문에 daoFactory에서 UserDao를 생성하며 connectionMaker 오브젝트를 넣어주는 게 이제 불가능해졌다.
- 여러 모로 따져보더라도 UserDao에 싱글톤 패턴을 도입하는 건 무리로 보인다.
싱글톤 패턴의 구현 방식의 문제점
- private 생성자를 가지고 있어서 상속 불가능하다.
- 오직 클래스 자신 만이 자기 오브젝트를 만들도록 제한한다.
- private 생성자를 가진 클래스는 다른 생성자가 없으면 상속이 불가능하다.
- 따라서 객체 지향의 장점인 상속과 다형성을 적용할 수 없다.
- 싱글톤은 테스트가 힘들다. 또는 불가능하다.
- 싱글톤은 만들어지는 방식이 제한적이므로 테스트에서 사용될 때, Mock 오브젝트로 대체가 힘들다.
- 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
- 클래스 로더 구성에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다.
- 자바 언어를 이용한 싱글 패턴 기법은 서버 환경에서는 싱글톤이 보장된다고 볼 수 없다.
- 여러 개의 JVM에 분산돼서 설치되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.
- 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못한다.
- 싱글톤은 사용하는 클라이언트가 정해져있지 않다.
- 아무 객체나 자유롭게 접근하고 수정하고 고유할 수 있는 전역 상태르 ㄹ갖는 것은 객체 지향형 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다.
싱글톤 레지스트리(Singletone registry)
- 스프링은 서버 환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것은 적극 지지한다.
- 그러나 자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다.
- 이를 바로 "싱글톤 레지스트리"이다.
- 스프링 컨테이너는 싱글톤을 생성, 관리, 공급하는 싱글톤 관리 컨테이너 이기도 한다.
- 싱글톤 레지스트리의 장점
- static 메서드와 private 생성자를 사용해야하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다.
- 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용해서 생성과 관계 설정 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다.
- 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다.
1.6.2 싱글톤과 오브젝트의 상태
- 싱글톤은 멀티스레드의 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있어야한다.
- 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태 정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야한다.
- 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 위험하다.
- 저장할 공간이 하나뿐이라서 서로 값을 덮어쓰고 자신이 저장하지 않은 값을 읽어올 수 있기 때문이다.
- 따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태 유지(stateful) 방식으로 만들지 않는다.
- 이를 지키지 않으면 여러 사용자들이 동시 접속하는 경우에 데이터가 엉망이 되어 버리는 심각한 문제가 발생할 수 있다.
싱글톤 패턴(Singleton Pattern)
Ex) 인스턴스 변수를 사용하도록 수정한 UserDao
<hide/>
public class UserDao {
private ConnectionMaker connectionMaker;
private Connection c;
private User user;
public UserDao(ConnectionMaker connectionMaker) {
connectionMaker = new DConnectionMaker();
}
public User get(String id) throws ClassNotFoundException, SQLException{
this.c = connectionMaker.makeConnection();
// get user
PreparedStatement ps = c.prepareStatement(
" SELECT * " + " FROM users WHERE id = ?;");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
this.user = new User();
this.user.setId(rs.getString("id"));
this.user.setName(rs.getString("name"));
this.user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return this.user;
}
}
- ConnectionMaker: 읽기 전용 변수
- 매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수
- 기존에는 로컬 변수로 선언하고 사용했던 connection과 User를 클래스의 인스턴스 필드로 선언했다는 것이다.
- 따라서 싱글톤으로 만들어져서 멀티스레드 환경에서 사용하면 위에 설명한 대로 심각한 문제가 발생한다.
1.6.3 스프링 빈의 스코프
- 스프링이 관리하는 오브젝트, 즉 빈이 생성되고 존재하고 적용되는 범위를 알아보자.
- 스프링에서는 이를 빈의 스코프(scope)라고 한다.
- 스프링 빈의 기본 스코프는 싱글톤이다.
- 싱글톤 스코프는 컨테이너 안에 하나의 오브젝트만 만들어서 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다.
- 스프링에서 만들어지는 대부분의 빈은 싱글톤 스코프를 갖는다.
- 싱글톤 이외의 스코프: 프로토파입(prototype) 스코프, 요청(request) 스코프, 세션(session) 스코프
- 프로토 타입 스코프는 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다.
- 요청(request) 스코프: 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 스코프
- 세션(session) 스코프: 웹의 세션과 스코프과 유사한 스코프
1.7 의존 관계 주입(DI)
1.7.1 제어의 역전(IoC)과 의존 관계 주입
- IoC는 소프트웨어에서 자주 발견할 수 있는 일반적인 개념이다.
- 보통 "스프링"이라고 하면 컨테이너의 뜻인지 아니며 ㄴ프레임워크 의미인지, 파악이 힘들다.
- 그래서 스프링이 제공하는 IoC 방식의 핵심을 집어주는 "의존 관계 주입(DI)"라는 의도가 명확한 이름을 사용하기 시작했다.
의존관계 주입, 의존성 주입, 의존 오브젝트 주입 (Dependency Injection)
- 의존성 주입이라는 말이 가장 많이 사용된다.
- DI는 오브젝트 레퍼런스를 외부로부터 제공 받고 이를 통해 여타 오브젝트와 다이나믹하게 의존관계가 만들어지는 것이 핵심이다.
- "의존 관계 설정"이라는 용어를 쓰면 의도를 강하게 드러낼 수 있다.
1.7.2 런타임 의존 관계 설정
의존 관계
- 두 개의 클래스 또는 모듈이 의존 관계가 있는 경우, 방향성을 부여해줘야한다.
- UML 에서는 A => B라고 나타내면 A가 B에 의존하고 있다는 걸 나타낸다.
- 의존이라는 것은 B의 기능 추가 / 변경 / 형식 변경이 일어날 경우에 A에도 그 영향이 미친다는 뜻이다.
UserDao의 의존관계
- 기존에 작업했던 구조를 보면 UserDao가 ConnectionMaker 인터페이스에 의존하고 있다.
- 따라서 ConnectionMaker가 변하면 UserDao가 그 영향을 직접 받게 된다.
- 인터페이스를 통해 의존관계를 제한하면 그만큼 변경에서 자유로워진다.
- 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존 관계 외에도 런타임 시에 오브젝틍 사이에서 만들어지는 의존관계도 있다. => "런타임 의존 관계, 오브젝트 의존 관계"
- 의존 관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.
의존관계 주입에 대한 세 가지 조건
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야한다.
- 런타임 시점의 의존관는 컨테이너나 팩토리 같은 제3 의 존재가 결정한다.
- 의존관계를 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.
UserDao의 의존 관계 주입
- 관계 설정 책임 분리 전의 생성자
<hide/>
public class UserDao {
connectionMaker = new DConnectionMaker();
}
- 위 코드에 따르면 UserDao 클래스 설계 시점에 이미 DConnectionMaker 라는 구체적인 클래스의 존재를 알고있다.
- 이 코드의 문제점은 런타임 시의 의존 관계가 코드 속에 다 미리 결정되어 있다는 점이다.
- 그래서 IoC 방식을 써서 UserDao로부터 런타임 의존 관계를 드러내는 코드를 제거하고 제3 의 존재에 런타임 의존관계 결정 권한을 위임한다.
- 최종적으로 만들어진 것이 DaoFactory이다.
- DaoFactory는 앞서 살펴본 세 가지 조건을 모두 만족한다.
- DaoFactory는 DI 컨테이너이다.
- DI 컨테이너는 UserDao를 만드는 시점에 생성자의 파라미터로 만들어진 DConnectionMaker의 오브젝트를 전달한다.
- 생성자를 이용하면 파라미터를 가장 손쉽게 전달할 수 있다.
- 아래 코드와 같이 생성자를이용해서 전달받은 런타임 의존관계를 갖는 오브젝트는 인스턴스 변수에 저장해둔다.
- 두 개의 오브젝트 사이에 런타임 의존관계가 만들어졌다.
- 이와 같이 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 "의존 관계 주입"이라고 부른다.
- DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC(제어의 역전) 개념에 들어 맞는다.
- 그래서 스프링을 IoC 컨테이너, DI 컨테이너, DI 프레임워크 라고 부르는 것이다.
<hide/>
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
1.7.3 의존관계 검색(Dependency Lookup, DL)과 주입
- 스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 게 아니다.
- 의존관계 검색(dependency lookup)도 존재한다.
- 의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다.
- 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 맡기지만 이를 가져오거나 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.
Ex) 의존 관계 검색
- DaoFactory를 이용하는 생성자
- 아래와 같이 만들어도 UserDao는 자신히 어떤 ConnectionMaker 오브젝트를 사용할지 미리 알지 못한다.
- 여전히 ConnectionMaker 인터페이스에만 의존한다.
- 런타임 시에 factory가 만들어서 돌려주는 오브젝트와 런타임 의존관계를 맺는다.
- IoC 개념을 잘 따르고 있다.
- 하지만 외부로부터 주입이 아닌 컨테이너인 factory에게 요청하는 방식이다.
- DaoFactory 경우에는 미리 준비된 메서드를 호출하니까 단순 요청
<hide/>
public UserDao() {
DaoFactory daoFactory = new DaoFactory();
this.connectionMaker = daoFactory.connectionMaker();
}
- 의존 관계 검색을 이용하는 UserDao 생성자
- DaoFactory 경우에는 미리 준비된 메서드를 호출하니까 단순 요청으로 보이지만 애플리케이션 컨텍스트의 경우는 이미 정해 놓은 이름을 전달해서 해당 빈을 찾게 된다. => "검색"
- 애플리케이션 컨텍스트는 getBean()이라는 메서드를 이용해서 의존 관계를 검색할 수 있다.
- 의존 관계 검색은 의존 관계 주입의 장점을 모두 가지고 있다. 코드가 더 깔끔하다.
- 그러나 의존 관계 검색은 코드 안에 오브젝트 팩토리 클래그나 스프링 API가 나타난다. 컴포넌트가 컨테이너처럼 성격이 다른 오브젝트에 의존하므로 바람직하지 않다.
- 따라서 일반적으로는 의존 관계 검색 보다는 의존 관계 주입을 사용하는 게 낫다.
<hide/>
public UserDao() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
- 앞에서 의존 관계 검색 보다는 의존 관계 주입을 사용하는 게 낫다는 내용을 살펴봤는데 만약 의존 관계 검색을 꼭 사용해야만 하는 경우라면?
- 애플리케이션 기동 시점에서 적어도 한 번은 의존 관계 검색방식을 사용해서 오브젝트를 가져와야한다. static 메서드의 main() 같은 기동 메서드는 없으나 사용자가 요청을 받을 때마다 main() 과 비숫한 역할을 하는 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번은 " 의존 관계 검색" 방식을 사용해서 가져와야한다.
- 서블릿은 스프링이 미리 만들어서 제공하므로 직접 구현할 필요 없다.
- 의존 관계 검색 / 의존 관계 주입 둘의 중요한 차이점은?
- 의존관계검색에서 UserDao에서 getBean()을 사용한 의존관계 검색을 적용한다고 가정하자. 그럼 오브젝트는 자신이 스프링의 빈일 필요가 없다. 직접 new UserDao()해서 만들어도 상관없다. ConnectionMaker만 스프링의 빈이면 된다.
- 의존관계주입에서는? UserDao와 ConnectionMaker 사이에 DI를 적용하려면 반드시 둘다 빈 오브젝트여야한다. UserDao에 ConnectionMaker 오브젝트를 주입하려면 UserDao에 대한 생성과 초기화 권한을 갖고 있어야하고 그러려면 UserDao는 빈이어야하기 때문이다.
- DI를 원하는 오브젝트는 먼저 자신이 빈이 되어야한다.
1.7.4 의존관계 주입의 응용
기능 구현의 교환
- DI의 장점은?
- DI(런타임 시에 사용 의존관계를 맺을 오브젝트를 주입)
- 개발 환경과 운영 환경에서의 설정 정보에 따른 DaoFactory만 다르게 두면 나머지는 변경 사항 없이 개발 시, 운영 시에 각각 다른 런타임 오브젝트에 의존 관계를 갖게 해줄 수 있다.
부가 기능 추가
- DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다면?
- 연결 횟수를 직접 세는 것은 아주 비효율적인 방법이다.
- DI 컨테이너를 이용하면 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 추가해서매우 간단하게 가능하다.
1.7.5 메소드를 이용한 의존관계 주입
생성자가 아닌 일반 메서드를 이용해서 의존 오브젝트와의 관계를 주입하는 방법
- 수정자(setter) 메서드를 이용한 주입: setter는 파라미터를 전달된 값을 내부의 인스턴스 변수에 저장한다. 외부로부터 제공받은 오브젝트 레퍼런스를 저장했다가 내부의 메소드에서 사용하게 하는 DI 방식에서 활용하기에 적당하다.
- 일반 메서드를 이용한 주입: 수정자는 하나의 파라미터만 가질 수 있는데 이러한 제약이 싫은 경우, 여러 개의 파라미터를 갖는 일반 메소드를 DI용으로 사용할 수 있다.
- 스프링은 수정자 메서드를 가장 많이 사용해왔다. 자바 코드 대신 XML을 사용하는 경우에는 자바빈 규약을 따르는 수정자 메소드가 가장 사용하기 편리하다.
1.8 XML을 이용한 설정
- 스프링은 DaoFactory와 같은 자바 클래스외에도 다양한 방법을 통해서 DI 의존 관계 설정 정보를 만들 수 있다.
- XML이 가장 대표적인 방법이다. XML은 단순한 텍스트 파일이므로 다루기 쉽다.
1.8.1 XML 설정
- 애플리케이션 컨텍스트는 XML에 담긴 DI 정보를 활용할 수 있다.
- DI가 담긴 XML 파일은 <beans>를 루트 엘리먼트로 사용한다.
- XML 설정은 @Configuration과 @Bean 이 붙은 자바 클래스로 만든 설정과 내용이 동일하다.
- @Configuration는 <beans> , @Bean은 <bean>라고 대응해서 생각하면 쉽다.
- 하나의 @Bean 메소드를 통해 얻을 수 있는 빈의 DI 정보는 다음과 같다.
- 빈의 이름: @Bean 메소드 이름이 빈의 이름이다. getBean()에서 사용된다.
- 빈의 클래스: 빈 오브젝트를 어떤 클래스를 이용해서 만들지 정의한다.
- 빈의 의존 오브젝트: 빈의 의존 오브젝트를 넣어준다. 의존 오브젝트도 하나의 빈이므로 이름이 있을 것이고 그 이름에 해당하는 메소드를 호출해서 의존 오브젝트를 가져온다. 의존 오브젝트는 하나 이상일 수도 있다.
- XML에서 <bean>을 사용해도 세 가지 정보를 정의할 수 있다.
- 더 이상 의존하고 있는 오브젝트가 없는 경우에는 세 번째 의존 오브젝트 정보를 생략 가능하다.
- XML은 자바 코드처럼 유연하게 정의될 수 있는 것이 아니므로 핵심 요소를 짚어서 그에 해당하는 태그와 애트리뷰트가 무엇인지 알아야한다.
connectionMaker() 전환
- content
title
- content
title
- content
title
- content
title
- content
찾아보기
- content
참고) 내용이 잘못됐거나 다르게 생각하시는 부분은 댓글 부탁드립니다❣
출처 - 「토비의 스프링 3.1 Vol 1 - 이일민」
'Spring Framework > 토비의 스프링' 카테고리의 다른 글
Chapter 04. 독립 실행형 스프링 애플리케이션 (0) | 2023.06.24 |
---|---|
Chapter 03. 독립 실행형 서블릿 애플리케이션 (0) | 2023.06.20 |
Chapter 02. 스프링 부트 시작하기 (0) | 2023.06.18 |
Chapter 01. 스프링 부트 살펴보기 (0) | 2023.06.17 |
머릿말 (0) | 2023.03.18 |