스프링이란 ?
Spring Container(스프링 컨테이너 = Application Context)를 기본 틀로 한다.
스프링은 표준 자바 엔터프라이즈 플랫폼에 기반을 두고 있다.
스프링 프레임워크에서 제공하는 프로그래밍 모델은 IoC/DI, 서비스 추상화, AOP가 있다.
왜 스프링을 사용하는가 ?
단순함
POJO(Plain Old Java Object) 프로그래밍으로 객체지향적인 개발 모델을 구현한다. POJO 프로그래밍이란, 컨벤션이나 프레임워크에 종속되지 않는 단순한 Java 오브젝트를 사용한 프로그래밍을 의미한다.
유연함
많은 Third Party의 지원으로 라이브러리가 많고, 유연성과 확장성이 뛰어나다. 예를 들어서 버전 호환성 문제를 겪거나 아키텍쳐를 대거 수정해야하는 등의 불필요한 트러블 슈팅이 적다.
초난감 DAO
public class UserDao {
// 사용자 생성
public void add(User user) throws ClassNotFoundException, SQLException{
Class.forName("com.mysql.jdbc.Driver");
// DB 연결을 위한 Connection 객체
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306", "root", "");
// SQL을 담은 Statement를 실행
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");
// DB 연결을 위한 Connection 객체
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306", "root", "");
// SQL을 담은 Statement를 실행
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(Data Access Object)를 만들었다. 이런 초난감 DAO를 개선하는 과정을 통해 개발을 할 때 어떤 고민을 해야하는 지 생각해보자.
관심사의 분리 (Seperation of Concerns)
객체지향 설계는 기존 절차적 프로그래밍보다 변화에 효과적으로 대처할 수 있다는 점에서 장점이고, 이를 구현하기 위해 초반의 번거로운 작업이 필요하다. 그 중 하나가 변경의 여지가 있는 하나의 관심사를 분류하는 것이다.
UserDAO 코드에서 DB Connection, Statement, close 부분으로 관심사항을 나눌 수 있다. 이러한 관심사항을 중복 코드의 메소드 추출로 분리한다. 이러한 기법을 메소드 추출 (extract method) 기법 이라고 한다.
상속을 이용한 확장
getConnection() 함수를 분리하는 것에 성공했다. 그렇다면 사용자마다 이 UserDAO를 사용할 때 서로 다른 DB를 사용하고 싶어하다면, 코드를 매번 바꿀 것인가 ? 아니다. 이렇게 확장 가능성을 고려하여 상속을 사용한다.
getConnection () 함수를 추상화하면, UserDao 클래스를 상속해 서브클래스에서 서로 다른 getConnection() 함수를 가질 수 있다.
- 클래스 계층 구조로 여러 개의 관심 (서로 다른 DB Connection)이 독립적으로 분리되었다.
- 상위 클래스 (UserDao)의 기능을 추상 메소드나 오버라이딩이 가능한 protected 메소드로 만든 뒤 서브 클래스에서 상속하여 구현하는 방법을 템플릿 메소드 패턴 (Template Method Pattern) 이라고 한다.
- 상위 클래스에서는 Connection 객체를 어떻게 생성할 지 서브 클래스들에게 팩토리 메서드르 사용해 레퍼런스를 제공하고 있다. 이렇게 오브젝트 생성 방법을 상위 클래스의 코드에서 독립시키는 방법을 팩토리 메소드 패턴 (Factory Method Pattern) 이라고 한다.
클래스의 분리
Connection 객체를 생성하는 역할을 하는 새로운 클래스를 만들어버린다.
그럼 위에서 구현하였던 상속으로 인한 확장은 어떻게 되는 건가 ? 이렇게 분리하더라도 여전히 N사, D사에 대한 확장성을 고려해야하고, 더 광범위한 확장에 대해서도 고려해야 한다.
그리고, UserDao는 N사, D사에 상관없이 DB를 연결하는 단일한 기능을 고려하고 싶은데, 어떤 Connection을 사용할 지 미리 알고 있어야 한다는 점이 확장을 막고 있다.
이를 해결하기 위해 인터페이스를 도입한다.
SimpleConnectMaker 클래스를 인터페이스화 하고, 어떤 기능을 구현하고 있는지만 대략적으로 작성한다.
이렇게 작성한 ConnectionMaker 인터페이스를 사용해서 UserDao를 리팩토링한다.
이렇게 다형성을 사용해 DConnectionMaker() 이라는 구체 클래스가 담긴 connectionMaker 인스턴스를 생성하였지만, 여전히 UserDao는 N사, D사 등의 정보를 알 필요가 있었다.
관계 설정 책임의 분리
왜 이런 일이 생길까 ? UserDao 안에 아직도 분리되지 않은 관심사항이 남아있기 때문이다.
connectionMaker = new DConnectionMaker();
이 코드는 그 자체로 독립적인 관심사를 담고 있다. 바로 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계 설정에 대한 관심이다. 이 관심사항을 다른 곳으로 두는 것이 필요한데, UserDao를 사용하는 클라이언트 오브젝트에 이 관심사항을 떠넘겨서 이 문제를 해결할 수 있다.
그 전에, UserDao가 어떤 ConnectionMaker의 구현 클래스를 사용할 지 결정하도록 만들기 위해 두 오브젝트 간의 관계를 설정한다. 즉 외부에서 만든 오브젝트를 UserDao 객체 생성 시 파라미터로 전달 받는 방식이다.
중요한 것은, 코드 상에서의 클래스 간의 관계가 아니라 런타임 시점에서의 오브젝트 사이의 동적인 관계가 만들어진다는 것이다.
이 관계를 UserDao의 클라이언트가 만든다. 따라서 클라이언트 역할은 (1) ConnectionMaker의 구현 클래스를 선택하고 (2) 그 클래스의 오브젝트를 생성해서 UserDao와 연결하는 것이다. UserDao를 사용하는 클래스를 UserDaoTest 클래스라고 하고, 위에서 말한 두 가지 기능을 구현해보자.
이렇게 클라이언트가 자신이 원하는 Connection에 따라서 connectionMaker 오브젝트를 생성하고, UserDao 오브젝트와 연결하게 된다. 이 과정을 통해 UserDao는 더 이상 특정 구현 클래스를 선택할 필요가 없어지므로, 아래와 같이 생성자를 재정의할 수 있다.
원칙과 패턴
스프링을 설명할 때 기본이 되는 원칙과 패턴의 용어들에 대해서 설명하고 포스팅을 마무리하겠다.
개방 폐쇄 원칙 (OCP, Open-Closed Priciple)
클래스나 모듈은 확장에는 열려 있어야하고, 변경에는 닫혀 있어야 한다. 예를 들어, 인터페이스를 통해 제공되는 특정 기능들은 다른 구체 클래스를 통한 확장에는 열려 있다. 그러나 이 인터페이스를 가져와서 사용하는 클래스(예를 들어 UserDao)는 변경에 대한 여지가 없이 닫혀 있다.
개방 폐쇄 원칙은 높은 응집도, 낮은 결합도라는 말로도 표현 가능하다.
응집도가 높다는 것은 말 그대로 하나의 관심사항이 다른 관심사항과 독립되어서 뭉쳐져 있는 것을 말한다. 예를 들어서, ConnectionMaker 인터페이스를 이용해 DB 연결 기능을 독립시킴으로써 다른 DB Connection 정보의 추가 요청이 들어온다면 구체 클래스를 만들기만 하면 되는 것이다. 이렇게 ConnectionMaker를 분리하는 방식으로 응집도를 높일 수 있다.
이해가 안 되는 점.
책 설명에는 "응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수 있다" 라는 언급이 있다. (p.85) ConnectionMaker로 분리하는 것이 응집도를 높이는 과정이라면, 변화 (DB Connection 정보의 추가) 가 일어날 때 UserDao 등 다른 클래스의 수정을 최소화하는 것이 응집도를 높이는 것이 아닌가 ? 변하는 부분을 작게 하는 것이 응집도가 높다는 판단이 드는데 위 문장이 이해가지 않는다.
반면 결합도가 낮다는 것은, 관심사항이 다른 오브젝트 간의 느슨한 연결 형태를 말한다. 결합도란 '하나의 오브젝트가 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도'라고 설명할 수 있다. ConnectionMaker 인터페이스를 만들고, UserDaoTest라는 UserDao 클라이언트로 관심사항을 분리함으로써 각 클래스의 변경이 다른 클래스에게 영향을 미치는 결합도가 낮아졌다.
전략 패턴
이렇게 UserDaoTest (클라이언트) - UserDao (컨텍스트) - ConnectionMaker (인터페이스) 구조를 전략 패턴이라고 한다.
전략 패턴은, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리하고, 필요에 따라 구체 클래스를 만들어서 사용하도록 하는 디자인 패턴이다. 컨텍스트를 사용하는 클라이언트는, 컨텍스트가 사용할 전략 (N사? D사?)를 컨텍스트의 생성자를 통해 제공하는 것이 일반적이다.
'DEV book > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] 3장 템플릿 (2) - JdbcContext를 UserDao에서 사용하는 두 가지 방법 (0) | 2023.09.24 |
---|---|
[토비의 스프링 3.1] 3장 템플릿 (1) - 디자인 패턴과 DI를 이용한 DAO 최적화 (0) | 2023.09.23 |
[토비의 스프링 3.1] 2장 테스트 - 단위 테스트와 테스트 코드 개선 (0) | 2023.09.20 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (3) - 싱글톤 레지스트리와 의존관계 주입 (0) | 2023.09.14 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (2) - 제어의 역전 (IoC)과 ApplicationContext (0) | 2023.09.13 |