본문 바로가기
Spring Framework/study

DAO 패턴은 왜 그렇게 생겼나?

by bloodFinger 2020. 3. 29.

spring을 공부하면서 DB에 접근할때 왜 이러한 패턴을 생겼는지 궁금했다.

 

어쩌다가 이렇게 구성이 되었는지 알아보자. 

 

내용은 토비의 스프링 3.1를 참고했습니다.

 

 

 

#1 . 초기 JDBC로 접근

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.set5tring(1, user.getld()); 
        ps.set5tring(2, user.getName()); 
        pS.set5tring(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 * 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;
    }

}

 

 

#2. 리펙토링 : 메소드 추출기법

public 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.set5tring(1, user.getld()); 
        ps.set5tring(2, user.getName()); 
        pS.set5tring(3, user.getPassword());

        ps.executeUpdate();

        ps.close(); 
        c.close();
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException ( 
        Connection c = getConnection(); //DB 연결 기능이 필요하면 메소드 사용
        
        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;
    }
    
    
    private Connection getConnection() throws ClassNotFoundException, SQLException {
    	Class.forName( "com.mysql. jdbc.Driver");
        Connection c =DriverManager.getConnection("jdbc:mysql://localhost/springbook" ,"spring" , "book");
		
        return c;
    }
    
 

}

 

지금은 메소드가 2개이지만 극단적으로 2만개가 있을때 DB의 종류나 접속 방법이 변경된다면  모든 메소드의 코드를 바꿔줘야 한다.

 

그렇기에 중복제거를 실행했다. 

 

중복 제거를 통해서 DB 연결과 관련된 부분이 변경 되었을때 getConnection() 메소드만 변경해주면 된다.

 

 

 

그렇다면 이제는 DB 커넥션을 만들어서 독립시켜보자.

 

상황) 

예를들어 네이버와 다음에서 사용자 관리를 위해 UserDAO를 구매하겠다고 컨텍이 왔다.

그런데 납품 과정에서 문제가 발생했다. 문제는 네이버와 다음에서 서로 다른 DB를 사용하고 있었고

DB 커넥션을 가져오는데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다.

이런 경우에 UserDAO의 소스코드를 고객에게 제공하고, 변경이 필요하면 getConnection() 메소드를

수정해서 사용하라고 할 수 있다. 하지만 독자적인 솔루션 기술을 공개하고 싶지 않기에 컴파일된 바이너리

파일만 제공하고 싶다.

이러한 경우 소스코드를 제공하지 않고도 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를

사용하게 할 수 있을까?..

 

 

#3. 상속을 통한 확장

 

 

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(); 
        
        ...
    }
    
    //구현 코드는 재거되고 추상 메소드로 바뀌었다. 매소드의 구현은 서브클래스가 담당한다.
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
    
 

}

상속을 통해서 확장

public class NUserDao extends UserDao{
	public Connection getConnection() throws ClassNotFoundException, SQLException{
    	//네이버 DB 커넥션 생성코드
        ....
    }
}
public class DUserDao extends UserDao{
	public Connection getConnection() throws ClassNotFoundException, SQLException{
    	//다음 DB 커넥션 생성코드
        ....
    }
}

 

이런식으로 상속을 사용했다는 단점이 존재한다.

상속 자체는 간단해 보이고 사용하기도 편리하게 느껴지지만 사실은 맣은 한계점이 있다.

 

만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면 어쩔 것인가?

  • 자바는 클래스의 다중상속을 허용하지 않는다.
  • 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하는 점이다.

    이유 : 서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있다. 그래서 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함꼐 수정하거나 다시 개발해야 할 수 도 있다. 반대로 그런 변화에 따른 불편을 주지 않기 위해 슈퍼클래스가 더이상 변화하지 않도록 제약을 가해야 할지도 모른다.
  • 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO클래스에 적용할 수 없다는 것.

 

이러한 문제들로 인해 기능은 잘 동작하지만 찝찝하기에 상속관계도 아닌 완전히 독립적인 클래스로 만들어 보자.

 

 

#4. 독립된 별도의 클래스 생성

public class UserDao {
	
    private SimpleConnectionMaker simpleConeectionMaker;
    
    public UserDao() { 
    	simpleConnectionMaker = new SimpleConeectionMaker();
    }
	
     public void add(User user) throws ClassNotFoundException, SQLException {
      	Connection c = simpleConnectionMaker.makeNewConnection();
        
        ...
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException ( 
        Connection c = simpleConnectionMaker.makeNewConnection();
        
        ...
    }
    

}

SimpleConnectionMaker.java 라는 별도의 클래스 생성

public class SimpleConnectionMaker{

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

}

 

 

성격이 다른 코드를 화끈하게 분리하기는 잘했지만, 이번에 다른 문제가 발생했다.

네이버와 다음에서 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던게 

다시 불가능 하게 되었다.

왜냐하면 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때 처럼 UserDAO

코드의 수정 없이 DB커넥션 생성 기능을 변경할 방법이 없다.

 

자유로운 확장이 가능하게 하려면 두 가지 문제를 해결해야 한다.

  • SimpleConnectionMaker 의 메소드

    네이버 에서 makeNewConnection이라는 메소드명 대신에 openConnection()으로 변경시 메소드 커넥션을 가져오는 코드를 다음과 같이 일일이 변경해야 한다.
  • DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고있어야 한다는 점.

    UserDao에 simpleConnectionMaker라는 클래스 타입의 인스턴스 변수까지 정위해놓고 있으니 , 네이버에서 다른 클래스를 구현 하면 어쩔 수 없이 UserDao 자체를 다시 수정해야 한다.

이런 문제의 근본적인 원인은 UserDao바뀔 있는 정보, DB 커넥션을 가져 오는 클래스에 대해 너무 많이 알고 있기 때문이다.

즉, 어떤 클래스가 쓰일지, 그 클래스 에서 커넥션을 가져오는 메소드는 이름이 뭔지까지 일일이 알고 있어야 한다.

따라서 UserDaoDB 커넥션을 가져오는 구체적인 방법에 종속되버린다.

지금은 UserDao SimpleConnectionMaker라는 특정 클래스와 그 코드에 종속적이기 때문에 앞으로 납후에 고객이 DB 커넥션을 가져오는 방법을 지유홉게 확장하기가 힘들어졌다.

 

 

그렇다면 이러한 불편을 다시 해결해보자.

어떤식으로  해결할 것인가?

가장 좋은 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다!

 

 

#5.인터페이스 도입

이제는 UserDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 된다.

단지 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.

 

public interface ConnectionMaker {
	public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

ConnectionMaker 구현 클래스

public class DConnectionMaker implements ConnectionMaker {
	...
    
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
    	//다음사의 독자적인 방법으로 Connection을 생성하는 코드
    }

}

인터페이스를 사용해서 DB커넥션을 가져와 사용하도록 수정한 UserDao

public class UserDao {
	private ConnectionMaker connectionMaker;
    
    public UserDao() {
    	connectionMaker = new DConnectionMaker();
    }
    
    public void add(User user) throws ClassNotFoundException, SQLException {
    	Connection c = connectionMaker.makerConnection();
        ...
    }
    
    public void get(String id) throws ClassNotFoundException, SQLException {
    	Connection c = connectionMaker.makerConnection();
    	...
    }
    

}

오케이.

USerDao의 다른 모든 곳에서는 인터페이스를 이용하게 만들어서 DB 커넥션을 제공하는 클래스에 대한 구체적인 정보는 모두 제거 가능했지만, 초기에 한번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 제거되지 않고 남아 있다.

 

클래스의 이름을 넣지 않으면 오브젝트를 어떻게 사용 하란것인가?

 

인터페이스를 이용한 분리에도 불구하고 여전히 UserDao 변경 없이는 DB커넥션 기능의 확징이 자유롭지 못한데,

그 이유는 UserDao 안에 분리되지 않은 또 다른 관심사항이 존재하고 있기 때문이다.....

 

UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다.

간단히 말하자면 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.

 

그렇다면 우리는 UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안 되게 해야 한다.

그래야지 UserDao의 수정 없이 DB 커넥션 구현 클래스를 변경할 수 있다.

 

 

#6.수정된 Dao

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

 

드디어 UserDao에 있으면 안 되는 다른 관심사항, 책임을 클라이언트로 떠넘기는 작업이 끝났다.

 

 

앞에서 사용했던 상속을 통한 확장 방법보다 더 깔삼하고 유연한 방법으로 UserDao와 ConnectionMaker 클래스들을

분리하고 서로 영향을 주지 않으면서도 필요에 따라 자유롭게 확장할 수 있는 구조로 탈바꿈 하였다~!

 

 

 

'Spring Framework > study' 카테고리의 다른 글

Spring 트랜잭션 처리 @Transactional  (0) 2020.03.07
Spring Bean Scope ?  (0) 2020.03.06
DI(Dependency Injection) ?  (0) 2019.12.23
IoC Container ?  (0) 2019.12.23
테스트  (0) 2019.12.20