14장 RESTful 웹 서비스: 장바구니 페이지 만들기

­이주현 (Joo Hyun Lee)·2023년 5월 15일
0
post-thumbnail

RESTful 웹 서비스의 개요

RESTful 웹 서비스는 HTTP와 웹의 장점을 최대한 활용할 수 있는 아키텍처인 REST(REpresentational State Transfer) 원리를 사용하여 구현된 웹 서비스입니다. REST는 HTTP에서 어떤 자원에 대한 CRUD 요청을 리소스와 메서드로 표현하여 특정한 형태로 전달하는 방식입니다. 즉, 어떤 자원에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하려고 URI로 자원을 명시하고 GET, POST, PUT, DELETE 등 HTTP 방식을 사용해서 요청을 보내며, 요청에 대한 자원은 JSON, XML, TEXT, RSS 등 특정한 형태(representation of resource)로 표현됩니다.

RESTful 웹 서비스는 HTTP와 웹의 장점을 최대한 활용할 수 있는 아키텍처인 REST(REpresentational State Transfer) 원리를 사용하여 구현된 웹 서비스입니다. REST는 HTTP에서 어떤 자원에 대한 CRUD 요청을 리소스와 메서드로 표현하여 특정한 형태로 전달하는 방식입니다. 즉, 어떤 자원에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하려고 URI로 자원을 명시하고 GET, POST, PUT, DELETE 등 HTTP 방식을 사용해서 요청을 보내며, 요청에 대한 자원은 JSON, XML, TEXT, RSS 등 특정한 형태(representation of resource)로 표현됩니다.

pom.xml 파일에 jackson-databind.jar 또는 jackson-mapper-asl.jar을 의존 라이브러리로 등록해야 합니다.

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>     		
    <artifactId>jackson-databind</artifactId>     
    <version>2.9.10</version>
</dependency>

<dependency>
	<groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>     	
    <version>1.9.11</version> 
</dependency>  

RESTful 방식의 애너테이션

REST 방식으로 컨트롤러를 작성할 때 사용되는 주요 애너테이션은 다음과 같습니다.

@RestController: @Controller와 @ResponseBody를 결합한 REST API를 제공하는 컨트롤러를 의미합니다.
@RequestBody: 컨트롤러 요청 처리 메서드의 매개변수에 선언되면 요청된 HTTP 요청 body를 해당 매개변수에 바인딩합니다.
@ResponseBody: 컨트롤러 요청 처리 메서드의 매개변수에 선언되면 반환 값을 응답 HTTP 응답 body에 바인딩합니다. 스프링은 요청된 메시지의 HTTP 헤더에 있는 Content-Type을 기반으로 HTTP Message converter를 사용하여 반환 값을 HTTP 응답 body로 변환합니다.

@RequestBody

@RequestBody 애너테이션은 HTTP 요청 body 내용인 XML, JSON 또는 기타 데이터 등을 자바 객체로 매핑하는 역할을 합니다. 일반적으로 폼 페이지에서 전송되는 매개변수가 name=value 형태이면 @RequestParam이나 @ModelAttribute로 전달받습니다. 하지만 XML이나 JSON처럼 형식을 갖춘 문자열 형태라면 @RequestParam이나 @ModelAttribute로 전달받을 수 없기 때문에 @RequestBody를 이용해야 합니다.
@RequestBody는 컨트롤러 내에 요청 처리 메서드의 매개변수에 설정하며, HTTP 요청 body 내용을 메서드의 매개변수가 전달받을 뿐만 아니라 HTTP 요청 body 내용 전체를 해당 매개변수 타입으로 변환해 줍니다.

예제01-1


사용자 웹 요청 URL이 http://.../exam01이면 Example01Controller 컨트롤러의 요청 처리 메서드 showForm()으로 webpage14_01.jsp 파일을 출력합니다. 폼 페이지에서 name, age, email 항목에 각각 ‘HongGilSon’, ‘20’, ‘hong@naver.com’을 입력하면 Example01Controller 컨트롤러의 요청 처리 메서드 submit()에서 @RequestBody가 선언된 매개변수 param은 name=HongGilSon&age=20&email=hong@naver.com을 전달받습니다. 그리고 webpage14_result.jsp 파일에 각각 title과 result 값을 출력합니다.

예제01-2


사용자 웹 요청 URL이 http://.../json이면 Example01Controller2 컨트롤러의 요청 처리 메서드 showForm()으로 webpage14_02.jsp 파일을 출력합니다. 폼 페이지에서 실행하기 버튼을 누르면 웹 요청 URL http://.../test가 호출되어 웹 요청 URL에 해당하는 Example01Controller2 컨트롤러의 요청 처리 메서드 submit()이 실행됩니다.
submit() 메서드에서 @RequestBody가 선언된 매개변수인 HashMap 타입의 map 객체는 HttpMessageConverter 타입의 메시지 변환기로 JSON 형식인 {"name":"kim","age":"30"}으로 전달받게 됩니다.

@RequestBody

@ResponseBody 애너테이션은 자바 객체를 HTTP 응답 body 내용으로 매핑하는 역할을 합니다. @RequestBody처럼 XML이나 JSON 형식을 갖춘 문자열 형태로 응답할 때 이용합니다.
컨트롤러 내 요청 처리 메서드 수준으로 설정하며, 요청 처리 메서드의 반환 결과 값을 HTTP 응답 body 내용으로 전달합니다.

예제02


사용자 웹 요청 URL이 http://.../exam02이면 Example02Controller 컨트롤러의 요청 처리 메서드 submit()에서 설정된 person 객체의 값을 JSON 형식으로 변환해서 {“name”: “HongGilSon”,“age”:“20”,“email”:“Hong@naver.com”}처럼 응답합니다.

@RestController

@RestController 애너테이션은 컨트롤러에 @ResponseBody가 추가된 것으로 JSON 형태로 데이터를 반환합니다. @Controller와는 다르게 @RestController는 반환 값에 자동으로 @ResponseBody가 붙어 자바 객체가 HTTP 응답 body 내용에 매핑되어 전달됩니다.
@RestController를 사용하면 @ResponseBody를 사용하지 않아도 되지만, @Controller일 때는 반드시 @ResponseBody를 선언해야 합니다.

예제03


앞서 살펴본 컨트롤러 내 요청 처리 메서드에 @ResponseBody를 선언한 예에서 @Controller 대신 @RestController로 선언하면 @ResponseBody를 생략할 수 있고, @ResponseBody처럼 요청 처리 메서드의 반환 결과 값을 JSON 형식으로 변환해서 응답합니다.

예제04

ResponseEntity는 HTTP 요청에 대한 응답 데이터를 포함하는 클래스로, 상태 코드(HttpStatus), 헤더(HttpHeaders), 몸체(HttpBody)를 포함합니다.
@RestController는 별도의 뷰 페이지를 제공하지 않는 형태로 실행하기 때문에 결과 데이터가 예외적인 오류를 발생할 수 있습니다. 이에 사용자가 직접 결과 데이터와 HTTP 상태 코드를 제어할 수 있습니다.

실습: RESTful 방식의 장바구니 기본 구조 만들기

1. 장바구니 정보가 담긴 도메인 객체 생성하기

1) com.springmvc.domain 패키지에 CartItem 클래스를 생성한다.
2) com.springmvc.domain 패키지에 Cart 클래스를 생성한다.
클래스 선언 시 생성자, getter와 setter는 툴바의 기능을 사용하면 손쉽게 생성이 가능하다.
setGrandTotal()은 없다.

2. 장바구니 정보를 관리하는 퍼시스턴스 계층 구현하기

1) com.springmvc.repository 패키지에 CartRepository 인터페이스를 생성한다.
2) com.springmvc.repository 패키지에 CartRepositoryImpl 클래스를 생성하고 create()와 read() 메서드를 구현한다.

package com.springmvc.repository;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Repository;
import com.springmvc.domain.Cart;

@Repository
public class CartRepositoryImpl implements CartRepository{
	
	private Map<String, Cart> listOfCarts;
	
	public CartRepositoryImpl() {
		listOfCarts = new HashMap<String, Cart>();
	}
	
	public Cart create(Cart cart) {
		if (listOfCarts.keySet().contains(cart.getCartId())) {
			throw new IllegalArgumentException(String.format("장바구니를 생성할 수 없습니다. 장바구니 id(%)가 존재하지 않습니다", cart.getCartId()));
		}
		
		listOfCarts.put(cart.getCartId(), cart);
		return cart;
	}
	
	public Cart read(String cartId) {
		return listOfCarts.get(cartId);
	}
	
	public void upadte(String cartId, Cart cart) {
		if (!listOfCarts.keySet().contains(cartId)) {
			throw new IllegalArgumentException(String.format("장바구니 목록을 갱신할 수 없습니다. 장바구니 id(%)가 존재하지 않습니다", cartId));
		}
		listOfCarts.put(cartId, cart);
	}
	
	public void delete(String cartId) {
		if (!listOfCarts.keySet().contains(cartId)) {
			throw new IllegalArgumentException(String.format("장바구니 목록을 삭제할 수 없습니다. 장바구니 id(%)가 존재하지 않습니다", cartId));
		}
		listOfCarts.remove(cartId);
	}
}
  • create() 메서드는 새로운 장바구니를 생성하여 장바구니 ID를 등록하고 생성된 장바구니 객체를 반환합니다. 동일한 장바구니 ID가 존재하면 예외 처리를 위해 IllegalArgumentException() 메서드를 호출합니다.
  • read() 메서드는 장바구니 ID를 이용하여 장바구니에 등록된 모든 정보를 가져와 반환합니다.

3. 도서 장바구니 정보를 반환하는 서비스 계층 구현하기

1) com.springmvc.service 패키지에 CartService 인터페이스를 생성한다.
2) com.springmvc.service 패키지에 CartServiceImpl 클래스를 생성하고 create()와 read() 메서드를 구현한다.

package com.springmvc.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.springmvc.domain.Cart;
import com.springmvc.repository.CartRepository;

@Service
public class CartServiceImpl implements CartService{

	@Autowired
	private CartRepository cartRepository;
	
	public Cart create(Cart cart) {
		return cartRepository.create(cart);
	}
	
	public Cart read(String cartId) {
		return cartRepository.read(cartId);
	}
	
	public void update(String cartId, Cart cart) {
		cartRepository.upadte(cartId, cart);
	}
	
	public void delete(String cartId) {
		cartRepository.delete(cartId);
	}
}
  • create() 메서드는 장바구니 저장소 객체에서 생성한 장바구니를 가져와 반환합니다.
  • read() 메서드는 저장소 객체에서 장바구니 ID에 대해 장바구니에 등록된 모든 정보를 가져와 반환합니다.

4. MVC를 담당하는 프레젠테이션 계층 구현하기

1) com.springmvc.controller 패키지에 CartController 클래스를 생성한 후 장바구니 요청을 위한 requestCartId()와 requestCartList() 메서드를 작성한다.

package com.springmvc.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.http.HttpStatus;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.springmvc.domain.Book;
import com.springmvc.domain.Cart;
import com.springmvc.domain.CartItem;
import com.springmvc.service.BookService;
import com.springmvc.service.CartService;
import com.springmvc.exception.BookIdException;

@Controller
@RequestMapping(value="/cart")
public class CartController {

	@Autowired
	private CartService cartService;
	
	@Autowired
	private BookService bookService;
	
	@GetMapping
	public String requestCartId(HttpServletRequest request) {
		String sessionid = request.getSession(true).getId();
		return "redirect:/cart/" + sessionid;
	}
	
	@PostMapping
	public @ResponseBody Cart create(@RequestBody Cart cart) {
		return cartService.create(cart);
	}
	
	@GetMapping("/{cartId}")
	public String requestCartList(@PathVariable(value="cartId") String cartId, Model model) {
		Cart cart = cartService.read(cartId);
		model.addAttribute("cart", cart);
		return "cart";
	}
	
	@PutMapping("/{cartId}")
	public @ResponseBody Cart read(@PathVariable(value="cartId") String cartId) {
		return cartService.read(cartId);
	}
  • requestCartId() 메서드는 웹 요청 URL이 http://localhost:8080/BookMarket/cart/일 때 요청 처리 메서드로 사용자 요청을 처리합니다. 세션 ID 값을 가져와서 URI cart/sessionid로 리다이렉션합니다.
  • create() 메서드는 웹 요청 URI가 /BookMarket/cart/고 HTTP 메서드가 POST 방식이면 매핑되는 요청 처리 메서드로, 사용자 요청을 처리합니다. Cart 클래스 정보를 HTTP 요청 body로 전달받아 장바구니를 새로 생성하고 HTTP 응답 body로 전달합니다.
  • requestCartList() 메서드는 웹 요청 URI가 /BookMarket/cart/cartId고, HTTP 메서드가 GET 방식이면 매핑되는 요청 처리 메서드로, 사용자 요청을 처리합니다. 요청 URL에서 경로 변수 cartId(장바구니 ID)에 대해 장바구니에 등록된 모든 정보를 읽어 와 커맨드 객체 cart 속성에 등록하고, 뷰 이름을 cart로 반환하므로 JSP 파일은 cart.jsp가 됩니다.
  • read() 메서드는 웹 요청 URI가 /cart/cartId고, HTTP 메서드가 PUT 방식이면 매핑되는 요청 처리 메서드로 사용자 요청을 처리합니다. read() 메서드는 요청 URL에서 경로 변수인 장바구니 ID(cartId)에 대해 장바구니에 등록된 모든 정보를 가져옵니다.
    2) 도서를 장바구니에 담고자 book.jsp 파일에 코드를 추가한다.
    3) 장바구니에 해당하는 cart.jsp 파일을 생성한다.
    *items와 item을 구분해서 작성해야 한다.

RESTful 웹 서비스의 CRUD

웹 애플리케이션은 게시판에 게시글을 올리고(Create), 읽고(Read), 수정하고(Update), 삭제하는(Delete) 등 리소스에 대한 CRUD 연산을 모두 포함하고 있습니다.
기존 웹 접근 방식으로 웹 게시판을 개발한다면 GET과 POST만으로도 CRUD 연산을 모두 처리할 수 있습니다. 예를 들어 웹 게시판의 글 읽기와 삭제하기는 GET 방식을 이용하고, 글쓰기와 수정하기는 POST 방식을 이용합니다. 하지만 URI는 실행을 위한 액션을 나타낼 뿐 제어하려는 리소스나 리소스 위치를 명확하게 나타내지는 않습니다. 이와 같이 기존 웹 접근 방식으로는 URI를 통해 제어하려는 리소스가 무엇이며, 리소스에서 GET이나 POST 방식으로만 어떤 액션을 할 것인지 명확하게 식별할 수 없습니다.
최근 웹 애플리케이션은 HTTP 메서드로 GET, PUT, POST, DELETE와 URI를 통해 리소스의 접근을 명확히 식별할 수 있도록 RESTful 웹 서비스 기반으로 개발되고 있습니다.

이런 RESTful 웹 서비스에서 GET, PUT, POST, DELETE 등 HTTP 메서드를 사용하려면 web.xml 파일에 다음과 같이 HiddenHttpMethodFilter 클래스를 설정해야 합니다.

	<filter>
		<filter-name>httpMethodFilter</filter-name>
		<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>httpMethodFilter</filter-name>
		<servlet-name>appServlet</servlet-name>
	</filter-mapping>

RESTful 웹 서비스에서 HTTP 메서드가 리소스에 접근하여 수행하는 CRUD 연산 작업은 다음과 같습니다.

  • POST: 기존 리소스를 갱신하거나 새로운 리소스를 생성하는 데 사용합니다. (Create)
  • GET: 리소스를 조회하여 읽어 오는 데 사용합니다. (Read)
  • PUT: 리소스를 변경하는 데 사용합니다. (Update)
  • DELETE: 기존 리소스를 삭제하는 데 사용합니다. (Delete)
  • OPTION: 기존 리소스에 대한 리소스 작업을 얻는 데 사용합니다.

예제04


사용자 웹 요청 URL이 http://.../exam05이면 Example05Controller 컨트롤러의 요청 처리 메서드 showForm()으로 webpage14_03.jsp 파일을 출력합니다. 폼 페이지에서 name, age, email 항목에 각각 ‘HongGilSon’, ‘20’, ‘hong@naver.com’을 입력하면 Example05Controller 컨트롤러의 요청 처리 메서드 submit()에서 매개변수 person으로 전달받습니다.

실습: RESTful 웹 서비스를 위한 장바구니 CRUD 만들기

1. HiddenHttpMethodFilter 필터 설정하기

	<filter>
		<filter-name>httpMethodFilter</filter-name>
		<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>httpMethodFilter</filter-name>
		<servlet-name>appServlet</servlet-name>
	</filter-mapping>

2. 장바구니에 도서 등록하기

1) Cart 클래스 안에 addCartItem() 메서드를 추가로 작성한다. addCartItem() 메서드는 도서 목록 중 선택한 도서를 장바구니에 등록한다.

2) CartRepository 인터페이스에 updateCart() 메서드를 선언한다.

3) CartRepositoryImpl 클래스에 update() 메서드를 구현한다.

4) CartService 인터페이스에 update() 메서드를 선언한다.

5) CartServiceImpl 클래스에 update() 메서드를 구현한다.

6) CartController 클래스에 장바구니에 등록하는 addCartByNewItem() 메서드를 추가로 작성한다.

  • addCartByNewItem() 메서드는 HTTP 메서드가 PUT 방식으로 요청 URI가 /cart/add/{bookId}일 때 경로 변수 bookId에 대해 해당 도서를 장바구니에 추가로 등록하고 장바구니를 갱신한다.

7) webapp/resources 폴더에 js 폴더를 생성한다. 여기에 자바스크립트 controllers.js 파일을 만든 후 장바구니에 등록하는 addToCart() 메서드를 작성한다.

8) 도서를 장바구니에 담는 book.jsp 파일을 수정한다.

  • ${pageContext.request.contextPath}는 요청 경로가 바뀌어도 소스를 수정하지 않고 적용하는 데 사용됩니다.
  • 스프링에서 제공하는 폼 태그 라이브러리 중 <form:form> 태그를 사용하여 [도서주문 >>] 버튼 동작을 수행하는 설정을 추가했습니다. 도서주문 >> 버튼을 누르면 자바스크립트의 addToCart() 함수가 호출됩니다. 또한 <form:form> 태그에 선언된 웹 요청 URI ../cart/add/${book.bookId} 및 HTTP 메서드가 PUT 방식으로 전송됩니다. CartController 클래스의 addCartByNewItem() 메서드에 매핑되어 해당 도서가 장바구니에 추가로 등록됩니다.

9) 9. 웹 브라우저 주소창에 ‘http://localhost:8080/BookMarket/books’를 입력해서 실행합니다. 도서 상세 정보 화면에서 도서주문 >> 버튼을 눌러 해당 도서를 장바구니에 추가합니다.

3. 장바구니에 등록된 도서 항목별 삭제하기

1) Cart 클래스에 장바구니에 등록된 도서 항목을 삭제하는 removeCartItem() 메서드를 추가한다.

2) CartController 클래스에 장바구니에 등록된 도서 항목을 삭제하는 removeCartByItem() 메서드를 추가한다.

  • removeCartByItem() 메서드는 HTTP 메서드가 PUT 방식으로 요청 URI가 /cart/remove/{bookId}일 때 경로 변수 bookId에 대해 해당 도서를 장바구니에서 삭제하고 장바구니를 갱신합니다.

3) 자바스크립트 controllers.js 파일에 장바구니에 등록된 도서 항목을 삭제하는 removeFromCart() 메서드를 추가한다.

4) 장바구니에 해당하는 cart.jsp 파일에 장바구니에 등록된 도서 항목을 삭제하기 위해 코드를 수정한다.

  • 애플리케이션에 적용할 자바스크립트 파일 controllers.js의 경로 위치를 설정합니다.
  • 스프링에서 제공하는 폼 태그 라이브러리 중 <form:form> 태그를 사용하여 [삭제] 버튼에 동작을 수행하는 설정을 추가했습니다. 삭제 버튼을 누르면 자바스크립트의 removeFromCart() 함수가 호출됩니다. 또한 웹 요청 URI ../cart/remove/도서ID 및 HTTP 메서드가 PUT 방식으로 전송됩니다. CartController 클래스의 removeCartByItem() 메서드에 매핑되어 장바구니에 등록된 도서 정보 중에서 선택한 도서를 삭제합니다.

5) 장바구니 화면에서 삭제 버튼을 누르면 해당 도서가 장바구니에서 삭제된다.

4. 장바구니에 등록된 모든 도서 삭제하기

1) CartRepository 인터페이스에 delete() 메서드를 선언한다.

2) CartRepositoryImpl 클래스에 delete() 메서드를 구현한다.

3) CartService 인터페이스에 delete() 메서드를 선언한다.

4) CartServiceImpl 클래스에 delete() 메서드를 구현한다.

5) CartController 클래스에 장바구니에 등록된 모든 도서를 삭제하는 deleteCartList() 메서드를 추가한다.

  • deleteCartList() 메서드는 웹 요청 URI가 /BookMarket/cart/cartId고 HTTP 메서드가 DELETE 방식일 때 매핑되는 요청 처리 메서드로, 사용자 요청을 처리합니다. delete() 메서드는 요청 URL에서 경로 변수인 장바구니 ID(cartId)에 대해 장바구니에 등록된 모든 정보를 삭제합니다.

6) 자바스크립트 controllers.js 파일에 장바구니에 저장된 모든 도서 항목을 삭제하는 clearCart() 메서드를 추가로 작성한다.

7) 장바구니에 해당하는 cart.jsp 파일에 장바구니에서 저장된 모든 도서 항목을 삭제하기 위해 코드를 수정한다.

  • 스프링에서 제공하는 폼 태그 라이브러리 중 <form:form> 태그를 사용하여 [삭제하기] 버튼에 동작을 수행하는 설정을 추가했습니다. 삭제하기 버튼을 누르면 자바스크립트의 clearCart() 함수를 호출합니다. 또한 디폴트 웹 요청 URI ../cart/cartId 및 HTTP 메서드가 DELETE 방식으로 전송됩니다. 이는 CartController 클래스의 deleteCartList() 메서드에 매핑되어 장바구니에 등록된 정보를 삭제해서 장바구니를 비웁니다.

8) 장바구니 화면에서 삭제하기 버튼을 누르면 장바구니에 담긴 모든 도서가 장바구니에서 삭제된다.

마치며

웹 애플리케이션에서 HTTP와 웹 애플리케이션 아키텍처 REST 원리를 이용하여 구현하는 RESTful 웹 서비스를 구축하는 방법을 살펴보았습니다. 또한 RESTful 웹 서비스를 위해 자바스크립트를 애플리케이션에 적용하는 방법도 살펴보았습니다.

0개의 댓글