์ํ ํด๋ ์ค๊ณ
์๊ตฌ์ฌํญ ๋ถ์
ํด๋ ๋ณ๋ก ๊ด์ฌ์ํ์ ์ ์ฅ/๊ด๋ฆฌํ ์ ์๋ ๊ธฐ๋ฅ ์ถ๊ฐ
- ํด๋ ์์ฑ
- ํ์๋ณ ํด๋ ์ถ๊ฐ
- ํด๋๋ฅผ ์ถ๊ฐํ ๋ 1๊ฐ ~ N๊ฐ ํ๋ฒ์ ์ถ๊ฐ ๊ฐ๋ฅ - ๊ด์ฌ์ํ์ ํด๋ ์ค์
- ๊ด์ฌ์ํ์ ํด๋๋ N๊ฐ ์ค์ ๊ฐ๋ฅ
- ๊ด์ฌ์ํ์ด ๋ฑ๋ก๋๋ ์์ ์๋ ์ด๋ ํด๋์๋ ์ ์ฅ๋์ง ์์
- ๊ด์ฌ์ํ ๋ณ๋ก 1๋ฒ์์ ์์ฑํ ํด๋๋ฅผ ์ ํํด ์ถ๊ฐ ๊ฐ๋ฅ - ํด๋ ๋ณ ์กฐํ
- ํ์์ ํด๋ ๋ณ๋ก ๊ด์ฌ์ํ ์กฐํ ๊ฐ๋ฅ
- ์กฐํ ๋ฐฉ๋ฒ
- '์ ์ฒด' ํด๋ฆญ ์: ํด๋์ ์๊ด์์ด ํ์์ด ์ ์ฅํ ์ ์ฒด ๊ด์ฌ์ํ๋ค ์กฐํ
- 'ํด๋๋ช ' ํด๋ฆญ ์: ํด๋๋ณ ์ ์ฅ๋ ๊ด์ฌ์ํ๋ค์ ์กฐํ
ํด๋์ ํ์ ๊ด๊ณ
- ํด๋๋ช (name): ํ์์ด ๋ฑ๋กํ ํด๋ ์ด๋ฆ ์ ์ฅ
- ํด๋ : ํ์ = N : 1 ๊ด๊ณ
- ํ์ ๊ฐ์ฒด์์ ํด๋ ๊ฐ์ฒด๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ ํด๋์ ํ์์ N : 1 ๋จ๋ฐฉํฅ ์ฐ๊ด๊ด๊ณ๋ก ์ค์
ํด๋ Entity์ Repository ์์ฑ
package com.sparta.myselectshop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "folder")
public class Folder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
public Folder(String name, User user) {
this.name = name;
this.user = user;
}
}
์ํ๊ณผ ํด๋ ๊ด๊ณ
1. ์ํ์ ์ถ๊ฐ๋ ํด๋์ ์ ๋ณด๊ฐ ํ์ํจ

- ๊ด์ฌ ์ํ์ ํด๋ ๋ฑ๋ก
- ์ํ์ ์ฌ๋ฌ ๊ฐ์ ํด๋๋ฅผ ๋ฑ๋ก ๊ฐ๋ฅ
- ํด๋ : ์ํ = N : 1
2. ํด๋์ ๊ด์ฌ ์ํ์ ๋ฑ๋ก

- ํ๋์ ํด๋๋ ์ฌ๋ฌ ๊ฐ์ ์ํ์ ํฌํจํ ์ ์์
- ํด๋ : ์ํ = 1 : N
3. ๊ฒฐ๊ณผ์ ์ผ๋ก ์ํ๊ณผ ํด๋๋ N : M ๊ด๊ณ

- ์ํ_ํด๋ ์ค๊ฐ ํ ์ด๋ธ ์์ฑ
- ์ํ : ์ํ_ํด๋ = 1 : N
- ํด๋ : ์ํ_ํด๋ = 1 : N
4. ์ฐ๊ด๊ด๊ณ์ ๋ฐฉํฅ ์ ํ
- ๊ด์ฌ ์ํ์ ์กฐํํ ๋ ํฌํจ๋ ํด๋๋ค์ ์ ๋ณด ํ์(์ํ ๊ฐ์ฒด๊ฐ ํด๋ ๊ฐ์ฒด ์กฐํ ํ์)
- ์ํ ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค์ผ๋ก ํด๋น ํด๋์ ํฌํจ๋ ์ํ๋ค์ ์กฐํ(์ํ ๊ฐ์ฒด๊ฐ ํด๋ ๊ฐ์ฒด ์กฐํ ํ์)
- ๋ฐ๋ผ์ ์ํ๊ณผ ์ํ_ํด๋๋ ์๋ฐฉํฅ ์ฐ๊ด๊ด๊ณ๋ก ์ค์
- ํด๋ ๊ฐ์ฒด์์ ์ํ ๊ฐ์ฒด๋ฅผ ์กฐํํ์ง ์์ ์์ ์ด๊ธฐ ๋๋ฌธ์ ํด๋๋ ์ํ_ํด๋์ ๊ด๊ณ ๋งบ์ง ์์!
์ํ_ํด๋ Entity ๋ฐ Repository ์์ฑ
package com.sparta.myselectshop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "product_folder")
public class ProductFolder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "folder_id", nullable = false)
private Folder folder;
public ProductFolder(Product product, Folder folder) {
this.product = product;
this.folder = folder;
}
}
Product ์ํฐํฐ ๋ด ์ํ_ํด๋ ์กฐํ ๊ฐ๋ฅํ๋๋ก ์์
public class Product extends Timestamped {
...
@OneToMany(mappedBy = "product")
private List<ProductFolder> productFolderList = new ArrayList<>();
...
}
ํด๋ ์์ฑ ๋ฐ ์กฐํ ๊ตฌํ
- ํ์๋ณ ํด๋ ์ถ๊ฐ(ํด๋๋ฅผ ์ถ๊ฐํ ๋ ํ ๋ฒ์ ์ฌ๋ฌ ๊ฐ ์ถ๊ฐ ๊ฐ๋ฅ)
- ํ์๋ณ ์ ์ฅํ ํด๋๋ค ์กฐํ
API ์ค๊ณ
| ์ค๋ช | URL | Request | Response |
| ํ์์ ํด๋ ์์ฑ | POST /api/folders | { folderName: [String, ...] } |
|
| ํ์์ ํด๋ ์กฐํ | GET /api/user-folder | index.html model ์ถ๊ฐ → folders |
ํด๋ ์์ฑ API ๊ตฌํ
- controller > FolderController ๋ด ํด๋ ์์ฑ API ์ถ๊ฐ
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.FolderRequestDto;
import com.sparta.myselectshop.security.UserDetailsImpl;
import com.sparta.myselectshop.service.FolderService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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 java.util.List;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class FolderController {
private final FolderService folderService;
@PostMapping("/folders")
public void addFolders(@RequestBody FolderRequestDto folderRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
List<String> folderNames = folderRequestDto.getFolderNames();
folderService.addFolders(folderNames, userDetails.getUser());
}
}
package com.sparta.myselectshop.dto;
import lombok.Getter;
import java.util.List;
@Getter
public class FolderRequestDto {
List<String> folderNames;
}
- service > FolderService ๋ด ํด๋ ์์ฑ ๋น์ฆ๋์ค ๋ก์ง ์์ฑ
package com.sparta.myselectshop.service;
import com.sparta.myselectshop.entity.Folder;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.repository.FolderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class FolderService {
private final FolderRepository folderRepository;
public void addFolders(List<String> folderNames, User user) {
// ํด๋๊ฐ ์ค๋ณต์ธ์ง ํ์ธ
// ์ ์ ๊ธฐ์ค์ผ๋ก ์ฐพ๊ณ , ํด๋ ์ด๋ฆ์ด ์ฌ๋ฌ๊ฐ(In)
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
List<Folder> folderList = new ArrayList<>();
for(String folderName : folderNames) {
if(!isExistFolderName(folderName, existFolderList)) {
// ์ค๋ณต๋์ง ์์ ๋ ํด๋ ์ ์ฅ
Folder folder = new Folder(folderName, user);
folderList.add(folder);
} else {
throw new IllegalArgumentException("ํด๋๋ช
์ด ์ค๋ณต๋์์ต๋๋ค.");
}
}
folderRepository.saveAll(folderList);
}
// ํด๋น ์ด๋ฆ์ ๊ฐ์ง ํด๋๊ฐ ์กด์ฌํ๋์ง ํ์ธํ๋ ๋ฉ์๋
private boolean isExistFolderName(String folderName, List<Folder> existFolderList) {
for (Folder existFolder : existFolderList) {
if(folderName.equals(existFolder.getName())) {
return true;
}
}
return false;
}
}
package com.sparta.myselectshop.repository;
import com.sparta.myselectshop.entity.Folder;
import com.sparta.myselectshop.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUserAndNameIn(User user, List<String> folderNames);
// select * from folder where user_id = ? and name in (?, ?, ...);
}
ํด๋ ์กฐํ API ๊ตฌํ
- UserController ๋ด ํด๋ ์กฐํ API ์ถ๊ฐ
public class UserController {
private final UserService userService;
private final FolderService folderService;
...
@GetMapping("/user-folder")
public String getUserInfo(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
model.addAttribute("folders", folderService.getFolders(userDetails.getUser()));
return "index :: #fragment";
}
}
- FolderService ๋ด ํด๋ ์กฐํํ๋ ๋น์ฆ๋์ค ๋ก์ง ์ถ๊ฐ
package com.sparta.myselectshop.dto;
import com.sparta.myselectshop.entity.Folder;
import lombok.Getter;
@Getter
public class FolderResponseDto {
private Long id;
private String name;
public FolderResponseDto(Folder folder) {
this.id = folder.getId();
this.name = folder.getName();
}
}
public class FolderService {
private final FolderRepository folderRepository;
public List<FolderResponseDto> getFolders(User user) {
List<Folder> folderList = folderRepository.findAllByUser(user);
List<FolderResponseDto> responseDtoList = new ArrayList<>();
for(Folder folder : folderList) {
responseDtoList.add(new FolderResponseDto(folder));
}
return responseDtoList;
}
}
- Product ์กฐํ ์, ํด๋ ์ ๋ณด๋ ๊ฐ์ด ํฌํจ์ํค๊ธฐ
package com.sparta.myselectshop.dto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.ProductFolder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Getter
@NoArgsConstructor
public class ProductResponseDto {
private Long id;
private String title;
private String link;
private String image;
private int lprice;
private int myprice;
private List<FolderResponseDto> productFolderList = new ArrayList<>();
public ProductResponseDto(Product product) {
this.id = product.getId();
this.title = product.getTitle();
this.link = product.getLink();
this.image = product.getImage();
this.lprice = product.getLprice();
this.myprice = product.getMyprice();
for (ProductFolder productFolder : product.getProductFolderList()) {
productFolderList.add(new FolderResponseDto(productFolder.getFolder()));
}
}
}
public class ProductService {
private final ProductRepository productRepository;
public static final int MIN_MY_PRICE = 100;
@Transactional(readOnly = true) // ํธ๋์ญ์
์ถ๊ฐ: ์ง์ฐ๋ก๋ฉ, ์ฑ๋ฅ ํฅ์์ ์ํด readOnly ์ค์
public Page<ProductResponseDto> getProducts(User user, int page, int size, String sortBy, boolean isAsc) {
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy); // Sort ๊ฐ์ฒด = (direction ๋ฐฉํฅ, ์ ๋ ฌ ํญ๋ชฉ)
Pageable pageable = PageRequest.of(page, size, sort);
// ํ์ฌ ๋ค์ด์จ User์ ๊ถํ์ด Admin์ธ์ง User์ธ์ง ํ์ธ
UserRoleEnum userRoleEnum = user.getRole();
Page<Product> productList;
if(userRoleEnum == UserRoleEnum.USER) {
productList = productRepository.findAllByUser(user, pageable);
} else {
productList = productRepository.findAll(pageable);
}
return productList.map(ProductResponseDto::new);
}
}


๊ด์ฌ ์ํ์ ํด๋ ์ถ๊ฐ ๊ตฌํ
- ๊ด์ฌ์ํ์ ํด๋๋ 0๊ฐ ~ N๊ฐ ์ค์ ๊ฐ๋ฅ

- ๊ด์ฌ์ํ์ด ๋ฑ๋ก๋๋ ์์ ์๋ ์ด๋ ํด๋์๋ ์ ์ฅ๋์ง ์์
- ๊ด์ฌ์ํ ๋ณ๋ก ๊ธฐ ์์ฑํ๋ ํด๋๋ฅผ ์ ํํด ์ถ๊ฐ ๊ฐ๋ฅ

๊ด์ฌ ์ํ์ ํด๋ ์ถ๊ฐ API ์ค๊ณ
| ์ค๋ช | URL | Request | Response |
| ํด๋ ์ ์ฒด ์กฐํ | GET /api/folders | List<FolderResponseDto> | |
| ํด๋ ์ถ๊ฐ | POST /api/products/{productId}/folder | {productId}: ๊ด์ฌ์ํ ID [Form ํํ] folderId: ์ถ๊ฐํ ํด๋ ID |
๊ด์ฌ ์ํ์ ํด๋ ์ถ๊ฐ API ๊ตฌํ
ํด๋ ์ ์ฒด ์กฐํ API
- FolderController ๋ด ์ ์ฒด ์กฐํ API ์ถ๊ฐ
public class FolderController {
private final FolderService folderService;
...
@GetMapping("/folders")
public List<FolderResponseDto> getFolders(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return folderService.getFolders(userDetails.getUser());
}
}
๊ด์ฌ ์ํ์ ํด๋ ์ถ๊ฐ API
- ProductController ๋ด API ์ถ๊ฐ
public class ProductController {
private final ProductService productService;
...
@PostMapping("products/{productId}/folder")
public void addFolder(
@PathVariable Long productId,
@RequestParam Long folderId,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
productService.addFolder(productId, folderId, userDetails.getUser());
}
}
- ProductService ๋ด ๋น์ฆ๋์ค ๋ก์ง ์ถ๊ฐ
public class ProductService {
private final ProductRepository productRepository;
private final FolderRepository folderRepository;
private final ProductFolderRepository productFolderRepository;
public void addFolder(Long productId, Long folderId, User user) {
Product product = productRepository.findById(productId).orElseThrow(
() -> new NullPointerException("ํด๋น ์ํ์ด ์กด์ฌํ์ง ์์ต๋๋ค."));
Folder folder = folderRepository.findById(folderId).orElseThrow(
() -> new NullPointerException("ํด๋น ํด๋๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค."));
// ํด๋น ์ํ ํน์ ํด๋ ๋ ์ค ํ๋๋ผ๋ ํด๋น ์ ์ ๊ฐ ๋ฑ๋กํ๊ฒ ์๋ ๊ฒฝ์ฐ ์์ธ ์ฒ๋ฆฌ
if(!product.getUser().getId().equals(user.getId())
|| !folder.getUser().getId().equals(user.getId())) {
throw new IllegalArgumentException("ํ์๋์ ๊ด์ฌ์ํ์ด ์๋๊ฑฐ๋, ํ์๋์ ํด๋๊ฐ ์๋๋๋ค.");
}
// ์ํ-ํด๋ ์ค๋ณต ํ์ธ
Optional<ProductFolder> overlapFolder = productFolderRepository.findByProductAndFolder(product, folder);
if(overlapFolder.isPresent()) {
throw new IllegalArgumentException("์ค๋ณต๋ ํด๋์
๋๋ค.");
}
productFolderRepository.save(new ProductFolder(product, folder));
}
}


ํด๋ ๋ณ ๊ด์ฌ์ํ ์กฐํ ๊ตฌํ
- ํ์์ ํด๋ ๋ณ๋ก ๊ด์ฌ์ํ ์กฐํ ๊ฐ๋ฅ
ํด๋ ๋ณ ๊ด์ฌ์ํ ์กฐํ API ์ค๊ณ
| ์ค๋ช | URL | Request | Response |
| ํด๋๋ณ ์ํ ์กฐํ | GET /api/folders/{folderId}/products | {folderId}: ์กฐํ๋ฅผ ์ํ๋ ํด๋ id | Page<ProductResponseDto> |
ํด๋ ๋ณ ๊ด์ฌ์ํ ์กฐํ API ๊ตฌํ
- ProductController ๋ด ๊ด์ฌ์ํ ์กฐํ API ์ถ๊ฐ
public class ProductController {
private final ProductService productService;
@GetMapping("/folders/{folderId}/products")
public Page<ProductResponseDto> getProductsInFolder(
@PathVariable Long folderId,
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
return productService.getProductsInFolder(
folderId,
page-1,
size,
sortBy,
isAsc,
userDetails.getUser()
);
}
}
- ProductService์ ๋น์ฆ๋์ค ๋ก์ง ์ถ๊ฐ
public class ProductService {
private final ProductRepository productRepository;
private final FolderRepository folderRepository;
private final ProductFolderRepository productFolderRepository;
public Page<ProductResponseDto> getProductsInFolder(Long folderId, int page, int size, String sortBy, boolean isAsc, User user) {
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy); // Sort ๊ฐ์ฒด = (direction ๋ฐฉํฅ, ์ ๋ ฌ ํญ๋ชฉ)
Pageable pageable = PageRequest.of(page, size, sort);
// ํด๋น ํด๋์ ๋ฑ๋ก๋์ด์๋ ์ํ ๊ฐ์ ธ์ค๊ธฐ
Page<Product> productList = productRepository.findAllByUserAndProductFolderList_FolderId(user, folderId, pageable);
Page<ProductResponseDto> responseDtoList = productList.map(ProductResponseDto::new);
return responseDtoList;
}
}
package com.sparta.myselectshop.repository;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findAllByUser(User user, Pageable pageable);
Page<Product> findAllByUserAndProductFolderList_FolderId(User user, Long folderId, Pageable pageable);
/*
* select *
* from product p
* left join product_folder pf
* on p.id = pf.product_id
* where p.user_id = ?
* and pf.folder_id = ?
* order by p.id asc|desc
* limit 0, 10; (ํ์ด์ง ๋ฒํธ, ํ ํ์ด์ง ๋น ๋ณด์ฌ์ค ๊ฐ์)
* */
}
