## 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() 메서드