IOC의 필요성 - 1. JDBC 예제코드 작성 및 메서드 추출(관심사 분리)

2022. 1. 9. 14:30북리뷰/토비의 봄

728x90

1. DAO 제작해보기

사용자 정보를 DB에 저장하고 조회하는 간단한 DAO를 만들어보자

DAO란?

Data Access Object,DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

User 객체 생성

package springbook.user.domain;

public class User {
    String id;
    String name;
    String password;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

sql table 생성

mysql에서 user을 저장할 테이블을 만들자

create table users (
    id varchar(10) primary key,
    name varchar(20) not null,
    password varchar(10) not null
)

UserDao 제작

일단 gradle에 mysql-connector 의존성을 추가하자

plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'

    implementation 'mysql:mysql-connector-java:8.0.27'
}

test {
    useJUnitPlatform()
}



```java
package springbook.user.dao;

import springbook.user.domain.User;

import java.sql.*;

public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

        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.execute();

        ps.close();
        c.close();
    }
    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

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

JDBC가 동작하는 일반적인 순서는 다음과 같다.

  1. DB 연결을 위한 Connection을 가져온다
  2. SQL을 담은 Statement(PrepareStatement)를 만든다.
  3. 만들어진 statement(PrepareStatement)를 실행한다.
  4. 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장하는 오브젝트에 옮겨준다.
  5. 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
  6. JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메서드에 throws를 선언해서 예외가 발생하면 메서드 밖으로 던지게 한다.

UserDao 테스트코드 작성

만들어진 UserDao를 검증하기 위한 테스트 코드를 간단하게 작성해보자

package springbook;

import springbook.user.dao.UserDao;
import springbook.user.domain.User;

import java.sql.SQLException;

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

        User user = new User();
        user.setId("asdf");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        System.out.println(user.getId() + "등록성공");

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

        System.out.println(user2.getId() + "조회성공");
    }
}

위 코드의 문제점

위 코드의 문제점은 변경, 발전, 확장이 용이하지 않다는 점이다.
객체지향의 세계에서는 모든것이 변하는데, 애플리케이션이 더 이상 사용되지 않는 한 오브젝트에 대한 설계와 이를 구현한 코드가 변한다는 뜻이다. 소프트웨어 개발은 끝이 없다. 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끊임없이 바뀌고 발전한다. 애플리케이션 기반 기술도 시간이 지남에 따라 바뀌고, 운영환경도 변화한다. 그래서 개발자는 객체를 설계할 때 미래의 변화를 항상 대비해야한다.
그러면 어떻게 변경이 일어날 때 필요한 작업을 최소화하고 변경이 다른 곳에 문제를 일으키지 않게 할 수 있을까?

관심사의 분리

관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리해야 한다.
모든 것을 뭉뚱그려서 한데 모으는 편이 처음에 쉽고 편할 수 있으나, 언젠가는 그 뭉쳐있는 여러 종류의 관심사를 적절하게 구분하고 따로 분리하는 작업을 해줘야만 할 때가 올 것이다.

커넥션 제작 메서드 추출

UserDao의 구현된 add메서드를 다시 살펴보자

public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

        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.execute();

        ps.close();
        c.close();
    }

add메서드에서 최소한 세 가지 관심사항을 발견할 수 있다.

  1. DB와 연결을 위한 커넥션을 어떻게 가져오는가
  2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행
  3. 작업 종료시 Statement와 Connection 오브젝트 종료
    특히 Connection 가져오는 부분이 치명적이다. 만약 DB의 비밀번호가 바뀌면? add의 커넥션 부분도 수정해야하고, get의 커넥션 부분도 수정해야 한다. 위 예시는 두 부분만 수정하면 되지만, 만약 Connection호출하는 메서드가 1000개이면? 1000개를 일일이 수정해야 하는 것이다.

중복 코드의 메서드 추출

package springbook.user.dao;

import springbook.user.domain.User;

import java.sql.*;

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.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.execute();

        ps.close();
        c.close();
    }
    public User get(String id) throws ClassNotFoundException, SQLException {
        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;
    }
    private Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby_mysql", "root", "<password>");

        return c;
    }
}

Connection 연결 부분을 getConnection()이라는 독립적인 메서드를 만들어서 커넥션을 가져오는 중복된 코드 부분을 분리하였다.
이렿게 수정하면 만약 password가 변경되어도 1000개를 일일이 수정할 필요 없이 getConnection메서느 일부만 수정하면 된다.
관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다.
관심이 다른 코드가 있는 메서드에는 영향을 주지도 않을 뿐 아니라 관심 내용이 독립적으로 존재하므로 수정도 간단해졌다.
위 같은 리펙토링 기법을 메서드 추출(extract method)라고 한다

728x90