chae._.chae

(6) 프로필 페이지 - 이미지 업로드 및 예외처리 본문

스프링/인스타그램 클론코딩

(6) 프로필 페이지 - 이미지 업로드 및 예외처리

walbe0528 2022. 7. 19. 16:37
728x90
반응형

 

프로필 페이지에 들어갔을때 사진을 등록해 업로드하고, 업로드한 사진이 하단에 뜨게끔 하는 기능을 구현해보자.

 

  • Image를 서버와 db에 업로드
  • 예외처리

위와 같은 순서로 진행

 

먼저, Image엔티티를 생성해준다. 

 

< Image >

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String caption;  // 이미지 설명.

    private String postImageUrl;  // 경로. 사진을 전송받아서 그 사진을 서버의 특정폴더에 저장 -> DB에는 저장된 경로를 insert해준다.

    @JoinColumn(name = "userId")  // "userId"의 컬럼명으로 저장되도록 지정해준다
    @ManyToOne
    private User user;  // 1명의 유저가 이미지 여러개 올릴 수 있음

    // 이미지 좋아요
    // 댓글

    private LocalDateTime createDate;

    @PrePersist
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }

}

 

 

 

사용자가 컴퓨터의 사진을 선택해 업로드 버튼을 누르면 이 사진을 받아와서 저장하는 방식이다. 

 

 

이미지를 받아와서 저장할 때에는,

사진을 전송받아서 그 사진을 서버의 특정 폴더에 저장하고,

db에는 사진이 저장된 경로가 저장된다. 

(사진 자체의 주소가 저장되는 것이 X)

 

 

 

 

 

 

< ImageController >

@PostMapping("/image")
public String imageUpload(ImageUploadDto imageUploadDto, @AuthenticationPrincipal PrincipalDetails principalDetails){
    // 서비스 호출
    imageService.사진업로드(imageUploadDto, principalDetails);
    return "redirect:/user/" + principalDetails.getUser().getId();  // "/user/유저인덱스번호"로 돌아가게끔
}

 

사진업로드가 정상적으로 수행되면, http://localhost:8080/user/{user_id}로 해당 유저의 프로필 페이지로 리다이렉트 되게 설정해준다.

 

 

< ImageUploadDto >

@Data
public class ImageUploadDto {

    private MultipartFile file;
    private String caption;

    public Image toEntity(String postImageUrl, User user){
        return Image.builder()
        .caption(caption)
        .postImageUrl(postImageUrl)
        .user(user)
        .build();
    }
}

 

파일을 다룰때에는 MultipartFile 타입을 사용한다. 

 

< ImageService >

 

@RequiredArgsConstructor
@Service
public class ImageService {

    private final ImageRepository imageRepository;

    @Value("${file.path}")  // application.yml에서 file-path에 있는 경로를 가져온다
    private String uploadFolder;  // yml에 적은 값도 가져올 수 있다!

//    private String uploadFolder = "C:/workspace/springbootwork/upload/";  // 경로뒤에 / 반드시 있어야함

    @Transactional
    public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails){
        UUID uuid = UUID.randomUUID();  // uuid(Universally Unique IDentifier) : 네트워크상에서 고유성이 보장되는 id를 만들기 위해 사용한다. 중복이 되지 않게 나온다.
        String imageFileName = uuid + "_" + imageUploadDto.getFile().getOriginalFilename();  // 실제 파일 이름이 들어간다  ex) bag.jpg
        // 서버의 upload라는 폴더에 bag.jpg가 저장되고, db에는 "/upload/bag.jpg"라는 경로가 저장된다.
        // 따라서 동일한 파일명이 저장되면 덮어씌워지기에 우리가 사진을 받아서 구분해줘야 한다. -> 구분에 uuid사용

        System.out.println("이미지 파일 이름: " + imageFileName);

        Path imageFilePath = Paths.get(uploadFolder + imageFileName);  // 실제 저장되는 경로를 지정(경로 + 파일명)

        // 통신이 일어나거나 I/O가(하드디스크에 기록하거나 읽을때) 일어날때 -> 예외가 발생
        // 어떤 파일을 읽어오고 싶은데 하드디스크에 해당 파일이 없는 경우 -> 컴파일시에 잡아낼 수가 없어 런타임시에만(실제 실행될때) 잡아낼 수 있음 -> 예외처리 해줘야한다
        try{
            Files.write(imageFilePath, imageUploadDto.getFile().getBytes());  // 경로, 실제 이미지 파일(바이트화해서)
        } catch (Exception e){
            e.printStackTrace();
        }

        // image테이블에 저장 (imageUploadDto로 image객체로 변환하는 과정이 필요)
        Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser());  // 13b00cd8-e623-47c5-be31-0a9c5acb6aef_cat.jpg가 db에 저장된다
        Image imageEntity = imageRepository.save(image);

        System.out.println(imageEntity);
    }
}

 

  • @Value어노테이션을 이용하면 application.yml에 있는 아래의 값을 읽어올 수 있다.  
file:
  path: C:/workspace/springbootwork/upload/
  • 위의 값을 가져오고 싶으면, @Value("${file.path}")로 어노테이션을 달아준다.
@Value("${file.path}")  // application.yml에서 file-path에 있는 경로를 가져온다
private String uploadFolder;  // yml에 적은 값도 가져올 수 있다!

 

 

 

String imageFileName = uuid + "_" + imageUploadDto.getFile().getOriginalFilename();

사진 파일 이름은 imageUploadDto.getFile().getOriginalFilename()으로 실제 파일이름이 들어간다. 

실제 파일 이름만으로 저장하면 동일한 파일명이 저장되면 기존의 사진이 사라지고 덮어씌워지게 된다. 

이를 사용자가 매번 다르게 설정해주기에는 사용자가 서버에 모든 사진의 파일이름을 알 수 없으므로, UUID를 사용해준다. 

 

UUID(Universally Unique IDentifier) : 네트워크 상에서 고유서잉 보장되는 id를 만들기 위해 사용한다. 중복이 되지 않게 값이 나오기에 unique하다. 

 

따라서, 파일을 저장할때 아래와 같이 uuid값 + 실제파일이름을 함께 저장해주어 이름이 겹치는 일이 없도록 막아준다. 

String imageFileName = uuid + "_" + imageUploadDto.getFile().getOriginalFilename();

 

설정해준 경로 uploadFolder에 위에서 만들어준 imageFileName으로 저장해준다. 

Path imageFilePath = Paths.get(uploadFolder + imageFileName);  // 실제 저장되는 경로를 지정(경로 + 파일명)

 

실제 프로젝트를 실행시키고 서버에 접속해서 파일을 업로들하면 사진이 해당 경로에 저장되는 것을 확인할 수 있다.!!

 

사진 파일이 내가 application.yml에서 file.path에서 설정해준 경로로 들어간다. 

C:/workspace/springbootwork/upload/

파일이름은 "uuid + _ + 기존에 내가 설정해둔 파일 이름"의 형태로 저장된다.

 

 

 

 

예외처리

이미지가 들어오지 않고, caption(이미지 설명)만 들어온 경우에 대해 예외처리를 해주자.

컨트롤러에서 이미지파일이 들어왔는지 확인하고, 들어오지 않았다면 예외를 던저주는 흐름으로 진행한다.

 

@PostMapping("/image")
   public String imageUpload(ImageUploadDto imageUploadDto, @AuthenticationPrincipal PrincipalDetails principalDetails){

       if (imageUploadDto.getFile().isEmpty()){
           throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
       }
       // 서비스 호출
       imageService.사진업로드(imageUploadDto, principalDetails);
       return "redirect:/user/" + principalDetails.getUser().getId();  // "/user/유저인덱스번호"로 돌아가게끔
   }
}

 

Dto에서 getFile()을 통해 파일이 있는지를 확인하고, 파일이 없다면 CustomValidationException을 던져준다.

 

 

< CustionValidationException >

public class CustomValidationException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    private Map<String, String> errorMap;

    public CustomValidationException(String message, Map<String, String> errorMap) {
        super(message);
        this.errorMap = errorMap;
    }

    public Map<String, String> getErrorMap(){
        return errorMap;
    }
}

 

< ControllerExceptionHandler >

 

@RestController  // 데이터로 리턴한다
@ControllerAdvice  // exception이 발생할때 모두 낚아챈다
public class ControllerExceptionHandler {

    // js를 리턴
    @ExceptionHandler(CustomValidationException.class)  // RuntimeException이 발생하면 이 함수가 가로채서 실행된다
    public String validationException(CustomValidationException e){
        // CMRespDto, Script 비교
        // 1. 클라이언트에게 응답할때는 Script가 좋음
        // 2. Ajax 통신 - CMRespDto
        // 3. Android 통신 - CMRespDto
        if (e.getErrorMap() == null){
            return Script.back(e.getMessage());
        }else {
            return Script.back(e.getErrorMap().toString());
        }
    }

    // 데이터를 리턴(ajax로 통신시)
    @ExceptionHandler(CustomValidationApiException.class)
    public ResponseEntity<?> validationApiException(CustomValidationApiException e){
        return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()), HttpStatus.BAD_GATEWAY);
    }

    @ExceptionHandler(CustomApiException.class)
    public ResponseEntity<?> apiException(CustomApiException e){
        return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), null), HttpStatus.BAD_REQUEST);
    }
}

 

ControllerExceptionHandler 는 예외가 터졌을 때, 그 예외를 모두 잡아주는 역할을 한다.

 

@ExceptionHandler는 Controller계층에서 발생하는 에러를 잡아 메서드로 처리해주는 어노테이션(예외처리를 전역적으로 핸들링하는 역할) 이다. Service, Repository에서 발생한 에러는 잡아주지X
@ExceptionHandler의 value 값으로 어떤 Exception을 처리할 것인지 넘겨줄 수 있는데,value를 설정하지 않으면 모든 Exception을 잡게 되기 때문에 Exception을 구체적으로 적어줘야 한다. 

 

CustionValidationException 예외가 터지면, @ExceptionHandler에 따라 validationException이 실행되어 정상적으로 예외처리가 진행된다. 

 

728x90