소개
안녕하세요! 이번 포스팅에서는 Spring Boot 애플리케이션에서 상품 더미데이터를 생성하기 위한 기획과 설계 과정을 설명드리겠습니다. 더미데이터는 개발 및 테스트 환경에서 매우 유용하게 사용될 수 있습니다. 이 글을 통해 자바 ApplicationRunner 를 통한 상품 재고 더미데이터 설계 과정을 이해하실 수 있을 것입니다.
대상 독자
이 글은 Spring Boot 애플리케이션을 개발한 경험이 있는 개발자분들을 대상으로 합니다.
목차
1. 더미데이터의 필요성
- 개발 및 테스트 환경에서의 더미데이터 활용
- 더미데이터의 장점과 필요성
2. 프로젝트 셋업
- Spring Boot 프로젝트 초기 설정
- 필요한 의존성 추가 (JPA, Lombok, Hibernate 등)
3. 더미데이터 생성 기획
- 더미데이터의 구성 요소 (상품, 썸네일, 카테고리, 색상 등)
- 데이터 관계 설정 (상품-카테고리, 상품-색상 등)
4. 더미데이터 설계
- 엔티티 설계 (Product, ProductThumbnail, Category, ProductColor, ProductManagement 등)
- 각 엔티티 간의 관계 설정 (OneToMany, ManyToOne 등)
- 예시 다이어그램 및 스키마 설명
5. 데이터 초기화 전략
- 애플리케이션 구동 시 데이터 초기화 방법
- ApplicationRunner 인터페이스 사용
- 프로파일 설정을 통한 환경 분리 (@Profile 어노테이션)
1. 더미데이터의 필요성
개발 단계에서 매번 상품을 만들고, 카테고리를 설정하며 인벤토리를 구성하는 작업은 매우 번거롭고 시간이 많이 소요됩니다.
특히 POSTMan과 같은 API 툴을 사용하여 이러한 작업을 반복하다 보면 개발팀 전체의 피로도가 증가할 수밖에 없습니다.
이러한 문제를 해결하고 개발 효율성을 높이기 위해 더미데이터를 자동으로 생성하여 데이터베이스에 저장하는 방법이 필요합니다.
JAVA Spring Boot 에서 더미데이터를 추가하는 방법에 4가지정도 있을 수 있다고 합니다.
- data.sql 스크립트 파일에 더미 데이터 추가하는 쿼리문 작성
- Spring의 @PostConstruct를 사용해 초기화
- Spring의 @EventListener(ApplicationReadyEvent.class) 사용
- Spring의 @ApplicationRunner를 사용해 초기화
그 중 @ApplicationRunner 를 이용한 더미데이터 생성에 대해서 다뤄보겠습니다.
각 방법에 대한 설명은 >> 블로그를 참고하시면 됩니다
2. 프로젝트 셋업
Spring Boot 프로젝트를 설정하려면, build.gradle 파일에 필요한 의존성을 추가해야 합니다. 아래의 설정을 참고하여 프로젝트를 구성합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'PU'
version = '1.0.0-SNAPSHOT-RC'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot JPA Starter: JPA와 관련된 기본 설정과 라이브러리를 포함
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Jackson Datatype Hibernate: Hibernate 엔티티를 JSON으로 직렬화할 때 필요
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6:2.16.1'
// Hibernate Core: Hibernate ORM 프레임워크의 핵심 라이브러리
implementation 'org.hibernate:hibernate-core:6.4.4.Final'
// Lombok: 코드 작성량을 줄이기 위한 애너테이션 라이브러리
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// MySQL Connector: MySQL 데이터베이스와의 연결을 위한 JDBC 드라이버
runtimeOnly 'com.mysql:mysql-connector-j'
// Spring Boot Test Starter: 스프링 부트 애플리케이션 테스트를 위한 기본 설정과 라이브러리를 포함
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
이 build.gradle 파일에서 실제로 상품 더미데이터 생성에 사용되는 주요 의존성들은 다음과 같습니다:
- spring-boot-starter-data-jpa: JPA(Java Persistence API)와 관련된 설정 및 라이브러리를 제공하여 데이터베이스 작업을 쉽게 처리할 수 있습니다.
- jackson-datatype-hibernate6: Hibernate 엔티티를 JSON으로 직렬화할 때 사용됩니다. 더미데이터를 생성하고 이를 JSON 형태로 변환하여 저장할 때 유용합니다.
- hibernate-core: Hibernate ORM의 핵심 라이브러리로, JPA 구현체로 사용됩니다.
- lombok: 반복되는 코드를 줄이기 위한 애너테이션 라이브러리로, 엔티티 클래스 작성 시 유용합니다.
- mysql-connector-j: MySQL 데이터베이스와의 연결을 위한 JDBC 드라이버로, MySQL을 데이터베이스로 사용할 때 필요합니다.
- spring-boot-starter-test: 스프링 부트 애플리케이션 테스트를 위한 라이브러리로, 더미데이터의 유효성을 검증할 때 사용됩니다.
이 게시글은 상품 관련 엔티티들의 관계에 대한 분석과 더미데이터 생성을 목적으로 하고 있기 때문에,
해당 의존성에 대한 방법이나 설명은 생략할 것입니다.
3. 더미데이터 생성 기획
- 더미데이터의 구성 요소 (상품, 썸네일, 카테고리, 색상) - 상품 재고 테이블과 연관 관계에 대한 이해
일단 더미데이터에 대한 기획에 앞서서, 전체 테이블 구조와 연관 관계에 대한 이해가 필요합니다. 쇼핑몰 프로젝트의 ERD 설계를 보면서 설명드리겠습니다.
ProductManagement 테이블은 product_id, category_id, color_id 를 Foreignkey 로 가지며 상품 Size 는 Enum 타입으로 넣어주었습니다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "product_management")
public class ProductManagement {
/*@Id
@SequenceGenerator(
name = "product_management_sequence",
sequenceName = "product_management_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "product_management_sequence"
)*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "inventory_id" )
private Long inventoryId; // ProductManagement 테이블의 pk
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@ManyToOne
@JoinColumn(name = "color_id", unique = false, nullable = false)
private ProductColor color;
@ManyToOne
@JoinColumn(name = "category_id", unique = false, nullable = false)
private Category category;
@Enumerated(EnumType.STRING)
@Column(name = "size", nullable = false)
private Size size;
@Column(name = "initial_stock")
private Long initialStock;
@Column(name = "additional_stock")
private Long additionalStock;
@Column(name = "product_stock")
private Long productStock;
private boolean isSoldOut = false;
private boolean isRestockAvailable = false;
private boolean isRestocked = false;
@ManyToMany(mappedBy = "productManagements")
private List<Orders> orders = new ArrayList<>();
}
상품 엔티티 설계
Products 테이블은 생성 시, 가격, 제조업체, 상품 정보, 상품이름, 상품 타입(남,여,공용)을 선택해야 하며
사용자가 상품에 찜하기를 했을 때, wishListCount 가 올라가는 것은 더미데이터에 반영하지 않아도 되는 내용입니다.
@Getter
@Entity
@Table(name = "products_table")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long productId;
@Enumerated(EnumType.STRING)
@Column(name = "product_type", nullable = false)
private ProductType productType;
@Column(nullable = false, name = "product_name")
@NotBlank(message = "상품 이름은 필수입니다.")
private String productName;
@Column(name = "price", nullable = false, columnDefinition = "INT CHECK (price >= 0)")
private Integer price;
@Column(name = "product_info")
private String productInfo;
@CreationTimestamp
@Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
private LocalDateTime updatedAt;
@Column(nullable = false)
private String manufacturer;
@Column(name = "is_discount", nullable = false)
private Boolean isDiscount = false;
@Column(name = "discount_rate", nullable = true)
private Integer discountRate;
@Column(name = "is_recommend", nullable = false)
private Boolean isRecommend = false;
@OneToMany(mappedBy = "product")
private List<WishList> wishLists = new ArrayList<>();
@Column(name = "wishlist_count")
private Long wishListCount;
@OneToMany(mappedBy = "product")
private List<ProductManagement> productManagements = new ArrayList<>();
@OneToMany(mappedBy = "product")
private List<PaymentHistory> paymentHistories = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ProductThumbnail> productThumbnails = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ContentImages> contentImages = new ArrayList<>();
public void setWishListCount(Long wishListCount) {
this.wishListCount = wishListCount;
}
}
할인 여부에 따라서, 할인률(discountRate) 를 정할 수 있습니다.
저희는 테스트 결제(실제로 결제되지 않는 임시가맹점)를 적용했으며
100원 이하의 금액이 나오지 않도록 200, 300, 400 원의 금액에 대한 랜덤 금액을
더미데이터 상품 가격으로 정해주었습니다.
사실 100원, 500원 이상의 금액을 정해주어도 실제 결제가 진행되는 서비스는 아니기에 상관없지만
사이드 프로젝트 단위 내에서 200원 ~ 400원 이면 오히려 개발단계에서 편리할 수 있어서 , 그렇게 적용했습니다.
상품 카테고리 엔티티 설계
@Getter
@Entity
@Table(name = "category")
public class Category {
@Id
@SequenceGenerator(
name = "category_sequence",
sequenceName = "category_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "category_sequence"
)
@Column(name = "category_id")
private Long categoryId;
@Column(name = "name", unique = true, nullable = false)
@NotBlank(message = "이름은 필수 필드입니다.")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent")
private Category parent;
@Column(name = "depth", nullable = false)
private Long depth;
@OneToMany(mappedBy = "parent")
private List<Category> children = new ArrayList<>();
}
카테고리 엔티티에는 부모 카테고리와 자식 카테고리를 정의함으로써 ManyToOne 을 통해서, 하나의 부모 카테고리
OneToMany 를 통해서 , 여러개의 자식 카테고리를 가질 수 있게 했습니다.
상품의 색상은 테이블로 정의해서, 자유롭게 추가하고 상품 재고 인벤토리(ProductManagement)에 반영시킬 수 있도록 했습니다.
상품 사이즈 또한 테이블로 정의한다면, 매번 Enum 타입을 추가하는 것이 아닌 따로 DB에 저장해서 저장할 수 있으면
추후 확장 및 관리가 용이해질 것 같다는 생각이 들었습니다.
하지만, 현재 쇼핑몰 팀 프로젝트에서 아직은 Product Size 에 대한 확장을 고려하지 않아도 되기에
확장하지 않았습니다. 이와같이 테이블 구조와 엔티티에 대한 이해를 바탕으로 더미데이터를 설계하도록 하겠습니다.
더미데이터 설계(DummyData)
상품 재고 인벤토리(ProductManagement)에 대한 더미데이터를 만들기 위해 기본적으로 다음과 같은 데이터가 필요합니다:
- Product: 상품에 대한 기본 정보
- ProductThumbnail: 상품의 썸네일 이미지
- Category: 상품이 속하는 카테고리
- ProductColor: 상품의 색상
기본적인 데이터를 기반으로 상품 인벤토리(ProductManagement)를 만듭니다. 여기서 각 데이터 간의 관계를 이해하는 것이 중요합니다. 예를 들어, 하나의 상품(Product)은 여러 썸네일(ProductThumbnail)과 여러 색상(ProductColor)을 가질 수 있으며, 특정 카테고리(Category)에 속합니다. 이러한 관계를 명확히 정의하고 설계하는 것이 더미데이터 생성의 첫 단계입니다.
현재 MySQL RDB 에 저장된 상품 재고 인벤토리(ProductManagement) 테이블을 확인해보겠습니다.
상품 인벤토리(ProductManagement) 엔티티는 하나의 product_id 에 대해서 다양한 색상과 사이즈를 가질수 있습니다.
그래서 참고해야 할 사항은, ProductManagement 과 Product 는 목적 자체가 다르다는 것입니다.
실제 상품에 대한 결제를 했을 때, 상품의 수량은 ProductManagement 에서 빠지게 됩니다.
본격적인 더미데이터 설계에 앞서서, 저희는 상품 카테고리가 반영된 이미지파일과, 이미지파일의 이름을 구했습니다.
개발 단계에서 사용한 사진이며, 실제로 서비스를 운영하고 있지 않기 때문에 저작권에 대한 침해는 하지 않았습니다.
(Chayami 감사해여!)
ApplicationRunner 를 implements 한 ProductInitializer 의 전체 코드입니다.
상품 이미지 이름을 imagePaths 리스트에 넣어주고, 그것을 유틸 클래스 인자로 넣어주었습니다.
@Component
@Profile("develop")
@Order(1)
@RequiredArgsConstructor
public class ProductInitializer implements ApplicationRunner {
private final ProductRepositoryV1 productRepositoryV1;
private final Hibernate6Module hibernate6Module;
// 더미데이터 생성 및 DB 저장역할을 하는 Util Class
private final ProductDataUtil productDataUtil;
private final ProductThumbnailDataUtil thumbnailDataUtil;
private final CategoryDataUtil categoryDataUtil;
private final ProductColorDataUtil productColorDataUtil;
private final ProductManagementDataUtil productManagementDataUtil;
@Override
public void run(ApplicationArguments args) throws Exception {
// 상품 더미 데이터 생성후, 더미 데이터 DB 저장
List<Product> products = productDataUtil.generateProductDataWithImages(imagePaths);
// 상품 썸네일 더미 데이터 생성후, 더미 데이터 DB 저장
thumbnailDataUtil.generateProductThumbnailDataV3(products, imagePaths);
// 카테고리 데이터 (34개)
List<Category> categories = categoryDataUtil.generateCategoryData();
// 상품 색상 더미데이터 생성후, 더미 데이터 DB 저장
List<ProductColor> productColors = productColorDataUtil.generateAndSaveProductColorData();
// 상품 관리 더미데이터 생성후, 더미 데이터 DB 저장
productManagementDataUtil.generateProductManagementData(products, imagePaths, productColors, categories);
}
List<String> imagePaths = Arrays.asList(
"accessories_bag_1.jpg", "outer_cardigan_2.jpg", "shoes_sneakers_1.jpg",
"accessories_bag_2.jpg", "outer_coat_1.jpg", "shoes_sneakers_2.jpg",
"accessories_cap_1.jpg", "outer_coat_2.jpg", "top_blouse_1.jpg",
"accessories_cap_2.jpg", "outer_jacket_1.jpg", "top_blouse_2.jpg",
"accessories_socks_1.jpg", "outer_jacket_2.jpg", "top_hoodie_1.jpg",
"accessories_socks_2.jpg", "outer_lightweight-padding_1.jpg","top_hoodie_2.jpg",
"bottom_long_1.jpg", "outer_lightweight-padding_2.jpg", "top_knit-sweater_1.jpg",
"bottom_long_2.jpg", "outer_long-padding_1.jpg", "top_knit-sweater_2.jpg",
"bottom_shorts_1.jpg", "outer_long-padding_2.jpg", "top_long-shirts_1.jpg",
"bottom_shorts_2.jpg", "outer_mustang_1.jpg", "top_long-shirts_2.jpg",
"bottom_skirt_1.jpg", "outer_mustang_2.jpg", "top_long-sleeve_1.jpg",
"bottom_skirt_2.jpg", "outer_short-padding_1.jpg", "top_long-sleeve_2.jpg",
"dress&set_dress_1.jpg", "outer_short-padding_2.jpg", "top_short-shirts_1.jpg",
"dress&set_dress_2.jpg", "outer_vest_1.jpg", "top_short-shirts_2.jpg",
"dress&set_set-up_1.jpg", "outer_vest_2.jpg", "top_short-sleeve_1.jpg",
"dress&set_set-up_2.jpg", "shoes_boots_1.jpg", "top_short-sleeve_2.jpg",
"dress&set_two-piece_1.jpg", "shoes_boots_2.jpg", "top_sweatshirt_1.jpg",
"dress&set_two-piece_2.jpg", "shoes_sandal_1.jpg", "top_sweatshirt_2.jpg",
"outer_cardigan_1.jpg", "shoes_sandal_2.jpg"
);
}
순서는 위와 같습니다.
1. 상품 생성,
2. 상품에 맞춰서 상품썸네일 설정,
3. 상품 썸네일의 이름에 있는 카테고리에 맞게 카테고리를 설정,
4. 상품의 Color 의 더미데이터를 생성,
5. productManagement(상품 인벤토리) 에서 상품과 카테고리에 대해서 각 상품에 따라 3개씩 랜덤한 색상과 사이즈를 설정하여 productManagement 를 생성하도록 한다. 상품이 20개라면, 상품 재고는 60개.
ApplicationRunner에 대해서, 참고한 블로그의 링크를 맨위에 걸어두었지만, 모르실 수 있기에 간단히 설명드리겠습니다.구글에 검색하시면, 보다 자세한 내용들을 확인하실 수 있습니다.
ApplicationRunner는 Spring Boot에서 제공하는 인터페이스로, 애플리케이션이 시작(빌드)된 후 특정 로직을 실행할 수 있도록 합니다. 이는 주로 애플리케이션 초기화 작업이나 시작 시점에 필요한 설정 작업을 수행하는 데 사용됩니다.
ApplicationRunner 인터페이스 사용 - 동작 순서
- Spring Boot 애플리케이션 시작:
- Spring Boot 애플리케이션이 시작될 때, 스프링 컨텍스트가 초기화되고 모든 빈(Bean)이 생성됩니다.
- ApplicationRunner 인터페이스 구현:
- ApplicationRunner 인터페이스를 구현한 빈이 스프링 컨텍스트에 등록됩니다.
- 이 인터페이스는 run(ApplicationArguments args) 메서드를 하나 가지고 있습니다. 이 메서드는 애플리케이션이 시작된 후 호출됩니다.
- 애플리케이션 초기화 로직 실행:
- 스프링 컨텍스트가 초기화되고 모든 빈이 생성된 후, ApplicationRunner를 구현한 빈의 run 메서드가 실행됩니다.
- 이 시점에서 애플리케이션 초기화 로직을 작성하여 원하는 작업을 수행할 수 있습니다.
@Component
@Profile("develop")
@Order(1)
@RequiredArgsConstructor
public class ProductInitializer implements ApplicationRunner {
@Order(숫자) 를 통한 더미데이터 순서 설정
이 코드에서, Order 는 우선순위이며 상품 더미데이터 추가 후, 회원 및 결제에 대한 더미데이터를 순차적으로 진행했습니다.
@Profile 프로파일 설정을 통한 환경 분리
Profile 은, application.yml 에서
빌드 시, 초기화 작업에 대해 profiles 이 develop 인 초기화 작업을 시행합니다.
현재 none 으로 되어 있다면, Profile name 이 develop 인 프로필은 빌드시 이니셜라이징 되지 않습니다.
위와 같이 더미데이터의 필요성과 프로젝트 초기설정
생성 기획부터 설계까지의 내용에 대해서 설명드렸습니다.
다음 포스팅에서는 리소스 디렉토리에 저장된 상품 이미지 파일을 통해서
어떻게 상품과 카테고리(부모,자식 카테고리), 색상, 사이즈, 썸네일 이미지, 상품 재고 관리 데이터를 DB 에 저장했는지
생성 로직에 대해 자세히 다루며 마치도록 하겠습니다!
감사합니다 !
'Library&Framework > Spring Boot' 카테고리의 다른 글
[Spring Boot]상품 더미데이터 만들기 - (2)유틸 클래스와 기능 설명[상품, 이미지, 카테고리, 색상, 인벤토리 등] (0) | 2024.07.29 |
---|---|
[문제해결]이미지를 등록하고나서 , 이미지가 바로 나오지 않는 이유 (2) | 2024.06.04 |