File Handling

calis_ws·2023년 6월 24일
0
post-custom-banner

정적 파일

사용자에게 변환(변함) 없이 전달되는 파일을 말한다.

static

content들을 두는 곳 (CSS, js, 이미지 …)

templates

thymeleaf의 파일들을 두는 곳

첨부해둔 정적 파일은 Spring Boot의 기본 동작으로, / 로 들어온 요청에 대해 정적 파일 응답을 보내준다

http://localhost:8080/image.jpg
http://localhost:8080/scripts.js
http://localhost:8080/styles.css

정적 파일을 제공하는 경로 변경

/static/ 으로 시작하는 요청에 대해 정적 파일 서빙

application.yaml

spring:
  mvc:
    static-path-pattern: /static/**
  web:
    resources:
      # spring 이 정적 파일 요청을 받을 때 그 파일을 찾는 경로들을 나타내는 설정
      static-locations: file:media/,classpath:/static

  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

Multipart

  • HTTP 요청에서 여러 부분으로 구성된 복합 데이터를 전송하는 방법
  • 파일 업로드, 폼 데이터 전송 등 다양한 데이터 유형을 처리
  • Multipart 요청은 일반적으로 POST 요청과 함께 사용

FileController

@Slf4j
@RestController
public class FileController {
    @PostMapping(
            value = "/multipart",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE
    )
    public ResponseDto multipart(
            @RequestParam("name") String name,
            @RequestParam("photo") MultipartFile multipartFile
    ) throws IOException {
        // 저장할 경로를 생성한다.
        Files.createDirectories(Path.of("media"));
        // 저장할 파일이름을 포함한 경로를 작성한다.
        // Screenshot From 2023-06-23 10:38:00.png
        LocalDateTime now = LocalDateTime.now();
        log.info(now.toString());
        String filename = now.toString().replace(":", "");
        Path uploadTo = Path.of(String.format("media/%s.png", filename));
        // 저장한다.
        multipartFile.transferTo(uploadTo);

//        File file = new File("./filename.png");
//        try (OutputStream outputStream = new FileOutputStream(file)){
//            byte[] fileBytes = multipartFile.getBytes();
//            // 여기에서 byte[]를 활용
//
//            outputStream.write(fileBytes);
//        }

        ResponseDto response = new ResponseDto();
        response.setMessage(String.format("/static/%s.png", filename));
        return response;
    }
}

form.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form
        action="/multipart"
        method="post"
        enctype="multipart/form-data">
    <input type="text" name="name">
    <input type="file" name="photo">
    <input type="submit">
</form>
</body>
</html>

ResponseDto

import lombok.Data;

@Data
public class ResponseDto {
    private String message;
}

Postman

filename.png 저장

contents-skeleton project

사용자 프로필 이미지 설정

사용자가 프로필 사진을 업로드 한다면, media 경로에 저장 후 해당 파일에 접근할 수 있는 URL을 레포지토리에 저장한다.

UserEntity

@Data
@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;
    private String email;
    private String phone;
    private String bio;
    private String avatar;
}

UserService

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository repository;

    // createUser
    public UserDto createUser(UserDto dto) {
        // 1. 회원가입 -> 프로필 이미지가 아직 필요없다.
        if (repository.existsByUsername(dto.getUsername()))
            throw new UsernameExistException();
         // throw new IllegalStateException();
        UserEntity newUser = new UserEntity();
        newUser.setUsername(dto.getUsername());
        newUser.setEmail(dto.getEmail());
        newUser.setPhone(dto.getPhone());
        newUser.setBio(dto.getBio());
        return UserDto.fromEntity(repository.save(newUser));
    }

    // readUserByUsername
    public UserDto readUserByUsername(String username) {
        Optional<UserEntity> optionalUser = repository.findByUsername(username);
        if (optionalUser.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);

        return UserDto.fromEntity(optionalUser.get());
    }

    // updateUser
    public UserDto updateUser(Long id, UserDto dto) {
        Optional<UserEntity> optionalUser = repository.findById(id);
        if (optionalUser.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
		// throw new UserNotFoundException();
        UserEntity userEntity = new UserEntity();
        userEntity.setEmail(dto.getEmail());
        userEntity.setPhone(dto.getPhone());
        userEntity.setBio(dto.getBio());
        return UserDto.fromEntity(repository.save(userEntity));
    }

    // updateUserAvatar
    public UserDto updateUserAvatar(Long id, MultipartFile avatarImage) {
        // 2. 사용자가 프로필 이미지를 업로드 한다.
        // 1) 유저 존재 확인
        Optional<UserEntity> optionalUser = repository.findById(id);
        if (optionalUser.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);

        // media/filename.png
        // media/<업로드 시각>.png
        // 2) 파일을 어디에 업로드할건지
        // media/{userId}/profile.{파일 확장자}

        // 2-1. 폴더만 만들기
        String profileDir = String.format("media/%d/", id);
        try {
            Files.createDirectories(Path.of(profileDir));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        // 2-2. 확장자를 포함한 이미지 이름 만들기 (profile.{확장자})
        String originalFilename = avatarImage.getOriginalFilename();
        // queue.png -> fileNameSplit = {"queue", "png"}
        String[] fileNameSplit = originalFilename.split("\\.");
        String extension = fileNameSplit[fileNameSplit.length - 1];
        String profileFilename = "profile." + extension;
        log.info(profileFilename);

        // 2-3. 폴더와 파일 경로를 포함한 이름 만들기
        String profilePath = profileDir + profileFilename;

        // 3. MultipartFile 을 저장하기
        try {
            avatarImage.transferTo(Path.of(profilePath));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        // 4. UserEntity 업데이트 (정적 프로필 이미지를 회수할 수 있는 URL)
        // http://localhost:8080/static/1/profile.png
        log.info(String.format("/static/%d/%s", id, profileFilename));

        UserEntity userEntity = optionalUser.get();
        userEntity.setAvatar(String.format("/static/%d/%s", id, profileFilename));
        return UserDto.fromEntity(repository.save(userEntity));
    }
}

UserController

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService service;

    // POST /user
    // 새 사용자 생성
    @PostMapping
    public UserDto create(@RequestBody UserDto userDto) {
        return service.createUser(userDto);
    }

    // GET /user/{username}
    // 사용자 정보 조회
    @GetMapping("/{username}")
    public UserDto read(@PathVariable("username") String username) {
        return service.readUserByUsername(username);
    }

    // PUT /user/{id}
    // 사용자 정보 수정
    @PutMapping("/{id}")
    public UserDto update(
            @PathVariable("id") Long id,
            @RequestBody UserDto userDto
    ) {
        return service.updateUser(id, userDto);
    }

    // PUT /user/{id}/avatar
    // 사용자 프로필 이미지 설정
    @PutMapping(value = "/{id}/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UserDto avatar(
            @PathVariable("id") Long id,
            @RequestParam("image") MultipartFile avatarImage
    ) {
        return service.updateUserAvatar(id, avatarImage);
    }

//    // Controller 내부에서 지정된 예외가 발생했을 때
//    // 실행하는 메소드에 붙이는 어노테이션
    @ExceptionHandler(IllegalStateException.class)
    public ResponseDto handleIllegalState(IllegalStateException exception) {
        log.error(exception.getMessage());
        ResponseDto response = new ResponseDto();
        response.setMessage("이러저러요리조리한 이유로 에러가 발생했습니다.");
        return response;
    }
}

/static/1/profile.jpg 경로에 이미지가 업로드 된다.

사용자 정보 조회

POST -> GET

사용자 정보 수정

PUT

Exception Handling

ResponseStatusException

스프링 프레임워크에서 제공하는 예외 클래스이며 HTTP 응답의 상태코드와 함께 예외를 생성할 수 있다.

이를 통해 웹 애플리케이션에서 예외가 발생한 경우 클라이언트에 적절한 HTTP 상태 코드를 반환할 수 있다.

  • 별도의 작업없이 간편하게 사용 가능하다.
  • 똑같은 코드를 여러번 반복하게 되는 단점이 존재한다.

종류

throw new ResponseStatusException(); "500 Internal Server Error"
throw new ResponseStatusException(HttpStatus.NOT_FOUND); "404 Not Found"
throw new ResponseStatusException(HttpStatus.BAD_REQUEST); "400 Bad Request"
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED); "501 Not Implemented"

UserControllerAdvice

@Slf4j
@RestControllerAdvice  // @ControllerAdvice + @ResponseBody
// 각 Controller에 나뉘어진 ExceptionHandler 메소드를 모으는 용도
public class UserControllerAdvice {
    // Status400Exception 을 상속받은 모든 예외들에 대하여 400 코드를 발생시킨다.
    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<ResponseDto> handleIllegalState(IllegalStateException exception){
        ResponseDto response = new ResponseDto();
        response.setMessage("UserControllerAdvice에서 처리한 예외입니다.");
        return  ResponseEntity.badRequest().body(response);
    }

    @ExceptionHandler(Status404Exception.class)
    public ResponseEntity<ResponseDto> handle404(Status404Exception exception) {
        ResponseDto response = new ResponseDto();
        response.setMessage(exception.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
}

exceptions package 생성

Status400Exception

package com.example.contents.exceptions;

public class Status400Exception extends RuntimeException {
    public Status400Exception(String message) {
        super(message);
    }
}

Status404Exception

package com.example.contents.exceptions;

public abstract class Status404Exception extends RuntimeException {
    public Status404Exception(String message) {
        super(message);
    }
}

UsernameExistException

package com.example.contents.exceptions;

public class UsernameExistException extends Status400Exception{
    public UsernameExistException() {
        super("username not unique");
    }
}

UserNotFoundException

package com.example.contents.exceptions;

public class UserNotFoundException extends Status404Exception {
    public UserNotFoundException() {
        super("target user not found");
    }
}

같은 username post 시 예외처리 test

@ExceptionHandler(IllegalStateException.class)

@RestControllerAdvice component

커스텀 예외

400Exception

POST 시 username 중복인 경우

404Exception

PUT 시 DB에 id가 없는 경우

출처 : 멋사 5기 백엔드 위키 2팀 회고의 정석

인사이트 타임

원기님의 BFS 알고리즘 특강
https://school.programmers.co.kr/learn/courses/30/lessons/1844

import java.util.LinkedList;
import java.util.Queue;

class Solution {
    //이동 방향을 저장하는 방위 (순서는 상관 없습니다) 
    static int dy[] = {1, -1, 0, 0};
    static int dx[] = {0, 0, -1, 1};

    public int solution(int map[][]) {
        int mapY = map.length;//맵의 세로 길이
        int mapX = map[0].length;//맵의 가로 길이
        Queue<int[]> qu = new LinkedList<>();//BFS의 핵심 QUEUE
        boolean visited[][] = new boolean[mapY][mapX];//방문 여부를 저장하는 배열

        qu.add(new int[] {0, 0, 1});//시작 점 넣고 시작!
        int result = 0;//결과를 저장할 변수

        while (!qu.isEmpty()){
            int current[] = qu.poll();//

            int curY = current[0];//현재 Y좌표
            int curX = current[1];//현재 X 좌표
            int count = current[2];//바닥에 쓰는 수 (현재 좌표까지 오는데 넘어온 칸수)

            //현재 좌표가 방문한 곳인지 다시 확인하기.
            if (visited[curY][curX]) continue;
            visited[curY][curX] = true;

            if (curY == mapY-1 && curX == mapX-1) {//현재 좌표가 목적지에 도착했다면.
                result = count;
                break;
            }

            //4방향 방위 조사 (현재 좌표 기점으로 4방향 조사 시작)
            for (int i = 0; i < 4; i++){
                //방문 조사를 시작할 좌표 구하기
                int ny = curY + dy[i];
                int nx = curX + dx[i];

                if (ny < 0 || ny >= mapY || nx < 0 || nx >= mapX) continue;//맵 경계선을 넘어가는지 검사
                if (map[ny][nx] == 0) continue;//벽인지 검사 (벽이라면 map[ny][nx] 가 0이다)
                if (visited[ny][nx]) continue;//방문한 장소인지 확인

                qu.add(new int[] {ny, nx, count+1});//방문 조사하기 안전한 장소라고 판단되면 qu에 넣는다. 
            }
        }
        if (result == 0) return -1;//result = 0이다? -> 목적지에 도달한 적이 없다! 따라서 return -1
        return result;//결과 리턴
    }
}

원기님이 제작하신 PPT 와 꼼꼼한 설명에 말을 잇지 못 했다. 역시 그는 King.

review

이번 주 내내 힘들었다. 일주일이 이주일같은 느낌이랄까. 멘토님께서 남긴 짤을 보고 순간 쓰러질뻔했다. 포스팅하기 너무 빡세서 임시저장하고 토요일에 마저 작성하고 있는데 확실히 푹 쉬고 나서 천천히 복습하는게 정신건강에 매우 이롭다는걸 느꼈다. 어제는 대체 이게 뭘 배우는건가 뭘 따라치고 있는건가 무슨 소린지 하나도 모르겠다 였는데 오늘 다시 위키를 보며 복습을 해보니 무엇을 배운건지 이해할 수 있었다.

황금같은 주말에 복습이라니 정말 시간이 아깝다는 생각으로 가득하지만 나의 실력은 여전히 형편없다는게 현실이기에 쉬는 시간을 줄이고 마저 스프링 공부를 해야겠다. 이번 주는 정말정말정말 고생많았다.

profile
반갑습니다람지
post-custom-banner

0개의 댓글