토비의 스프링 정리 프로젝트 #1.2 DAO의 분리 (관심사의 분리, 디자인 패턴, 리팩토링, 테스트, 템플릿 메소드 패턴, 팩토리 메소드 패턴)

Jake Seo·2021년 7월 6일
1

토비의 스프링

목록 보기
3/29

관심사의 분리

변화에 어떻게 대응할 것인가?

핵심은 변화에 어떻게 대응할 것인가이다. 객체지향설계와 프로그래밍은 절차적 프로그래밍 패러다임에 비해 조금 더 많은 초기 비용을 소모한다. 많은 번거로운 작업들이 요구된다. 그러나, 처음에만 이러한 수고를 하여 제대로 객체지향 설계를 해두면, 객체지향 기술의 특성으로 변화에 효과적으로 대응할 수 있다.

객체지향 기술은 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전 확장시킬 수 있다.

변경이 일어날 때, 필요한 작업을 최소화하고 그 변경이 다른 곳에 문제를 일으키지 않게 하려면 어떻게 해야할까? 분리와 확장을 고려한 설계를 해야한다.

변화에서의 관심사

보통 변화에서의 관심사는 단 한 곳에서 일어나는 반면, 잘 설계되지 못한 프로그램을 변화시키려면 수백 개의 클래스를 수정해야 할 수도 있다.

이전의 초난감 DAO의 코드를 이용하여 프로그램을 작성했다면, DB의 계정과 암호라는 단 하나의 관심사만 변경하려 해도, 초난감 DAO와 같은 방식으로 작성된 DAO라면 모든 DAO 코드를 뒤집어 엎어야 할 수 있다.

단 한 곳의 관심사를 수정할 때, 단 한 곳의 코드만 수정하면 되도록 바꾸는 것이 우리의 전략이다.

관심사 분리의 핵심

변화가 일반적으로 한 번에 한가지 관심에 집중돼서 일어난다는 것을 알았으니, 관심이 같은 것 끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것으로써 변화에 대비할 수 있다.

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

관심사 분리는 처음부터 완벽하게 설계된 채로 할 수는 없다. 처음에는 뭉뚱그려 쉽게 코딩하더라도 설계를 생각하며 지속적 리팩토링을 거치는 과정이 중요하다고 생각한다.

UserDao의 관심사 뽑아보기

    public void add(User user) throws SQLException, ClassNotFoundException {
    	// 1. 커넥션 가져오기
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );


        // 2. SQL 문장을 담을 Statement를 만들고 실행하기
        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();


        // 3. 리소스 반납하기
        ps.close();
        c.close();
    }

총 3가지의 관심사가 나온다.

  • DB와 연결하기 위한 커넥션을 가져오는 것
  • DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
  • 공유 리소스를 시스템에 돌려주는 것

중복 코드를 메소드로 추출하기

현재 마주한 가장 큰 문제는 위의 .add() 메소드의 관심사 코드가 .get()에도 중복되어 있다는 것이다. 앞으로 DAO를 만들 때마다 이렇게 중복된 관심사 코드가 발생하면, 변경이 일어날 때 엄청난 고통을 일으킬 수 있다. 중복된 코드를 메소드로 추출하자.

    public void add(User user) throws SQLException, ClassNotFoundException {
        // 1.2.2 중복 코드의 메소드 추출
        Connection c = getConnection();

        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 SQLException, ClassNotFoundException {
        // 1.2.2 중복 코드의 메소드 추출
        Connection c = getConnection();

        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;
    }
    
    // 커넥션 가져오기 관심사
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );
    }

위는 커넥션 가져오기 관심사를 따로 분리해서 중복된 코드를 제거한 형태이다. 이제 커넥션에 대한 ID, 패스워드, URL, JDBC 드라이버 등에 대한 부분들이 변경되어도 getConnection()이라는 메소드의 내용에만 관심을 두면 전체 프로그램이 맺는 커넥션을 훌륭하게 관리할 수 있다.

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

위와 같이 코드에 변화가 일어난 후에는 반드시 제대로 작동하는지 다시 테스트를 해보아야 한다. 초난감 DAO에서 작성한 main 메소드는 현재까지 그럭저럭 테스트 코드의 역할을 해줄 수 있다. 물론 현재는 등록한 회원을 DB에서 수동으로 지우지 않으면, 기본키가 겹쳐서 에러가 뜨겠지만 말이다.

    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao dao = new UserDao();

        User user = new User();
        user.setId("1");
        user.setName("제이크");
        user.setPassword("jakejake");

        dao.add(user);

        System.out.println(user.getId() + " register succeeded");

        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());

        System.out.println(user2.getId() + " query succeeded");
    }

리팩토링의 의미

이번 작업에서 UserDao의 기능은 단 하나도 변하지 않았다. 다만, 중복된 코드가 사라져 이전보다 조금 더 깔끔해지고, 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 됐다. 이러한 작업을 리팩토링이라고 한다.

메소드로 중복된 코드를 뽑아내서 정리하는 것을 리팩토링에서는 메소드 추출 기법이라고 한다. 리팩토링은 객체지향 개발자라면 반드시 익혀야 하는 기법이다.

이 과정에서 기능을 추가하기보다는 코드 구조와 구현 방법을 바꿈으로써 더 나은 DAO를 만드는데 주력했다. 앞으로 이러한 작업을 리팩토링이라고 부르겠다.

리팩토링의 정의

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

리팩토링이 필요한 코드의 특징을 나쁜 냄새 라고 부르기도 한다. 중복 코드는 매우 흔하게 발견되는 나쁜 냄새이다. 적절한 리팩토링 방법을 적용해 나쁜 냄새를 제거해주어야 한다.

리팩토링은 개발자가 직관적으로 수행할 수도 있지만 본격적으로 적용하려면 학습과 훈련이 필요하다. 나쁜 냄새에는 어떤 종류가 있고 그에 따른 적절한 리팩토링 방법은 무엇인지 알아보고 충분한 연습을 해두면 도움이 된다.

DB 커넥션을 2개로 독립시켜보기

실제로 서로 다른 2개의 DB 커넥션을 이용할 수 있는 UserDao를 구성해보자.

상속을 통한 확장

public abstract class UserDao {
    public void add(User user) throws SQLException, ClassNotFoundException {
        Connection c = getConnection();

        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 SQLException, ClassNotFoundException {
        Connection c = getConnection();

        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;
    }

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

위와 같이 UserDaoabstract class로 구성하여, 상속하는 클래스에게 getConnection()을 직접 구현하라고 위임할 수 있다.

NUserDao

public class NUserDao extends UserDao{
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

        return c;
    }
}

DUserDao

public class DUserDao extends UserDao {
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.postgresql.Driver");

        String user = "postgres";
        String password = "password";

        Connection c = DriverManager.getConnection(
                "jdbc:postgresql://localhost/toby_spring"
                , user
                , password
        );

        return c;
    }
}

위와 같이 두 개의 클래스로 나눌 수 있다.

NUserDaoDUserDaoUserDao를 상속하는 방식으로 변경되었다. 이제 상속하는 곳에서 getConnection()을 자유롭게 구현하여 쓸 수 있다.

디자인 패턴과 상속을 이용한 확장에 쓰이는 패턴들

템플릿 메소드 패턴

상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다.

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

슈퍼 클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브 클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 훅(hook) 메소드라고 한다. 서브 클래스에서는 훅 메소드를 오버라이드하여 기능의 일부를 확장한다.

public abstract class Super {
  public void templateMethod() {
    // 기본 알고리즘 코드
    hookMethod(); // 서브 클래스에서 선택적으로 작성한다.
    abstractMethod(); // 서브 클래스에서 필수적으로 작성한다.
    ...
  }
  
  protected void hookMethod() {} // 서브 클래스에서 선택적으로 오버라이드 가능
  public abstract void abstractMethod() // 서브 클래스에서 반드시 구현해야 하는 추상 메소드
}

public class Sub1 extends Super {
  protected void hookMethod() {
    ...
  }
  
  public void abstractMethod() {
    ...
  }
}

팩토리 메소드 패턴

템플릿 메소드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 서브 클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 부른다.

주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지 슈퍼클래스에서는 알지 못하며, 슈퍼클래스의 관심사도 아니다.

서브 클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드를 팩토리 메소드라 하며 이 방식을 통해 오브젝트 생성 방법을 슈퍼 클래스에서 독립시키는 방법을 팩토리 메소드 패턴이라고 한다.

자바는 종종 오브젝트를 생성하는 기능을 가진 메소드를 팩토리 메소드라고 부르기도 한다. 이 때 말하는 팩토리 메소드와 팩토리 메소드 패턴의 팩토리 메소드는 의미가 다르므로 혼동하지 않도록 주의해야 한다.

중요한 건 디자인 패턴들의 이름보다도 상속 구조를 통해 성격이 다른 관심사항을 분리한 코드를 만들어냈고, 어떻게 서로 영향을 덜주도록 했는지를 이해하는 것이다.

디자인 패턴

디자인 패턴을 잘 아는 개발자라면 "UserDao에 팩토리 메소드 패턴을 적용해서 getConnection()을 분리합시다." 라는 한마디로 앞에 배웠던 내용을 한 문장으로 정리할 수 있다.

디자인 패턴은 소프트웨어 설계시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션을 말한다. 모든 패턴에는 간결한 이름이 있어서 간단히 패턴 이름으로 설계 의도와 해결책을 함께 설명할 수 있다는 장점이 있다.

디자인 패턴은 주로 객체지향 설계에 관한 것이며, 객체지향적 설계 원칙을 이용해 문제를 해결한다. 패턴의 설계 구조는 생각보다 대부분 비슷한데, 그 이유는 문제를 해결하기 위한 방법을 선택할 때, 클래스 상속오브젝트 합성이라는 두가지 길에서 크게 벗어나지 않기 때문이다.

패턴에서 가장 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도이다. 패턴을 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 각 요소의 역할과 함께 핵심 의도가 무엇인지 잘 기억해두어야 한다.

상속의 단점

상속을 사용하는 것은 장점만 있을 것 같지만, 사실 단점도 크다.

  • 자바는 다중 상속을 지원하지 않으므로, 단 한번 상속을 이용하면 다른 오브젝트를 또 상속할 수는 없다.
  • 상속을 통한 상하위 클래스 관계는 생각보다 밀접하다.
    • 슈퍼클래스의 변경이 있을 때, 서브클래스를 함께 수정해야 하거나 다시 개발해야할 수 있다.
    • 위와 같은 변화를 주지 않기 위해 슈퍼클래스의 변화가 일어나지 않도록 제약을 가해야 할지도 모른다.
  • 현재 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에는 적용할 수 없다.
    • 계속 상속하면 상속하는 DAO 마다 전부 getConnection()을 구현해주어야 하는데, 사실 같은 프로젝트 내에서는 같은 디비에 연결할텐데 중복된 코드가 잔뜩 나오게 될 것이다.
profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글