단의 개발 블로그
데이터 베이스 본문
데이터베이스
대부분의 서비스는 서버와 상호작용하여 작동한다. 스프링 서버는 클라이언트에 요청을 받고 어떠한 데이터를 가공해서 처리해서 다시 뷰로 전달한다. 이때 데이터는 저장된 어떠한 것을 사용하는데, 이전에 작업한 식자재를 수기로 입력하는 것이 아닌 데이터베이스 서버와 통신하여 데이터를 불러온다. 주로 사용되는 데이터베이스는 관계형 데이터베이스가 범용적으로 사용된다. 자바에서 해당 데이터베이스와 연결하기 위해선 JDBC와 JPA가 주로 사용된다. 스프링은 이 두가지를 모두 지원한다.
스프링 mariadb + jpa
스프링은 여러 하위 프로젝트가 다수 구성되어 있다. Spring data 연결 종류는 아래와 같다.
- Spring data JPA: 관계형 데이터 베이스
- Spring data MongoDB : 문서형 데이터 베이스 (=No SQL)
- Spring Data Neo4 : Neo4j 그래프 데이터 베이스
- Spring Data Redis : 캐시 메모리 데이터 베이스
- Spring Data Cassandra : 카산드라 데이터 베이스 (=Message)
여기서는 mariadb와 관계형 데이터베이스 jpa를 사용할 예정이다. 해당 의존성을 추가한다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
DB 의존성을 추가할 경우 DB 서버와 정상적으로 연결되어야 오류가 발생하지 않는다. 설정파일에 DB 접속 정보를 입력하여 연결해야 한다. 여기서는 설정파일을 yml로 사용한다. 설정 시 ddl-auto 옵션은 주의해야 한다. show-sql 옵션을 사용하면 sql 문을 로그에 보여준다.
application.yaml
spring:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://아이피:포트/DB명
username: 아이디
password: 비밀번호
jpa:
show-sql: true
hibernate:
ddl-auto: update
ddl-auto
- create: 애플리케이션이 실행될때 모든 테이블을 지우고 새로 생성
- create-drop: 애플리케이션이 종료하는 시점에 모든 테이블을 삭제
- update: SessionFactory 실행 시 객체를 검사하여 변경된 스키마만 갱신
- validate: update와 동일하나 스키마 변경하지 않음, 정보가 다를 경우 에러 발생
- none: ddl-auto 기능을 사용하지 않습니다.
도메인 객체 수정하기
데이터 베이스에 객체를 저장하기 위해선 고유하게 식별해주는 필드가 필요하다. Ingredients는 이미 id를 가지고 있지만 Taco와 Order는 없다. 또한 언제 생성되었는지도 필요하다. JPA 코드도 추가한다.
Ingredient.java
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PUBLIC, force = true)
@Entity
public class Ingredient {
@Id
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
- JPA 개체로 선언하기 위해선 @Entity 어노테이션을 추가해야 한다. Id 속성에는 @Id 속성을 반드시 지정하여 개체를 고유하게 식별하는 값을 알려줘야 한다.
- JPA는 매개변수가 없는 생성자를 가져야 한다. 따라서 @NoArgsConstructor 어노테이션으로 지정하여 생성자를 지정한다. accesslevel은 접근 가능한 범위이고, force는 클래스가 생성될 때 속성을 null로 할지 선택하는 옵션 값이다.
Taco.java
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Date createAt;
@NotNull
@Size(min=5, message = "Name must be at least 5 character long")
private String name;
@ManyToMany(targetEntity = Ingredient.class)
@Size(min=1, message = "You must choose at least 1 ingredient")
private List<String> ingredients;
@PrePersist
void createdAt() {
this.createAt = new Date();
}
}
- @GeneratedValue와 옵션 값으로 사용한 값은 해당 ID의 값을 자동으로 생성해서 사용한다는 설정이다.
- @ManyToMany는 타코와 식자재 관계를 표현할 때 사용되는 어노테이션이다. 하나의 타코는 많은 식자재를 가질 수 있고, 하나의 식자재는 여러 타코를 가질 수 있기 때문에 해당 어노테이션으로 지정한다.
- @PrePersist 는 타코 객체가 생성되기 전에 생성된 날짜를 지정할 때 사용한다.
Order.java
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
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;
@NotBlank(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;
@ManyToMany(targetEntity = Taco.class)
private List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
@PrePersist
void placedAt() {
this.placedAt = new Date();
}
}
- Order 클래스는 테이블 예약명이랑 겹칠 수 있어 따로 네임을 @Table 어노테이션을 사용해서 지정해준다.
JPA 리포지토리 만들기
JPA에서 제공해주는 리포지토리를 사용하여 간단하게 데이터베이스에서 질의하여 데이터를 가져올 수 있다. 새로운 패키지 repository를 생성하고 해당 파일을 만든다. @Repository 어노테이션은 @Controller 처럼 스프링에게 해당 파일이 리포지토리로 동작하는 것을 알려주는 어노테이션이다.
IngredientRepository.java
@Repository
public interface IngredientRepository extends CrudRepository<Ingredient, String> {
}
- CrudRepository 인터페이스에는 기본적으로 데이터베이스에 CRUD (create, read, update, delete)를 할 수 있는 많은 기능을 제공한다. 제네릭으로 사용하고자 하는 객체와, ID 의 타입을 지정해주면 된다.
TacoRepository.java
@Repository
public interface TacoRepository extends CrudRepository<Taco, Long> {
}
OrderRepository.java
@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {
}
애플리케이션 시작 시 데이터 추가하기
스프링 부트스트랩 클래스를 조작하면 애플리케이션이 실행 되기 전에 해야할 동작을 지정할 수 있다. 여기서는 애플리케이션이 시작하면 식자재 데이터를 추가하는 동작을 지정한다.
TacoJavaApplication.java
...
@Bean
public CommandLineRunner dataLoader(IngredientRepository repo) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
repo.save(new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
repo.save(new Ingredient("COTO", "Corn Tortilla", Type.WRAP));
repo.save(new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
repo.save(new Ingredient("CARN", "Carnitas", Type.PROTEIN));
repo.save(new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES));
repo.save(new Ingredient("LETC", "Lettuce", Type.VEGGIES));
repo.save(new Ingredient("CHED", "Cheddar", Type.CHEESE));
repo.save(new Ingredient("JACK", "Monterrey Jack", Type.CHEESE));
repo.save(new Ingredient("SLSA", "Salsa", Type.SAUCE));
repo.save(new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
}
};
}
...
데이터 베이스 사용하기
데이터베이스에 저장된 데이터를 읽은 뒤 서버에서 사용할 데이터로 변환하기 위한 컨버터를 만든다.
IngredientByIdConverter.java
@Component
public class IngredientByIdConverter
implements Converter<String, Ingredient> {
private final IngredientRepository ingredientRepository;
@Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepository = ingredientRepo;
}
@Override
public Ingredient convert(String id) {
Optional<Ingredient> optionalIngredient = ingredientRepository.findById(id);
return optionalIngredient.orElse(null);
}
}
Contoller도 수정한다.
DesignTacoController.java
@Log4j2
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
@Autowired
private TacoRepository tacoRepository;
@Autowired
private IngredientRepository ingredientRepository;
@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<>();
ingredientRepository.findAll().forEach(ingredients::add);
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
model.addAttribute("taco", new Taco());
return "design";
}
private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
}
@PostMapping
public String processDesign(@Valid Taco design, Errors errors, @ModelAttribute Order order) {
if (errors.hasErrors()) {
return "design";
}
Taco saved = tacoRepository.save(design);
order.addDesign(saved);
return "redirect:/orders/current";
}
}
OrderContoller.java
@Log4j2
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@GetMapping("/current")
public String orderForm(Model model) {
model.addAttribute("order", new Order());
return "orderForm";
}
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus) {
if (errors.hasErrors()){
return "orderForm";
}
orderRepository.save(order);
sessionStatus.setComplete();
return "redirect:/";
}
}
OrderRepository에서 save 메소드를 통해 객체를 데이터베이스에 저장한다.
주문 객체가 저장되면 세션을 보존할 필요가 없으므로 완료 시킨다. 제거하지 않을 경우 연관된 타코가 남게된다.
orderForm.html 도 수정한다.
...
<form method="POST" th:action="@{/orders}" th:object="${order}">
<h1>Order your taco creations!</h1>
<img th:src="@{/images/TacoCloud.png}" /> <a th:href="@{/design}"
id="another">Design another taco</a><br />
<ul>
<li th:each="taco : ${order.tacos}">
<span th:text="${taco.name}">taco name</span>
</li>
</ul>
...
실행하고 주문을 제출하면 데이터베이스에 해당 내용이 저장된다.



'Web > Spring' 카테고리의 다른 글
| Security (0) | 2024.10.08 |
|---|---|
| JPA (1) | 2024.09.05 |
| 사용자 요청 처리하기 (0) | 2024.09.03 |
| 간단한 웹 요청 만들기 (0) | 2024.05.08 |
| 스프링 요청 및 라이브러리 살펴보기 (1) | 2024.05.02 |