Jin's Dev Story

[스프링 부트 쇼핑몰 프로젝트 with JPA] 6-2. [상품 등록] Controller, Service, Repository 본문

Web & Android/스프링 부트 쇼핑몰 프로젝트 with JPA

[스프링 부트 쇼핑몰 프로젝트 with JPA] 6-2. [상품 등록] Controller, Service, Repository

woojin._. 2023. 10. 15. 10:36
이 내용은 스프링 부트 쇼핑몰 프로젝트 with JPA 책을 학습한 내용입니다.

1. application.properties 설정 추가

  • 파일 크기 및 경로 지정 (경로는 프로젝트 외부 디렉토리에 저장함)
##########################
# 파일 업/다운로드 설정
##########################
# 파일 한 개당 최대 사이즈
spring.servlet.multipart.max-file-size=20MB
# 요청 당 최대 파일 크기
spring.servlet.multipart.max-request-size=100MB
# 상품 이미지 업로드 경로
itemImgLocation=D:/shop/item
# 리소스 업로드 경로
uploadPath=file:///D:/shop/

2. WebMvcConfigurer 인터페이스

  • addResourceHandlers 메소드를 오버라이딩하여 파일 업로드 경로 지정
  • "${uploadPath}" - application.properties 에 설정한 "uploadPath" 프로퍼티 값
  • " /images/** " 패턴의 URL 은 uploadPath 폴더를 기준으로 탐색
package kr.spring.config;
// 이미지 업로드 파일 경로
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//파일 올리고 이미지 올리고 이럴 때 웹이 로컬을 막 건들면 안되니까 설정 파일을 하나 만들어줌
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 이미지/파일을 업로드하는 진짜 경로를 application properties에 uploadPath로 저장해놨는데 그걸 가져오게 하기 위함
    @Value(value = "${uploadPath}")
    private String uploadPath;  // 해당 경로를 사용할 수 있음

    @Override
    // registry를 등록해서 쓸 수 있게 함
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
         registry.addResourceHandler("/images/**") // 접근을 이 주소로 하게 함
                 .addResourceLocations(uploadPath); // 실제로는 여기인데 (위에 처럼함)
    }
}

3. 상품 이미지 파일 (File)Service

  • FileService.java
    • 이미지 파일 저장 로직을 담당할 Service 객체
    • 파일 저장은 DB 에 저장되는 것이 아니기 때문에 Repository 필요 없음 (FileOutputStream 가 대신함)
    • 이미지 파일 업로드
    • UUID(Universally Unique IDentifier) - 서로 다른 개체들을 구별하기 위한 클래스
    • FileOutputStream 클래스를 이용하여 파일을 저장
package kr.spring.item.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.UUID;

@Service
// 파일을 만들고 지우는 일을 함
@Slf4j
public class FileService {

    // 특정 폴더를 잡고 올리고 싶지만, 파일 명칭이 충돌이 날 수 있기 때문에 파일 명칭이 충돌이 나지 않게 하는 메소드
    public String uploadFile(String uploadPath, String oriFileName, byte[] fileData) throws IOException {

        UUID uuid = UUID.randomUUID();  // 임의의 UUID 자동으로 생성
        // file의 확장자를 떼기 위함
        // 요즘 파일은 중간중간에도 .을 찍기 때문에 맨 마지막에 있는 확장자만 지운다.
        String extension = oriFileName.substring(oriFileName.lastIndexOf(".")); // 마지막 .을 기준으로 문자열 가져옴
        // uuid는 문자열이 아니니까 toString 꼭 해줘야 함 거기에 확장자만 붙여주면 됨
        String savedFileName = uuid.toString() + extension;
        // 실제 파일 경로 -> D://어쩌고~ / uuid로 만든 파일 이름.extension으로 저장됨
        String fileUploadUrl = uploadPath + "/" + savedFileName;
        FileOutputStream fos = new FileOutputStream(fileUploadUrl); // 출력
        // 위에서 받아온 fileData를 써줌
        fos.write(fileData);
        fos.close();

        return savedFileName;

    }

    // 파일을 지우는 동작
    public void deleteFile(String filePath) {
        // 파일을 지우기 위해서는 file 객체 필요
        File deleteFile = new File(filePath);

        // 지우려는 파일 존재하면
        if(deleteFile.exists()) {
            deleteFile.delete();
            log.info("파일을 삭제했습니다.");
        } else {
            log.info("파일이 존재하지 않습니다.");
        }
    }
}

4. 상품 이미지 정보 Repository

package kr.spring.item.repository;

import kr.spring.item.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {

}

5. 상품 이미지 Service

  • 상품 이미지 업로드, 상품 이미지 정보 저장 Service
  • 클래스 생성 및 DI
  • application.properties 에 설정한 itemImgLocation 값을 가져옴
  • 상품 이미지 업로드 - FileService
  • 상품 이미지 정보 저장 - itemImgRepository
@Service
@RequiredArgsConstructor // 이걸 사용하면 autowired를 안해도 됨
@Transactional //그림을 세 개를 올리다가 네 개까지 갔는데 하나만 오류가 날 경우 다 취소하고 다시 해야함. 이게 transaction
public class ItemImgService {

    // application properties에 설정해놓은 itemImgLocation를 가져와서 service에서 uploadfile 할 때 사용
    @Value(value = "${itemImgLocation}")
    private String itemImgLocation;

    private final ItemImgRepository itemImgRepository;

    private final FileService fileService;
}
  • 상품 이미지 저장 (내부에 사진 업로드 포함)
  • if (!StringUtils.isEmpty(oriImgName))상품 이미지가 존재하지 않는다면 건너 뜀존재한다면 fileService.uploadFile() 메소드로 파일 업로드 후 UUID 를 통해 변경된 파일명 return
  • updateItemImg 메소드와 itemImgRepository.save() 메소드를 이용해서 상품 이미지 정보 저장
// 이미지 저장
    public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile) throws IOException {

        // 파일을 받아온 시점에 보면 multiopart 안에 오리지날 이름 등 파일 정보가 다 들어있는데 일단 저건 원래 이름을 가지고 오는 것
        // 왜냐면 DB에 넣을 때는 원래 이름을 알아야 하기 때문
        String oriImgName = itemImgFile.getOriginalFilename();
        // fileService에서 만든 imgName임
        String imgName = "";
        // 이미지 경로
        String imgUrl = "";

        // 원래 경로가 값이 비어있는지 타임리프 유틸을 이용해서 확인
        if(!StringUtils.isEmpty(oriImgName)) {
            // 진짜 이미지 이름 받아옴
            // 파일의 정보는 itemImgFile에 다 있으니까 이걸 byte배열로 가져옴
            imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
            imgUrl = "/images/item/" + imgName;
        }

        // 실제 상품 이미지 저장
        itemImg.updateItemImg(oriImgName, imgName, imgUrl);
        // 이미지만 저장 그러므로 아이템도 저장해줘야함
        itemImgRepository.save(itemImg);
    }

6. 상품 Service

  • 클래스 생성 및 DI
  • 상품 저장 - itemRepository
  • 상품 이미지 저장 - itemImgService
@Service
@RequiredArgsConstructor
@Transactional // 서비스 등록하다가 깨지면 지금 했던 일을 다시 해야하기 때문에
@Slf4j
public class ItemService {

    private final ItemRepository itemRepository;
    private final ItemImgRepository itemImgRepository;
}
  • 상품 저장
  • ItemFormDto 객체를 DB 에 저장하기 위해 Item 객체로 변환
  • 상품의 첫 번째 이미지는 대표 사진으로 설정
// 아이템 저장
    // 아이템을 등록했으니까 아이템 아이디가 넘어왔을 것
    // itemDto 값을 넘겨 받고, multipart 형식으로 되어있는 리스트를 받아옴
    public Long saveItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgeFileList) throws IOException {

        // DTO를 entity로 바꾼다. createItem에서 mapper로 바꿨으니까
        Item item = itemFormDto.createItem();
        itemRepository.save(item);

        // 그림 저장하기
        // 내가 저장한 이미지의 갯수만큼 돌림
        for(int i=0; i<itemImgeFileList.size(); i++) {
            ItemImg itemImg = new ItemImg();
            // 기존에 있던 아이템의 id값을 세팅해야한다. 영속성 영역에 떠있기 때문에 지금 값이 채워져있는 상태가 돼서 그냥 값을 가져오기만 하면 됨
            itemImg.setItem(item);

            // 첫 번째 이미지면 대표 이미지로 씀
            if(i == 0) {
                itemImg.setRepimgYn("Y");
            } else {
                itemImg.setRepimgYn("N");
            }

            // 실제로 DB에 집어넣어야한다 파일 리스트에 있는 애들 중 i번째에 있는 애들을 꺼내서 등록을 해줌
            itemImgService.saveItemImg(itemImg, itemImgeFileList.get(i));
        }

        // 아이디를 반환함
        return item.getId();
    }

7. 상품 등록 Controller

  • 상품 등록 페이지 접근 (GetMapping)
@Controller
@RequiredArgsConstructor // @Autowired로도 쓸 수 있음
public class ItemController {

    // @Autowired ->  @RequiredArgsConstructor를 써도 됨 (final 붙여야함)
    private final ItemService itemService;

    // 웹 페이지로 이동
    @GetMapping("/admin/item/new")
    public String itemForm(Model model) {
        model.addAttribute("itemFormDto", new ItemFormDto());

        return "item/itemForm";
    }
}
  • 상품 등록 (PostMapping)
  • 입력값 검증 및 에러 메시지 표시 참고
  • POST 입력으로 들어온 이미지 파일 (name = "itemImgFile")을 MultipartFile 객체로 받음
  • 입력값이 비정상이거나, 첫 번째 상품 이미지를 지정하지 않았으면 다시 상품 등록 페이지로 돌아감
  • 입력값이 정상이면 itemService.saveItem(itemFormDto, itemImgFileList) 수행
// 웹 페이지에서 정보를 가져와 저장
    @PostMapping("/admin/item/new")
    // itemFormDto에 에러 발생 시 메시지 주려고 처리해놨으므로 @Valid 해줌
    public String itemNew(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
                          Model model, @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList) {

        // 에러가 있는 경우 상품등록으로 다시 가기
        if(bindingResult.hasErrors()) {
            return "item/itemForm";
        }

        // 0번째 이미지 리스트가 값이 비었고 itemFormDto의 아이디가 없다면
        if(itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
            model.addAttribute("errorMessage", "첫 번째 상품 이미지는 필수입니다.");
            return "item/itemForm";
        }

        try {
            itemService.saveItem(itemFormDto, itemImgFileList);
        } catch (IOException e) {
            e.printStackTrace();
            model.addAttribute("errorMessage", "상품 등록 중에 오류 발생");
            return "item/itemForm";
        }

        return "redirect:/";
    }

8. 상품 등록 동작 과정

① "ADMIN" 권한을 가진 아이디로 상품 등록 페이지 Get 요청

② Item Controller 에서 상품 등록 페이지를 반환하면서 itemFormDto 객체도 넘김

③ 상품 등록 페이지에서 상품 정보 및 이미지를 입력하고 "저장" (POST 요청)

④ Item Controller 에서 입력값을 검증하고 itemService.saveItem() 메소드를 수행

   - 이 때, 파라미터는 입력받은 itemFormDto 객체와 이미지 정보를 담고있는 itemImgFileList 를 넘김    

⑤ itemService 에서 itemFormDto 객체를 item 엔티티로 변환하고 itemRepository.save() 메소드 수행

⑥ itemService 에서 itemImg 객체를 생성하고 itemImgService.saveItemImg() 메소드 수행        

   - 이 때, 파라미터는 itemImg 객체와 이미지 정보를 담고있는 itemImgFileList.get(i) 객체 하나를 지정

⑦ ItemImgService 에서 상품 이미지가 존재한다면 fileService.uploadFile() 메소드 수행

   - 이 때, 파라미터는 "저장위치", "원래 파일명", "이미지 Byte 파일"

⑧ FileService 에서 UUID 객체를 이용해 파일명을 새로 만들고 FileOutputStream 을 이용하여 저장

⑨ ItemImgService 에서 itemImgRepository.save() 메소드 수행