빠른 개발을 위해 SQLite를 사용하기로 했지만 개발하는 과정에서 마주했던 문제들이 있었다.
SQLite는 앱 실행 시 모든 데이터를 새로 입력하고 데이터베이스를 생성하는 방식이라
초기 데이터가 많은 경우 로딩 시간이 오래 걸린다는 단점이 있었다.
오늘은 이 단점을 해결해 나간 과정에 대해서 작성해보도록 하겠다!!
🤔 첫번째 의문점
보통 SQLiteOpenHelper 클래스를 상속받아 앱 실행 시
onCreate() 메서드를 통해 처음 데이터베이스를 생성하고 테이블을 정의하여 초기 데이터를 입력할 수 있다.
술안주 월드컵 앱은 다수의 메뉴 데이터와 이미지 경로, 위치 정보 등을 데이터베이스에 저장해야 하는데, 앱 실행 시마다 데이터를 새로 입력하고 데이터베이스를 생성하는 방식은 로딩 시간이 오래 걸려 비효율적이지 않을까 하는 생각이 들었다.
이미 만들어 놓은 데이터베이스 테이블과 데이터 정보를 저장해둔
.sql 파일을 바로 이용할 수 있는 방법이 없을까?
🎀 해결 과정
1.DB Browser for SQLite를 사용해서 미리 구축한 DB를 저장해둔다.
2. Assets 폴더 생성 후, 이 폴더 안에 미리 만들어둔 DB 파일을 넣는다.
위 이미지처럼 worldcupDB.db 파일을 assets 폴더 내에 복사하면
/main/assets/worldcupDB.db 경로를 가진다.
만약 DB파일 접근 권한 오류가 발생하면
AndroidManifest.xml 파일에 권한을 추가해야 한다.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
📜 DataBaseHelper.java
public class DataBaseHelper extends SQLiteOpenHelper {
private final static String TAG = "DataBaseHelper";
private static String DB_PATH = "";
private final static String DB_NAME = "worldcupDB.db";
private static final int DB_VERSION = 1;
private SQLiteDatabase mDataBase;
private Context mContext;
public DataBaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
DB_PATH = "/data/data/" + context.getPackageName() + "/databases/";
this.mContext = context;
dataBaseCheck();
}
private void dataBaseCheck() {
File dbFile = new File(DB_PATH + DB_NAME);
if (!dbFile.exists()) {
dbCopy();
Log.d(TAG,"Database is copied.");
}
}
@Override
public synchronized void close() {
if (mDataBase != null) {
mDataBase.close();
}
super.close();
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(TAG,"onCreate()");
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
Log.d(TAG,"onOpen() : DB Opening!");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.d(TAG,"onUpgrade() : DB Schema Modified and Excuting onCreate()");
}
private void dbCopy() {
try {
File folder = new File(DB_PATH);
if (!folder.exists()) {
folder.mkdir();
}
String out_filename = DB_PATH + DB_NAME;
InputStream inputStream = mContext.getAssets().open(DB_NAME);
OutputStream outputStream = new FileOutputStream(out_filename);
byte[] mBuffer = new byte[1024];
int mLength;
while ((mLength = inputStream.read(mBuffer)) > 0) {
outputStream.write(mBuffer,0,mLength);
}
outputStream.flush();;
outputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
Log.d("dbCopy","IOException 발생함");
}
}
}
DataBaseHelper 생성자에서 생성과 동시에 dataBaseCheck() 메서드에서
해당 DB파일이 없다면 assets 폴더의 DB파일을 복사해오는 로직이 있다.
SQLite는 내장 데이터베이스이기에 복사된 DB파일은 Device File Explorer에서 아래 경로로 확인할 수 있다.
date > data > com. (프로젝트 이름) > databases > (DB 이름).db
🤔 두번째 의문점
데이터베이스의 menu 테이블에서 선택된 라운드에 따라 16개, 8개, 4개의 메뉴를 가져오는 코드 구현해야 한다.
하지만 이러한 문제가 있었다..
1. SQLite에서는 특정 갯수에 맞게 랜덤으로 컬럼을 선택하는 내장 함수가 없다
2. Cursor는 특정 행을 index로 접근할 수 있는 마땅한 메서드가 없다.
커서의 첫 행, 마지막 행 또는 현재 커서의 다음 행으로만 이동 가능하다.
3. 메인 스레드에서 데이터베이스 쿼리와 같은 긴 작업 수행으로 인해
ANR(Application Not Responding) 오류 발생 가능 → 즉, UI가 응답하지 않음.
🎀 해결 과정
1. 라운드에 맞는 갯수만큼 랜덤으로 인덱스를 추출하여 List에 저장한다.
랜덤 인덱스가 저장된 리스트를 반환하기 전 오름차순으로 정렬한다.
2. cursor.getPosition() 메서드를 사용해 현재 커서가 가르키고 있는 행의 index와
랜덤 index가 동일한 데이터만 menuList에 저장한다.
3. ANR 오류를 방지하기 위해 스레드풀을 사용하여 데이터베이스 작업은 백그라운드 스레드에서 처리한다.
📜 WorldCupActivity
public class WorldCupActivity extends AppCompatActivity {
int round;
DataBaseHelper worldCupDB;
SQLiteDatabase db;
List<MenuInfo> menuList;
List<Integer> randomColumnIndexes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_worldcup);
round = getIntent().getIntExtra("ROUND_NUMBER", 0);
Log.d("WorldCupActivity", round + "round start");
worldCupDB = new DataBaseHelper(WorldCupActivity.this);
db = worldCupDB.getWritableDatabase();
// Run database operation in a new thread
new Thread(() -> {
getMenuByRound();
}).start();
}
private void getMenuByRound() {
Cursor cursor = db.rawQuery("SELECT * FROM menu;", null);
if (cursor != null && cursor.moveToFirst()) {
int totalColumns = cursor.getCount();
Log.d("WorldCupActivity","DB select Record : " + totalColumns);
randomColumnIndexes = getRandomIndexes(totalColumns, round);
menuList = new ArrayList<>();
do {
MenuInfo menuInfo;
for (int index : randomColumnIndexes) {
if(cursor.getPosition() == index){
String menuName = cursor.getString(1);
String imagePath = cursor.getString(2);
menuInfo = new MenuInfo(menuName, imagePath);
menuList.add(menuInfo);
}
}
} while (cursor.moveToNext());
Log.d("WorldCupActivity","Menu information list complete");
cursor.close();
} else {
Log.d("Menu", "No data available");
}
runOnUiThread(() -> {
// UI-related operations after database operation
printMenuList();
});
}
private List<Integer> getRandomIndexes(int totalColumns, int limit) {
List<Integer> indexes = new ArrayList<>();
Random random = new Random();
while (indexes.size() < limit) {
int randomIndex = random.nextInt(totalColumns);
if (!indexes.contains(randomIndex)) {
indexes.add(randomIndex);
}
}
Collections.sort(indexes);
Log.d("WorldCupActivity", "random number size : " + indexes.size());
return indexes;
}
private void printMenuList() {
if (menuList != null && !menuList.isEmpty()) {
for (MenuInfo menuInfo : menuList) {
Log.d("MenuInfo", "Menu Name: " + menuInfo.getMenuName() + ", Image Path: " + menuInfo.getImagePath());
}
} else {
Log.d("MenuInfo", "MenuList is empty or null");
}
}
}
8강 선택시 8개의 술안주 메뉴를 성공적으로 추출하는 것을 아래 log를 통해 확인할 수 있다.
'프로젝트' 카테고리의 다른 글
[Android Studio] SQLite 데이터베이스 구축 (0) | 2024.11.14 |
---|---|
[Android Studio] 술안주 월드컵 제안서 (0) | 2024.11.14 |