초난감 UserDao


예제 위주로 중요한 개념들을 포스팅 하도록 하겠습니다.

다음과 같이 사용자 기본 정보를 필드로 가지는 User 클래스를 만들겠습니다.

@Getter
@Setter
@Builder
public class User {
    private String id;
    private String nickName;
}

그리고 실제 데이터베이스에 접근하여 User 데이터를 조작하는 기능을 담당하는 UserDao 도 만들겠습니다.

public class UserDao {
    //유저 생성
    public void add(User user) throws ClassNotFoundException, SQLException {
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "pw");
            PreparedStatement ps = conn.prepareStatement("insert into User(id, nick_name) values(?, ?)")) {

            ps.setString(1, user.getId());
            ps.setString(2, user.getNickName());

            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        }
    }
    
    // ID 로 유저 조회
    public User findById(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");

        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "pw");
            PreparedStatement ps = conn.prepareStatement("select id, nick_name from User where id = ?")) {

            ps.setString(1, id);
            ResultSet rs = ps.executeQuery();

            if (!rs.next()) return null;

            return User.builder()
                    .id(rs.getString("id"))
                    .nickName(rs.getString("nick_name"))
                    .build();
        } catch (SQLException e) {
            throw e;
        }
    }

    // ID 로 유저 삭제
    public void deleteById(String id) throws ClassNotFoundException, SQLException {
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "pw");
            PreparedStatement ps = conn.prepareStatement("delete from User where id = ?")) {

            ps.setString(1, id);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        }

    }  
}

UserDao 를 살펴보면 id 로 해당 유저를 삽입/조회/삭제하는 메서드를 추가하였고, 모두 mysql jdbc Driver 를 통해 DB Connection 을 얻어 SQL 을 실행하고 있습니다.

이 문제 투성이 UserDao 를 “초난감 UserDao” 라고 부릅니다.

호출해보면 동작하는데는 문제가 없습니다.

public class UserDaoTest {
    public static void main(String[] args) {
        try {
            UserDao userDao = new UserDao();

            //user 삽입
            User newUser = User.builder()
                    .id("happykoo")
                    .nickName("해피쿠")
                    .build();
            userDao.add(newUser);

            //user 조회
            User user = userDao.findById(newUser.getId());
            String nickName = Optional.ofNullable(user)
                    .map(User::getNickName)
                    .orElse(null);
            System.out.println("user nickName >> " + nickName);

            //user 삭제
            userDao.deleteById(newUser.getId());

            //삭제 후 user null
            System.out.println("user empty >> " + userDao.findById(newUser.getId()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

그렇다면, 잘 동작하는 “초난감 UserDao” 의 문제점은 무엇일까요?

  1. jdbc Driver 로부터 DB 정보를 넣어 Connection 을 얻는 로직이 중복된다.

    UserDao 에 메서드를 추가할 때마다 해당 로직을 추가해야 하며, 만약 DB 정보가 바뀌는 경우에는 사용한 곳을 모두 수정해야합니다.

  2. 관심사가 분리되어 있지 않아 낮은 응집도, 높은 결합도를 가진다.

    응집도와 결합도는 SW 공학에서 자주 등장하는 개념입니다. 응집도란 모듈의 독립성을 나타내는 개념으로 모듈 내부의 구성 요소들이 얼마나 연관성을 가지는지 측정하는 척도이고, 결합도란 외부 모듈에 얼마나 의존하는지 측정하는 척도입니다.

    따라서 응집도가 높을수록, 결합도가 낮을수록 올바르게 설계된 모듈이라고 할 수 있습니다. 이는 객체지향의 설계 원칙인 SOLID 와도 연관성이 깊습니다. SOLID 원칙에 의하면 하나의 모듈은 하나의 책임만 가져야 하며(단일 책임 원칙, SRP), 변경에는 닫혀있고 확장에는 열려있어야 합니다. (개방 패쇄 원칙, OCP)

    허나 “초난감 UserDao” 는 User 모델의 Persistence 계층으로서의 역할을 함과 동시에 DB Connection 을 획득하는 기능을 모두 하고 있습니다. DB Connection 을 얻는 기능은 User 모델에만 국한된 기능도 아니기 때문에 “초난감 UserDao” 는 SOLID 원칙에도 위배되고, 높은 응집도, 낮은 결합도를 가졌다고 볼 수 없어 잘 설계된 모듈이 아닙니다.

  3. 어떤 jdbc Driver 를 사용할 것인지를 직접 제어하고 있다.

    현재 “초난감 UserDao” 는 mysql jdbc Driver 를 사용하여 Connection 을 획득하고 있는데, 만일 dbms 를 mysql 이 아닌 oracle, postgresql 등의 다른 dbms 로 변경하려면 UserDao 를 직접 수정해야하며, 동시에 여러 dbms 를 사용하려면 그에 따라서 메서드를 따로 추가해야 합니다.

    이는 UserDao 가 jdbc Driver 구현에 직접적으로 관여하고 있기에 발생하는 문제입니다. 또 SOLID 원칙 중 DIP(의존 관계 역전 원칙) 에도 위배하는데, 의존 관계 역전 원칙이란 상위 모듈이 하위 모듈 구현에 의존하는 정톡적인 관계를 역전시켜, 상위 모듈을 하위 모듈 구체화로부터 독립해야 한다는 것을 말합니다.

    즉, 하위 모듈을 추상화하고 상위 모듈은 추상화된 하위 모듈을 사용할 뿐 하위 모듈이 어떻게 구현되었는지 구체적으로 알 필요가 없게 설계해야 합니다.

  4. DB 정보가 그대로 소스에 있으며, 암호화 되지 않았다.

    DB 정보가 Java 소스에 그대로 노출되는 것은 보안상 좋지 못합니다. 또한 DB 정보가 바뀔 때마다 빌드를 새로 해야하는데, 이를 해결하기 위해서는 DB 정보를 암호화 모듈로 암호화하고 yaml 파일이나 xml 파일로 관리하는 것이 바람직합니다.