반응형

 

도서 '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>
        ...
  1. 의존성 추가하기(pom.xml에서 마우스 우클릭 -> Spring -> add Spring)
  2. 버전 추가하기(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

+ Recent posts