Java프로젝트하면서 문자열 특화 입출력인 Reader/Writer클래스 사용하기 및 CUI 프로그래밍
자바 입출력
지금까지는 프로그램을 종료시킬 때마다 주소록에 입력되었던 내용이 저장되지 않고 모두 사라졌습니다.
그 이유는 주소록에 입력된 데이터들이 RAM에서만 임시로 저장되어 있다가 프로그램이 종료되면서 RAM에서 프로그램이 사용하던 공간을 반납하면서 저장된 데이터도 날라가기 때문입니다.
그래서 프로그램이 종료된 후에도 다시 프로그램을 실행시킬 때 기존에 입력했던 데이터를 다시 불러올 수 있도록(load) 주소록의 데이터를 보조기억장치의 외부파일에 저장하는(save) 메소드를 구현합니다.
save 코드
//AddressBook에 있는 Personal객체 정보를 외부파일에 저장하기
public void save()
{
//해당위치에 있는 data를 쓸 File객체를 생성한다.
File file = new File("AddressBook.txt");
//출력을 위한 스트림을 생성한다.
try(Writer writer = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(writer);){
//AddressBook에 저장된 Personal 객체의 수만큼 반복한다.
int i = 0;
Personal personal = null;//AddressBook의 personal을 저장하기 위한 임시공간
while(i < this.personals.size())
{
//AddressBook에서 Personal객체를 구한다.
personal = this.personals.get(i);
//personal 단위로 외부에 출력한다.
bufferedWriter.write(new String(personal.getName() + "," +
personal.getAddress() + "," + personal.getTelephoneNumber()
+ "," + personal.getEmailAddress() + "\n"));
i++;
}
bufferedWriter.flush();
} catch (IOException e) {e.printStackTrace();}
}
save 코드 설명
주소록(AddressBook) 객체에 저장된 ArrayList<Personal>을 Personal단위로 외부의 텍스트파일에 저장하기 위해 save메소드를 설계합니다.
이 때, 저장규칙은 Personal의 필드 구분은 콤마(,)로 하고, 마지막 필드에는 콤마를 입력하지 않습니다.
(Ex. 홍길동,서울시 중구,023348594,Hong@naver.com)
그리고 Personal을 단위로 하여 한줄씩 출력합니다.
즉, Personal 한 객체당 한 줄이고 다음 Personal객체의 정보는 다음 줄에 출력합니다.
이렇게 미리 규칙을 정하는 이유는 나중에 저장된 외부 텍스트파일에서 데이터를 읽어와서 AddressBook의 ArrayList<Personal>에 저장하기 위해서(load메소드) 외부텍스트파일을 읽는 규칙이 필요하기 때문입니다.
외부에 데이터를 저장할 때는 문자열 출력을 위해 특화된 Writer클래스를 사용합니다.
먼저 해당텍스트파일의 상대경로를 매개변수로 입력하여 File클래스 생성자를 이용해 File객체를 생성합니다.
출력의 경우 파일이 해당 위치에 없으면 자동으로 생성해주기 때문에 따로 파일의 존재여부를 확인할 필요가 없습니다.
이 후에 이 file객체를 매개변수로 하여 추상클래스인 Writer의 자식인 FileWriter생성자를 이용해 Writer객체를 생성합니다.
이 후에 이 출력의 속도를 향상시켜줄 필터 개념인 BufferedWriter 클래스를 생성합니다.
둘 다 Try-with-resocures를 이용해 자원을 쉽게 해제할 수 있도록 합니다.
Try-with-resocures에 대해 짧게 설명하자면 try()의 소괄호 안에 해당 클래스들을 넣음으로써 try catch 구문이 끝날 때, 자동으로 해당클래스들의 자원을 해제시켜줍니다.
Writer나 BufferedWriter의 경우 사용했으면 사용이 끝난 후에는 반드시 프로그래머가 직접 close()해서 자원을 반납해야 합니다. 그러려면 코드가 중복되고 길어지고, 일일이 확인을 해야 하는데, try()소괄호 안에 넣음으로써 try catch 구문이 끝날 때 프로그램이 자동으로 close를 해줍니다.
대신 이 try()소괄호 안에 들어가는 객체들의 클래스는 AutoCloseable 인터페이스를 구현해야만 합니다.
try구문의 내부에서는 BufferedWriter의 write 메소드를 통해 주소록에 저장된 Personal객체 수만큼 반복을 돌리면서 외부텍스트파일에 출력을 합니다.
load 코드
//외부 파일에 있는 정보를 읽어서 AddressBook에 Load하기
public int load()
{
//해당위치에 있는 data를 읽어 File객체를 생성한다.
File file = new File("AddressBook.txt");
//해당위치에 파일이 존재하면
if(file.exists() == true)
{
//입력을 위한 스트림을 생성한다.
try (Reader reader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(reader);) {
Personal personal;
//줄단위(Personal 객체단위)로 읽는다.
int beginIndex = 0;//시작위치를 저장할 임시공간
int endIndex = 0;//콤마(,)위치를 저장할 임시공간(,단위로 필드가 구분됨)
String personalInformation;//한줄단위로 읽은 데이터를 저장할 임시공간
String name;//한줄단위로 읽은 데이터 중에서 이름을 저장할 임시공간
String address;//한줄단위로 읽은 데이터 중에서 주소를 저장할 임시공간
String telephoneNumber;//한줄단위로 읽은 데이터 중에서 전화번호를 저장할 임시공간
String emailAddress;//한줄단위로 읽은 데이터 중에서 이메일주소를 저장할 임시공간
while((personalInformation = bufferedReader.readLine()) != null)
{
//시작위치를 초기화한다.
beginIndex = 0;
//personalInformaion에서 처음부터 시작해서 ","의 위치를 찾는다.
endIndex = personalInformation.indexOf(",");
//name 문자열을 구한다.
name = personalInformation.substring(beginIndex, endIndex);
//","위치 다음을 시작위치로 재설정한다.
beginIndex = endIndex + 1;
//재설정한 시작위치부터 다음 ","위치를 찾는다.
endIndex = personalInformation.indexOf(",", beginIndex);
//address 문자열을 구한다.
address = personalInformation.substring(beginIndex, endIndex);
//","위치 다음을 시작위치로 재설정한다.
beginIndex = endIndex + 1;
//재설정한 시작위치부터 다음 ","위치를 찾는다.
endIndex = personalInformation.indexOf(",", beginIndex);
//telephone 문자열을 구한다.
telephoneNumber = personalInformation.substring(beginIndex, endIndex);
//","위치 다음을 시작위치로 재설정한다.
beginIndex = endIndex + 1;
//emailAddress 문자열을 구한다.
//(더이상 ,가 없고 마지막 문자열이므로 endIndex필요없음)
emailAddress = personalInformation.substring(beginIndex);
//새로운 Personal객체를 생성한다.
personal = new Personal(name, address , telephoneNumber, emailAddress);
//새로 생성한 Personal객체를 ArrayList<Personal>에 추가한다.
this.personals.add(personal);
}
} catch (IOException e) {e.printStackTrace();}
}
//load한 Personal객체 수를 반환한다.
return this.personals.size();
}
load 코드 설명
위에서 구현한 save메소드를 통해 프로그램이 종료되기 전에 save메소드 호출을 하여 외부텍스트파일에 AddressBook의 Personal객체들에 대한 정보가 저장할 것입니다.
이제 프로그램을 다시 실행시킬 때 외부텍스트파일로부터 이 데이터를 읽어 오기 위해 load메소드를 구현합니다.
구현 시 규칙은 한 줄에는 한 Personal이고, 콤마(,)단위로 필드가 구분되어 있다는 가정 하에서 텍스트를 읽습니다.
읽은 데이터를 바탕으로 Personal객체를 새로 생성한 뒤에 이를 ArrayList<Personal>에 추가합니다.
구체적으로 설명하자면 우선, 상대경로를 매개변수로 가지는 File생성자를 통해 File객체를 생성합니다.
여기서 주의할 점이 File객체는 해당경로에 파일의 존재 여부와는 상관없이 생성됩니다.
이 후에 입력의 경우 해당경로에 파일이 없으면 FileNotFoundException이 발생하기 때문에 파일이 존재하는지 여부를 먼저 확인해야 합니다.
파일 존재 여부를 확인하기 위해 File객체의 메소드인 exists를 활용하여, true이면 입력을 위한 다음 과정을 거치도록 하고, false이면 그대로 메소드가 종료되도록 합니다.
파일이 존재하면 텍스트 입력에 특화되어 있는 Reader클래스를 생성합니다.
이 후 입력 속도를 증가시켜주는 필터개념인 BufferedReader클래스를 생성합니다.
이 클래스들의 객체들 역시 Try-with-resources를 이용하여 자동으로 자원을 해제시켜주도록 합니다.
Save에서 한 줄 단위로 한 Personal객체, 필드는 콤마(,)단위로 규칙을 정해서 저장하도록 했기 때문에 이 규칙대로 외부텍스트 파일에서 데이터를 읽어오면 됩니다.
Personal객체를 임시 저장할 공간, name, address, telephoneNumber, emailAddress를 임시 저장할 공간 등을 마련해둡니다.
그리고 BufferedReader클래스에서는 한 줄 단위로 읽은 데이터를 문자열로 반환하고, 파일에 더이상 읽은 데이터가 없을 때 null을 반환하는 readLine()이라는 메소드가 있는데 이를 통해 외부텍스트파일의 마지막(null)이 아닌 동안 반복구조를 돌립니다.
외부텍스트파일로부터 한 줄 단위로 읽은 문자열(String)personalInformation의 indexOf(“,”)를 통해 처음부터 시작해서 콤마의 위치를 구합니다.
beginIndex는 처음에는 0이고, endIndex가 바로 콤마의 위치입니다.
이제 이 위치들을 활용해 substring(beginIndex, endIndex)을 이용해 name을 추출합니다.(beginIndex는 포함, endIndex - 1까지 포함)
이 후 beginIndex를 처음 찾았던 콤마 다음 위치로 이동시키고,(beginIndex = endIndex + 1) 다시 여기서부터 다음 콤마의 위치를 찾습니다.(indexOf(“,”, beginIndex))
이 후 다시 substring(beginIndex, endIndex)을 통해 address를 추출합니다.
telephoneNumber까지는 동일한 방식으로 추출합니다.
emailAddress의 경우 마지막 필드이기 때문에 뒤에 더이상 콤마가 없습니다.
그래서 별도로 콤마를 찾지 않습니다.
또한 emailAddress가 문자열의 마지막이기 때문에 endIndex가 필요없습니다.
그래서 substring에 beginIndex만 매개변수로 입력하면 emailAddress의 문자열을 구할 수 있습니다.
이렇게 추출한 name, address , telephoneNumber, emailAddress를 이용하여 새로운 Personal객체를 생성하고, 이를 ArrayList<Personal>에 추가합니다.
파일의 데이터가 끝날 때까지(readLine에서 null이 반환될때까지) 반복하다가 파일이 끝나면 추가된 데이터의 개수를 반환하면서 load메소드는 종료됩니다.
load 코드 개선하기
현재 load코드는 substring을 사용하여 문자열을 분리하고 있어서 굉장히 코드 수가 길고 복잡합니다.
이를 해결 하기 위해 2가지 방법이 있습니다.
String의 split메소드 사용하기
//외부 파일에 있는 정보를 읽어서 AddressBook에 Load하기(split사용)
public int load()
{
//해당위치에 있는 data를 읽어 File객체를 생성한다.
File file = new File("AddressBook.txt");
//해당위치에 파일이 존재하면
if(file.exists() == true)
{
//입력을 위한 스트림을 생성한다.
try (Reader reader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(reader);) {
Personal personal;
//줄단위(Personal 객체단위)로 읽는다.
String personalInformation;//한줄단위로 읽은 데이터를 저장할 임시공간
String[] tokens = null;//분리된 문자열들을 저장할 배열
//줄단위(Personal 객체단위)로 파일의 끝까지 읽는다.
while((personalInformation = bufferedReader.readLine()) != null)
{
//외부파일에서 읽은 한 줄 데이터를 콤마(,)기준으로 분리한다.
tokens = personalInformation.split(",");
//콤마(,)기준으로 분리한 데이터가 4개이면
if(tokens.length == 4)
{
//새로운 Personal객체를 생성한다.
personal = new Personal(tokens[0], tokens[1], tokens[2], tokens[3]);
//새로 생성한 Personal객체를 ArrayList<Personal>에 추가한다.
this.personals.add(personal);
}
}
} catch (IOException e) {e.printStackTrace();}
}
//load한 Personal객체 수를 반환한다.
return this.personals.size();
}
String클래스의 split메소드를 이용하여 매개변수로 구분할 기준에 콤마(,)를 입력합니다.
그럼 반환값으로 콤마(,)를 기준으로 분리된 문자열 배열이 반환됩니다.
외부데이터가 save될 떄 정상적으로 save되었다면 현재 문자열 배열의 개수는 4일 것이고,
이 개수를 확인하여 정상이면 각 배열요소(tokens[0], tokens[1], tokens[2], tokens[3])들을 매개변수로 하는
Personal 생성자를 이용해 Personal객체를 생성한 다음에 이를 ArrayList에 add해줍니다.
보시다시피 substring을 사용하는 것보다 코드양이 확 줄어든 것을 알 수 있습니다.
StringTokenizer 클래스 이용하기
//외부 파일에 있는 정보를 읽어서 AddressBook에 Load하기(StringTokenizer사용하기)
public int load()
{
//해당위치에 있는 data를 읽어 File객체를 생성한다.
File file = new File("AddressBook.txt");
//해당위치에 파일이 존재하면
if(file.exists() == true)
{
//입력을 위한 스트림을 생성한다.
try (Reader reader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(reader);) {
Personal personal;
//줄단위(Personal 객체단위)로 읽는다.
String personalInformation;//한줄단위로 읽은 데이터를 저장할 임시공간
StringTokenizer tokens = null;//분리된 문자열들을 저장할 배열
//줄단위(Personal 객체단위)로 파일의 끝까지 읽는다.
while((personalInformation = bufferedReader.readLine()) != null)
{
//외부파일에서 읽은 한 줄 데이터를 콤마(,)기준으로 분리한다.
tokens = new StringTokenizer(personalInformation, ",");
//콤마(,)기준으로 분리한 데이터가 4개가 맞으면
if(tokens.countTokens() == 4)
{
//새로운 Personal객체를 생성한다.
personal = new Personal(tokens.nextToken(), tokens.nextToken(),
tokens.nextToken(), tokens.nextToken());
//새로 생성한 Personal객체를 ArrayList<Personal>에 추가한다.
this.personals.add(personal);
}
}
} catch (IOException e) {e.printStackTrace();}
}
//load한 Personal객체 수를 반환한다.
return this.personals.size();
}
StringTokenizer클래스의 객체를 생성할 때 매개변수로 분리할 문자열인 personalInformation과
분리할 기준인 콤마(,)를 매개변수로 입력합니다.
그러면 반환값에는 personalInformation이 콤마(,)기준으로 분리되어 저장될 것입니다.
외부 파일이 정상적으로 save되었다면 countTokens메소드의 반환값이 4일텐데 이를 확인하여 정상이면
Personal객체를 생성합니다.
Personal객체를 생성할 때 매개변수로 StringTokenizer클래스의 nextToken메소드를 이용합니다.
처음 nextToken메소드를 호출하면 반환값이 name일 것이고, 다음에 nextToken를 호출하면 address가 반환될 것입니다.
이런식으로 nextToken메소드를 매개변수로 4번 입력하여 Personal객체를 생성한 다음 이를 ArrayList에 add합니다.
이 방식 역시 substring에 비하면 획기적으로 코드를 줄일 수 있습니다.
CUI 프로그래밍
이제 load와 save 메소드를 구현했으니 이 메소드들이 잘돌아가는지 확인하기 위하여 CUI(Character User Interface)프로그래밍을 구현합니다.
이 때 사용하는 AddressBook에 대한 메소드들(record, getAt, find, correct, erase, arrange)은 아래 링크를 참고하여 먼저 확인해보신 후에 읽으시면 이해가 빠를 것입니다.
record부터 설명하여 아래로 내려보시면 순차적으로 메소드들의 코드와 설명이 있습니다.
AddressBook 메소드 설명
CUI의 main메소드 코드
package AddressBook;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
public class Main
{
public static void main(String[] args) throws IOException
{
//메인테스트 시나리오
//1. AddressBook객체를 생성한다.
AddressBook addressBook = new AddressBook();
//2. 외부파일이 있으면 외부파일에 있는 Personal 객체정보를 load한다.
addressBook.load();
//3. 메뉴화면을 출력한다.
displayMenu();
//4. 콘솔창에서 menu 번호를 입력을 받는다.
Scanner scanner = new Scanner(System.in);
int menuNumber = scanner.nextInt();
//5. 입력한 menu 번호로 이동한다.
while (menuNumber != 0)
{
switch (menuNumber)
{
case 1: formFoRecording(addressBook); break;
case 2: formFoFinding(addressBook); break;
case 3: formFoCorrecting(addressBook); break;
case 4: formFoErasing(addressBook); break;
case 5: formFoArranging(addressBook); break;
case 6: formFoViewingAll(addressBook); break;
default: break;
}
displayMenu();
menuNumber = scanner.nextInt();
}
addressBook.save();
}
}
CUI의 main메소드 코드 설명
우선 main메소드가 시작되면 AddressBook 객체를 생성한 뒤에 load메소드를 통해 입력받을 외부데이터가 있으면 불러옵니다.
이 후에 콘솔창에 메뉴화면을 출력시켜주는 메소드인 displayMenu를 호출합니다.
콘솔창에서 사용자로부터 번호를 입력받기 위해 Scanner클래스를 생성한 다음 nextInt메소드를 이용해 사용자로부터 숫자를 입력받습니다.
while반복을 통해 사용자로부터 받은 번호가 0이 아닌동안 반복을 합니다.
즉, 사용자가 프로그램을 종료시키고 싶으면 0번을 누르고 그 이외에 숫자를 눌렀을 때는 프로그램이 종료되지 않습니다.
사용자가 1번을 누르면 기재하기화면(formFoRecording)을 출력하고, 2번을 누르면 찾기화면(formFoFinding), 3번을 누르면 수정하기화면(formFoCorrecting), 4번을 누르면 지우기화면(formFoErasing), 5번을 누르면 정리하기화면(formFoArranging), 6번을 누르면 전체보기화면(formFoViewingAll)을 출력합니다.
이후 각 화면을 콘솔창에 출력한 다음에 각 화면이 종료되고 나면 다시 disPlayMenu함수를 호출해 메뉴화면을 출력한 다음 사용자로부터 원하는 화면으로 이동하기 위한 번호를 입력받는 반복을 합니다.
displayMenu 코드
public class Main
{
public static void displayMenu()
{
System.out.printf("\t주소록\n");
System.out.printf("\t====================\n");
System.out.printf("\t[1] 기재하기\n");
System.out.printf("\t[2] 찾 기\n");
System.out.printf("\t[3] 고 치 기\n");
System.out.printf("\t[4] 지 우 기\n");
System.out.printf("\t[5] 정리하기\n");
System.out.printf("\t[6] 전체보기\n");
System.out.printf("\t[0] 끝 내 기\n");
System.out.printf("\t--------------------\n");
System.out.printf("\t메뉴를 선택하십시오! ");
}
}
이 코드를 입력하면 콘솔창에는 다음과 같이 출력됩니다.
displayMenu 결과
이렇게 콘솔창에 메뉴화면을 출력하면 displayMenu메소드는 종료되고, main으로 돌아가서 사용자로부터 번호를 받습니다.
즉, displayMenu는 실제 기능은 없고, 콘솔화면에 메뉴창을 출력해주는 것이 전부입니다.
실제로 사용자로부터 번호를 입력받고, 입력 받은 번호화면으로 이동시켜주는 것은 전부 main에서 처리합니다.
순차적으로 이동한다고 가정했을 때, 일단 입력을 해야 다른 메뉴들이 의미가 있기 때문에 먼저 기재하기 창으로 이동합니다.
그러려면 아래 이미지처럼 1번을 입력하고, 엔터를 칩니다.
formFoRecording 코드
public class Main
{
//[1] 기재하기
public static void formFoRecording(AddressBook addressBook) throws IOException
{
Scanner scanner = new Scanner(System.in);
String going = "Y";
while (going.equals("Y") == true || going.equals("y") == true)
{
System.out.println();
System.out.printf("\t주소록 - 기재하기\n");
System.out.printf("\t==============================================\n");
System.out.printf("\t성 명 : ");
String name = scanner.nextLine();
System.out.printf("\t주 소 : ");
String address = scanner.nextLine();
System.out.printf("\t전화 번호 : ");
String telephoneNumber = scanner.nextLine();
System.out.printf("\t이메일주소 : ");
String emailAddress = scanner.nextLine();
System.out.printf("\t----------------------------------------------\n");
System.out.printf("\t기재하시겠습니까?(Y/N) ");
String recording = scanner.nextLine();
if (recording.equals("Y") == true || recording.equals("y") == true)
{
int index = addressBook.record(name, address, telephoneNumber, emailAddress);
System.out.printf("\t========================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t------------------------------------------------------------------------\n");
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index+1,
addressBook.getPersonals().get(index).getName(),
addressBook.getPersonals().get(index).getAddress(),
addressBook.getPersonals().get(index).getTelephoneNumber(),
addressBook.getPersonals().get(index).getEmailAddress());
}
System.out.printf("\t============================================================================\n");
System.out.printf("\t계속하시겠습니까?(Y/N) ");
going = scanner.nextLine();
}
}
}
formFoRecording 코드 설명
콘솔창에 입력을 받기 위해 Scanner클래스를 생성한 뒤에 (String)going은 “Y”를 초기값으로 설정합니다.
going이 “Y” 또는 “y”(사용자가 대문자로 Y를 입력하든, 소문자로 y를 입력하든 상관없이 반복구조에 들어가도록 하기 위해서)인 동안 반복구조를 돌기 때문에 일단 초기값을 “Y”로 설정하여 이 반복구조에 들어갈 수 있도록 합니다.
Scanner클래스의 nextLine을 만나면 입력을 받기 전까지 프로그램이 정지되기 때문에 처음으로 성명을 입력받기 전까지는 콘솔창에 출력되고 나머지는 출력되지 않습니다.
이 후 여기에 성명을 입력하고 엔터키를 치면 다음 입력란인 “주소 : “가 출력됩니다.
주소를 입력한 뒤에 엔터키를 치면 “전화번호 : “가 출력됩니다.
전화번호를 입력한 뒤에 엔터키를 치면 “이메일주소 : “가 출력됩니다.
이런식으로 이메일까지 입력을 마친 다음에 엔터키를 치면 “기재하시겠습니까?”를 묻는 텍스트가 출력됩니다.
String recording = scanner.nextLine();을 통해 “Y” 또는 “y”를 입력 받으면 formFoRecording메소드에서 매개변수로 입력 받은 addressBook의 record메소드를 호출합니다.
record 메소드의 매개변수로 콘솔창에서 사용자로부터 입력 받은 name, address, telephoneNumber, emailAddress를 입력합니다.
그리고 사용자로부터 입력 받은 Personal객체정보를 다시 한번 콘솔창에 출력한 다음에 계속 기재하기를 진행할 지 여부를 묻습니다.
여기서 “Y” 또는 “y”를 누르면 반복구조의 조건이 성립하기 때문에 이때까지 했던 과정을 다시 진행하고, 이외의 키를 누르면 다시 메뉴화면으로 돌아갑니다.
String recording = scanner.nextLine();을 통해 “Y”또는 “y”이외의 키를 입력받으면 기재하지 않는다는 뜻이기 때문에 addressBook의 record메소드를 호출하지 않고, 바로 계속 기재하기를 진행할지 여부를 묻습니다.
여기서도 “Y” 또는 “y”이외의 키를 누르면 제일 처음 시작했던 while반복구조에서 빠져나오기 때문에 formFoRecoring메소드가 종료되고, 다시 main메소드로 돌아갑니다.
formFoFinding 코드
public class Main
{
//[2] 찾기
public static void formFoFinding(AddressBook addressBook)
{
Scanner scanner = new Scanner(System.in);
int index = 0;
String going = "Y";
while (going.equals("Y") == true || going.equals("y") == true)
{
System.out.println();
System.out.printf("\t주소록 - 찾기\n");
System.out.printf("\t================================================================================\n");
System.out.printf("\t성명 : ");
String name = scanner.nextLine();
System.out.printf("\t================================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t--------------------------------------------------------------------------------\n");
ArrayList<Integer> indexes = addressBook.find(name);
index = 0;
while (index < indexes.size())
{
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index + 1,
addressBook.getPersonals().get(indexes.get(index)).getName(),
addressBook.getPersonals().get(indexes.get(index)).getAddress(),
addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber(),
addressBook.getPersonals().get(indexes.get(index)).getEmailAddress());
index++;
}
System.out.printf("\t================================================================================\n");
System.out.printf("\t계속하시겠습니까?(Y/N) ");
going = scanner.nextLine();
}
}
}
formFoFinding 코드 설명
formFoFinding도 formFoRecoding과 원리는 같습니다.
다른 점이라면 성명을 입력받아서 이를 통해 addressBook의 find메소드를 호출하고, 찾은 개인의 정보를 콘솔창에 출력하는 것입니다.
주소록에 저장된 개인들 중에 동명이인이 있을 경우 여러명, 아니면 한 명만 출력될 것입니다.
아래 예시는 “홍길동”으로 검색했을 때 찾기 콘솔창에서 나타나는 화면입니다.
나머지는 formFoRecording에서 설명한 것과 같기 때문에 생략하도록 하겠습니다.
formFoCorrecting 코드
public class Main
{
//[3] 고치기
public static void formFoCorrecting(AddressBook addressBook)
{
Scanner scanner = new Scanner(System.in);
int index = 0;
String going = "Y";
while (going.equals("Y") == true || going.equals("y") == true)
{
System.out.println();
System.out.printf("\t주소록 - 고치기\n");
System.out.printf("\t====================================================================================\n");
System.out.printf("\t성명 : ");
String name = scanner.nextLine();
System.out.printf("\t=====================================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t------------------------------------------------------------------------------------\n");
ArrayList<Integer> indexes = addressBook.find(name);
index = 0;
while (index < indexes.size())
{
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index + 1,
addressBook.getPersonals().get(indexes.get(index)).getName(),
addressBook.getPersonals().get(indexes.get(index)).getAddress(),
addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber(),
addressBook.getPersonals().get(indexes.get(index)).getEmailAddress());
index++;
}
//찾은 게 있으면
if (indexes.size() > 0)
{
while(true)
{
System.out.printf("\t================================================================================\n");
System.out.printf("\t번호 : ");
//nextInt대신에 nextLine을 써야 뒤에서 탈이 안남.
String stringIndex = scanner.nextLine();
index = Integer.valueOf(stringIndex);
index = index - 1;//배열첨자는 실제입력받은 숫자보다 1이 작기때문에
//번호를 제대로 눌렀으면(번호가 배열길이를 넘지 않고 음수가 아니면)
if(index < indexes.size() && index >= 0)
{
System.out.printf("\t--------------------------------------------------------------------------------\n");
System.out.println("\t변경할 사항만 입력하시고, 변경하지 않는 사항은 Enter키를 입력해주세요.");
System.out.printf("\t--------------------------------------------------------------------------------\n");
System.out.printf("\t주 소 : %s ",
addressBook.getPersonals().get(indexes.get(index)).getAddress());
String address = scanner.nextLine();
if (address.equals("") == true)
{
address = addressBook.getPersonals().get(indexes.get(index)).getAddress();
}
System.out.printf("\t전화 번호 : %s ",
addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber());
String telephoneNumber = scanner.nextLine();
if (telephoneNumber.equals("") == true)
{
telephoneNumber = addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber();
}
System.out.printf("\t이메일주소 : %s ",
addressBook.getPersonals().get(indexes.get(index)).getEmailAddress());
String emailAddress = scanner.nextLine();
if (emailAddress.equals("") == true)
{
emailAddress = addressBook.getPersonals().get(indexes.get(index)).getEmailAddress();
}
System.out.printf("\t---------------------------------------------------------------------------------\n");
System.out.printf("\t고치시겠습니까?(Y/N) ");
String correcting = scanner.nextLine();
if (correcting.equals("Y") == true || correcting.equals("y") == true)
{
index = addressBook.correct(indexes.get(index), address, telephoneNumber, emailAddress);
System.out.printf("\t==============================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t------------------------------------------------------------------------------\n");
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index + 1,
addressBook.getPersonals().get(index).getName(),
addressBook.getPersonals().get(index).getAddress(),
addressBook.getPersonals().get(index).getTelephoneNumber(),
addressBook.getPersonals().get(index).getEmailAddress());
}
break;//무한반복문 탈출함.
}
//번호를 잘못눌렀으면
else
{
System.out.println("\t번호를 잘못 선택하셨습니다.");
System.out.println("\t다시 번호를 선택해주세요.");
}
}
}
System.out.printf("\t======================================================================================\n");
System.out.printf("\t계속하시겠습니까?(Y/N) ");
going = scanner.nextLine();
}
}
}
formFoCorrecting 코드 설명
고치기 콘솔창의 경우 시작은 찾기화면과 똑같은 형식으로 진행됩니다.
사용자로부터 성명을 입력 받아 이를 바탕으로 find(성명으로 찾기)메소드를 먼저 수행합니다.
indexes.size() > 0이면, 즉 찾은 결과가 있으면 사용자로부터 바꾸고 싶은 대상의 번호를 입력받습니다.
여기서 번호를 입력 받을 때, nextInt대신에 nextLine을 이용한 뒤에 이를 int로 바꿔주는 방법을 이용합니다.
왜인지는 아직 잘모르겠으나 여기서 nextInt로 입력을 받으면 뒤에서 주소를 입력받을 때 nextLine을 이용하는데 여기서 프로그램이 입력받기 전에 정지를 해야하는데 정지하지않고, 전화번호를 입력받을 때 nextLine으로 바로 이동을 하기 때문입니다;;
이 때 사용자가 실수로 번호를 잘못 입력하는 경우가 있을 수 있는데 이에 대한 처리를 해주지않으면 ArrayIndexOutofBoundsException이 발생할 수 있어서 이를 미리 프로그래머가 방지해줘야 합니다.
그래서 while(true)라는 무한 반복문 내에서 사용자로부터 번호를 입력 받고, 입력 받은 숫자를 -1해주고(사용자가 입력하는 숫자는 1부터 시작하지만 배열은 0부터 시작하기 때문에) 사용자가 올바르게 번호를 입력했는지 선택문으로 확인합니다.
index < indexes.size() && index >= 0 의 선택문 조건 : 사용자로부터 입력 받은 숫자에 -1을 해준 index는 배열 첨자로 사용될 것인데 이 배열첨자가 최소 0(첫 배열요소, 아까 위에서 indexes.size() > 0이라는 선택문을 통과했기 때문에, 즉, 최소한 해당 성명으로 찾은 개인이 1명은 있다는 뜻임)이거나 배열의 길이(해당 성명으로 찾은 개인의 수)보다 작은 경우(배열첨자는 0부터 시작, 개수는 1부터시작하기 때문에)까지는 올바르게 사용자가 입력한 것이고, 그 이외에는 전부 ArrayIndexOutofBoundsException가 발생합니다.
그래서 사용자가 올바르게 입력을 한 경우는 정상적으로 formFoCorrecting의 나머지 로직을 진행한 뒤에 앞의 while무한반복을 탈출하기 위해 마지막에 break를 해줍니다.
잘못 입력한 경우는 else로 처리하여 사용자에게 숫자를 잘못입력했음을 알려주고 다시 while무한 반복으로 돌아가 사용자가 번호를 다시 입력할 수 있도록 합니다.
이렇게 사용자로부터 잘못된 번호를 입력받았을 경우 프로그램이 종료되는 것을 방지하기 위한 처리를 해주고, 정상적인 번호를 입력받았다면 이제 바꿀 내용(주소, 전화번호, 이메일주소)을 사용자로부터 nextLine메소드를 통해 콘솔창으로 입력받습니다.
내용을 바꾸고 싶으면 바꾸고 싶은 내용을 입력하고, 내용을 바꾸지 않을 항목은 그냥 엔터키를 치면 됩니다.
바꿀 내용을 입력하였을 경우 nextLine의 반환값으로 돌려받은 내용을 그대로 사용하면 되고, 엔터키를 쳤을 경우는 nextLine에서 공백문자를 반환하는데 이를 선택구조를 통해 공백인 경우는 원래 내용을 그대로 다시 저장하도록 합니다.
이렇게 바꿀 내용을 사용자로부터 입력을 받은 뒤에 사용자가 고치겠다고 하면 correct메소드를 호출하여 해당 위치의 객체의 내용을 바꿉니다.
이 때 바꿀 위치는 아까 find메소드를 통해 반환받은 ArrayList<Integer>를 이용하여 이 ArrayList에서 몇번째인지를 매개변수로 입력받습니다.
이 ArrayList<Integer>는 아까 성명으로 찾은 개인들이 ArrayList<Personal>에서 몇번째에 위치하는지 그 위치 인덱스를 배열로 저장하고 있습니다.
correct메소드 호출 후에는 바꾼 내용을 콘솔창에 보여준 다음에 계속 고치기를 진행할지 물어봅니다.
나머지 원리는 formFoRecording에서 설명한 것과 같기 때문에 생략하도록 하겠습니다.
formFoErasing 코드
public class Main
{
//[4] 지우기
public static void formFoErasing(AddressBook addressBook)
{
Scanner scanner = new Scanner(System.in);
int index = 0;
String going = "Y";
while (going.equals("Y") == true || going.equals("y") == true)
{
System.out.println();
System.out.printf("\t주소록 - 지우기\n");
System.out.printf("\t====================================================================================\n");
System.out.printf("\t성명 : ");
String name = scanner.nextLine();
System.out.printf("\t=====================================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t------------------------------------------------------------------------------------\n");
ArrayList<Integer> indexes = addressBook.find(name);
index = 0;
while (index < indexes.size())
{
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index + 1,
addressBook.getPersonals().get(indexes.get(index)).getName(),
addressBook.getPersonals().get(indexes.get(index)).getAddress(),
addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber(),
addressBook.getPersonals().get(indexes.get(index)).getEmailAddress());
index++;
}
if (indexes.size() > 0)
{
while(true)
{
System.out.printf("\t================================================================================\n");
System.out.printf("\t번호 : ");
//nextInt대신에 nextLine을 써야 뒤에서 탈이 안남.
String stringIndex = scanner.nextLine();
index = Integer.valueOf(stringIndex);
index = index - 1;//배열첨자는 실제입력받은 숫자보다 1이 작기때문에
//번호를 제대로 눌렀으면(번호가 배열길이를 넘지 않고 음수가 아니면)
if(index < indexes.size() && index >= 0)
{
System.out.printf("\t--------------------------------------------------------------------------------\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t------------------------------------------------------------------------------\n");
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index + 1,
addressBook.getPersonals().get(indexes.get(index)).getName(),
addressBook.getPersonals().get(indexes.get(index)).getAddress(),
addressBook.getPersonals().get(indexes.get(index)).getTelephoneNumber(),
addressBook.getPersonals().get(indexes.get(index)).getEmailAddress());
System.out.printf("\t================================================================================\n");
System.out.printf("\t지우시겠습니까?(Y/N) ");
String erasing = scanner.nextLine();
if (erasing.equals("Y") == true || erasing.equals("y") == true)
{
addressBook.erase(indexes.get(index));
System.out.printf("\t--------------------------------------------------------------------------------\n");
System.out.printf("\t지워졌습니다.\n");
}
break;//무한반복문 탈출함.
}
//번호를 잘못눌렀으면
else
{
System.out.println("\t번호를 잘못 선택하셨습니다.");
System.out.println("\t다시 번호를 선택해주세요.");
}
}
}
System.out.printf("\t================================================================================\n");
System.out.printf("\t계속하시겠습니까?(Y/N) ");
going = scanner.nextLine();
}
}
}
formFoErasing 코드 설명
고치기 콘솔창과 마찬가지로 먼저 지울 개인을 성명으로 찾아야 하기 때문에 찾기 화면을 먼저 출력하여 사용자로부터 성명을 입력받습니다.
성명으로 찾은 개인이 있으면 번호를 입력하여 해당 Personal객체를 선택합니다.
아까 formFoCorrecting과 마찬가지로 사용자가 번호를 잘못 입력할 경우에 대한 처리를 똑같이 해줍니다.
지우기 전에 해당 Personal객체의 내용을 콘솔창에 출력한 후에 지울지 여부를 물어봅니다.
지우겠다고 하면 erase메소드를 호출하여 아까 find를 통해 반환 받은 위치를 매개변수로 입력하여 해당 위치의 Personal객체응 ArrayList<Personal>에서 지웁니다.
코드의 자세한 설명은 formFoCorrecting과 중복이 많아 그것을 참고하면 되고, 나머지는 formFoRecording과 같기 때문에 생략하도록 하겠습니다.
formFoViewingAll 코드
정리하기 전에 전체코드를 보는 것이 더 좋을 것 같아서 전체보기부터 먼저 설명하도록 하겠습니다.
public class Main
{
//[6] 전체보기
public static void formFoViewingAll(AddressBook addressBook)
{
Scanner scanner = new Scanner(System.in);
System.out.println();
System.out.printf("\t주소록 - 전체보기\n");
System.out.printf("\t================================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t--------------------------------------------------------------------------------\n");
int i = 0;
int index = 0;
while (index < addressBook.getPersonals().size())
{
i = 1;
while (i <= 5 && index<addressBook.getPersonals().size())
{
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index+1,
addressBook.getPersonals().get(index).getName(),
addressBook.getPersonals().get(index).getAddress(),
addressBook.getPersonals().get(index).getTelephoneNumber(),
addressBook.getPersonals().get(index).getEmailAddress());
index++;
i++;
}
System.out.printf("\t================================================================================\n");
System.out.printf("\t아무 키나 누르세요!");
scanner.nextLine();
}
}
}
formFoViewingAll 코드 설명
전체보기는 AddressBook의 아무 메소드도 호출하지 않고, 현재까지 저장된 ArrayList<Personal>을 저장한 순서대로 콘솔창에 출력하여 보여주는 역할을 수행합니다.
입력된 개인들이 너무 많은 경우를 대비하여, 게시판처럼 5명씩 개인들을 보여주고(i <= 5), 아무 키나 누르면 다음 5명씩 차례로 보여주는 구조를 가지고 있습니다.
반복구조는 이중 반복구조로 처음 반복구조에서는 ArrayList<Personal>의 size개수만큼 반복합니다.
처음 반복구조 내부에서는 사람을 세는 반복제어변수는 i = 1을 매번 초기화합니다.
그 이유는 1부터 시작하면 5명의 결과를 콘솔창에 출력하면 두번째 반복구조가 종료되고, 첫번째 반복구조로 갔다가 조건이 충족되지 않으면 다시 두번째 반복구조로 들어가서 다음 화면에 개인을 출력해줘야 하는데 다음 화면도 5명까지 출력해야하기 때문에 반복구조가 끝날때마다 다시 1로 초기화해줍니다.
ArrayList<Personal>의 배열요소 위치를 의미하는 index는 초기화하지 않습니다.
그 이유는 i가 다섯 명인동안 그리고 5명이 덜 되었더라도 index가 ArrayList<Personal>의 size개수와 같으면 중단을 하도록 두번째 반복구조의 조건을 설정하려면 index는 초기화하기 않고, 그 결과가 누적되야 때문입니다.
그리고 두번째 반복구조 내부에서 i와 index를 각각 증가시킵니다.
현재 ArrayList<Personal>에 7명이 저장되어 있기 때문에 처음 5명이 출력되고, 아무 키나 누르면 다음에 2명을 따로 출력하여 보여줍니다.
출력이 다 된 다음에 아무 키나 누르면 반복구조가 끝났기 때문에 다시 메뉴창으로 돌아갑니다.
formFoArranging 코드
public class Main
{
//[5] 정리하기
public static void formFoArranging(AddressBook addressBook)
{
Scanner scanner = new Scanner(System.in);
System.out.println();
System.out.printf("\t주소록 - 정리하기\n");
System.out.printf("\t================================================================================\n");
System.out.printf("\t번호\t\t성명\t\t주소\t\t\t\t전화번호\t\t\t이메일주소\n");
System.out.printf("\t--------------------------------------------------------------------------------\n");
addressBook.arrange();
int i = 0;
int index = 0;
while (index < addressBook.getPersonals().size())
{
i = 1;
while (i <= 5 && index < addressBook.getPersonals().size())
{
System.out.printf("\t%d\t\t%s\t%s\t\t%s\t\t%s\n", index+1,
addressBook.getPersonals().get(index).getName(),
addressBook.getPersonals().get(index).getAddress(),
addressBook.getPersonals().get(index).getTelephoneNumber(),
addressBook.getPersonals().get(index).getEmailAddress());
index++;
i++;
}
System.out.printf("\t================================================================================\n");
System.out.printf("\t아무 키나 누르세요!");
scanner.nextLine();
}
}
}
formFoArranging 코드 설명
정리하기 코드는 전체보기와 완전히 똑같고, 다른 점은 딱 하나인데 전체 내용을 출력하기 전에 AddressBook의 메소드인 Arrange()를 먼저 호출하여 이름을 기준으로 오름차순으로 정렬한 다음에 출력을 한다는 것입니다.
위에서 전체보기로 출력했을 때는 정렬이 안되어있었는데, 정렬하기 창에서는 정렬을 한 다음 아래와 같이 출력됩니다.
출력이 다 된 다음에 아무 키나 누르면 반복구조가 끝났기 때문에 다시 메뉴창으로 돌아갑니다.
save잘되었는지 확인하기
이제 프로그램을 종료하면, 해당경로에 텍스트 파일이 생성됩니다.
이를 열어서 확인해보면 다음과 같습니다.
위에서 마지막으로 정렬하기를 하고 프로그램을 종료하였는데 그대로 텍스트창에 잘 출력되었음을 확인할 수 있습니다.
load잘되는지 확인하기
이제 프로그램을 다시 실행시켜보면 해당경로의 텍스트파일을 읽어서 잘 load가 되는지 전체보기창으로 이동하여 확인합니다.
위 결과를 보면 load가 잘되었음을 확인할 수 있습니다.
마치며
load와 save 메소드를 구현하면서 자바입출력, 그 중에서도 문자단위 입출력에 특화되어 있는 Reader와 Writer와 그 자식클래스들을 직접 사용하며 배워볼 수 있었습니다.
또한 자바입출력에서는 close를 반드시 해줘야 하는데 이를 자동으로 처리해서 번거로운 코드를 줄여주는 Try-with-resocures도 적용해볼 수 있었습니다.
그리고 load와 save, 그리고 AddressBook의 모든 메소드가 제대로 돌아가는지 확인하기 위한 과정으로 사용자가 직접 사용하며 입력을 받을 수 있는 CUI프로그램을 구현하였고, 모든 메소드가 정상적으로 돌아감을 확인할 수 있었습니다.
다음에는 “Java프로젝트하면서 데이터베이스 MySQL 연동시키기”를 작성하도록 하겠습니다.
이상으로 글을 마치도록 하겠습니다.
미흡한 글을 읽어 주셔서 감사합니다
질문사항 또는 수정사항은 댓글을 남겨주시거나 이메일로 보내주시면 빠른 시일 내에 답하도록 하겠습니다^^
댓글남기기