반응형
도서 'Spring in Action' 제 5판을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.
키워드 : JDBC, SQL, 쿼리
1. JDBC
관계형 데이터베이스를 사용할 경우
- JDBC
- JPA
를 사용(둘 다 스프링이 지원)
JDBC의 지원 = JdbcTemplate Class에 기반을 둠.
JdbcTemplate을 사용하지 않은 경우
@Override
public Ingredient findOne(String id) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
statement = connection.prepareStatement(
"select id, name, type from Ingredient");
statement.setString(1, id);
resultSet = statement.executeQuery();
Ingredient ingredient = null;
if(resultSet.next()) {
ingredient = new Ingredient(
resultSet.getString("id"),
resultSet.getString("name"),
Ingredient.Type.valueOf(resultSet.getString("type")));
}
return ingredient;
} catch (SQLException e) {
// 여기서 무엇을 해야할까?
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {}
}
}
return null;
}
<코드 분석>
- 해당 코드는 데이터베이스를 쿼리하는 코드가 있지만 연결(connection) 생성, 명령문(statement) 생성, 그리고 연결과 명령문 및 결과 세트(result set)를 닫고 클린업 하는 코드들로 쿼리코드가 둘러쌓여있음.
- 연결이나 명령문 등의 객체를 생성할 때 또는 쿼리를 수행할 때 예외발생가능 -> SQLException 예외 처리
JdbcTemplate을 사용한 경우
private JdbcTemplate jdbc;
@Override
public Ingredient findOne(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
<코드 분석>
- 명령문 혹은 데이터베이스 연결 객체를 생성하는 코드가 아예 없음.
- 예외 처리x
- 쿼리를 수행(JdbcTemplate의 queryForObject() 메서드)하고 그 결과를 Ingredient 객체로 생성하는(mapRowToIngredient() 메서드) 것에 초점을 두는 코드만 존재
현재 만들고 있는 Taco 웹 애플리케이션에 추가해보기
<사전 작업>
Taco.java
package tacos;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
import java.util.Date;
@Data
public class Taco {
private Long id;
private Date createdAt;
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
}
Order.java
package tacos;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.CreditCardNumber;
import lombok.Data;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
@Data
public class Order {
private Long id;
private Date placedAt;
@NotBlank(message="Name is required")
private String deliveryName;
@NotBlank(message="Street is required")
private String deliveryStreet;
@NotBlank(message="City is required")
private String deliveryCity;
@NotBlank(message="State is required")
private String deliveryState;
@NotBlank(message="Zip code is required")
private String deliveryZip;
@CreditCardNumber(message="Not a valid credit card number")
private String ccNumber;
@Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$",
message="Must be formatted MM/YY")
private String ccExpiration;
@Digits(integer=3, fraction=0, message="Invalid CVV")
private String ccCVV;
private List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
}
<추가된 점>
- Ingredient는 id 필드를 가지고 있지만 Taco와 Order는 id 필드가 없으므로 추가
- 타코가 언제 생성되었는지, 주문이 언제 되었는지 알면 유용
- Taco에 createdAt 추가
- Order에 placedAt 추가
<JdbcTemplate 사용하기>
Jdbc 사용을 위해 우리 프로젝트 classpath에 추가
...
<properties>
<java.version>11</java.version>
<h2.version>1.4.196</h2.version>
</properties>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
...
- 의존성 추가하기(pom.xml에서 마우스 우클릭 -> Spring -> add Spring)
- 버전 추가하기(H2 데이터베이스의 경우 의존성 추가와 더불어 버전 정보도 추가)
JDBC 리퍼지터리 정의
- 데이터베이스의 모든 식자재 데이터를 쿼리하여 Ingredient 객체 컬렉션(여기서는 List)에 넣어야 함
- id를 사용해서 하나의 Ingredient를 쿼리
- Ingredient 객체를 데이터베이스에 저장해야 함
(데이터베이스에 관련된 클래스와 인터페이스는 tacos.data 패키지에 둘 예정)
package tacos.data;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findById(String id);
Ingredient save(Ingredient ingredient);
}
JdbcTemplate을 이용해서 데이터베이스 쿼리에 사용할 수 있도록 IngredientRepository 인터페이스 구현
package tacos.data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import tacos.Ingredient;
@Repository
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbc;
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
@Override
public Ingredient findById(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
@Override
public Ingredient save(Ingredient ingredient) {
jdbc.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
}
<코드 분석>
- @Repository 어노테이션
- @Controller와 @Component외에 스프링이 정의하는 몇 안되는 stereotype(스테레오타입)어노테이션 중 하나
- 스프링 컴포넌트 검색에서 해당 클래스를 자동으로 찾아서 스프링 애플리케이션 컨텍스트 빈으로 생성
- 생성자
- JdbcIngredientRepository 빈이 생성되면 @Autowired 어노테이션을 통해서 스프링이 해당 빈을 JdbcTemplate에 주입(연결)함
- JdbcTemplate 참조를 인스턴스 변수(jdbc)에 저장 -> 데이터베이스의 데이터를 쿼리하고 추가하기 위해 다른 메서드에서 활용
- findAll()
- 객체가 저장된 컬렉션을 반환
- query() 메서드 사용
- 2개의 인자를 받음
- query(쿼리를 수행하는 SQL(select 명령), 스프링의 RowMapper 인터페이스를 구현한 mapRowToIngredient 메서드)
- 해당 쿼리에서 요구하는 매개변수들의 내역을 마지막 인자로 받을 수 있음
- A::B -> A객체의 B메서드를 사용하겠다는 뜻
- findById()
- 하나의 Ingredient 객체만 반환 -> query()가 아닌 queryForObject() 메서드 활용
- query와 동일하게 실행되지만 객체의 List를 반환하는 대신 하나의 객체만 반환
- queryForObject(쿼리를 수행하는 SQL(select 명령), 스프링의 RowMapper 인터페이스를 구현한 mapRowToIngredient 메서드, 검색할 행의 id(여기서는 식자재의 id))
- 세번째 인자의 id는 SQL에 있는 ?에 대입
- mapRowToIngredient
- 쿼리로 생성된 결과 세트(ResultSet 객체)의 행(row)개수만큼 호출
- 결과 세트의 모든 행을 각각 객체(여기서는 Ingredient)로 생성하고 List에 저장한 후 반환
- jdbc.update()
- 데이터베이스를 추가하거나 변경하는 어떤 쿼리에도 사용
- update()는 결과 세트의 데이터를 객체로 생성할 필요가 없음
- 수행될 SQL을 포함하는 문자열("select ...")과 쿼리 매개변수(?)에 저장할 값만 인자(get 활용)로 전달
※ stereotype(스테레오타입)어노테이션
- 역할 그룹을 나타내는 어노테이션
- @Component
- 스프링이 자동으로 탐색하여 생성하는 빈으로 특정 클래스를 지정하는 클래스 수준 어노테이션
- @Repository
- @Component에서 특화된 데이터 액세스 관련 어노테이션
- @Controller
- @Component에서 특화된 어노테이션, 지정된 클래스가 스프링 웹 MVC 컨트롤러라는 것을 알려줌
<DesignTacoController에 JdbcIngredientRepository를 주입>
...
@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@GetMapping
public String showDesignFrom(Model model) {
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepo.findAll().forEach(i -> ingredients.add(i));
Type[] types = Ingredient.Type.values();
for(Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
model.addAttribute("taco", new Taco());
return "design";
}
...
- 데이터베이스로부터 읽은 데이터로 생성한 Ingredient 객체의 List를 제공
- findAll()메서드를 showDesignForm()메서드에서 호출
참고 자료
1. 크레이그 월즈, Spring in Action, Fifth Edition(출판지 : 제이펍, 2020)
반응형
'Spring' 카테고리의 다른 글
Spring 데이터로 작업하기 3 (0) | 2022.05.18 |
---|---|
Spring 데이터로 작업하기 2 (0) | 2022.05.18 |
Spring 웹 애플리케이션 개발 3 (0) | 2022.05.11 |
Spring 웹 애플리케이션 개발 2 (0) | 2022.05.11 |
Spring 웹 애플리케이션 개발 1 (0) | 2022.05.11 |