
Spring Data JPA로 JPA를 더 간단하게.
Repository인터페이스와 쿼리 메소드만으로 CRUD, 정렬, 페이징, 동적 조회까지 한 번에!
Repository + 쿼리 메소드로 JPQL/SQL을 대체.Repository/쿼리 메소드 제공 (CRUD·정렬·페이징).EntityManager* 직접 사용 X (대부분 대체).| 인터페이스 | 역할 |
|---|---|
Repository | 마커 인터페이스 |
CrudRepository | CRUD 기본 메소드 |
PagingAndSortingRepository | 정렬/페이징 |
JpaRepository | JPA 확장(배치 삭제, flush 등) |
| 메소드 | 설명 |
|---|---|
long count() | 전체 개수 |
Optional<T> findById(ID) | 단건 조회 |
Iterable<T> findAll() | 전체 조회 |
S save(S) / saveAll | 저장·수정 |
void deleteById(ID) | 삭제 |
find + [Entity] + By + 조건findByCode(...) == findMenuByCode(...) (제네릭이 Menu일 때)| 키워드 | 예시 | 생성 SQL |
|---|---|---|
And/Or | findByCodeAndName | ... where x.code=? and x.name=? |
| 비교 | findByPriceBetween | ... where x.price between ? and ? |
| 크기 | LessThan/GreaterThanEqual | <, >= |
| Null | findByNameIsNull | is null |
| Like | findByNameLike/StartingWith | like / like '값%' |
| In | findByNameIn(Collection) | in (?) |
| 정렬 | OrderBy...Desc | order by ... desc |
Spring Boot DevTools, Spring Data JPA, Thymeleaf, Spring Web, MySQL Driver, Lombokapplication.ymlspring:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/menudb
username: swcamp
password: swcamp
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
@Controller
public class MainController {
@GetMapping({"/", "/main"})
public String main() { return "main/main"; }
}
<!-- templates/main/main.html -->
<h1>OHGIRAFFERS RESTAURANT에 오신 것을 환영합니다.</h1>
<button onclick="location.href='/menu/7'">7번 메뉴 목록 보기</button>
@Entity @Table(name="tbl_menu") @Getter @NoArgsConstructor(access=PROTECTED)
public class Menu {
@Id @GeneratedValue(strategy=IDENTITY) private int menuCode;
private String menuName; private int menuPrice; private int categoryCode;
private String orderableStatus;
// 엔티티 변경 메서드(예: 이름 변경)만 노출, Setter 지양
public void modifyMenuName(String name){ this.menuName = name; }
}
public interface MenuRepository extends JpaRepository<Menu, Integer> {}
implementation 'org.modelmapper:modelmapper:3.1.1'
@Configuration
public class BeanConfig {
@Bean public ModelMapper modelMapper(){ return new ModelMapper(); }
}
public class MenuDTO {
private int menuCode; private String menuName; private int menuPrice;
private int categoryCode; private String orderableStatus;
// getters/setters/toString
}
@Service
public class MenuService {
private final MenuRepository repo; private final ModelMapper mm;
public MenuService(MenuRepository repo, ModelMapper mm){ this.repo=repo; this.mm=mm; }
public MenuDTO findMenuByCode(int code){
Menu menu = repo.findById(code).orElseThrow(IllegalArgumentException::new);
return mm.map(menu, MenuDTO.class);
}
}
@Controller @RequestMapping("/menu")
public class MenuController {
private final MenuService service;
public MenuController(MenuService s){ this.service=s; }
@GetMapping("/{menuCode}")
public String find(@PathVariable int menuCode, Model model){
model.addAttribute("menu", service.findMenuByCode(menuCode));
return "menu/detail";
}
}
<!-- templates/menu/detail.html -->
<table border="1">
<tr><th>번호</th><th>이름</th><th>가격</th><th>카테고리</th><th>상태</th></tr>
<tr>
<td th:text="${menu.menuCode}"></td>
<td th:text="${menu.menuName}"></td>
<td th:text="${menu.menuPrice}"></td>
<td th:text="${menu.categoryCode}"></td>
<td th:text="${menu.orderableStatus}"></td>
</tr>
</table>
public List<MenuDTO> findMenuList(){
return repo.findAll(Sort.by("menuCode").descending()).stream()
.map(m -> mm.map(m, MenuDTO.class))
.toList();
}
@GetMapping("/list")
public String list(Model model){
model.addAttribute("menuList", service.findMenuList());
return "menu/list";
}
th:each로 출력(생략)
Pageable/Page)public Page<MenuDTO> findMenuList(Pageable pageable){
pageable = PageRequest.of(
Math.max(0, pageable.getPageNumber()-1),
pageable.getPageSize(),
Sort.by("menuCode").descending());
Page<Menu> page = repo.findAll(pageable);
return page.map(m -> mm.map(m, MenuDTO.class));
}
@GetMapping("/list")
public String list(@PageableDefault Pageable pageable, Model model){
Page<MenuDTO> menuPage = service.findMenuList(pageable);
PagingButton paging = Pagination.getPagingButtonInfo(menuPage);
model.addAttribute("paging", paging);
model.addAttribute("menuList", menuPage);
return "menu/list";
}
public class PagingButton {
private int currentPage, startPage, endPage;
// ctor/getters
}
public class Pagination {
public static PagingButton getPagingButtonInfo(Page<?> page){
int current = page.getNumber()+1, block=10;
int start = (int)(Math.ceil((double)current/block)-1)*block + 1;
int end = Math.min(start+block-1, Math.max(1, page.getTotalPages()));
return new PagingButton(current, start, end);
}
}
<!-- 맨 앞으로 -->
<button th:onclick="'location.href=\'/menu/list?page=' + @{${paging.startPage}} + '\''"><<</button>
<!-- 이전 -->
<button th:onclick="'location.href=\'/menu/list?page=' + @{${paging.currentPage - 1}} + '\''"
th:disabled="${menuList.first}"><</button>
<!-- 숫자 -->
<th:block th:each="p : ${#numbers.sequence(paging.startPage, paging.endPage)}">
<button th:onclick="'location.href=\'/menu/list?page=' + @{${p}} + '\''"
th:text="${p}" th:disabled="${paging.currentPage eq p}"></button>
</th:block>
<!-- 다음 -->
<button th:onclick="'location.href=\'/menu/list?page=' + @{${paging.currentPage + 1}} + '\''"
th:disabled="${menuList.last}">></button>
<!-- 맨 끝 -->
<button th:onclick="'location.href=\'/menu/list?page=' + @{${paging.endPage}} + '\''">>></button>
List<Menu> findByMenuPriceGreaterThan(Integer price);
List<Menu> findByMenuPriceGreaterThanOrderByMenuPrice(Integer price);
List<Menu> findByMenuPriceGreaterThan(Integer price, Sort sort);
public List<MenuDTO> findByMenuPrice(Integer price){
return repo.findByMenuPriceGreaterThan(price, Sort.by("menuPrice").descending())
.stream().map(m -> mm.map(m, MenuDTO.class)).toList();
}
@GetMapping("/search")
public String search(@RequestParam Integer menuPrice, Model model){
model.addAttribute("menuList", service.findByMenuPrice(menuPrice));
model.addAttribute("menuPrice", menuPrice);
return "menu/searchResult";
}
(뷰는 목록 테이블 출력)
<button onclick="location.href='/menu/regist'">메뉴 입력하기</button>
regist.html에서 카테고리 목록을 fetch로 로드:
<select name="categoryCode" id="categoryCode"></select>
<script>
fetch('/menu/category')
.then(res => res.json())
.then(data => {
const sel = document.getElementById('categoryCode');
data.forEach(c => {
const opt = document.createElement('option');
opt.value = c.categoryCode; opt.textContent = c.categoryName;
sel.appendChild(opt);
});
});
</script>
@Entity @Table(name="tbl_category") @Getter @NoArgsConstructor(access=PROTECTED)
public class Category {
@Id private int categoryCode; private String categoryName; private Integer refCategoryCode;
}
public class CategoryDTO { int categoryCode; String categoryName; Integer refCategoryCode; /*get/set*/ }
public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Query(value = "SELECT category_code, category_name, ref_category_code FROM tbl_category ORDER BY category_code ASC",
nativeQuery = true)
List<Category> findAllCategory();
}
@Service
public class MenuService {
private final MenuRepository menuRepo; private final CategoryRepository catRepo; private final ModelMapper mm;
// ctor...
public List<CategoryDTO> findAllCategory(){
return catRepo.findAllCategory().stream().map(c -> mm.map(c, CategoryDTO.class)).toList();
}
@Transactional
public void registNewMenu(MenuDTO dto){
menuRepo.save(mm.map(dto, Menu.class));
}
}
@GetMapping("/category") @ResponseBody
public List<CategoryDTO> categories(){ return service.findAllCategory(); }
@PostMapping("/regist")
public String regist(MenuDTO newMenu){
service.registNewMenu(newMenu);
return "redirect:/menu/list";
}
@Transactional
public void modifyMenu(MenuDTO dto){
Menu found = repo.findById(dto.getMenuCode()).orElseThrow(IllegalArgumentException::new);
found.modifyMenuName(dto.getMenuName()); // 엔티티 메서드로 변경 감지
}
@PostMapping("/modify")
public String modify(MenuDTO dto){
service.modifyMenu(dto);
return "redirect:/menu/" + dto.getMenuCode();
}
@Transactional
public void deleteMenu(Integer menuCode){ repo.deleteById(menuCode); }
@PostMapping("/delete")
public String delete(@RequestParam Integer menuCode){
service.deleteMenu(menuCode);
return "redirect:/menu/list";
}
setXxx 지양).@ToString에서 연관관계 필드 제외(순환 참조 위험).Pageable/Page 적극 활용(뷰에 필요한 메타 함께 제공).Sort, 동적 조건은 쿼리 메소드/Specification/Querydsl 검토.@Query(nativeQuery = true) 또는 Named Native Query와 결과 매핑 사용.