토비의 스프링 3.1 vol.1 1장 오브젝트와 의존 관계 - 1

K PizzaCola·2021년 7월 6일
0

Spring

목록 보기
2/3

초난감 UserDao

토비의 스프링 3.1 1장은 다음의 코드에서 시작한다.

public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection(...);
        
        PreparedStatement ps = c.prepareStatement("INSERT ...");
        ...
        
        ps.executeUpdate();
        ps.close();
        c.close();
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection(...);
        
        PreparedStatement ps = c.prepareStatement("SELECT ...");
        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        ...
        
        rs.close();
        ps.close();
        c.close();
        
        return user;
    }
} 

JDBC를 이용해서 DB에 접근하는 과정은 다음의 과정을 거친다.

  1. Connection을 가져온다.
  2. SQL을 담은 Statement를 만든다.
  3. Statement를 실행한다.
  4. 결과가 담긴 ResultSet를 이용해서 적당히 처리한다.
  5. 리소스를 반환한다.

이 과정은 JDBC를 이용하면 매번 반복하게 될 과정이다.

관심사의 분리

세상에는 변하는 것과 변하지 않는 것이 있다. 하지만 객체지향의 세계에서는 모든 것이 변한다.

토비의 스프링 1.2 DAO의 분리 첫 줄에 언급된 내용이다. 애플리케이션이 변화하지 않는 경우는 오직 하나, 프로그램을 사용하지 않아 폐기처분될 때라고 한다.

언제나 프로그램이 변화한다면, 좋은 개발자가 되려면 변화에 대비할 수 있는 코드를 작성할 수 있어야 할 것이다. 여기서 객체지향 프로그래밍 패러다임이 좋은 이유로 이러한 변화에 대해 준비할 수 있어, 쉽게 변경하고 확장, 발전시킬 수 있기 때문이라고 할 수 있을 것이다.

그렇다면 어떻게 해야 변화에 대비할 수 있을까? 그것은 분리와 확장을 고려한 설계라고 한다. 책에서 모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다고 한다.

여기서, 관심사의 분리(Separation of Concerns) 라는 프로그래밍의 기초 개념을 생각해볼 수 있다. 같은 관심사는 한 곳에 모으고, 관심이 다른 것은 서로 영향을 받지 않도록 분리하는 것이다.

위의 코드에서도 관심사를 크게 3가지로 나눌 수 있다.

  1. DB와 연결을 위한 Connection을 가져오는 방법
  2. 쿼리를 실행하고 결과를 가져오는 것 (이는 실행 / 결과 로도 더 나눌 수 있다.)
  3. 작업이 끝나면 리소스(Connection, Statement, ResultSet 등)를 반환한다.

여기서 1번을 먼저 정리해보자.

중복 코드를 메소드로 추출

Connection에 대한 것을 메소드로 추출하면 다음처럼 수정할 수 있다.

public class UserDao {
    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.cj.jdbc.Driver");
        return DriverManager.getConnection(...);
    }
} 

만약, 수정하기 전이라고 생각해보자. 그리고 UserDao 내에 메소드가 매우 많다고 상상해보자. 이 때 DB에 접속하는 주소가 바뀐다면, 매우 많은 메소드에서 DB 접속 주소를 모두 바꿨어야 했다. 하지만, 이에 대한 관심사를 getConnection()에 모두 모았기 때문에, getConnection() 메소드 내부만 수정하면 된다.

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

일단 책에서는 이에 대한 검증을 main 메소드의 출력으로 확인할 수 있다. 위와 같은 변경은 기능에 영향을 주지 않으면서, 코드의 구조만 변경되었다. 이러한 과정을 리팩토링이라고 하며, 메소드로 중복 코드를 뽑아내는 것을 메소드 추출(extract method) 기법이라고 한다.

DB 커넥션 만들기의 독립

만약 사용자 스스로 DB 커넥션 생성 방식을 원하는 것으로 선택하게 하려면 어떻게 해야할까? 한가지 방법은 상속을 이용하는 것이다. getConnection()을 추상 메소드로 수정하여 UserDao롤 추상 클래스로 바꾼다. 그리고 UserDao를 상속받는 서브클래스를 만들어서 getConnection()을 원하는 형태로 구현하면 된다.

public abstract class UserDao {
    public void add(User user) ...
    ...
    
    public User get(String id) ...
    ...

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class MySQLUserDao extends UserDao {
	
    @Override
    public Connection getConnection() ... {
       ....
    }
}

이제 MySQL, H2, PostgreSQL 등 원하는 DB를 UserDao에 연결할 수 있게 되었다. 이렇게 수퍼클래스에서 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 서브클래스에서 구현하도록 하여 원하는 기능을 수행하도록 하는 방법을 템플릿 메소드 패턴 이라고 한다.

한편, 위의 코드에서 Connection을 생성하는 방법은 서브클래스에서 정의한다. 이와 같이 서브클래스에서 구체적인 객체 생성 방법을 결정하는 방법을 팩토리 메소드 패턴 이라고 한다.

UserDao는 Connection이 어떻게 만들어지는지는 관심이 없다. UserDao는 Connection이 제공하는 기능을 사용하는 것에만 관심이 있다. 그리고 UserDao의 서브클래스들은 Connection을 어떻게 만드는 지에만 관심이 있다. 이렇게 상속을 이용하여 관심사를 깔끔하게 분리했다.

그러나 상속을 사용한 방법은 단점이 있다. 먼저, 자바와 같이 다중상속을 허용하지 않는다면 UserDao를 상속할 수 없을 수도 있다. 또한, 객체 상속은 사실 수퍼클래스와 서브클래스 사이에 매우 강력한 의존관계가 생기기 때문에 수퍼클래스가 변할 때 서브클래스도 수정해야할 수도 있다. 마지막으로, UserDao 말고 다른 DAO가 생긴다면, getConnection() 메소드가 중복해서 나타날 것이다.

클래스의 분리

이번에는 Connection을 만들어내는 객체를 별도로 만들어보자. 그리고 이 객체를 UserDao에서 이용하도록 해보자.

public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker;
    
    public UserDao() {
        this.simpleConnectionMaker = new SimpleConnectionMaker();
    }

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

public class SimpleConnectionMaker {	
    public Connection makeNewConnection() ... {
       ...
    }
}

구현 내용 자체는 크게 변하지 않는다. 다만 이렇게 구현하면 다시 원하는 DB를 그때 그때 바꿔서 사용할 수 없다. 그 외에도 UserDao가 SimpleConnectionMaker를 알고 있어야 한다.

인터페이스의 도입

그렇다면, 인터페이스를 이용해보자. 인터페이스를 이용하여 추상화를 한다면, UserDao는 구체적인 객체가 무엇인지 알 필요가 없다.

public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public UserDao() {
        this.connectionMaker = new MySQLConnectionMaker();
    }

    public void add(User user) ... {
        Connection c = simpleConnectionMaker.makeConnection();
        ...
    }
    
    public User get(String id) ... {
        Connection c = simpleConnectionMaker.makeConnection();
        ...
    }
}

public interface ConnectionMaker {	
    public Connection makeConnection() ... ;
}

public class MySQLConnectionMaker implements ConnectionMaker {	
    public Connection makeConnection() ... {
        ...
    }
}

그런데, 이 코드에서 MySQLConnectionMaker를 직접 객체에 할당하니, 다른 ConnectionMaker를 사용하기가 어렵다. 이를 어떻게 수정하면 좋을까?

관계설정 책임의 분리

간단하다. 객체 관계를 설정하는 책임, 관심사가 생겼으니, 이를 해결해줄 또 하나의 객체에 이 책임을 쥐어주는 것이다. 어딘가에서 UserDao를 사용하는 객체, 클라이언트가 하나는 존재할 것이다. 이 객체가 UserDao와 ConnectionMaker 사이의 관계를 결정해주면 좋을 것이다.

먼저, 생성자에서 ConnectionMaker를 대입하도록 한다.

public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
    ...
    
}

이렇게 되면, MySQLConnectionMaker는 이제 UserDao 내에서 완전히 사라졌다. 그리고 어떤 ConnectionMaker를 사용할 지는 UserDao를 사용하는 곳에서 정해줄 수 있다. main 메소드에서 이를 할당하면 다음과 같다.

public class UserDaoTest {
    public static void main(String[] args) throws ... {
        ConnectionMaker connectionMaker = new MySQLConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
        ...
    }
}

그래서, UserDao는 ConnectionMaker를 사용하는 책임만을 가졌다. 어떤 ConnectionMaker인지는 관심이 없다. 계약에 맞도록 설계된 ConnectionMaker라면 UserDao는 적절하게 동작할 것이다. ConnectionMaker는 Connection을 만들어서 제공하는 책임을 가졌다. 그리고 UserDaoTest는 UserDao에 ConnectionMaker를 제공하고, 사용하는 책임을 가졌다.

원칙과 패턴

개방 패쇄 원칙

SOLID 원칙 중의 하나인 개방 폐쇄 원칙이다. UserDao는 DB 연결 방법이라는 기능을 확장하는데 열려있다. 또한 이런 변화에 UserDao의 핵심 기능은 닫혀있다.

높은 응집도와 낮은 결합도

개방 폐쇄 원칙은 또한 높은 응집도와 낮은 결합도로 설명할 수 있다. 응집도가 높다는 것은 모듈, 클래스, 패키지 등이 모두 하나의 책임 또는 관심사에만 집중 되어 있다는 뜻이다. 불필요하거나 관련 없는 외부의 관심과 책임에는 얽혀 있지 않으며, 영향 받지도 않는다는 뜻이다.

높은 응집도

응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것이기도 하다. 만약 하나의 변경이 생겼을 때, 일부분만 바뀌어도 된다면, 모듈 전체에서 어디가 바뀌어야 하는지도 파악해야 하고, 그 변경이 어디에 영향을 미칠지도 확인해야한다.

처음 만든 초난감 DAO에서 다른 DB 커넥션 풀 라이브러리를 이용하는 방법으로 변경한다고 하면, 모든 메소드를 수정해야할 것이다. 반면에 ConnectionMaker를 이용하면 이를 구현하는 클래스를 만들면 될 것이다. DB 연결 방법이 ConnectionMaker을 구현한 클래스만 수정하면 된다.

낮은 결합도

낮은 결합도는 높은 응집도보다 더 민감한 원칙이다. 결합도란 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도 라고 설명할 수 있다. ConnectionMaker를 구현한 객체 내부를 수정해도 UserDao를 수정할 필요가 없다. 그 반대도 마찬가지이다. 그러면서, UserDao와 MySQLConnectionMaker 사이에 ConnectionMaker라는 느슨한 연결관계가 있다.

전략 패턴

개선한 UserDaoTest - UserDao - ConnectionMaker는 디자인 패턴으로 보면 전략 패턴에 해당한다. UserDao는 context에 해당하고, ConnectionMaker는 전략에 해당한다. 그리고 UserDaoTest는 전략을 준비하여 UserDao에게 전달한다.

profile
공부하는 개발자입니다.

0개의 댓글