들어가며 : 스프링이란 무엇인가 ?
- 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크
- 기본 틀 - 스프링 컨테이너
- 스프링은 스프링 컨테이너 (또는 애플리케이션 컨텍스트) 라고 불리는 스프링 런타임 엔진을 제공
- 스프링 컨테이너는 설정 정보를 참고로 해서 애플리케이션을 구성하는 오브젝트를 생성하고 관리
- 스프링 컨테이너는 독립적으로 동작할 수도 있지만 보통 웹 모듈에서 동작하는 서비스나 서블릿으로 등록해서 사용
- 공통 프로그래밍 모델 - IoC/DI, 서비스 추상화, AOP
- 1) IoC/DI : 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델으로 스프링 프레임워크에 동작하는 코드는 IoC/DI 방식을 따라서 작성돼야 스프링이 제공하는 가치를 제대로 누릴 수 있다.
- 2) 서비스 추상화 : 스프링을 사용하면 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있는데 이 때 환경에 종속되지 않도록 유연한 추상 계층을 둘 수 있다.
- 3) AOP : 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델이다.
- 기본 틀 - 스프링 컨테이너
1장. 오브젝트와 의존관계
1.1 초난감 DAO
- 사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 하나 만들어보자.
User.java
package springbook.user.domain;
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;
}
}
UserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
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?characterEncoding=UTF-8", "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?characterEncoding=UTF-8", "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 ClassNotFoundException, SQLException {
UserDao dao = new UserDao();
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
1.2 DAO의 분리
1.2.1 관심사의 분리
- 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끈임없이 발전하고 바뀐다.
- 그래서 개발자가 객체를 설계할 때 가장 중요한 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다.
- 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.
- ex) 단지 DB 접속용 암호를 변경하려고 DAO 클래스 수백 개를 모두 수정해야 한다면 ?
분리와 확장을 고려한 설계가 있어야 한다.
프로그래밍의 기초 개념 중에 관심사의 분리 Seperation of Concerns 라는 게 있다. 이를 객체지향에 적용하면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리해야 한다.
1.2.2 커넥션 만들기의 추출
UserDao의 관심사항
- DB 연결을 위한 커넥션을 어떻게 가져올까
- 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement을 만들고 실행하는 것
- 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것
DB 연결을 위한 커넥션을 어떻게 가져올까 => 중복 코드의 메소드 추출 extract method 기법
extract method 기법
- DB 종류와 접속 방법이 바뀌거나 로그인 정보가 변경돼어도 getConnection 한 메소드만 수정하면 된다.
1.2.3 DB 커넥션 만들기의 독립
템플릿 메소드 패턴 template method pattern
팩토리 메소드 패턴 factory method pattern
- 핵심 기능인 데이터를 등록하고 가져오는 기능은 UserDao가,
- DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분이 되고 있다.
// UserDao.java
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
}
}
// NUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class NUserDao extends UserDao {
protected Connection getConnection() throws ClassNotFoundException,
SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8",
"spring", "book");
return c;
}
}
// DUserDao.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DUserDao extends UserDao {
protected Connection getConnection() throws ClassNotFoundException,
SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8",
"spring", "book");
return c;
}
}
1.3 DAO의 확장
1.3.1 클래스의 분리
package springbook.user.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public abstract class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
this.simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = this.simpleConnectionMaker.getConnection();
.....
}
SimpleConnectionMaker.java
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class SimpleConnectionMaker {
public Connection getConnection() throws ClassNotFoundException,
SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/springbook?characterEncoding=UTF-8", "spring", "book");
return c;
}
}
- 두 가지 문제를 해결해야 한다.
- 첫째는 SimpleConnectionMaker에서 DB 커넥션을 가져오는 함수가 변경이 된다면 add(), get() 메소드 커넥션을 가져오는 코드를 일일이 변경해야 한다.
- 두번째는 DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다.
문제의 근본적인 원인은 UserDao가 바뀔 수 있는 정보, 즉 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다.
어떤 클래스가 쓰일지, 그 클래스에서 가져오는 메소드는 이름이 뭔지까지 일일이 알고 있어야 한다.
1.3.2 인터페이스의 도입
- 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다.
인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다.
UserDao가 인터페이스를 사용하게 한다면 인터페이스의 메소드를 통해 알 수 있는 기능에만 관심을 가지면 되지, 그 기능을 어떻게 구현했는지에는 관심을 둘 필요가 없다.
1.3.3 관계설정 책임의 분리
- 인터페이스를 써가면서 완벽하게 분리했는데도, UserDao가 구체적인 클래스를 알아야 한다는 문제가 발생한다.
- UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 존재한다.
- 이 관심사를 담은 코드를 UserDao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장 가능한 클래스가 될 수 없다.
- UserDao의 클라이언트라고 하면 UserDao를 사용하는 오브젝트를 가리킬 수 있다.
- 바로 이 클래스가 UserDao와 ConnectionMaker 구현 클래스의 관심 관계를 결정해주는 기능을 분리해서 두기에 적절한 곳이다.
- 오브젝트 사이에 런타임 사용관계 또는 링크, 또는 의존관계라고 불리는 관계를 맺어주면 된다.
package springbook.user.dao;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao dao = new UserDao(connectionMaker);
...
}
}
- 이렇게되면 이제부터 UserDao는 자신의 관심사이자 책임인 제이터 엑세스 작업을 위해 SQL 생성하고, 이를 실행하는 데만 집중할 수 있다.
1.3.4 원칙과 패턴
개방 폐쇄 원칙 OCP, Open-Closed Principle
- 클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
- UserDao는 DB 연결 방법이라는 기능을 확장하는 데는 열려 있다. => UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있다.
- 동시에 UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀 있다고 말할 수 있다.
높은 응집도와 낮은 결합도 High Coherence And Low Coupling
- 높은 응집도
- 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.
- 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다.
- 만약 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체에서 어떤 부분이 바뀌어야 하는지 파악해야 하고,
- 또 그 변경으로 인해 바뀌지 않는 부분에는 다른 영향을 미치지는 않은지 확인해야 하는 이중의 부담이 생긴다.
- 낮은 결합도
- 높은 응집도보다 더 민감한 원칙이다.
- 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결함도를 유지하는 것이 바람직하다.
전략 패턴
- 개선한 UserDaoTest - UserDao - ConnectionMaker 구조를 디자인 패턴의 시각으로 보면 전략 패턴이다.
- 전략 패턴은 자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴
1.4 제어의 역전 (IoC)
1.4.1 오브젝트 팩토리
- 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 팩토리 Factory 라고 한다.
// UserFactory.java
package springbook.user.dao;
public class UserDaoFactory {
public UserDao userDao() {
UserDao dao = new UserDao(connectionMaker());
return dao;
}
...
}
// UserDaoTest.java
package springbook.user.dao;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
UserDao dao = new UserDaoFactory().userDao();
...
}
}
1.4.2 오브젝트 팩토리의 활용
- AccountDao, MessageDao를 만든다고 했을때, ConnectionMaker 구현 클래스의 오브젝트를 생성하는 코드가 메소드마다 반복되게 된다.
- extract method 기법을 써서 중복된 함수를 뽑아낸다.
1.4.3 제어권의 이전을 통한 제어관계 역전
제어의 역전 Inversion Of Control
- 제어의 역전이라는 건, 간단히 프로그램 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다.
- 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다.
- 또 자신이 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다.
- 프레임워크도 제어의 역전 개념이 적용된 대표적인 기술이다.
- 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다.
- 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다.
1.5 스프링의 IoC
1.5.1 오브젝트 팩토리를 이용한 스프링 IoC
애플리케이션 컨텍스트와 설정정보
- 스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈 bean 이라고 부른다.
- 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다.
- 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리 bean factory 라고 부른다.
- 보통 bean factory는 좀 더 확장한 애플리케이션 컨텍스트 application context 라고도 한다.
- application context는 IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 생각하면 된다.
DaoFactory를 사용하는 애플리케이션 컨텍스트
- 먼서 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration이라는 애노테이션을 추가한다. 그리고 오브젝트를 만들어주는 메소드에는 @Bean이라는 애노테이션을 붙여준다.
package springbook.user.dao;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao dao = new UserDao(connectionMaker());
return dao;
}
@Bean
public ConnectionMaker connectionMaker() {
ConnectionMaker connectionMaker = new DConnectionMaker();
return connectionMaker;
}
}
- 그리고 @Configuration이 붙은 자바코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.
package springbook.user.dao;
import java.sql.SQLException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import springbook.user.domain.User;
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);
...
}
}
1.5.2 애플리케이션 컨텍스트의 동작방식
- 오브젝트 팩토리에 대응되는 것이 스프링의 애플리케이션 컨텍스트다.
- 스프링에서는 이 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너 라고도 한다. 또는 빈 팩토리라고 부를 수도 있다.
- DaoFactory가 UserDao를 비롯한 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 하는 데 반해, 애플리케이션 컨텍스트는 애플리케이션 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다.
- 장점은 다음과 같다.
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
- 애플리케이션 컨텍스트를 사용하면 오브젝트 팩토리가 아무리 많아져도 이를 알아야 하거나 직접 사용할 필요가 없다.
- 애플리케이션 컨텍스트를 이용하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
- 또, DaoFactory 처럼 자바 코드를 작성하는 대신 XML 처럼 단순한 방법 사용해 애플리케이션 컨텍스트가 사용할 IoC 설정정보를 만들 수도 있다.
- 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
- 단지 오브젝트 생성과 다른 오브젝트의 관계설정만이 전부가 아니라, 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 부가적으로 자동생성, 후처리 등 다양한 기능을 제공한다.
- 애플리케이션 컨텍스트는 빈을 검색한 다양한 방법을 제공한다.
- getBean() 메소드는 빈의 이름을 이용해 빈을 찾아줄 수도 있고, 타입만으로 빈을 검색하거나 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수 있다.
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
1.5.3 스프링 IoC의 용어 정리
- 빈 bean
- 스프링이 IoC 방식으로 관리하는 오브젝트
- 주의할 점은 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니고, 그 중에서 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈 bean 이라고 한다.
- 빈 팩토리 bean factory
- 스프링 IoC를 담당하는 핵심 컨테이너
- BeanFactory라고 붙여쓰면 빈 팩토리가 구현하는 가장 기본적인 인터페이스의 이름이 된다.
- 애플리케이션 컨텍스트 application context
- 빈 팩토리를 확장한 IoC 컨테이너
- 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일하다.
- 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다.
- 설정정보/설정 메타정보 configuration metadata
- 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보
- IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용한다.
- 컨테이너 또는 IoC 컨테이너
- IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고 한다.
- 컨테이너라는 말 자체가 IoC의 개념을 담고 있기 때문에 이름이 긴 애플리케이션 컨텍스트 대신에 스프링 컨테이너라고 부르기를 선호한다.
- 스프링 프레임워크
- IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다.
1.6 싱글톤 레지스트리와 오브젝트 스코프
DaoFactory를 직접 사용하는 것과 @Configuration 애노테이션을 추가해서 스프링의 애플리케이션 컨텍스트 사용하는 것 비교
- DaoFactory의 userDao()를 여러번 호출했을때 동일한 오브젝트일까
- 코드를 보면 매번 new 연산자에 의해 돌아올 것이라고 예상할 수 있다.
- userDao를 매번 호출하면 계속해서 새로운 오브젝트가 만들어진다.
- 스프링의 애플리케이션 컨텍스트를 사용하면 오브젝트가 동일한 것을 알 수 있다.
1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트
- 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다.
- 왜 스프링은 싱글톤으로 빈을 만드는 것일까 ?
- 이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.
- 매번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만든다면, 부하가 걸릴수가 있다.
- 싱글톤 구현 방식
- 클래스 밖에서는 객체를 생성하지 못하도록 생성자를 private으로 만든다.
- 생성된 싱글톤 객체를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
- 스태틱 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한 번만 객체를 생성하게 한다. 생성된 객체는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 객체를 미리 만들어 둘 수도 있다.
- 한 번 객체(싱글톤)가 만들어지고 난 후에는 getInstance() 메서드를 통해 이미 만들어진 스태틱 필드에 저장된 객체를 넘겨준다.
package springbook.user.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import springbook.user.domain.User;
public class UserDao {
private static UserDao INSTANCE;
public UserDao(ConnectionMaker simpleConnectionMaker) {
this.connectionMaker = simpleConnectionMaker;
}
public static synchronized UserDao getInstance) {
if (INSTANCE == null) INSTANCE = new UserDao(??);
return INSTANCE;
}
}
토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main
GitHub - AcornPublishing/toby-spring3-1: 토비의 스프링 3.1
토비의 스프링 3.1. Contribute to AcornPublishing/toby-spring3-1 development by creating an account on GitHub.
github.com
토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358
토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 이일민 - 교보문고
토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 대한민국 전자정부 표준 프레임워크 스프링을 설명하는 No. 1 베스트셀러! 단순한 예제를 스프링 3.0과 스프링 3.1의 기술을 적용하며 발전시켜 나
product.kyobobook.co.kr