프로그램을 만들 때 데이터베이스에 이미지 파일을 저장할 일이 종종 생기는데, 안드로이드에서는 해당 작업을 어떻게 수행하는지 알아보도록 하자. 해당 포스팅에서 통신 라이브러리는 Retrofit2
를 사용한다.
create table measurement (
mnum int not null auto_increment primary key,
id varchar(50),
image longblob,
time int,
start_time varchar(30),
end_time varchar(30),
dist double,
kcal double,
FOREIGN KEY measurement(id) references user(id)
) default character set utf8 collate utf8_general_ci;
ALTER TABLE measurement auto_increment = 1;
select * from measurement;
이번 포스팅에서 사용할 테이블은 위의 내용과 같다. 기본적으로 해당 테이블은 유저의 자전거 기록 측정 데이터를 보관할 용도로 사용된다.
MySQL을 설명하는 포스팅이 아니기 때문에 자질구레한 것들은 넘어가고, 주의깊게 봐야하는 부분은 이미지를 보관할 때 longblob
데이터형을 사용한다는 것이다.
Binary Large Object
위의 설명 그대로 바이너리 형태로 이미지, 비디오와 같이 큰 객체, 즉 멀티미디어 객체를 저장하는 용도로 사용된다. 크기에 따라 tinyblob
, blob
, mediumblob
, longblob
으로 사용된다.
<?php
$con = mysqli_connect("YOUR IP", "YOUR DB ID", "YOUR DB PASSWORD", "YOUR DB SCHEMA");
mysqli_query($con, 'SET NAMES utf8');
$requestMethod = $_SERVER["REQUEST_METHOD"];
switch($requestMethod) {
case 'GET' : // GET 방식, 주행기록 출력
$id = $_GET["id"];
$statement = mysqli_prepare($con, "select * from measurement where id = ?");
mysqli_stmt_bind_param($statement, "s", $id);
mysqli_stmt_execute($statement);
mysqli_stmt_store_result($statement);
mysqli_stmt_bind_result($statement, $mnum, $id, $image, $time, $start_time, $end_time, $avg_speed, $dist, $kcal);
$response = array();
$response["success"] = false;
$result = array();
while(mysqli_stmt_fetch($statement)){
$image = base64_encode($image);
$response["success"] = true;
$response["mnum"] = $mnum;
$response["id"] = $id;
$response["image"] = $image;
$response["time"] = $time;
$response["start_time"] = $start_time;
$response["end_time"] = $end_time;
$response["avg_speed"] = $avg_speed;
$response["dist"] = $dist;
$response["kcal"] = $kcal;
array_push($result, array(
"success"=>$response["success"],
"mnum" => $response["mnum"],
"id" => $response["id"],
"image" => $response["image"],
"time" => $response["time"],
"start_time" => $response["start_time"],
"end_time" => $response["end_time"],
"avg_speed" => $response["avg_speed"],
"dist" => $response["dist"],
"kcal" => $response["kcal"]));
}
echo json_encode($result);
break;
case 'POST' : // POST 방식, 주행기록 추가
$id = $_POST["id"];
$image = $_POST["image"];
$time = $_POST["time"];
$dist = $_POST["dist"];
$kcal = $_POST["kcal"];
$avg_speed = $_POST["avg_speed"];
$start_time = $_POST["start_time"];
$end_time = $_POST["end_time"];
$image = base64_decode($image);
$statement = mysqli_prepare($con, "INSERT INTO measurement(id,image, time, start_time, end_time, avg_speed, dist,kcal) VALUES (?,?,?,?,?,?,?,?)");
mysqli_stmt_bind_param($statement, "ssissddd", $id, $image, $time, $start_time, $end_time, $avg_speed, $dist, $kcal);
mysqli_stmt_execute($statement);
$response = array();
$response["success"] = true;
echo json_encode($response);
break;
default :
break;
}
mysqli_close($con);
?>
안드로이드에서 서버로 요청하는 HTTP 메소드가 어떤 것인지 파악한 후, 메소드에 맞춰 필요한 데이터를 MySQL 과 주고받는 코드이다. 해당 PHP 코드에 대한 기본적인 설명은 따로 포스팅이 존재하니 도움이 될 것이다. 😊
안드로이드에서 MySQL로 이미지를 보낼 때 Base64 문자열 디코딩 (base64_decode
)을 해주고, 반대로 MySQL에서 안드로이드로 이미지를 보낼때는 Base64 문자열 인코딩(base64_encode
)을 실시해주면 된다.
모든 요청마다 이미지를 인코딩/디코딩 하는 과정에서 서버에서 많은 처리 연산이 일어나므로 속도가 느려지는 것은 감안해야 한다. 😖
바이너리 데이터를 공통 ASCII 영역의 문자로만 이루어진 문자열로 바꾸는 인코딩
바이너리 데이터를 전송할 때 일반 ASCII로 인코딩하여 전송하면 문제가 발생한다.
ASCII는 기본적으로 문자를 7비트 인코딩으로 표현하는데, 8비트를 채우기 위해 나머지 1비트를 처리하는 방식이 시스템별로 달라 시스템 간 데이터를 전달하기에 안전하지 않다는 것이다.
그래서 그 점을 보완하고자 Base64가 등장해 ASCII 중 제어문자와 일부 특수문자를 제외한 64개의 안전한 출력 문자만 사용하게끔 도와주는 것이다. 초기 문자보다 데이터의 크기가 커지는 단점이 있지만 웹 상에서 공통적으로 사용할 수 있기 때문에 많이 쓰인다.
이제 안드로이드에서 취해야 할 행동들을 정의하면 된다.
public class Measure {
@SerializedName("success")
private boolean success;
@SerializedName("mnum")
private int mnum;
@SerializedName("id")
private String id;
@SerializedName("image")
private String image;
@SerializedName("time")
private int time;
@SerializedName("start_time")
private String start_time;
@SerializedName("end_time")
private String end_time;
@SerializedName("avg_speed")
private double avg_speed;
@SerializedName("dist")
private double dist;
@SerializedName("kcal")
private double kcal;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
. . . 중략
}
데이터 모델은 늘 그렇듯 받아올 데이터를 그대로 설정해주면 된다.
public interface MeasureService {
String MEASURE_URL = BuildConfig.SERVER; // 라즈베리파이 서버
@GET("/android/userinfo/measure/measurement.php")
Call<List<Measure>> getMeasure(
@Query("id") String id
);
@FormUrlEncoded
@POST("/android/userinfo/measure/measurement.php")
Call<CheckSuccess> insertMeasure(
@Field("id") String id,
@Field("image") String image,
@Field("time") int time,
@Field("start_time") String start_time,
@Field("end_time") String end_time,
@Field("avg_speed") double avg_speed,
@Field("dist") double dist,
@Field("kcal") double kcal
);
}
annotation 으로 HTTP 메소드를 지정하고, 보낼 데이터를 정의한다. Retrofit2
에 대한 이해가 있다면 어렵지 않을 것이다.
해당 다이얼로그에서 확인 버튼을 누르면 사진 속의 비트맵을 MySQL에 전송하는 코드를 구성해보도록 하자.
Bitmap bitmap = your_bitmap; // 데이터베이스로 보낼 비트맵 선언
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 바이트 배열 데이터 출력 클래스 선언
bitmap.compress(Bitmap.CompressFormat.PNG, 50, baos);
// 비트맵을 PNG로 저장하며 압축한다. 해당 코드에서는 50%로 압축
byte[] bytes = baos.toByteArray();
// 압축 내용을 바이트 배열로 변환 후 bytes 변수에 저장
String mapImage_String = Base64.encodeToString(bytes, Base64.DEFAULT);
// 바이트 형식에서 스트링 형식으로 변환
// 개행문자에 조심해야 한다면 매개변수로 Base64.NO_WRAP 사용
insertButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Gson gson = new GsonBuilder().setLenient().create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(MeasureService.MEASURE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
MeasureService retrofitAPI = retrofit.create(MeasureService.class);
// 데이터베이스 저장
retrofitAPI.insertMeasure(userId, mapImage_String, timer, s_time, f_time, avg_speed, sum_dist, 10.00).enqueue(new Callback<CheckSuccess>() {
@Override
public void onResponse(Call<CheckSuccess> call, retrofit2.Response<CheckSuccess> response) {
if (response.isSuccessful()) {
Log.d("Measurement", "Response");
dismiss();
} else {
Toast.makeText(context, "오류 발생", Toast.LENGTH_SHORT);
dismiss();
}
}
@Override
public void onFailure(Call<CheckSuccess> call, Throwable t) {
Log.d("Measurement", "Not Response");
loadingDialog.dismiss();
t.printStackTrace();
}
});
}
});
코드를 보았다면 이제 어떤식으로 돌아가는지는 감이 올 것이다. 🤗
이미지를 Base64 와 함께 문자열 형태로 바꾸는 것이 포인트이다. Retrofit 통신을 통해 MySQL로 데이터를 전송하게 되며, MySQL 에서는 이 Base64 형태의 문자열을 디코딩한 후 Blob 데이터로 이미지를 저장하게 된다.
위의 사진과 같이 리사이클러뷰에 이미지를 담아내보자.
String mapString = datas.get(position).getImage(); // 데이터에서 이미지를 받아온다.
Bitmap bitmap = StringToBitmaps(mapString);
// String -> Bitmap 변환
public static Bitmap StringToBitmaps(String image) {
try {
byte[] encodeByte = Base64.decode(image, Base64.DEFAULT);
// Base64 코드를 디코딩하여 바이트 형태로 저장
Bitmap bitmap = BitmapFactory.decodeByteArray(encodeByte, 0, encodeByte.length);
// 바이트 형태를 디코딩하여 비트맵 형태로 저장
return bitmap;
} catch (Exception e) {
e.getMessage();
return null;
}
}
리사이클러뷰에서 이미지를 넣는 부분이다.
크게 어려울 점은 없다. PHP에서 Base64 로 인코딩하여 안드로이드로 전송하니 안드로이드에선 그대로 디코딩하여 사용하면 된다.
코드를 이해했다면 흐름도 같이 이해할 수 있었을 것이다.
Base64를 이용해 이미지를 보내는 것이 포인트이다.
흐름을 완벽히 이해해 안드로이드에서 이미지를 자유자재로 다룰 수 있길 바란다.
참고 및 출처
Base64 인코딩이란
mysql - MySQL-Base64 및 BLOB
Android에서 image BLOB MySQL에 저장하기
저 안녕하세요 혹시 이 코드들은 어디에 작성을 해야하나요?