[toby's spring] 1장. 오브젝트(Object)와 의존 관계 - 1
스프링(Spring)이란?
스프링은 JVM 위에서 동작하는 어플리케이션 개발에 사용되는 application framework이다. application framework는 개발을 빠르고 효율적으로 할 수 있도록 틀과 공통 프로그래밍 모델, 기술 API 등을 제공한다.
스프링 컨테이너(spring container)
spring context라고도 불리는 스프링 런타임 엔진이다. 스프링 프레임워크의 기본 틀이다. 설정 정보를 참고로 application을 구성하는 오브젝트를 생성하고 관리한다.
공통 프로그래밍 모델
application 코드가 작성되어야 하는 기준인 공통 프로그래밍 모델을 제공한다. 세 가지 핵심 프로그래밍 모델을 지원한다.
IoC/DI
오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델이다.
서비스 추상화
스프링을 사용하면 서버, 특정 기술, 환경에 종속되지 않고 이식성이 뛰어나다. 이는 스프링의 서비스 추상화 덕분이다. 구체적인 기술과 환경에 application이 종속되지 않도록 유연한 추상 계층을 둔다.
AOP
application 코드의 부가적인 기능을 독립적으로 모듈화 하는 프로그래밍 모델이다.
스프링의 특징
- 단순함 : 가장 단순한 POJO 프로그래밍을 지향
- 유연성
1장. 오브젝트(Object)와 의존 관계
스프링은 자바를 기반으로 한 기술이므로 자바에 대해 이해하고 자바의 가치를 따라 가야 한다. 자바의 가장 큰 특징은 객체지향 프로그래밍이라는 것 이다. 그러므로 객체지향적 설계의 기초원칙과 재사용 가능한 설계 방법인 디자인 패턴, 리팩토링, 동작 검증을 위한 단위테스트 등의 응용 기술과 지식이 필요하다.
자바빈(JavaBean)
원래는 비주얼 툴에서 조작 가능한 컴포넌트를 말한다. 웹 기반의 엔터프라이즈 방식으로 바뀌며 비주얼 컴포넌트로 자바빈 보다 자바빈 스타일의 아래와 같은 오브젝트를 가리킨다.
- 디폴트 생성자(default constructor) : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 툴이나 프레임워크에서 리플렉션을 이용하여 오브젝트를 생성하기 때문이다.
- 프로퍼티(property) : 자바 빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 set으로 시작하는 수정자 메소드(setter)와 get으로 시작하는 접근자 메소드(getter)를 이용하여 수정 또는 조회할 수 있다.
DAO(Data Access Object)
DB를 사용하여 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트
package com.example.demo.user.domain;
import lombok.Data;
@Data
public class User {
private String id;
private String name;
private String password;
}
데이터베이스의 테이블의 내역을 저장하는 User 클래스를 생성하였다. 이 클래스는 RDBMS의 User 라는 테이블의 필드를 객체로 구성한 클래스이다.
이 객체의 정보가 실제로 저장되는 위치는 DB(MySQL, MSSQL, Oracle, h2 등의 RDBMS)이고, 해당 클래스는 데이터베이스에서 정보를 받아 수정하고 표출할 수 있다.
package com.example.demo.user.dao;
import com.example.demo.user.domain.User;
import java.sql.*;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo","sa","123");
PreparedStatement ps = c.prepareStatement(
"insert into user(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("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo","sa","123");
PreparedStatement ps = c.prepareStatement(
"select * from user 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클래스는 실행될 때, JDBC는 아래와 같은 동작을 한다.
- DB 연결을 위한 Connection을 가져온다.
- SQL을 담은 statement(또는 preparedStatement)를 만든다.
- 만들어진 statement를 실행한다.
- 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장한 Object에 옮긴다.
- 작업 중 생성된 Connection, Statement, ResultSet 등의 리소스 작업을 마친 후 닫는다.
- 작업 중 예외를 잡아 직접 처리하거나 throws 한다.
반복되는 리소스 작업들을 줄일 수 있다면 단순하게 코드를 줄일 수 있을 것이다.
DAO의 분리
관심사의 분리(Separation of Concerns)이라는 프로그래밍의 기초 개념을 객체지향에 적용해보면, 관심이 같은 것 끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라고 생각 할 수 있다.
- Connection 생성 추출
- 중복 코드의 메소드 추출
- 변경 사항에 대한 검증
package com.example.demo.user.dao;
import com.example.demo.user.domain.User;
import java.sql.*;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(
"insert into user(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 {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(
"select * from user 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;
}
private Connection getConnection() throws ClassNotFoundException, SQLException{
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo","sa","123");
return c;
}
}
DB Connection 생성 독립
- 상속을 통한 확장
환경에 따라 변경되는 DB 정보를 반영할 수 있는 DAO를 생성하기 위해서는 위의 DAO 코드를 한 단계 더 분리해야한다.
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(
"insert into user(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 {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(
"select * from user 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;
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
public class NUserDao extends UserDao{
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo","sa","123");
return c;
}
}
public class DUserDao extends UserDao{
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo2","sa","123");
return c;
}
}
클래스의 계층구조를 통해 두 개의 관심이 독립적으로 분리되며 변경을 용이하게 하였다. 이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브 클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메소드 패턴(template method pattern)이라고 한다.
템플릿 메소드 패턴은 스프링에서 자주 사용되는 디자인 패턴이다.
그리고, getConnection()메소드 처럼 서브 클래스에서 구체적으로 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴(Factory Method Pattern)이라고 한다.
DAO의 확장
추상 클래스를 만들고 이를 상속한 서브 클래스에서 변화가 필요한 부분을 바꾸어 쓸 수 있게 만든 이유는 변화의 성격이 다른 것을 분리하여 서로 영향을 주지 않고 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서이다.
클래스의 분리
상속을 통한 방법이 아닌 별도로 Connection이 이루어지는 클래스를 생성하고, 이를 저장하여 DB 연결이 필요할때 사용할 수 있다. 하지만 이 방법의 경우 두가지 문제점이 있다.
첫 번째, 자유로운 확장을 원하지만 메소드가 추가될때 마다 Connection 정보를 넣어주어야 한다. 수십~수백개의 기능이 생성되면 관리와 작업량이 늘어난다.
두 번째, DB 커넥션을 제공하는 클래스가 어떤 것인지 DAO가 명확하게 알고 있어야한다는 것이다. DB 커넥션 정보가 변경되면 다시 DAO 자체를 수정하게 되는 문제가 발생한다.
public class UserDao {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDao(){
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"insert into user(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 {
Connection c = simpleConnectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"select * from user 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;
}
}
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/demo","sa","123");
return c;
}
}
인터페이스의 도입과 관계설정 책임의 분리
클래스를 분리하며 위의 문제를 해결하기 위해 추산적인 느슨한 연결고리를 만들어주어야한다. 이를 위해 자바의 추상화 도구인 인터페이스를 활용한다. 인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보는 모두 은닉한다.
그러나 여전히 Connection c = connectionMaker.makeConnection() 이라는 오브젝트를 생성하는 코드가 DAO안에 남아있다.
package com.example.demo.user.dao;
import com.example.demo.user.domain.User;
import java.sql.*;
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker){
this.connectionMaker = connectionMaker;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"insert into user(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 {
Connection c = connectionMaker.makeNewConnection();
PreparedStatement ps = c.prepareStatement(
"select * from user 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;
}
}
원칙과 패턴
개방 폐쇄의 원칙(Open Closed Principle)은 깔끔한 설계를 위해 적용 가능한 객체 지향 설계 원칙 중의 하나이다.
DAO는 DB 연결 방법이라는 기능을 확장하는데 열려 있어 이를 만족한다. 대부분 인터페이스를 사용하여 확장 기능을 정의한 대부분의 API는 이 원칙을 따른다.
개발 폐쇄의 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 고전적 원리로도 설명이 가능하다. 초기 DAO에 여러 관심사와 책임이 얽혀 있던 코드에서 인터페이스를 활용하여 각각 DB 커넥션, DAO 기능에 관심사를 갖도록 변경 되었다. 또한 DB 연결 정보가 변경되어도 DAO 의 기능들은 변경되지 않음을 알 수 있다.
개선한 구조는 전략 패턴(Strategy pattern)에 해당한다고 볼 수 있다. 자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적 알고리즘 클래스를 필요에 바꾸어 따라 사용할 수 있게 하는 디자인 패턴이다.
제어의 역전(IoC)
IoC(Inversion of Control)은 스프링을 통해 일반 개발자에게 많이 알려진 용어이지만, 오래전부터 사용되던 용어이다.
오브젝트 팩토리
객체의 생성 방법을 결정하고 만들어진 오브젝트를 돌려주는 클래스를 팩토리(factory)라고 한다. 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을깔끔하게 분리하여는 목적으로 사용하는 것이다.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
UserDao dao = new DaoFactory().userdao();
User user = new User();
user.setId("sjkim");
user.setName("김수정");
user.setPassword("123");
try {
User user2 = dao.get(user.getId());
System.out.println(user2.getName() + " is found.");
}catch (ClassNotFoundException | SQLException e){
e.printStackTrace();
}
}
}
public class DaoFactory {
public UserDao userdao(){
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
DaoFactory의 DAO 메소드를 호출하여 DB 커넥션을 가져오도록 하면, 설정된 오브젝트를 돌려준다. 해당 Dao를 실행하는 코드는 더이상 UserDao의 생성/구성/초기화에 관여하지 않는다. 관심사의 분리를 완전히 진행한다.
핵심 기술인 DAO는 변경이 필요 없으므로 소스 코드를 보존할 수 있다. 동시에 DB 연결 방식은 자유로운 확장이 가능하다.
여러 개의 DAO가 생기면 이전과 같이 Connection 설정을 메소드마다 변경해야하는 일이 발생한다. 이러한 중복 문제를 위해 또 한번 분리를 진행할 수 있다.
제어권 이전을 통한 제어관계 역전
제어의 역전이라는 것은 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다.
제어 역전에서는 객체가 자신이 사용할 객체를 스스로 선택하지도, 생성하지도 않는다. 또한, 객체 자신이 어떻게 만들어지고 사용되는지 알 수 없고 모든 제어의 권한이 다른 대상에게 위임하게 된다. main과 같은 엔트리 포인트를 제외하면 모든 객체는 이렇게 위임받은 제어 권한을 갖는 특별한 객체에 의해 결정되고 만들어진다.
프레임워크는 제어의 역전 개념이 적용된 대표적인 기술이다. application 코드는 프레임 워크에 의해 사용이된다. 개발자가 만든 코드를 프레임 워크가 사용하게 된다. 예제의 코드의 경우 DaoFactory에서 데이터베이스를 선택하는 권한을 위임하게 되어있다. 이로인해 Dao는 팩토리에 의해 수동적으로 만들어지고 공급되는 정보로 수동적으로 동작하게된다. 이를 통해 제어의 역전을 볼 수 있다.
스프링의 IoC
오브젝트 팩토리를 이용한 스프링 IoC
스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(Bean)이라고 한다. 스프링 빈은 스프링 컨테이너가 생성과 관계 설정, 사용 등을 제어해주는 IoC가 적용된 오브젝트를 일컫는다. 빈 생성과 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 한다. 애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리 라고 생각하면 된다.
애플리케이션 컨텍스트는 별도의 정보를 참고하여 빈의 생성, 관계설정 등의 작업을 총괄한다.
애플리케이션 컨텍스트의 동작방식
기존의 오브젝트 팩토리에 대응 되는 것이 스프링의 애플리케이션 컨텍스트이다. 스프링에서는 이 애플리케이션을 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너 혹은 빈 팩토리라고 부를 수 있다.
이전에는 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 DaoFactory가 실행하였다. 애플리케이션 컨텍스트에서는 애플리케이션에서 IoC를 적용하여 모든 오브젝트에 대한 생성과 관계설정을 담당한다. 그로인해 DaoFactory와 같이 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없다. DaoFactory와 같은 오브젝트 팩토리에서 사용했던 IoC원리를 그대로 적용하지만 이에비해 범용적이고 유연하여 기능을 확장하기 용이하다. 장점은 아래와 같다.
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
- 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
- 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
스프링 IoC의 용어 정리
- 빈(Bean)
스프링이 IoC 방식으로 관리하는 오브젝트이다. 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 한다.
- 빈 팩토리(Bean Factory)
스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록/생성/조회 등의 관리하는 기능을 담당한다.
- 애플리케이션 컨텍스트(application context)
빈 팩토리를 확장한 IoC 컨테이너이다. 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일하다. 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다.
- 설정정보/설정 메타정보(configuration metadata)
애플리케이션 컨텍스트 혹인 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다.
- 컨테이너 또는 IoC 컨테이너
IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고 한다.