쇼핑몰을 만드는 과정에서 예전에는 포괄적으로 상품을 만들고 판매를 한다면 이번에는 상품마닥 가지는 특성을 살리면서 등록 및 판매가 하고 싶어졌다.
근데 상품 수 마다 클래스를 만들 수는 없다는 생각이 들었고.. 전에 소셜로그인 할때 팩토리 패턴을 쓴다는 말이 언뜻 생각나서 팩토리패턴을 뭔지 알아보고 이용해보기로 했다.
팩토리 패턴으로 여러 상품을 하나의 api로도 가능하지 않을까?? 해서 시도 +_+
상품의 타입을 열거형으로 상수 처리하고, 팩토리패턴에 이용할 서브클래스를 별도로 만들어 클래스의 인스턴스를 서브클래스에서 결정하도록 했다.
ItemController:
@RestController
@RequestMapping("/items")
public class ItemController {
private final ItemService itemService;
public ItemController(ItemService itemService) {
this.itemService = itemService;
}
@PostMapping
public ResponseEntity<ItemResponse> registerItem(@RequestBody ItemRequestDto itemRequestDto) {
Item item = itemService.registerItem(itemRequestDto);
ItemResponse response = new ItemResponse(item.getId(), item.getName(), item.getPrice());
return ResponseEntity.ok(response);
}
}
ItemService:
@Service
@Transactional
public class ItemService {
private final ItemFactory itemFactory;
public ItemService(ItemFactory itemFactory) {
this.itemFactory = itemFactory;
}
public Item registerItem(ItemRequestDto itemRequestDto) {
Item item = itemFactory.createItem(itemRequestDto);
return itemRepository.save(item);
}
}
ItemFactory:
@Component
public class ItemFactory {
private final AlbumRepository albumRepository;
private final BookRepository bookRepository;
private final MovieRepository movieRepository;
public ItemFactory(AlbumRepository albumRepository, BookRepository bookRepository, MovieRepository movieRepository) {
this.albumRepository = albumRepository;
this.bookRepository = bookRepository;
this.movieRepository = movieRepository;
}
public Item createItem(ItemRequestDto itemRequestDto) {
ItemType itemType = itemRequestDto.getItemType();
switch (itemType) {
case ALBUM:
return createAlbum(itemRequestDto);
case BOOK:
return createBook(itemRequestDto);
case MOVIE:
return createMovie(itemRequestDto);
default:
throw new IllegalArgumentException("Invalid item type: " + itemType);
}
}
private Album createAlbum(ItemRequestDto itemRequestDto) {
Album album = new Album();
// Set album properties from itemRequestDto
return albumRepository.save(album);
}
private Book createBook(ItemRequestDto itemRequestDto) {
Book book = new Book();
// Set book properties from itemRequestDto
return bookRepository.save(book);
}
private Movie createMovie(ItemRequestDto itemRequestDto) {
Movie movie = new Movie();
// Set movie properties from itemRequestDto
return movieRepository.save(movie);
}
}
ItemRequestDto , UpdateRequest
public class ItemRequestDto {
private ItemType itemType;
private String name;
private int price;
private int stockQuantity;
// Add additional properties for each item type (e.g., artist, author, etc.)
// Getters and setters
}
단점 : switch 문을 이용하면 간결하긴 하지만 setter를 너무 많이 이용하는 점이 맘에 들지 않아서 setter 없이 할 수 있는 방법이 없을까 고민했다.
ItemController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sellers")
public class ItemController {
private final ItemService itemService;
/**
* 상품 등록
*/
@PostMapping("/items")
public ResponseEntity registerItem(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestBody ItemRequest request) {
ItemResponse item = itemService.resisterItem(userDetails.getUser().getNickname(), request);
return ResponseEntity.ok(item);
}
/**
* 판매자가 등록한 상품 전체 조회
*/
@GetMapping("")
public Page<ItemResponse> getRegisteredItems(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestParam(value = "page", required = false, defaultValue = "1") int page,
@RequestParam(value = "size", required = false, defaultValue = "5") int size,
@RequestParam(value = "direction", required = false, defaultValue = "DESC") Direction direction,
@RequestParam(value = "properties", required = false, defaultValue = "createdDate") String properties) {
return itemService.getRegisteredItems(page, size, direction, properties,userDetails.getUser().getNickname());
}
/**
* 상품 수정 : 해당 판매자와 관리자 삭제 가능
* @param userDetails
* @param request
* @param id
* @return
*/
@PutMapping("/{id}")
public ItemResponse updateItem(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestBody UpdateRequest request,
@PathVariable Long id){
return itemService.updateItem(userDetails.getUser().getNickname(),userDetails.getUser().getRole(),id,request);
}
ItemRequest
package study.wonyshop.item.dto;
@Getter
@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED)
public class ItemRequest {
private final ItemType type;
private final String name; //상품명
private final int price; // 가격
private final int stockQuantity; // 재고 수량
private final List<Category> categories = new ArrayList<>();
private final String image;
private final String itemDetail;
//앨범
private String artist;
private String etc;
//도서
private String author;
private String isbn;
//음반
private String director;
private String actor;
@Builder
public ItemRequest(ItemType type, String name, int price, int stockQuantity, String image,
String itemDetail, Long sellerId) {
this.type = type;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.image = image;
this.itemDetail = itemDetail;
}
ItemFactory
package study.wonyshop.item.entity;
@Component
public class ItemFactory {
/**
* 팩토리 패턴을 이용
* 타입에 따라 아이템 객체 생성
* @param sellerNickname
* @param request
* @return
*/
public Item createItem(String sellerNickname, ItemRequest request) {
ItemType type = request.getType();
if (type.equals(ItemType.ALBUM)) {
Album album = Album.builder()
.name(request.getName())
.price(request.getPrice())
.stockQuantity(request.getStockQuantity())
.itemDetail(request.getItemDetail())
.image(request.getImage())
.sellerNickname(sellerNickname)
.artist(request.getArtist())
.etc(request.getEtc())
.build();
return album;
} else if (type.equals(ItemType.BOOK)) {
Book book = Book.builder()
.name(request.getName())
.price(request.getPrice())
.stockQuantity(request.getStockQuantity())
.itemDetail(request.getItemDetail())
.image(request.getImage())
.sellerNickname(sellerNickname)
.author(request.getAuthor())
.isbn(request.getIsbn())
.build();
return book;
} else if (type.equals(ItemType.MOVIE)) {
Movie movie = Movie.builder()
.name(request.getName())
.price(request.getPrice())
.stockQuantity(request.getStockQuantity())
.itemDetail(request.getItemDetail())
.image(request.getImage())
.sellerNickname(sellerNickname)
.actor(request.getActor())
.director(request.getDirector())
.build();
return movie;
} else {
throw new IllegalStateException("유효하지 않은 상품 유형입니다: " + request.getType());
}
}
/**팩토리 패턴 이용
* 타입에 따라 아이템 정보 수정
* @param request
* @param item
*/
public void updateItem(UpdateRequest request,Item item){
ItemType type = request.getType();
if(type.equals(ItemType.ALBUM)){
Album album = (Album) item;
album.updateAlbum(request.getArtist(), request.getEtc());
} else if (type.equals(ItemType.BOOK)) {
Book book = (Book) item;
book.updateBook(request.getAuthor(),request.getIsbn());
} else{
Movie movie =(Movie) item;
movie.updateMovie(request.getDirector(),request.getActor());
}
}
}
Item abstract class
package study.wonyshop.item.entity;
import static javax.persistence.InheritanceType.SINGLE_TABLE;
@Entity
@Table(name = "items")
@Getter
@NoArgsConstructor//(access = AccessLevel.PROTECTED)
@Inheritance(strategy = SINGLE_TABLE)// 상속 전략을 싱글테이블로 지정
@DiscriminatorColumn(name = "dtype") // 싱글테이블에서 상품을 구분하기위한 키 타입 컬럼 추가
public abstract class Item extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
private String name; //상품명
private int price; // 가격
private int stockQuantity; // 재고 수량
private String itemDetail; // 상품정보
private String image; // 상품이미지
private String sellerNickname; // 판매자
public Item(String name, int price, int stockQuantity, String itemDetail, String image,
String sellerNickname) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.itemDetail = itemDetail;
this.image = image;
this.sellerNickname = sellerNickname;
}
// @ManyToMany(mappedBy = "items")
// private List<Category> categories = new ArrayList<>();
//== 비지니스 로직 === //
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
public void removeStock(int quantity) {
int restStock = stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
public void update(String name, int price, int stockQuantity, String image,
String itemDetail) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.image = image;
this.itemDetail = itemDetail;
}
}
Album
package study.wonyshop.item.entity;
@Entity
@Getter
@DiscriminatorValue("A")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Album extends Item {
private String artist;
private String etc;
@Builder
public Album(String name, int price, int stockQuantity, String itemDetail, String image,
String sellerNickname, String artist, String etc) {
super(name, price, stockQuantity, itemDetail, image, sellerNickname);
this.artist = artist;
this.etc = etc;
}
public void updateAlbum(String artist, String etc) {
this.artist = artist;
this.etc = etc;
}
}
Book
package study.wonyshop.item.entity;
@Entity
@DiscriminatorValue("B") // 싱글테이블에서 상품을 구분하기 위해 컬럼과 밸류를 추가함
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book extends Item {
private String author;
private String isbn;
@Builder
public Book(String name, int price, int stockQuantity, String itemDetail, String image,
String sellerNickname, String author, String isbn) {
super(name, price, stockQuantity, itemDetail, image, sellerNickname);
this.author = author;
this.isbn = isbn;
}
public void updateBook(String author, String isbn){
this.author = author;
this.isbn = isbn;
}
}
Movie
package study.wonyshop.item.entity;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import study.wonyshop.item.dto.UpdateRequest;
@Entity
@DiscriminatorValue("M")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Movie extends Item {
private String director;
private String actor;
@Builder
public Movie(String name, int price, int stockQuantity, String itemDetail, String image,
String sellerNickname, String director,
String actor) {
super(name, price, stockQuantity, itemDetail, image, sellerNickname);
this.director = director;
this.actor = actor;
}
public void updateMovie(String director,String actor){
this.director = director;
this.actor = actor;
}
}
ItemType
@Getter
public enum ItemType {
ALBUM,
BOOK,
MOVIE;
}
ItemService
package study.wonyshop.item.service;
@Service
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
private final ItemFactory itemFactory;
/**
* 상품 등록
*/
@Transactional
public ItemResponse resisterItem(String sellerNickname, ItemRequest itemRequest) {
Item item = itemFactory.createItem(sellerNickname, itemRequest);
itemRepository.save(item);
ItemResponse itemDto = new ItemResponse(item);
return itemDto;
}
/**
* 판매자가 등록한 상품 리스트 조회
*
* @param page
* @param size
* @param direction
* @param properties
* @param sellerNickname
* @return
*/
public Page<ItemResponse> getRegisteredItems(int page, int size, Direction direction,
String properties, String sellerNickname) {
Page<Item> itemList = itemRepository.findAllBySellerNickname(
PageRequest.of(page - 1, size, direction, properties), sellerNickname);
Page<ItemResponse> itemPageList = itemList.map(o -> new ItemResponse(o));
return itemPageList;
}
/**
* 상품 삭제 : 판매자 및 관리자만 삭제 가능
*
* @param sellerNickname
* @param role
* @param id
* @return
*/
public ResponseEntity deleteItem(String sellerNickname, UserRoleEnum role, Long id) {
Item findItem = findById(id);
isSellerOrAdmin(sellerNickname, role, findItem);
itemRepository.delete(findItem);
return ResponseEntity.ok("상품 삭제 완료");
}
/**
* 상품조회 메서드
*/
public Item findById(Long id) {
Item findItem = itemRepository.findById(id).orElseThrow(
() -> new CustomException(ExceptionStatus.NOT_EXIST)
);
return findItem;
}
@Transactional
public ItemResponse updateItem(String sellerNickname, UserRoleEnum role, Long id,
UpdateRequest updateRequest) {
Item findItem = findById(id);
isSellerOrAdmin(sellerNickname, role, findItem);
String name = updateRequest.getName();
int price = updateRequest.getPrice();
int stockQuantity = updateRequest.getStockQuantity();
String image = updateRequest.getItemDetail();
String itemDetail = updateRequest.getItemDetail();
findItem.update(name, price, stockQuantity, image, itemDetail);
itemFactory.updateItem(updateRequest,findItem);
itemRepository.save(findItem);
return new ItemResponse(findItem);
}
public void isSellerOrAdmin(String sellerNickname, UserRoleEnum role, Item item) {
if (!sellerNickname.equals(item.getSellerNickname()) && !role.equals(UserRoleEnum.ADMIN)) {
throw new CustomException(ExceptionStatus.WRONG_USER_T0_CONTACT);
}
}
}
상품을 추가할때마다 각각의 등록api를 만들지 않고, 팩토리패턴을 이용함으로 하나의 api로 모든 상품을 추가 할 수 있어 >.<ㅎㅎ 너무 좋다~~~
아직 까진 불편함은 없고 ㅎㅎ 좋기만 함 ~~
팩토리 패턴 (factory pattern)
팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를
만들지는 서브클래스에서 결정하게 만든다. 즉 팩토리 메소드 패턴을 이용하면
클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것.
추상 팩토리 패턴 : 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성.
참고자료
https://jusungpark.tistory.com/14