Chap 3. 데이터로 작업하기
스프링 인 액션(5판) 챕터 3장을 요약한 내용 입니다.
- 1.데이터 퍼시스턴스(persistence)를 이용한 CRUD
- 2.상용구 코드(boilerplate code)를 없애기 위한 스프링의 JDBC 전환
- 3.코드를 간소화 하기 위해 JPA 적용
JdbcTemplate은 JDBC를 사용할 떄 요구되는 모든 형식적이고 상투적인 코드없이 개발자가 관계형 데이터베이스에 대한 SQL 연산을 수행할 수 있는 방법을 제공한다.
만약 JdbcTemplate을 사용하지 않고 자바로 간단한 SQL 쿼리를 수행하는 방법을 살펴보자
@Override
public Ingredient findById(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) {
// ??? What should be done here ???
} 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;
}
이것과 대조되는 JdbcTemplate 사용 메서드를 알아보자
public Ingredient findById(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
해당 코드에는 명령문이나 데이터베이스 연결 객체를 생성하는 코드가 아예 없다. 그리고 메서드의 실행이 끝난 후 그런 객체들을 클린업하는 코드 또한 없다. 또한, catch 블록에서 올바르게 처리할 수 없는 예외를 처리하는 어떤 코드도 없다.
findAll()과 findById() 모두의 두 번째 인자로는 스프링 RowMapper 인터페이스를 구현한 mapRowToIngredient() 메서드의 참조가 전달된다. 이처럼 메서드 인자로 다른 메서드의 참조를 전달할 수 있는 것은 자바 8에서 메서드 참조와 람다(lambda)가 추가되었기 때문이며, JdbcTemplate을 사용할 때 매우 편리하다. 그러나 종전처럼 RowMapper 인터페이스의 mapRow(0 메서드를 구현하는 방법을 사용할 수도 있다.
schema.sql이라는 이름의 파일이 애플리케이션 classpath의 루트 경로에 있으면 애플리케이션이 시작될 때 schema.sql 파일의 SQL이 사용 중인 데이터베이스에서 자동 실행된다. 따라서 schema.sql이라는 이름의 파일로 src/main/resources 폴더에 저장하면 된다.
create table if not exists Ingredient (
id varchar(4) not null,
name varchar(25) not null,
type varchar(10) not null
);
create table if not exists Taco (
id identity,
name varchar(50) not null,
createdAt timestamp not null
);
create table if not exists Taco_Ingredients (
taco bigint not null,
ingredient varchar(4) not null
);
alter table Taco_Ingredients
add foreign key (taco) references Taco(id);
alter table Taco_Ingredients
add foreign key (ingredient) references Ingredient(id);
DesignTacoController의 processDesign() 메서드를 수정해보자.
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
@Autowired
public DesignTacoController(
IngredientRepository ingredientRepo,
TacoRepository designRepo) {
this.ingredientRepo = ingredientRepo;
this.designRepo = designRepo;
}
@ModelAttribute(name = "order")
public Order order() {
return new Order();
}
@ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
@GetMapping
public String showDesignForm(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));
}
return "design";
}
...
}
@SessionAttributes("order")를 사용하는 이유는 Order 인스턴스를 세션에 담아두기 위함이다. 하나의 세션에서 생성되는 Taco 객체와 다르게 주문은 다수의 HTTP 요청에 걸쳐 존재해야 한다. 다수의 타코를 생성하고 그것들을 하나의 주문으로 추가할 수 있게 하기 위해서다. 그러면 세션에서 계속 보존되면서 다수의 요청에 걸쳐 사용될 수 있다.
주문 객체가 데이터베이스에 저장된 후에는 더 이상 세션에 보존할 필요가 없다. 그러나 만일 제거하지 않으면 이전 주문 및 이것과 연관된 타코가 세션에 남아 있게 되어 다음 주문은 이전 주문에 포함되었던 타코 객체들을 가지고 시작하게 될 것이다.
@PostMapping
public String processOrder(@Valid Order order, Errors errors,
SessionStatus sessionStatus) {
if (errors.hasErrors()) {
return "orderForm";
}
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}
마지막으로 데이터의 타입을 변환해 주는 컨버터 클래스를 작성하자. 이 클래스는 스프링의 Converter 인터페이스에 정의된 convert() 메서드를 구현한다.
@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
@Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id);
}
}
JDBC 버전의 리퍼지터리에서는 리퍼지터리가 제공하는 메서드를 우리가 명시적으로 선언하였다. 그러나 스프링 데이터에서는 그 대신 CrudRepository 인터페이스를 확장(extends)할 수 있다. CrudRepository 인터페이스에는 데이터베이스의 CRUD 연산을 위한 많은 메서드가 선언되어 있다. CrudRepository는 매개변수화 타입이다.
지금까지 작성한 로직은 비교적 간단한 쿼리에서는 유용할 수 있다. 그러나 더 복잡한 쿼리의 경우는 메서드 이름만으로는 감당하기 어렵다. 따라서 이때는 어떤 이름이든 우리가 원하는 것을 지정한 후 해당 메서드가 호출될 때 수행되는 쿼리에 @Query 애노테이션을 지정하자.
@Query("Order o where o.deliveryCity = 'Seattle'")
List<Order> readOrdersDeliveredInSeattle();
이름 규칙을 준수하여 쿼리를 수행하는 것이 어렵거나 불가능할 때에도 @Query를 사용할 수 있다.
Last modified 3yr ago