지나공 : 지식을 나누는 공간

Spring : 오브젝트와 의존 관계 ~ IoC 제어의 역전까지 본문

Tech/Spring

Spring : 오브젝트와 의존 관계 ~ IoC 제어의 역전까지

해리리_ 2022. 10. 23. 15:54

토비의 스프링을 읽고 정리했다. 티스토리가 사라져버린 그 날에 노션에 정리했던 걸 옮겨본다.

https://selective-bathtub-e65.notion.site/f72b75db1b8945e089d00b01c9bb5ada

 

 

목차


 

 

목차만 보고 절차를 대충 상기할 수 있으므로 목차는 매우 중요하다...ㅎㅎ

 

1.1 초난감 DAO


DAO : 데이터를 조회하거나 조작하는 기능을 전담하는 오브젝트

 

책에서 초반에 DAO를 하나 만들었다. 데이터를 추가하는 add 메소드 안에는 세 가지 관심사항이 같이 들어있다.

  1. DB Connection을 가져오는 부분
  2. 실제 SQL Statement를 만들고 실행하는 부분
  3. 작업을 마친 뒤 Statement와 Connection 오브게트를 닫는 부분

→ 왜 초난감 DAO인걸까? 문제점이 뭔가?

모든 관심사가 하나의 DAO 클래스에서 이루어지고 있는 것이 문제임.

 

좋은 개발은, 변경이 일어날 때 필요한 작업을 최소화하고 그 변경이 다른 곳에 문제를 일으키지 않도록 하는 것이다. 변화는 대체로 집중된 한 가지 관심에 대해 일어나지만, 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다. 단지 DB 접속용 암호를 변경하려고 DAO 클래스 수백 개를 모두 수정해야 한다면 정말 끔찍함.

 

관심사의 분리가 필요하다

그러므로 우리가 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것. 관심이 같은 것끼리 모으고, 관심이 다른 건 서로 영향을 주지 않도록 떨어져 있게 하는 것이 필요하다.

 

1.2 DAO의 분리 : 추상클래스와 상속 활용


위에서 DAO의 관심사가 너무 많이 한 곳에 몰려 있었다. 일단 중복되어 있는 Connection 가져오는 작업부터 분리를 시작해서, 확장까지 해보자.

 

1) ‘메소드’로 DB 커넥션 가져오는 부분 분리

add에도 있고 get에도 있는 중복되는 코드다. 그러므로 getConnection() 이라는 독립적인 메소드를 만들자.

매우 간소화한 코드임.

void add () {
	Connection c = getConnection();
}

void get () {
	Connection c = getConnection();
}

Coonection getConnection() {
	return connection;
}

 

2) ‘추상클래스 상속’으로 DB 커넥션 가져오는 부분 분리

메소드 추출만으로도 변화에 좀 더 유연하게 대처할 수 있지만, 변화에 대응하는 수준을 넘어서 아예 변화를 반기는 DAO를 만들자.

 

상황 가정 :

우리가 만든 DAO가 초슈퍼울트라캡짱 사용자관리 DAO가 되어서 인기를 얻었다고 가정하자. 급기야 N사와 D사에서 이 UserDAO를 구매하고 싶다고 한다. 근데 각 고객사는 서로 다른 종류의 DB를 사용하고 있고, 커넥션 가져오는 방법도 다르다.

 

이제 getConnection을 고객사가 원하는대로 구현할 수 있다.

 


그래서 이제 getConnection 메소드 내부를 각 고객사가 구현해보자.

  • 추상 클래스인 UserDao는 그저 getConnection이 Connection 타입을 반환한다는 기능에만 관심이 있다.
  • 각 NUserDao나 DuserDao들은 구체적으로 어떤 식으로 Connection 기능을 제공할건지에 관심이 있다.
    • 서버의 DB 커넥션 풀에서 가져올 수도 있고
    • 드라이버를 직접 이용해서 새로운 커넥션을 만들 수도 있다.

즉, UserDao는 Connection 오브젝트가 만들어지는 방법과 내부 동작에는 관심 없이, 자신이 필요한 기능을 Connection 인터페이스를 통해 사용하기만 할 뿐이다.

 

* 템플릿메소드패턴
부모 클래스에 getConnection()처럼 서브클래스가 상속 받아서 필요에 맞게 구현하여 사용하게 하는 방법.

 

*팩토리메소드패턴
오브젝트를 어떻게 생성할건지 구체적인 방법을 서브클래스에서 결정하게 하는 것.
서브 클래스에서 getConnection을 통해 만들어진 Connection 오브젝트의 종류가 달라질 수도 있게 하려는 것이 팩토리메소드패턴의 목적이다. NUserDao랑 DUserDao가 혹시 같은 종류의 Connection 구현체를 리턴한다 할지라도 그 구현체를 생성하는 방식이 다르다면 그것도 팩토리메소드패턴이라고 볼 수 있다.

참고로, 그냥 자바에서 오브젝트를 생성하는 기능을 가진 메소드를 부를 때 쓰는 그 ‘팩토리메소드’와는 다르다. 현재처럼 슈퍼클래스가 본인이 필요한 오브젝트를 사용할 때, 서브클래스에서 구현할 메소드를 호출해서 그 오브젝트를 가져오는 것이 팩토리메소드패턴임. 구체적인 오브젝트 생성 방법을 서브클래스가 결정하게 했으니까 서브클래스가 구현한 그 메소드를 호출해서 쓰는 것임.

 

1.3 DAO의 확장


1) 클래스로 Connection Maker 분리 : Connection을 한번만 만들자

일단 지금까지 DB 커넥션과 관련된 부분을 서브클래스에서 해결하게 했는데, 이걸 한번 더 분리해서 아예 별도 클래스에서 맡게 하자.

  • 각 메소드에서 매번 Connection을 만들 필요가 없이 한번만 만들어두고 저장해놓은 상태에서 계속 이를 사용할 수 있다.
    • 이제는 더이상 상속을 통한 확장 방식을 쓰지 않으니 추상 클래스를 만들 필요가 없어졌다.
  • 이제는 특정하게 SimpleConnectionMaker라는 클래스로 정해져버려서 DB 커넥션 생성 기능을 마음대로 수정할 수가 없다.
    • DB 커넥션을 제공하는 클래스를 사용하려면 UserDao의 소스코드를 직접 수정해서 Connection 객체를 바꿔야 하니까 맨 처음처럼 UserDao 소스코드 없이는 DB 연결 방법을 바꿀 수 없다는 처음의 문제로 돌아왔다.

 

2) 클래스 분리 시의 문제점 정리

1. SimpleConnectionMaker의 메소드인 makeNewConnection()을 통해 DB 커넥션을 가져왔는데, 만약 D사는 openConnection()이라는 이름을 사용했다면 UserDao내의 add(), get() 메소드에서 커넥션 가져오는 코드들을 다 변경해야 한다.

Conection c = simpleConnectionMaker.openConnection(); 
Conection c = simpleConnectionMaker.makeNewConnection();

 

2. DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있는 것이 문제다. 그래서 N사에서 만약 아예 다른 클래스를 구현한다면 어쩔 수 없이 UserDao 코드 자체를 또 수정해야 한다.

 

근본 원인은, UserDao가 바뀔 수 있는 정보 (=DB커넥션을 가져오는 클래스)에 대해서 너무 많이 알고 있다는 점이다. 어떤 클래스가 쓰이는지, 그 클래스에서 커넥션 가져오는 메소드의 이름이 뭔지 등을 모두 알고 있어야 하니까 UserDao는 현재 DB 커넥션을 가져오는 구체적인 방법에 종속되어 있다.

 

3) 인터페이스를 도입하자

위처럼 이런 인터페이스를 만들어 놓고 DB커넥션을 가져오는 메소드를 makeConnection()이라고 정하자.

 

→ 이 인터페이스를 사용하는 UserDao 입장에서는 ConnectionMaker 인터페이스 타입의 오브젝트라면 어떤 클래스로 만들어졌든지 상관 없이 makeConnection 메소드만 호출하면 커넥션 타입의 오브젝트를 받을 수 있다.

 

--- ConnectionMaker 인터페이스 ----
public interface ConnectionMaker {
	public Connection makeConnection();
}

--- D사와 N사의 ConnectionMaker 구현체 ---

public class DConnectionMaker implements ConnectionMaker{
	public Connection makeConnection() {
		// D사의 독자적인 방법으로 Connection을 생성하는 코드	
	}
}

public class NConnectionMaker implements ConnectionMaker{
	public Connection makeConnection() {
		// N사의 독자적인 방법으로 Connection을 생성하는 코드	
	}
}

--- 인터페이스 사용으로 개선된 UserDao ---

public class UserDao {
	private ConnectionMaker connectionMaker;
	
	public UserDao() {
		ConnectionMaker = new ConnectionMaker(); -> 앗!! 근데 여기엔 구체적 클래스명이 나옴!!
	}

	public void add() {
		Connection c = connectionMaker.makeConnection(); 
		-> 클래스가 바뀌더라도 이제 메소드 이름이 바뀌진 않는다.
	}

	public void get() {
		Connection c = connectionMaker.makeConnection(); 
		-> 클래스가 바뀌더라도 이제 메소드 이름이 바뀌진 않는다.
	}
}

 

4) 아직도 UserDao는 구현체를 알고 있다. 이걸 분리해보자!

이렇게 열심히 분리했는데 왜 아직도! UserDao가 인터페이스 외에 구현체 이름까지 알고 있는 문제가 발생할까?

//UserDao.java 속 코드
ConnectionMaker = new ConnectionMaker(); -> 앗!! 근데 여기엔 구체적 클래스명이 나옴!!

→ 그 이유는, 여전히 UserDao에는 어떤 ConnectionMaker 구현체를 사용할 건지 결정하는 코드가 남아있어서다.

→ 즉, UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 대한 관심이 아직 UserDao에서 분리되지 않아서다. 얘를 분리해야 UserDao가 비로소 독립적으로 확장 가능한 클래스가 될 수 있다.

 

5) 관계 설정 책임의 분리 : 클라이언트한테 떠넘기자!

UserDao를 사용하는 클라이언트가 있을 것이다.

바로 이 UserDao가 제 3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스의 관계를 결정해주는 기능을 분리해서 두기에 적절한 곳이다.

 

→ 따라서, UserDao의 클라이언트에서 UserDao를 사용하기 전에 먼저 UserDao가 어떤 ConnctionMaker의 구현 클래스를 사용할 지를 결정해주자.

--- UserDao의 생성자 수정 ---
public UserDao(ConnectionMaker connectionMaker) {
	this.connectionMaker = connectionMaker;
}

--- 관계 설정의 책임을 맡게 된 UserDao의 클라이언트  --- 
public class UserDaoTest {
	public static void main(String [] args) {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao dao = new UserDao(connectionMaker);
	}
}

1. UserDao 생성
2. 사용할 ConnectionMaker 타입의 오브젝트를 제공.

결국 두 오브젝트 사이의 의존관계 설정 효과
객체지향 프로그래밍의 ‘다형성’ | 클래스 사이 관계 vs 오브젝트 사이 관계

기존에는 클래스 사이에 관계가 만들어져 있었는데, 이걸 클래스가 아닌 오브젝트끼리의 관계를 만드는 것으로 바꿨다. 둘 차이를 잘 구분해야 한다.

- 클래스 사이의 관계 :
코드에 다른 클래스 이름이 나타나기 때문에 만들어진다.

- 오브젝트 사이의 관계 :
코드에서 특정 클래스를 전혀 알지 못하더라도, 해당 클래스가 구현한 인터페이스를 활용할 수 있다. 받으려는 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용이 가능하다!

 

오브젝트 사이에 런타임 사용관계가 만들어졌다. 모델링 시에는 없었던, 그래서 코드에는 보이지 않던 관계가 오브젝트로 만들어진 이후에 생성된 것이다.

 

6) 클라이언트의 책임 정리

첫째 그림 같은 구조의 클래스들을 이용해서 아래 그림과 같은 런타임 오브젝트 관계를 갖는 구조로 만들어주는 게 클라이언트의 책임이다.

 

 

UserDao의 클라이언트까지 같이 구조를 그리면 아래와 같은 그림이 된다.

 

이제 UserDao는 ConnectionMaker라는 인터페이스를 알 뿐, 구체적인 클래스는 외부에서 주입받는다.

7) UserDao를 개선하면서 쓰인 원칙과 패턴들 정리

 

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

클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

  • UserDao는 DB 연결 방법이라는 기능을 확장하는 데에 열려 있다.
    • UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있다.
  • userDao의 핵심 기능을 구현한 코드는 DB 연결 방법에 변화가 생기더라도 영향을 받지 않고 유지되므로 변경에는 닫혀 있다.

 

높은 응집도와 낮은 결합도

높은 응집도

  • 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.
  • 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로도 설명할 수 있다. 즉 변경이 일어날 때 해당 모듈에서 많은 부분이 함께 바뀐다면 그게 응집도가 높은 것이다.
    • 만약 모듈의 일부에서만 변경이 일어나도 되는 거라면, 전체에서 어느 부분이 바뀌어야 하는지 파악해야 하고, 또 그 변경으로 인해 바뀌지 않는 부분에 다른 영향을 미치지는 않는지 이중으로 확인해야 하는 부담이 있다.
  • ConnectionMaker 인터페이스로 DB 연결 기능을 독립시켰기 때문에, 구현 클래스를 새로 만들기만 하면 된다.

→ 작업이 항상 전체적으로 일어나고 무엇을 변경할지 명확하며, 다른 클래스의 수정을 요구하지 않는다.

 

낮은 결합도

  • 하나의 변경이 발생할 때 다른 모듈과 객체로까지 변경에 대한 요구가 전파되지 않는 상태
  • UserDao는 구체적인 ConnectionMaker 구현 클래스를 알 필요도 없고, 구현 방법이나 전략, 뒤에서 사용하는 또다른 오브젝트에 대해서도 신경 쓰지 않아도 된다.

 

전략패턴

필요에 따라 변경이 필요한 알고리즘(= 독립적인 책임으로 분리 가능한 기능)을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴

전략패턴의 구성요소

  • 컨텍스트
    • 자신의 기능을 수행하는 데 필요한 기능 중 변경 가능한 알고리즘을 인터페이스로 정의하고, 이를 구현한 클래스(전략)을 바꿔가면서 사용한다.
    • UserDao
  • 클라이언트
    • 컨텍스트가 사용할 구체적인 전략을 생성자 등을 통해 제공해준다.
    • UserDaoTest
  • 전략
    • 알고리즘 인터페이스를 구현한 실제 구현 클래스.
    • 변경 가능한, 독립적인 책임으로 분리 가능한 기능.
    • DConnectionMaker, NConnectionMaker

 

1.4 제어의 역전(IoC)


1) 오브젝트 팩토리 만들기 : 설계도로서의 팩토리

사실 UserDaoTest는 UserDao의 기능이 잘 동작하는지 테스트를 하려고 만든 것인데 현재까지의 리팩토링 과정에서 엉겁결에 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능까지 떠맡았다. 그러므로 아래 세 가지를 분리하자.

  • UserDao 오브젝트를 만드는 것
  • ConnectionMaker 구현 클래스의 오브젝트를 만드는 것
  • 위의 두 오브젝트가 연결되어서 사용될 수 있도록 관계를 맺어주는 것

팩토리

객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 일을 하는 오브젝트.

이건 단순히 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 책임을 깔끔하게 분리한느 목적이으로, 어떻게 만들지와 어떻게 사용할지랑은 다른 관심이다.

--- 기존의 UserDaoTest랑 팩토리를 사용하도록 수정한 UserDaoTest 비교 ---
-- 기존 --
public class UserDaoTest {
	public static void main(String[] args) {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao userDao = new UserDao(connectionMaker);
	}
}

-- 개선된 --
public class UserDaoTest {
	public static void main(String[] args) {
		UserDao userDao = new DaoFactory.userDao;
	}
}

--- 팩토리 클래스 : UserDao를 생성하는 책임 ---
puvlic class DaoFactory {
	public UserDao userDao() {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao useDao = new UserDao(connectionMaker);
		return userDao;
	}
}

 

팩토리는 설계도?!

  • 컴포넌트 : UserDao와 ConnectionMaker는 각각 애플리케이션의 핵심 기술 로직을 담당한다.
  • 설계도 : DaoFactory는 이런 애플리케이션의 오브젝트들을 구성하고 그 관계를 정의하는 책임을 가진다.

DaoFactory로 분리했을 때 얻을 수 있는 장점은 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리할 수 있다는 것이다.

 

2) 오브젝트 팩토리 활용 : Dao가 많아진다면?

DAO의 종류가 많아지면 DAO를 생성하는 메소드를 많이 만들어야 한다. 근데 현재는 Dao를 생성할 때에 ConnectionMaker 구현 클래스를 결정해주기 때문에 구현 클래스가 바뀌면 모든 Dao 생성 메소드의 코드도 바뀌어야 한다.

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 구현 클래스를 생성하는 부분을 분리할 수 있다.

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

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

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

--- 중복을 분리해낸 부분 ---
public ConnectionMaker connectionMaker() {
	return new DConnectionMaker();
}

3) 제어권 이전으로 제어관계 역전

일반적인 프로그램의 흐름

  1. main과 같이 프로그램이 시작되는 지점에서 다음에 사용될 오브젝트를 결정
  2. 결정된 오브젝트를 생성
  3. 생성된 오브젝트의 메소드를 호출
  4. 그 메소드 안에서 다음에 사용할 것을 결정해서 호출

각 오브젝트들은 프로그램 흐름을 결정하거나 직접 생성하면서 사용할 오브젝트를 구성하는 작업에 능독적으로 참여한다.

이 흐름을 리팩토링하기 전 맨 초반의 UserDao에 대입해보자

  1. main() 메소드는 UserDao 클래스의 오브젝트를 직접 생성하고
  2. 만들어진 UserDao 오브젝트의 메소드를 사용한다.
  3. UserDao는 자신이 쓸 ConnectionMaker 구현 클래스를 직접 결정하고
  4. 이를 필요한 시점에 생성하고 메소드를 호출한다.

제어의 역전이란, 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다.

  • 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지도, 생성하지도 않는다.
  • 자기 자신도 어떻게 만들어져서 어디서 사용되는지를 알 수 없다.

프로그램 시작을 위한 main 메소드를 제외하고는, 자기 자신이 아니라 제어 권한을 가진 특별한 오브젝트에 의해 결정되고 만들어진다.

 

 

제어의 역전 예시

  • 기존에 추상 UserDao를 상속한 서브 클래스가 getConnection을 구현해놨을 때 이 메소드가 언제 어디서 사용될지 자기 자신은 모른다. 슈퍼 클래스인 UserDao 속 템플릿 메소드 add나 get에서 필요할 때 호출해서 사용한다. 즉 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되도록 한다.
  • UserDao가 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만들었었는데 이제 이 제어권이 DaoFactory에게 있다. 그래서 UserDao 자신도 팩토리에 의해 수동적으로 만들어지고 자신이 사용할 오브젝트도 DaoFactory가 공급해주는 걸 수동적으로 사용한다.
  • 프레임워크와 라이브러리
    • 라이브러리는, 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 툴킷, 엔진, 라이브러리는 프레임워크가 아니다!
    • 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용되기 때문에, 보통 프레임워크 위에 개발한 클래스들을 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만든다.

 

제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계 설정, 사용, 생명주기 등을 관장하는 존재가 필요하다. 위에서 이런 설계도 역할이 DaoFactory였다.

 

→ DaoFactory는, 오브젝트 수준의 가장 단순한 IoC 컨테이너 또는 IoC 프레임워크라고 할 수 있다.

728x90
Comments