SPRING
토비의 스프링 1. 오브젝트와 의존관계
짜비
2022. 5. 27. 21:54
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book"
);
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();
ps.close();
connection.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book"
);
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"));
rs.close();
ps.close();
connection.close();
return user;
}
}
관심사의 분리
- 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것.
- UserDao 의 add() 메소드의 관심사항
- DB 커넥션
- SQL 만들고 실행
- 리소스 반환
중복 코드의 메서드 추출
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book"
);
return connection;
}
class NUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
//N사 DB Connection 생성 코드
}
}
템플릿 메소드 패턴
- 슈퍼클래스에 기본적인 로직의 흐름을 만들어 두고, 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법.
팩토리 메소드 패턴
- 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것.
상속이 아닌 독립적인 클래스로 분리
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook", "spring", "book"
);
return connection;
}
}
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
//Connection connection = getConnection();
Connection connection = simpleConnectionMaker.makeNewConnection();
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();
ps.close();
connection.close();
}
}
위 코드의 문제점
- userDao가 DB 커넥션을 가져오는 클래스에 대해 너무 많은 정보를 알고 있다. (e.g. 어떤 클래스가 쓰일지, 커넥션을 가져오는 메소드의 이름이 무엇인지 …)
해결 방법 : 인터페이스의 도입
- 중간에 추상적인 느슨한 연결고리를 만들어 주는 것.
- userDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 된다. 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
public class NConnectionMaker implements ConnectionMaker{
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
//N사의 독자적인 Connection 생성하는 코드
return null;
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
NConnectionMaker connectionMaker = new NConnectionMaker();
//구체적인 클래스 이름이 나옴 ...
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection connection = connectionMaker.makeConnection();
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();
ps.close();
connection.close();
}
}
관계설정 책임의 분리
- (참고) 두 개의 오브젝트가 있고 한 오브젝트가 다른 오브젝트의 기능을 사용한다면, 사용되는 쪽 : 서비스 / 사용하는 쪽 : 클라이언트
- 클라이언트가 ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 오브젝트를 생성해서 UserDao와 연결하도록 수정.
- 위 코드에서는 main() 메소드가 클라이언트 역할 수행 중
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new NConnectionMaker();
UserDao dao = new UserDao(connectionMaker);
}
}
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection connection = connectionMaker.makeConnection();
//...
}
개방 폐쇄 원칙
- 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
- UserDao : DB 연결 방법 확장 가능 & 자신의 핵심 기능을 구현한 코드는 변화에 영향을 받지 않고 그대로 유지.
높은 응집도
- 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있음.
- 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다. 따라서 작업은 항상 전체적으로 일어나고 무엇을 변경해야할 지 명확하다.
낮은 결합도
- 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도
- e.g. ConnectionMaker 인터페이스 도입으로 DB 연결 기능을 구현한 클래스가 바뀌더라도 DAO 코드는 변경될 필요가 없게 됨.
전략 패턴
- 자신의 기능 맥락(context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴.
- e.g. UserDao : Context, DB 연결 방식 : 알고리즘(전략), UserDaoTest: 클라이언트
- 컨텍스트를 사용하는 클라이언트는 컨텍스트가 사용할 전략을 컨텍스트의 생성자를 통해 제공해주는 것이 일반적.
제어의 역전(IoC)
오브젝트 팩토리
- 팩토리 : 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것.
- 기존 UserDaoTest 내 1) ConnectionMaker 구현 클래스의 오브젝트를 만드는 것 & 2) userDao 기능 테스트. 두 가지 관심사를 분리.
public class DaoFactory {
public UserDao userDao() {
NConnectionMaker connectionMaker = new NConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new DaoFactory().userDao();
//...
}
}
- userDao, ConnectionMaker : 실질적인 로직을 담당하는 컴포넌트
- DaoFactory : 어플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도
제어의 역전
- 일반적인 프로그램의 흐름 : 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장. 다시 말해 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조.
- 제어의 역전 : 위 흐름을 거꾸로 뒤집는 것. 즉, 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하거나 생성하지 않음. 자신 또한 어떻게 만들어지고 어디서 사용되는지 알 수 없음.
- 라이브러리 vs 프레임워크
- 전자 : 애플리케이션 코드가 흐름을 직접 제어
- 후자 : 어플리케이션 코드가 프레임워크에 의해 사용됨.
스프링의 IoC
어플리케이션 컨텍스트와 설정정보
bean
- 스프링이 제어권을 가지고 직접 만들고, 관계를 부여하는 오브젝트. 혹은 오브젝트 단위의 어플리케이션 컴포넌트. 혹은 제스프링 컨테이너가 제어하는 제어의 역전이 적용된 오브젝트.
bean factory
- 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트
application context
- bean factory를 확장한 것.
DaoFactory를 사용하는 어플리케이션 컨텍스트
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao(connectionMaker());
return userDao;
}
@Bean
public ConnectionMaker connectionMaker() {
return new NConnectionMaker();
}
}
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
//...
}
}
- getBean() : ApplicationContext가 관리하는 오브젝트를 요청하는 메소드
Application Context 동작 방식
- DaoFactory 클래스를 설정정보로 등록
- @Bean이 붙은 메소드 이름을 가져와 빈 목록 생성
- 클라이언트가 getBean() 호출시 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메소드를 호출해서 오브젝트 생성 후 반환
Application Context 사용 장점
- 클라이언트는 구체적인 팩토리 클래스를 알 필요 없음
- 오브젝트 생성, 관계설정 이외의 다양한 기능 제공
- e.g. 오브젝트 생성 방식, 시점 다변화, 오브젝트 후처리 등…
- bean 검색하는 다양한 방법 제공
싱글톤
- 어프리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용.
- 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다.
- e.g. getBean() 여러 번 호출 -> 모두 같은 오브젝트
- 이유
- 스프링은 엔터프라이즈 시스템을 위해 고안된 기술. 대부분 서버 환경에서 사용됨.
- 서버는 하나의 요청 처리를 위해 다양한 오브젝트들이 참여하는 계층형 구조가 대부분. 만약 클라이언트에게 요청이 올 때마다 오브젝트가 새로 만들어진다면 서버가 부하를 감당하기 힘들 것.
일반적인 싱글톤 패턴 구현의 문제점
- private 생성자를 가짐. -> 상속 불가능
- 테스트 어려움. (생성자를 통한 오브젝트 주입 불가능)
- 전역 상태로 사용되기 쉬움.
싱글톤 레지스트리
- 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공.
- static, private 생성자를 사용하지 않고도 클래스를 싱글톤으로 활용 가능.
싱글톤 주의사항
- 싱글톤 오브젝트는 ‘무상태’로 만들어야 함.
- 파라미터, 로컬 변수, 리턴 값을 활용해서 정보를 다뤄야 함.
public class UserDao {
//OK
private ConnectionMaker connectionMaker;
//매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수
private Connection c;
private User user;
public User get(String id) throws ClassNotFoundException, SQLException {
this.c = connectionMaker.makeConnection();
this.user = new User();
this.user.setId(rs.getString("id"));
//...
}
}
의존관계 주입(Dependency Injection)
- 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않음. 이를 위해서는 인터페이스에만 의존하고 있어야 함.
- 런타임 시의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
의존관계 검색(dependency lookup, DL)
- 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다.
public UserDao(ConnectionMaker connectionMaker) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
DI vs DL
- DI 의 코드가 더 깔끔하다.
- DL 의 경우, 오브젝트 팩토리 클래스 혹은 스프링의 API가 나타남.
- 그럼에도, DL을 사용해야만 하는 경우가 있음.
- 어플리케이션 기동 시점 -> DI를 통해 오브젝트를 주입받을 방법이 없음. -> 의존관계 검색을 통해 오브젝트를 가져옴.
- e.g. main 메소드, 서블릿
- DI : DI를 받으려면 자기 자신이 컨테이너가 관리하는 빈이 되어야 함.
- DL : 자신이 스프링 빈일 필요 없음.
DI 응용
부가기능 추가
- 예시 : Dao 에서 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 {
return realConnectionMaker.makeConnection();
}
public int getCounter() {
return counter;
}
}
@Configuration
public class CountingDaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker() {
return new NConnectionMaker();
}
}
public class UserDaoConnectionCountTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CountingDaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
//...
CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
System.out.println("ccm.getCounter() = " + ccm.getCounter());
}
}
수정자(Setter) 를 통한 의존관계 주입
- outdated
XML 을 이용한 의존관계 설정
- outdated
DataSource 인터페이스로 전환
- DB connection을 가져오는 오브젝트의 기능을 추상화해서 사용할 수 있도록 만들어진 인터페이스.
public class UserDao {
private DataSource dataSource;
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection connection = dataSource.getConnection();
//...
}
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao(dataSource());
return userDao;
}
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
// dataSource 설정 정보
return dataSource;
}
}