TIL - 0629

java-ims

파일 업로드 기능 만들기

먼저 자바 파일 IO부터 알아보기

  • 파일 쓰기
    • jvm 데이터를 API를 통해 파일에 쓰기
    • FileOutputStream : 파일 쓰기 API, 생성할 때 경로를 지정하지않으면 해당 프로젝트 루트 경로에 생성됨(절대경로 지정이 아니면 프로젝트 루트 디렉토리를 기준으로 생성 혹은 쓰기)
    • IO를 위한 자원 사용 후 해제까지 해줘야함
    • 데이터 쓰기 : byte 타입(배열까지) - FileStream이라면 byte 단위 쓰기, FileWriter를 사용하면 문자, 문자열 입력가능
    • 덮어쓰기와 붙여쓰기도 고려해야함 : 각 io API마다 덮어쓰기를 할 것인지 붙여쓰기를 할 것인지 정할 수 있음(지원하지않는다면 다른 API로)
public class FileWriter {
    
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream(FILE_NAME);
        fos.write(String.valueOf("안녕하세요").getBytes());
        fos.close();

        FileWriter writer = new FileWriter(FILE_NAME);
        writer.write("안녕!");
        writer.close();
    }
}
  • 파일 읽기
    • API를 통해 파일 데이터를 jvm 메모리로 읽어들임
    • FileInputStream : 바이트 단위로 읽어들이는데, 한꺼번에 정해진 크기만큼 읽기위해서 바이트배열을 인자로 넘김(여러번 파일에 접근하는게 아니라 주어진 버퍼만큼 읽기)
    • FileReader : 문자 단위로 읽음
    • BufferedReader : 문자 단위로 읽으면서 내부적으로 버퍼를 사용해서 라인 단위로 읽을 수 있음, EOF - null(읽어들인 데이터가 없을 때 읽는 것을 종료시키면 됨)
public class FileRead {

    public static void main(String[] args) throws IOException {
        byte[] buffer = new byte[1024];
        FileInputStream fis = new FileInputStream(FILE_NAME);
        int length = fis.read(buffer);
        System.out.println("읽어들인 길이 : " + length);
        System.out.println("데이터 : " + new String(buffer));

        BufferedReader bufferedReader = new BufferedReader(new FileReader(FILE_NAME));
        while (true) {
            String data = bufferedReader.readLine();
            if (data == null) {
                System.out.println("끝");
                break;
            }
            System.out.println("데이터 : " + data);
        }
    }
}
  • Stream이란
    • 입출력을 위한 API : 입출력 통로라고 생각하면 됨, byte와 char 단위 입출력 API가 있음
    • XXXStream : 바이트 단위의 입출력 API
    • Writer/Reader는 XXXStream을 래핑한 io API : 문자, 문자열 단위 읽기 쓰기

업로드된 파일 가져오기, 파일 정보 관리하기

  • form 태그에 의해 요청받은 파일 맵핑 : MultipartFile 클래스 인스턴스로 맵핑
    • 스프링에서 제공하는 API
    • 맵핑되어서 생성된 file에서 정보를 가져올 수 있음
      @PostMapping
      public String upload(MultipartFile file)
      
  • 파일명을 저장해둘 repository를 생성하기
    • 만약 요청받은 파일명과 같은 파일명이 repository에 저장되어있을 경우 어떻게 구별할 것인가 : 일관된 정책으로 저장해야함 - 발급받은 ID(java.util.UUID로 구현)로 파일을 저장하고, 키값을
    • 파일은 서버에 저장해두고, repository에는 파일과 파일을 업로드한 사용자를 식별하는 값을 저장 : 파일은 서버에 같이 저장해둘 수도 있지만 파일 서버에 저장해둘 수 있음(interface로 표준화한 다음 구현체로 각기 다른 save 방법을 제공할 수도 있음)
/* test(acceptance) */
public class AttachmentAcceptanceTest extends AcceptanceTest {
    private static final Logger log = LoggerFactory.getLogger(AttachmentAcceptanceTest.class);
    private static final String UPLOAD_PATH = "/issues/1/attachment";

    private HttpEntity<MultiValueMap<String, Object>> getFileUploadRequest() {
        return HtmlFormDataBuilder.multipartFormData()
                .addParameter("file", new ClassPathResource("logback.xml"))
                .build();
    }

    @Test
    public void upload() {
        ResponseEntity<String> response = requestPost(basicAuthTemplate(), UPLOAD_PATH, getFileUploadRequest());
        assertThat(response.getStatusCode(), is(HttpStatus.FOUND));
    }

    @Test
    public void upload_fail_unAuthentication() {
        ResponseEntity<String> response = requestPost(template(), UPLOAD_PATH, getFileUploadRequest());
        assertEquals("/users/loginForm", getPath(response));
    }

    @Test
    public void download() {
        ResponseEntity<String> response = requestPost(template(), UPLOAD_PATH, getFileUploadRequest());
        assertEquals("/users/loginForm", getPath(response));
    }
}

@Controller
@RequestMapping("/issues/{issueId}/attachment")
public class AttachmentController {

    @Autowired
    private AttachmentService attachmentService;

    @Autowired
    private IssueService issueService;

    @PostMapping
    public String upload(@LoginUser User loginUser, @PathVariable Long issueId, MultipartFile file) throws IOException {
        Issue issue = attachmentService.upload(loginUser, issueService.findById(issueId), file);
        return issue.generateRedirectUri();
    }
}

@Service
public class AttachmentService {

    @Autowired
    private AttachmentRepository attachmentRepo;

    @Resource(name = "localFileSaver")
    private FileSaver fileSaver;

    @Transactional
    public Issue upload(User loginUser, Issue issue, MultipartFile file) throws IOException {
        String savedFileName = fileSaver.save(file, AttachmentNameConverter.convertName(file.getOriginalFilename()));
        Attachment attachment = new Attachment()
                .uploadBy(loginUser)
                .toIssue(issue)
                .setOriginName(file.getOriginalFilename())
                .setManageName(savedFileName);
        attachmentRepo.save(attachment);
        return issue;
    }
}  

public interface FileSaver {
    String save(MultipartFile file, String fileName) throws IOException;
}

@Component(value = "localFileSaver")
public class LocalFileSaver implements FileSaver {
    private static final String PATH = "/Users/imjinbro/Desktop/codesquad/workspace/jwp/java-ims/src/main/resources/upload/";

    @Override
    public String save(MultipartFile file, String fileName) throws IOException {
        File saveFile = new File(PATH + fileName);
        file.transferTo(saveFile);
        return fileName;
    }
}
  • UUID로 생성한 랜덤네임 + 원본네임으로 파일 저장 - Attachment에는 원본과 실제 저장 이름을 설정한 후 저장
  • FileSaver : 로컬에 저장하는 것이 아니라 외부 파일 서버에 저장으로 변경된다면? 다양한 전략이 나올 수 있기때문에 DI하도록 함

서버 아키텍쳐

프론트 프록시

  • 클라이언트를 위한 프록시 : 내부망을 사용한다고 가정했을 때 서버로의 요청 이전에 클라이언트의 요청이 거치는 단계를 하나 만들어놓은 것
    • 얻는 이점 : 캐시 가능(같은 요청에 대해 매번 서버에 요청을 하지않고, 리소스를 캐시해뒀다가 캐시해둔 리소스를 줌 - 통신 비용을 아낌), 외부로의 요청을 제한시킬 수 있음

리버스 프록시

  • 서버를 위한 프록시 서버 : 클라이언트로부터 WAS가 바로 요청을 받는 것이 아니라 앞 단에 프록시 서버를 하나 더 두고 요청을 받도록 하는 것
    • 얻는 이점 : 분산 시스템을 만들 수 있음(WAS를 여러대로 분리해두고 트래픽 분산시킬 수 있음), 같은 요청에 대해 응답 리소스를 캐시해뒀다가 WAS로 요청 위임을 하지않고 캐시한 리소스를 응답해줄 수 있음(서버 부하 분산), WAS 장애가 발생했을 때 클라이언트 요청 처리가능(장애 페이지 띄우기 - static)