스프링은 자바를 기반으로 한 기술이다. 자바를 선택한 이유는 객체지향 프로그래밍이 가능한 언어라는 점이다.
자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 스프링의 핵심 철학이다.
따라서 스프링이 가장 많이 관심을 두는 대상은 오브젝트다. 이 오브젝트를 설계하고 구현해나가는 기준을 마련해주는 프레임워크의 형태로 제공한다.
개발 환경
- m1맥, Spring 3.0.7, 이클립스 2022-12, JDK 1.8, MAVEN, MySQL
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
- pom.xml 의존 라이브러리
1.1 초난감 DAO
스프링이 관심을 갖는 대상인 오브젝트의 설계와 구현, 동작원리에 집중해보자.
DAO란 Data Access Object라는 의미로 DB를 사용해 데이터를 조작하기 위해 만든 오브젝트를 의미한다.
1.1.1 User
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;
}
}
- 사용자 정보를 저장하기 위한 User 클래스를 만들자. id, name, password 세 개의 프로퍼티를 가졌다.
create table users (
id varchar(10) primary key,
name varchar(20) not null,
password varchar(10) not null
);
- User 오브젝트가 정보를 담을 Users 테이블을 만들자.
1.1.2 UserDao
사용자 정보를 DB에 넣고 관리할 수 있는 UserDao 클래스를 만들어보자.
JDBC를 이용하여 작업하는 순서는 일반적으로 다음과 같다.
- DB 연결을 위한
Connection
을 가져온다. - SQL을 담은
Statement
또는PreparedStatement
를 만든다. - 실행한 SQL 결과를
ResultSet
으로 받아서 정보를 저장할 오브젝트에 옮겨준 후 실행했던 리소스 (Connection, Statement, ResultSet) 들을 닫아준다. - JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메서드에 throws를 선언해서 던지게 한다.
package springbook.user.dao;
import springbook.user.domain.User;
import java.sql.*;
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/tobi", "root", "");
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/tobi", "root", "");
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;
}
}
- 사용자 정보를 추가하는 add 와 가져오는 get 메서드를 추가했다.
1.1.3 main()을 이용한 DAO 테스트 코드
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new UserDao();
User user = new User();
user.setId("sasca");
user.setName("jalgayo");
user.setPassword("jalgayo");
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() +" 조회 성공");
}
- 간단한 검증을 위해 main 메서드를 사용하여 콘솔 출력을 해보자. 커넥션이 정상이라면 등록과 조회 성공 메시지를 볼 수 있을 것이다. 초난감 DAO 코드를 객체지향의 원리에 맞게 개선해보자.
1.2 DAO의 분리
1.2.1 관심사의 분리
프로그래밍의 개념 중 관심사의 분리라는 게 있다. 즉, 관심이 같은 것끼리는 하나의 객체 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 서로 영향을 주지 않도록 분리하는 것을 의미한다.
1.2.2 커넥션 만들기의 추출
UserDao의 관심사항
- DB 커넥션 (어떤 DBMS를 쓸지 등)에 대한 고민
- SQL 실행 시 파라미터를 바인딩할 지, 어떤 SQL을 사용할 지에 대한 고민
- 작업이 끝난 후 리소스인 Statement와 Connection 오브젝트를 반환
중복 코드의 메서드 추출
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/tobi", "root", "");
return c;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
- 커넥션을 가져오는 중복 코드를 메서드 분리를 통해 처리하자. 앞으로 드라이버나 URL이 바뀔 일이 발생해도 getConnection() 만 수정하면 된다.
변경사항에 대한 검증 : 리팩토링과 테스트
앞서 main() 테스트를 통해 UserDao의 기능이 동작하는 것을 테스트했었다. 하지만, DB에 실제 값이 들어가있으므로 재테스트를 시도하면 PK 오류가 발생할 것이다. 따라서 기존 데이터를 삭제하고 재실행해야 한다.
현재까지의 작업은 UserDao의 기능에 아무런 변화를 주지 않았지만, 중복돼서 등장하는 특정 관심사항을 메소드로 분리를 통해 깔끔하고 미래에 대응하기 쉬운 코드가 됐다. 이러한 작업을 리팩토링이라고 하며, 그 중 메서드 추출 기법이라고 부른다.
1.2.3 DB 커넥션 만들기의 독립
메서드 추출을 통해 유연하게 대처할 수 있는 코드를 만들었지만, 변화에 대응하기는 아직 부족하다.
커넥션의 변화에 대응하기 위해서 UserDao를 개선해보자.
- 다음과 같이 UserDao를 추상클래스로 만들어 상속을 통해 확장을 제공하는 방법이 있다.
public abstract class UserDao {
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
public void add(...) {
Connection c = getConnection();
...
}
}
- 코드는 다음과 같이 구현 코드를 제거하고, 추상 메서드로 변경한다. 이후 확장된 NUserDao 나 DUserDao에서 입맛대로 구현해서 사용할 수 있게 되었다.
- 하지만, UserDao의 관심사는 Connection이라는 오브젝트를 사용하는 것이지, 구현체에 대해 관심사항이 아니다.
- 다음 그림과 같이 Connection을 인터페이스화 하여 분리한다면, 매우 깔끔한 방식으로 관심사항을 분리할 수 있다.
이렇게 슈퍼클래스에 로직의 흐름을 만들고, 그 기능의 구현을 서브 클래스에서 처리하도록 사용하는 방법을 팩토리 메서드 패턴이라고 한다.
1.3 DAO의 확장
UserDAO는 현재 문제점이 있다. 바로 상속관계를 통해 긴밀한 결합을 허용하여 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 점이다.
1.3.2 인터페이스의 도입
- 다음 그림과 같이 ConnectionMaker라는 인터페이스를 통해 기능만 정의하자.
- UserDao의 입장에서는 ConnectionMaker 인터페이스 타입의 오브젝트라면, 어떤 클래스든지 상관없이 Connection 타입의 오브젝트를 만들어 준다고 기대할 수 있으며, 그 구현체에 대한 내용은 관심이 없도록 만들 수 있다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
- 다음과 같이 커넥션을 만들어주는 훅 메서드를 정의하자.
public class DConnectionMaker implements ConnectionMaker {
@Override
public Connection makeConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/tobi", "root", "");
return c;
}
}
- ConnectionMaker 인터페이스를 상속받아, 구현체에서 직접 커넥션에 대한 기능을 구현하자.
public class UserDao {
private ConnectionMaker connectionMaker; // 인터페이스를 통해 구체적인 클래스 정보를 모른다.
public UserDao() {
connectionMaker = new DConnectionMaker(); // 구체 클래스명이 나오는 문제 발생!
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeConnection(); // 클래스가 바뀌어도 영향 받지 않음
}
}
- ConnectionMaker 인터페이스를 통해 긴밀한 결합을 허용하지 않도록 변경하였다. 하지만 아직도 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 구현체에 대한 정보를 알고 있어, 변경에 열려있다.
1.3.3 관계설정 책임의 분리
UserDao와 DConncectionMaker 사이에 의존관계가 발생해서, 변경에 열려있는 문제가 생겼다.
이 문제를 해결하는 방법은 바로 런타임 오브젝트 관계를 갖는 구조로 만들어 주는 것이다. 즉, 구현체에 대한 설정을 클라이언트의 책임으로 만드는 것이다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
- UserDao의 생성자에 클라이언트를 통해 구현 정보를 받도록 변경하자.
public class UserDaoTest {
public static void main(String[] args) {
ConnectionMaker connectionMaker = new DConnectionMaker(); // 클라이언트가 구현 클래스 결정
UserDao userDao = new UserDao(connectionMaker);
}
}
- UserDaoTest를 통해 클라이언트가 직접 런타임 오브젝트 의존관계를 설정하는 책임을 맡도록 변경했다.
- 관계 설정 책임을 담당한 클라이언트의 등장으로 다음과 같은 깔끔한 구조의 설계가 되었다.
1.3.4 원칙과 패턴
객체지향 설계에는 SOLID 5원칙(SRP, OCP, LSP, ISP, DIP)이 있다. 100퍼센트 지켜야하는 절대적 기준은 아니지만, 대부분의 상황에 잘 들어맞는 가이드라인과 같은 것이다.
SOLID 5원칙에 대한 내용은 하단의 포스팅을 참고하자.
https://sasca37.tistory.com/17
개방 폐쇄 원칙 (Open - Closed Principle)
- 클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 즉, UserDao는 DB 연결 방법에 대한 확장은 열려 있고, UserDao의 기능을 구현한 코드는 확장에 영향 받지 않고 유지할 수 있도록 하는 것이 닫혀있다고 말할 수 있다.
전략 패턴
UserDaoTest - UserDao - ConnectionMaker 구조를 디자인 패턴의 시각으로 보면 전략 패턴으로 볼 수 있다.
전략패턴은 자신의 context에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 필요에 따라 바꿔서 사용할 수 있게하는 디자인 패턴이다.
즉, UserDao는 전략 패턴의 Context가 되고, ConnectionMaker는 DB 연결 방식을 선정하는 알고리즘 역할을 한다.
전략 패턴은 곧 OCP 원칙을 잘 따르게 되며, 응집력이 높고 결합도는 낮게 설계할 수 있다.
1.4 제어의 역전 (IoC)
UserDaoTest는 테스트를 위한 용도로 만들어졌지만, 엉겁결에 클라이언트의 책임까지 맡게 되었다.
IoC란 Inversion of Control
의 약자로 앞서 개선한 UserDaoTest (클라이언트의 책임) 역할을 분리하는 용도로 사용이 된다.
1.4.1 오브젝트 팩토리
- UserDaoTest를 분리시킬 기능을 담당할 클래스를 만들어보자. 이 클래스의 역할은 객체의 생성 방법을 결정하고, 만들어진 오브젝트를 돌려주는 것인데, 이러한 일을 담당하는 오브젝트를
팩토리
라고 부른다.
public class DaoFactory {
public UserDao userDao() {
ConnectionMaker connectionMaker = new DConnectionMaker();
UserDao userDao = new UserDao(connectionMaker);
return userDao;
}
}
- 팩토리 역할을 하는 DaoFactory를 만들어보자.
public class UserDaoTest {
public static void main(String[] args) {
UserDao userDao = new DaoFactory().userDao();
}
}
- 이로써 UserDaoTest는 테스트에만 집중하도록 만들 수 있게 되었다.
1.4.2 오브젝트 팩토리 활용
public UserDao userDao() {
return new UserDao(new DConnectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(new DConnectionMaker());
}
- 만약 다음과 같이 DaoFactory에서 UserDao가 아닌 다른 DAO의 생성 기능이 생긴다면 중복 문제가 발생한다. 중복 문제가 발생하면 분리해내는 것이 가장 좋은 방법이다.
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
public UserDao userDao() {
return new UserDao(connectionMaker());
}
public AccountDao accountDao() {
return new AccountDao(connectionMaker());
}
- 분리를 통해 ConnectionMaker 타입의 오브젝트 생성 코드를 만들어주자.
1.4.3 제어권의 이전을 통한 제어관계 역전
제어의 역전이란 main() 메서드와 같이 프로그램이 시작되는 시점에서 오브젝트를 결정하는 것이 아닌, 제어 흐름의 개념을 거꾸로 뒤집는 것이다.
즉, main() 메서드와 같은 엔트리 포인트를 제외하고 모든 오브젝트는 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어지도록 하는 것이다.
예를 들어, 서블릿 또한 IoC 기술이 들어가있다. 서블릿에 대한 제어 권한을 가진 서블릿 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메서드들을 호출한다.
DaoFactory
가 제어의 역전의 예시로 볼 수 있다. UserDao는 팩토리에 의해 수동적으로 만들어지고 사용되는 입장이 되었다. 자연스럽게 관심을 분리하고 책임을 나누어 확장 가능한 구조로 만들면서 IoC를 적용하게 된 것이다. 이러한 팩토리를 IoC 컨테이너 내지는 IoC 프레임워크라고 불릴 수 있다.
1.5 스프링의 IoC
스프링은 프레임워크로, 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 기능을 통해 IoC의 역할을 수행한다.
1.5.1 오브젝트 팩토리
스프링이 제어권을 갖고, 오브젝트를 만들고 관계를 부여한 오브젝트를 빈(bean)이라고 부른다.
이러한 빈의 생성과 관계설정 같은 제어를 담당하는 것이 빈 팩토리로 더 확장하자면 애플리케이션 컨텍스트로 불리운다.
DaoFactory를 스프링 빈 팩토리로 만들어보자.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 애플리케이션 컨텍스가 사용할 설정정보 지정
public class DaoFactory {
@Bean // 오브젝트 생성을 담당하는 IoC용 메서드 지정
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
}
- 다음과 같이 어노테이션을 통해 스프링 빈 팩토리로 만들 수 있다.
@Bean
을 통해 만든 메서드 명이 빈의 이름이 된다.
public class UserDaoTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
}
}
ApplicationContext
는 관리하는 오브젝트를 요청하는 메서드로, 팩토리 클래스명을 명시하여 빈설정 정보를 가져올 수 있다.
1.5.2 애플리케이션 컨텍스트 동작방식
- 스프링에서 애플리케이션 컨텍스트를 IoC 컨테이너라고도 하고, 스프링 컨테이너라고도 부른다. 애플리케이션 컨텍스트는 DaoFactory 클래스 정보를 설정정보로 등록해두고, @Bean이 붙은 메서드의 이름을 가져와 빈 목록으로 만들어준다.
애플리케이션 컨텍스트 장점
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.즉, 오브젝트 팩토리가 많아져도 직접 알아야하거나, 사용할 필요가 없이 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
- 종합 IoC 서비스를 제공해준다. (오브젝트가 만들어지는 방식, 시점과 전략 등 다양한 기능 제공)
- 빈을 검색하는 다양한 방법을 제공한다.
1.6 싱글톤 레지스트리와 오브젝트 스코프
public class UserDaoTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao1 = context.getBean("userDao", UserDao.class);
UserDao dao2 = context.getBean("userDao", UserDao.class);
System.out.println(dao1 == dao2); //true
}
}
- 스프링은 빈을 싱글톤으로 만들어준다. 따라서 빈을 새로 가져와도 항상 같은 오브젝트를 반환한다.
싱글톤 패턴의 한계
싱글톤은 엔터프라이즈 환경에 GC 최적화를 위해 사용된다. 하지만, 그에 따라 단점이 있어 안티패턴으로 불리기도 한다.
- private 생성자를 갖고 있기 때문에 상속할 수 없다. 싱글톤은 생성되는 것을 한번만 처리하기 위해 private한 생성자를 갖고 있다.
- 테스트하기 어렵다. 싱글톤은 만들어지는 방식이 제한적이기 때문에 목 오브젝트 등으로 대체하기가 어렵다.
- 서버 환경에서 클래스 로더의 구성에 따라 하나 이상의 오브젝트가 만들어 질 수 있다.
- 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
1.6.1 싱글톤 레지스트리
스프링은 위와 같은 싱글톤의 단점을 해결하기 위해 싱글톤 레지스트리를 제공한다.
스태틱메서드와 private 생성자를 사용하는 방식이 아닌, 평범한 자바 클래스로 싱글톤을 활용하게 해준다. 즉, IoC 컨테이너를 사용해서 싱글톤 생성과 관계 설정 등에 대한 제어권을 갖고 관리해준다.
즉, 싱글톤이 public한 생성자를 가질 수 있어서 객체지향적인 설계에 제약이 없다는 점이다.
1.6.2 싱글톤과 오브젝트의 상태
싱글톤은 멀티스레드 환경에서 여러 쓰레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다.
즉, 상태정보를 내부에 갖고 있지 않은 stateless
한 방식으로 만들어져야 한다.
public class UserDao {
// 읽기 전용은 상관이 없으며, 심지어 스프링이 관리해주는 빈이기 때문에 멀티쓰레드 환경에서도 최초 한번 초기화 보장
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"));
return this.user;
}
}
- 인스턴스 변수를 사용하는 UserDao로 멀티쓰레드 환경에서 Connection 또는 User 정보를 덮어 씌울 수 있는 매우 심각한 문제를 발생시킨다.
1.6.3 스프링 빈 스코프
스프링에서의 빈은 대부분 싱글톤으로 관리한다. 이 빈은 스프링 컨테이너가 존재하는 동안 계속 유지가된다. 경우에 따라서 빈을 요청할 때마다 새로운 오브젝트를 반환하는 프로토타입이나, HTTP 요청마다 생성하는 request 스코프, 세션 스코프 등이 있다.
1.7 의존관계 주입 (DI)
IoC와 DI
의존관계 주입 (Dependency Injection)은 광범위한 IoC 컨테이너의 기술 중 핵심 기술을 의미한다. 따라서 IoC 컨테이너를 DI 컨테이너라고도 불리고 있다.
1.7.2 런타임 의존관계 설정
의존 관계랑 두 개의 클래스 또는 모듈이 의존하고 있는 관계에 있다는 것을 의미한다.
즉, A 가 B의 의존관계에 있다고 한다면, B의 기능이 변경되면 A에 영향을 미친다.
UML 모델에서는 의존 관계를 점선이된 화살표료 표현한다.
public class UserDao {
public UserDao() {
connectionMaker = new DConnectionMaker();
}
}
- 이전에 UserDao 클래스에서 팩토리를 생성하기 전 코드이다. 이 코드는 현재 DConnectionMaker라는 구체적인 클래스의 존재를 알고 있다. 따라서 런타임 시 의존관계가 코드 속에 미리 결정되어 있다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
- 변경했던 코드는 다음과 같다. UserDao는 어떤 ConnectionMaker가 들어올지 모르고, 런타임 시점에 생성자를 통해 주입받은 구현체로 처리를 한다.
같은 의존관계를 갖고 있어도, 이미 구현체를 알고 있는 것과 런타임 시점에 구현체가 들어오는 것의 차이가 DI 적용의 차이라고 볼 수 있다.
1.7.3 의존관계 검색과 주입
IoC 방법에는 DI 뿐만아니라 DL(dependency lookup)이 있다. 런타임 시에 의존관계를 결정한다는 점에서 DI와 비슷하지만, 의존관계를 맺는 방법이 외부 주입이 아닌, 스스로 검색을 이용하는 방식이다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
DaoFactory daoFactory = new DaoFactory();
this.connectionMaker = daoFactory.connectionMaker();
}
}
- 다음과 같이 생성자에 팩토리를 제공해주어 설정해줘도, 런타임 시에 구현체가 결정되도록 설계 된다.
- DI와 다른 점이 바로 생성자 내부에서 직접 스스로 IoC 컨테이너인 DaoFactory에게 요청하는 것이다.
DI나 DL 모두 같은 장점을 지녔다. 코드의 단순함과 깔끔함을 봤을 때 DI를 사용하는 편이 낫다.
DL을 사용하는 케이스는 getBean 과 같이 스태틱 메서드인 main() 에서 직접 IoC 컨테이너를 지정해주는 경우에 사용한다.
DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말자.
1.7.5 메서드를 이용한 의존관계 주입
지금까지는 UserDao의 의존관계 주입을 생성자를 사용해왔다. 그 외에도 세터 방식과 일반 메서드 방식이 존재한다.
스프링은 DI 방법 중에 세터 메서드를 가장 많이 사용해왔다. 그 이유는 XML을 사용하는 경우에 세터 메서드가 사용하기 편리하기 때문이다.
- 세터 메서드 주입 방식 : 외부에서 오브젝트 내부의 애트리뷰트 값을 변경하려는 용도로 주로 사용된다.
- 일반 메서드 주입 방식 : 세터 메서드처럼 set으로 시작해야 하고, 한 번에 여러 파라미터를 주입할 수 있는 장점이 있다. 반대로 실수를 할 수 있다는 단점도 있다.
public class UserDao {
private ConnectionMaker connectionMaker;
public void setConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
- 세터 메서드 주입 방식을 사용한다면 다음과 같이 처리 된다.
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}
@Bean
public Connection connectionMaker() {
return new DaoFactory.connectionMaker();
}
- 세터 방식대로 오브젝트를 먼저 생성한 후, 이후 setter에 담아 처리되도록 빈을 등록한다.
1.8 XML을 이용한 설정
스프링은 DaoFactory 같은 자바 클래스 외에도, 다양한 방법을 통해 DI 설정정보를 만들 수 있게 해준다. 가장 대표적으로 XML이 있다. XML은 별도의 빌드 작업 없이 빠르게 반영할 수 있고, 스키마나 DTD를 이용해 포맷을 따라 작성 되었는 지 손쉽게 확인할 수 도 있다.
1.8.1 XML 설정
XML 파일은 <beans>
를 루트 엘리먼트로 사용한다. 즉, @Configuration
과 동일한 역할을 한다.
<beans>
<bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connectionMaker" ref="connectionMaker"/>
</bean>
</beans>
<property>
태그의 name과 ref는 의미가 다르다.- name은 DI에 사용할 수정자 메서드의 프로퍼티 이름이며, ref 는 주입할 오브젝트를 정의한 빈 ID이다. 즉 , 현재는 동일한 이름이지만, 빈 이름이 바뀐다면 name과 ref는 다른 값으로 처리 된다.
<beans>
<bean id="localDBConnectionMaker" class="springbook.user.dao.LocalDBConnectionMaker" />
<bean id="prodDBConnectionMaker" class="springbook.user.dao.ProdDBConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connectionMaker" ref="localDBConnectionMaker"/>
</bean>
</beans>
- 다음과 같이 같은 인터페이스 타입의 빈을 여러 개 정의한 후 환경에 따라 ref 값을 지정하여 사용할 수 있다.
DTD와 스키마
XML 문서에는 DTD와 스키마를 토해 정의 된 구조를 따라 작성됐는지 검사할 수 있다. 스프링 XML 설정파일은 이 두 가지 방식을 모두 지원한다.
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
- DTD 선언 방식이다. 스프링은 DI를 위한 기본태그인 beans, bean 외에도 별도의 태그를 제공한다. 이 태그들은 각각 별개의 스키마 파일에 정의되어 있고, 독립적인 네임스페이스를 사용해야만 한다. 따라서 이런 태그를 사용하려면 스키마를 사용해야한다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
- 기본 스키마 방식이다.
특별한 이유가 없다면 DTD 보다는 스키마를 사용하는 편이 바람직하다.
1.8.2 XML을 이용한 애플리케이션 컨텍스트
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="connectionMaker" ref="connectionMaker" />
</bean>
</beans>
- src/main/resources 에 XML 설정 정보를 담은 applicationContext.xml을 만들어주자. property의 name은 세터 주입을 사용하므로 생성자 주입에서 세터 주입으로 변경하자.
public class UserDaoTest {
public static void main(String[] args) {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao1 = context.getBean("userDao", UserDao.class);
UserDao dao2 = context.getBean("userDao", UserDao.class);
System.out.println(dao1 == dao2); // true 반환
}
}
GenericXmlApplicationContext
를 통해 XML 설정정보를 읽어와서 정상 결과를 반환하는 것을 확인할 수 있다.
1.8.3 DataSource 인터페이스로 변환
ConnectionMaker는 DB 커넥션을 생성해주는 인터페이스 역할을 하고 있다. 자바에서는 이 기능을 DataSource 라는 인터페이스로 제공하고 있다.
public class UserDao {
private DataSource dataSource;
public UserDao() {}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = dataSource.getConnection();
}
}
- 다음과 같이 ConnectionMaker 를 DataSource로 변경해주자. 이어서 DataSource의 구현 클래스로 SimpleDriverDataSource라는 클래스가 있다. 이 클래스는 JDBC 드라이버, URL, 아이디, 비밀번호 등의 수정자 메서드를 갖고 있다.
@Configuration
public class DaoFactory {
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost/tobi");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setDataSource(dataSource());
return userDao;
}
}
- DaoFactory를 다음과 같이 DataSource를 사용할 수 있도록 변경하였다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
- applicationContext.xml 도 다음과 같이 dataSource 빈을 사용하도록 변경하자.
DaoFactory 에서 SimpleDriverDataSource에 DB 접속 정보를 넣어주었다. 이 오브젝트의 경우 단순 Class 타입의 오브젝트 인데 XML에서는 어떻게 dataSource() 메서드처럼 DB 연결정보를 넣을 수 있도록 설정했을까?
1.8.4 프로퍼티 값의 주입
dataSource() 메서드처럼 수정자 메서드에는 다른 빈이나 오브젝트 뿐 아니라, 단순 값을 넣어줄 수도 있다. 이 때는 DI와 같은 의존관계 주입이 아닌, 단순 값 주입이라고 볼 수 있다.
<!-- 코드를 통한 연결정보 주입
dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost/tobi");
dataSource.setUsername("root");
dataSource.setPassword("");
-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost/tobi"></property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
- xml 단에서 단순 값 주입을 할 수 있다.
- 이 때 ref가 아닌 value를 사용하고 있는데, 스프링의 빈으로 등록될 클래스에 수정자 메서드가 정의되어 있다면,
<property>
를 사용해 주입할 정보를 지정할 수 있다는 점에는 같으나, 단순 값을 주입한다는 의미에서 value 애트리뷰트를 사용한다. driverClass
의 경우 단순 String이 아닌 Class 타입이다. 스프링이 프로퍼티의 값과 수정자 메서드의 파라미터 타입을 참고해서 적절한 형태로 변환해준다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost/tobi"></property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
- xml 전체 내용은 다음과 같다.
public class UserDaoTest {
public static void main(String[] args) {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
ApplicationContext context2 = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao1 = context.getBean("userDao", UserDao.class);
UserDao dao2 = context.getBean("userDao", UserDao.class);
UserDao dao3 = context2.getBean("userDao", UserDao.class);
UserDao dao4 = context2.getBean("userDao", UserDao.class);
System.out.println(dao1 == dao2);
System.out.println(dao3 == dao4);
}
}
- 다음과 같이 자바에서 설정한 DaoFactory와 XML에서 설정한 applicationContext.xml 모두 정상 출력되는 것을 확인할 수 있다.
정리
책임이 다른 코드를 분리해가면서, 스프링이란 어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크라는 사실을 기억하자.
스프링의 관심은 오브젝트와 그 관계이다. 이 오브젝트를 어떻게 설계하고, 분리하고, 의존관계를 가질지는 개발자의 역할이며 책임이다.
REFERENCES
- 토비의 스프링 3.1