수업 정리

73일차 수업 정리(Android - 파일 입출력, SQLite)

Vita500cc 2020. 7. 20. 18:26

**파일 입출력

    => 안드로이드의 파일 시스템은 리눅스의 것을 그대로 이용

    => 자바의 파일 입출력 API - java.io 패키지

        - File : 파일에 대한 정보와 관련된 클래스

        - 바이트 단위 입출력 클래스

          -> InputStream  -> FileInputStream(파일) -> BufferedInputStream(버퍼 사용)

          ->  OutputStream -> FileOutputStream(파일) -> PrintStream(버퍼 사용)

          -> Serializable :  객체 다누이로 입출력하기 위한 인터페이스이고 스트림은 ObjectInputStream, ObjectOutputStream

        - 문자 단위 입출력 클래스

          -> Reader -> FileInputStreamReader(파일) -> BufferedReader(버퍼사용)

          -> Writer -> FileOutputStreamWriter(파일) -> PrintWriter(버퍼사용)

        - 위의 클래스 들을 이용해서 파일이나 콘솔에 입출력 하기도 하지만 네트워크에서 데이터를 다운받거나 업로드 시도 사용

    => 자바 입출력 라이브러리를 사용하는데 보안 문제로 인해 Context클래스에서 파일 입출력 관리 메소드를 제공

        - FileOutputStream openFileOutput(String name, int mode)

        - FileInputStream openFileInput(String name)

    => 안드로이드에서는 자신의 디렉토리 영역에만 접근이 가능

        - 앱의 데이터 디렉토리는 /data/data/패키지명/files 디렉토리

    => 앱이 설치되면 자신의 자원을 가진 번들 디렉토리와 데이터를 읽고 있는 /data/data/패키지명/file 디렉토리가 생성

    => 번들 디렉토리에 존재하는 파일은 읽을 수 있지만 수정 불가

        - 수정시 앱을 재배포하여 승인을 받아야 함

    => mode는 4가지

        - MODE_PRIVATE : 현재 앱만 사용가능, 수정시 이전내용이 지워지고 기록

        - MODE_APPEND : 현재 앱만 사용가능, 수정시 이전내용 뒤에 기록

        - MODE_WORLD_READABLE : 다른 앱이 읽을수 있도록 생성 - ContentProvider를 이용

        - MODE_WORLD_WRITEABLE : 다른 앱이 기록할 수 있도록 생성 - ContentProvider를 이용

    => 공용 디렉토리 : 모든 앱이 사용할 수 있는 디렉토리

        - Environment.DIRECTORY_ALARMS : 알람 디렉토리 - 오디오 디렉토리

        - Environment.DIRECTORY_DCIM : 사진 디렉토리

        - Environment.DIRECTORY_DOWNLOADS : 다운로드 디렉토리 

        - Environment.DIRECTORY_MUSIC : 음악 디렉토리 

        - Environment.DIRECTORY_MOVIE : 영화 디렉토리

        - Environment.DIRECTORY_NOTIFICATIONS : 알림으로 사용할 오디오 디렉토리 

        - Environment.DIRECTORY_PICTURE : 사진 디렉토리

    => 다운로드 받은 파일을 저장하기 위해서 공용 디렉토리 경로 생성

        - File f = new

        - File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTUES), "파일명);

    => 번들에 포함된 파일을 읽기

        - 안드로이드에서 이진 파일을 앱 내에 포함하고자 할 때는 res 디렉토리에 raw라는 디렉토리에 복사

        - 이 파일의 내용을 읽고자 하는 경우 InputStream openRawResource(int id) 메소드를 호출하여 스트림을 만들어 읽음

 

**파일 입출력 실습

1. Android Project 생성

 

2. 앱 안에서 사용할 텍스트 파일 추가

  1) res 디렉토리에 raw라는 디렉토리 생성

 

  2) 파일을 raw 디렉토리에 복사 - creed.txt

 

3. activity_main.xml 파일에서 화면 디자인

    => Button 4개(리소스에서 읽기, 파일 기록, 기록한 파일 읽기, 삭제)와 문자열을 입력할 EditText 1개를 배치

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="리소스에서 읽기"
        android:id="@+id/resread"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="파일 기록"
        android:id="@+id/filewrite"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="기록한 파일 읽기"
        android:id="@+id/fileread"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="파일 삭제"
        android:id="@+id/filedelete"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/content"/>

</LinearLayout>

 

4. 레이아웃에 디자인 한 뷰들을 전부 찾아서 변수에 대입 - Activity 파일에 작성

  1) Activity 파일에 뷰들의 참조형 변수를 인스턴스로 선언

//디자인 한 뷰들의 참조를 저장 할 변수 선언
private Button resread;
private Button filewrite;
private Button fileread;
private Button filedelete;
private EditText content;

 

  2) onCreate 메소드에서 위의 변수에 뷰를 찾아서 대입하는 코드를 작성

//xml파일에 디자인 한 뷰 찾아오기
resread = (Button)findViewById(R.id.resread);
fileread = (Button)findViewById(R.id.fileread);
filewrite = (Button)findViewById(R.id.filewrite);
filedelete = (Button)findViewById(R.id.filedelete);
content = (EditText) findViewById(R.id.content);

 

5. 버튼의 이벤트 처리 코드를 onCreate에 추가

//버튼의 클릭을 처리할 이벤트 핸들러
Button.OnClickListener clickListener = new Button.OnClickListener(){

	@Override
	public void onClick(View view) {
		switch (view.getId()){
			case R.id.resread:
				try {
					//creed.txt 파일의 내용을 읽어서 content에 출력
					//creed.txt 파일은 raw디렉토리에 존재
					InputStream fis = getResources().openRawResource(R.raw.creed);
					//파일의 내용 한번에 전부 읽기
					byte[] data = new byte[fis.available()];
					fis.read(data);
					fis.close();

					//읽어낸 데이터를 문자열로 변환
					String msg = new String(data);
					//출력
					content.setText(msg);
				}catch (Exception e){
					Log.e("파일 읽기 에러", e.getMessage());
				}
				break;
                
			case R.id.filewrite:
				try {
					//기록할 파일의 경로를 생성
					FileOutputStream fos = openFileOutput("data.txt", Context.MODE_PRIVATE);
					//내용을 기록
					fos.write(content.getText().toString().getBytes());
					fos.close();
				}catch (Exception e){
					Log.e("파일 읽기 에러", e.getMessage());
				}
				break;
			case R.id.fileread:
				break;
			case R.id.filedelete:
				break;
		}
	}
};
//버튼과 이벤트 핸들러 연결
resread.setOnClickListener(clickListener);
fileread.setOnClickListener(clickListener);
filewrite.setOnClickListener(clickListener);
filedelete.setOnClickListener(clickListener);

 

**안드로이드에서 앱 내에서 생성한 파일 확인

    => [View] - [Tool Window] - [Device File Explorer]를 실행

    => data/data/자신의 패키지명/files 에서 확인 가능

    => 앱을 재설치, 재실행해도 기존에 존재하는 파일은 삭제되지 않으므로 Explorer나 에뮬레이터 디렉토리를 찾아 직접 삭제해야 함

 

**파일 입출력 기능

    => 간단한 텍스트 데이터를 앱에 저장할 때 사용

    => In App Puarchase를 구현하는 경우(인앱 구매)

        - 아이템 구매시 서버에 저장해야 하지만, 실제 아이템 사용시 앱 내에서 해당 데이터를 로드하는 경우 서버에서 가져오면 너무 느리므로 파일, 설정에 아이템 구매 항목을 저장해두고 주기적으로 업데이트하도록 만들고 실제 아이템은 파일에서 읽어서 설정

 

**SQLite

1. 개요

    => Embedded 분야에 많이 사용되는 관계형 데이터베이스

    => 무료, Android, iOS, WebBrowser, Python 언어에 내장된 데이터 베이스

    => C언어로 제작됨

    => 안드로이드에서는 자바 기반의 wrapper 클래스가 제공되어 이 Wrapper 클래스를 이용하여 사용

    => iOS에서는 Wrapper 클래스가 Objective-C로 제공(3rd Party 라이브러리)되기 때문에 Bridge를 이용하여 Swift로 변환하여 사용

    => 안드로이드에서는 data/data/패키지명/databases 디렉토리에 데이터가 저장

 

2. 안드로이드에서의 사용

  1) SQLiteOpenHelper

    => SQLite 사용을 위한 도우미 클래스

    => 이 클래스를 상속받는 클래스를 만들고 3개의 메소드를 재정의

    => 생성자, onCreate, onUpgrade

    => 생성자에서는 SQLiteOpenHelper의 생성자를 호출하여 Context와 데이터베이스 파일명, 표준 커서 사용여부와 버전을 설정

    => onCreate에서는 테이블을 만들고 기본데이터를 삽입하는 작업을 수행

    => onUpgrade에는 버전이 변경되었을 때 호출되는 메소드인데 이때는 기존 테이블의 내용을 지우고 다시 생성

  2) SQLite Database 클래스

    => SQLite에 명령을 전달하고 명령을 실행하여 결과를 가져오는 클래스

    => SQLiteOpenHelper 클래스의 getReadableDatabase 나 getWritableDatabase 메소드를 이용하여 객체 생성

    => SQL 실행

        - execSQL() : 결과 데이터를 반환하지 않는 SQL실행

        - rawQuery() : 쿼리를 실행하고 일치된 결과를 Cursor 객체 형태로 반환

          (Cursor - Iterator, Enumeration, 빠른 열거와 같은 개념 - 데이터의 목록을 앞에서부터 순서대로 하나씩 접근하기 위한 포인터)

        - query() :  쿼리를 실행하고 일치된 결과를 Cursor 객체 형태로 반환

    => ContentValues를 매개변수로 받는 insert, update, delete 메소드가 있음

        - ContentValues는 key와 values로 구성되는 일종의 Map

        - ORM 형식으로 데이터를 삽입, 수정, 삭제 가능

 

3. 자바에서 데이터베이스 연동

  1) 순수한 JDBC코드(Framework없이 수행 - Connection, Steatment, ResultSet)를 가지고 연동

    => Steatement(Statement, PreparedStatement, CallableStatement)

  2) 프레임 워크를 이용하여 연동

    => SQL Mapper : SQL과 Java코드를 분리 - MyBatis

    => ORM : Class와 Table을 매핑하는 방식 - Hibernate

 

**SQLite 파일을 직접 제작하거나 확인

    => DBeaver와 같은 데이터베이스 접속프로그램을 이용하거나 Firefox, Chrome의 Plug-in을 이용

 

**SQLite를 이용한 CRUD(Create, Read, Update, Delete)

    => 번호, 이름, 나이를 저장하는 테이블을 이용

1. 실행가능한 Activity를 추가(SqliteActivity)

 

2. layout 파일에 삽입, 갱신, 삭제, 전체 데이터 조회를 위한 버튼 4개와 출력할 TextView 1개 작성 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SqliteActivity">
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="삽입"
        android:id="@+id/btnCreate"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="갱신"
        android:id="@+id/btnUpdate"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="삭제"
        android:id="@+id/btnDelete"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="전체 조회"
        android:id="@+id/btnRead"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/txtdisplay"/>
</LinearLayout>

 

3. SQLite 사용을 위한 SQLiteOpenHelper 클래스로부터 상속받는 클래스를 생성

package com.example.fileinput_0720;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class PersonDBHelper extends SQLiteOpenHelper {
    //상위 클래스에 DefaultConstructor가 없어서 생성자를 직접 생성
    public PersonDBHelper(Context context){
        //상위 클래스의 생성자 호출
        super(context, "person.db", null, 1);
    }
    //데이터베이스를 처음 사용할 때 호출 되는 메소드
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        //샘플 테이블을 생성 - 샘플 데이터 추가
        sqLiteDatabase.execSQL("create table person(num INTEGER primary key autoincrement, name TEXT, age INTEGER);");
    }

    //SQLite 또는 App의 버전이 변경된 경우 호출되는 메소드
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        //기존의 데이터를 제거하고 새로 생성
        sqLiteDatabase.execSQL("drop table person");
        onCreate(sqLiteDatabase);
    }
}

 

4. SqliteActivity.java 파일에 인스턴스 변수를 선언

    => View에 대한 변수를 생성

    => PersonDBHelper 변수 생성

//뷰에 대한 참조
private Button btnCreate, btnUpdate, btnDelete, btnRead;
private TextView txtdisplay;
//데이터베이스 사용을 위한 변수
private PersonDBHelper dbHelper;

 

5. SqliteActivity.java 파일의  onCreate 메소드에서 변수에 대한 참조를 생성

    => 클래스를 직접 만들고 생성자를 호출하여 객체를 생성하는 경우 : 생성자에서 초기화

         객체를 직접생성하지 않는 경우 : API에서 권장하는 메소드에서 초기화

    => Android의 Activity 클래스에서는 onCreate에서 초기화하는 것을 권장

//뷰에 대한 참조 생성
btnCreate = (Button)findViewById(R.id.btnCreate);
btnUpdate = (Button)findViewById(R.id.btnUpdate);
btnDelete = (Button)findViewById(R.id.btnDelete);
btnRead = (Button)findViewById(R.id.btnRead);
txtdisplay = (TextView)findViewById(R.id.txtdisplay);

//데이터베이스 사용을 위한 참조도 생성
dbHelper = new PersonDBHelper(SqliteActivity.this);

 

6. SqliteActivity.java 파일에 버튼 4개를 클릭했을 때 수행할 내용을 작성

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sqlite);

	//뷰에 대한 참조 생성
	btnCreate = (Button)findViewById(R.id.btnCreate);
	btnUpdate = (Button)findViewById(R.id.btnUpdate);
	btnDelete = (Button)findViewById(R.id.btnDelete);
	btnRead = (Button)findViewById(R.id.btnRead);
	txtdisplay = (TextView)findViewById(R.id.txtdisplay);

    //데이터베이스 사용을 위한 참조도 생성
    dbHelper = new PersonDBHelper(SqliteActivity.this);

    //버튼 4개의 클릭 이벤트를 처리할 객체를 생성
    Button.OnClickListener buttonHandler = new Button.OnClickListener() {
        @Override
        public void onClick(View view) {
            //변수를 선언하고 시작
            //데이터베이스 사용을 위한 변수
            SQLiteDatabase db;
            //데이터를 저장하기 위한 변수
            ContentValues row;
            switch (view.getId()){
                case R.id.btnCreate:
                    //데이터 삽입
                    //데이터베이스 사용 객체를 생성
                    db = dbHelper.getWritableDatabase();
                    //SQL 실행
                    //db.execSQL("insert into person(name, age) values('최성권', 30);");

                    //ORM형태로 데이터 삽입
                    row = new ContentValues();
                    row.put("name", "이봉청");
                    row.put("age", 37);
                    //데이터 삽입
                    db.insert("person", null, row);

                    //데이터베이스 닫기
                    dbHelper.close();
                    break;
                case R.id.btnRead:
                    //데이터베이스 사용 객체를 생성
                    db = dbHelper.getReadableDatabase();
                    //데이터베이스에서 읽기 작업 수행
                    Cursor cursor = db.rawQuery("select * from person", null);
                    //각행을 읽어서 하나의 문자열로 만드는 작업
                    StringBuilder sb = new StringBuilder();
                    //행 단위로 검색 결과 읽기
                    while(cursor.moveToNext()){
                        String name = cursor.getString(1);
                        int age = cursor.getInt(2);
                        sb.append(name + " " + age + "\n");
                    }
                    //결과를 문자열로 만들기
                    String msg = sb.toString();
                    if(msg.length() == 0)
                        txtdisplay.setText("읽은 데이터 없음");
                    else
                        txtdisplay.setText(msg);

                    //사용한 객체 정리
                    cursor.close();
                    dbHelper.close();
                    break;
                case R.id.btnUpdate:
                    db = dbHelper.getWritableDatabase();
                    //변경할 데이터 생성
                    row = new ContentValues();
                    row.put("age", 33);
                    //person 테이블에서 name이 최성권인 데이터를 row로 변경
                    db.update("person", row, "name='최성권'", null);
                    db.close();
                    break;
                case R.id.btnDelete:
                    db = dbHelper.getWritableDatabase();

                    //삭제 구문 수행
                    //delete from person where name = '최성권'
                    db.delete("person", "name='최성권'", null);
                    db.close();
                    break;
            }
        }
    };

    //버튼과 이벤트 핸들러 연결
    btnCreate.setOnClickListener(buttonHandler);
    btnRead.setOnClickListener(buttonHandler);
    btnUpdate.setOnClickListener(buttonHandler);
    btnDelete.setOnClickListener(buttonHandler);
}

 

**클래스 상속 받았을 때의 에러

    => final class라 상속받을 수 없는 경우

    => 모든 Member가 static 에서 생성자를 private로 만들었을 때

    => singleton이나 factory method pattern을 적용하기 위해 생성자를 public 으로 만들지 않은 경우

    => 상위 클래스에 추상 메소드가 있는 경우

    => 매개변수가 없는 생성자(default constructor)가 없는 경우

 

**Tip(공부해보기)

1. 연산자

  1) ++, -- 에서의 전위 연산과 후위 연산 차이

  2) ==와 equals의 차이

  3) &&와 &의 차이

  4) &&에서 뒤의 연산을 수행하지 않는 경우

 

2. 제어문

  1) 분기문의 종류와 차이

  2) 반복문의 종류

  3) break와 continue의 역할