BackEnd Developer, Love Crossfit, Welcome to Jay's blog

토비의 스프링 3.1 - 객체 지향 설계 과정 - 관심사의 분리1

|

진행했던 스프링 스터디(프로그래머스 웹백엔드) 첫 세션날 리더분께서 토비 스프링 책을 읽어 볼 것을 권해주셨다. 비록 스프링 3이지만, 스프링이 만들어진 배경과 그 과정에 대해 깊이 이해할 수 있는 책이라 하셔서 바로 구매를 행하였다. 하지만, 스터디 기간 동안에는 새로운 미션이 매주마다 주어져서 새로 공부할 것도 많았고 여건상 책을 읽기가 힘들었다.라는 변명같은 말을 해본다..ㅋㅋ. 스터디가 다 끝난 후 이제야 책을 읽어본다.

1장을 읽으면서 연신 감탄을 거듭했다. 객체 지향 설계란 어떻게 하는 건지 그리고 왜 그렇게 설계를 해야하는 건지 무엇보다 유연한 설계란 무엇인지가 알기 쉽게 설명되어 있었다.


책 토비의 스프링 3.1의 1장 오브젝트와 의존관계를 정리한 내용입니다.


초난감 DAO

요 절에서는 객체지향설계와 거리가 매우 먼 코드를 예시로 든다. 앞으로 이 코드의 문제점을 하나씩 개선해 나가면서 객체 지향적 설계란 무엇인가에 대해 알아갈 것이다.

먼저, 사용자 정보를 저장하고 조회할 수 있는 기능을 간단하게 만들어보자.

  • 사용자 정보를 저장할 User클래스가 하나 필요하고,
  • User 객체가 실제로 보관될 DB 테이블(Users)과
  • 사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스(UserDao)가 필요하다.

JDBC를 이용한 등록과 조회 기능이 있는 UserDao 클래스

public class UserDao {
    // 유저 정보 추가 기능
    public void add(User user) throws ClassNotFoundException, SQLException {
        // 1. DB 연결 위한 커넥션
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnetion(
            "jdbc:mysql://localhost/springbook", "spring", "book");

        // 2. SQL을 실행하는 statement
        PreparedStatement ps = c.preparedStatement(
            "insert into users(id, name, password) values(?,?,?)");
        )
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPasword());

        ps.executeUpdate();

        // 3. statement, 커넥션 닫기
        ps.close();
        c.closer();
    }

    // 유저 정보 조회 기능
    public User get(String id) throws ClassNotFoundException, SQLException {
        // 1. DB 연결 위한 커넥션
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnetion(
            "jdbc:mysql://localhost/springbook", "spring", "book");

        // 2. SQL을 실행하는 statement
        ...
        // 3. statement, 커넥션 닫기
        ...
    }
}

위 코드의 문제점이 보이는가? 저자는 질문을 던진다.
잘 동작하는 코드를 굳이 수정하고 개선해야하는 이유는 무엇일까?
이 코드를 개선했을 때의 장점은 무엇일까?
객체 지향 설계 원칙과는 무슨 상관이 있을까?

DAO의 분리

1.2.1 관심사의 분리

객체지향의 세계에서는 모든 것이 변한다. 여기서 변한다는 것은 오브젝트에 대한 설계와 구현한 코드가 변한다는 뜻이다. 소프트웨어 개발에서 끝이란 개념은 없다. 사용자의 비즈니스 프로세스와 그에 때른 요구사항은 끊임없이 바뀌고 발전한다. 때문에 우리는 객체를 설계할 때 미래의 변화를 어떻게 대비할 것인가를 가장 염두에 두어야 한다.

그렇다면 변화에 어떻게 대비해야하는가?

분리와 확장을 고려한 설계로 대비해야 한다. 변경이 일어났을 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않도록 해야한다

여기서 분리란, 관심사의 분리를 의미하며 관심이 같은 것끼리는 하나의 객체 안으로, 관심이 다른 것은 가능한 떨어뜨려 서로 영향을 주지 않도록 분리하는 것이다.

1.2.2 커넥션 만들기의 추출

이제 UserDao 클래스 코드로 다시 돌아와서, 분리와 확장을 고려한 설계로 개선해보자.
UserDao의 관심사는 무엇인가. 크게 3가지의 관심사가 같은 메소드 안에 얽혀 있다.

  1. DB 커넥션을 가져오는 일
  2. SQL 문장을 담는 Statement를 만들고 실행하는 일
  3. 리소스를 close하고 반환하는 일

책에서는 1번을 개선하는 작업을 수행한다.

관심사의 분리는 어떻게 할 수 있을까?

첫번째, 여러 메소드에 중복되어 있는 코드를 별도의 메소드로 분리시킨다. (메소드 추출 기법)

여기서는 DB 연결 부분을 getConnection()이라는 별도의 메소드로 만들어 분리시킬 수 있다.

getConnection() 메소드를 추출해서 중복을 제거한 UserDao

public void add(User user) throws ClassNotFoundException, SQLException {
    Connection c = getConnection();
    ...
}

public User get(String id) throws ClassNotFoundException, SQLException {
    Connection c = getConnection();
    ...
}

/**
 * 중복된 코드를 독립적인 메소드로 만들어서 중복을 제거했다.(분리)
 * 이제 DB 연결과 관련된 부분에 변경이 일어났을 경우, 한 메소드의 코드만 수정하면된다.
 */
private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
       "jdbc:mysql://localhost/springbook", "spring", "book");
    )
    return c;
}

1.2.3 DB 커넥션 만들기의 독립

하지만, 문제가 있다. 만약 UserDao를 사용하는 고객사 N사, D사가 각기 다른 종류의 DB를 사용한다면 어떻게 해야 할까?

두번째, 커넥션 확장을 위해 상속을 사용한다.

같은 클래스 안에서 독립적인 메소드로 분리됐던 DB 커넥션 연결이라는 관심을 이번에는 상속을 통해 서브클래스로 분리해버리는 것이다.

상속을 통한 확장 방법이 제공되는 UserDao

public abstract class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ... // SQL 생성, 실행, 반환
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ... // SQL 생성, 실행, 반환
    }

    // 구현 코드는 제거되고 **추상 메소드**로 바뀌었다. 메소드 구현은 서브클래스가 담당한다.
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class NUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N사 DB Connection 생성 코드
    }
}

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

UserDao를 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분(DB 연결)을 바꿔서 사용할 수 있도록 바꾸었다.

이를 통해 DB 연결 방법이 바뀌면 UserDao 코드는 그대로인 채 해당 클래스를 상속하고 있는 고객사의 코드만 바꾸면 된다. 이렇게함으로써 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 한다. 여기서 변화의 성격이 다르다는 건 변화의 이유와 시기, 주기 등이 다름을 의미한다.

템플릿 메소드 패턴
슈퍼클래스의 메소드(=템플릿메소드)에서 기본적인 로직의 흐름의 골격을 정의한다 - 여기서는 커넥션 가져오기, SQL 생성, 실행, 반환이 되겠다. 이때 자주 변경되며 확장될 로직은 추상 메소드나 오버라이딩이 가능한 protected 메소드로 만들어 서브클래스에서 만들도록 한다. 여기서는 DB 연결 부분인 geConnection() 메소드가 되겠다.

팩토리 메소드 패턴
상속을 통해 기능을 확장하는 패턴으로 서브클래스에서 구체적인 오브젝트 생성방법을 결정한다.

하지만, 이 상속을 이용한 방법도 단점이 있다. 만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면 어쩔 것인가? 자바는 클래스의 다중상속을 허용하지 않는다. 또한 상하위 클래스 관계는 밀접해서 슈퍼클래스 내부의 변경이 있을 때 모든 서브 클래스를 함께 수정해야 할 수도 있다. 그리고 UserDao 외의 DAO 클래스들이 계속 만들어진다면 DB커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다. 그 결과 상속을 통해 만들어진 DB 연결 메소드가 DAO 클래스마다 중복될 것이다.

그래서 책에서는 세번째, 인터페이스를 통해 클래스를 분리시킨다

해당 내용은 다음 포스트에서 - [토비의 스프링 - 인터페이스를 이용한 DAO 분리]

리팩토링
기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 / 기술을 말한다. 리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고 변화에 효율적으로 대응할 수 있다.