요즘 객체 지향 설계를 위해 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 이후 나오는 타입들을 변환해주는 컨버터를 직접 생성해야 할지 모르는 부분이었다.
'JAVA_SPRING' 카테고리의 다른 글
스프링을 이용한 메시지 기능과 메시지 기능 국제화(HTTP 헤더 값을 이용한 방법과 파라미터를 이용한 방법) (0) | 2022.01.22 |
---|---|
스프링에서 로깅 (0) | 2022.01.18 |
SPRING - 스프링 기본 원리 정리를 끝으로 생각 정리 (0) | 2021.12.28 |
SPRING - 프록시 (0) | 2021.12.28 |
SPRING - 웹 스코프 (0) | 2021.12.28 |