본문 바로가기

책/토비의 스프링 3.1(1권, 완)

1장 오브젝트와 의존관계

 

1.1 초난감 DAO

자바빈 규약을 따르는 User 클래스 이다.

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;
	}
}

 

User 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO이다.

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;
	}

}

 

만들어진 코드의 기능을 검증하도록 하는 가장 간단한 방법이다.

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;
	}

	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. DAO를 통해 어떤 쿼리를 날려야 하는지 3. DAO로 부터 받은 데이터를 어떻게 가공해서 보여줘여 하는지 등등..

그렇기에 관심사의 분리가 필요하다.

이를 객체지향적으로 생각해보면 비슷한 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한객체로 모이게 하고 관심이 다른 것은 떨어트려 서로 영향을 주지 않도록 분리하는 것이라고 생각할 수 있다.

 

이를 UserDao의 add() 메소드에 대입시켜 생각해보면 다음과 같은 결론을 내릴 수 있다.

먼저 UserDao의 관심사항이다.

  1. DB의 연결은 어떻게 할지
  1. SQL문장을 어떻게 사용할지
  1. DB리소스 닫기

 

그리고 UserDao의 add()에서 DB연결을 설정하는 코드관련 문제들과 해결방법이다.

  • ❓add()메소드에 있는 DB 커넥션을 가져오는 코드가 get() 메소드에 중복되어 있으며 다른 관심사(조회..)와 섞여있다.

    ❗중복되어 있는 코드를 독립적인 메소드로 만들어 추가로 DB 커넥션이 필요할 경우 새로운 커넥션을 위한 DB정보를 작성할 필요없이 해당 메소드를 호출해 커넥션을 가져올수 있다.

    ..
    private 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;
    	}
    ..

 

위와 같은 작업은 리팩토링이라고 하며 중복되는 코드를 하나의 메소드로 추출하는 것을 메소드 추출이라고 한다.

💡
리팩토링 기존의 코드를 외부의 동작방식에는 변화없이 내부 구조를 변경해서 재구성하느 작업 또는 기술을 말한다.

 

하지만 동일한 클래스에서 DB 커넥션 정보설정 코드와 비즈니스 로직이 존재하고있다. 그렇기에 더 나아가 이를 추상 클래스로 분리할 수 있다.

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 abstract class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
		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 ClassNotFoundException, SQLException {
		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;
	}

	abstract protected Connection getConnection() throws ClassNotFoundException, SQLException ;


	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		UserDao dao = new NUserDao();

		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() + " 조회 성공");
	}

}
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;
	}
}
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;
	}
}

 

추상클래스인 UserDao에서 전체적인 비즈니스 로직을 만들어놓고 세부적인 코드 구현은 추상클래스를 구현한 클래스에서 구현하는 것을 템플릿 메소드 패턴이라고 한다. 그리고 서브 클래스에서 구체적인 객체 생성 방법을 경정하게 하는 것을 팩토리 메소드 패턴이라고 한다.

  • 템플릿 메소드 패턴 추가예제

    추상 클래스인 abstractTemplate를 상속받아 call 메소드를 구현하면 된다.

 

템플릿 메소드 패턴 vs 팩토리 메소드 패턴

템플릿 메소드 패턴은 전체적인 비즈니스 로직 즉 공통된 코드를 미리 작성해놓고 부분적인 수정을 통해 전체 알고리즘의 구조를 보호하고 재사용을 강조하는 측면이 있다면

팩토리 메소드 패턴은 객체를 생성하는 공장을 따로 두어 새로운 클래스가 추가되더라도 기존 코드를 변경할 필요가 없어져 유지보수가 용이해지는 측면이 존재한다.

 

그러나 이러한 방법을 통한 문제점 역시 존재한다.

  1. 자바는 다중 상속을 지원하지 않기에 한번에 하나의 상속만을 허용한다.
  1. 상속은 상하위 클래스가 밀접한 관계를 맺게 하고 하위 클래스는 슈퍼클래스의 메서드를 사용할 수 있기에 위험성이 존재한다.
  1. 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에서는 적용할수 없기 때문에 DAO클래스 마다 동일한 구현 코드가 중복된다는 단점이 존재한다. (템플릿 메서드 패턴을 사용해 구현했기에 분리 불가능)

 

1.3 DAO의 확장

1.2에서 관심사(DB 커넥션, 데이터 액세스)가 다른 코드를 다른 메소드, 상하위 클래스로 구분해서 구현했다. 여기서 더 나아가 아예 독립된 클래스로 구현할 수 있다.

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();

		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 {
		Connection c = this.simpleConnectionMaker.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 static void main(String[] args) throws ClassNotFoundException, SQLException {
		UserDao dao = new NUserDao();

		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() + " 조회 성공");
	}

}
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;
	}
}

기존의 DUserDaoNUserDao에서 상속을 통해 DB 커넥션 기능을 확장해서 사용하는 것이 불가능해졌다.

  1. SimpleConnectionMaker에서 DB설정 정보를 변경불가능하게 하드 코딩으로 작성했고
  1. DUserDaoNUserDao에서는 SimpleConnectionMaker를 가져와서 수정이 불가능하며
  1. UserDao의 소스 코드를 제공하지 않는 이상 DB 연결 정보를 가지고 있는 클래스를 수정 불가능하고 이는 UserDao를 변경하지 않으면서 라는 요구사항을 만족시키지 못한다.

 

이렇게 클래스를 분리했음에도 두 가지 문제점이 존재한다.

  1. SimpleConnectionMaker의 메소드 이름을 강제할 수 없다. 만약 커넥션을 가져오는 과정에서 메소드 이름을 getConnection이 아닌 다른 메소드 이름으로 작성시 (ex. openConnection) 이를 사용하는 UserDao의 DB 커넥션 관련 코드를 전부 수정해야 한다.➕ 이는 SOLID 5가지 원칙중 “하위클래스가 변경되어도 상위클래스는 영향받지 않는다”는 LSP(리스코프 치환 원칙)을 위배한다.
  1. UserDao가 어떤 DB 커넥션을 사용하는지 구체적으로 알고 있어야 한다.

 

이를 해결하기 위해 인터페이스를 사용할 수 있다. 인터페이스를 통해 UserDao는 자신이 사용하는 클래스가 어떤 클래스인지 몰라도 되며 단지 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.

package springbook.user.dao;
...
public interface ConnectionMaker{
	public Connection makeConnection() throws ClassNotFoundExcepion, SQLExcepion;
}

그리고 이 인터페이스를 사용하는 UserDao에서는 ConncectionMaker 인터페이스 타입의 오브젝트라면 어떤 클래스로 만들어졌든지 상관없이 makeConnection을 호출하면 Connection 타압의 오브젝트를 만들어서 돌려줄 것이라고 기대할 수 있다.

package springbook.user.dao;
...
public class DConnectionMaker implements ConnectionMaker{
	...
	public Connection makeConnection() throws ClassNotFoundException, SQLException{
		//D 사의 독자적인 방법으로 Connection을 생성하는 코드
	}
}
public class UserDao{
	// 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없다.
	private ConnectionMaker connectionMaker;

	// 여전히 구체적인 클래스 이름을 알아야 한다.
	public UserDao(){
		connectionMaker = new DConnectionMaker();
	}

	public void add(User user) throws ClassNotFoundException, SQLException{
		//인퍼테이스에 정의된메소드를 사용하므로 클래스가 바뀐다고 해도 메소드 이름이 변경될 걱정은 없다.
		Connection c = connectionMaker.makeConnection();
		...
	}

	public void get(String id) throws ClassNotFoundException, SQLException{
		Connection c = connectionMaker.makeConnection();
		...
	}

}

그러나 DConnectionMaker의 클래스 생성자를 호출하는 코드가 UserDao에 남아 있다.

그렇기 때문에 UserDao 소스코드를 함꼐 제공해서 필요할때마다 UserDao의 생성자 메소드를 직접 수정하라고 하지 않고는 자유로운 DB 커녁션 확장 기능을 가진 UserDao를 제공할 수가 없다.

 

위의 코드에서 DB 액세스(UserDao), DB 설정정보(ConnectionMaker)라는 공통의 관심사를 분리했지만 여전히 하나의 관심사가 남아있다. DB액세스(UserDao)가 어떤 DB 설정정보(ConnectionMaker) 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다. 즉 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.

따라서 UserDao의 클라이언트(UserDao를 사용하는 클래스)에서 UserDao가 어떤 ConnectionMaker의 구현 클래스를 사용하리를 결정하도록 만들어야한다.

구현 클래스 사이의 관계가 만들어진다는 것은 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻이다. 따라서 클래스가 아니라 오브젝트와 오브젝트의 사이의 관계를 설정해주어야 한다.

그리고 오브젝트 사이의 관계는 런타임시 한쪽이 다른 오브젝트의 레퍼런스를 갖고있는 방식으로 만들어진다.

 

이를 UserDao에 적용시켜보면 외부에서 만든 오브젝트를 다형성을 이용해 전달받을 수 있어야 하고 이는 메소드 파라미터나 생성자 파라미터를 이용하면된다.

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);

		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() + " 조회 성공");
	}
}
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 ConnectionMaker connectionMaker;
	
	public UserDao(ConnectionMaker simpleConnectionMaker) {
		this.connectionMaker = simpleConnectionMaker;
	}

	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = this.connectionMaker.makeConnection();

		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 {
		Connection c = this.connectionMaker.makeConnection();
		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 초난감 DAO, 1.2 DAO의 분리 1.3 DAO를 통해서 구현하고 개선한 결과를 객체지향 기술의 여러가지 이론을 통해 설명하면 다음과 같다.

 

개방폐쇄 원칙(OCP, Open-Closed Principle)

클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀있어야 한다. 이를 UserDao에 빗대어 설명하면 DB 연결 방법이라는 기능을 확장하는 데는 열려있으면서 동시에 UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀있다고 말할수 있다.

구체적으로 이야기 할 경우 인터페이스를 통해 제공되는 확장 포인트는 확장을 위해 개방되어 있다. 반면 인터페이스를 이요아는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 굳게 폐쇄되어 있다.

 

높은 응집도와 낮은 결합도

높은 응집도

응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 의미이며 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다.

 

낮은 결합도

결합도의 정의는 “하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는다른 오브젝트에게 변화를 요구하는 정도”이며 낮은 결합도는 느슨한 연결된 형태를 유지하는 것을 의미한다. 느슨한 연결된 형태는 관계를 유지하는 데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고 나머지는 서로 독립적이고 알 필요도 없게 만들어 주는 것이다.

 

전략 패턴(Strategy Pattern)

자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리시키고 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.

이름 전략패턴 템플릿 메소드 패턴
목적 알고리즘을 동적 으로 교체하고 클라이언트와 알고리즘 간의 결합도를 낮추기 위한 패턴 공통된 알고리즘의 뼈대를 정의하고 일부 단계는 서브클래스에서 구체화하여 다양한 변형을 지원하기 위한 패턴
구조 전략 인터페이스를 정의하고, 이를 구현하는 여러 전략 클래스들을 만듭니다. 클라이언트는 전략 인터페이스를 통해 알고리즘을 호출하며, 실행 시에 원하는 전략 객체를 주입하여 동작을 변경할 수 있습니다 추상 클래스에 공통된 알고리즘의 뼈대를 정의한 후, 일부 단계는 추상 메소드로 선언하여 서브클래스에서 오버라이딩할 수 있도록 합니다. 클라이언트는 추상 클래스를 상속받은 서브클래스를 생성하여 사용합니다.

 

1.4 제어의 역전(IoC)

IoC라는 약자로 많이 사용되는 제어의 역전(Inversion of Controll)이라는 용어가 있다 이는 오브젝트 스스로가 사용할 오브젝트를 결정하지도, 생성하지도 않으며 제어에 대한 권한이 개발자에서 외부 환경으로 역전되는 것이다. 대표적인 예로 프레임워크가 있다.

UserDaoTest로 설명하자면 UserDaoTest의 원래 목적은 기능이 잘 동작하는지 확인하려 만들었지만 UserDao가 어떤 ConnectionMaker를 사용할지 결정하는 기능까지 담당하고 있다. 이는 SRP를 위배하며 서로 분리를 해야한다.

 

이를 해결하기 위해 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것을 담당하는 팩토리 클래스를 만들어야 한다.

package springbook.user.dao;
...
public class DaoFactory{
	public UserDao userDao(){
		ConnectionMaker connectionMaker = ner DConnectionMaker();
		UserDao userDao = new UserDao(connectionMaker);
		return userDao;
	}
}

이제 Factory 클래스를 이용한 UserDaoTest의 코드는 다음과 같다.

public class UserDaoTest{
	public static void main(String[] args) throws ClassNotFoundException, SQLException{
		UserDao dao = new DaoFactory().userDao();
		...
	}
}

 

오브젝트들의 역할과 관계를 보면

UserDao와 ConnectionMaker는 각각 애플리케이션의 핵심적인 데이터 로직과 기술 로직을 담당하고 있고, 실질적인 로직을 담당하는 컴포넌트이다.

DaoFactory는 이런 애플리케이션의 오브젝트들을 구성하고 그 관계를 정의하는 책임을 맡고 있으며 애플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도와 같은 역할을 한다. (어떤 오브젝트가 어떤 어떤 오브젝트를 사용하는지를 정의해놓은 코드이다.)

 

DaoFactory를 통해서 오브젝트들의 관계를 정의하였지만 만약 Dao가 많아질 경우 다음과 같은 문제점이 발생한다.

public class DaoFactory{
	public UserDao userDao(){
		return new UserDao(new DConnectionMaker());	
	}

	public AccountDao accountDao(){
		return new AccountDao(new DConnectionMaker());	
	}

	public MessageDao messageDao(){
		return new MessageDao(new DConnectionMaker());	
	}
}

그리고 위와 같이 오브젝트 생성 코드가 중복되어 있는 상황에서 ConnectionMaker의 구현 클래스를 바꿀 때마다 모든 메소드를 일일이 수정해줘야 하는 문제점이 발생한다.

중복되는 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메소드로 뽑아내어 분리해내는 것이 가장 좋은 방법이다.(리팩토링)

public class DaoFactory{
	public UserDao userDao(){
		return new UserDao(connectionMaker());	
	}

	public AccountDao accountDao(){
		return new AccountDao(connectionMaker());	
	}

	public MessageDao messageDao(){
		return new MessageDao(connectionMaker());	
	}

	public ConnectionMaker connectionMaker() {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		return connectionMaker;
	}
}

 

제어의 역전(Inversion of Controll)에 대해 다시 한번 설명하면 프로그램의 제어 흐름이 뒤바뀌는 것이다. 일반적으로 제어의 역전은 다음과 같이 말할 수 있다.

💡
모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문에 오브젝트는 자신이 사용할 오브젝트를 스스로 선택하지 않고 생성하지도 않는다. 또 자신이 어떻게 만들어지고 어디서 사용되는지를 알 수 없다.

IoC는 디자인 패턴, 프레임워크 등 다양한 곳에서 발견할 수 있다. 그리고 프레임워크 없이도 이미 UserDao는 제어의 역전을 사용하고 있다.

1.5 스프링의 IoC

스프링의 핵심은 빈 팩토리 혹은 애플리케이션 컨텍스트라고 하는 것이다. 그리고 이는 위에서 만든 DaoFactory를 더 일반화 한것이다.

다음은 스프링프레임워크에서 사용되는 기본적인 개념들이다.

용어 정의
빈(bean), 스프링 빈 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트이며 스프링 컨테이너가 생성과 관계설정, 사용들을 제어해주는 IoC가 적용된 오브젝트를 의미
빈 팩토리 스프링에서 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 의미한다.
애플리케이션 컨텍스트 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC엔진

애플리케이션 컨텍스트가 사용하는 설정정보를 만들기 위해서는 DaoFactory를 조금 손을 봐야 한다. 그리고 이 자바 코드로 작성된 애플리케이션 컨텍스트의 설정정보를 가지고 애플리케이션이 생성된다

 

DaoFactory를 스프링의 빈 팩토리가 사용할 수 있게끔 만들기 위해 다음과 같은 어노테이션을 달아주어야 한다.

@Configuration : 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 의미

@bean : 오브젝트 생성을 담당하는 IoC용 메소드라는 표시

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;
	}
}

자바 코드의 모습을 하고 있지만 XML과 같은 스프링 전용 설정정보라고 보는것이 좋다.

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어야한다.

애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트이고 이를 구현한 클래스는 다양하게 있지만 DaoFactory는 어노테이션 기반으로 코드를 작성했기에 AnnotationConfigApplicationContext를 이용하면 된다.

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);
		...
	}
}

이때 애플리케이션 컨텍스트에는 메소드의 이름을 빈의 이름으로 하여 오브젝트가 등록된다.

💡
@Component vs @Bean @Component를 붙였을 경우 어플리케이션 컨텍스트에는 소문자로 시작하는 클래스의 이름이 붙으며 @Bean을 붙일 경우 메서드 이름이 붙어 빈이 등록된다.

이렇게 생성된 애플리케이션 컨텍스트에서 getBean()으로 오브젝트를 가져올수 있다. 이때 조심해야할 것은 getBean()은 기본적으로 값을 가져올때 오브젝트 타입으로 가져오기에 타입 캐스팅을 해주어야 하지만 자바5의 제네릭을 이용해 바로 원하는 타입의 빈을 가져올 수 있다.

 

어플리케이션 컨텍스트를 사용했을때 얻을수 있는 장점은 다음과 같다.

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다만약 DaoFactory처럼 IoC를 적용한 오브젝트도 계속 추가될경우 클라이언트는 원하는 오브젝트를 어떠한 팩토리 클래스에서 가져올지 알아야하며 팩토리 클래스를 계속해 생성해야 한다.
  1. 어플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.오브젝트가 만들어지는 방식, 시점, 전략을 다르게 설정할 수 있고 자동생성, 오브젝트에 대한 후처리, 설정 정보의 다변화등 다양한 기능을 제공한다.
  1. 어플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

➕ 어플리케이션 컨텍스트는 보통 여러개가 만들어지며 이를 통들어서 스프링 컨테이너라고 부를 수 있다.

1.6 싱글톤 레지스트리와 오브젝트 스코프

기존에 만들었는 DaoFactory를 통해 생성한 UserDao오브젝트들을 서로 비교해보면 매번 서로 다른 오브젝트임을 알 수 있다.

💡
오브젝트의 동일성과 동등성 자바의 오브젝트가 완전히 같은 동일한 오브젝트라고 할 경우 비교하는 오브젝트들을 사실 하나의 오브젝트이고 다른 레퍼런스 변수만을 지니고 있다는 의미고 동등성은 서로 다른 오브젝트이지만 동일한 정보를 가지고 있음을 의미한다.

그러나 애플리케이션 컨텍스트에 DaoFactory를 설정 정보로 주입하고 getBean()을 통해 UserDao오브젝트를 꺼내면 몇번을 꺼내든 꺼낸 오브젝트들은 모두 동일하다는 것을 알 수 있다.

 

이는 애플리케이션 컨텍스트가 싱글톤 레지스트리이기때문이다. 스프링은 기본적으로 별 다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트는 전부 싱글톤으로 만든다. 이때 나오는 싱글톤은 디자인 패턴에서 말하는 싱글톤 패턴과 비슷하지만 구현 방법이 다르다.

스프링이 사용되는 환경이 주로 서버환경이기에 싱글톤으로 빈을 생성한다. 만약 싱글톤을 적용하지 않는다면 오브젝트가 필요할때마다 객체가 생성되는데 싱글톤을 사용하면 이를 방지할수 있고 또 하나의 오브젝트를 공유할 수 있기에 싱글톤을 사용한다.

그러나 다음과 같은 단점으로 인해 싱글톤 패턴을 안티 패턴이라고도 부른다.

  1. private 생성자를 갖고 있기 때문에 상속할 수 없다. (스태틱 필드와 메소드를 이용하기 때문에 다형성 적용하기도 어렵다.)
  1. 싱글톤은 테스트하기 힘들다. (객체를 생성하는 방식이 제한적이기 때문)
  1. 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.(JVM이 여러개일 경우)
  1. 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

 

자바의 기존적인 싱글톤 패턴의 구현 방식은 위와 같은 여러가지 단점이 있기때문에 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하며 이를 싱글톤 레지스트리라 부른다.

이를 통해 평범한 자바 클래스(public 생성자를 지니고, 테스트를 자유롭게 할 수 있으며, 생성자 파라미터를 통한 DI) 싱글톤으로 사용할 수 있다.

 


 

스프링에서 싱글톤을 멀티스레드 환경에서 사용할때 주의해야 한다.

멀티스레드 환경에서 요청마다 싱글톤 객체에 값을 저장하고 읽는다면 저장하지 않은 값을 읽을 수 있다. 그렇기에 멀티 스레드 환경에서는 상태정보를 내부에 가지고 있지 않는 stateless 방식으로 만들어야 한다.

  • ❓만약 요청에 대한 정보나 DB나 서버의 리소스로부터 생성한 값이 필요할 경우…

    ❗파라미터나 로컬변수 리턴값을 이용하면 된다. 이러한 값들은 매번 새로운 값들이 생성되기 때문이다.

그리고 읽기 전용의 정보일 경우 인스턴스 변수를 생성해도 괜찮다.

혹은 다른 싱글톤 빈을 저장하기 위한 용도로 인스턴스 변수를 생성하는 것 역시 괜찮다.

 

싱글톤 타입의 오브젝트의 스코프는 하나의 스프링 컨테이너에 하나의 오브젝트만 만들어져서 강제로 제거하지 않는 이상 스프링 컨테이너가 존재하는 동안 계속 유지된다.

그러나 매 요청마다 새로운 오브젝트를 생성하는 방법인 프로토타입이 존재한다. http요청이 올때마다 생성되는 프로토타입 빈은 스프링 컨테이너에 의해 관리되지 않기때문에 사용을 마치고나면 명시적으로 제거하거나 혹은 GC에 의해 수거될때까지 유지 된다.

1.7 의존관계 주입(DI)

객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화한 것이 스프링의 IoC 컨테이너이다.

그러나 위와 같이 표현하면 다음과 같은 불명확한 점이 존재한다.

  1. 스프링이 ,서블릿 컨테이너처럼, 서버에서 동작하는 서비스 컨테이너인지
  1. 단순히 IoC개념이 적용된 템플릿 메소드 패턴을 이용해 만들어진 프레임 워크인지
  1. IoC특징을 지닌 또다른 기술인지

위와 같은 불명확한점이 존재하는 이유는 IoC가 매우 폭넓게 사용되기 때문이다.

그렇기에 스프링이 제공하는 IoC 방식의 핵심인 의존 관계 주입(DI, Dependency Injection)을 따서 DI컨테이너 라고 부른다.

💡
의존 관계 주입 DI(Dependency Injection) DI는 오브젝트 레퍼런스를 외부로부터 제공받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것을 의미한다.

 

의존관계

의존관계에는 방향성이 있다. 그리고 의존한다는 것은 의존대상이 변하면 의존대상에 의존하고 있는 오브젝트가 영향을 미친다는 것이다.

 

설계모델(UML, 코드 ,클래스)에서의 의존관계가 있는 반면 런타임시 오브젝트 사이에서 만들어지는 의존관계도 있다.

 

설계모델에서의 의존관계

이를 UserDao를 예시로 설명을 하면 UserDao는 인터페이스인 ConnectionMaker에 의존하고 있다.

그리고 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다.

인터페이스를 통해 의존관계를 제한해주면 그만큰 변경에서 자유로워지는 것이다.

 

런타임시 생성되는 의존관계

런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고한다. (여기서는 ConnectionMaker를 구현한 DConnectionMaker를 말한다.)

의존관계 주입은 이렇게 구체적인 의존 오브젝트(DConnectionMaker)와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를(UserDao) 런타임 시에 연결해주는 작업을 말한다.

 

정리하면 의존관계 주입은 아래의 3가지 조건을 충족하는 것을 말한다.

💡
조건
  1. 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에 의존하고 있어야 한다.
  1. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
  1. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 의부에서 제공(주입)해줌으로써 만들어진다.

의존관계 주입의 핵심은 설계 시점에는알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다. 그리고 스프링의 애플리케이션 컨텍스트, 빈팩토리, IoC컨테이너등이 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제3의 존재라고 볼 수 있다.

 

이전의 UserDao는 인터페이스를 통해서 ConnectionMaker와 의존관계를 느슨하게 하였지만 여전히

  1. 코드 상으로 구체적인 클래스를 UserDao가 알고 있는 문제가 존재했고
  1. UserDao가 이 구체적인 클래스의 사용을 결정하고 관리하는 문제점이 존재했다.
public UserDao(){
	connectionMaker = new DConnectionMaker();
}

그렇기에 IoC 방식을 사용해서

  1. UserDao가 런타임 의존관계를 드러내는 코드를 제거하고
  1. 제 3의 존재에 런타임 의존관계 결정 권한을 위임한다.

따라서 DaoFactory는 의존관계를 주입하는 주입하는 컨테이너라고 볼 수 있다.

그리고 주입은 일반적으로 생성자 메소드의 파라미터 오브젝트의 레퍼런스를 전달해주는 방법으로 이루어진다. (setter를 통해서 주입받을수도 있다.)

그리고 전달받은 객체의 레퍼런스를 인스턴스 변수에 저장하고 사용한다.

 

요약하면 DI는 자신이 사용할 오브젝트에 대한 선택과 생성제어권을외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다.

 

의존관계 검색과 주입

의존관계 주입(DI, Dependency Injection)이외에도 의존관계 검색(DL, Dependency Lookup)이라는 것이 존재한다. 이는 의존관계를 파라미터로 주입받는 것이 아니라 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는 것을 의미한다.

그리고 의존관계 검색(DependencyLookUp)은 스스로 컨테이너에 요청하는 방법으로 사용된다.

public UserDao(){
	DaoFactory daoFactory = new DaoFactory()
	this.connectionMaker = daoFactory.connectionMaker()
}

즉 외부에서 주입이 아니라 스스로 IoC 컨테이너인 DaoFactory에게 요청하는 것이다.

스프링IoC 컨테이너인 애플리케이션 컨텍스트는 getBean() 이라는 방식으로 의존관계 검색을 사용한다.

결과적으로 보았을때 의존관계 주입(DI)이나 의존관계 검색(DL)은 모두 동일한 결과를 내지만 의존관계 검색은 다음과 같은 단점이 존재하다.

 

“의존관계” 검색 방법은 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타나게 된다.

이는 애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 바람직하지 않다.

 

그렇지만 테스트 코드를 작성할때는 의존관계 검색을 사용해야만 한다. 테스트 코드에서 main() 메소드가 없기 때문에 DI를 통해서 의존관계 주입이 안되기 때문이다. 즉 스프링 컨테이너에 담긴 오브젝트를 사용하려면 의존관계 검색 방식을 이용해 사용해야 하는데 테스트 코드에는 main()메서드가 없어 의존관계 주입을 할 수 없다.

 

의존관계 검색(DL)에는 다음과 같은 장점도 있는데

검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다.

 

추가

➕의존관계를 주입 받는 메소드 파라미터가 이미 특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다. 그렇기에 인터페이스를 이용한 다형성으로 주입받아야한다.

➕스프링을 사용하면 LomBok의@RequiredArgsConstructor@Autowired는 의존관계를 맺어주지만 그 방식에 차이가 있다.

분류 @RequiredArgsConstructor @Autowired
특징 Lombok의 @RequiredArgsConstructor 은 의존관계를 맺어줄 경우 final이 붙은 인스턴스 변수에 의해서 자동으로 생성자를 만들어준다. 그리고 이는 파라미터를 통해 의존관계를 주입받는 방식이다. 스프링 IoC컨테이너인 애플리케이션 컨택스트에서 getBean()과 같이 의존관계 검색을 이용해서 의존관계를 맺어준다.
포인트 의존관계 주입(DI) 의존관계 검색(DL)

그렇기 때문에 Junit을 이용한 테스트 코드를 작성시에도 @RequiredArgsConstructor 를 통해서 코드를 작성할 경우 의존관계 주입이기 때문에 정상적으로 동작하지 않는다.

 

런타임 시에 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점을 다음과 같다.

💡
모든 객체지향 설계와 프로그래밍의 원칙을 따랐을 때 얻을 수 있는 장점을 모두 얻을 수 있다.

이에 대한 예시로

  1. 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않고
  1. 인터페이스를 통해 겹합도가 낮은 코드를 만든다.
  1. 다른 책임을 가진 사용의존관계이 있는 대상이 바뀌거나 변경되어도 자신은 영향받지 않는다.
  1. 변경을 통한 다양한 확장 방법에는 자유롭다

UserDao가 ConnectionMaker라는 인터페이스에만 의존하고 있다는 건 ConnectionMaker를 구현하기만 하고 있다면 어떤 오브젝트든지 사용할 수 있다는 뜻이다.

 

실제로 얻을 수 있는 이점들의 예시들이다.

  1. 기능 구현의 교환
    1. DB설정 코드를 DI방식을 통해 구현했다면 DB변경에 신경쓰지 않아도 된다.
  1. 부가기능 추가
    1. DB에 연결하는 총 횟수를 구하는 부가기능을 의존관계를 맺어주는 쪽에만 추가해주면 새로운 기능을 빠르게 구현할 수 있다.

 

메소드를 이용한 의존관계 주입

수정자(setter) 메소드를 이용해서 주입을 하기도 한다. 이 방식은 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다 내부의 메소드에서 사용하게 하는 DI방식에 활용하기 적당하다. 그러나 이는 주입받는 객체가 바뀔 가능성이 있을때만 사용해야 한다.

 

그리고 일반 메서드를 이용해서 주입을 하기도 하는데 이는 한번에 한 개의 파라미터만 가질수 있는 제약이 때문에 한번에 여러개를 갖는 일반 메소드를 DI용으로 사용할 수도 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

' > 토비의 스프링 3.1(1권, 완)' 카테고리의 다른 글

6장 AOP  (2) 2023.11.23
5장 서비스 추상화  (1) 2023.11.23
4장 예외  (1) 2023.11.02
3장 템플릿  (0) 2023.10.12
2장 테스트  (1) 2023.10.05