chap 3_유정현 - JAVA-JIKIMI/SPRING-IN-ACTION-5 GitHub Wiki

JDBC

데이터 퍼시스턴스(persistence, 저장 및 지속성 유지)를 위해 RDB가 가장 많이 사용된다.

자바에서는 RDB를 사용할 수 있는 방법엔 2가지가 있다.

  1. JDBC
  2. JPA

스프링 역시 이 두가지 방법을 지원한다.


스프링의 JDBC는 JdbcTemplate 클래스에 기반을 둔다.

JdbcTemplate를 사용하면 JDBC를 사용할 때 요구되는 모든 형식적이고 상투적인 코드를 작성하지 않아도 된다.

개발자가 RDB에 대한 SQL 연산에 집중할 수 있도록 도와준다.

퍼시스턴스를 고려한 도메인 객체 수정하기

객체를 DB에 저장하고자 할 때는 객체를 고유하게 식별해주는 필드를 하나 추가하는 것이 좋다.

Ingredient 클래스는 이미 id 필드를 갖고 있다. 그러나 Taco와 Order에는 id 필드가 없으므로 추가해야 한다.

이와 더불어 타코(Taco 객체)가 언제 생성되었는지, 주문(Order 객체)이 언제 되었는지 알면 유용하다.

또한, 객체가 저장된 날짜와 시간을 갖는 필드를 각 객체에 추가할 필요도 있다.

Taco 클래스에 필요한 id와 createdAt 속성을 추가한다.

package tacos;

import java.util.List;
import java.util.Date;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Data;

@Data
public class Taco {

	private Long id;
        private Date createdAt;

...

}

getter와 setter 및 생성자는 런타임 시에 Lombok이 자동 생성해 주므로 id와 createdAt 속성만 추가하면 된다.

같은 방법으로 Order 클래스도 수정한다.

package tacos;

import java.util.ArrayList;
import java.util.List;
import java.util.Date;

import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.CreditCardNumber;

import lombok.Data;

@Data
public class Order {
	
	private Long id;
        private Date placedAt;

...

}

JdbcTemplate 사용하기

JdbcTemplate을 사용하려면 JdbcTemplate를 프로젝트의 classpath에 추가해야 한다.

이때는 다음과 같은 스프링 부트의 JDBC 스타터 의존성을 빌드 명세에 추가하면 간단히 해결된다

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

또한, 데이터를 저장하는 DB가 필요하다. H2 내장 DB 의존성을 추가한다

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

H2 DB의 경우에 의존성 추가와 더불어 버전 정보도 추가해야 한다.

<properties>
	...
	<h2.version>1.4.196</h2.version>
	...
</properties>

JDBC 리퍼지터리 정의하기

식자재 리퍼지터리는 다음 연산을 수행해야 한다.

  • DB의 모든 식자재 데이터를 쿼리하여 Ingredient 객체 컬렉션(여기서는 List)에 넣어야 한다.
  • id를 사용해서 하나의 Ingredient를 쿼리해야 한다.
  • Ingredient 객체를 DB에 저장해야 한다.

다음의 IngredientRepository 인터페이스에서는 방금 얘기한 세 가지 연산을 메서드로 정의한다.

package tacos.data;

import tacos.Ingredient;

public interface IngredientRepository {

	Iterable<Ingredient> findAll();

	Ingredient findById(String id);

	Ingredient save(Ingredient ingredient);

}

Ingredient 리퍼지터리가 해야 할 일을 IngredientRepository 인터페이스에 정의했으므로

이제는 JdbcTemplate을 이용해서 DB 쿼리에 사용할 수 있도록 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 tacos.Ingredient;

@Repository
public class JdbcIngredientRepository implements IngredientRepository {

	private JdbcTemplate jdbc;

	@Autowired
	public JdbcIngredientRepository(JdbcTemplate jdbc) {
	  this.jdbc = jdbc;
	}
}

여기에서 JdbcIngredientRepository 클래스에는 @Repository 애노테이션이 지정되었다.

이것은 @Controller와 @Component 외에 스프링이 정의하는 몇 안되는 스테레오타입 애노테이션 중 하나다.

즉, JdbcIngredientRepository 클래스에 @Repository를 지정하면, 스프링 컴포넌트 검색에서 이 클래스를 자동으로 찾는다.

그리고 스프링 애플리케이션 컨텍스트의 빈으로 생성해 준다.

그리고 JdbcIngredientRepository 빈이 생성되면 @Autowired 애노테이션을 통해서 스프링이 해당 빈을 JdbcTemplate에 주입한다.

JdbcIngredientRepository JdbcTemplate jdbc 필드는 DB의 데이터를 쿼리하고 추가하기 위해 다른 메서드에서 사용될 것이다.

이러한 메서드로 findAll과 findById를 살펴보자

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

JdbcIngredientRepository 클래스는 IngredientRepository 인터페이스를 구현하므로

클래스 이름 다음에 implements IngredientRepository를 추가하였다.

그리고 findAll과 findById는 모두 유사한 방법으로 JdbcTemplate을 사용한다.

객체가 저장된 컬렉션을 반환하는 findAll 메서드는 JdbcTemplate의 query()메소드를 사용한다.

query 메서드는 두 개의 인자를 받는다.

첫 번째 인자는 쿼리를 수행하는 SQL이며

두 번째 인자는 스프링의 RowMapper 인터페이스를 구현한 우리가 구현한 mapRowToIngredient 메서드다. 아래는 RowMapper 인터페이스의 정의이다.

@FunctionalInterface
public interface RowMapper<T> {
    @Nullable
    T mapRow(ResultSet var1, int var2) throws SQLException;
}

이 메서드는 쿼리로 생성된 결과 세트(ResultSet 객체)의 행 개수만큼 호출되며

결과 세트의 모든 행을 각각 객체(Ingredient)로 생성하고 List에 저장한 후 반환한다.

findById 메서드는 하나의 Ingredient 객체만 반환한다

따라서 query 메서드 대신 JdbcTemplate의 queryForObject 메서드를 사용한다.

이 메서드는 query 메서드와 동일하게 실행되지만, 객체의 List를 반환하는 대신 하나의 객체만 반환한다는 것이 다르다.

queryForObject 메서드의 첫 번째와 두 번째 인자는 query 메서드와 같다.

세 번째 인자로는 검색할 행의 id를 전달한다. 그러면 이 id가 첫 번째 인자로 전달된 SQL에 있는 물음표 대신 교체되어 쿼리에 사용된다.

findAll와 findById 메서드들은 스프링 RowMapper 인터페이스를 구현한 mapRowToIngredient 메서드의 참조를 jdbc 메소드에 전달한다.

이는 java 8 에서 메서드 참조와 람다가 추가되었기 때문이며 JdbcTemplate을 사용할 때 매우 편리하다.

데이터 추가하기

DB의 데이터를 읽으려면 데이터를 쓸 수 있어야 하므로 지금부터는 IngredientRepository 인터페이스의 save 메서드를 구현한다.

JdbcTemplate의 update 메서드는 DB에 데이터를 추가하거나 변경하는 어떤 쿼리에도 사용될 수 있다.

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 {

...

	  @Override
	  public Ingredient save(Ingredient ingredient) {
	    jdbc.update(
	        "insert into Ingredient (id, name, type) values (?, ?, ?)",
	        ingredient.getId(),
	        ingredient.getName(),
	        ingredient.getType().toString());
	    return ingredient;
	  }
...
}

JdbcTemplate의 update 메서드는 결과 세트의 데이터를 객체로 생성할 필요가 없으므로 query나 queryForObject보다 훨씬 간단하다.

update 메서드에는 수행될 SQL을 포함하는 문자열과 쿼리 매개변수에 지정할 값만 인자로 전달한다.

이것으로 JdbcIngredientRepository가 완성되었다. 이제는 JdbcIngredientRepository을 DesignTacoController에 주입한다.

그리고 showDesignForm에 있던 하드코딩된 List를 삭제한다.

package tacos.web;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.validation.Errors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;

import tacos.Order;
import tacos.data.IngredientRepository;


import lombok.extern.slf4j.Slf4j;
import tacos.Taco;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.data.TacoRepository;

@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {

	private final IngredientRepository ingredientRepo;
	
	private TacoRepository tacoRepo;

	@Autowired
	public DesignTacoController(
			IngredientRepository ingredientRepo, TacoRepository tacoRepo) {
	  this.ingredientRepo = ingredientRepo;
	  this.tacoRepo = tacoRepo;
	}

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

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

	  @ModelAttribute(name = "order")
	  public Order order() {
	    return new Order();
	  }

	  @ModelAttribute(name = "taco")
	  public Taco taco() {
	    return new Taco();
	  }

	  @PostMapping
	  public String processDesign(
			  @Valid Taco design, 
			  Errors errors, @ModelAttribute Order order) {
		  if (errors.hasErrors()) {
			 return "design";
		  }

		  Taco saved = tacoRepo.save(design);
		  order.addDesign(saved);

		  return "redirect:/orders/current";
	  }

}

애플리케이션을 실행할 준비가 거의 다 되었다.

그러나 쿼리에서 참조되는 Ingredient 테이블로부터 데이터를 읽기 위해서는 먼저 이 테이블을 생성한 후 식자재 데이터를 미리 추가해 놓아야 한다.

스키마 정의하고 데이터 추가하기

Ingredient 테이블 외에도 주문 정보와 타코 디자인(각 타코의 식자재 구성) 정보를 저장할 테이블들이 필요하다.

아래의 SQL을 사용하여 스키마를 정의한다.

  • Ingredient : 식자재 정보를 저장한다.
  • Taco : 사용자가 식자재를 선택하여 생성한 타코 디자인에 관한 정보를 저장한다.
  • Taco_Ingredients : Taco와 Ingredient 테이블 간의 관계를 나타내며, Taco 테이블의 각 행에 대해 하나 이상의 행을 포함한다.
  • Taco_Order : 주문 정보를 저장한다
  • Taco_Order_Tacos : Taco_Order와 Taco 테이블 간의 관계를 나타내며, Taco_Order 테이블의 각 행에 대해 하나 이상의 행을 포함한다.
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);

create table if not exists Taco_Order (
  id identity,
    deliveryName varchar(50) not null,
    deliveryStreet varchar(50) not null,
    deliveryCity varchar(50) not null,
    deliveryState varchar(2) not null,
    deliveryZip varchar(10) not null,
    ccNumber varchar(16) not null,
    ccExpiration varchar(5) not null,
    ccCVV varchar(3) not null,
    placedAt timestamp not null
);

create table if not exists Taco_Order_Tacos (
  tacoOrder bigint not null,
  taco bigint not null
);

alter table Taco_Order_Tacos
    add foreign key (tacoOrder) references Taco_Order(id);
alter table Taco_Order_Tacos
    add foreign key (taco) references Taco(id);

이 스키마 정의를 어디에 두어야 할까? 스프링 부트가 그 답을 알려준다.

schema.sql이라는 이름의 파일이 애플리케이션 classpath의 루트 경로에 있으면

애플리케이션이 시작될 때 schema.sql 파일의 SQL이 사용중인 데이터페이스에서 자동 실행된다.

따라서 위 SQL을 schema.sql라는 이름으로 src/main/resources 폴더에 저장하면 된다.

그리고 또한 식자재 데이터를 DB에 미리 저장해야 한다.

다행스럽게도 스프링 부트는 애플리케이션이 시작될 때 data.sql이라는 이름의 파일도 자동 실행한다.

delete from Taco_Order_Tacos;
delete from Taco_Ingredients;
delete from Taco;
delete from Taco_Order;

delete from Ingredient;
insert into Ingredient (id, name, type)
                values ('FLTO', 'Flour Tortilla', 'WRAP');
insert into Ingredient (id, name, type)
                values ('COTO', 'Corn Tortilla', 'WRAP');
insert into Ingredient (id, name, type)
                values ('GRBF', 'Ground Beef', 'PROTEIN');
insert into Ingredient (id, name, type)
                values ('CARN', 'Carnitas', 'PROTEIN');
insert into Ingredient (id, name, type)
                values ('TMTO', 'Diced Tomatoes', 'VEGGIES');
insert into Ingredient (id, name, type)
                values ('LETC', 'Lettuce', 'VEGGIES');
insert into Ingredient (id, name, type)
                values ('CHED', 'Cheddar', 'CHEESE');
insert into Ingredient (id, name, type)
                values ('JACK', 'Monterrey Jack', 'CHEESE');
insert into Ingredient (id, name, type)
                values ('SLSA', 'Salsa', 'SAUCE');
insert into Ingredient (id, name, type)
                values ('SRCR', 'Sour Cream', 'SAUCE');

다음으로 Taco와 Order의 리퍼지터리를 작성해 보자.

타코와 주문 데이터 추가하기

지금까지 JdbcTemplate을 사용해서 DB에 데이터를 저장하는 방법을 전반적으로 알아보았다.

JdbcIngredientRepository의 save 메서드에서는 update 메서드를 사용해서 객체를 DB로 저장한다.

사실 JdbcTemplate을 사용해서 데이터를 저장하는 방법은 다음 두 가지가 있다.

  • 직접 update 메서드를 사용

  • SimpleJdbcInsert 래퍼 클래스 사용

객체를 저장할 때보다 퍼시스턴스 처리가 더 복잡할 때는 어떻게 update 메서드를 사용하는지 알아보자

JdbcTemplate을 사용해서 데이터 저장하기

앞의 식자재에서 했던 것처럼 우선 타코와 주문 리퍼지터리에서 Taco와 Order 객체를 저장하기 위한 인터페이스를 정의하자.

Taco 객체를 저장하는데 필요한 TacoRepository 인터페이스는 다음과 같다.

package tacos.data;

import tacos.Taco;

public interface TacoRepository {

	Taco save(Taco design);
	
}

Order 객체를 저장하는데 필요한 OrderRepository 인터페이스는 다음과 같다.

package tacos.data;

import tacos.Order;

public interface OrderRepository {

	Order save(Order order);
	
}

사용자가 식자재를 선택하여 생성한 타코 디자인을 저장하려면

해당 타코와 연관된 식자재 데이터도 Taco_Ingredients 테이블에 저장해야 한다.

어떤 식자재를 해당 타코에 넣을지 알 수 있어야 하기 때문이다.

마찬가지로 주문을 저장하려면 해당 주문과 연관된 타코 데이터를 Taco_Order_Tacos 테이블에 저장해야 한다.

해당 주문에 어떤 타코들이 연관된 것인지 알 수 있어야 하기 때문이다.

이런 이유로 식자재를 저장하는 것보다 타코와 주문을 저장하는 것이 조금 더 복잡하다.

TacoRepository를 구현하려면 다음 일을 수행하는 save메서드를 구현해야 한다.

즉, 타코 디자인 정보(예를 들어, 이름과 생성 시간)를 저장한 다음에

Taco 객체 id와 이 객체의 List에 저장된 각 Ingredient 객체 id를 Taco_Ingredients 테이블의 행으로 추가한다.

package tacos.data;

import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Date;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import tacos.Ingredient;
import tacos.Taco;

@Repository
public class JdbcTacoRepository implements TacoRepository {

	private JdbcTemplate jdbc;

	  public JdbcTacoRepository(JdbcTemplate jdbc) {
	    this.jdbc = jdbc;
	  }

	  @Override
	  public Taco save(Taco taco) {
	    long tacoId = saveTacoInfo(taco);
	    taco.setId(tacoId);
	    
	    // 스프링 Converter를 우리가 구현한 IngredientByIdConverter의 Convert() 메서드가 이때 자동 실행된다.
	    for (Ingredient ingredient : taco.getIngredients()) { 
	      saveIngredientToTaco(ingredient, tacoId);
	    }

	    return taco;
	  }

	  private long saveTacoInfo(Taco taco) {
	    taco.setCreatedAt(new Date());
	    PreparedStatementCreator psc =
	        new PreparedStatementCreatorFactory(
	            "insert into Taco (name, createdAt) values (?, ?)",
	            Types.VARCHAR, Types.TIMESTAMP
	        ).newPreparedStatementCreator(
	           Arrays.asList(
	               taco.getName(),
	               new Timestamp(taco.getCreatedAt().getTime())));

	    KeyHolder keyHolder = new GeneratedKeyHolder();
	    jdbc.update(psc, keyHolder);

	    return keyHolder.getKey().longValue();
	  }

	  private void saveIngredientToTaco(
	          Ingredient ingredient, long tacoId) {
	    jdbc.update(
	        "insert into Taco_Ingredients (taco, ingredient) " +
	        "values (?, ?)",
	        tacoId, ingredient.getId());
	  }

}

save 메서드에서는 우선 Taco 테이블에서 각 식자재를 저장하는 saveTacoInfo 메서드를 호출한다.

그리고 이 메서드에서 반환된 타코Id를 사용해서 타코와 식자재의 연관 정보를 저장하는 saveIngredientToTaco 메서드를 호출한다.

조금 골치 아픈 코드는 saveTacoInfo 메서드에 있다.

Taco 테이블에 하나의 행을 추가할 때는 DB에서 생성되는 id를 알아야 한다.

그래야만 각 식자재를 저장할 때 참조할 수 있기 때문이다.

더 앞에서 식자재 데이터를 저장할 때 사용했던 update 메서드로는

생성된 타코 id를 얻을 수 없으므로 여기서는 다른 update 메서드가 필요하다.

여기서 사용하는 update 메서드는 PreparedStatementCreator 객체와 KeyHolder 객체를 인자로 받는다.

생성된 타코 id를 제공하는 것이 바로 이 KeyHolder다. 그러나 이것을 사용하기 위해서는 PreparedStatementCreator도 생성해야 한다.

PreparedStatementCreator 객체의 생성은 간단하지 않다.

실행할 SQL 명령과 각 쿼리 매개변수의 타입을 인자로 전달하여

PreparedStatementCreatorFactory 객체를 생성하는 것으로 시작한다.

그리고 이 객체의 newPreparedStatementCreator 메서드를 호출하며

이때 PreparedStatementCreator를 생성하기 위해 쿼리 매개변수의 값을 인자로 전달한다.

이렇게 하여 PreparedStatementCreator 객체가 생성되면

이 객체와 KeyHolder 객체(여기서는 GeneratedKeyHolder 인스턴스)를 인자로 전달하여 update를 호출할 수 있다.

그리고 update 메소드의 실행이 끝나면 keyHolder.getKey().longValue()의 연속 호출로 타코 ID를 반환할 수 있다.

그 다음에 save 메서드로 제어가 복귀된 후 saveIngredientToTaco를 호출하여

Taco 객체의 List에 저장된 각 Ingredient 객체를 반복 처리한다.

saveIngredientToTaco 메서드는 더 간단한 형태의 update를 사용해서

타코 id와 Ingredient 객체 참조를 Taco_Ingredients 테이블에 저장한다.

그런데 여기까지 코드 작성이 끝나면 save 메소드의 for 문에서 에러가 표시될 것이다.

왜냐하면 현재 Taco 클래스의 ingredients 속성은 해당 타코와 연관된 식자재들의 id(String 타입)만 저장하는 List이기 때문이다.

따라서 ingredients 속성의 자동 생성된 getter인 getIngredients 메서드에서 반환되는 List의 각 요소는

String 타입이므로 Ingredient 객체를 반복 처리하는 for문에서 타입이 일치하지 않는다.

그러므로 이 시점에서 Taco 클래스의 ingredients 속성을 Ingredient 객체로 저장하는 List로 변경해야 한다.

package tacos;

import java.util.List;
import java.util.Date;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Data;

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

이제는 TacoRepository를 DesignTacoController에 주입하고 타코를 저장할 때 사용하는 일만 남았다.

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {

	private final IngredientRepository ingredientRepo;
	
	private TacoRepository tacoRepo;

	@Autowired
	public DesignTacoController(
			IngredientRepository ingredientRepo, TacoRepository tacoRepo) {
	  this.ingredientRepo = ingredientRepo;
	  this.tacoRepo = tacoRepo;
	}
}

그리고 processDesign 메서드도 수정하자

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {

	  @ModelAttribute(name = "order")
	  public Order order() {
	    return new Order();
	  }

	  @ModelAttribute(name = "taco")
	  public Taco taco() {
	    return new Taco();
	  }

	  @PostMapping
	  public String processDesign(
			  @Valid Taco design, 
			  Errors errors, @ModelAttribute Order order) {
		  if (errors.hasErrors()) {
			 return "design";
		  }

		  Taco saved = tacoRepo.save(design);
		  order.addDesign(saved);

		  return "redirect:/orders/current";
	  }

}

위 코드에서 주목할 점은 @SessionAttributes("order")가 추가되고 order와 taco 메서드에는 @ModelAttribute가 추가되었다는 점이다.

taco 메서드와 동일하게 order 메서드의 @ModelAttribute 어노테이션은 Order 객체가 모델에 생성되도록 해준다.

그러나 하나의 세션에서 생성되는 Taco 객체와 다르게 주문은 다수의 HTTP요청에 걸쳐 존재해야 한다.

다수의 타코를 생성하고 그것들을 하나의 주문으로 추가할 수 있게 하기 위해서다.

이때 클래스 수준의 @SessionAttributes 애노테이션을 주문과 같은 모델 객체에 지정하면 된다.

그러면 세션에서 계속 보존되면서 다수의 요청에 걸쳐 사용될 수 있다.

하나의 타코 디자인을 실제로 처리(저장)하는 일은 processDesign 메서드에서 수행된다.

Order 매개변수에는 @ModelAttribute 애노테이션이 지정되었다.

이 매개변수의 값이 모델로부터 전달되어야 한다는 것과 스프링 MVC가 이 매개변수에 요청 매개변수를 바인딩하지 않아야 한다는 것을 나타내기 위해서다.

전달된 데이터의 유효성 검사를 한 후 processDesign 메서드에서는 주입된 TacoRepository를 사용해서 타코를 저장한다.

그다음에 세션에 보존된 Order에 Taco 객체를 추가한다.

타코 디자인을 저장하고 주문과 연결시키는 코드를 추가한 후에는 processDesign 메서드의 order.addDesign(saved);에서 에러가 표시될 것이다.

현재 Order 클래스에서는 addDesign 메서드가 없기 때문이다. 또한 해당 주문과 연관된 Taco 객체들을 저장하는 List타입의 속성인 tacos도 추가한다.

Order.java의 제일 끝에 다음과 같이 tacos속성과 addDesign 메서드를 추가하자

@Data
public class Order {
	private List<Taco> tacos = new ArrayList<>();
	
	public void addDesign(Taco design) {
	  this.tacos.add(design);
	}
}

사용자가 주문 폼에 입력을 완료하고 제출할 때까지 Order 객체는 세션에 남아있고 DB에 저장되지 않는다.

이제는 주문을 저장하기 위해 OrderController가 OrderRepository를 사용할 수 있어야 한다.

OrderRepository를 구현하는 클래스를 작성하자.

⚠️ **GitHub.com Fallback** ⚠️