Generic Dao 직접 구현 해보기

이성묵·2021년 6월 2일
0

# Generic DAO

How to Reduce Repetitive code in Data Access Object


Generic Dao 삽질기
코드는 github 에 있습니다.

문제점

  1. 기본적인 CRUD 작업시 데이터에 따라 비슷한 코드가 많이 발생한다.
  2. 오타로 인한 버그 (Column 이름 string sql query) 가많이 발생하고 그에따라 디버깅이 힘들다.
  3. Connection 관리의 실수로 인한 Connection 자원 이 모자람
  4. Database Column 변경시 관련 클래스들의 대량 코드수정이 불가피 하다
  5. 로직이 아닌 데이터에 의존적인 일회성 코드들이 많이 발생하여 재활용이 힘들다

목표

  1. 간단한 CRUD JDBC 작업시 객체 를 대신 맵핑 해줄 클래스
  2. Select query 에 조건 달기
  3. DTO 에 의존적이지 않은 DAO

CRUD 처리 해주는 메서드

Select 시에 키값만 다른 반복적인 코드를 줄이자

sql 쿼리 생성은 mariadb 기준으로 했습니다

Primary key 로 객체 탐색하는 코드

public <T, K> T select(Class<T> tClass, K key) {
        String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());
        String keyName = converter.convertToDbNameConvention(getKeyName(tClass));
        String sql = "SELECT * FROM " + tbName  + " WHERE " + keyName+ "=?";
        Connection conn = manager.getConn();
        try {
            assert conn != null;
            try(PreparedStatement preparedStatement = conn.prepareStatement(sql)){
                preparedStatement.setObject(1, key);
                ResultSet rs = preparedStatement.executeQuery();
                if (rs.next()) {
                    return setPOJO(tClass, rs);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            manager.close(conn);
        }
        return null;
    }

ResultSet 에서 객체 Mapping 시에 반복적인 코드를 줄이자

ResultSet 에서 Object 를 만들때 Column index 또는 Column name 만 다른 같은 코드들이 대량 발생하고

순서가 조금 바뀌거나 name 오타시 디버깅이 매우 힘들다.

Result set 에서 Class 에 따라 맵핑

public <T> T setPOJO(Class<T> target, ResultSet rs)
  throws InstantiationException, IllegalAccessException {
  T out = target.newInstance();
  cachedFields.computeIfAbsent(target, k -> target.getDeclaredFields());
  Field[] fields = cachedFields.get(target);

  for (Field f: fields) {
    f.setAccessible(true);
    String name = converter.convertToDbNameConvention(f.getName());

    try{
      f.set(out, rs.getObject(name));
    }catch (Exception e){
      e.printStackTrace();
    }
  }

  return out;
}

Insert , Delete, Update

public  <T> T insertMapper (T t) {
  String tbName = converter.convertClassNameToDbName(t.getClass().getSimpleName());
  ArrayList<Object> list = new ArrayList<>();
  StringJoiner columns = new StringJoiner(",","(",")");
  StringJoiner values = new StringJoiner(",","(",")");

  Field[] fields = t.getClass().getDeclaredFields();
  for(Field f: fields) {
    f.setAccessible(true);
    Object o = null;
    try {
      o = f.get(t);
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }

    if (o != null) {
      columns.add(converter.convertToDbNameConvention(f.getName()));
      values.add("?");
      list.add(o);
    }
  }
  String sqlPre = "INSERT INTO {table} {columns} VALUES {values}";
  String sql = sqlPre.replace("{table}", tbName)
    .replace("{columns}", columns.toString())
    .replace("{values}", values.toString());

  Connection conn = manager.getConn();
  try{
    try(PreparedStatement preparedStatement = conn.prepareStatement(sql)){
      for (int i=0;i<list.size();i++){
        Object o = list.get(i);
        preparedStatement.setObject(i+1, o);
      }
      if (preparedStatement.executeUpdate() > 0) {
        conn.commit();
        return t;
      }
    }
  }catch (Exception e) {
    e.printStackTrace();
  }
  finally {
    manager.close(conn);
  }

  return null;
}


// primary key 값으로 삭제
public <T, K> boolean deleteByKey(Class<T> tClass, K key) {
  String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());
  String keyName = converter.convertToDbNameConvention(getKeyName(tClass));

  StringBuilder sb = new StringBuilder();
  sb.append("DELETE FROM ").append(tbName).append(" WHERE ").append(keyName).append("=?");
  Connection conn = manager.getConn();
  try {
    assert conn != null;
    try (PreparedStatement preparedStatement = conn.prepareStatement(sb.toString())) {
      preparedStatement.setObject(1, key);
      if (preparedStatement.executeUpdate() > 0) {
        conn.commit();
        return true;
      }
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
  finally {
    manager.close(conn);
  }

  return false;
}


// update 
public <T> T updateMapper (T t) throws SQLException ,IllegalAnnotationException{
  String tbName = converter.convertClassNameToDbName(t.getClass().getSimpleName());
  String keyName = getKeyName(t.getClass());
  ArrayList<Object> list = new ArrayList<>();

  StringJoiner values = new StringJoiner(",");
  Field[] fields = t.getClass().getDeclaredFields();
  Object keyObj = null;
  for (Field f:fields) {
    f.setAccessible(true);
    try{
      Object o = f.get(t);
      String name = converter.convertToDbNameConvention(f.getName());
      if (o != null) {
        if (f.getName().equals(keyName)){
          keyObj = f.get(t);
        }
        else {
          values.add(name + "=?");
          list.add(o);
        }
      }
    }catch (Exception e) {
      e.printStackTrace();
    }
  }

  if (keyObj == null) {
    throw new IllegalArgumentException("key object should not be null");
  }


  String preSql = "UPDATE {table} SET {values} WHERE {key}=?";
  String sql = preSql.replace("{table}", tbName)
    .replace("{values}", values.toString())
    .replace("{key}", keyName);

  Connection conn = manager.getConn();
  try {
    assert conn != null;
    try (PreparedStatement prst = conn.prepareStatement(sql)){
      for (int i=0;i<list.size();i++) {
        prst.setObject(i + 1, list.get(i));
      }
      prst.setObject(list.size()+1, keyObj);
      if (prst.executeUpdate() > 0) {
        conn.commit();
        return t;
      }
    }
  } catch (SQLException e) {
    throw new SQLException(e);
  }
  finally {
    manager.close(conn);
  }
  return null;
}

조건 있는 select

문제 : 같은 table 에서 select 할때도 서로 다른 키가 다르면 여러개의 메소드를 만들어야 한다

쿼리들을 java 로 처리할수 있게 만들자

Filter 객체를 사용해서 Where 절 뒤에 오는 조건들은 구현

public <T> List<T> filteredSelect(Class<T> tClass, ObjectFilter<T> option) {
  String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());

  String sqlp = "SELECT * FROM " + tbName + " WHERE " ;
  StringBuilder sql = new StringBuilder();
  sql.append("SELECT * FROM ").append(tbName).append(" WHERE ");

  List<Object> list = new ArrayList<>();
  List<FilterOption> options = option.getOptions();
  List<FilterOrder> orders = option.getOrders();
  int limit = option.getLimit();
  int offset = option.getOffset();

  int numberOfOptions = options.size();

  for (int i = 0; i< numberOfOptions; i++) {
    FilterOption f = options.get(i);
    sql.append(converter.convertToDbNameConvention(f.getKey()));
    sql.append(f.getOpcode().getOp());

    sql.append("?").append(" ");
    if (i != numberOfOptions - 1) {
      sql.append(f.getLogical()).append(" ");
    }
    list.add(f.getValue());
  }

  if (!orders.isEmpty()){
    sql.append("ORDER BY ");
    StringJoiner joiner = new StringJoiner(",");
    orders.forEach(f -> {
      joiner.add(converter.convertToDbNameConvention(f.getKey()) + " " + f.getOrder());
    });
    sql.append(joiner);
  }

  if (limit != 0) {
    sql.append("LIMIT ?");
    list.add(limit);
    if (offset != 0){
      sql.append(" OFFSET ?");
      list.add(offset);
    }
  }

  Connection conn = manager.getConn();
  try {
    assert conn != null;
    try(PreparedStatement prst = conn.prepareStatement(sql.toString())){
      for (int i=0;i<list.size();i++) {
        prst.setObject(i+1, list.get(i));
      }
      System.out.println(sql);
      System.out.println(list.get(0));

      ResultSet rs = prst.executeQuery();
      List<T> out = new ArrayList<>();
      while (rs.next()){
        out.add(setPOJO(tClass, rs));
      }
      return out;
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
  finally {
    manager.close(conn);
  }
  return null;
}

Generic DAO 구현

Interface

public interface GenericAccess<T> {

    List<T> selectAll();

    List<T> selectWithFilter(ObjectFilter<T> keyOptions) throws IllegalAnnotationException;

    <K> T select(K key) throws IllegalAnnotationException;

    T insert(T t) throws SQLException, IllegalAnnotationException;


    T update(T t) throws SQLException, IllegalAnnotationException;


    boolean delete(T t) throws SQLException, IllegalAnnotationException;

    <K> boolean deleteById(K key) throws SQLException, IllegalAnnotationException;

    ObjectFilter<T> getFilter();
}

Interface 구현체

public abstract class GenericAccessImpl<T> implements GenericAccess<T> {

    private Class<T> entity;
    protected SqlMapper mapper = new SqlMapper();
    private String keyName;


    public void set(Class<T> tClass) throws NullPointerException, IllegalAnnotationException{
        if (tClass == null) {
            throw new NullPointerException("class should not be null");
        }
        // key annotation 이 없으면 exception 발생
        keyName = mapper.getKeyName(tClass);
        entity = tClass;
    }


    @Override
    public List<T> selectAll() {
        return mapper.selectAll(entity);
    }

    @Override
    public <K> T select(K key) throws IllegalAnnotationException {
        return mapper.select(entity, key);
    }

    @Override
    public T insert(T t) throws SQLException, IllegalAnnotationException {
        return mapper.insertMapper(t);
    }

    @Override
    public T update(T t) throws SQLException, IllegalAnnotationException {
        return mapper.updateMapper(t);
    }

    @Override
    public boolean delete(T t) throws SQLException, IllegalAnnotationException {
        return mapper.deleteMapper(t);
    }

    @Override
    public <K> boolean deleteById(K key) throws SQLException, IllegalAnnotationException {
        return mapper.deleteByKey(entity, key);
    }

    @Override
    public List<T> selectWithFilter(ObjectFilter<T> keyOptions) throws IllegalAnnotationException {
        return mapper.filteredSelect(entity, keyOptions);
    }

    @Override
    public ObjectFilter<T> getFilter() {
        return new ObjectFilter<>();
    }
}

데이터에 따른 메서드를 통합하기 위해 제네릭 클래스와 메서드들을 사용해서 시도 해보았습니다.

간단한 CRUD 작업시 에는 사용 할수 있겠지만 그이상 작업시에는 프레임워크를 제작하는 수준이 되어 배보다 배꼽이 더 커지는 상황이되어 여기 까지만 만들었습니다.

구현한 부분

  • 기본적인 CRUD 를 객체만 사용해서 다루기
  • Select 조건

구현 하지 못한부분

  • 포함관계에 있는 클래스들 맵핑
  • 관계 만들기

그동안 직접 사용해보지 못해서 정확한 개념이 없던 Generic Class 와 Reflection 을 사용 하려는 시도로 삽질을 하면서 개념이 조금 잡힌 느낌입니다.

작업을 하던 도중에 Hibernate 라는 ORM 이 있다는 것을 알았고 JPA 와 Hibernate를 공부 해야겠습니다.

결론

JPA 를 사용하자

0개의 댓글