저번 게시물에서 JdbcTemplate에서 1:N 관계 처리하려고할 때 N+1 Problem이 생기고 결국 ORM 또는 쿼리 mapper를 사용해야 한다고 설명했습니다

이번에도 비슷한 상황입니다. 이번엔 1:N이 아니고 1:1 관계인데 이건 어떻게 한번의 쿼리로 처리할 수 있을 것도 같아 보입니다.

(참고 링크: JDBCTemplate set nested POJO with BeanPropertyRowMapper, Stackoverflow)

class User {
    String userName;
    String displayName;
}

class Message {
    String title;
    String question;
    User user;
}

유저, 메시지 클래스가 위와 같이 있고 각 유저, 메시지 테이블이 1:1 관계를 갖고 있다고 해보겠습니다.

"1:1관계면 그냥 두 클래스 합치면 되는거 아닌가?"

맞습니다. 근데 그냥 나눠놔야만 한다고 해봅시다. 그런 경우가 종종 있잖아요?

조인으로 쿼리 만들어서 갖고오는건 물론 쉽습니다. 그런데 이걸 RowMapper에 어떻게 넣어야 Message라는 객체에 user가 온전히 들어갈까.. 매핑을 어떻게 해줘야할까.. 어렵습니다.

해법 여러가지를 한번 생각해봅시다.

 

해법 (1) : 쿼리 두번 쓰기

그냥 쿼리를 두번 날려서 message, user를 받아온 뒤 message 객체에 user 객체를 넣습니다.

코드는 제일 짧습니다. 쿼리를 두배로 보내니 비효율적입니다.

 

해법 (2) : ResultSet의 인덱스를 이용해 직접 매핑

List<Message> messages = jdbcTemplate.query("SELECT * FROM message m, user u WHERE u.message_id = m.message_id", new RowMapper<Message>() {
    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        Message message = new Message();
        message.setTitle(rs.getString(1));
        message.setQuestion(rs.getString(2));

        User user = new User();
        user.setUserName(rs.getString(3));
        user.setDisplayName(rs.getString(4));

        message.setUser(user);

        return message;
    }
});

message, user 테이블을 조인하고 쿼리를 날리는데 이때 Message RowMapper의 mapRow 메소드를 오버라이딩합니다.

언뜻 합리적으로 보입니다. 그런데 row 매핑함수를 직접 오버라이딩 하기 때문에 rs.getString(1) 아니면 rs.getString("~~") 이런식으로 컬럼을 직접 매핑해줘야 합니다.

이러면 안좋습니다! 왜냐면 이렇게 직접 매핑하지 않으려고 BeanPropertyRowMapper 등을 사용하는건데 직접 ResultSet의 인덱스갖고 매핑해주면 나중에 유지보수가 어렵겠죠. 한 객체 바꾸면 mapRow 함수도 수정해야 되고 아무튼 별로입니다

 

해법 (3) : RowMapper 구현 (1)

SELECT title AS "message.title", question AS "message.question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id

쿼리를 이렇게 써줍니다. 각 컬럼이 "클래스.필드" 이런식으로 되도록 해줍니다.

package nested_row_mapper;

import org.springframework.beans.*;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

public class NestedRowMapper<T> implements RowMapper<T> {
  private Class<T> mappedClass;

  public NestedRowMapper(Class<T> mappedClass) {
    this.mappedClass = mappedClass;
  }

  @Override
  public T mapRow(ResultSet rs, int rowNum) throws SQLException {
    T mappedObject = BeanUtils.instantiate(this.mappedClass);
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
    bw.setAutoGrowNestedPaths(true);

    ResultSetMetaData meta_data = rs.getMetaData();
    int columnCount = meta_data.getColumnCount();

    for (int index = 1; index <= columnCount; index++) {
      try {
        String column = JdbcUtils.lookupColumnName(meta_data, index);
        Object value = JdbcUtils.getResultSetValue(rs, index, Class.forName(meta_data.getColumnClassName(index)));
        bw.setPropertyValue(column, value);
      } catch (TypeMismatchException | NotWritablePropertyException | ClassNotFoundException e) {
         // Ignore
      }
    }

    return mappedObject;
  }
}

그리고 이런식으로 NestedRowMapper를 만들어줍니다.

설명을 읽어보면 쿼리의 각 컬럼이 "클래스.컬럼" 이런식으로 되어 있으면 알아서 inner object를 만들어서 매핑을 한다고 합니다.

... 잘 모르겠습니다;

어쨌거나 지금까지 나온 구현방식중에 가장 나은 것 같긴 합니다.

 

해법(4) : RowMapper 구현 (2)

public class MessageRowMapper implements RowMapper<Message> {
    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = (new BeanPropertyRowMapper<>(User.class)).mapRow(rs, rowNum);
        Message message = (new BeanPropertyRowMapper<>(Message.class)).mapRow(rs, rowNum);
        message.setUser(user);
        return message;
     }
}

BeanPropertyRowMapper를 사용해 각각 매핑을 하고 setter를 이용해 포함시키는 방법입니다.

일단 코드가 제일 짧기는 합니다. 제일 직관적이기도 합니다. 이때 BeanPropertyRowMapper를 사용하려면 dto와 컬럼명이 일치해야 합니다(COLUMN_NAME == columnName)

단점이 있다면 각 객체가 중복된 컬럼명을 갖고 있으면 사용이 안됩니다.

 

결론

그래서 결론은 무엇이냐.. jdbcTemplate을 사용한다면 RowMapper를 새로 만들어야 하는 것은 필연적으로 보입니다. 아무거나 골라 쓰시면 되겠는데 저는 (3)번의 해법이 제일 나은 듯 하네요. 중복 필드 있어도 상관 없고요 (아마도?)

JPA를 사용하면 @OneToOne으로 해결 가능하다고 합니다. 빨리 공부해서 JPA를 써보고 싶습니다.... JdbcTemplate은 좀 불편하네요 ㅠㅠ

반응형