Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags more
Archives
Today
Total
관리 메뉴

까마귀코딩.log

백기선 자바스터디 13주차 본문

카테고리 없음

백기선 자바스터디 13주차

까마귀코딩 2023. 2. 3. 18:03
더보기
왜? 라는 생각을 달고 살자!

<< 총 15주차 까지 진행되며 주어지는 키워드를 가지고 블로그에 정리하며 공부하는 스터디 입니다 ! >>


목표

자바의 Input과 Output에 대해 학습하세요.

학습할 것 (필수)

  • 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
  • InputStream과 OutputStream
  • Byte와 Character 스트림
  • 표준 스트림 (System.in, System.out, System.err)
  • 파일 읽고 쓰기

 


스트림 / 버퍼 / 채널 기반의 io


자바에서는 파일이나 콘솔의 입출력을 직접 다루지 않아 !!!! 

뭔소리야

 

파일 입력하고 출력하고 콘솔내용 출력하고 이런거 말하는거고

그럼 뭐로 다뤄 ? 

 

스트림이라는 흐름을 통해 다뤄 !

 

 

 

그럼 

스트림이란 ? 뭐야  ? 

실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미해 ~

 

 

즉, 스트림은 운영체제에 의해 생성되는 가상의 연결고리를 의미하며, 

중간 매개자 역할을 한당!

 

 

 

 

입출력 스트림 

스트림은 한 방향으로만 통신할수 있어 

한 방 향 ! 스 ! 트 ! 림 !

 

입력과 출력을 동시에 처리할 수는 없어 !

 

 

따라서 스트림은 사용목적에 따라

입력스트림과 / 출력스트림으로 구분되용!!

 

자바에서는 java.io 패키지를 통해 inputstream클래스랑 

outputstream 클래스를 제공하고 있고!

 

즉 ~ 자바에서의 스트림 생성이란 이러한 스트림 클래스 타입의 인스턴스를 생성한다는 의미 !!!@

 

 

 

inputstream 클래스에는 read() 메소드,

outputstream 클래스에는 write()메소드가 각각 추상메소드로 포함되어있습니다 !

 

 

사용자는 이 두 메소드를 상황에 맞게 적절히 구현해야만 입출력스트림을 생성하여 사용할수 있습니다!

 

 

read() 메소드는? 

해당 입력 스트림에서 더이상 읽어들일 바이트가 없으면 

== -1을 반환해야해 !

 

그런데 반환타입을 byte타입으로 하면?

0부터 255까지의 바이트 정보는 표현할수 있지만 

-1은 표현할수가 없겠네......

 

따라서 inputstream 의 read() 메소드는 반환타입을 int형으로 선언하고 있어 

 

왜냐면 숫자로 반환하니까 반환타입을 int 로 정해주는거지 

그리고서 다음에 읽어들일 입력스트림을 바이트로 읽어들임

 

보조 스트림 

 

자바에서 제공하는 보조스트림은 실제로 데이터를 주고받을수는 없지만, 

다른 스트림의 기능을 향상시키거나 새로운 기능을 추가해주는 스트림이다 !!!!

 

 

 

문자기반스트림

자바에서 스트림은 기본적으로 바이트단위로 데이터를 전송한다 !

 

하지만 자바에서 가장 작은타입인 char형 ! 요게 2바이트 이므로, 

1바이트씩 전송되는 바이트 기반 스트림으로는 원활한 처리가 힘든경우가 있습니다. 

 

따라서 자바에서는 바이트 기반 스트림뿐만 아니라 문자기반의 스트림도 별도로 제공함 !

 

이러한 문자 기반 스트림은 기존의 바이트 기반 스트림에서

inputstream 을 Reader 로, 

outputsteram 을 Writer 로 변경하면 사용할수 있다 !

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

내가 오늘 기억할 수 있는 일과가 5개 있다고 가정

내 버퍼가 5개가 최대치인데, 2개가 추가로 들어오면

1,2부터 얼른 처리하고 6,7은 대기한다.

 

 

통신에서 Buffer를 쓰는 방법

A와 B가 통신한다고 가정

통신시 A와 B에게 Buffer를 같은 크기로 설정한다.

내가 보내고 싶은 데이터를 Buffer에 적재하고, Buffer를 전송한다.

ex) 하지만 Buffer가 다 안차도 강제로 보낼 수 있다.

Buffer는 꽉차면 자동으로 flush 된다 = AutoFlush (즉 전송)

A는 OutpurBuffer (Write)

B는 inputBuffer (Read)

만약 데이터의 크기가 Buffer보다 크다면

나머지 데이터는 대기한다.

AutoFlush를 실행하고, A 빈 공간을 다시 데이터로 채운다.

하지만 B는 전송받은 데이터로 인해 데이터를 추가로 적재 할 수 없다.

B가 추가 데이터를 적재하기 위해서는 Read 해서 빈공간을 만든다.

남은 데이터를 A에 넣고 강제 flush 해준다.

--> 가변적으로 데이터를 변경할 수 있다는 장점

 

 

BufferedReader 사용법

 

 

입력한 readLine();

InputStream()으로 값을 읽고(바이트 단위),

InputStreamReader(문자 단위)로 읽고, BufferedReader()로 최종적으로 값을 받아옴

 

세줄을 한 줄로 변경

BufferedReader br2 = new BufferedReader(new InputStreamReader(System.in));

read()로 처리한 데이터는 Line단위로 나눠지는데, 공백 단위로 데이터를 가공하려면

readLine()을 사용하면 된다. (Buffer를 비울 때 사용)

전송 받은 데이터를 읽을 때 사용

 

초기화 오류 발생

어떤 값이라도 넣어주면 오류 해결 완료

아까와는 다르게 데이터를 많이 입력해도 그대로 값을 출력한다.

 

 

 

 

readLine을 While(true)문으로 돌리기

하지만 Buffer에 데이터가 없어도 계속 실행되는 오류가 발생한다.

Buffer에 아무값이 없으면 즉 null이 아니면 실행

오류가 발생하면 try/catch 실행

바깥에 try/catch가 생기니까 안에 try/catch는 삭제시켜줌

 

 

 

 

 

BufferedWriter 사용법

B
많은 양을 출력할 때 BufferedWriter를 사용한다.

값을 출력시키고 반드시, flush()로 해당 버퍼를 비워주고, close()로 버퍼를 닫아준다.

.write()에는 Enter기능이 없기 때문에, 줄 개행시 \n을 사용!

 

 

 

 

 

입출력에서 사용되는 버퍼

프로그래밍에서의 버퍼란 어떤것일까요?

프로그래밍이나 운영체제에서 사용하는 버퍼는 거의 대부분 CPU 보조기억장치 사이에서 사용되는 임시 저장 공간을 의미합니다.

 

CPU는 기술이 발전함에 따라 1초에 수십억 bit 그 이상의 데이터를 처리할 수 있습니다.

(수학에 약해 정확한 계산은 하지 않지만 CPU가 크게 나누었을 때 컴퓨터에서 속도가 가장 빠른 장치입니다.)

 

그러나 보조기억 장치의 경우 데이터를 주고 받는데에 많은 시간이 필요합니다.

물리적인 HDD(하드디스크)의 경우 데이터를 전송할 수 있는 속도는 빨라도 초당 100mb ~ 300mb 입니다.

 

 

 

간단하게 얘기 하자면 CPU는 1초에 100개의 데이터를 처리할 수 있지만 정작 처리할 데이터를 가지고 있는 보조 기억 장치는 데이터를 1초에 세 개밖에 보내주지 않는 것입니다. CPU입장에서는 아무리 일을 열심히 하고 싶어도 데이터를 보내주지 않기 때문에 능력에 비해 97 개 만큼 효율성을 잃게 됩니다.

 

 

 

이 때 버퍼를 사용하게 됩니다. 버퍼는 CPU 내부에 있는 캐시메모리 보다는 느리지만 보조 기억 장치보다 훨씬 빠른 주기억 장치(RAM)를 이용합니다. 보조기억장치는 주기억장치의 버퍼로 마련해둔 공간에 데이터를 열심히 보내 쌓아 둡니다. CPU는 처리가 빠르므로 밀려있는 다른일을 처리한 후 시간이 남을때 가끔 버퍼를 확인하여 데이터가 모두 쌓였는지 확인하고 모두 쌓였다면 가져다 한꺼번에 처리합니다.

그 덕분에 CPU는 100퍼센트의 효율로 연산을 할 수 있습니다.

이렇게 버퍼라는 것은 속도차가 큰 두 대상이 입출력을 수행할 때 효율성을 위해 사용하는 임시 저장공간이라고 할 수 있겠습니다.  

 

 

 

 


inputstream outputstream


 

 

 

 

프로그램끼리의 데이터 입출력

항상 프로그램을 기준으로 데이터가 들어오면 입력스트림이고 데이터가 나가면 출력스트림이라고 생각하시면 됩니다. 프로그램이 네트워크상의 다른 프로그램과 데이터를 교환을 하기 위해서는 양쪽 모두 입력 스트림과 출력스트림이 따로 필요합니다. 스트림은 단방향 통신을 한다는 특징이 있으므로 하나의 스트림으로 입출력을 동시에 할 수 없기 때문입니다.

 

 

 

 

 

 

Java.io 패키지

자바의 기본적인 데이터 입출력은 Java.io 패키지에서 제공합니다. java.io 패키지에서는 파일 시스템의 정보를 얻기 위한 File클래스와 데이터를 입출력하기 위한 다양한 입출력 스트림 클래스를 제공합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


byte 스트림 character 스트림


 

 

 

 

 

 

 

 

 

 

 

 


표준스트림 (System.in, System.out, System.err)


그들이 가지고 있는 메서드 중 콘솔에 대한 출력을 하는 println 메서드가 존재하는 것이고,

이것이 우리가 흔히 사용하는 System.out.println의 정체입니다.

 

 

 

InputStream in = System.in;

OutputStream = System.out;

 

System.out.println(in.getClass().getName());

System.out.println(out.getClass().getName());

 

 

 

 

public static void main(String[] args) throws IOException {
InputStream in = System.in;
OutputStream out = System.out;
byte[] readData = new byte[1024];
int readCount = readData.length;

while((readCount = in.read(readData))!= -1) {
out.write(readData, 0 ,readCount);

//readData: 쓸 데이타담고있는버퍼를 말해 (= 버퍼는 스트림에서 배열마니 사용해)
//0, : 어디서부터 ( 범위1)
//readCount:어디까지 쓸거야 ( 범위2) 

}
in.close();
out.close();
}

 

 

 

 

 

 

System.in 필드 : 콘솔로부터 데이터를 입력 받을 때 사용.

 

 

new Scanner(System.in)

sc.next();

Scanner sc = new Scanner(System.in);
String str = sc.next();

공백을 입력하기 전까지 값을 입력받는다.

 

 

 

 


파일 읽고 쓰기


FileInputStream 파일입력 = new FileInputStream ("/gst/git/project.txt");

FIleInputStream 클래스는 파일과 연결한 바이트 스트림 "파일입력"  을 생성한다. 

"파일입력"은 파일로부터 바이너리 값을 그대로 읽어들일 것이다 !

 

byte b[]= new byte[6];
int n =0, c;

while(c = 파일입력.read()) != -1) {

b[n] = (byte)c;
n++;

}

//위의코드는 아래코드 한줄과 같다 !!

파일입력.read(b); // 요거

//파일에서 배열 b[] 의 크기만큼 바이트를 읽는것 !

파일의 끝을 만나면 fin.read()메소드는 -1을 리턴한다. 

 

 

 

 

 

현재 파일 입출력을 스프링으로 하려고 하면 어떻게 해야할까? 

 

저렇게 인풋아웃풋 스트림 안쓴다던데.....

 

 

 

 

 

유저는 브라우저 상에서 렌더링 된 HTML을 통해 파일 업로드를 요청하고, 업로드된 데이터는 HTTP의 Multi-Part로 백엔드 서버로 데이터가 전송됩니다. HTTP의 Multi-Part 요청을 처리하는 애플리케이션이 Springboot로 구현되어 있다면, HTTP의 MultiPart로 전송받은 데이터를 처리하는 로직을 구현해야 합니다. 이번 포스팅에서는 SpringBoot에서 Multi-Part로 전송된 데이터를 처리하는 내용에 대해서 알아보겠습니다.

Springboot Web 용 프로젝트 다운로드

https://start.spring.io/ 에서 Dependencies항목에서 Spring Web과 Thymeleaf(타임리프)를 선택합니다. 나머지 프로젝트 설정은 로컬 PC의 개발환경에 맞추어 설정하시면 됩니다.

Spring Web 프로젝트 다운로드

IntelliJ로 다운로드한 프로젝트를 열어 build.gradle파일을 확인하면, dependecie에 타임리프와 스프링 웹 디펜던시가 추가된 것을 확인할 수 있습니다.

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
   implementation 'org.springframework.boot:spring-boot-starter-web'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

만약 프로젝트 다운로드 및 소스코드 작성 과정을 건너뛰고 싶으시면, 아래 URL(https://github.com/spring-guides/gs-uploading-files)에서 프로젝트를 다운로드하실 수 있습니다.

파일 업로드 테스트용 HTML 화면 구현

SpringBoot에서는 템플릿 엔진을 타임리프(Thymeleaf) 사용을 권장하고 있습니다. 템플릿 엔진에 대해서 잠깐 설명드리면, HTML과 같은 정적 문서에서 동적으로 변하는 변수를 이용해 HTML 문서에 추가하여, 서버 측에서 HTML 파일을 동적으로 변환하여 렌더링 해주는 템플릿 엔진입니다. 즉, HTML 태그 안에 ${변수명}가 있으면, 템플릿 엔진인 타임리프가 ${변수명}과 Springboot에서 정의한 객체와 매핑하여 HTML 문서를 동적으로 생성한다고 이해하시면 됩니다. 타임리프를 활용하여 HTML 코드를 아래와 같이 작성해줍니다.

<html xmlns:th="https://www.thymeleaf.org">
<body>

<div th:if="${message}">
    <h2 th:text="${message}"/>
</div>
<div>
    <form method="POST" enctype="multipart/form-data" action="/">
        <table>
            <tr>
                <td>File to upload:</td>
                <td><input type="file" name="file" /></td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit" value="Upload" /></td>
            </tr>
        </table>
    </form>
</div>
<div>
    <ul>
        <li th:each="file : ${files}">
            <a th:href="${file}" th:text="${file}" />
        </li>
    </ul>
</div>

</body>
</html>


HTML 태그 안에 th:으로 시작하는 문법이 템플릿 엔진인 타임리프 문법입니다. 작성된 HTML은 업로드할 파일을 입력받는 버튼과 이를 업로드하는 전송하는 버튼을 나타냅니다. SpringBoot 코드 작성 없이, 작성된 HTML 문서만 브라우저로 출력하면, 아래와 같은 화면이 출력됩니다. HTML의 태그 중 th:if="{message}는 정의된 내용이 없으므로, 출력되지 않은 것을 확인할 수 있습니다. 즉, HTML에서 타임리프 코드는 무시된 것입니다.

파일 업로드 처리를 위한 Springboot 코드 작성하기

브라우저로부터 업로드할 파일을 요청받으면, 백엔드 서버에서는 HTTP Method 처리 함수, 데이터 로드, 데이터 저장, 파일 입출력 예외처리 로직을 구현해야 합니다. HTTP Request 요청을 처리할 Controller를 작성하고, 그 이후에 파일 저장 및 서버에 저장된 데이터를 로드하는 로직을 Service로 구현해보겠습니다. 먼저 FileUploadController를 com/example/demo/FileUploadController.java에 작성합니다.

package com.example.demo;

import java.io.IOException;
import java.util.stream.Collectors;

import com.example.demo.storage.StorageFileNotFoundException;
import com.example.demo.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;


@Controller
public class FileUploadController {

    private final StorageService storageService;

    @Autowired
    public FileUploadController(StorageService storageService) {
        this.storageService = storageService;
    }

    @GetMapping("/")
    public String listUploadedFiles(Model model) throws IOException {
        model.addAttribute("files", storageService.loadAll().map(
                path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
                        "serveFile", path.getFileName().toString()).build().toUri().toString())
                .collect(Collectors.toList()));
        return "uploadForm";
    }

    @GetMapping("/files/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {

        Resource file = storageService.loadAsResource(filename);
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + file.getFilename() + "\"").body(file);
    }

    @PostMapping("/")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                   RedirectAttributes redirectAttributes) {
        storageService.store(file);
        redirectAttributes.addFlashAttribute("message",
                "You successfully uploaded " + file.getOriginalFilename() + "!");

        return "redirect:/";
    }

    @ExceptionHandler(StorageFileNotFoundException.class)
    public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
        return ResponseEntity.notFound().build();
    }
}

@GetMapping("/") 어노테이션이 달려있는 listUploadedFiles() 메서드는 앞서 작성한 uploadForm.html을 반환하는 코드입니다. 이때, files 속성에 대한 값을 추가하여 HTML에 작성된 files변수의 값을 지정해줍니다. @GetMapping("/files/{filename:+}")가 작성된 serveFiles() 메서드는 서버의 업로드 경로에 저장되어 있는 파일목록을 반환해주는 코드입니다. listUploadedFiles()에서 호출하고 있습니다. @PostMapping("/")은 파일을 업로드 요청을 처리하는 메서드입니다.

위 컨트롤러 클래스 이외에 추가로 작성해야 할 소스코드가 많으므로, 아래 git repostory에서 소스코드를 다운로드 또는 참조 부탁드립니다. 

https://github.com/spring-guides/gs-uploading-files.git

위 리포지토리의 complete/src/main/java/com/example/uploadingfiles 경로에 진입하면, 스토리지 관련 클래스들이 있습니다. 파일 업로드용 클래스에 대한 설명은 아래와 참조 바랍니다.

  • FileUploadController : 브라우저로부터 업로드 화면용 HTML 응답, 업로드 처리를 수행하는 Controller
  • FileSystemStorageService : 업로드 디렉터리 자원 조회, 파일 저장, 삭제를 수행하는 Service
  • StorageException : Storage 오퍼레이션 수행 시 발생하는 Exception을 처리하는 클래스
  • StorageExceptionNotFoundException : 업로드 경로를 찾을 수 없을 때 발생하는 예외를 처리하는 클래스
  • StorageProperties : 파일이 업로드될 경로
  • StorageService : 업로드 디렉터리 파일 입출력 연산을 나타내는 Interface

StorageProperties 클래스 위에 명시된 @ConfigurationProperties는 프로퍼티 설정 값을 Java 코드로 작성한 것을 말합니다. 즉, storage.location = "upload-dir"로 작성한 것과 동일한 의미입니다. FileUploadController 클래스에 StorageService 객체로 파일 IO를 수행하고 있는데 StorageService에는 FileSystemStorageService가 주입되어 동작합니다.

파일 업로드 기능 테스트하기

Main클래스 상단에 @EnableConfigurationProperties를 추가하고, SpringBoot 애플리케이션을 실행해줍니다.

package com.example.demo;

import com.example.demo.storage.StorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class DemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
   }

}

Springboot가 정상적으로 실행된 후, localhost로 접속합니다. port가 이미 사용 중일 경우, port를 아래와 같이 변경해줍니다. 그리고 파일 업로드 제약이 있는데, 이를 변경하고 싶으면 아래와 같이 속성 값을 변경하여 업로드 파일 크기를 변경할 수 있습니다.

server.port = 7080
spring.servlet.multipart.max-file-size=512MB
spring.servlet.multipart.max-request-size=512MB

다음으로 업로드 파일을 저장할 디렉터리를 생성해줍니다. upload-dir은 앞서 StorageProperties에 지정한 경로입니다.

SpringBoot가 정상적으로 실행되고 나면, localhost:7080에 접속합니다. 파일 선택 버튼을 클릭하여, 업로드할 파일을 지정해봅니다. 업로드를 성공하면, 브라우저에서 업로드가 성공하였다는 문구가 출력되고, 업로드 완료된 파일의 최종경로도 브라우저에 출력됩니다.

Multipart를 이용한 파일 업로드 성공 화면

실제 업로드된 파일이 저장되었는지 확인하기 위해선, 이전에 지정한 디렉터리에 업로드 파일이 저장되었는지 확인해봅니다. 업로드 디렉터리에 파일이 추가되어 있다면, 업로드 기능 구현은 완료된 것입니다.

이상으로, SpringBoot에서 Multi-Part를 이용한 업로드 기능 구현에 대해 알아보았습니다.