[TIL ] 23.07.03 팩토리 패턴 적용

hyewon jeong·2023년 7월 2일
1

TIL

목록 보기
135/138

1 . 문제점

쇼핑몰을 만드는 과정에서 예전에는 포괄적으로 상품을 만들고 판매를 한다면 이번에는 상품마닥 가지는 특성을 살리면서 등록 및 판매가 하고 싶어졌다.
근데 상품 수 마다 클래스를 만들 수는 없다는 생각이 들었고.. 전에 소셜로그인 할때 팩토리 패턴을 쓴다는 말이 언뜻 생각나서 팩토리패턴을 뭔지 알아보고 이용해보기로 했다.


2 . 시도한 점

팩토리 패턴으로 여러 상품을 하나의 api로도 가능하지 않을까?? 해서 시도 +_+

2-1. switch 문 활용 , setter

상품의 타입을 열거형으로 상수 처리하고, 팩토리패턴에 이용할 서브클래스를 별도로 만들어 클래스의 인스턴스를 서브클래스에서 결정하도록 했다.

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 없이 할 수 있는 방법이 없을까 고민했다.


3 . 해결

setter 없이 if로 하나하나 직접 생성자를 통해 type에 따른 item 생성

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로 모든 상품을 추가 할 수 있어 >.<ㅎㅎ 너무 좋다~~~
아직 까진 불편함은 없고 ㅎㅎ 좋기만 함 ~~


4 . 알게 된점

팩토리 패턴 (factory pattern)

팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를

만들지는 서브클래스에서 결정하게 만든다. 즉 팩토리 메소드 패턴을 이용하면
클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것.

추상 팩토리 패턴 : 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성.

참고자료
https://jusungpark.tistory.com/14


profile
개발자꿈나무

0개의 댓글