![Thumbnail image](/images/spring-basic.png)
Table of Contents
[스프링인액션] 데이터로 작업하기
개요
스프링인액션 2장을 읽고 스프링에서 JDBC, JPA를 이용하는 방법에 대해 작성하였다.
- JDBC template 사용하기
- SimpleJdbcInsert를 이용해 데이터 가공하기
- Custom Converter 사용법
에 대해 기술하였다.
JDBC로 작업하기
스프링의 JDBC 지원은 JDBCTemplate 클래스에 기반을 둔다.
JDBC Template은 Spring JDBC 접근 방법 중 하나로,
내부적으로 Plain JDBC API를 사용하지만 Plain JDBC의 문제점들을 제거하고 원래의 JDBC에서 지원되지 않는 편리한 기능을 제공하는 Spring에서 제공하는 class이다.
Plain JDBC API의 문제점
- 쿼리를 실행하기 전과 후에 연결부터 resultSet을 닫고 클린업하는 등등 많은 코드를 작성해야한다.
- 데이터베이스 로직에서 예외 처리 코드를 수행해야 한다.
- 트랜잭션을 처리해야 한다.
- 이러한 모든 코드를 반복하는 것으로, 시간이 낭비된다.
JDBC Template을 사용하지 않은 예시
1@Override
2public Ingredient findById(String id) {
3 Connection connection = null;
4 PreparedStatement statement = null;
5 ResultSet resultSet = null;
6
7 try {
8 connection = dataSource.getConnection();
9 statement = connection.prepareStatement(
10 "select id, name, type from Ingredient where id = ?");
11 statement.setString(1, id);
12 resultSet = statement.executeQuery();
13
14 Ingredient ingredient = null;
15 if (resultSet.next()) {
16 ingredient = new Ingredient(resultSet.getString("id"), resultSet.getString("name"),
17 resultSet.Type.valueOf(resultSet.getString("type")));
18 }
19 return ingredient;
20
21 } catch (SQLException e) {
22
23 } finally {
24 if (resultSet != null) {
25 try {
26 resultSet.close();
27 } catch (SQLException e) {}
28 }
29 if (statement != null) {
30 try {
31 statement.close();
32 } catch (SQLException e) {}
33 }
34 if (connection != null) {
35 try {
36 connection.close();
37 } catch (SQLException e) {}
38 }
39 }
40}
이와 같이 데이터베이스 연결 생성, 명령문 생성, 연결과 명령문 및 resultSet을 닫고 클린업하는 코드들로 쿼리 코드가 둘러싸여 있다.
또 예외처리 시 catch 블록에서 해결될 수 없으므로 현재 메서드를 호출한 상위 코드로 예외 처리를 넘겨야 하는 단점이 존재한다.
JDBC Template을 이용한 예시
1private JdbcTemplate jdbc;
2
3@Override
4public Ingredient findById(String id) {
5 return jdbc.queryForObject(
6 "select id, name, type from Ingredient where id=?",
7 this::mapRowToIngredient, id);
8}
9
10private Ingredient mapRowToIngredient(ResultSet rs, int rowNum) throws SQLException {
11 return new Ingredient(rs.getString("id"), rs.getString("name"),
12 Ingredient.Type.valueOf(rs.getString("type")));
13}
명령문(statement)나 데이터베이스 연결 객체를 생성, 메서드의 실행이 끝난 후 객체들을 클린업하는 코드가 없는 것을 알 수 있다.
또한 예외처리 (catch 블록)를 수행하는 코드도 없어 코드가 훨씬 간결해졌다.
JDBC Template 사용하기
빌드명세에 추가
1dependencies {
2 implementation "org.springframework.boot:spring-boot-starter-jdbc"
3 runtimeOnly "com.h2database:h2:2.1.212"
4}
스프링 부트의 JDBC 스타터 의존성을 build.gradle 파일에 추가한다.
또한 H2 내장 데이터베이스를 사용하기 위해 h2database에 대한 의존성도 추가한다. (https://mvnrepository.com/artifact/com.h2database/h2)
JDBC repository 정의하기
1@Repository
2public class JdbcIngredientRepository implements IngredientRepository {
3
4 private JdbcTemplate jdbc;
5
6 @Autowired
7 public JdbcIngredientRepository(JdbcTemplate jdbc) {
8 this.jdbc = jdbc;
9 }
10
11 @Override
12 public Iterable<Ingredient> findAll() {
13 return jdbc.query("select id, name, type from Ingredient", this::mapRowToIngredient);
14 }
15
16 @Override
17 public Ingredient findById(String id) {
18 return jdbc.queryForObject(
19 "select id, name, type from Ingredient where id=?",
20 this::mapRowToIngredient, id);
21 }
22
23 @Override
24 public Ingredient save(Ingredient ingredient) {
25 jdbc.update(
26 "insert into Ingredient(id, name, type) values(?, ?, ?)",
27 ingredient.getId(),
28 ingredient.getName(),
29 ingredient.getType().toString()
30 );
31 return ingredient;
32 }
33
34 private Ingredient mapRowToIngredient(ResultSet rs, int rowNum) throws SQLException {
35 return new Ingredient(rs.getString("id"), rs.getString("name"),
36 Ingredient.Type.valueOf(rs.getString("type")));
37 }
38}
-
findAll() : 객체가 저장된 컬렉션을 반환 - 쿼리로 생성된 ResultSet의 행 개수만큼 호출하고 각각의 객체로 생성하여 List에 저장한다. JdbcTemplate의 query() 메서드는 두개의 인자를 받는데, 첫번째 인자는 (1)쿼리를 수행하는 SQL, 두번째 인자는 스프링의 (2)RowMapper 인터페이스를 구현한 mapRowToIngredient 메서드이다.
-
findById() : 하나의 객체만 반환 JdbcTemplate의 queryForObject() 메서드는 세개의 인자를 받는데, 첫번째와 두번째 인자는 query() 메서드와 같고, 세번째 인자는 (3)검색할 행의 id를 전달한다.
-
update() : 데이터를 추가하거나 변경하는 쿼리에 사용할 수 있다. JdbcTemplate의 update() 메서드는 (1)쿼리를 수행하는 SQL, (2)쿼리 매개변수에 지정할 값을 인자로 전달한다.
RowMapper 인터페이스를 이용해 구현
1@Override
2public Ingredient findById(String id) {
3 return jdbc.queryForObject(
4 "select id, name, type from Ingredient where id=?",
5 new RowMapper<Ingredient>() {
6 public Ingredient mapRow(ResultSet rs, int rowNum) throws SQLException {
7 return new Ingredient(rs.getString("id"), rs.getString("name"),
8 Ingredient.Type.valueOf(rs.getString("type")));
9 };
10 }, id);
11}
RowMapper를 구현한 익명 클래스를 이용하여 findById() 메서드를 생성할 수 있다.
findById() 메서드가 호출될 때마다 익명 클래스 인스턴스가 생성되어 인자로 전달된 후 mapRow() 메서드가 실행되는 방식이다.
엔티티를 삽입한 후 자동 생성된 키를 받을 경우
다른 테이블과 외래키를 통해 참조되는 경우 객체에 대한 primary key 값이 필요한데, 상단의 update() 메서드를 사용할 경우 해당 객체의 id 값을 받아올 수 없다.
이때, 사용하는 것이 KeyHolder이다.
1private long saveTacoInfo(Taco taco) {
2 taco.setCreatedAt(new Date());
3 PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(
4 "insert into Taco (name, createdAt) values (?, ?)",
5 Types.VARCHAR, Types.TIMESTAMP
6 );
7 factory.setReturnGeneratedKeys(true);
8
9 PreparedStatementCreator psc =
10 factory.newPreparedStatementCreator(
11 Arrays.asList(
12 taco.getName(),
13 new Timestamp(taco.getCreatedAt().getTime())));
14
15 KeyHolder keyHolder = new GeneratedKeyHolder();
16 jdbc.update(psc, keyHolder);
17 return keyHolder.getKey().longValue();
18}
여기에서 update() 메서드는 (1) PreparedStatementCreator 객체와 (2) KeyHolder 객체를 인자로 받는다.
PreparedStatementCreator 객체를 만들기 위해서는
- 우선 실행할 SQL 명령어와 각 쿼리 매개변수의 타입을 인자로 전달하는 PreparedStatementCreatorFactory를 만들고,
- newPreparedStatementCreator()를 호출하여 쿼리 매개변수 값을 인자로 전달한다.
update() 메서드의 실행이 끝나면 keyHolder.getKey() 를 통해 ID 값을 반환할 수 있다.
예시 2.
1jdbc.update(connection -> {
2 PreparedStatement ps = connection
3 .prepareStatement("insert into Taco (name, createdAt) values (?, ?)");
4 ps.setString(1, taco.getName());
5 ps.setTimestamp(2, new Timestamp(taco.getCreatedAt().getTime()));
6 return ps;
7 }, keyHolder);
이와 같이 사용할 수도 있다.
SimpleJdbcInsert를 사용하기
복잡한 PreparedStatementCreator 대신 SimpleJdbcInsert를 이용하면 손쉽게 id 값을 받아올 수 있다.
SimpleJdbcInsert는 데이터를 더 쉽게 테이블에 추가하기 위해 JdbcTemplate를 래핑한 객체이다.
SimpleJdbcInsert 인스턴스 초기화
1@Repository
2public class JdbcOrderRepository implements OrderRepository {
3
4 private SimpleJdbcInsert orderInserter;
5 private ObjectMapper objectMapper;
6
7 @Autowired
8 public JdbcOrderRepository(JdbcTemplate jdbc) {
9 this.orderInserter = new SimpleJdbcInsert(jdbc).withTableName("Taco_Order")
10 .usingGeneratedKeyColumns("id");
11
12 this.objectMapper = new ObjectMapper();
13 }
14}
JdbcTemplate를 이용하여 SimpleJdbcInsert 인스턴스를 생성하는 코드이다.
SimpleJdbcInsert 객체에는 테이블 이름이 Taco_Order이고, id는 데이터베이스가 생성해주는 값으로 사용한다고 명시해주었다.
1private long saveOrderDetails(Order order) {
2 @SuppressWarnings("unchecked")
3 Map<String, Object> values = objectMapper.convertValue(order, Map.class);
4 values.put("placedAt", order.getPlacedAt());
5 long orderId = orderInserter.executeAndReturnKey(values).longValue();
6
7 return orderId;
8}
SimpleJdbcInsert는 데이터를 추가하는 execute() 메서드와 executeAndReturnKey() 메서드가 있다. - 두 메서드는 Map<String, Object>의 타입의 인자를 받는다.
Order 객체를 Map으로 변환하기 위해 objectMapper의 convertValue() 메소드를 사용하였다.
executeAndReturnKey() 메서드를 이용하면 데이터가 테이블에 저장된 후 데이터베이스에서 생성된 ID가 Number 객체로 반환된다.
Custom Converter 사용
ID 값을 사용하여 특정 객체로 변환이 필요할 경우가 있는데, 이때 Converter 클래스를 사용한다.
Converter를 사용하면 가독성을 높이는 효과도 있다.
1@Component
2public class IngredientByIdConverter implements Converter<String, Ingredient> {
3 private final IngredientRepository ingredientRepo;
4
5 @Autowired
6 public IngredientByIdConverter(IngredientRepository ingredientRepo) {
7 this.ingredientRepo = ingredientRepo;
8 }
9
10 @Override
11 public Ingredient convert(String id) {
12 return ingredientRepo.findById(id);
13 }
14}
Converter<S,T> 인터페이스를 이용하면 S타입을 T타입으로 변환할 수 있다.
상태 정보가 없기 때문에 얼마든지 빈으로 등록해서 사용해도 상관없다.
Posts in this Series
- [스프링인액션] JMX로 스프링 모니터링
- [스프링인액션] 스프링 관리
- [스프링인액션] 스프링 액추에이터 사용
- [스프링인액션] 실패와 지연 처리
- [스프링인액션] 클라우드 구성 관리
- [스프링인액션] 마이크로서비스 이해
- [스프링인액션] 리액티브 데이터 퍼시스턴스
- [스프링인액션] 리액티브 API 개발
- [스프링인액션] 리액터 개요
- [스프링인액션] 스프링 통합 플로우 사용
- [스프링인액션] 비동기 메시지 전송하기 - Kafka
- [스프링인액션] 비동기 메시지 전송하기 - RabbitMQ
- [스프링인액션] 비동기 메시지 전송하기 - JMS
- [스프링인액션] REST API 사용하기
- [스프링인액션] REST API 생성하기
- [스프링인액션] 구성 속성 사용
- [스프링인액션] 스프링 시큐리티
- [스프링인액션] 데이터로 작업하기
- [스프링인액션] 스프링 초기 설정