Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JDBC 라이브러리 구현하기 - 1단계] 헙크(정현승) 미션 제출합니다. #294

Merged
merged 8 commits into from
Sep 30, 2023
119 changes: 26 additions & 93 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
@@ -1,121 +1,54 @@
package com.techcourse.dao;

import com.techcourse.domain.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

public class UserDao {
private static final RowMapper<User> USER_ROW_MAPPER = rs -> new User(
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
);

private static final Logger log = LoggerFactory.getLogger(UserDao.class);
private final JdbcTemplate template;

private final DataSource dataSource;

public UserDao(final DataSource dataSource) {
this.dataSource = dataSource;
}

public UserDao(final JdbcTemplate jdbcTemplate) {
this.dataSource = null;
public UserDao(final JdbcTemplate template) {
this.template = template;
}

public void insert(final User user) {
final var sql = "insert into users (account, password, email) values (?, ?, ?)";

Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = dataSource.getConnection();
pstmt = conn.prepareStatement(sql);

log.debug("query : {}", sql);

pstmt.setString(1, user.getAccount());
pstmt.setString(2, user.getPassword());
pstmt.setString(3, user.getEmail());
pstmt.executeUpdate();
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
} finally {
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException ignored) {}

try {
if (conn != null) {
conn.close();
}
} catch (SQLException ignored) {}
}
template.update(sql, user.getAccount(), user.getPassword(), user.getEmail());
}

public void update(final User user) {
// todo
final var sql = "update users set password = ? where id = ?";
template.update(sql, user.getPassword(), user.getId());
}

public List<User> findAll() {
// todo
return null;
final String sql = "select id, account, password, email from users";
return template.query(sql, userRowMapper());
}

public User findById(final Long id) {
final var sql = "select id, account, password, email from users where id = ?";

Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();

log.debug("query : {}", sql);

if (rs.next()) {
return new User(
rs.getLong(1),
rs.getString(2),
rs.getString(3),
rs.getString(4));
}
return null;
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
} finally {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException ignored) {}

try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException ignored) {}
public Optional<User> findById(final Long id) {
final String sql = "select id, account, password, email from users where id = ?";
return template.queryForObject(sql, userRowMapper(), id);
}

try {
if (conn != null) {
conn.close();
}
} catch (SQLException ignored) {}
}
public Optional<User> findByAccount(final String account) {
final String sql = "select id, account, password, email from users where account = ?";
return template.queryForObject(sql, userRowMapper(), account);
}

public User findByAccount(final String account) {
// todo
return null;
private RowMapper<User> userRowMapper() {
return USER_ROW_MAPPER;
}
}
6 changes: 4 additions & 2 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.techcourse.domain.User;
import com.techcourse.domain.UserHistory;

import java.util.Optional;

public class UserService {

private final UserDao userDao;
Expand All @@ -15,7 +17,7 @@ public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) {
this.userHistoryDao = userHistoryDao;
}

public User findById(final long id) {
public Optional<User> findById(final long id) {
return userDao.findById(id);
}

Expand All @@ -24,7 +26,7 @@ public void insert(final User user) {
}

public void changePassword(final long id, final String newPassword, final String createBy) {
final var user = findById(id);
final var user = findById(id).orElseThrow(() -> new IllegalArgumentException("해당 유저가 존재하지 않습니다."));
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
Expand Down
15 changes: 8 additions & 7 deletions app/src/test/java/com/techcourse/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -16,7 +17,7 @@ class UserDaoTest {
void setup() {
DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());

userDao = new UserDao(DataSourceConfig.getInstance());
userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance()));
final var user = new User("gugu", "password", "[email protected]");
userDao.insert(user);
}
Expand All @@ -30,15 +31,15 @@ void findAll() {

@Test
void findById() {
final var user = userDao.findById(1L);
final var user = userDao.findById(1L).orElseThrow();

assertThat(user.getAccount()).isEqualTo("gugu");
}

@Test
void findByAccount() {
final var account = "gugu";
final var user = userDao.findByAccount(account);
final var user = userDao.findByAccount(account).orElseThrow();

assertThat(user.getAccount()).isEqualTo(account);
}
Expand All @@ -49,20 +50,20 @@ void insert() {
final var user = new User(account, "password", "[email protected]");
userDao.insert(user);

final var actual = userDao.findById(2L);
final var actual = userDao.findById(2L).orElseThrow();

assertThat(actual.getAccount()).isEqualTo(account);
}

@Test
void update() {
final var newPassword = "password99";
final var user = userDao.findById(1L);
user.changePassword(newPassword);
final var user = userDao.findById(1L).orElseThrow();

user.changePassword(newPassword);
userDao.update(user);

final var actual = userDao.findById(1L);
final var actual = userDao.findById(1L).orElseThrow();

assertThat(actual.getPassword()).isEqualTo(newPassword);
}
Expand Down
8 changes: 4 additions & 4 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -39,7 +39,7 @@ void testChangePassword() {
final var createBy = "gugu";
userService.changePassword(1L, newPassword, createBy);

final var actual = userService.findById(1L);
final var actual = userService.findById(1L).orElseThrow();

assertThat(actual.getPassword()).isEqualTo(newPassword);
}
Expand All @@ -56,7 +56,7 @@ void testTransactionRollback() {
assertThrows(DataAccessException.class,
() -> userService.changePassword(1L, newPassword, createBy));

final var actual = userService.findById(1L);
final var actual = userService.findById(1L).orElseThrow();

assertThat(actual.getPassword()).isNotEqualTo(newPassword);
}
Expand Down
58 changes: 53 additions & 5 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,65 @@
package org.springframework.jdbc.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcTemplate {

private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class);

private final DataSource dataSource;

public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public <T> Optional<T> queryForObject(final String sql, final RowMapper<T> rowMapper, Object... args) {
try (final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
try (final ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return Optional.ofNullable(rowMapper.mapRow(rs));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullable 좋네요👍👍

}
return Optional.empty();
}
} catch (final SQLException e) {
throw new DataAccessException(e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataAccessException으로 바꿔 던지는거 좋습니다👍

}
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
try (final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = conn.prepareStatement(sql)) {
final List<T> result = new ArrayList<>();
try (final ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
result.add(rowMapper.mapRow(rs));
}
return result;
}

} catch (final SQLException e) {
throw new DataAccessException(e);
}
}

public int update(final String sql, final Object... args) {
try (final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
Comment on lines +57 to +59

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 중복되어서 사용이 되는데 메서드 분리를 해보는건 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드를 추출한다면 해당 메서드로 이동하여 로직을 파악하여야 하기 때문에, 전체적인 코드의 맥락을 바로 파악하기엔 추출하지 않는 것도 좋다고 생각합니다 ㅎㅎㅎ 조금 더 복잡한 로직이었더라면 메서드로 추출하고 네이밍을 주어 가독성을 높일 수 있을 것 같다는 생각을 했습니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다! 확실히 가독성이 떨어지긴 하죠!

return pstmt.executeUpdate();
} catch (final SQLException e) {
throw new DataAccessException(e);
}
}
}
13 changes: 13 additions & 0 deletions jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.springframework.jdbc.core;

import javax.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;

@FunctionalInterface
public interface RowMapper<T> {

@Nullable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable을 붙이신 이유가 따로 있을까요?(진짜 궁금)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제 JdbcTemplate 라이브러리에서도 이 부분에 @Nullable 애너테이션이 붙어있어서 따라했습니다. 일단 이 애너테이션을 붙임으로써, 해당 메서드 mapRow의 반환값이 Null일 수 있음을 다른 개발자들이 조금 더 잘 인지할 수 있도록 하기 위함이라고 합니다.

실제로 저도 JdbcTemplate에서 mapRow()를 활용한 부분을 보자면 저는 queryForObjectquery 메서드에서 사용했습니다. 전자는 하나의 객체만 반환하는 것인데, 값이 없으면 원래는 null을 반환하곤 합니다. 저는 이를 막기 위해 Optional로 감싸 놓은 상태입니다. 후자 역시 null을 반환할 수 있지만 반환 타입이 List이므로 null에 대한 핸들링을 굳이 해주지 않았습니다.

어쨌든 저도 따라 붙이긴 했는데, 다른 개발자들에게 해당 메서드가 null을 반환할 수 있다고 알려주는 역할이라고 하네요! ㅎㅎㅎㅎ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반환값이 null 이 될 수도 있다는 뜻이었군요! 배워갑니다~😄

T mapRow(ResultSet rs) throws SQLException;

}
Loading