Spring boot로 REST API 구현(MyBatis)

✒️ 2025-05-28 13:34 내용 수정



주문 추가 및 조회하는 페이지 만들기 2

1. DB 연결 및 테이블 구성

  1. DB 연결을 위한 Mybatis 설정을 참고하여 DB 연결에 필요한 MyBatis 설정을 진행한다.
  2. 3 Tier Architecture#1. DB 연결 및 테이블 구성에서 사용한 테이블을 그대로 사용한다.

2. 전체 코드 흐름

spring boot rest projectfile.png

3. DTO와 Mapper 인터페이스, mapper.xml 생성

  1. src/main/java 폴더의 com.example.tier처럼 group id와 artifact로 된 패키지의 하위 패키지로 dto, vo, mapper 패키지를 만든다.
  2. dto 패키지에 ProductDTO와 OrderDTO를 만든다.
package com.example.rest.dto;

import lombok.Data;

@Data
public class ProductDTO {
	private int productId;
	private String productName;
	private int productStock;
	private int productPrice;
	private String registerDate;
	private String updateDate;
}
package com.example.rest.dto;

import lombok.Data;

@Data
public class OrderDTO {

	private int orderId;
	private int productId;
	private int productCount;
	private String orderDate;

}
  1. FK관계로 이어진 Product 테이블과 Order 테이블의 정보를 모두 담을 수 있는 OrderVO 클래스를 vo 패키지에 만든다.
package com.example.rest.vo;

import lombok.Data;

@Data
public class OrderVO {
	private int productId;
	private String productName;
	private int productStock;
	private int productPrice;
	private String registerDate;
	private String updateDate;
	
	private int orderId;
	private int productCount;
	private String orderDate;
	private int orderPrice;
	
}
  1. mapper 패키지에 ProductMapper와 OrderMapper를 인터페이스로 만든다.
    • Mapper 클래스에는 @Mapper Annotation을 추가하고, Mapper에 작성된 메소드 이름은 SQL문의 id와 동일해야 한다.
    • ProductMapper에 상품을 1개 조회하는 select 메소드를 추가하고, 파라미터는 productId를 받도록 설정한다.
package com.example.rest.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.rest.dto.OrderDTO;
import com.example.rest.dto.ProductDTO;

@Mapper
public interface ProductMapper {

	// 상품 추가
	// INSERT INTO PRODUCT VALUES(?,?,?,?,?)
	public void insert(ProductDTO productDTO);
	
	// 상품 조회
	public List<ProductDTO> selectAll();
	
	// 상품 재고 수정
	public void updateStock(OrderDTO orderDTO);
	
	// 상품 1개 조회
	public ProductDTO select(int productId);
}
package com.example.rest.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;

@Mapper
public interface OrderMapper {

	// 주문 추가
	public void insert(OrderDTO orderDTO);
	
	// 주문 조회
	public List<OrderVO> selectAll(String sort);
}
  1. src/main/resources 폴더에 mapper 패키지를 만들고, Mapper 인터페이스와 연결할 mapper.xml(product.xml과 order.xml)의 SQL문을 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
	  
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
		<typeAlias type="com.example.rest.dto.ProductDTO" alias="productDTO"/>
		<typeAlias type="com.example.rest.dto.OrderDTO" alias="orderDTO"/>
    	<typeAlias type="com.example.rest.vo.OrderVO" alias="orderVO"/>
    </typeAliases>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.rest.mapper.ProductMapper">
	<insert id="insert">
		INSERT INTO PRODUCT (
			PRODUCT_ID,
			PRODUCT_NAME,
			PRODUCT_STOCK,
			PRODUCT_PRICE
		) VALUES(
			SEQ_PRODUCT.nextVal,
			#{productName},
			#{productStock},
			#{productPrice}
		)
	</insert>
	
	<select id="selectAll">
		<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
		SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_STOCK, 
				PRODUCT_PRICE, REGISTER_DATE, UPDATE_DATE 
		FROM PRODUCT
	</select>
	
	<update id="updateStock">
		UPDATE PRODUCT
		SET PRODUCT_STOCK = PRODUCT_STOCK - #{productCount}
		WHERE PRODUCT_ID = #{productId}
	</update>
	
	<select id="select">
		SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_STOCK,
				PRODUCT_PRICE, REGISTER_DATE, UPDATE_DATE
		FROM PRODUCT
		WHERE PRODUCT_ID = #{productId}
	</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.rest.mapper.OrderMapper">
	<insert id="insert">
		INSERT INTO "ORDER"(
			ORDER_ID,
			PRODUCT_ID,
			PRODUCT_COUNT
		) VALUES(
			SEQ_ORDER.nextVal,
			#{productId},
			#{productCount}
		)
	</insert>
	
	<!-- mybatis 동적 쿼리문을 사용해 특정 분기에 따라 유동적으로 처리하는 sql문을 작성 -->
	<select id="selectAll" resultType="orderVO">
		<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
		SELECT P.PRODUCT_NAME, P.PRODUCT_STOCK, P.PRODUCT_PRICE, P.REGISTER_DATE, P.UPDATE_DATE, 
			O.ORDER_ID, O.PRODUCT_COUNT, O.ORDER_DATE, (P.PRODUCT_PRICE * O.PRODUCT_COUNT) AS ORDER_PRICE
		FROM PRODUCT P JOIN "ORDER" O 
		ON P.PRODUCT_ID = O.PRODUCT_ID
		<choose>
			<when test="sort == 'recent'.toString()">
				ORDER BY O.ORDER_ID DESC
			</when>
			<otherwise>
				ORDER BY ORDER_PRICE DESC
			</otherwise>
		</choose>
	</select>

</mapper>

4. DAO와 Service

  1. 이제 src/main/java의 하위 dao패키지를 만들어 Mapper 인터페이스를 사용할 ProductDAO와 OrderDAO를 만든다.
    • DAO 클래스에는 @Repository Annotation을 추가한다.
    • DAO는 Mapper 인터페이스를 생성자 주입하고, Mapper 인터페이스의 메소드를 호출한다.
package com.example.rest.dao;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.example.rest.dto.OrderDTO;
import com.example.rest.dto.ProductDTO;
import com.example.rest.mapper.ProductMapper;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class ProductDAO {

	private final ProductMapper productMapper;
	
	// 상품 추가
	public void save(ProductDTO productDTO) {
		productMapper.insert(productDTO);
	}
	
	// 상품 조회
	public List<ProductDTO> findAll() {
		return productMapper.selectAll();
	}
	
	// 상품 재고 수정
	public void setProductStock(OrderDTO orderDTO) {
		productMapper.updateStock(orderDTO);
	}
	
	// 상품 1개 조회
	public ProductDTO find(int productId) {
		return productMapper.select(productId);
	}
}
package com.example.rest.dao;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.example.rest.dto.OrderDTO;
import com.example.rest.mapper.OrderMapper;
import com.example.rest.vo.OrderVO;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class OrderDAO {

	private final OrderMapper orderMapper;
	
	// 주문 추가
	public void save(OrderDTO orderDTO) {
		orderMapper.insert(orderDTO);
	}
	
	// 주문 내역
	public List<OrderVO> findAll(String sort) {
		return orderMapper.selectAll(sort);
	}
}
  1. 서비스 기능을 처리할 Service 인터페이스와 그 구현 클래스들을 만든다.
    • Service 구현 클래스에는 @Service Annotation을 추가한다.
    • Service 구현 클래스에선 DAO를 생성자 주입하고, DAO의 메소드를 호출하여 서비스 기능을 수행한다.
package com.example.rest.service;

import java.util.List;

import com.example.rest.dto.ProductDTO;

public interface ProductService {
	
	// 상품 추가
	public void register(ProductDTO productDTO);
	
	// 상품 조회
	public List<ProductDTO> getList();
	
	// 상품 1개 조회
	public ProductDTO getProduct(int productId);
}
package com.example.rest.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.example.rest.dao.ProductDAO;
import com.example.rest.dto.ProductDTO;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService{

	final ProductDAO productDAO;
	
	@Override
	public void register(ProductDTO productDTO) {
		productDAO.save(productDTO);
	}
	
	@Override
	public List<ProductDTO> getList() {
		return productDAO.findAll();
	}
	
	@Override
	public ProductDTO getProduct(int productId) {
		return productDAO.find(productId);
	}
}
package com.example.rest.service;

import java.util.List;

import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;

public interface OrderService {
	// 인터페이스로 만드는 이유
	// FoodOrder, ToolOrder, ClotheOrder 등 여러 상품에 대한 Order를 구현하기 위함
	
	// 주문 추가
	public void order(OrderDTO orderDTO);
	
	// 주문 조회
	public List<OrderVO> getList(String sort);
}
package com.example.rest.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.example.rest.dao.OrderDAO;
import com.example.rest.dao.ProductDAO;
import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

	private final OrderDAO orderDAO;
	private final ProductDAO productDAO;
	
	// 주문하기
	@Override
	public void order(OrderDTO orderDTO) {
		productDAO.setProductStock(orderDTO);
		orderDAO.save(orderDTO);
	}

	@Override
	public List<OrderVO> getList(String sort) {
		return orderDAO.findAll(sort);
	}

}

5. Controller

  1. src/main/java의 하위 패키지로 controller 패키지를 만들고, ProductController와 OrderController 클래스를 만든다.
    • Controller 클래스는 @Controller Annotation과 @RequestMapping("/상위경로") Annotation(선택사항)을 추가한다.
    • @RequestBody : 메소드의 파라미터가 request의 body에 의존함을 표시하는 Annotation
      • HTML에서 Ajax를 통해 data를 JSON 형식으로 전달하며, 해당 데이터를 DTO 클래스로 받아 DB로 전달한다.
    • @PathVariable : 경로 변수를 메소드의 파라미터로 바인딩 할 때 사용하며, /abc/id와 같은 형태로 전달하기 위해 사용한다.
package com.example.rest.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.rest.dto.ProductDTO;
import com.example.rest.service.ProductService;

import lombok.RequiredArgsConstructor;

@Controller
@RequestMapping("/product/*")
@RequiredArgsConstructor
public class ProductController {

	private final ProductService productService;
	
	@GetMapping(value={"/", "list"})
	public String list(Model model) {
		model.addAttribute("productForm", new ProductDTO());
		model.addAttribute("list", productService.getList());
		return "product/product";
	}
	
	@PostMapping("new")
	@ResponseBody
	public void register(@RequestBody ProductDTO productDTO) { // request로부터 온 데이터를 ProductDTO에 저장
		productService.register(productDTO);
	}
	
	@GetMapping("/{productId}")
	@ResponseBody
	public ProductDTO getProduct(@PathVariable("productId") int productId) {
		return productService.getProduct(productId); // HTML에 조회한 product를 보낸다
	}
}
package com.example.rest.controller;

import java.util.List;

import org.springframework.ui.Model;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.rest.dto.OrderDTO;
import com.example.rest.service.OrderService;
import com.example.rest.vo.OrderVO;

import lombok.RequiredArgsConstructor;

@RestController // @Controller + @ResponseBody
				// RESTful web API를 더 쉽게 만들기 위해 Spring framework 4.0에 도입됨
@RequiredArgsConstructor
@RequestMapping("/order/*")
public class OrderController {

	private final OrderService orderService;
	
	@GetMapping("list/{sort}") // /list/sort와 같은 경로 변수
	public List<OrderVO> list(Model model, @PathVariable("sort") String sort) {
		return orderService.getList(sort); // HTML에 주문 내역 list를 보낸다.
	}
	
	@PostMapping("write")
	public void register(@RequestBody OrderDTO orderDTO) {
		orderService.order(orderDTO);
	}
}

6. HTML, CSS, JS

  1. 이제 src/main/resources/templates 패키지 하위에 product 폴더를 만들고 HTML을 만든다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>상품 목록</title>
	<!-- css 경로 설정 https://dkfkslsksh.tistory.com/41 -->
	<link rel="stylesheet" type="text/css" th:href="@{/product.css}">
</head>
<body>
	<div class="section">
		<div class="container">
			<button type="button" class="register-ready">상품추가</button>
			<div class="register-wrap" th:object="${productForm}">
				<div>
					<label for="*{productName}">상품 이름</label>
					<input type="text" th:field="*{productName}" placeholder="상품이름">
				</div>
				<div>
					<label for="*{productStock}">재고량</label>
					<input type="text" th:field="*{productStock}" placeholder="재고량">
				</div>
				<div>
					<label for="*{productPrice}">상품 가격</label>
					<input type="text" th:field="*{productPrice}" placeholder="상품가격">
				</div>
				<button type="button" class="register-done">등록</button>
			</div>
		</div>

		<div class="container">
			<table>
				<tr>
					<th>단일 선택</th>
					<th>주문 개수</th>
					<th>번호</th>
					<th>이름</th>
					<th>재고</th>
					<th>가격</th>
					<th>등록 날짜</th>
					<th>수정 날짜</th>
				</tr>
				<th:block th:each="product : ${list}">
				<tr th:object="${product}">
					<td><input type="radio" name="productId" th:value="*{productId}"></td>
					<td><input type="text" class="productCount" readOnly></td>
					<td th:text="*{productId}"></td>
					<td th:text="*{productName}"></td>
					<td th:text="*{productStock}"></td>
					<td th:text="*{productPrice}"></td>
					<td th:text="*{registerDate}"></td>
					<td th:text="*{updateDate}"></td>
				</tr>
				</th:block>
			</table>
			<button type="button" id="order-done">주문완료</button><button type="button" id="order-list">주문내역</button>
		</div>
		
		<div id="container">
			<div class="sort">
				<span class="on" id="recent" data-sort="recent">최신순</span>
				<span class="" id="price" data-sort="price">결제 금액순</span>
			</div>

			<!-- 주문 목록이 작성될 div -->
			<div class="order-list"></div>	
		</div>
		
	</div>

	<!-- JQuery 사용 -->
	<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
	<!-- sr/main/resources/static에 있는 product.js 사용 -->
	<script th:src="@{/product.js}"></script>
</body>
</html>
  1. src/main/resources/static에 HTML에서 JQuery와 Ajax 등 Javascript를 처리할 js파일을 따로 만든다.
const $radios = $("input[type='radio']");
const $inputs = $("input[class='productCount']");
const $done = $("#order-done");
const $registerReady = $("button.register-ready");
const $registerDone = $("button.register-done");
const $orderList = $("button#order-list");
const $spans = $("div.sort span");
let $temp, i, sort; 

const $ids = $("input[name='productId']");

// 상품 추가 버튼을 눌렀을 때
$registerReady.on("click", function() {
	$(this).hide();
	$("div.register-wrap").show();
});
		
// 상품 추가 완료 버튼을 눌렀을 때
$registerDone.on("click", function() {		
	// ajax
	$.ajax({
		url : "new",
		type : "post", // 요청 타입
		data : JSON.stringify({ // 보낼 데이터를 JSON 형태로 변환
					productName:$("#productName").val(),
					productStock:$("#productStock").val(),
					productPrice:$("#productPrice").val()
				}),
		contentType: "application/json; charset=utf-8", // 요청 contentType 설정
		success: function() { // 성공적으로 처리되면 실행할 callback
			location.reload(); // 현재 페이지 새로고침
		}
	})
});
		
$radios.on("click", function() {
	i = $radios.index(this); // 변수 i에 선택한 라디오 버튼의 index값 저장
	/* console.log(i) */
	if($temp) {
		$temp.prop("readOnly", true);
		$temp.value = "";
	}
			
	// input 태그가 i번인 태그를 선택하고, readOnly를 false로 설정
	$inputs.eq(i).prop("readOnly", false); 
	// $temp에 선택된 input 태그를 저장
	$temp = $inputs.eq(i);
});

// 주문 완료 버튼을 눌렀을 때
$done.on("click", function() {
	// ajax
	$.ajax({
		url : "/order/write",
		type : "post",
		data : JSON.stringify({ productId : $ids.eq(i).val(), productCount : $inputs.eq(i).val() }),
		contentType : "application/json; charset=utf-8",
		success: function() { 
			$.ajax({ // 재고량 부분을 새로 데이터를 받아 변경하도록 설정
				url : "/product/" + $ids.eq(i).val(),
				success : function(products) { // Controller에서 조회한 결과를 ResponseBody로 보내준 것을 파라미터로 사용
					// tr 중에서 선택한 라디오 버튼의 idx에 해당하는 칸의 하위 요소 중, 재고량 칸의 내용을 수정
					$($("tr").eq(i+1).children()).eq(4).text(products.productStock);
				}
			});
			
			$orderList.click();
		}
	});
});

// 정렬 옵션을 눌렀을 때
$spans.on("click", function() {
	$spans.attr("class", ""); // 전체 span 태그의 class 값을 비움
	$(this).attr("class", "on"); // 누른 span 태그의 class 값을 on으로 변경
	$orderList.click(); // 주문내역 버튼을 누른 것으로 설정 -> ajax 요청을 실행
});

// 주문 내역 버튼을 눌렀을 때
$orderList.on("click", function() {
	$("#container").show(); // 주문내역 container가 보이게 설정
	
	$spans.each((i, span)=>{ // 각각의 태그에 대해 함수 실행
		if($(span).attr("class")) { // 각 span 태그의 class 속성이 존재하면
			sort=$(span).data("sort"); // span의 data-sort의 값을 가져와 sort에 저장
		}
	});

	$("span").attr("class", ""); // span 태그의 class 속성을 비우고
	$("span#"+sort).attr("class", "on"); // id가 sort인 span 태그의 class를 on으로 설정
	
	// ajax
	$.ajax({
		url : "/order/list/"+sort,
		success: function(orders) { // controller에서 받은 주문 내역 list를 파라미터로 사용
			// ajax 결과로 받은 list를 사용해 HTML에 표시할 태그를 작성
			let text =
			`<table>
				<tr>
					<th>상품 이름</th>
					<th>상품 가격</th>
					<th>주문 개수</th>
					<th>결제 금액</th>
					<th>주문 날짜</th>
				</tr>`;
			
			orders.forEach(order=>{ // list의 각 OrderDTO에 대해 수행
				text += 
				`<tr>
					<td>${order.productName}</td>
					<td>${order.productPrice}</td>
					<td>${order.productCount}</td>
					<td>${order.orderPrice}</td>
					<td>${order.orderDate}</td>
				</tr>`;
			});
			
			text += '</table>';
			$("div.order-list").html(text);
		}
		
	});
});
  1. css도 간단하게 설정한다.
@charset "UTF-8";
*{margin:0; padding:0;}
ul, ol, li{list-style:none;}
a{text-decoration:none;}

#container{margin:0 auto; width:1000px;}

div{margin:0 auto; width:1000px;}
table{width:100%; border:1px solid black; border-collapse:collapse; text-align:center;}
tr, td{border: 1px solid black;}
button{width:50%;}

button.register-ready, button.register-done{width:100%;}
div.register-wrap{width:500px; display:none;}
div.register-wrap div, div.register-wrap input{width:100%;}

span{cursor:pointer;}
span.on{font-weight:bold;}

div.sort{text-align:right;}

spring boot rest 1.png

spring boot rest 2.png
spring boot rest 3.png

spring boot rest 4.png
spring boot rest 5.png