SPRING

[토비의 스프링] 오브젝트와 의존관계

혀내 2022. 10. 25. 23:16
반응형
초난감 DAO

DAO (Data Access Object)

DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

 

자바빈 (JavaBean, 빈)

다음 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다.

  • 디폴트 생성자
    • 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다.
    • 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
  • 프로퍼티
    • 자바빈이 노출하는 이름을 가진 속성을 말한다.
    • 수정자 메소드(setter)와 접근자 메소드(getter)를 이용해 수정/조회할 수 있다.

 

예시) UserDAO

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자.

JDBC를 이용하는 작업의 일반적인 순서는 다음과 같다.

- DB 연결을 위한 Connection을 가져온다.
- SQL을 담은 Statement(또는 PreparedStatement)를 만든다.
- 만들어진 Statement를 실행한다.
- 조회의 경우 SQL 쿼리 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
- 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
- JDBC API가 만드는 예외를 잡아 직접 처리하거나, throws를 선언해 메소드 밖으로 던지게 한다.

 

 

  • UserDAO 코드
public class UserDao {
	public void add(User user) throws ... {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "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 ... {
		...
	}
}

 

main()을 사용한 테스트 코드

 UserDao를 main()을 이용해 테스트해보자. 테스트는 통과하지만 사실 UserDao 코드에는 문제가 정말 많다. 왜일까?

 


DAO의 분리

관심사의 분리

  • 객체 지향의 세계에서는 모든 것이 변한다.
  • 분리확장을 고려한 설계가 필요하다.

 

분리?

  • 관심이 같은 것끼리는 한 객체 안으로 또는 친한 객체로 모이게 한다.
  • 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리한다.

 

커넥션 만들기의 추출

UserDao 코드의 add() 메소드 하나에서는 적어도 세 가지의 관심사항을 발견할 수 있다.

1. DB와 연결을 위한 커넥션을 가져오는 것
2. DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
3. 작업이 끝나면 사용한 리소스를 닫아주는 것

 

 

 

중복 코드의 메소드 추출

여러 메소드에서 중복되는 DB 연결 코드를 getConnection()이라는 독립적인 메소드로 만들자. (메소드 추출 기법)

public void add(User user) throws ... {
	Connection c = getConnection();
	...
}

private Connection getConnection() throws ... {
		Class.forName("com.mysql.jdbc.Driver");
		return DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
}

 

 

변경사항에 대한 검증: 리팩토링과 테스트

  • 코드를 수정한 후에 기능에 문제가 없다는 게 보장되지 않는다. 재검증이 필요하다.
  • 따라서 앞서 만든 main() 메소드 테스트를 다시 실행해본다.

 

 

DB 커넥션 만들기의 독립

  • UserDao를 사용하는 업체들이 각기 다른 종류의 DB를 사용한다면?
  • UserDao 소스코드를 고객에게 제공하지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용하도록 하고 싶다.

 

 

상속을 통한 확장

  • getConnection 메소드를 추상 메소드로 만든다.
  • 각 고객들은 UserDao 클래스를 상속하는 서브클래스를 만들어 getConnection 메소드를 구현한다.
public class UserDao {
	public abstract Connection getConnection() throws ... ;

	public void add(User user) throws ... {	
		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 ... {
		...
	}
}

⇒ 이제 UserDao는 변경이 용이할 뿐만 아니라 손쉽게 확장된다. (템플릿 메소드 패턴, 팩토리 메소드 패턴 사용)

 

 

 

그러나 상속은 많은 한계점이 있다.

  • 이미 UserDao가 다른 목적으로 상속을 사용하고 있다면?
  • 상속을 통한 상하위 클래스 관계는 생각보다 밀접하다. → 슈퍼 변경시 서브도 수정해야 함
  • DB 커넥션 생성 코드를 다른 DAO 클래스에 적용할 수 없다.

 

 


DAO의 확장

클래스의 분리

관심사가 다르고 변화의 성격, 주기가 다른 두 가지 코드를 아예 독립적인 클래스로 분리해보자.

public class UserDao {
	private SimpleConnectionMaker simpleConnectionMaker;

	public UserDao() {
		simpleConnectionMaker = new SimpleConnectionMaker();
	}

	public void add(User user) throws ... {
		Connection c = simpleConnectionMaker.makeNewConnection();
	}

	...
}
public class SimpleConnectionMaker {
	public Connection makeNewConnection() throws ... {
		Class.forName("com.mysql.jdbc.Driver");
		return DriverManager.getConnection(
									"jdbc:mysql://localhost/springbook", "spring", "book");
	}
}

 

  • UserDao 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되었다.
  • 고객에게 UserDao 클래스만 공급하고 상속을 통해 기능을 확장하는게 불가능해졌다.

 

 

인터페이스의 도입

클래스를 분리하면서도 두 클래스가 서로 긴밀하게 연결되어 있지 않도록 추상화(인터페이스)한다.

 

ConnectionMaker를 인터페이스로 정의하면,

  • UserDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 된다.
  • UserDao는 인터페이스의 메소드 기능만 관심을 가질 뿐, 기능 구현 방법은 몰라도 된다.

 

public interface SimpleConnectionMaker {
	public Connection makeNewConnection() throws ... ;
}

public class DConnectionMaker implements Connection Maker {
	public Connection makeNewConnection() throws ... {
		// D 사의 Connection 생성 코드
	}
}
public class UserDao {
	private ConnectionMaker connectionMaker;

	public UserDao() {
		connectionMaker = new DConnectionMaker(); // 클래스 이름이 나타남
	}

	...
}

⇒ UserDao 생성자 메소드를 직접 수정하지 않고는 자유롭게 확장할 수 없다.

 

 

 

관계설정 책임의 분리

  • UserDao의 클라이언트에서 직접 UserDao가 사용할 ConnectionMaker의 구현 클래스를 결정하도록 만들어보자.
  • UserDao의 생성자 파라미터를 인터페이스로 선언해 해당 인터페이스의 구현 클래스는 모두 사용할 수 있도록 한다.

 

즉, UserDao와 ConnectionMaker는 자신이 맡은 역할만 담당하고 다른 관심을 분리해 클라이언트에게 떠넘긴다.

 

 

public class UserDao {
	private ConnectionMaker connectionMaker;

	public UserDao(ConnectionMaker connectionMaker) {
		this.connectionMaker = connectionMaker; // 클래스 이름이 나타남
	}

	...
}

 

  • main() 메소드
// 클라이언트가 사용할 ConnectionMaker 구현 클래스 생성
ConnectionMaker connectionMaker = new DConnectionMaker();

// UserDao와 ConnectionMaker 두 오브젝트 사이의 의존관계 설정
UserDao dao = new UserDao(connectionMaker);

UserDao는 자신의 관심사인 SQL 생성과 실행에만 집중하게 되었다.

 

 

  • 결론적으로 인터페이스 도입은 상속보다 유연하다.
  • 다른 DAO 클래스에서도 ConnectionMaker 기능을 사용할 수 있기 때문이다.

 

 


원칙과 패턴

개방 폐쇄 원칙

  • OCP, Open-Closed Principle
  • 객체지향 설계 원칙 중 하나로 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

 

ex) UserDao

  • DB 연결 방법이라는 기능 확장에 열려있다.
  • UserDao의 핵심 기능을 구현한 add, get 코드는 변화에 영향을 받지 않기에 닫혀 있다.

 

 

 

높은 응집도와 낮은 결합도

  • 응집도가 높다
    • 한 모듈, 클래스가 하나의 책임/관심사에만 집중되어 있다.
    • 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다.
  • 결합도가 낮다
    • 책임과 관심사가 다른 오브젝트/모듈과는 느슨하게 연결된 형태를 유지한다.
    • 결합도: 한 오브젝트가 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도
    • 변화 대응 속도가 높아지고, 구성이 깔끔해진다. 확장에 편리하다.

 

전략 패턴

  • Strategy Pattern
  • UserDao는 전략 패턴의 컨텍스트에 해당한다.

 

 


제어의 역전(IoC)

오브젝트 팩토리

  • 현재 main() 메소드는 테스트 뿐만 아니라 의존관계를 설정하는 또 다른 책임까지 맡고 있다.

 

팩토리

  • 의존관계를 설정하는 역할의 새로운 클래스를 만들자!
  • factory: 객체 생성 방법을 결정하고 만들어진 오브젝트를 돌려준다.
public class DaoFactory {
	public UserDao userDao() {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao userDao = new UserDao(connectionMaker);
		return userDao;
	}
}

 

  • main 메소드
UserDao dao = new DaoFactory().userDao();

 

 

설계도로서의 팩토리

  • UserDao, ConnectionMaker: 각 애플리케이션의 핵심 로직
  • DaoFactory: 오브젝트 구성과 관계 정의 (설계도의 역할)

 

이제 ConnectionMaker 구현 클래스 변경이 필요하면 DaoFactory만 수정하면 된다.

 

핵심 로직을 담당하는 오브젝트와 구조를 결정하는 오브젝트를 분리해냈다!

 

 

 

오브젝트 팩토리의 활용

여러개의 DAO 클래스를 사용할 때 중복되는 ConnectionMaker 코드를 줄이자.

public class DaoFactory {
	public ConnectionMaker connectionMaker() {
		return new DConnectionMaker();
	}

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

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

 

 

제어권 이전을 통한 제어관계 역전

 

라이브러리 ↔ 프레임워크

  • 라이브러리
    • 애플리케이션 흐름을 직접 제어한다.
    • 필요한 기능이 있을 때 능동적으로 라이브러리를 사용한다.
  • 프레임워크
    • 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다.
    • 분명한 제어의 역전 개념이 적용되어 있다.

 

 

오브젝트 팩토리를 이용한 스프링 IoC

 

애플리케이션 컨텍스트와 설정정보

빈(Bean)

  • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
  • 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 IoC 적용

 

빈 팩토리(Bean Factory)

  • 빈 생성, 관계 설정 등 제어를 담당하는 IoC 오브젝트
  • == 애플리케이션 컨텍스트(application context)
  • 별도의 설정 정보를 담고 있는 무언가를 가져와 제어 작업 실행

 

 

DaoFactory를 사용하는 애플리케이션 컨텍스트

@Configuration
public class DaoFactory {
	@Bean
	public ConnectionMaker connectionMaker() {
		return new DConnectionMaker();
	}

	@Bean
	public UserDao userDao() {
		return new UserDao(connectionMaker);
	}

	@Bean
	public AccountDao accountDao() {
		return new UserDao(connectionMaker);
	}
}

 

  • main() 코드
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);

 

 

애플리케이션 컨텍스트의 동작방식

 

애플리케이션 컨텍스트의 또다른 이름

  • IoC 컨테이너
  • 스프링 컨테이너
  • 빈 팩토리

 

DaoFactory ↔ Application Context

  • DaoFactory
    • DAO 오브젝트 생성, 관계 맺어주기 담당
  • Application Context
    • IoC를 적용해 관리할 모든 오브젝트의 생성, 관계설정 담당

 

애플리케이션 컨텍스트의 장점

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
  2. 종합적인 IoC 서비스를 제공한다.
    • 오브젝트 생성, 다른 오브젝트와의 관계설정
    • 오브젝트의 생성 방식, 시점, 전략을 다르게 설정
    • 자동 생성, 후처리, 정보 조합, 설정 방식 다변화, 인터셉팅 등
  3. 다양한 빈 검색 방법을 제공한다.

 

 

 

스프링 IoC 용어 정리

  • 빈 팩토리
    • 스프링의 IoC를 담당하는 핵심 컨테이너
    • 보통 빈 팩토리를 바로 사용하지 않고 이를 확장한 애플리케이션 컨텍스트 이용
  • 애플리케이션 컨텍스트
    • 빈 팩토리를 확장한 IoC 컨테이너
    • 빈 등록, 관리 기능 + 스프링의 부가 서비스 추가 제공
  • 설정정보/설정 메타정보
    • IoC를 적용하기 위해 애플리케이션 컨텍스트가 사용하는 메타정보
    • 영어로 ‘configuration’
  • 컨테이너(IoC 컨테이너)
    • 애플리케이션 컨텍스트나 빈 팩토리의 다른 이름
  • 스프링 프레임워크
    • 위의 것들을 포함한 스프링의 모든 제공 기능

 

 

 

싱글톤 레지스트리로서의 애플리케이션 컨텍스트

  • 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 ‘싱글톤 레지스트리’이기도 하다.
  • 별다른 설정이 없으면 빈 오브젝트를 모두 싱글톤으로 만든다.

 

 

 

서버 애플리케이션과 싱글톤

  • 클라이언트에서 요청이 올 때마다 담당 오브젝트를 생성해 사용하면 한 시간에 몇 백만 개의 오브젝트가 만들어진다.
  • 그래서 엔터프라이즈 분야는 서비스 오브젝트라는 개념을 사용한다. ex) 서블릿
  • 이 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다.

 

싱글톤 패턴

  • 애플리케이션 내에서 어떤 클래스를 하나만 존재하도록 강제하는 패턴
  • 안티 패턴으로 풀리기도 한다.

 

 

싱글톤 패턴의 한계

  • private 생성자를 갖고 있어 상속할 수 없다.
  • 만들어지는 방식이 제한적이라 테스트하기가 힘들다.
  • 서버환경에서 싱글톤이 하나만 만들어지는 것을 보장할 수 없다.
  • 싱글톤 사용은 전역 상태를 만들 수 있어 바람직하지 못하다.

 

 

싱글톤 레지스트리

  • 스프링은 자바 구현 방식을 사용하지 않고 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다.
  • 싱글톤 레지스트리는 고전적인 싱글톤 패턴과 달리 아무 제약이 없다.

 

 

 

싱글톤과 오브젝트의 상태

  • 멀티스레드 환경에서 여러 스레드가 싱글톤에 동시에 접근할 수 있다.
  • 즉, 싱글톤은 상태 정보를 내부에 갖고 있지 않는 무상태 방식이어야 한다.
  • 무상태 방식: 각 요청에 대한 정보, 서버 리소스로부터 생성한 정보는 파라미터, 로컬 변수, 리턴 값등을 이용한다.

 

 

 

스프링 빈의 스코프

빈의 스코프

  • 빈이 생성되고, 존재하고, 적용되는 범위
1. 싱글톤 스코프: 기본 스코프
2. 프로토타입 스코프: 컨테이너에서 빈을 요청할 때마다 매번 새로운 오브젝트 생성
3. 요청 스코프: 새 HTTP 요청마다 생성
4. 세션 스코프: 웹 세션과 유사

 

 

 


의존관계 주입(DI)

런타임 의존관계 설정

의존한다?

  • 의존대상인 B가 변하면 그것이 A에 영향을 미친다.
  • ex) A가 B에 정의된 메소드를 호출해서 사용하는 경우

 

현재 UserDao 클래스는 ConnectionMaker 인터페이스에게만 직접 의존한다.

이렇게 인터페이스에만 의존관계를 만들면 구현 클래스와의 관계가 느슨해지면서 변화에 영향을 덜 받게 된다.

 

 

즉, 의존관계 주입은 다음 3가지 조건을 충족하는 작업이다.

1. 클래스 모델, 코드에는 런타임 시점의 의존관계가 드러나지 않는다. ⇒ 인터페이스에만 의존한다.
2. 런타임 시점 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 재공해줌으로써 만들어진다.

 

 

 

UserDao의 의존관계 주입

DaoFactory는 의존관계 주입(DI)을 이용한 클래스이다.

  • 의존관계 주입 주도
  • IoC 방식으로 오브젝트 생성, 초기화, 제공 등의 작업 수행

 

 

 

의존관계 검색과 주입

의존관계 검색(Dependency Lookup)

  • 스스로 의존관계를 검색해 맺는 방법이다.
public UserDao() {
	AnnotationConfigApplicationContext context = 
			new AnnotationConfigApplicationContext(DaoFactory.class);
	this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

 

  • DL의 단점
    • DL은 코드 안에 오브젝트 팩토리 클래스, 스프링 API가 함께 나타난다.
  • DL의 장점
    • 테스트 코드처럼 오브젝트를 가져와야 하는 경우가 발생한다.
    • DI와 달리 DL의 검색하는 오브젝트는 자신이 스프링 빈일 필요가 없다.

 

 

 

의존관계 주입 응용

기능 구현의 교환

개발용 로컬 DB에서 배포를 위해 서버 DB로 변경해야 하는 경우

  • DI를 사용하지 않으면, 코드를 전면 수정해야 함
  • DI를 사용하면,
    • ConnectionMaker 구현 클래스를 하나더 만들면 된다.
    • DaoFactory 코드는 connectionMaker 구현 클래스 설정 코드 1줄만 변경

 

 

부가기능 추가

  • DB 연결 횟수를 카운팅해보자.
    • DI를 사용하지 않으면 모든 DB 연결 코드 부분에 카운터 증가 코드를 넣어야 한다.
    • DI를 사용하면 연결 횟수 카운팅 기능이 있는 구현 클래스를 또 만들면 된다.

 

 

 

XML을 이용한 설정

connectionMaker() 전환

  자바 코드 설정정보 XML 설정 정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean methodName() <bean id=”methodName”
빈의 클래스 return new BeanClass(); class=”a.b.c… BeanClass”>

 

 

userDao() 전환

  • <property>: 의존 오브젝트와의 관계 정의
    • name: 프로퍼티 이름
    • ref: 수정자 메소드를 통해 주입할 오브젝트 빈 이름
<bean id="userDao" class="springbook.dao.UserDao">
	<property name="connectionMaker" ref="connectionMaker" />
</bean>

 

 

XML의 의존관계 주입 정보

<beans>
	<bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />
	<bean id="userDao" class="springbook.dao.UserDao">
		<property name="connectionMaker" ref="connectionMaker" />
	</bean>
</beans>

 

 

 

XML을 이용하는 애플리케이션 컨텍스트

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

 

 

 

DataSource 인터페이스 변환

자바에 이미 존재하는 DB 커넥션 용도의 DataSource 인터페이스를 사용한다.

public interface DataSource extends CommonDataSource, Wrapper {
	Connection getConnection() throws SQLException;
	...
}

 

 

자바 코드 설정 방식

@Bean
public DataSource dataSource() {
	SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

	dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
	dataSource.setUrl("jdbc:mysql://localhost:springbook");
	dataSource.setUsername("spring");
	dataSource.setPassword("book");
	
	return dataSource;
}

@Bean
public UserDao userDao() {
	UserDao userDao = new UserDao();
	userDao.setDataSource(dataSource());
	return UserDao;
}

 

 

XML 설정 방식

<bean id="dataSource"
	class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />

⇒ 의존관계 주입 정보는 나와있지 않다.

 

 

 

 

프로퍼티 값 주입

<property name"driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/springbook" />
<property name="username" value="spring" />
<property name="password" value="book" />
  • 모두 스트링 타입으로 연결 정보를 넣어주고 있다.
  • com.mysql.jdbc.Driver 은 스프링이 setter 파라미터 타입을 참고해 자동 타입 변환
반응형