자바 입출력 두번째.

지난 번에 자바 입출력에 대해서 공부 했다. 그 기억을 다시 읽어서 두번 째 이야기를 해보도록 하자.

덤으로 예외 처리 까지 하자.

지난번에는 텍스트 파일을 읽고 쓰는것을 가지고 다뤘던 것으로 기억한다.
(기억이 안난다면 복습)

그런데 지난 시간에는 바이트스트림에 대해서는 다루지 않았다. 그럼 그 차이점에 대해서 알아보자.

바이트 입출력 스트림과 문자 입출력 스트림에 가장 큰 차이점을 데이터를 읽어 들이는 것 부터가 차이가 난다.

쉽게 말하면 문자 입출력 스트림은 모든 데이터를 읽을 때 해당 코드가(2byte 사이즈로 읽은 2진수) 문자로 인식이 된다. 그럼 어떤 결과가 일어날까?

텍스트 파일을 읽었다면 탈이 없겠지만 바이너리 파일을 읽었다면 외계어가 되버리고 만다.

가장 큰 차이점이 바로 이 점이다. 출력 또한 같다고 보면 된다.

잘 모르겠다면 텍스트 파일이 아닌 엉뚱한 파일을 읽어서 화면에 출력하는 예제를 만들어서 돌려보거나

메모장을 이용해서 아무 파일이나 열어보면 알수 있다.

그리고 본인에 프로젝트에 파일 복사예제에 대해서 존재 하는데 그 예제를 예로 들 수 있다.

그런데 신기한 것은 두 입출력 스트림은 차이점을 보이지만 원리는 같은 방식을 취한다.

파일을 복사하는 방법은 파일을 읽어서 다시 그것을 저장하는 방식이다.
말이 좀 어렵거나 그거야 누구나 다 아는거 아닌가 하는 생각이 들 수도 있지만 문자 입출력 스트림을 예로 들면 이해가 쉽다.

예제로는 텍스트 파일을 복사하라는 예제가 없을 것이다. 그 이유는 보통 문자 입출력 스트림에서는 예제로 학생의 성적을 입력 받거나 혹은 텍스트 파일을 읽어서 그 데이터를 가공해서 평균과 총점을 내고 파일로 저장하라는 예제가 많을 것이다.

그런데 잘 생각해보자. 이게 무엇인가? 바로 읽어서 다시 저장하는 것이다. 바이트 스트림과 구조상 같은 방식을 취한다. 그러니 말이 갑자기 좀 어렵다 생각하지 말고 문자 입출력과 원리는 같다고 생각하자.

다만 다른것은 약간의 코딩의 차이일뿐이다. 그것은 우리의 백과사전 API가 있으니 큰 문제 없을 것이다.

그럼 잡담 같은 이야기는 이정도로 접고 예제 문제에 대한 해설을 하도록 하겠다.
(소스를 해설 하면서 공부하는게 큰 도움이 되므로~~)

첫번째 예제는 문자 입출력 클래스를 이용하는 예제이다.
학생의 이름, 성적(컴퓨터, 국어, 영어) 성적이 들어 있는 텍스트 파일을 읽어서 총점과 평균을 계산하고 다른 이름으로 저장하는 예제이다.

각자 알아서 짜도 문제가 없지만 요구 조건이 있으므로 요구 조건에 맞게 짜도록 하자.
 - 파일을 읽을 때는 BufferedReader 클래스의 readLine() 메소드를 사용해 읽어 들인다.
 - 읽어드린 문자열의 각 필드는 공백문자("\\s")를 토큰으로 사용한다.
 - 읽어드린 성적 정보를 저장할 Data 클래스를 만들고 Vector 클래스를 사용해야 한다.
 - 파일을 저장할 때는 FileWriter 클래스를 이용한다.
 - 처리된 성적을 저장할 때는 탭문자("\t")를 사용해 각 필드를 구분한다.

그럼 가장먼저 해야 할 일을 생각하자. UML을 구성해야 편하겠지만 이정도는 머릿속으로 그리거나 아니면 각자 알아서 그리도록 하자. 크게 어렵지 않으니까.

그리고 Data클래스 부터 만들자. Data 클래스는 학생의 이름과 성적을 갖고 있는 클래스이므로 잘 만들도록 하자. 지난 예제인 Iterator 패턴을 기억하고 있고 이해를 하고 있다면 어렵지 않게 술술 작성이 가능할 것이다.(그리고 자바가 재밌어 질것이다.)

class Data {
 private String name;
 private int com;
 private int kor;
 private int eng;
 
 //Data 클래스의 기본 생성자.
 public Data(){
  this.name = null;
  this.com = 0;
  this.kor = 0;
  this.eng = 0;
 }
 
 //Data 클래스의 또 다른 생성자. 한번에 모든 맴버 변수에 값을 넣어 줄수 있다.
 public Data(String n,int c,int k,int e){
  this.name = n;
  this.com = c;
  this.kor = k;
  this.eng = e;
 }
 
 //name 설정, 리턴
 public void setName(String n){
  this.name = n;
 }
 public String getName(){
  return this.name;
 }
 
 //com(컴퓨터성적) 설정, 리턴
 public void setCom(int c){
  this.com = c;
 }
 public int getCom(){
  return this.com;
 }
 //입력이 문자열일 때 형변환(오버로딩)
 public void setCom(String c){
  try{
   this.com = Integer.parseInt(c);   
  }catch(Exception e){
   this.com = 0;
   System.out.println("데이터 입력 형식이 잘못됐습니다.\n컴퓨터 점수는 0으로 설정합니다.");
  }
 }
 
 //kor(국어성적) 설정, 리턴
 public void setKor(int k){
  this.kor = k;
 }
 public int getKor(){
  return this.kor;
 }
 //입력이 문자열일 때 형변환(오버로딩)
 public void setKor(String k){
  try{
   this.kor = Integer.parseInt(k);   
  }catch(Exception e){
   this.kor = 0;
   System.out.println("데이터 입력 형식이 잘못됐습니다.\n국어점수는 0으로 설정합니다.");
  }
 }
 
 //eng(영어성적) 설정, 리턴
 public void setEng(int e){
  this.eng = e;
 }
 public int getEng(){
  return this.eng;
 }
 //입력이 문자열일 때 형변환(오버로딩)
 public void setEng(String e){
  try{
   this.eng = Integer.parseInt(e);   
  }catch(Exception ioe){
   this.eng = 0;
   System.out.println("데이터 입력 형식이 잘못됐습니다.\n영어점수는 0으로 설정합니다.");
  }
 }
 
 //총점과 평균 계산 메소드
 public int pointSum(){
  return (this.kor+this.com+this.eng);
 }
 public int pointAvg(){
  return ((this.kor+this.com+this.eng)/3);
 }

}

첫번 째 소스다. 확실히 주석을 달아서 일일이 설명해야 할 부분은 이제 없을 것이다. 그러나 패턴을 이해 한 뒤 부터는 코딩 방식이 약간 변화 했다. 왜일까? 패턴이란 룰에 맞춰서 했으니까? 아니라고 말은 못하지만 다른 이유도 존재 한다. 패턴을 알고 난 뒤로 어떻게 짜야 효율적이고 호환성이 좋아지는지 더 잘 알게 됐다. 객체지향적으로 변화해 가는 것이다.

문제가 될만한 경우와 편리를 위한 것, 그리고 데이터를 입력하는것과 출력하는 것 이런 종류의 메소드를 만들어서 다른 클래스에서 사용이 편리하게끔 짜는 것이 좋은 방법의 예이다.

위에 Data 클래스 소스는 각 맴버변수의 설정과 출력, 평균과 합계, 그리고 형변환과 2개의 생성자가 있다.

아 그리고 여기서 예외 처리를 잠깐 보도록 하자. 예외 처리란 지난 프로젝트 때 간단하게 이야기를 했었다. 어려운 것이 아니므로 겁먹지 말자. 예외 처리를 프로그램이 구동 중에 발생 할 수 있는 오류에 대한 대책을 마련하기 위해 존재 한다고 보면 된다. 그리고 특징이 있다면 예외 처리는 모든 예외 처리를 해결해주는 것이 아니라 한계가 있고 한번의 예외 처리 조건을 만들어 주면 그 오류에 대해서 1번 예외적으로 처리를 해준다. 더 많은 처리를 해주고 싶다면 많이 쓰면 된다.

예외 처리 간단 정리.
예외 처리 한 문장은 1회성이다. 단 그 메소드 실행 될 때 발생하는 예외처리에 대해서 1번이다. 다시 해당 메소드를 읽어 들일 때 또 문제가 된다면 또 처리를 해준다. 그러나 해당 메소드에서 5번의 오류가 발생을 했고 4번의 예외 처리를 구성 했다면 프로그램이 비정상적으로 종료가 된다. 그리고 예외 처리도 종류가 존재한다.API참고.

그럼 이제 메인만 구성하면 된다. 쉽다.
메인은 Vector과 파일입출력을 사용하기 때문에 최 상단에 import를 두개 해줘야 하는걸 기억하자.

import java.io.*;
import java.util.*;
public class Exam0802 {

 public static void main(String[] args) {
  
  Vector vec = new Vector();
    
  //데이터 읽기.
  try{
   File ReadFile = new File("score.txt");
   BufferedReader TxtData = new BufferedReader(new FileReader(ReadFile));
   
   String str = null;
   
   System.out.println("읽어 들인 성적 데이터");
   System.out.println("이름\t컴퓨터\t국어\t영어");
   
   while((str = TxtData.readLine()) != null){
    String x[] = str.split(" ");
    
    Data data = new Data();
    data.setName(x[0]);
    data.setCom(x[1]);
    data.setKor(x[2]);
    data.setEng(x[3]);
    
    vec.add(data);
    // vec.add(Data data = new Data(x[0],x[1],x[2],x[3]));
    //읽은 데이터의 출력 확인.
    System.out.println(data.getName()+"\t"+data.getCom()+"\t"+data.getKor()+"\t"+data.getEng());    
   }
   
   TxtData.close();
  
  }catch(IOException e){
   System.out.println("에러 발상");
  }
  
  
  
  //데이터 쓰기.
  try{
   File fw = new File("result.txt");
   PrintWriter pw = new PrintWriter(new FileWriter(fw));
   
   Enumeration enu = vec.elements();
   
   System.out.println("--------------------------------------------------");
   System.out.println("저장된 성적 데이터");
   System.out.println("이름\t컴퓨터\t국어\t영어\t총점\t평균");
   
   while(enu.hasMoreElements()){
    Object SaveData = enu.nextElement();
    
    pw.print(((Data)SaveData).getName()+"\t");
    pw.print(((Data)SaveData).getCom()+"\t");
    pw.print(((Data)SaveData).getKor()+"\t");
    pw.print(((Data)SaveData).getEng()+"\t");
    pw.print(((Data)SaveData).pointSum()+"\t");
    pw.println(((Data)SaveData).pointAvg());
    
    System.out.println(((Data)SaveData).getName()+"\t"+((Data)SaveData).getCom()
      +"\t"+((Data)SaveData).getKor()+"\t"+((Data)SaveData).getEng()
      +"\t"+((Data)SaveData).pointSum()+"\t"+((Data)SaveData).pointAvg());
   }
   
   pw.flush();
   pw.close();
   
  }catch(IOException e){
   System.out.println("에러 발상");
  }
  
 }

}

소스에 대한 해설은 적다. 단지 여기서 알아두면 좋은 것이 있다면 예외처리에 e.printStackTrace(); 이 문장을 넣는 것이 오류 찾는데 도움이 될 것이다. printStackTrace() 이 메소드는 에러가 난 지점을 알려주는 기능이다. 더 정확하게는 API를 참고하길 바란다.

그리고 위 소스에서는 Iterator을 사용하지 않았다. 만약 Iterator을 사용했다면 조금 더 간편 했을 것이다.
또, 제네릭이란 것도 알아두면 좋다. 보통 위 소스에서 처럼 백터를 이용한다면 캐스팅 과정이 매우 중요하기 때문에 많은 부분이 비효율적일 때도 있다. 그러니 Iterator과 제네릭이라는 것을 필요에 따라서는 적절히 이용해주면 매우 간편해 질 것이다.

그럼 위에 것을 이용해 좀 더 간편하게 구성하는 방법을 이용해보자.
이번에는 상속을 이용해 보자.

1. BufferedReader을 상속하는 DataReader 클래스를 만들자.
 - 생성자는 String형으로 열어올 파일의 이름을 인자로 받는다.
 - Data형을 반환하는 readData() 메소드를 구현한다.

2. FileWriter을 상속하는 DataWriter 클래스를 만들자.
 - 생성자는 String으로 저장할 파일의 이름을 인자로 받는다.
 - Data형 객체를 인자로 받아 파일에 쓰는 write() 메소드를 구현한다.

3. 메인에서 새로 작성항 두 클래스를 이용해서 첫번째와 같은 기능을 하는 프로그램을 작성한다.

import java.io.*;
class DataReader extends BufferedReader{
 
 //super을 이용해 조상 클래스에 생성자를 사용한다.
 public DataReader(String name) throws IOException{
  super(new FileReader(new File(name)));
 }
 
 public Data readData() throws IOException{
  Data data = new Data();
  
  String str = null;
  if((str = readLine()) != null){
   String x[] = str.split(" ");
   
   data.setName(x[0]);
   data.setCom(x[1]);
   data.setKor(x[2]);
   data.setEng(x[3]);

   return data;
  }
  else{
   return null;
  }
  
 }
  
}
------------------------------------------------------------------
import java.io.*;
class DataWriter extends FileWriter{
 
 //super을 이용해 조상 클래스에 생성자를 사용한다.
 public DataWriter(String name) throws IOException{
  super(new File(name));
 }
 
 public void write(Data data) throws IOException{
  super.write(data.getName()+"\t"+data.getCom()+"\t"+data.getKor()+
    "\t"+data.getEng()+"\t"+data.pointSum()+"\t"+data.pointAvg()+"\r\n");
 }

}

이번에도 소스에 대한 주석은 적다. 그러나 여기서 볼것은 super이다. 분명 이번에는 조건이 쉽기는 했지만 실제로 하려 하면 super을 모른다면 매우 난해한 조건일 것이다. 그럼 알아보자. super은 무엇인가?

super은 조상 클래스로부터 상속 받은 클래스에서 조상 클래스의 생성자에 접근 하는 방법이다.
쉽게 말해 조상클래스의 생성자를 이용하겠다는 것이다.

 public DataReader(String name) throws IOException{
  super(new FileReader(new File(name)));
 }

위에 소스를 보면 BufferedReader 생성자에 접근 하는 것이다. 우리가 BufferedReader 를 사용 할 때는
 BufferedReader br = new BufferedReader(...
위와 같이 쓴다. 그런데 왜 조상 클래스에 접근을 하는데 저런 모양이 나올까?
쉽게 이해하자. super()은 new BufferedReader리더가 되는거다.

super(new FileReader(new File(name))); 이것을 위 설명과 같이 풀어보면
new BufferedReader(new FileReader(new File(name))); 이렇게 되는 것이다.
이제 조상클래스에 접근하는 방법은 쉽게 이해 될 것이다.

그러나 너무 쉽게만 받아들이지는 말자. super은 this와 비슷한 기능을 하고 this와는 다른 기능도 한다. 기초에서는 쉽게 설명을 하지만 앞으로는 이것에 대해서 확실히 알아두지 않으면 쉽게 짤 수 있는 내용을 너무 어렵게 짤 수 있기 때문이다.

그 증거가 바로 메인 소스에서 나오게 된다.
import java.util.*;

public class Exam0803 {

 public static void main(String[] args) {
  
  Vector vec = new Vector();
  Data data = null;
  
  try{
   DataReader dr = new DataReader("score.txt");
   while((data = dr.readData()) != null){
    vec.add(data);
   }
   
   dr.close();
   
   DataWriter dw = new DataWriter("result2.txt");
   
   Iterator<Data> it = vec.iterator();
   
   while(it.hasNext()){
    dw.write(it.next());
   }
   
   dw.flush();
   dw.close();
      
  }catch(Exception e){}
  
  

 }

}

위에 소를 봐라..상속을 통해 만들어진 2개의 클래스 덕에 엄청 간편해졌다. 실로 자바가 재밌어 지는 순간이다.
그리고 위에 소스에서는 복잡한 Vector을 사용하지 않고 Vector과 Iterator을 이용해서 간편하게 소스를 줄였다.

특별한 주석이 없으니 이번 강좌는 API와 패턴 super에 대한 자료를 참고해서 보면 쉽게 이해가 될 것이다.

이 글과 관련있는 글을 자동검색한 결과입니다 [?]

by 만성피로 | 2008/09/22 23:26 | 정리할 자료들 | 트랙백 | 덧글(1)

트랙백 주소 : http://maydaisy.egloos.com/tb/860634
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by 김교수 at 2008/09/23 23:08
역시 정리가 참 잘되어 있네요. 공부를 완벽하고 깔끔하게 해 나가는 것을 보니 정말 좋습니다. 훌륭한 자바프로그래머가 될 것 같습니다.

:         :

:

비공개 덧글

◀ 이전 페이지다음 페이지 ▶