ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Project] 가상착용 쇼핑몰 백엔드 설계 프로젝트 : 서버에 파일 업로드하기
    ヽ(✿゚▽゚)ノ 2022. 11. 20. 16:26

    가상착용 쇼핑몰 백엔드를 설계하는 프로젝트에 관한 게시글입니다. 이 프로젝트에서는 여러 엔티티 중 상품 엔티티에 주목해 설계할 예정입니다.

     

    프로젝트 구현 기능

    - 회원 : 소셜로그인

    - 상품: 상품 등록 / 상품 조회(상품 가상 착용)/ 상품 상세페이지/ 상품 카테고리/ 상품 링크 연결

    - 좋아요: 상품 좋아요

    - 브랜드: 브랜드 회원 가입, 로그인 및 로그아웃, 브랜드 승인

     

    1. 초기 ERD 설계

    프로젝트의 초기 아이디어는 상품을 판매하는 링크를 연결해주고,  그 상품의 모델을 가상 착용하는 식으로 설계했기 때문에 주문 엔티티는 따로 없게 설계했다.

     

    ManyToMany를 사용하지 않기 위해 카테고리 연령별과 안경 종류별로 나누었다.

    2. 상품 모델 업로드

    2.1 Spring initializer로 스프링부트 프로젝트 생성

    https://start.spring.io/

    spring initializer를 통해 스프링부트 프로젝트를 생성한다. 추가해 준 라이브러리는 다음과 같다.

    H2는 내장 데이터베이스 이다.

    ***

    알고보니 Thymeleaf를 추가해주지 않아서.. 뒤늦게 build.gradle에서 
    implementation group: 'org.thymeleaf', name: 'thymeleaf', version: '3.1.0.RELEASE' 로 의존성을 추가해주고 다시 빌드 해주었다.

    ***

    이렇게 스프링부트 프로젝트를 generate하면 컴퓨터 내에서 압축을 푼 후 IntelliJ에서 프로젝트를 연다.

    이제는 프로젝트의 초기 세팅을 할 차례이다. 

    2.1.1 프로젝트 초기 세팅

    생성된 프로젝트 안에 controller와 repository, service 패키지를 만들어준다.

    이건 MVC 패턴을 따라서 프로젝트를 생성해주는건데 controller는 MVC 중 C에 해당한다.

    주로 사용자의 요청을 처리할 후 지정된 뷰에 모델 객체를 넘겨주는 역할을 수행한다. 쉽게 말하면 사용자와 백엔드를 연결해주는 역할이라고 생각하면된다.

    repository는 DB와 연결하는 패키지이다. DB에서 데이터를 빼오거나 저장한다.

    Servic는 기능 구현을 위한 패키지인데, Repository를 사용하여 DB에서 빼오거나 저장하면서 구현하기 위한 패키지이다.

     

    그리고 controller 패키지 안에 MainController 클래스를 작성한다.

    이렇게 SeeFit 스트링을 반환하는 main 메서드를 포함한 MainController를 작성했다.

     

    이제 제대로 세팅이 됐는지 확인하기 위하여 프로젝트를 실행해보겠다.

     localhost:8080에 접속해보면 이렇게 SeeFit이 출력되는 걸 볼 수 있다.

     

    초기세팅은 끝났다! 이제 엔티티에 해당하는 클래스를 만들어보자.

     

    그 전에 엔티티 패키지를 추가해주고,  그 엔티티 패키지 안에 엔티티 클래스를 만들어주어야 한다.

     

    2.2 브랜드 엔티티 생성

    아래 코드에서 @Entity는 JPA에서 관리할 객체를 선언한다. 그리고 @Entity 어노테이션을 통해 자동으로 테이블을 생성해준다.

     

    어노테이션 @Id는 객체의 Primary Key를 의미한다. JPA는 이 id를 통해 객체를 구분한다.

    어노테이션 @GeneratedValue는 기본키 값에 대한 생성 전략을 제공한다. @Id와 함께 엔티티 또는 매핑된 슈퍼 클래스의 기본 키 속성 또는 필드에 적용할 수 있다.

    strategy를 통해 결정한 생성 전략인 AUTO는 JPA 구현체가 자동으로 생성 전략을 결정하는 방법이다.

    2.3 상품 엔티티 생성

    photo 대신에 3d 모델 데이터를 저장해야 하고 불러와야 한다. 하지만 플로우가 제품의 사진을 업로드하고, 그 사진을 바탕으로 제품의 3d 모델을 만들어 데이터베이스에 넣는 방식이기 때문에 먼저 사진을 업로드하는 기능을 구현해보겠다.

     

    - 이 외로 카테고리 엔티티도 생성해준다.

     

    2.4 엔티티 연관관계 생성

    브랜드와 상품은 일대다 연관관계를 가지고 있다.

    외래키가 있는 곳을 주인으로 지정해주어야하므로 Shop이 연관관계의 주인이 되어야 한다.

     

    상품 클래스에서는 shop에 ManyToOne 매핑을 해주고,  Shop(브랜드) 클래스에서는 OneToMany 매핑을 한다.

    이 외에도 상품은 카테고리들과 연관관계를 가지고 있으므로 연관관계 매핑을 한다.

     

    2.4.1 레포지토리 개발

    위에서 엔티티를 선언함으로써 데이터베이스 구조를 만들었다면, 여기에 어떤 값을 넣거나, 넣어진 값을 조회하는 등의 CRUD(Create, Read, Update, Delete)를 해야 쓸모가 있는데, 이것을 어떻게 할 것인지 정의해주는 계층이라고 생각하면 된다.

     

    2.5 상품 업로드

    Product 엔티티를 설계하였으니, Shop 객체와 Product 객체를 하나 생성후 모델에 데이터를 담아서 뷰에 전달해보자.

    그 전에 파일 자체를 저장할 클래스인 ProductModel 엔티티를 설계해야 한다.

     

    2.5.1 file 클래스 ProductModel 생성

    updateProductImg 함수는 원본 이미지 파일명, 업데이트할 이미지 파일명, 이미 경로를 파라미터로 받아서 이미지 정보를 업데이트하는 메소드이다.

     

    이제 상품을 저장할 DTO 클래스를 만들어야 한다. 

     

    DTO(Data Transfer Object) 는 계층 간 데이터 교환을 하기 위해 사용하는 객체로, DTO는 로직을 가지지 않는 순수한 데이터 객체(getter & setter 만 가진 클래스)이다.

     

    상품을 등록할 때는 화면으로부터 전달받은 DTO객체를 엔티티 객체로 변환하는 작업을 해야한다.

     

    이를 도와주는 라이브러리로 modelmapper 라이브러리가 있다. 이 라이브러리는 서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter,setter를 통해 값을 복사해서 객체를 반환해준다.

     

    Gradle을 사용한다면 이 라이브러리를 .gradle 파일에 dependencies를 추가하고 실행하므로써 라이브러리를 추가할 수 있다.

    implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.8'

    매번 Modelmapper 객체를 생성해주기보다 Bean을 사용해보자.

    이제 상품을 저장 후 상품 이미지에 대한 데이터를 전달할 DTO 클래스를 만들어보자.

    ProductModel 엔티티 객체를 받아서 ProductModel 객체의 자료형과 멤버변수 이름이 같을 때 ProductImgDto로 값을 복사해서 반환한다. 

     

    상품 등록 html 파일에서 이미지 관련 설정을 합니다.

    • form에는 enctype="multipart/form-data" 속성을 넣습니다. enctype은 form data가 서버로 날아갈 때 해당 데이터가 인코딩 되는 방법을 명시합니다. 주의할 점은 form 요소의 method 속성값이 "post"인 경우에만 가능합니다.
    • multipart/form-data 는 form 요소가 파일이나 이미지를 서버로 전송할 때 주로 사용합니다.
    • input 태그의 type="file" 이고, id와 name은 상품등록 Controller에서 매개변수 명과 동일하게 설정합니다.

    결과적으로 이렇게 나타난다.

     

    상품의 이미지 정보를 저장하기 위해서 repository 패키지 아래에 ProductImgRepository 인터페이스를 만들고 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ProductImgService 클래스를 service 패키지 아래에 생성한다.

    @Service
    @RequiredArgsConstructor
    @Transactional
    public class ProductImgService {
    
        @Value("${productImgLocation}")
        private String productImgLocation;
        private final ProductImgRepository productImgRepository;
    
        private final FileService fileService;
    
        public void saveProductImg(ProductModel productModel, MultipartFile itemImgFile) throws Exception{
            String oriImgName = itemImgFile.getOriginalFilename();
            String imgName = "";
            String imgUrl = "";
    
            //파일 업로드
            if(!StringUtils.isEmpty(oriImgName)){
                imgName = fileService.uploadFile(productImgLocation, oriImgName,
                        itemImgFile.getBytes());
                imgUrl = "/images/item/" + imgName;
            }
    
            //상품 이미지 정보 저장
            productModel.updateProductImg(oriImgName, imgName, imgUrl);
            productImgRepository.save(productModel);
        }
    
        public void updateItemImg(Long itemImgId, MultipartFile itemImgFile) throws Exception{
            if(!itemImgFile.isEmpty()){
                ProductModel savedProductImg = (ProductModel) productImgRepository.findById(itemImgId)
                        .orElseThrow(EntityNotFoundException:: new);
    
                //기존 이미지 파일 삭제
                if(!StringUtils.isEmpty(savedProductImg.getImgName())) {
                    fileService.deleteFile(productImgLocation+"/"+
                            savedProductImg.getImgName());
                }
    
                String oriImgName = itemImgFile.getOriginalFilename();
                String imgName = fileService.uploadFile(productImgLocation, oriImgName, itemImgFile.getBytes());
                String imgUrl = "/images/item/" + imgName;
                savedProductImg.updateProductImg(oriImgName, imgName, imgUrl);
            }
        }
    }

    다음은 상품을 등록하는 ProductService클래스이다.

    이제 상품을 등록하는 url을 ProductController에 추가하겠다.

    테스트 실행

    @SpringBootTest
    @Transactional
    @TestPropertySource(locations="classpath:application-test.properties")
    class ProductServiceTest {
    
        @Autowired
        ProductService productService;
    
        @Autowired
        ProductRepository productRepository;
    
        @Autowired
        ProductImgRepository productImgRepository;
    
        List<MultipartFile> createMultipartFiles() throws Exception{
    
            List<MultipartFile> multipartFileList = new ArrayList<>();
    
            for(int i=0;i<5;i++){
                String path = "C:/shop/Product/";
                String imageName = "image" + i + ".jpg";
                MockMultipartFile multipartFile =
                        new MockMultipartFile(path, imageName, "image/jpg", new byte[]{1,2,3,4});
                multipartFileList.add(multipartFile);
            }
    
            return multipartFileList;
        }
    
        @Test
        @DisplayName("상품 등록 테스트")
        void saveProduct() throws Exception {
            ProductFormDto productFormDto = new ProductFormDto();
            productFormDto.setPrice(1000);
    
            List<MultipartFile> multipartFileList = createMultipartFiles();
            Long ProductId = productService.saveProduct(productFormDto, multipartFileList);
    
            Product product = (Product) productRepository.findById(ProductId)
                    .orElseThrow(EntityNotFoundException::new);
    
        }
    
    }

    createMultipartFiles()함수는 MockMultipartFile 클래스를 이용하여 가짜 MultipartFile 리스트를 만들어서 반환해주는 메소드이다.

     

    여기까지, shop의 권한으로 상품을 올려놓은 과정을 진행하였다. 

    다음엔 로그인 기능을 구현하고 3d모델을 서버에 저장하는 방법에 대해서 알아보겠다.

     

Designed by Tistory.