딸기말차

[Java] 5. 파일 입출력, 다중 클래스 본문

Bootcamp/Java

[Java] 5. 파일 입출력, 다중 클래스

딸기말차 2023. 6. 29. 00:49

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io


1. 외부 데이터 다루기

Java에선 외부 데이터를 다루기 위한 파일 입출력 기능이 존재한다. 말 그대로 외부에서 가져온 Java 내의 파일이 아니기 때문에, 이 파일을 다루기 위해선 반드시 예외 처리를 해줘야 한다. 해당 파일을 읽어 들였을 때, 에러가 있는 파일이거나 잘못 된 파일임을 확인하기 위해서 이다.

 * 외부 데이터 다루기
 * 1. 예외처리 : 문제가 발생했을 때 app은 유지를 시키고, 사용자에게 문제점에 대한 내용을 알리기 위함
 * 2. 예외처리 사용 : 로컬 파일 / 인터넷을 통한 데이터를 읽을 때 / 데이터베이스
 * 3. 예외처리 방법 : 
 * 	1) try {} catch(예외처리 클래스타입 매개변수) {}  
 * 	매개변수 : 문제가 발생한 내용을 전달(저장) 받는 변수
 * 	예외처리 클래스 : Exception
 * 	2) throws : 메서드의 선언부에 사용
 * 	3) try {} catch() {} finally {try 내부가 성공적으로 끝나거나, catch 내부가 끝났을 경우}
 
 * 메서드 내부에 try {실행 내용들} catch(Exception e) {try 내에서 문제가 발생했을 경우 해야할 일들} 를 사용하는 경우에는 
 * 다른 메서드에서 예외처리를 하지 않아도 되지만, 메서드 선언부에 throws를 이용하면 다른 메서드에서 다시 예외처리를 해야한다.
 * 이 때문에 주로 try - catch를 많이 사용한다.
 * 
 * Scanner를 사용할 때 마지막에 close를 해주는 것처럼, finally에는 접속한 DB를 종료하는 등 마무리작업이 필요할 때 반드시 사용해야한다.
 * 
 * 모든코드에 예외처리를 하는 것은 아니다. 일반적인 상황에서도 예외처리를 하게되면 오히려 에러가 발생할 수 있다.

 


2.  외부 로컬파일 다루기

위에서 작성한 예외처리가 끝났으면, 이제 파일 읽기, 쓰기를 직접 실행해볼 차례이다. Java에선 파일 입출력을 위한 클래스가 존재하고, 해당 클래스를 사용해 파일을 읽어오고, 읽어온 파일의 내용을 변경할 수도 있고, 작성한 내용을 파일로 다시 내보내는 것도 가능하다.

 * 외부 로컬파일 다루기
 * 1. 파일 쓰기 : java.io.FileWriter.class
 * 	FileWriter fw = new FileWriter("파일경로");
 * 	fw.write("문자열\n");
 * 	fw.close();
 * 
 * 파일경로 : c:/폴더/파일.확장자, / 대신 \ 를사용하려면 \\ 로 사용하면된다.
 * FileWriter 객체를 생성 시엔 어디에, 어떤 파일로 쓸 것인지를 만들어줘야 한다.
 * windows의 c드라이브엔 파일 생성이 불가능하게 되어있기 때문에, 파일을 관리할 폴더를 하나 만들어 사용해야한다.
 * FileWriter 클래스는 경로에 파일이 없어도 해당 경로에 파일을 만들어 열어둔다.
 * 그리고 writer 메서드를 통해 해당 파일에 데이터를 쓰기 시작한다.
 * 
 * 2. 파일 읽기 : java.io.FileReader.class
 * 	FileReader fr = new FileReader("파일경로");
 * 	// 줄 단위로 읽기 : java.io.BufferedReader.class
 * 	BufferedReader br = new BufferedReader(FileReader 객체);
 * 	br.readLine();
 * 	String tmp = "";
 * 	while ((tmp = br.readLine()) != null) { tmp = tmp + "\n"; } -> 한 줄씩 띄어 저장하기 위해 \n을 사용한다.
 * 	br.close();
 * 
 * FileReader는 FileWriter와 다르게 파일 경로에 해당 파일이 없으면 에러가 난다.
 * 일반적으로 데이터를 읽을 때 줄 단위로 읽는데, FileReader에는 줄 단위로 읽는 메서드가 없다.
 * 때문에 줄 단위로 읽기 위해 BufferedReader를 사용한다. 이때 반복문을 사용하는데, 끝나는 시점을 알 수 없기 때문에 while을 사용한다.

우선 사용자가 원하는 내용을 작성 후 파일을 내보내주는 fileWriter 메서드를 구현하였다. 파일을 다루는 작업이기 때문에 예외처리는 반드시 들어가야하고, FileWriter 클래스를 통해 데이터의 통로를 열어둔 것이기 때문에 마지막에 close로 닫아주는 작업까지 해야한다.

public static void fileWrite(String uri) {
    FileWriter fw = null;

    try {
        fw = new FileWriter(uri);
        String txt = "hello ㅁㅇㅁㄹ\nasdf\n";
        fw.write(txt);
        fw.close();
    } catch (IOException e) {
        System.out.println("FileWriter: " + e.getMessage());
//			e.printStackTrace(); -> 기본제공 메서드지만, 에러가 많아 잘 사용하지 않는다.
    }
}

다음으로 파일을 읽어들이기 위한 fileRead 메서드를 구현하였다. write 클래스와 차이가 있다면, java의 FileReader 클래스에는 줄 단위로 읽는 메서드가 없기 때문에 BufferedReader를 사용한다는 점과 작성한 경로에 파일이 없는 경우의 예외처리를 추가한다는 점이다.

public static void fileRead(String uri) {
    FileReader fr = null;
    BufferedReader br = null;
    try {
        fr = new FileReader(uri);
        br = new BufferedReader(fr);

        String tmp = "";
        while ((tmp = br.readLine()) != null) {
            System.out.println(tmp);
        }
        br.close();
        fr.close();

    } catch (FileNotFoundException e) {
        System.out.println("FileReader: " + e.getMessage());
    } catch (IOException e) {
        System.out.println("FileReader: " + e.getMessage());
    }	
}

3.  다중 클래스 실습_5) 뉴스 데이터를 사용한 파일 입출력

txt로 존재하는 뉴스 데이터를 FileReader로 읽어, 그 중 image 데이터를 사용해 HTML파일을 만들고, uri를 사용해 해당 경로에 파일로 내보내는 실습을 진행하였다.

처음으로, 읽은 데이터를 저장하기 위한 객체인 NaverNewsClass를 작성하였다.

/*
 * Model, 한 신문사 정보 저장
 */
public class NaverNewsClass {
	String press = null;
	String image = null;
	String uri = null;
	String category = null;

	public NaverNewsClass() {
	}
	
	public NaverNewsClass(String press, String image, String uri, String category) {
		this.press = press;
		this.image = image;
		this.uri = uri;
		this.category = category;
	}

	public NaverNewsClass(String image, String uri) {
		this.image = image;
		this.uri = uri;
	}	
}

두 번째로, NaverDataClass에 뉴스 데이터를 읽어 그 중 image와 uri 를 추출하기 위한 메서드를 작성하였다.

/*
 * 모든 신문사들 정보 저장
 * 1. 파일 읽기
 * 2. 읽은 내용으로 NaverNewsClass 객체를 생성
 * 3. ArrayList에 추가
 */
public class NaverDataClass {

	ArrayList<NaverNewsClass> news = new ArrayList<NaverNewsClass>();
	
	public NaverDataClass() {
	}
	
	// 사용할 메서드인 readData가 인자로 String을 받기 때문에, 기본생성자가 아니라 인자를 받는 생성자를 하나 더 만들어 실행한다.
	public NaverDataClass(String uri) {
		this.readData(uri);
	}
	
	private void readData(String uri) {
		FileReader fr = null;
		BufferedReader br = null;
		
		try {
			fr = new FileReader(uri);
			br = new BufferedReader(fr);
			
			String tmp = "";
			String[] splits = null;
			while ((tmp = br.readLine()) != null) {
				splits = tmp.split("\", \"");
				news.add(new NaverNewsClass(splits[1], splits[2]));
			}
			
			br.close();
			fr.close();	
		} catch (FileNotFoundException e) {
			System.out.println("FileNotFountException: " + e.getMessage());
		} catch (IOException e) {
			System.out.println("IOException: " + e.getMessage());
		}
	}
}

다음으로, 추출한 데이터를 사용해 HTML 태그들을 만드는 NaverMethodClass를 작성하였다. 이 클래스의 메서드를 구현할 때, 이미지들을 단순히 순서대로 출력하면 한 줄로만 출력되기 때문에 격자 모양으로 바꾸는 로직을 구현해야 했다. 

 

1.  이중 for-loop를 이용한 로직

격자 모양을 생각하면 대표적으로 떠오르는 방법은 이중 for-loop를 사용하는 것이다. 주의해야 할 점은, index를 계속 증가시키다가 읽어온 데이터의 크기와 동일해지면 break를 통해 loop를 탈출해야 한다는 것이다.

int index = 0;
for (int i = 0; i < 10; i++) {
    tags += "<tr>";
    for (int j = 0; j < 18; j++) {
        if (index == news.size())
            break;
        tags += "<td>";
        tags += "<a href='" + news.get(index).uri + "'>";
        tags += "<img src='C:\\filetest\\newsImages\\" + news.get(index).image + "' />";
        tags += "</a>";
        tags += "</td>";
        index++;
    }
    tags += "</tr>";
}

2. 나머지 연산을 이용한 로직

이중 for-loop를 사용하지 않고 나머지연산을 사용하여, 나머지가 0이 될 경우 HTML 태그를 삽입하여 구현하는 방법이다.

코드가 좀 더 간단해지는 장점이 있지만, 추후에 css를 추가하거나 다른 동작을 추가할 시 수정이 어려워진다는 단점 또한 존재한다.

tags += "<tr>";
for (int i = 0; i < news.size(); i++) {
    if (i % 18 == 0)
        tags += "</tr><tr>";
    tags += "<td>";
    tags += "<a href='" + news.get(i).uri + "'>";
    tags += "<img src='C:\\filetest\\newsImages\\" + news.get(i).image + "' />";
    tags += "</a>";
    tags += "</td>";
}
tags += "</tr>";

로직구현을 완료한 후, Main에서 각 클래스를 호출 후 파일을 저장할 경로와 구현한 HTML 태그를 PrintClass로 넘겨

파일 출력까지 완료하였다.

String uri = "C:\\filetest\\naver_news_stand_data_edit.txt";
NaverDataClass naver = new NaverDataClass(uri);
System.out.println(naver.news.size());
for(NaverNewsClass tmp : naver.news)
    System.out.println(tmp.image + " " + tmp.uri);

String tags = NaverMethodClass.createHTML(naver.news);
System.out.println(tags);

String uri2 = "C:\\filetest\\naver_news_stand.html";
NaverPrintClass.saveHTML(uri2, tags);
public static void saveHTML(String uri, String tags) {
    try {
        FileWriter fw = new FileWriter(uri);
        fw.write(tags);
        fw.close();
    } catch (IOException e) {
        System.out.println("FileWriter: " + e.getMessage());
    }
    System.out.println("파일 생성 완료");
}

4. 다중 클래스 실습_6) 두 가지 txt 파일을 통한 파일 입출력

검색단어리스트와 하둡분산처리시스템을 정리해 둔 txt파일을 읽어 검색 단어들이 하둡분산처리시스템 파일에 총 몇 번 나오는지 개수를 세서 HTML 파일로 내보내는 실습을 진행하였다. 

해당 실습을 진행하기 위해 우선 DataClass에서 검색단어리스트 파일을 읽어와 저장하는 메서드를 구현하였다. 

ArrayList<String> datanames = new ArrayList<String>();
Map<String, Integer> hadoops = new HashMap<String, Integer>();

public void readDataName(String uri) {
    FileReader fr = null;
    BufferedReader br = null;

    try {
        fr = new FileReader(uri);
        br = new BufferedReader(fr);

        String tmp = "";
        while((tmp = br.readLine()) != null)
            datanames.add(tmp);

        br.close();
        fr.close();
    } catch (FileNotFoundException e) {
        System.out.println("FileNotFoundException: " + e.getMessage());
    } catch (IOException e) {
        System.out.println("IOException: " + e.getMessage());
    }
}

 

다음으로, 하둡분산처리시스템 파일에 있는 단어를 중복 제거해 총 몇 번 나오는지 세는 메서드를 구현하였다. 이를위해 hash를 사용하였고, 단어의 이름과 개수 두 가지 데이터를 저장해야 했기 때문에 Map을 사용하였다.

그리고 Map 초기화를 위해 putIfAbsent 메서드와, 중복제거 후 개수를 세기위해 computeIfPresent 메서드를 사용하여 출력할 데이터를 완성시켜 주었다.

public void setData(String uri1, String uri2) {
    for (String s : datanames)
        hadoops.putIfAbsent(s, 0);

    FileReader fr = null;
    BufferedReader br = null;

    try {
        fr = new FileReader(uri2);
        br = new BufferedReader(fr);

        String tmp = "";
        String[] splits = null;
        while((tmp = br.readLine()) != null) {
            splits = tmp.split(" ");
            for (String s : splits) {
                for (String h : hadoops.keySet()) {
                    if (s.contains(h))
                        hadoops.computeIfPresent(h, (k, v) -> v + 1);
                }	
            }
        }

        br.close();
        fr.close();
    } catch (FileNotFoundException e) {
        System.out.println("FileNotFoundException: " + e.getMessage());
    } catch (IOException e) {
        System.out.println("IOException: " + e.getMessage());
    }
}

이 후 Main에서 각 클래스를 호출 한 뒤, 파일을 저장할 경로와 구현한 HTML 태그를 PrintClass로 넘겨 파일 출력을 완료하였다.

public static void main(String[] args) {
    String uri1 = "C:\\filetest\\검색단어리스트.txt";
    String uri2 = "C:\\filetest\\하둡 분산 처리 파일 시스템.txt";
    String uri3 = "C:\\filetest\\hadoops.html";

    HadoopDataClass hadoopDataClass = new HadoopDataClass();
    hadoopDataClass.readDataName(uri1);
    hadoopDataClass.setData(uri1, uri2);
    HadoopPrintClass.saveHtml(uri3, HadoopMethodClass.createHtml(hadoopDataClass.hadoops));
}
public static void saveHtml(String uri, String tags) {
    FileWriter fr;
    try {
        fr = new FileWriter(uri);
        fr.write(tags);
        fr.close();
    } catch (IOException e) {
        System.out.println("IOException: " + e.getMessage());
    }
    System.out.println("hadoops 파일 생성 완료");
}

실행결과


5. 5일차 후기

파일 입출력을 학습 후 다중 클래스 실습에 추가하는 작업을 계속 진행하며, 확실히 다중 클래스 간 데이터 이동에 능숙해져가고 있다고 느꼈다. 또한 어제 실습했던 중복제거 작업에 파일 입출력까지 추가하니, 코드가 점점 복잡해져 가는 것을 느꼈다.

 

개발자는 기능 구현도 중요하지만, 구현한 코드를 다른사람이 읽어도 쉽게 알아볼 수 있게 짜는 것과 메모리 효율이 잘 나올수 있게 유지보수하는 것이 더 중요하다고 생각한다.

그래서 최근 실습을 주시면 기능 구현을 우선 완료한 후 코드를 최대한 정리하려고 노력하고 있는데, 이 부분이 많은 도움이 되는 것 같다. 코드를 계속 리뷰하며 놓쳤던 부분도 찾아낼 수있고, 줄일 수 있는 부분은 최대한 줄여 클린코드를 완성해 갈 수 있기 때문이다.