본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 26일차 (2023-06-28) 자바 기초 DAY24_자바 프로그래밍_File I/O API를 이용하여 데이터를 바이너리 형식으로 입출력하기_개인프로젝트 - 마트 관리 시스템

by prometedor 2023. 6. 28.
## 27. File I/O API를 이용하여 데이터를 바이너리 형식으로 입출력하기

- FileInputStream/FileOutputStream 사용법
- 바이너리 형식으로 데이터를 입출력하는 방법

 

27번으로 변경 전 App 클래스 리팩토링

App.java

=>

App.java

ㄴ memberList, itemList, boardList, noticeList 와 BreadcrumbPrompt 를 main 메서드 바깥으로 이동시킴

 

App.java

ㄴ App 클래스의 생성자를 생성하고  그 안에 main 안에 속해있던 여러 개의 메뉴(Menu)와 메뉴 리스너(MenuListener)를 생성하도록 설정하는 코드를 이동시킴

 

App.java

ㄴ 생성자 바로 밑에 execute() 메서드를 생성하여 그 안에 main 안에 속해있던 메뉴 출력과 메뉴 실행 부분을 이동시킴

 

App.java

ㄴ 생성자와 메서드로 분리한 App(), execute() 를 main 메서드 안에서 실행하도록 만들어줌

=> App 클래스의 인스턴스 app을 생성하는 순간 App 클래스의 생성자가 실행되고, app 객체를 통해 execute() 메서드를 호출하여 애플리케이션을 실행함

 

App.java

ㄴ createMenu() 라는 메서드를 생성하여 App 클래스의 생성자에 존재하는 코드를 모두 잘라내어 이동시킴

ㄴ createMenu() 메서드는 return 타입이 MenuGroup 이므로 mainMenu 를 리턴해줌

=> 초기 메뉴 구성을 담당하는 메서드임

 

App.java

ㄴ createMenu() 는 메뉴 초기화 메서드이므로 생성자에서 mainMenu 를 초기화해주는 역할을 해줌

ㄴ App 클래스의 인스턴스 필드로 mainMenu 변수를 선언하고, 생성자에서 createMenu() 메서드를 호출하여 초기화함

=> createMenu() 메서드를 생성자에서 호출함으로써 mainMenu 가 원하는 구조로 초기화됨

=> App 클래스 내에서 mainMenu 에 접근하고 필요한 작업 수행 가능

 

App.java

=>

App.java

ㄴ createMenu() 메서드에 있던 MenuGroup 객체 mainMenu 를 "메인" 이라는 이름으로 설정하는 부분을 App 내부로 이동시켜줌

=> 컴파일 시 객체 생성 부분은 생성자 안으로 들어감

=> App 클래스의 인스턴스가 생성될 때 mainMenu 객체가 초기화 됨

 

App.java

ㄴ 해당 코드를 prepareMenu() 로 변경

=>

App.java

ㄴ createMenu 메서드 이름을 prepareMenu 로 변경

=>

App.java

=>

App.java

ㄴ prepareMenu() 메서드를 void 타입으로 변경하여 메뉴를 생성하고 mainMenu 객체에 추가할 수 있으므로 void 타입으로 변경해줌

 

App.java

=>

App.java

ㄴ main 메서드의 위치를 생성자 바로 아래로 변경해줌 => 코드 가독성 높이기

 

App.java

=>

ㄴ 실무에서는 이렇게 더 많이 사용

 

 

LoadData() 메서드 생성

App.java

ㄴ 실행 시 데이터를 로딩할 것이므로 실행 코드를 모아둔 execute() 메서드에 loadData() 메서드 추가

 

saveData() 메서드 생성

App.java

ㄴ 실행 시 데이터를 로딩할 것이므로 실행 코드를 모아둔 execute() 메서드에 saveData() 메서드 추가

 

=> loadData()는 프로그램 실행 전에 이전에 저장된 데이터를 불러오는 작업을 수행

=> saveData()는 프로그램 실행 후에 변경된 데이터를 저장하는 작업을 수행

 

App.java

=>

ㄴ 실행메서드 execute() 메서드는 main 메서드 바로 아래로 이동시킴 => 코드 가독성 높이기

 

saveData() 메서드 작성하기

App.java

ㄴ memberList 에서 한 명씩 Member 타입으로 가져오기

 

App.java

ㄴ FileOutputStream을 생성하여 memberList에 저장된 각 회원 데이터를 파일에 저장

=> FileOutputStream 은 예외처리 필요

=>

App.java

=>

App.java

ㄴ try ~ catch 이용하여 예외처리

ㄴ out.close() => close() 메서드가 존재하므로 작성하기

 

App.java

ㄴ out.write()를 사용하여 no 값을 4바이트로 분할하여 파일에 순서대로 저장

ㄴ out.write(no >> 24); => shift 연산

=> 

ex) 받은 no 값이 00 03 이라면

ㄴ 00 00 00 03 순으로 저장됨

 

App.java

ㄴ getName()을 호출하여 직원의 "이름"을 가져온 후, getBytes("UTF-8")을 사용하여 해당 문자열을 UTF-8 인코딩으로 인코딩한 바이트 배열을 생성함
=> getBytes("UTF-8") 메서드는 문자열을 UTF-8 형식으로 인코딩하여 해당 문자열의 바이트 표현을 반환함

     ㄴ 반환된 바이트 배열은 문자열의 각 문자를 UTF-8으로 인코딩한 결과
ㄴ member.getName().getBytes("UTF-8") => UTF-8로 인코딩된 직원의 이름을 바이트 배열로 얻는 코드

 

App.java

ㄴ UTF-8 인코딩으로 변환된 바이트 배열을 out.write(bytes)를 통해 파일에 기록

 

=> 직원의 "이름", "전화번호", "암호", "직책" 순서로 저장

 

App.java

ㄴ 전화번호, 암호는 이름과 같은 타입이므로 바이트 배열 bytes 재활용

 

App.java

ㄴ 직책은 char 타입이므로 out.write() 메서드를 통해 파일에 쓰기 위해서는 1바이트씩 분리하여 기록해야 함

=> no 의 shift 연산과 같은 연산

 

App.java

ㄴ 출력할 바이트의 개수를 2바이트로 표시해줘야 함 (이름이 몇 바이트인지 알아내기 위함)

=> 데이터를 읽을 때 얼마만큼의 바이트를 읽어와야 하는지를 사전에 알 수 있기 때문

     ㄴ 이를 통해 데이터를 구분하고 추출하는 작업이 용이해짐

 

App.java

ㄴ 전화번호, 암호는 이름과 같은 타입이므로 이름과 똑같이 작성해줌

 

App.java

=> 저장할 데이터의 개수를 미리 알려주는 것은 데이터의 구조를 정의하고 읽기를 용이하게 하므로 가장 먼저 실행해주기

 

App 실행하여 member 등록해보기

=>

생성된 member.data 파일 확인

ㄴ member.data 파일이 생성된 것을 확인할 수 있음

=> 맨 앞에 00 03 을 통해 3명의 데이터가 있음을 확인

=> 바로 다음에 나오는 00 00 00 01 을 통해 첫번째 직원의 데이터임을 확인

=> 그 다음에 나오는 00 03 을 통해 저장된 member 의 첫 번째 값인 이름이 3글자임을 확인

=> 그 다음에 나오는 61 61 61 을 통해 이름이 a a a 임을 확인

=> 그 다음에 나오는 00 0B 를 통해 저장된member 의 두 번째 값인 전화번호가 11글자임을 확인

=> 그 다음에 나오는 30 31 30 31 31 31 31 32 32 32 32 를 통해 전화번호가 0 1 0 1 1 1 1 2 2 2 2 임을 확인

=> 그 다음에 나오는 00 04 를 통해 저장된 member 의 세 번째 값인 암호가 4글자임을 확인

=> 그 다음에 나오는 31 31 31 31 을 통해 1 1 1 1 

=> 그 다음에 나오는 00 30 을 통해 저장된 member 의 네 번째 값인 직책이 0(관리자)임을 확인

=> 그 다음에 나오는 00 00 00 02 를 통해 3명 중 두 번째 직원의 데이터임을 확인

이렇게 계속 반복됨

 

=>

 

LoadData() 메서드 작성하기

App.java

=>

App.java

ㄴ try ~ catch 이용하여 예외처리

ㄴ out.close() => close() 메서드가 존재하므로 작성하기

 

App.java

ㄴ 읽은 두 개의 1바이트를 조합하여 16비트 크기인 size 변수에 저장

ㄴ 첫 번째 in.read() 호출은 파일에서 1바이트를 읽어 size 변수에 저장(size 변수에는 하위 8비트(1바이트)가 저장됨)
ㄴ 두 번째 in.read() 호출은 파일에서 다음 1바이트를 읽어옴(size 변수의 값을 왼쪽으로 8비트(shift) 이동시킨 후, 두 번째 in.read()에서 읽은 값과 논리 OR(|) 연산을 수행하여 두 개의 바이트를 조합함)

=> size 변수에는 파일에서 읽어온 2바이트 크기의 데이터가 저장됨

 

=>

size 변수 부분 코드 변경

App.java

 

App.java

ㄴ 각 회원 정보가 4바이트 크기의 no 값을 가지고 있으므로 in.read() 메서드를 size 만큼 호출하여 4개의 1바이트 값을 읽어와서 no 변수에 설정함

ㄴ 각각의 1바이트 값을 왼쪽으로 시프트 한 후 비트 OR 연산을 수행하여 32비트 크기의 no 값을 구성함

     ㄴ 이 no 값을 member.setNo() 메서드를 사용하여 Member 객체에 설정함
=> 파일에서 읽은 회원 정보의 개수(size)만큼 반복하면서 파일에서 no 값을 읽어와 Member 객체에 설정

 

=> 위 코드는 아래처럼 한 줄로 변경 가능

 

App.java

ㄴ 변수 no 를 생성할 필요 없이 setNo() 메서드 안에 넣어줌

 

App.java

ㄴ buf 라는 byte[] 배열을 생성하여 배열의 크기를 넉넉하게 잡아줌

 

App.java

ㄴ in.read(buf) 코드는 파일에서 읽은 데이터를 주어진 바이트 배열 buf에 저장하고, 실제로 읽은 바이트 수를 반환하므로 count 변수에는 실제로 읽은 바이트 수가 저장됨

=> 이때, buf 배열의 크기는 읽을 데이터의 크기에 맞게 충분히 설정되어야 함

      ㄴ 만약 buf 배열의 크기보다 읽을 데이터의 크기가 더 크다면, buf 배열은 최대 크기까지만 데이터를 저장하고 나머지 데이터는 버려짐
=> in.read(buf)를 호출하여 파일에서 바이트 배열의 데이터를 읽을 때는, buf 배열의 크기와 읽을 데이터의 크기를 적절히 처리해야 함

ㄴ new String(buf, 0, count, "UTF-8")를 통해 buf 배열의 바이트 값을 문자열로 변환 (setName() 메서드가 아규먼트를 String 으로 받아야되므로)

    ㄴ buf 는 바이트 배열, 0은 시작 인덱스, count는 배열에서 읽을 바이트 개수, "UTF-8" 은 UTF-8 로 인코딩하라는 의미

    ㄴ new String() 은 문자열로 변환하는 메서드
=> buf 배열에서 0부터 count까지의 바이트를 UTF-8로 인코딩하여 문자열로 변환합니다.
ㄴ member.setName() 메서드를 사용하여 Member 객체 member에 이름을 설정

 

App.java

ㄴ length는 이름을 저장할 바이트 배열의 길이를 나타냄 (count 대신 length 사용)

ㄴ 파일에서 읽은 이름의 바이트 배열은 해당 길이만큼의 크기를 가짐
ㄴ length는 파일에서 읽은 2바이트 데이터를 이용하여 계산됨

ㄴ in.read() << 8 | in.read()는 파일에서 2바이트를 읽어서 16진수 형태로 비트 연산을 수행하여 길이값을 얻어냄

=> 이렇게 얻어진 길이값은 이름을 저장할 바이트 배열의 크기가 되며, 이후에 in.read(buf, 0, length)를 통해 해당 크기만큼의 데이터를 파일에서 읽어와 buf 배열에 저장

ㄴ 전화번호, 암호는 이름과 같은 타입이므로 이름과 똑같이 작성해줌

 

App.java

ㄴ 파일에서 읽어온 2바이트 데이터를 char 타입으로 변환하여 Member 객체의 position에 설정

ㄴ in.read() << 8 | in.read()를 통해 파일에서 2바이트 데이터를 읽어와 position 값으로 설정함

=> in.read() << 8은 첫 번째 바이트를 8비트 왼쪽 시프트한 값을 나타내고, in.read()는 두 번째 바이트를 읽어옴

=> 이 두 값을 | (비트 OR) 연산자를 사용하여 조합하여 2바이트로 만듦
ㄴ (char) 캐스트를 사용하여 2바이트 값을 char 타입으로 변환하고, 이를 Member 객체의 position 필드에 설정함

 

App.java

ㄴ memberList 에 member.data 파일에서 저장된 회원 데이터를 읽어와 생성한 Member 객체의 필드에 값을 설정한 후 memberList 에 추가 함

 

App.java

ㄴ 맨 아래쪽에 있는 printTitle() 메서드를 main 메서드 바로 밑에 위치시켜줌 => 가독성을 높임

 

App.java

ㄴ loadMember() 메서드를 생성하고 loadData() 에 있던 내용을 모두 잘라내어 추가

ㄴ loadData() 메서드에는 loadMember() 메서드를 실행하도록 추가

 

App.java

ㄴ saveMember() 메서드를 생성하고 saveData() 에 있던 내용을 모두 잘라내어 추가

ㄴ saveData() 메서드에는 saveMember() 메서드를 실행하도록 추가

 

=> 나머지 item() 과 Board(), Notice() 도 같은 방법으로 추가해주면 됨

 

App.java

 

 

Board 에서 CreatedDate

loadBoard() 메서드

 

saveBoard() 메서드