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)