성실한 사람이 되자

성실하게 글쓰자

This is spear

JAVA_SPRING

spring - JDBC을 사용한 데이터 저장과 출력, 그리고 auto increment 키 값 가져오기

Imaspear 2022. 2. 15. 09:47
728x90

 

요즘 객체 지향 설계를 위해 JPA를 공부하고 있어서 서버에서 데이터베이스에 입출력할 수 있는 라이브러리에 관심이 많아졌다. 스프링에서는 MyBatis, JDBC, JPA가 대표적으로 존재했고, 한국에서 두 번째로 많이 사용하는 JDBC에 관심이 생겼다. 구글 트렌드에서 국내에서의 관심도를 보면 JDBC의 관심도가 생각보다 많았다. 그 외 전 세계적으로도 JDBC의 관심도가 많아서 오랜만에 JDBC를 이용해 데이터를 입출력하는 프로젝트를 만들어보자는 생각을 했다.

 

 

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
<!--<scope>runtime</scope>-->
</dependency>

application.properties

spring.datasource.url= jdbc:h2:mem:test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.sql.init.mode=always
logging.level.com.github.prgrms.socialserver = debug

h2 데이터베이스를 이용한 인메모리 형식의 데이터베이스를 생성해 값을 입출력했다.

spring.datasource.url= jdbc:h2:mem:test

resorces 폴더에 sql 파일을 생성해 프로젝트가 시작하면서 데이터베이스에 데이터를 넣어줬다.

spring.sql.init.mode=always

 

JDBCUserRepository

인터페이스에 4가지 기능을 명세했고, JdbcTemplate 클래스를 이용해 jdbc와 연동을 했다.

public interface UserRepository {
    Long save(SignUpUser user);

    SearchedUser findById(Long id);

    List<SearchedUser> findAll();

    boolean findByEmail(String email);
}
@Repository
@RequiredArgsConstructor
public class JDBCUserRepository implements UserRepository{
    private final JdbcTemplate template;
}

Long save(SignUpUser user)

save() 메서드를 만들면서,,,

  • `template.update()'를 사용하는 이유
    • insert 쿼리를 사용하는데 update() 메서드를 사용하는 이유는 auto_increment 를 이용해 키 값을 생성해 빈 테이블을 만들어 그 빈 테이블에 값을 넣는 update 쿼리를 날리기 때문이다.
  • auto_increment 되는 id 값을 어떻게 가져올 것인가?
    • KeyHolder keyHolder = new GeneratedKeyHolder(); 메서드를 이용해 새로 생성되는 키 값을 잡아주는 역할을 하는 객체가 필요하다.
    • 그리고 아래와 같이 prepareStatement() 메서드를 사용해 값을 저장할 때, 키 값을 받을 수 있는 new String[]{"seq"} 이라는 영역을 추가해줘야 한다.
    • 마지막으로 keyHolder에 저장된 키를 가져와 컨버팅해줘야 한다. 서버와 데이터베이스와의 트레이드오프시에 오류가 발생하는 상황이 아니라면 null값이 들어오지 않을거라 생각한다.
    public static final String INSERT_MESSAGE_SQL = "insert into users(email, passwd) values (?, ?)" ;

    @Override
    public Long save(SignUpUser user) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(connection -> {
        PreparedStatement ps = connection
            .prepareStatement(INSERT_MESSAGE_SQL, new String[]{"seq"});
            ps.setString(1, user.getPrincipal());
            ps.setString(2, user.getCredentials());
            return ps;
            }, keyHolder);
        return (Long) keyHolder.getKey();
    }

SearchedUser findById(Long id), List<SearchedUser> findAll()

  • NoUserDataException : RuntimeException을 상속받은 클래스를 생성해 해당 id에 데이터가 존재하지 않으면 해당 예외를 던져준다.
  • rs.getTimestamp(data).toLocalDateTime() : java8 이후부터는 Date, Timestamp 타입 말고 LocalDateTime 타입을 지원한다. 하지만 JDBC에서는 LocalDateTime 타입으로 바로 변환할 수 있는 메서드가 존재하지 않아서 getTimestamp() 메서드를 이용하고, toLocalDateTime() 메서드를 이용했다.

LocalDateTime으로 변환할 때의 null값을 어떡하는가?

last_login_at 해당 값은 default가 null인데, rs.getTimestamp(data).toLocalDateTime() 코드로 값을 변경할 때, last_login_at 값이 null이면 NullPointerException 오류가 발생했다. getTimestamp() 메서드까지는 null 값이 입력이 되지만 Timestamp 타입으로 변경된 값이 LocalDateTime 타입으로 변경이 될 때, 오류가 발생한다.

java.lang.NullPointerException: Cannot invoke "java.sql.Timestamp.toLocalDateTime()" because the return value of "java.sql.ResultSet.getTimestamp(String)" is null

getTimestamp(), oLocalDateTime()

더보기
더보기

getTimestamp()

    @Override
    public Timestamp getTimestamp(String columnLabel) throws SQLException {
        try {
            debugCodeCall("getTimestamp", columnLabel);
            return get(columnLabel).getTimestamp(null);
        } catch (Exception e) {
            throw logAndConvert(e);
        }
    }

toLocalDateTime()

    @SuppressWarnings("deprecation")
    public LocalDateTime toLocalDateTime() {
        return LocalDateTime.of(getYear() + 1900,
                                getMonth() + 1,
                                getDate(),
                                getHours(),
                                getMinutes(),
                                getSeconds(),
                                getNanos());
    }

 

    public static final String SELECT_MESSAGE_SQL = "select email, login_count, last_login_at, create_at from users";

     RowMapper<SearchedUser> usersRowMapper = ((rs, rowNum) -> {
        if (LastLoginIsNull(rs)) {
            return new SearchedUser(rs.getString("email"),
                    rs.getInt("login_count"),
                    null,
                    getToLocalDateTime(rs, "create_at"));
        }
        return new SearchedUser(rs.getString("email"),
                rs.getInt("login_count"),
                getToLocalDateTime(rs, "last_login_at"),
                getToLocalDateTime(rs, "create_at"));
    });

    private LocalDateTime getToLocalDateTime(ResultSet rs, String data) throws SQLException {
        return rs.getTimestamp(data).toLocalDateTime();
    }

    private boolean LastLoginIsNull(ResultSet rs) throws SQLException {
        return rs.getTimestamp("last_login_at") == null;
    }


    @Override
    public SearchedUser findById(Long id) {
        String sql = SELECT_MESSAGE_SQL +" where seq ="+ id;
        log.debug("query : {}", sql);
        try {
            return template.queryForObject(sql, usersRowMapper);
        } catch (EmptyResultDataAccessException e) {
            log.debug("{}", e.getMessage());
            throw new NoUserDataException("찾는 사용자가 존재하지 않습니다.");
        }
    }

    @Override
    public List<SearchedUser> findAll() {
        String sql = SELECT_MESSAGE_SQL;
        log.debug("query : {}", sql);
        List<SearchedUser> userList = template.query(sql, usersRowMapper);
        if (userList.isEmpty()) {
            throw new NoUserDataException("사용자가 존재하지 않습니다.");
        }
        return userList;
    }

boolean findByEmail(String email)

    @Override
    public boolean findByEmail(String email) {
        String sql =  "select count(*) from users where email = '" + email + "'";
        log.debug("query : {}", sql);
        Integer integer = template.queryForObject(sql, new RowMapper<Integer>() {
            @Override
            public Integer mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getInt(1);
            }
        });
        log.debug("already email : {}", integer);
        return Objects.requireNonNull(integer).equals(1);
    }

 

JDBC를 사용하면서 드는 생각

  • 데이터베이스의 엔티티와 바로 매핑되는 JPA와는 다르게 JDBC는 DTO를 만들어 원하는 값만 가져오고 저장할 수 있다.
  • 문자열로 쿼리를 짤 때, 띄어쓰기에 신경 써야 하는 점이 매우 불편했다.
  • LocaDateTime 타입으로 바로 변환해주는 메서드가 존재하지 않는다.

 

JDBC의 가장 큰 특징은 데이터베이스에 존재하는 엔티티와 1대1 매핑을 해서 사용할 필요가 없다는 점이고, View에서 원하는 값만 가져와 출력할 수 있다는 점이었다. 하지만 문자열로 쿼리를 작성할 때, 띄어쓰기와 같은 자잘한 부분을 신경 써야 하는 점이 불편했고, LocaDateTime 타입과 같이 java8 이후 나오는 타입들을 변환해주는 컨버터를 직접 생성해야 할지 모르는 부분이었다.