Posts 학원 #40일차: 파일 입출력
Post
Cancel

학원 #40일차: 파일 입출력

mini pms 프로젝트 v.30: 파일 입출력 API를 활용하여 데이터를 읽고 쓰기

v.30-a: CSV 파일 포맷

이전 버전까지 사용자가 입력한 데이터를 컬렉션 객체에 저장했다. 즉 RAM에 데이터가 저장되어 있어서 프로그램을 종료하거나 컴퓨터를 끄면 데이터가 지워지는 문제가 있었다. 프로그램을 종료하더라도 데이터가 지워지지 않게 하려면, 외부 저장장치(ex: 하드 디스크, SSD 등)에 저장해야 한다. 즉 데이터를 파일로 출력해야 한다.

파일 입출력 API는 데이터를 파일로 입출력하는 다양한 도구(클래스, 인터페이스)를 제공한다.

CSV(Comma-Seperated Values) 파일 포맷

  • 확장자는 .csv를 사용한다.
  • MIME 형식은 text/csv로 표현한다.
  • 각 레코드(한 단위의 값)는 한 줄의 문자열로 표현한다.
  • 한 줄은 줄바꿈 기호(CRLF)로 구분한다.
  • 레코드를 구성하는 필드는 콤마(,)로 구분한다.
  • 각 필드는 큰 따옴표를 쳐도 되고 안 쳐도 된다.
  • 파일을 저장할 때 마지막 레코드는 줄바꿈 기호가 있을 수도 있고 없을 수도 있다.
aaa, bbb, ccc (CRLF)
aaa, "bbb", ccc (CRLF)
aaa, bbb, ccc

레코드(record)

  • 컴퓨터 과학에서 한 단위의 정보를 가리키는 용어
    • ex) 학생정보, 성적정보, 도서정보, 주문정보, 결제정보
  • 한 개 이상의 필드(field)로 구성된다.
    • ex) 학생정보: 이름, 전화번호, 나이, 우편번호, 주소, 이메일, 암호 등
  • 객체지향 프로그래밍에서 레코드는 보통 클래스로 정의한다.
  • 필드는 클래스의 인스턴스 필드로 정의한다.
1
2
3
4
5
6
class Movie { // 레코드 => 클래스
  String title; // 필드 => 인스턴스 필드
  String director;
  int year;
  int runningTime;
}

Decorator 디자인 패턴

주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 객체에 추가적인 요건을 동적으로 첨가하며, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

FileReader/FileWriter와 Scanner

FileReader 객체에는 read()read(byte[] arr) 메서드밖에 없다. 이 메서드만으로 한 줄씩 값을 꺼내기 위해서는 다음과 같이 코딩해야 할 것이다.

1
2
3
4
5
6
7
8
out= new FileReader(file);

StringBuffer buf = new StringBuffer();
int ch = -1;
while (ch = out.read() != '\n') {
  buf.append((char)ch);
}
String str = buf.toString();

이 코드는 이미 Scanner의 nextLine() 메서드에 들어 있다. Filereader의 read() 메서드만으로는 번거롭기 때문에 FileReader라는 수도꼭지에 Scanner라는 플러그인(필터)을 장착해야 한다. 즉 필터인 Scanner는 수도꼭지에 연결되어 있어야 한다. 필터 자체가 File에서 읽을 수는 없으니까 수도꼭지를 넘겨준다. 그러면 이제 Scanner의 nextLine(), nextInt() 메서드를 통해 한 줄의 문자열 또는 4바이트의 int 값을 리턴받을 수 있게 된다. Scanner의 메서드 내부적으로는 read()를 대신 호출할 것이다. 다시 말해, 이 과정은 원래 객체가 있는데 객체에 원하는 기능이 없으니까, 플러그인을 통해 그 기능을 강화시킨다. 이처럼 동작을 수행하는 코드를 쓰기 좋게끔 메서드에 포장하고 그걸 더 쓰기 좋게끔 class에 포장하는 것이 캡슐화이다.

1
2
out =  new FileReader(file);
scanner = new Scanner(out); // FileReader 객체에 플러그인을 꼽는다.

실습

1단계: 애플리케이션을 실행했을 때 파일에서 데이터를 읽어오는 loadXxx()를 정의한다.

.csv 파일에서 한 줄 단위로 문자열을 읽는 기능이 필요한데, FileReader에는 그런 기능이 없다. 그래서 FileReader를 그대로 사용할 수 없고, FileReader 객체에 다른 도구인 Scanner를 연결하여 사용해야 한다.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static void loadBoards() {
  FileReader in = null;
  Scanner dataScan = null;
  
  try {
    // 파일을 읽을 때 사용할 도구를 준비한다.
    in = new FileReader(boardFile);
    dataScan = new Scanner(in);
    int count = 0;
    
    while (true) {
      try {
        // 파일에서 한 줄을 읽는다. 
        String line = dataScan.nextLine();
        // 한 줄을 콤마(,)로 나눈다.
        String[] data = line.split(",");
        
        // 한 줄에 들어 있던 데이터를 추출하여 Board 객체에 담는다.
        Board board = new Board();
        board.setNo(Integer.parseInt(data[0]));
        baord.setTitle(data[1]);
        board.setContent(data[2]);
        board.setWriter(data[3]);
        board.setRegisteredDate(Date.valueOf(data[4]));
        board.setViewCount(Integer.parseInt(data[5]));
        
        // 게시글 객체를 Command가 사용하는 목록에 저장한다.
        boardList.add(board);
        count++;
      } catch (Exception e) {
        break;
      }
    }
    System.out.println("총 %d개의 게시글 데이터를 로딩했습니다.");
  } catch (FileNotFoundException e) {
    System.out.println("게시글 파일 읽기 중 오류 발생! - " + e.getMessage());
    // 파일에서 데이터를 읽다가 오류가 발생하더라도
    // 시스템을 멈추지 않고 계속 실행하게 하는 것이 예외 처리를 하는 목적이다.
  } finally {
    // 자원이 서로 연결된 경우에는 다른 자원을 이용하는 객체부터 닫는다.
    // 객체를 닫다가 (close()) 오류가 발생하더라도 무시한다.
    // 닫다가 발생한 오류는 특별히 처리할 게 없기 때문이다.
    try { dataScan.close(); } catch (Exception e) {}
    try { in.close(); } catch (Exception e) {}
  }
}

보통 Scanner 객체를 닫으면 out 객체도 닫힌다. 그러나 scanner.close()에서 닫다가 실패하면 out.close()에서 닫을 수 있게 하기 위해 둘다 닫도록 한다.

데이터를 저장할 List 객체는 위에서 만든 메서드에서 접근할 수 있도록 static field로 선언한다.

1
2
3
public class App {
  static List<Board> boardList = new ArrayList();
}

2단계: 애플리케이션을 종료할 때 데이터를 파일에 저장하는 saveXxx()를 정의한다.

데이터를 저장할 파일 정보를 제공하는 File 객체는 saveBoards() 메서드에서 접근할 수 있돌고 static field로 선언한다.

1
2
3
4
public class App {
  static List<Board> boardList = new ArrayList<>();
  static File boardFile = new File("./board.csv");
}
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
private static void saveBoards() {
  FileWriter out = null;
  
  try {
    // 파일로 데이터를 출력할 때 사용할 도구를 준비한다.
    out = new FileWriter(boardFile);
    int count = 0;
    
    for (Board board : boardList) {
      String line = String.format("%d,%s,%s,%s,%s,%d\n", 
            board.getNo(),
            board.getTitle(),
            board.getContent(),
            board.getWriter(),
            board.getRegisteredDate(),
            board.getViewCount());
      out.write(line);
      count++;
    }
    System.out.println("총 %개의 게시글 데이터를 저장했습니다.");
  } catch (IOException) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!");
  } finally {
    try {
      out.close();
    } catch (IOException e) {
      // FileWriter를 닫을 때 발생하는 예외는 무시한다.
    }
  }
}

File 인스턴스를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다. 파일명이나 디렉토리명으로 지정된 문자열이 유효하지 않더라도 컴파일 에러나 예외를 발생시키지 않는다. 새로운 파일을 생성하기 위해서는 File 인스턴스를 생성한 다음, 출력 스트림을 생성하거나 createNewFile()을 호출해야 한다.

3단계: main()의 시작과 끝에서 load()save()를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  public static void main(String[] args) {

    // 파일에서 데이터 로딩
    loadBoards();
    loadMembers();
    loadProjects();
    loadTasks();
	
    //...
    
    // 데이터를 파일에 저장
    saveBoards();
    saveMembers();
    saveProjects();
    saveTasks();
  }

Member, Project, Task에 대해서도 같은 작업을 수행한다.

v. 30-b 리팩토링: 객체 생성에 팩토리 메서드 패턴 적용

리팩토링

소프트웨어를 보다 쉽게 이해할 수 있고, 적은 비용으로 수정할 수 있도록 겉으로 보이는 동작의 변화 없이 내부 구조를 변경하는 것을 말한다. 중복된 코드를 제거하여 설계(design)를 개선시키고, 코드를 더 이해하기 쉽게 만든다. 따라서 버그를 찾기 쉽게 해주고 프로그램을 빨리 작성하도록 도와준다.

비슷한 코드를 중복해서 작성할 때, 기능을 추가할 때, 버그를 수정할 때, 코드 리뷰(code review)를 수행할 때 리팩터링을 한다.

팩토리 메서드(factory method) 패턴

  • new 명령을 사용하여 객체를 생성하기보다는, 메서드를 통해 객체를 리턴받는다.
  • 인터페이스로 객체 생성 규칙을 정의하여 프로그래밍의 일관성을 제공한다.
  • 또한 객체 생성의 책임을 인터페이스 구현체에게 떠넘긴다.

GRASP 패턴

  • General Responsibility Assignment Software Patterns
  • 객체지향 디자인의 핵심은 각 객체에 책임을 부여하는 것이다.
  • GRASP 패턴은 책임을 부여하는 원칙을 말하고 있는 패턴이다.
  • 구체적인 구조는 없고, 총 9가지의 원칙을 가지고 있다.

Information Expert

  • GRASP 패턴의 원칙 중 하나이다.
  • 책임을 수행할 수 있는 데이터를 가지고 있는 객체에 책임을 부여하는 것이다.
  • 객체는 데이터와 처리로직이 함께 묶여 있는 것이다.
  • 정보 은닉을 통해 자신의 데이터를 감추고 오직 메서드로만 데이터를 처리하고, 외부에는 그 기능(책임)만을 제공한다.
  • 어떤 객체가 그 일을 한다는 것은 그 일을 하는 메서드가 그 객체에 있다는 것이다.
  • 어떤 클래스에 메서드를 부여하는 것은 책임을 부여하는 것이다.

실습

1단계: CSV 문자열을 가지고 도메인 객체를 생성하는 valueOfCsv() 메서드 추출

loadBoards()에서 CSV 문자열을 가지고 Board 객체를 생성하는 코드를 뽑아 App클래스의 valueOfCsv()라는 static 메서드로 정의한다.

parsing: 분석하다, 해석하다 (ex: Date.valueOf())

1
2
3
4
5
6
7
8
9
10
11
  static Board valueOfCsv(String csv) {
    String[] values = csv.split(",");
    Board board = new Board();
    board.setNo(Integer.parseInt(values[0]));
    board.setTitle(values[1]);
    board.setContent(values[2]);
    board.setWriter(values[3]);
    board.setRegisteredDate(Date.valueOf(values[4]));
    board.setViewCount(Integer.parseInt(values[5]));
    return board;
  }

원래 코드가 놓여져 있던 loadBoards()의 자리에서 valueOfCsv() 를 호출하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void loadBoards() {
  FileReader in = null;
  Scanner scanner = null;
  
  try {
    in = new FileReader(boardfile);
    scanner = new Scanner(in);
    
    while(true) {
      try {
        // valueOfCsv() 호출
        boardList.add(valueOfCsv(scanner.nextLine()));
      } catch (NoSuchElementException e) {
        break;
      }
    }
  } catch (IOException e) {
    System.out.println("파일 읽기 작업 중에 오류 발생!");
    e.printStackTrace();
  } finally {
    try {scanner.close();} catch(Exception e){}
    try {in.close();} catch(Exception e) {}
  }
}

2단계: 도메인 객체의 필드 값을 CSV 문자열로 리턴하는 toCsvString() 메서드 추출

saveBoards()에서 Board 객체의 필드 값을 CSV 문자열로 바꿔주는 코드를 뽑아 App클래스의 valueOfCsv()라는 static 메서드로 정의한다.

1
2
3
4
5
6
7
8
9
static String toCsvString(Board board) {
  return String.format("%d,%s,%s,%s,%s,%d", 
                       board.getNo(), 
                       board.getTitle(), 
                       board.getContent(),
                       board.getWriter(), 
                       board.getRegisteredDate(), 
                       board.getViewCount());
}

원래 코드가 놓여져 있던 saveBoards()의 자리에서 toCsvString() 를 호출하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void saveBoards() {
  FileWriter out = null;
  
  try {
    // 파일로 데이터를 출력할 때 사용할 도구를 준비한다.
    out = new FileWriter(boardFile);
    int count = 0;
    
    for (Board board : boardList) {
      out.write(toCsvString(board));
      count++;
    }
    System.out.println("총 %개의 게시글 데이터를 저장했습니다.");
  } catch (IOException) {
    System.out.println("게시글 데이터의 파일 쓰기 중 오류 발생!");
  } finally {
    try {
      out.close();
    } catch (IOException e) {
      // FileWriter를 닫을 때 발생하는 예외는 무시한다.
    }
  }
}

3단계: 데이터를 CSV 형식의 문자열로 다루는 메서드를 도메인 클래스로 이동한다.

GRASP 패턴의 Information Expert 원칙에 따라, CSV 형식의 문자열을 다루는 코드 valueOfCsv(), toCsvString() 메서드로 추출하여 그 값을 다루는 도메인 클래스에 둔다.

valueOfCsv()의 경우 특정 게시글이 필요 없다. 즉 인스턴스 객체가 필요 없다. 특정 Board 객체를 가지고 작업하는 메서드가 아니기 때문에 static 메서드로 냅둔다.

반면 toCsvString()은 특정 Board 객체가 필요한 메서드이다. 원래 메서드에서는 특정 Board 객체를 파라미터로 받았다. 그러나 인스턴스 메서드로 둘 경우 특정 Board 객체를 통해 호출하게 되면 this에 특정 Board 인스턴스의 주소가 담기기 때문에 이에 접근할 수 있다. 따라서 인스턴스 메서드로 선언한다.

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
public class Board {
  //...
  
  // 스태틱 메서드
  public static Board valueOfCsv(String csv) {
    String[] values = csv.split(",");
    Board board = new Board();
    board.setNo(Integer.parseInt(values[0]));
    board.setTitle(values[1]);
    board.setContent(values[2]);
    board.setWriter(values[3]);
    board.setRegisteredDate(Date.valueOf(values[4]));
    board.setViewCount(Integer.parseInt(values[5]));
    return board;
  }
  
  // 인스턴스 메서드
  //public static String toCsvString(Board board) {
  public String toCsvString() {
  	return String.format("%d,%s,%s,%s,%s,%d", 
                       board.getNo(), 
                       board.getTitle(), 
                       board.getContent(),
                       board.getWriter(), 
                       board.getRegisteredDate(), 
                       board.getViewCount());
	}
}

이해하기 전에 익숙해지는 단계가 필요하다.

파일 입출력

java.io.File 클래스

  • 파일이나 디렉토리 정보를 관리

  • 파일이나 디렉토리를 생성, 삭제, 변경

프로그램 세계에서는 파일, 디렉토리 모두 파일로 퉁친다.

File 인스턴스를 생성하였다고 해서 파일이나 디렉토리가 생성되는 것은 아니다. 파일명이나 디렉토리명으로 지정된 문자열이 융효하지 않다 하더라도 컴파일 에러나 예외를 발생시키지 않는다. 새로운 파일을 생성하기 위해서는 File인스턴스를 생성한 다음 출력스트림을 생성하거나 createNewFile()을 호출해야 한다.

  • 이미 존재하는 파일을 참조할 때
    • File f = new File("C:\\jdk1.8\\work\\ch15", "FileEx1.java")
  • 기존에 없는 파일을 새로 생성할 때
    • File f = new File("C:\\jdk1.8\\work\\ch15", "NewFile.java")
    • f.createNewFile(); -> 새로운 파일이 생성된다.

폴더 정보 조회

  • .으로 경로를 표시한다.
    • 이클립스에서 프로그램을 실행한다면 프로젝트 폴더를 가리킨다.
    • 콘솔에서 프로그램을 실행한다면 현재 명령어를 실행하는 위치를 가리킨다.

다음과 같이 File 인스턴스를 생성하였다.

1
File currentDir = new File("./src/main/java");

다음 결과는 모두 이클립스에서 실행한 결과이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
System.out.printf("폴더명: %s\n", currentDir.getName());
System.out.printf("경로: %s\n", currentDir.getPath());
System.out.printf("절대경로: %s\n", currentDir.getAbsolutePath());
System.out.printf("계산된 절대경로: %s\n", currentDir.getCanonicalPath());

// 존재하지 않는 폴더인 경우 크기를 알아낼 수 없다.
System.out.printf("총크기: %d\n", currentDir.getTotalSpace());
System.out.printf("남은크기: %d\n", currentDir.getFreeSpace());
System.out.printf("가용크기: %d\n", currentDir.getUsableSpace());

// 존재하지 않는 폴더인 경우 정보를 알아낼 수 없다. 모두 false
System.out.printf("디렉토리여부: %b\n", currentDir.isDirectory());
System.out.printf("파일여부: %b\n", currentDir.isFile());
System.out.printf("감춤폴더: %b\n", currentDir.isHidden());
System.out.printf("존재여부: %b\n", currentDir.exists());
System.out.printf("실행가능여부: %b\n", currentDir.canExecute());

결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
폴더명: java
경로: .\src\main\java
절대경로: C:\Users\Monica Kim\git\eomcs-java-basic\.\src\main\java
계산된 절대경로: C:\Users\Monica Kim\git\eomcs-java-basic\src\main\java
총크기: 232334409728
남은크기: 77701234688
가용크기: 77701234688
디렉토리여부: true
파일여부: false
감춤폴더: false
존재여부: true
실행가능여부: true

상위 경로는 ..으로 표시한다.

1
File currentDir = new File("./src/main/java/../../test/java");

결과

1
2
3
4
5
6
7
8
9
10
11
12
폴더명: java
경로: .\src\main\java
절대경로: C:\Users\Monica Kim\git\bitcamp-workspace\bitcamp-java-basic\.\src\main\java
계산된 절대경로: C:\Users\Monica Kim\git\bitcamp-workspace\bitcamp-java-basic\src\main\java
총크기: 232334409728
남은크기: 74741284864
가용크기: 74741284864
디렉토리여부: true
파일여부: false
감춤폴더: false
존재여부: true
실행가능여부: true

디렉토리 생성: mkdir()

1
2
3
4
5
File dir = new File("temp/a"); // 생성할 디렉토리 경로 지정
if (dir.mkdir()) // 디렉토리 생성
  System.out.println("temp 디렉토리를 생성하였습니다.");
else
  System.out.println("temp 디렉토리를 생성할 수 없습니다.");

만약 해당 경로 (.temp2)의 디렉토리가 존재하지 않을 때는 디렉토리(a)를 새로 만들 수 없다.

디렉토리 삭제: delete()

1
2
3
4
5
File dir = new File("temp");
if (dir.delete())
  System.out.println("temp 디렉토리를 만들었습니다 .");
else
  System.out.println("temp 디렉토리를 생성할 수 없습니다.");

디렉토리 안에 파일이나 다른 하위 디렉토리가 있다면 삭제할 수 없다. 또한 존재하지 않는 디렉토리도 삭제할 수 없다.

파일 생성: createNewFile()

1
2
3
4
5
6
File file = new File("temp2/a/test.txt");

if (file.createNewFile())
  System.out.println("test.txt 파일을 생성하였습니다.");
else
  System.out.println("test.txt 파일을 생성할 수 없습니다.");

이미 파일이 있거나, 경로에 디렉토리가 없다면 파일을 생성할 수 없다. 예외가 발생한다.

파일 삭제

1
2
3
4
5
File file = new File("temp2/a/test.txt");
if (file.delete())
  System.out.println("test.txt 파일을 삭제하였습니다.");
else
  System.out.println("test.txt 파일을 삭제할 수 없습니다.");

파일이나 존재하지 않으면 삭제할 수 없다. 경로가 존재하지 않으면 당연히 그 경로에 파일이 없으니까 삭제할 수 없다.

특정 폴더를 생성하여 그 폴더에 파일을 만든다.

파일을 생성하기 전에 존재하지 않는 폴더를 만들고 싶다면 다음과 같이 부모 경로의 파일 객체를 만들어 주고 mkdir() 로 먼저 생성해 주고 그 다음에 createNewFile()로 파일을 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
File file = new File("temp/b/test.txt");

File dir = file.getParentFile();
/*
위의 코드는 다음과 같다.
String path = file.getParent(); // => "temp/b"
File dir = new File(path);
*/

dir.mkdir();
file.createNewFile();

디렉토리에 들어 있는 파일이나 하위 디렉토리 정보 알아내기

list() 메서드는 해당 파일에 있는 파일 이름을 담아 String 배열로 리턴한다.

1
2
3
4
5
6
7
8
9
// 현재 폴더의 정보를 알아낸다.
File dir = new File(".");

// 현재 폴더에 있는 파일이나 하위 디렉토리 이름을 알아내기
String[] names = dir.list();

for (String name : names) {
  System.out.println(name);
}

listFiles() 메서드는 파일이나 디렉토리 정보File 객체로 받는다. 파일의 이름만을 담아 리턴하는 list() 메서드와는 달리 정보를 리턴한다.

1
2
3
4
5
6
7
8
9
File dir = new File(".");

File[] files = dir.listFiles();
for (File file : files) {
  System.out.printf("%s 12d %s\n",
                   file.isDirectory() ? "d" : "-",
                   file.length(),
                   file.getName());
}

디렉토리에 들어 있는 파일 목록 필터

  • FilenameFilter의 accept() 역할

    JavaFilter 클래스의 accept 메서드는 list()에서 호출한다. 해당 폴더에 들어 있는 파일이나 디렉토리를 찾을 때마다 호출한다. 단 하위 폴더 아래는 뒤지지 않는다. 이 메서드에서 해야 할 일은 찾은 파일이나 디렉토리를 리턴할 배열에 포함시킬지 여부이다. true를 리턴하면 배열에 포함되고, false를 리턴하면 배열에 포함되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
  
  class JavaFilter implements FilenameFilter {
    @Override
    public boolean accept(File dir/*부모 경로*/, String name/*파일 이름*/) {
      // 파일, 디렉토리 이름이 .java로 끝나는 경우만 리턴 배열에 포함시킨다.
      if (name.endsWith(".java"))
        return true;
      return false;
    }
  }
  // 확장자가 .java인 파일의 이름만 추출하기
  File dir = new File(".");
  // 1) 필터 준비
  JavaFilter javaFilter = new JavaFilter();
  
  // 2) 필터를 사용하여 디렉토리의 목록을 가져오기
  String[] names = dir.list(javaFilter);
  
  for (String name : names) {
    System.out.println(name);
  }
}

확장자가 .java인 파일의 이름만 추출해야 한다고 하자. 다음과 같이 FilenameFilter 구현체를 사용할 수 있다. 단, 위의 코드는 파일 이름으로만 검사한다는 문제점이 있다. 만약 temp.java라는 이름의 디렉토리가 있다고 해보자. 그러면 위의 코드는 해당 디렉토리까지 출력할 것이다. 해당 이름이 디렉토리 이름인지 파일 이름인지 알아내려면 File 객체를 생성해야 한다. 파라미터로 받은 디렉토리 정보와 파일 이름을 합쳐 파일 객체를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
  
  class JavaFilter implements FilenameFilter {
    @Override
    public boolean accept(File dir, String name) {
      // 해당 이름이 디렉토리 이름인지 파일 이름인지 알아내려면
	    // File 객체를 생성해야 한다.
      File file = new File(dir, name);
      // 디렉토리 정보와 이름을 합쳐 파일 객체를 생성할 수 있다.
      if (file.isFile() && name.endsWith(".java"))
        return true;
      return false;
    }
  }
  File dir = new File(".");
  JavaFilter javaFilter = new JavaFilter();
  String[] names = dir.list(javaFilter);
  
  for (String name : names) {
    System.out.println(name);
  }
}

그러나 만약 파일 이름뿐만 아니라 디렉토리인지, 크기는 어떤지 등 파일에 대한 정보를 출력하고 싶다면 어떻게 해야 할까? list()listFiles()로 대체하면 된다. list()의 경우 파일 이름 문자열 배열을 리턴하지만, listFiles()는 File객체를 담은 배열을 리턴하기 때문에, 이 객체의 메서드를 이용하여 해당 파일의 정보를 출력할 수 있다.

단, listFiles()의 파라미터로 올 수 있는 값은 FilenameFilter 구현체가 아니라 FileFilter 구현체여야 한다. FilenameFilter는 파일 이름으로, FileFilter는 파일 정보로 필터링한다. (물론 FilenameFilter의 accept에서 디렉토리 정보와 파일 이름을 통해 파일 객체를 생성하여 파일 정보로 필터링할 수도 있다. 그러나 FileFilter의 accept처럼 바로 파라미터로 받은 파일 객체를 사용하는 것보다는 수고로움이 요구된다.)

FileFilter의 accept(File file) 메서드는 지정한 폴더에 들어 있는 파일이나 디렉토리를 찾을 때마다 listFiles() 메서드에서 호출한다. 리턴 값 File[] 에 찾은 파일 정보를 포함 시킬지 여부를 결정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Exam0610 {
  public static void main(String[] args) {
    class JavaFilter implements FileFilter {
      @Override
      public boolean accept(File file) {
        if (file.isFile() && name.endsWith(".java"))
          return true;
        return false;
      }
    }
    File dir = new File(".");
    File[] files = dir.listFiles(new JavaFilter());
    
    for (File file : files) {
      System.out.printf("%s %12d %s\n",
                       file.isDirectory() ? "d" : "-",
                       file.length(),
                       file.getName());
    }
  }
}

FileFilter 구현체를 리팩토링 해보자.

필터 객체를 한 개만 만들 것이라면 익명 클래스로 만드는 것이 낫다. 또한 객체를 사용할 위치인 listFiles() 파라미터 자리에 익명 클래스를 정의하는 것이 코드를 더 읽기 쉽게 만든다.

1
2
3
4
5
6
7
    File dir = new File(".");
    File[] files = dir.listFiles(new FileFilter() {
      @Override
      public boolean accept(File file) {
        return file.isFile() && name.endsWith(".java")
      }
    });

메서드 한 개짜리 인터페이스의 경우 람다(Lambda) 문법을 사용하면 더 간결하게 코드를 작성할 수 있다. 표현식(expression)은 return 문장을 생략할 수 있다.

1
2
File dir = new File(".");
File[] files = dir.listFiles(file -> file.isFile() && name.endsWith(".java");
This post is licensed under CC BY 4.0 by the author.

학원 #39일차: 예외처리

학원 #41일차: 람다(Lambda)

Loading comments from Disqus ...