[토비의 스프링] DAO의 분리 / 확장

706__·2021년 3월 5일
2

Toby's Spring

목록 보기
1/1
post-thumbnail

SpringJava 를 기반으로 한 기술로써, Java language 에서 가장 중요하게 가치를 두는 것은 바로 객체 지향 프로그래밍이 가능한 언어라는 점이다. Java Enterprise 기술의 혼란 속에서 잃어버렸던 객체 지향 기술의 진정한 가치를 회복시키고, 그로부터 객체 지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가는 것이 바로 Spring의 핵심 철학이다.

Object

Spring 이 가장 관심을 많이 두는 대상은 Object 이다. Spring 을 제대로 이해하려면 먼저 Object 에 깊은 관심을 두어야 한다. Application 에서 Object 가 생성되고 다른 Object 와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 진지하게 생각해볼 필요가 있다. 더 나아가서 Object 는 어떻게 설계되어야 하는지, 어떤 단위로 만들어지며, 어떤 과정을 통해 자신의 존재를 드러내고 등장해야 하는지에 대해서도 살펴보아야 한다.

Object에 대한 관심이란?

  • ObjectLife Cycle
    • Object의 생성 / 소멸
    • 타 Object와의 관계 맺기 / 사용

  • Object 의 설계
    • OOD : Object Oriented Design 원칙 / 기초
    • Design Pattern : 다양한 목적에 부합되게 재활용 가능한 설계
    • Refactoring : 지속적인 구조 개선
    • Unit Test : 동작이 기대한 대로 되는지 검증

초난감 DAO

🏷 DAO Data Access Object

DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 Object 를 말한다.

사용자 정보를 JDBC API를 이용해서 DB에 저장 / 조회할 수 있는 간단한 DAO를 예로 들어보자.

User

사용자 정보를 저장할 때는 Java Bean 규약을 따르는 Object 를 이용하면 편리하다. 먼저 사용자 정보를 저장할 User Class를 만들었다.


🅱️ Java Bean 규약

Java Bean은 다음 두 가지 관례를 따라서 만들어진 Object 를 가리킨다. 간단히 Bean 이라고도 칭한다.

  • Default Constructor
    파라미터가 없는 기본 생성자를 갖고 있어야 한다. 프레임워크에서 Reflection을 이용해 Object를 생성하기 때문에 필요하다.
  • Property
    Java Bean이 노출하는 이름을 가진 속성을 말한다. set 으로 시작하는 수정자 메소드 setter, get 으로 시작하는 접근자 메소드 getter를 이용해 수정 / 조회할 수 있다.

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 에 담긴 정보가 실제로 보관될 DB의 테이블을 만들어보자.

UserDAO

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자. 우선, 새로운 사용자를 생성 Add / 아이디를 통해 사용자의 정보를 조회 Get 하는 두 개의 method를 먼저 만들겠다.


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

  • DB 연결을 위한 Connection 을 가져온다.
  • SQL을 담은 Statement / PreparedStatement 를 만든다.
  • 만들어진 Statement 를 실행한다.
  • 조회의 경우, SQL Query의 실행 결과를 ResultSet 으로 받아서 정보를 저장할 Object 에 옮겨준다.
  • 작업 중에 생성된 Resource 들은 작업을 마친 후 반드시 닫아준다.
  • JDBC API가 만들어내는 Exception 을 잡아서 직접 처리하거나, method에 throws 를 선언해서 예외가 발생하면 method 밖으로 던지게 한다.

일단 예외는 모두 method 밖으로 던져버리는 편이 간단하다.

package springbook.user.dao;

...

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", "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", "spring", "book");
                
		PreparedStatement ps = c.prepareStatement(
        		"Select into 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;
	}
}

제법 복잡해 보이는 코드는 완성되었다. 코드를 눈으로만 확인하고서 잘 동작할 것이라고 안심하기엔 좀 꺼림칙하다. 이 클래스가 제대로 동작하는지 어떻게 확인할 수 있을까?

main()을 이용한 DAO Test Code

만들어진 코드의 기능을 검증하고자 할 때 사용할 수 있는 가장 간단한 방법은 Object 스스로 자신을 검증하도록 만들어주는 것이다. 모든 클래스에는 자신을 Entry Point 로 설정해 직접 실행가능하게 해주는 Static method main() 이 있으니까 말이다!

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

완성한 main() method를 실행하면, 다음과 같은 Test 성공 메세지가 출력될 것이다. 실패했을 경우, DB 설정과 Connection 정보, User 테이블 등록 여부 등을 확인해보면 된다.

whiteship 등록 성공
백기선
marreid
whiteship 조회 성공

이렇게 해서 사용자 정보의 등록과 조회가 되는 초간단 DAO와 Test method까지 완성했다. 그런데 지금 완성한 UserDao 클래스는 여러 가지 문제점을 가지는 한심한 코드이다. 지금부터 이 문제 많은 초난감 DAO를 객체 지향 기술의 원리에 충실한 멋진 Spring 스타일로 개선해볼 것이다.

DAO의 분리

세상에는 변하는 것과 변하지 않는 것이 있다. 그렇지만 객체지향의 세계에서는 모든 것이 변한다. Object 에 대한 설계와 이를 구현한 코드는 계속해서 변한다. 소프트웨어 개발에서 끝이란 개념은 없다. 사용자의 Bussiness Process와 그에 따른 요구사항은 끊임없이 바뀌고 발전하며, Application을 기반으로 하는 기술 / 운영되는 환경도 시간이 지남에 따라 변화한다. 그렇기 때문에 개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할것인가! 이다.

결과적으로, 미래를 준비하는 데 있어 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.

두 명의 개발자에게 동일한 기능 변경을 요청했다고 하자. 당연하지만, 최소한의, 확신을 가진 작업만으로도 기능을 수정해낸 개발자가 미래의 변화를 더욱 잘 준비한 것이다. 그러면 어떻게 변경이 일어날 때, 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있을까?

그것은 분리와 확장을 고려한 설계가 있었기 때문이다.

관심사의 분리

먼저 분리에 대해 생각해보자. 변경에 대한 요청은 "DB를 Oracle에서 MySQL로 바꾸면서, 웹 화면의 Layout을 다중 프레임 구조에서 단일 프레임에 Ajax를 적용한 구조로 바꾸고..."와 같은 식으로 발생하지는 않는다. 무슨 얘긴가 하면, 모든 변경과 발전은 한번에 한 가지 관심사항에 집중해서 일어난다는 뜻이다.

문제는, 변화는 대체로 집중된 한 가지 관심에 대해 일어나지만, 그에 따른 작업은 한 곳에 집중되지 않는 경우가 대부분이라는 점이다.

단지 DB 접속용 암호를 변경하기 위해 DAO 클래스 수백 개를 모두 수정해야 한다면? Transaction 기술을 다른 것으로 바꿨다고 Bussiness Logic 이 담긴 코드의 구조를 모두 변경해야 한다면? 또는 다른 개발자가 개발한 코드에 변경이 일어날 때마다 내가 만든 클래스도 함께 수정을 해줘야 한다면?

변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것이다. 즉 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.

이러한 개념을 프로그래밍 기초 개념 중 관심사의 분리라고 한다.

Connection 만들기의 추출

UserDao 의 구현된 method를 다시 살펴보자. 자세히 들여다보면 add() method 하나에서만 적어도 세 개의 관심사항을 발견할 수 있었다.


UserDAO의 관심사항

  • DB와 연결을 위한 Connection을 어떻게 가져올까?

  • 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하자!

    • 여기서의 관심은 파라미터로 넘어온 사용자의 정보를 Statement 에 바인딩시키고, 담겨진 SQL을 DB를 통해 실행시키는 방법이다.
  • 작업이 끝나면 사용한 Resource 들을 닫아 시스템에 돌려주자!


가장 문제가 되는 부분은 첫째 관심사인 DB 연결을 위한 Connection Object 를 가져오는 부분이다. 현재 이 부분은 다른 관심사와 섞여서 같은 add() method에 담겨 있다. 더 큰 문제는 add() method에 있는 DB Connection 코드는 get() method에도 중복되어 있다는 점이다. 이와 같이 하나의 관심사가 방만하게 중복되어 있고, 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면, 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다.

중복 코드의 method 추출

가장 먼저 할 일은 Connection 을 가져오는 중복된 코드를 분리하는 것이다. 중복된 DB 연결 코드를 getConnection() 라는 이름의 독립적인 method로 만들어보자.

public void add(User user) throws ClassNotFoundException, SQLException {

	Connection c = getConnection();
	...
}

public User get(String id) throws ClassNotFoundException, SQLException {

	Connection c = getConnection();
	...
}

private Connection getConnection() throws ClassNotFoundException, SQLException {

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

UserDao 클래스의 method가 수천 개쯤 된다고 상상해보자. DB 연결과 관련된 부분에 변경이 일어난 경우, 예를 들어 DB 종류와 접속 방법이 변경되었다거나, 로그인 정보가 변경되더라도 getConnection() 한 method의 코드만 수정하면 된다. 관심이 다른 코드가 존재하는 method에는 영향을 주지도 않을뿐더러, 관심 내용이 독립적으로 존재하므로 수정 역시 간단해졌다.

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

다음과 같이 코드를 수정한 후에는 기능에 문제가 없다는 것이 보장되지 않는다. 다시 검증이 필요하다. 변경된 UserDao 의 기능이 변경 전과 동일한지 확인해보려면 어떻게 해야할까?

방법은 간단하다. 앞에서 만들어둔 main() method를 이용한 테스트를 실행해보면 된다. 현재 main() method에는 한 가지 단점이 있는데, 여러 번 실행할 경우 무조건 예외가 발생한다는 점이다. 테이블의 PK인 id 값이 중복되기 때문이다. 따라서 재실행하기 위해서는 User 테이블의 사용자 정보를 모두 삭제해줘야 한다.

whiteship 등록 성공
백기선
marreid
whiteship 조회 성공

다음과 같이 동일한 결과가 출력된 것을 확인하였다. 이로써 관심사의 분리를 개선한 코드로 한 걸음 내딛게 되었다.

방금 한 작업들은 UserDao 의 기능에는 아무런 변화를 주지 않았다. 하지만 중요한 변화가 있었다. 바로, 여러 method에 중복돼서 등장하는 특정 관심사항이 담긴 코드를 별도의 method로 분리해낸 것이다. 이 작업은 기능에는 영향을 주지 않으면서 코드의 구조만 변경한다. 이런 작업을 리팩토링 Refactoring 이라고 한다. 또한, 위에서 사용한 getConnection() 의 공통의 기능을 담당하는 method로 중복된 코드를 뽑아내는 것을 Refactoring 에서 method 추출 기법이라고 부른다.

🏷 리팩토링 Refactoring

기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 / 기술을 말한다. 이 작업을 함으로써 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다. 결국 생산성은 올라가고, 코드의 품질은 높아지며, 유지 보수에 용이해지고, 견고하면서도 유연한 제품을 개발할 수 있다.

Refactoring 은 객체지향 개발자라면 반드시 익혀야 하는 기법이다. 초난감 DAO 코드를 개선해나가는 과정에서, 기능을 추가하고 변경하기보다는, 겉으로 드러나는 기능은 그대로이지만 코드 구조와 구현 방법을 바꿈으로써 더 나은 DAO를 만드는게 주력해보자.

DB Connection 만들기의 독립

개선된 코드를 보면, 아주 초보적인 관심사의 분리작업이지만, method 추출만으로도 변호에 좀 더 유연하게 대처할 수 있게 되었다. 이번엔 좀 더 나아가서 변화에 대응하는 수준이 아니라, 아예 변화를 반기는 DAO를 만들어보자.

앞에서 만든 UserDao 가 발전에 발전을 거듭해서, 업계에 널리 알려지면서 N 사와 D 사에서 사용자 관리를 위해 이 UserDao 를 구매하겠다는 주문이 들어왔다. 그런데 납품 과정에서 문제가 발생했다. 문제는 N 사와 D 사가 각기 다른 종류의 DB를 사용하고 있고, DB Connection을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 더욱 큰 문제는 UserDao 를 구매한 이후에도 DB Connection을 가져오는 방법이 종종 변경될 가능성이 있다는 점이다. 과연 이런 경우에 UserDao 소스코드를 N 사와 D 사에 제공해주지 않고도 고객 스스로 원하는 DB Connection 생성 방식을 적용해가면서 UserDao 를 사용하게 할 수 있을까?

상속을 통한 확장

방법은 기존 UserDao 코드를 한 단계 더 분리하는 것이다. UserDao 에서 method의 구현 코드를 제거하고, getConnection() 을 추상 method로 만든다. 이제 이 추상 클래스 UserDao 를 N 사와 D 사에 판매한다.

기존에는 같은 클래스에 다른 method로 분리했던 DB Connection 연결이라는 관심을 상속을 통해 서브 클래스로 분리해버리는 것이다.

public abstract class UserDao {

	public void add(User user) throws ClassNotFoundException, SQLException {
    
		Connection c = getConnection();
		...
	}
    
	public User get(String id) throws ClassNotFoundException, SQLException {
    
		Connection c = getConnection();
		...
	}
    
	// 구현 코드는 제거되고 추상 method로 변경
	// method의 구현은 서브 클래스가 담당    
	public abstract Connection getConnection() throws ClassNotFoundException, SQLException;

}

public class NUserDao extends UserDao {

	// 상속을 통해 확장된 getConnection() method
	public Connection getConnection() throws ClassNotFoundException, SQLException {
    
		// N 사 DB Connection 생성 코드
	}
}

public class DUserDao extends UserDao {

	public Connection getConnection() throws ClassNotFoundException, SQLException {
    
		// D 사 DB Connection 생성 코드
	}
}

수정된 코드를 잘 살펴보자. DAO의 핵심 기능인 어떻게 데이터를 등록하고 가져올 것인가를 담당하는 UserDao 와, DB 연결 방법은 어떻게 할 것인가를 담당하는 NUserDao, DUserDao 가 클래스 레벨로 구분이 되고 있다. 클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업이 용이해졌다.

이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 overriding이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 디자인 패턴에서 템플릿 메소드 패턴 template method pattern 이라고 한다. 이 패턴은 Spring에서 애용되는 디자인 패턴이다.

🏷 오버라이딩 Overriding

상속에서 사용되는 개념으로, 같은 이름의 함수가 존재할 경우, 자식의 함수가 우선된다.

UserDaogetConnection() 메소드는 Connection 타입 Object를 생성한다는 기능을 정의해놓은 추상 메소드이다. 그리고 UserDao 의 서브클래스의 getConnection() 메소드는 어떤 Connection 클래스의 Object를 어떻게 생성할 것인지를 결정하는 방법이라고도 볼 수 있다. 이렇게 서브클래스에서 구체적인 Object 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴 Factory method pattern 이라고 한다.

UserDao의 관심사항

  • UserDao
    • 어떤 기능을 사용할까?
  • NUesrDao / DUserDao
    • 어떤 식으로 Connection 기능을 제공할까?
    • 어떤 방법으로 Connection Object 를 만들어낼까?

getConnection() 메소드에서 생성하는 Connection Object 의 구현 클래스는 제각각이겠지만 UserDaoConnection 인터페이스 타입의 Object라는 것 이외에는 관심을 두지 않는다. 그저 Connection 인터페이스에 정의된 메소드를 사용할 뿐이다.

UserDaoConnection Object 가 만들어지는 방법과 내부 동작 방식에는 상관없이 자신이 필요한 기능을 Connection 인터페이스를 통해 사용하기만 할 뿐이다.

이처럼 매우 깔끔한 방식으로 관심사항을 분리해 상하위 클래스에 나눠 담도록 코드를 개선한 것이다.

그림 1-2는 서브클래스의 getConnection() 을 통해 만들어진 Connection Object 의 종류가 달라질 수 있게 하는 것을 목적으로 하는 디자인 패턴인 팩토리 메소드 패턴을 나타낸다. NUserDaoDUserDao 가 모두 같은 종류의 Connection 클래스의 Object를 반환할 수도 있지만, Object를 생성하는 방식이 다르다면 이는 팩토리 메소드 패턴으로 이해할 수 있다.


🏷 디자인 패턴 Design pattern

소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션을 말한다. 디자인 패턴은 주로 객체지향 설계에 관한 것이고, 이러한 원칙을 이용해 문제를 해결한다. 패턴의 설계 구조를 보면 대부분 비슷한데, 그 이유는 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 대부분 두 가지 구조로 정리되기 때문이다.

  • Class 상속IS-A
    상속은 일반적인 클래스를 좀 더 구체적인 기능의 클래스로 만들 때 사용한다. 객체끼리 is -a 관계일 때 상속을 사용한다.
  • Object 합성HAS-A
    합성은 클래스 간의 관계가 일반적이고, 구체적인 관계가 아닐 때 사용한다. 객체끼리, has-a 관계일 때 합성을 사용한다.

디자인 패턴에서 가장 중요한 것은?

  • 목적과 의도
  • 적용할 상황
  • 해결해야 할 문제
  • 솔루션의 구조와 각 요소의 역할

상속과 합성에 대한 자세한 설명은 다음 링크로 넘어가서 공부하자.


🏷 템플릿 메소드 패턴 template method pattern

상속을 통해 Super 클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 Super 클래스 에 만들어두고 자주 변경되며 확장할 기능은 Sub 클래스 에서 만들도록 한다.

public abstract class Super {

	public void templateMethod() {
		// 기본 알고리즘 코드
		hookMethod();
		abstractMethod();
		// 기본 알고리즘 골격을 담은 메소드를 템플릿 메소드라고 부른다.
		// 템플릿 메소드는 서브클래스에서 오버라이드하거나 구현할 메소드를 사용한다.
 		...
	}

Super 클래스에서는 미리 abstract method / Override 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 template method 를 만든다.

    
	protected void hookMethod() { }
	// 선택적으로 오버라이드 가능한 훅 메소드
    
	public abstract void abstractMethod();
	// 서브클래스에서 반드시 구현해야 하는 추상 메소드
}

🏷 훅 메소드 hook method

Super 클래스에서 Default 기능을 정의해두거나 비워뒀다가 Sub 클래스에서 선택적으로 Override 할 수 있도록 만들어둔 메소드를 말한다.


// 슈퍼클래스의 메소드를 오버라이드하거나 구현해서 기능을 확장한다.
// 다양한 확장 클래스를 만들 수 있다.
public class Sub1 extends Super {

	protected void hookMethod() {
		...
	}
        
	public void abstractMethod() {
 		...
 	}
 }

Sub 클래스에서는 abstract method 를 구현하거나, hook method 를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.


🏷 팩토리 메소드 패턴 Factory method pattern

템플릿 메소드 패턴과 마찬가지로, 상속을 통해 기능을 확장하게 하는 패턴으로 구조도 비슷하다. Super 클래스에서는 Sub 클래스에서 구현할 메소드를 호출해서 필요한 타입의 Object를 가져와 사용한다. 이 메소드는 주로 인터페이스 타입으로 Object를 반환하므로 Sub 클래스에서 정확히 어떤 클래스의 Object를 만들어 반환할지 Super 클래스에서는 알지 못하며 관심도 없다.

  • 팩토리 메소드
    Sub 클래스에서 Object 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드를 말한다.
  • 팩토리 메소드 패턴
    Factory method 방식을 통해 Object 생성 방법을 나머지 로직, 즉 Super 클래스의 기본 코드에서 독립시키는 방법을 말한다.

✔️ Java 에서는 종종 Object를 생성하는 기능을 가진 메소드를 일반적으로 팩토리 메소드라고 부르기도 하지만, 의미가 다르므로 혼동하지 않도록 주의해야 한다.


이처럼 template method 또는 factory method 패턴으로 관심 사항이 다른 코드를 분리해내고, 서로 독립적으로 변경 / 확장할 수 있도록 만드는 것은 간단하면서도 매우 효과적인 방법이다.

상속이 갖는 한계점

그렇다면, 이렇게 완성된 코드는 더 이상의 문제가 없을까?

정답은 NO이다. 이 코드의 문제점은 바로 상속 을 사용했다는 점에 있다. 상속 자체는 간단해보이고 편리해보이지만 사실 많은 한계점을 갖는다.

만약, 이미 UserDao 가 다른 목적을 위해서 상속을 사용하고 있었다면?

Java클래스의 다중 상속을 허용치 않는다. 단지, Connection Object 를 가져오는 방법을 분리하기 위해 상속 구조로 만들어버림으로써, 후에 다른 목적을 위해 상속을 적용할 수 없다는 것이다.

또 다른 문제는, 상속을 통한 상하위 클래스의 관계는 생각하는 것보다 밀접하다는 점에 있다. 관심이 다른 기능을 분리하고, 필요에 따라 다양한 변신이 가능하도록 확장성을 부여하면서 상속이라는 기능을 선택하였지만, 여전히 상속 관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용한다.

그렇기 때문에, 상위 클래스의 내부 변경이 필요할 때, 상속된 모든 하위 클래스를 함께 수정해야할 수도 있다.

또한, 확장된 기능인 DB Connection 기능을 생성하는 코드는 다른 Dao 클래스에 적용할 수 없다는 것도 큰 단점으로 작용한다.

DAO의 확장

모든 Object 는 변화한다. 그렇지만 모든 Object 가 동일한 방식으로 변화하는 것은 아니다. 관심사에 따라 분리된 Object 는 제각기 독특한 변화의 특징을 갖는다.

지금까지, abstract 클래스를 만들고 이를 상속한 서브 클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 변화의 성격이 다른 것들을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위함이었다.

그러나 상속을 사용했다는 사실이 불편하게 느껴진다.

클래스의 분리

이번에는 관심사가 다르고 변화의 성격이 다른 이 두 가지 코드를 좀 더 화끈하게 분리해볼 생각이다. 두 개의 관심사를 본격적으로 독립시키면서 동시에 손쉽게 확장할 수 있는 방법을 알아보자.

지금까지 관심사를 분리한 방법은 다음과 같다.

성격이 다른, 그래서 다르게 변할 수 있는 관심사를 분리하는 작업을 점진적으로 진행해왔다. 처음으로 독립된 메소드를 만들어서 분리한 뒤, 상속 을 통해 상하위 클래스로 분리했다.

이번에는 상속의 개념을 사용하지 않고, 완전히 독립적인 클래스로 만들어보자.

SimpleConnectionMaker 라는 새로운 클래스를 만들고, DB 생성 기능 makeNewConnection() 을 그 안에 넣는다. 그리고 UserDaonew 키워드를 사용하여 해당 클래스의 Object 를 만들어두고, 이를 add() , get() 메소드에서 사용하면 된다.

UserDao 는 상속을 통한 방법을 사용하지 않으니 더 이상 abstract 일 필요가 없다.

먼저 생성자에서 SimpleConnectionMakerObject 를 만들어두고, add / get 메소드에서 이를 사용해 가져오면 된다.

이렇게 수정한 UserDao 코드는 다음과 같다.

public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker;
    
    public UserDao() {
    	simpleConnectionMaker = new SimpleConnectionMaker();
        // 상태를 관리하는 것도 아니니 한 번만 만들어 인스턴스 변수에 저장해두고 여러 메소드에서 사용하게 한다.
    }
    
    public void add(User user) throws ClassNotFoundException, SQLException {
    	Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException {
    	Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }
}

DB Connection 생성 기능을 독립시킨 SimpleConnectionMaker 는 다음과 같다.

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

기존 코드에 많은 수정을 했지만, 기능에 변화를 준 것은 없다. 단지 내부 설계를 변경해서 좀 더 나은 코드로 개선했을 뿐이다. 기능에 변화가 없다는 것은 이런 refactoring 작업의 전제이기도 하지만, 사실은 검증 내용이기도 하다. 그러므로 이전에 만들어둔 main 메소드를 실행하여 이전과 동일한 결과가 나오는지 확인해보자.

성격이 다른 코드를 화끈하게 분리하는 데는 성공한 것 같은데, 이번에는 다른 문제가 발생했다.

N 사와 D 사에 UserDao 클래스만 공급하고, 상속 을 통해 DB Connection 기능을 확장해서 사용하게 했던 것이 다시 불가능해졌다.

그 이유는, UserDao 의 코드가 SimpleConnectionMaker 라는 특정 클래스에 종속되어 있기 때문에, 상속을 사용했을 때와 동일하게, UserDao 코드의 수정 없이 DB Connection 생성 기능을 변경할 방법이 없다.

1개의 댓글

comment-user-thumbnail
2022년 11월 7일

잘읽었습니다!

답글 달기