이 글은 요약 정리이며, 상세한 정리는 여기 에 있다.
1.1 초난감 DAO
public class UserDao {
public void add(User user) throws SQLException, ClassNotFoundException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
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 SQLException, ClassNotFoundException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
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;
}
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new UserDao();
User user = new User();
user.setId("1");
user.setName("제이크");
user.setPassword("jakejake");
dao.add(user);
System.out.println(user.getId() + " register succeeded");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " query succeeded");
}
}
초난감 DAO의 내용은 위의 코드로 설명된다.
- 각 메서드 내부에 핵심 로직이 아닌 부가 로직을 적용시켜 중복된 코드가 가득하다.
- 연결될 DB의 주소 혹은 드라이버가 바뀌어야 한다면?
- 모든 메서드를 찾아가며, DB 연결에 해당하는 모든 로직을 변경해야 한다.
- 연결될 DB의 주소 혹은 드라이버가 바뀌어야 한다면?
main
메서드에서 테스트를 하고 있다.- 많은 테스트를 진행할 때는 어떻게 해야 할까?
- 매번 썼다 지웠다 할 수는 없는 노릇이다.
- 많은 테스트를 진행할 때는 어떻게 해야 할까?
코드 작성 시 중복으로 나오게 되는 코드인 공통 부분(부가 로직)에 대한 고려가 전혀 되지 않았다.
1.2 DAO의 분리
변화에 대응하는 방법
- 객체지향 언어는 변화에 대응할 준비가 되어있다.
- 상속, 다형성 등이 그러한 특성을 가지고 있다.
- 분리와 확장을 미리 고려해야 한다.
- 한 관심사가 변경되었을 때, 한 곳만 수정해도 되는 것이 이상적이다.
- 즉, 관심사별로 코드를 분리(결합도는 낮추고)해두고 관심사에 맞게 모아두는(응집도는 높이고) 것이 좋다.
DB의 접속정보와 같은 것은 언제든 변경될 수 있다.
변경될만한 코드를 당장에 다 알 수는 없다. 개발 과정을 거치며 자주 변경되는 부분을 파악해가며 리팩토링하는 방법도 좋다.
변화에 대응하기 - 중복 코드를 메서드로 추출하는 방법
public void add(User user) throws SQLException, ClassNotFoundException {
// 1.2.2 중복 코드의 메소드 추출
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();
}
public User get(String id) throws SQLException, ClassNotFoundException {
// 1.2.2 중복 코드의 메소드 추출
Connection c = getConnection();
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;
}
// 커넥션 가져오기 관심사
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
}
Connection
이라는 객체를 생성하는 부분이 중복되었다.- 코드의 중복을 메서드 형태로 추출하여 관심사를 분리할 수 있다.
- 언어에 구애받지 않고 어느 언어에서나 적용 가능한 방식이다.
리팩토링(코드 변화) 이후에 할 일
- 리팩토링 이후에는 반드시 테스트가 필요하다.
변화에 대응하기 - 상속을 통해 확장하기
public abstract class UserDao {
public void add(User user) throws SQLException, ClassNotFoundException {
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();
}
public User get(String id) throws SQLException, ClassNotFoundException {
Connection c = getConnection();
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;
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
- 변화가 일어나는 부분만 추상 메서드로 지정하여 추상 클래스를 만들어놓으면 추후 중복되는 부분에 대한 코드 재작성 없이 변화가 일어나는 부분만 작성하여 코드를 확장할 수 있다.
상속을 통한 확장 - 템플릿 메서드 패턴
- 디자인 패턴의 한 종류이다.
- 기본 로직 흐름을 만든다.
- 슈퍼클래스에 반드시 구현해야 하는 추가되거나 변경될 수 있는 로직을 추상 메서드로 작성해둔다.
- 선택적으로 작성할 수 있는 로직은
protected
메서드로 작성해둔다.
- 선택적으로 작성할 수 있는 로직은
- 이렇게 만들어놓은 추상 클래스를 상속하여 확장할 수 있다.
public abstract class Super {
public void templateMethod() {
// 기본 알고리즘 코드
hookMethod(); // 서브 클래스에서 선택적으로 작성한다.
abstractMethod(); // 서브 클래스에서 필수적으로 작성한다.
...
}
protected void hookMethod() {} // 서브 클래스에서 선택적으로 오버라이드 가능
public abstract void abstractMethod() {} // 서브 클래스에서 반드시 구현해야 하는 추상 메소드
}
public class Sub1 extends Super {
protected void hookMethod() {
...
}
public void abstractMethod() {
...
}
}
선택적으로 작성 가능한 메서드를
hookMethod
(훅 메서드)라고 한다.abstractMethod
전에 수행하고 싶은 로직이 있다면, 선택적으로 작성하면 된다.
상속을 통한 확장 - 팩토리 메서드 패턴
- 특정한 타입의 객체를 생성할 것이라는 정보를 갖고 있는 추상 클래스만 만든다.
- 구체적인 타입의 객체 생성 로직은 해당 추상 클래스를 상속하는 자식 클래스가 담당한다.
- 이전
UserDao
코드에서Connection
을 반환하는 부분만 위임했던 코드와 흡사하다.- 정확히 어떤
Connection
을 생성할지는 서브 클래스에게 맡긴다. - 접속정보가 바뀌어도 올바른
Connection
만 구현한다면, 코드가 유연하게 동작하게 된다.
- 정확히 어떤
객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 하는 것
자세한 예제: https://jdm.kr/blog/180
디자인 패턴의 문제해결 방식
- 주로 객체지향 설계의 특성을 이용하여 문제를 해결한다.
- 디자인 패턴의 설계 구조는 생각보다 비슷하다.
클래스 상속
,오브젝트 합성
보통 두 키워드로 끝이 난다.- 우리는 여태까지 클래스 상속 방식으로 문제를 해결해보았다.
템플릿 메서드 패턴
,팩토리 메서드 패턴
- 디자인 패턴에서 가장 중요한 것은 핵심 의도와 목적이다.
클래스 상속 방식으로 문제를 해결하는 것의 단점
- 단 하나의 클래스만 상속할 수 있다.
- 자바는 다중 상속을 지원하지 않는다.
- 상하위 클래스 관계가 생각보다 밀접하다.
- 슈퍼 클래스의 변경이 있을 때 그 여파가 생각보다 크다.
- 슈퍼 클래스에 새로운 메서드가 추가된다면? 혹은 기존 메서드의 기능이 변경된다면? 서브클래스를 함께 수정해야 할 수도 있다.
- 당장만 해도 다른 클래스에
UserDao
를 상속하여 사용한다면, 상속하는 모든Dao
에getConnection()
을 직접 구현해야 한다.
1.3 DAO의 확장
UserDao의 너무 많은 관심사
UserDao
는 사실 어떤 DB를 사용하는지 관심 없다.- DB에서 데이터만 가져오면 된다.
- 그렇다면, DB를 연결하는 부분은 아예 다른 클래스로 분리해보자.
커넥션을 다른 클래스로 분리
public class UserDao {
// 상속의 단점을 해결하고자, 상속을 피하고 클래스를 나누는 방식으로 해결 시도.
// 커넥션 때문에 상속을 적용하면 추후에 다른 이유로 상속을 할 수 없음. (다중상속을 지원 안 함)
// 슈퍼 클래스가 변경되면 하위 클래스를 모두 변경해야 함.
// 애초에 변화가 있어도 독립적으로 서로 영향을 주지 않는 디자인을 원했는데, 상속 자체가 변화를 불편하게 만듦.
// 위와 같은 이유 때문에 클래스를 분리함
SimpleConnectionMaker simpleConnectionMaker;
public UserDao() {
this.simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) throws SQLException, ClassNotFoundException {
Connection c = simpleConnectionMaker.makeNewConnection();
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 SQLException, ClassNotFoundException {
Connection c = simpleConnectionMaker.makeNewConnection();
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;
}
}
public class SimpleConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "iwaz123!@#";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
return c;
}
}
- 상속 말고 이렇게
오브젝트를 합성
(다른 오브젝트를 사용)해서 문제를 해결할 수도 있다. - 다만, 이렇게 되면 이 오브젝트는 단 하나의 클래스의 구현에 의존하게 된다.
- 인터페이스를 도입한다면, 더 유연해질 것이다.
인터페이스 도입
// 1.3.2 인터페이스의 사용, 1.3.3 관계 설정의 책임의 분리를 이용하여
// UserDao 는 생성자에서 해당 인터페이스를 주입 받는 방식으로 변경됨
// 어떤 Connection 을 이용할 것인지는 UserDao 의 관심사가 아니라 클라이언트의 관심사가 됨
// UserDao 는 SimpleConnectionMaker 라는 인터페이스에 맞는 오브젝트로 Connection 을 생성하는 것만이 관심사
// 구체적으로 어떤 SimpleConnectionMaker 가 들어올 것인지에 대해서는 관심이 없다.
// 이렇게 구현함으로써 훨씬 유연해진다.
public interface SimpleConnectionMaker {
Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}
public UserDao(SimpleConnectionMaker simpleConnectionMaker) {
this.simpleConnectionMaker = simpleConnectionMaker;
}
이제 UserDao
객체는 생성자의 인자로 SimpleConnectionMaker
인터페이스를 받게 된다. 인터페이스 타입을 받기 때문에 어떤 클래스라도 이 인터페이스를 구현했다면, 받을 수 있다.
Naver
의 커넥션에도 연결할 일이 있고,Daum
의 커넥션에도 연결할 일이 있다면, 각각의 커넥션을 구현한 클래스NConnectionMaker
와DConnectionMaker
를 준비해두고 바꿔끼며 사용해도 문제가 없다.
원칙과 패턴
- 앞서 설명한 구현은 사실 잘 알려진 원칙을 따른 것이다.
개방 폐쇄 원칙 (OCP, Open-Closed Principle)
- 확장엔 열려있어야 하고 변경엔 닫혀있어야 한다.
- 이전의 사례로 설명하면
UserDao
는 인터페이스를 구현한 어떤 구현체든 생성자에 주입할 수 있으니 확장엔 열려있다.UserDao
의 코드를 직접 수정하지 않아도, 연결 정보를 변경할 수 있으니 변경엔 닫혀있다.
단순히 인터페이스를 구현한 클래스를 주입받음으로써 OCP를 지켰다.
높은 응집도
- 어떤 한 관심사를 한 클래스에 모아놓는 것이다.
- ex)
DB Connection
을 생성하는 부분을ConnectionMaker
클래스에 몰아놓는다.
- ex)
- 변화가 일어날 때 해당 모듈에서만 변화가 일어나야 한다.
- 다른 클래스에 영향을 주지 않아야 한다.
낮은 결합도
- 느슨하게 연결된 형태가 설계상 더 선호된다는 것이다.
- 클래스에서 다른 클래스를 사용할 때, 구체적인 클래스보다는 인터페이스를 사용하는 것이 느슨한 결합에 유리하다.
전략 패턴
- 이전에 클래스였던
SimpleConnectionMaker
를 인터페이스SimpleConnectionMaker
로 변화시킨 것은 전략 패턴이라고 할 수 있다.- 자신의 기능 컨텍스트에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 해주는 디자인 패턴이다.
- 이 인터페이스를 구현하는 것을 구체적인 전략이라고 보기 때문에 이름이 전략패턴이다.
- 자신의 기능 컨텍스트에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 해주는 디자인 패턴이다.
1.4 제어의 역전
오브젝트 팩토리(DaoFactory) 도입
- 팩토리는 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 반환하는 것이다.
- 추상 클래스나 인터페이스를 작성하고 구체적인 클래스 생성을 서브 클래스에 위임하는 의도를 갖는 팩토리 메서드 패턴과 같은 팩토리라는 용어를 쓰지만 명백히 다르므로 유의하자.
팩토리는 단순히 오브젝트 생성하는 부분과 생성된 오브젝트를 사용하는 부분을 명확히 나누기 위해 존재한다.
public class DaoFactory {
public UserDao userDao() {
return new UserDao(new DSimpleConnectionMaker());
}
}
public class UserDaoTest {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new DaoFactory().userDao();
User user = new User();
user.setId("1");
user.setName("제이크");
user.setPassword("jakejake");
dao.add(user);
System.out.println(user.getId() + " register succeeded");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " query succeeded");
}
}
팩토리 도입 이후에 달라진 설계
UserDao
: 사용자 데이터 관련 로직에 관한 책임ConnectionMaker
: DB 연결 기술에 관한 책임UserDaoTest
: 동작을 테스트하는 책임DaoFactory
: 오브젝트를 구성하고 관계를 정의하는 책임- 애플리케이션 컴포넌트 역할을 하는 오브젝트와 애플리케이션 구조를 결정하는 오브젝트를 분리했다.
팩토리 확장해보기
public class DaoFactory {
public UserDao userDao() {
return new UserDao(getConnectionMaker());
}
public MessageDao messageDao() {
return new MessageDao(getConnectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(getConnectionMaker());
}
private DSimpleConnectionMaker getConnectionMaker() {
return new DSimpleConnectionMaker();
}
}
DaoFactory
를 좀 더 확장하면 위와 같은 모양이 될 수 있다. 구체적으로 어떤 클래스를 사용하는지 설계하는 설계도의 역할을 담당한다.
팩토리로 배워보는 제어관계 역전
- 이전까지는 클라이언트가 직접 어떤 클래스를 사용할지 구체적인 클래스를 결정했다.
- 이제는 팩토리가 '대신' 어떤 클래스를 사용할지 구체적인 클래스를 결정한다.
- 클라이언트는 모든 제어 권한을
*Factory
에게 위임한 것과 같다.
서블릿 관점에서의 제어의 역전
- 스프링을 통해 웹을 배포하면, 보통 개발자가 직접 모든 로직을 작성하지 않는다.
- 그냥 컨트롤러를 통해 특정 URL에 사용자가 접근한다면, 어떤 로직을 실행하거나 어떤 페이지를 반환하는 정도이다.
- 내부적으로는 서블릿이 컨테이너를 갖고 있어서 웹서버와 소켓을 만들어 통신한다.
- 동적인 웹페이지 생성을 가능하게 해준다.
템플릿 메서드 패턴 관점에서의 제어의 역전
- 서브 클래스에서 작성한 메서드는 상위 클래스가 지정한 흐름에서 호출된다.
- 템플릿 메서드 패턴은 제어의 역전 개념을 이용해 문제를 해결한다.
프레임워크 관점에서의 제어의 역전
- 프레임워크라 불리는 것들은 이미 만들어진 틀이 있고, 거기에 개발자가 원하는 로직을 주입한다. 개발자가 주입한 로직은 프레임워크에서 지정한 타이밍에 실행된다.
제어의 역전과 스프링 프레임워크
- 스프링 프레임워크는 제어의 역전을 이용하여 깔끔한 설계, 높은 유연성을 얻는 것을 목표로 한다.
- 제어의 역전은 복잡한 것이 아니라 단순히 폭 넓게 사용되는 프로그래밍 모델이다.
스프링의 IoC(제어의 역전)
오브젝트 팩토리를 이용한 스프링 제어의 역전
- 이전
UserDaoFactory
클래스 처럼 스프링도Factory
를 가지고 있다.- 스프링이 가진
Factory
를BeanFactory
라 부른다.
- 스프링이 가진
- 스프링은
BeanFactory
를 통해 생성한 객체를Bean
이라고 부른다. BeanFactory
를 조금 더 확장하면ApplicationContext
가 된다.- 이는 복잡한 개념이 아니라, 제어의 역전을 따르는
BeanFactory
일 뿐이다.
- 이는 복잡한 개념이 아니라, 제어의 역전을 따르는
DaoFactory를 BeanFactory의 설정정보로 만들어보기
@Configuration // `애플리케이션 컨텍스트` 혹은 `빈 팩토리`가 사용할 설정 정보라는 표시이다.
public class DaoFactory {
@Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
public UserDao userDao() {
return new UserDao(getConnectionMaker());
}
@Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다.
public DSimpleConnectionMaker getConnectionMaker() {
return new DSimpleConnectionMaker();
}
}
- 클래스에
@Configuration
애노테이션을 붙인다. - 객체 생성 메서드에
@Bean
애노테이션을 붙인다. - 위와 같이 애노테이션을 붙여주면, 스프링 컨테이너는 이를 자동으로 인식하여 스프링 컨테이너에
Bean
으로서 올려준다.
DaoFactory 빈 활용하기
public class UserDaoTest {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao = applicationContext.getBean("userDao", UserDao.class);
// 아래 주석처리된 방법과 같이 작성해도 무관하다.
// UserDao userDao = applicationContext.getBean(UserDao.class);
User user = new User();
user.setId("2");
user.setName("제이크2");
user.setPassword("jakejake");
userDao.add(user);
System.out.println(user.getId() + " register succeeded");
User user2 = userDao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " query succeeded");
}
}
AnnotationConfigApplicationContext()
라는 객체를 만듦으로써, 활용가능하다.- 인자로 이전에 작성해둔
DaoFactory.class
를 주면 된다.
- 인자로 이전에 작성해둔
오브젝트 팩토리와 ApplicationContext 비교하기
ApplicationContext
는BeanFactory
를 상속받아 확장시킨 것이다.- 단순히 객체 생성만 하는 것이 아니라 제어의 역전이 적용된 형태이다.
ApplicationContext의 장점
- 클라이언트가 구체적인 팩토리 클래스를 알 필요 없다.
- 일관된 방식으로 원하는 클래스를 가져올 수 있다.
- 그저 상위 타입의 인터페이스를 가져온다고 명시만 하면 구현체에 상관없이 해당하는 타입을 가져올 것이다.
- 일관된 방식으로 원하는 클래스를 가져올 수 있다.
Factory
처럼 오브젝트를 단순히 생성해주는 것이 아니라 종합 IoC(제어의 역전) 서비스를 제공해준다.- 오브젝트가 만들어지는 방식을 설정하는 방법
- 시점과 전략을 다르게 가져가는 방법
- 오브젝트 자동 생성
- 오브젝트 후처리
- 정보의 조합
- 설정방식 다변화
- 인터셉팅 등 다양한 기능을 제공한다.
- 일단 원하는 객체를 빈으로 등록해놓으면 다양한 방법으로 가져올 수 있다.
- 빈의 이름으로 검색
- 빈의 타입으로 검색
- 특정 애노테이션이 설정된 빈 검색 등 다양한 방식으로 빈을 가져올 수 있다.
스프링 IoC 용어 정리
빈(Bean, 스프링 빈)
: 스프링이 생성과 제어를 담당하는 객체빈 팩토리(Bean Factory)
: 스프링 핵심 컨테이너로 빈 생성, 등록, 조회, 반환 등 부가적인 빈 관리 기능을 담당한다.- 보통
빈 팩토리(Bean Factory)
그 자체를 사용하기보다 이를 확장한ApplicationContext
를 사용한다.
- 보통
ApplicationContext
빈 팩토리를 확장한 IoC 컨테이너이다. 기본적으로 인터페이스이며, 이를 구현한 구체 클래스가 있다.- 빈 팩토리는 빈의 생성과 제어 관점에서 쓰이는 용어이고, 애플리케이션 컨텍스트는 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함하는 용어이다.
Configuration Metadata
: 보통@Configuration
안에 담겨ApplicationContext
혹은BeanFactory
가 사용하는 메타정보이다.Container
:ApplicationContext
,BeanFactory
와 같은 의미이다.스프링 프레임워크
:ApplicationContext
,BeanFactory
,IocContainer
를 포함하여 스프링이 포함하는 모든 기능을 통틀어 말하는 것이다.
1.6 싱글톤 레지스트리와 오브젝트 스코프
스프링 빈을 가져오면, 매번 동일한 오브젝트를 돌려준다.
동일하다는 것은 객체의 주소 값까지 똑같다는 것이다. 이렇게 매번 새로운 오브젝트를 만들지 않고, 동일한 오브젝트를 사용하는 디자인 패턴을 우리는 싱글톤 패턴이라고 한다.
왜 매번 만들지 않고 동일한 오브젝트를 돌려주도록 설계했을까?
서버 환경의 특성에 잘맞는 싱글톤
- 웹서버에는 동시에 수많은 접속자가 접속할 수 있다.
- 만일 싱글톤 방식을 이용하지 않는다면, 이용자가 들어올 때마다 매번 새로운 객체를 만들고 이용자가 나가면 제거해야 한다.
- 엔터프라이즈 분야에서는
서비스 오브젝트
라는 개념이 있어서 매번 오브젝트를 생성하지 않고 만들어둔 하나의 오브젝트를 공유해 사용해왔다.
싱글톤의 치명적인 단점들
싱글톤 구현의 예.java
public class UserDaoSingleton {
private static UserDaoSingleton INSTANCE;
private UserDaoSingleton() {
}
public static synchronized UserDaoSingleton getInstance() {
if(INSTANCE == null) INSTANCE = new UserDaoSingleton();
return INSTANCE;
}
}
위는 싱글톤 디자인 패턴을 구현한 예이다. 생성자가
private
접근 제한자여서 외부에서new
로 인스턴스를 생성할 수 없다. 오직getInstance()
메서드만 이용해야 한다.
- 상속이 불가능하다.
- 생성자의 접근자가
private
이라 다른 생성자가 없다면 상속이 불가능하다. static
필드와 메서드를 사용하는 것도 문제이다.- 상속을 사용할 수 없으므로, 객체지향의 큰 장점을 잃는다.
- 생성자의 접근자가
- 테스트가 힘들다.
- 초기화 과정에서 필요한 오브젝트 등을 다이나믹하게 주입하기 어렵다.
- 오직
getInstance()
를 이용해 불러와지는 한 순간에 파라미터 없이 생성되어야 한다.
- 오직
- 초기화 과정에서 필요한 오브젝트 등을 다이나믹하게 주입하기 어렵다.
- 서버 환경에서 싱글톤을 보장하기 힘들다.
- 서버 환경은 단순한 자바 환경과는 다르다. 몇 개의 다른 서버를 묶어서 분산처리를 할 수도 있는데, 이럴 때 싱글톤이 단 하나만 만들어졌는지 보장하기 어렵다.
- 전역 상태를 만들 수 있어서 바람직하지 못하다.
- 아무 객체나 자유롭게 접근하고 수정하는 건 객체지향과 전혀 맞지 않는다.
- 관심사를 분리해야 하는데, 마음대로 다른 관심사를 건드려버리면 안 된다.
- 아무 객체나 자유롭게 접근하고 수정하는 건 객체지향과 전혀 맞지 않는다.
싱글톤 레지스트리
- 싱글톤 레지스트리는 싱글톤의 문제를 스프링이 나름의 방식으로 해결해본 것이다.
- 싱글톤 레지스트리는 이전에 설명했듯, 빈을 스프링 컨테이너에 올려두고 갖다 쓰는 것을 말한다.
싱글톤 레지스트리 장점
- 평범한 자바 클래스를 싱글톤으로 활용할 수 있다.
static
,private 생성자
등을 이용할 필요가 없다.
- 테스트에서 싱글톤 방식으로 사용될 클래스를 활용할 수 있다.
- 테스트 환경에 따라 자유롭게 오브젝트를 만들 수 있다.
- 생성자 파라미터에 구현체를 주입하는데도 아무런 문제가 없다.
- 위를 보장함으로써, 객체지향 설계 방식에 맞게 싱글톤을 이용할 수 있다.
싱글톤 주의점
stateless
여야 한다.- 여러 스레드에서 접근할 수 있기 때문에, 상태가 있으면 동시성 문제 등 다양한 문제가 발생하게 된다.
- 내가 읽어야 할 상태값을 다른 스레드가 갑자기 수정하여 이상한 값을 읽는 등의 문제가 발생할 수 있다.
- 여러 스레드에서 접근할 수 있기 때문에, 상태가 있으면 동시성 문제 등 다양한 문제가 발생하게 된다.
스프링 빈의 스코프
- 빈이 생성되고 존재하고 적용되는 범위를 빈의 스코프라고 한다.
- 사실 빈은 무조건 싱글톤 스코프만 갖는 것은 아니다.
프로토타입 스코프
: 컨테이너에 빈을 요청할 때마다 매번 새로운 빈을 만든다. (일반 팩토리 비슷)요청 스코프
: HTTP 요청이 생길 때마다 매번 새로운 빈을 만든다.세션 스코프
: 웹의 세션과 비슷한 스코프를 갖는다.
1.7 의존관계 주입(DI)
의존관계 주입 용어의 탄생
제어의 역전(IoC)
는 프레임워크 사이에서 매우 폭 넓게 사용되던 용어이다.- 스프링에 더욱 알맞은 용어가 새로 나왔고 그것이
의존관계 주입(Dependency Injection)
이다.
의존관계 주입(Dependency Injection)
은 오브젝트와 오브젝트 레퍼런스를 외부로부터 제공받고, 여타 오브젝트와 다이나믹하게 의존관계가 만들어지는 것이 핵심이다.
의존관계 주입이란?
- 위에서
UserDao
클래스는 의존관계를 주입받는다고 할 수 있다. - 의존관계 주입은
의존 오브젝트를 사용할 오브젝트
와의존 오브젝트
의 관계를 런타임 시 연결해주는 작업을 말한다.- 클래스 모델이나 코드에서는 런타임 시점의 의존관계가 드러나지 않아야 한다. 즉, 인터페이스에 의존해야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리와 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.
- 스프링에서는 빈의
Constructor
혹은Setter
등을 이용하여 의존관계 주입이 가능하다.
:클래스명
은 런타임에 존재하는 인스턴스라는 뜻이다.
의존관계 검색
- 의존관계를
Constructor
혹은Setter
를 이용해 가져오지 않고, 이름이나 타입으로 검색하여 가져오는 방식이다.
public class UserDao {
ConnectionMaker connectionMaker;
// DL (Dependency Lookup) 를 이용한 방법
public UserDao() {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = applicationContext.getBean(ConnectionMaker.class);
}
}
직접
ApplicationContext
를 뒤져 빈을 찾아냈다.
의존관계 검색이 필요한 경우
- 스프링 빈이 아닌 오브젝트에서 스프링 빈을 가져와야 할 때 유용하다.
의존관계 주입의 응용 방법
- 기능 구현의 교환
- 빈 생성 메서드의 반환 타입을 인터페이스로 두고, 구현체를 갈아끼우는 방식으로 가능하다.
- 부가기능 추가
- 기존 인터페이스 혹은 클래스를 상속하여 부가기능을 추가한 새로운 클래스를 만든 뒤, 구현체를 바꾸면 된다.
위의 응용이 좋은 점은 기존에 작성된 코드를 변경하지 않고도 동작을 변경할 수 있다는 것이다.
'프레임워크 > 토비의 스프링' 카테고리의 다른 글
토비의 스프링 5장 요약 정리 - 서비스 추상화 (0) | 2022.09.06 |
---|---|
토비의 스프링 4장 요약 정리 - 예외 처리 (0) | 2022.06.21 |
토비의 스프링 3장 요약 정리 - 템플릿 (0) | 2022.06.20 |
토비의 스프링 2장 요약 정리 - 테스트 (2) | 2021.12.26 |
토비의 스프링 0장 정리 (0) | 2021.12.14 |