[Spring in Action] Ch.3 데이터로 작업하기 2

이신영·2023년 1월 24일

Spring in Action

목록 보기
4/4
post-thumbnail

JPA는 내부적으로 jdbc를 사용하여 직접 SQL을 사용하지않아서 CRUD나 수정해야할 코드가 적은 장점이 있다. JPA를 활용해 실습해보자!

1. JPA 의존성 추가

pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

2. 도메인 객체에 어노테이션 추가

JPA 매핑 어노테이션을 도메인 객체에 추가해보자!

Ingredient.java

package tacos;

import javax.persistence.Entity;
import javax.persistence.Id;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, 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
	}
}

Taco.java

package tacos;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.PrePersist;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
import java.util.Date;

@Data
@Entity
public class Taco {
	
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	private Date createdAt;
	
	@NotNull
	@Size(min=5, message="Name must be at least 5 characters long")
	private String name;
	
	@ManyToMany(targetEntity=Ingredient.class)
	@Size(min=1, message="You must choose at least 1 ingredient")
	private List<Ingredient> ingredients;
	
	@PrePersist
	void createdAt() {
		this.createdAt = new Date();
	}
}

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 javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.PrePersist;
import javax.persistence.Table;

import lombok.Data;
import java.util.Date;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
@Table(name="Taco_Order")
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;
	
	@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;
	
	@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();
	}
}

3. JPA 리포지토리 선언하기

jdbc와는 달리 리포지토리에 제공하는 메소드를 CrudRepository 인터페이스를 extend하여 사용할 수 있다.

IngredientRepository.java

package tacos.data;

import org.springframework.data.repository.CrudRepository;

import tacos.Ingredient;

public interface IngredientRepository 
			extends CrudRepository<Ingredient, String> {
	
}

TacoRepository.java

package tacos.data;

import org.springframework.data.repository.CrudRepository;

import tacos.Taco;

public interface TacoRepository 
			extends CrudRepository<Taco, Long> {
	
}

OrderRepository.java

package tacos.data;

import org.springframework.data.repository.CrudRepository;

import tacos.Order;

public interface OrderRepository 
			extends CrudRepository<Order, Long> {

}

애플리케이션이 시작될 때 스프링 데이터 JPA가 각 인터페이스 구현체를 자동으로 생성해주기 때문에 jdbc를 활용할 때 사용된 JdbcIngredientRepositoryJdbcTacoRepository, JdbcOrderRepository는 제거해주자

4. 부트스트랩 클래스 변경

CloudApplication.java

package tacos;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;

import tacos.Ingredient.Type;
import tacos.data.IngredientRepository;

@SpringBootApplication
public class TacoCloudApplication {

	public static void main(String[] args) {
		SpringApplication.run(TacoCloudApplication.class, args);
	}

	@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));
			}
		};
	}
}

부트스트랩 클래스를 변경해준 이유는 jdbc를 사용할땐 애플리케이션이 시작되면서 data.sql을 호출하여 식자재 데이터를 데이터베이스에 직접 추가하였지만 스프링 데이터 JPA를 사용하기위해 dataLoader()메소드로 식자재 데이터를 데이터베이스에 미리 저장해둘 필요가 있기 때문이다.

5. 컨버터 변경

IngredientByIdConverter.java

package tacos.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import tacos.Ingredient;
import tacos.data.IngredientRepository;

import java.util.Optional;

@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) {
		Optional<Ingredient> optionalIngredient = ingredientRepo.findById(id);
		return optionalIngredient.isPresent() ?
							optionalIngredient.get() : null;
	}
}

실행 결과

jdbc를 쓴것과 동일하게 잘 실행되는 모습

📝 요약

jdbc는 SQL Query를 직접 작성하고 쿼리수행을 전달해줄뿐인 API이라 객체지향적인 개발에 어려움이 생겼다.
JPA는 이러한 번거로운 과정을 생략해 자바코드를 사용해서 db에 접근할 수 있고 ORM(Object–relational mapping)을 사용하도록 지원하는 API이다.

✍ 후기

이전과 이어서 JPA를 사용하니 어노테이션으로 생략되는부분과 쿼리를 직접 짜지않는다는 장점이 개발에 굉장히 편리함을 주었다. jdbc까지했을땐 과연 내가 직접 이렇게 개발할 수 있을까? 라는 불안감이 엄습했는데 JPA는 매핑만해주면 알아서 쿼리문을 작성해주니 해볼만할지도? 라는 생각이 든다. 물론 그만큼 잘 알수있어야하겠지만말이다 😅 계속해서 공부해보자!


주인장의 실력이 강해져서 돌아올때까지 잠정 중지한 시리즈입니다 이 책 중반부 넘어가고나서는 진짜 어려워요..😭

profile
후회하지 않는 사람이 되자 🔥

0개의 댓글