1.1 초난감 DAO
1.1.1 User
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;
}
}
1.1.2 UserDao
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book");
PreparedStatement ps = c.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();
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/springbook", "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;
}
}
1.1.3 main()을 이용한 DAO 테스트 코드
public static void main(String[] args) throws Exception {
UserDao dao = new UserDao();
User user = new User();
user.setId("choijaewon");
user.setName("최재원");
user.setPassword("choijaewon");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
}
1.2 DAO의 분리
1.2.1 관심사의 분리
- 객체지향에서는 객체의 설계와 코드가 항상 변함
- 그래서 변화의 폭을 최소한으로 줄여줘야 함
- 분리와 확장을 고려한 설계
- 분리
- 관심이 같은 것끼리는 모으고, 다른 것은 떨어져 있게 → 관심사의 분리
1.2.2 커넥션 만들기의 추출
- add() 메소드 안의 세가지 관심사항
- DB와 연결을 위한 커넥션을 어떻게 가져올 지
- SQL 문장을 담을 Statement를 만들고 실행하는 것
- 작업 후 Statement와 Connection 닫기
중복 코드의 메소드 추출
- 중복된 DB 연결 코드를 getConnection() 으로 분리
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book"
);
return c;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = getConnection();
PreparedStatement ps = c.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();
ps.close();
c.close();
}
- 나중에 DB 종류, URL 등이 바뀌어도 getConnection() 만 수정하면 됨
변경사항에 대한 검증: 리팩토링과 테스트
- 리팩토링 : 기능에는 변화를 주지 않고 코드의 구조를 바꾸는 것
1.2.3 DB 커넥션 만들기의 독립
- 만약 DB 커넥션을 가져오는데 있어 독자적으로 만든 방법을 적용하고 싶다면?
- 그리고 컴파일된 클래스 바이너리 파일만 제공하고 싶다면?
상속을 통한 확장
- UserDao 클래스를 추상 클래스로 만든다.
- getConnection() 을 추상 메소드로 만들고, 이를 사용하는 측에서 구현하도록 만든다.
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
class NUserDao extends UserDao {
public Connection getConnection() throws ClassNotFoundException, SQLException {
}
}
- 슈퍼클래스에 기본 로직 흐름을 만들고 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드로 만들고 서브클래스에서 알맞게 구현하는 방법 → 템플릿 메소드 패턴
- 서브클래스에서 구체적인 객체 생성 방법을 결정하게 하는 것 → 팩토리 메소드 패턴
- 단점
- 자바에서는 다중 상속이 불가능 → 이미 UserDao가 상속을 사용하고 있다면?
- 상속은 생각보다 밀접한 관련 → 슈퍼클래스 내부 변경 시 모든 서브클래스에 영향이 감
1.3 DAO의 확장
1.3.1 클래스의 분리
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = simpleConnectionMaker.makeNewConnection();
}
}
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의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되게 됨 → 코드 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없음
1.3.2 인터페이스의 도입
- 두 개의 클래스가 서로 긴밀하게 연결되지 않도록 중간에 추상적인 느슨한 연결고리 삽입
- 인터페이스 → 자신을 구현한 클래스에 대한 구체적 정보를 감춤
- 이를 통해 구현 클래스가 바뀌어도 신경쓸 일이 없음
interface ConnectionMaker {
Connection makeConnection() throws ClassNotFoundException, SQLException;
}
class DConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException {
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new DConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = connectionMaker.makeConnection();
}
}
- N사와 D사가 DB 접속용 클래스를 다시 만든다 해도 UserDao의 코드를 고칠일은 없음
- 그러나 여전히 생성자에서 DConnection 클래스의 생성자를 직접 호출한다.
- 결국 UserDao를 수정해야만 가능한 경우가 되었다.
1.3.3 관계설정 책임의 분리
- 위의 문제는 UserDao가 어떤 ConnectionMaker 구현 클래스를 이용할지를 결정하게 만드는 관심 사항이 존재하고 있기 때문이다.
- UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 만들어야함
- UserDao 오브젝트가 동작하려면 특정 클래스의 오브젝트와 관계를 맺어야 하긴 함
- 위의 경우 코드에 특정 클래스 이름이 들어감 → 클래스 간의 관계가 형성된 것
- 오브젝트간의 관계는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면 사용 가능 → 다형성
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
- UserDao의 클라이언트에게 구현 클래스를 선택하는 책임을 넘김
1.3.4 원칙과 패턴
개방 폐쇄 원칙
- OCP (Open-Closed Principle) : 클래스, 모듈은 확장에는 열려있어야 하고 변경에는 닫혀 있어야 함
높은 응집도와 낮은 결합도
- 응집도가 높다 → 하나의 모듈, 클래스가 하나의 책임 또는 관심사에 집중
- 결합도 → 하나의 객체가 변경될 때 관계를 맺고 있는 다른 객체에게 변화를 요구하는 정도
전략 패턴
- 자신의 기능 맥락(Context)에서 변경이 필요한 알고리즘을 인터페이스로 통째로 분리시키고, 구현 클래스를 필요에 따라 바꿔서 사용하는 디자인 패턴
1.4 제어의 역전 (IoC)
1.4.1 오브젝트 팩토리
- UserDaoTest 클라이언트 코드는 얼떨결에 구현 클래스를 지정하는 역할까지 맡게 되었다.
- 원래의 책임은 UserDao를 테스트하는 것이므로, 이 부분을 분리해줘야 한다.
팩토리
- 객체의 생성 방법을 결정하고 만든 객체를 리턴하는 클래스
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
UserDao dao = new DaoFactory().userDao();
- UserDaoTest에서는 이제 DaoFactory로부터 UserDao를 받아 실행 → UserDao를 어떻게 만들고 어떻게 초기화하는지 관심을 가지지 않아도 됨
1.4.2 오브젝트 팩토리의 활용
- 만약 다른 DAO가 만들어지는 경우 중복된 코드가 발생 가능
- 거기서 또 분리
public class DaoFactory {
public UserDao userDao() {
return new UserDao(connectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(connectionMaker());
}
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
1.4.3 제어권의 이전을 통한 제어관계 역전
- 제어의 역전 : 객체가 자신이 사용할 객체를 스스로 선택하지 않고 생성하지도 않음
- 라이브러리와 프레임워크의 차이 : 라이브러리를 사용하면 어플리케이션 코드 흐름을 직접 제어 but 프레임워크는 코드가 프레임워크에 의해 사용됨
1.5 스프링의 IoC
1.5.1 오브젝트 팩토리를 이용한 스프링 IoC
- 빈(Bean) : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체
- 이러한 빈의 생성 및 제어를 담당하는 IoC 오브젝트 → 빈 팩토리(Bean Factory) 혹은 Application Context
1.5.2 애플리케이션 컨텍스트의 동작방식
- DaoFactory와 달리 ApplicationContext 에는 직접 객체를 생성하고 관계를 맺는 코드가 없고, 별도의 설정 (@Configuration) 정보를 통해 얻음
- @Bean 이 붙은 메소드의 이름을 가져와 bean 목록을 만들어둠
- ApplicationContext를 사용했을 때의 장점
- 구체적인 팩토리 클래스를 알 필요 없음
- 종합 IoC 서비스 제공
- 빈을 검색하는 다양한 방법 제공
1.6 싱글톤 레지스트리와 오브젝트 스코프
- DaoFactory를 사용할 경우 dao를 계속 만들 때 계속 다른 객체가 나온다.
- 그러나 ApplicationContext로 계속 만들면 똑같은 객체가 나온다
1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
- 스프링에서는 별다른 설정 안하면 싱글톤으로 빈 객체를 생성
서버 애플리케이션과 싱글톤
- 서버 환경에서는 매번 요청이 올 때마다 객체를 새로 만들면 오버헤드가 너무 큼
- 그래서 엔터프라이즈 분야에서 서블릿은 멀티스레드 환경에서 싱글톤으로 동작
- 서블릿 클래스당 하나의 객체만 만들고, 여러 스레드에서 하나의 오브젝트를 공유해 사용
싱글톤 패턴의 한계
- 자바를 이용한 싱글톤의 한계
- private 생성자 → 상속 불가능
- 테스트가 힘듦
- 서버환경에서는 싱글톤이 하나임을 보장하기 어려움 → 여러 JVM에 분산되어있는 경우 각각 독립적으로 싱글톤이 생성됨
- 전역 상태를 만들 수 있어 바람직하지 않음
싱글톤 레지스트리
- 이러한 자바의 싱글톤 구현 방식 문제를 해결 → 싱글톤 레지스트리
1.6.2 싱글톤과 오브젝트의 상태
- 멀티스레드 환경에서 싱글톤은 상태정보를 가지고 있지 않은 무상태 방식으로 만들어야함
- 이 때 각 요청에 대한 정보, DB, 서버의 리소스로부터 만든 정보는 어떻게?
- 파라미터, 로컬 변수, 리턴 값 등을 이용
- 이러한 값들은 스택에 저장 → 각각의 독립적인 공간에서 사용됨
public class UserDao {
private ConnectionMaker connectionMaker;
private Connection c;
private User user;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement(
"select * from users where id = ?"
);
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
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;
}
}
- 위의 코드에서 Connection과 User 인스턴스 변수는 심각한 문제를 발생시킬 수 있다.
- 이러한 개별적으로 바뀌는 정보는 모두 로컬 변수로 사용해야 한다.
- connectionMaker는 왜 가능?
- 읽기 전용의 정보이고, 스프링의 의존성 주입으로 받는 객체로써 스프링의 관리를 받기 때문
- 읽기 전용의 인스턴스 변수의 경우 final을 붙여주자.
1.6.3 스프링 빈의 스코프
- 빈이 생성되고, 존재하고, 적용되는 범위 → 스코프 (Scope)
- 싱글톤 : 컨테이너 내에 한 개의 객체만 만들어지고, 컨테이너가 존재하는 동안 계속 유지
1.7 의존관계 주입 (DI)
1.7.1 제어의 역전(IoC)과 의존관계 주입
- 스프링 IoC 기능의 대표 동작 원리는 의존관계 주입 (DI)이다.
1.7.2 런타임 의존관계 설정
의존관계
- UML 모델에서 A가 B에 의존할 경우 점선 화살표로 A → B라고 표현
- 의존 - B가 변하면 A에 영향을 미침
- 의존관계에는 방향성이 있음 → B는 A의 변화에 영향을 받지 않음
UserDao의 의존관계
- UserDao가 ConnectionMaker를 사용하므로, UserDao → ConnectionMaker라고 할 수 있음
- 하지만 ConnectionMaker를 구현한 클래스가 바뀌는 것은 UserDao에 영향을 주지 못함
- 인터페이스에 대해서만 의존 관계를 만들면 결합도를 낮출 수 있음
- 런타임 의존관계도 존재 - 런타임 시에 의존관계를 맺는 실제 사용 대상인 객체를 의존 오브젝트라고 함
- 의존 관계 주입 (DI) : 구체적인 의존 오브젝트, 그것을 사용할 주체를 런타임 시에 연결해주는 작업
1.7.3 의존관계 검색과 주입
- 스프링은 외부로부터 주입이 아니라 스스로 검색을 이용하게도 만들 수 있음 → 의존관계 검색 (Dependency Lookup)
- ApplicationContext는 getBean() 메소드를 제공하여 의존관계 검색
- 검색 vs 주입
- 검색 방법의 경우 스프링 API가 나타남 → 스프링에 의존하는 코드를 만들게 됨
- 그래서 주입이 나음
- 검색의 경우 검색하는 객체가 스프링의 빈일 필요가 없음 → 어딘가에서 new로 생성해서 사용해도 됨
- 그러나 주입은 자기 자신이 컨테이너가 관리하는 빈이어야함
1.7.4 의존관계 주입의 응용
기능 구현의 교환
// 개발 시
@Bean
public ConnectionMaker connectionMaker() {
return new LocalDBConnectionMaker();
}
// 배포 시
@Bean
public ConnectionMaker connectionMaker() {
return new ProductionDBConnectionMaker();
}
- Configuration 파일만 손보면 다른 코드는 손대지 않고 DB를 바꿀 수 있음
부가기능 추가
- 만약 DB 연결횟수를 카운팅하고 싶다면?
- DAO와 DB 커넥션을 만드는 객체 사이에 연결횟수를 카운팅하는 객체를 하나 더 추가
public class CountingConnectionMaker implements ConnectionMaker {
int counter = 0;
private ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
this.realConnectionMaker = realConnectionMaker;
}
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter() {
return counter;
}
}
- UserDao → CountingConnectionMaker → DConnectionMaker 로 런타임 의존관계가 형성됨
- @Configuration 파일을 수정함으로써 UserDao의 ConnectionMaker를 CountingConnectionMaker로 주입시킴
- CountingConnectionMaker의 실제 사용은 클라이언트 코드에서 직접 getBean으로 호출하고, getCounter() 를 호출하면서 이루어진다.
1.7.5 메소드를 이용한 의존관계 주입
- setter를 이용해서 주입받거나, 일반 메소드를 이용해서 주입받을 수도 있다.
https://product.kyobobook.co.kr/detail/S000000935360