본문 바로가기
개발 시행착오 정리

[안드로이드] 이미지 여러 장 처리 시행착오 및 구현 과정

by 문톰 2022. 1. 27.

1.시행착오

1)서버에 대한 개념 부족으로 인한 문제

-다른 회원한테는 나의 프로필사진이 보이지 않는 문제가 생겼었다. 알고보니 이미지를 서버에 저장해서 DB에 이미지의
URI를 저장하는 방식을 사용했어야 했는데 당시에는 서버와 클라이언트에 대한 이해가 부족해서 그런 생각을 하지 못했었다. 

 

 

2)파일 용량 오류 

413 Request Entity Too Large
The server is refusing to process a request because the request entity is larger than the server is willing or able to process. The server MAY close the connection to prevent the client from continuing the request.

 

서버의 이미지를 업로드할 때 허용 용량이 정해져있다 이 용량을 php.ini 파일에서 수정해야 한다.

post_max_size = 400M // POST로 서버에 보낼 수 있는 최대 용량
file_uploads = On // 파일 업로드 가능
upload_max_filesize = 300M // 최대 파일 사이즈
max_file_uploads = 20 // 한꺼번에 업로드할 수 있는 최대 파일 개수

 

3)절대경로 변환

-처음에 디바이스에 있는 이미지의 경로를 갖고오면 content://~~ 형식으로 되어있는데 이미지를 파일형식으로 변환하기 위해서는 이미지의 경로를 절대경로로 바꿔줘야한다.

-하지만 Cursor를 사용하여 MediaStore에 접근하여 절대경로를 구하는 메서드가 보안상의 이유로 Deperated되어서 다른 방법을 찾게 되었고 ContentResolver를 사용해서 접근 하는 방식으로 구현했었습니다.

// 절대경로 파악할 때 사용된 메소드
@Nullable
fun createCopyAndReturnRealPath(context: Context, uri: Uri): String? {
    val contentResolver: ContentResolver = context.getContentResolver() ?: return null


    // 파일 경로를 만듬
    val filePath: String = (context.getApplicationInfo().dataDir + File.separator
            + System.currentTimeMillis())
    val file = File(filePath)
    try {
        // 매개변수로 받은 uri 를 통해  이미지에 필요한 데이터를 불러 들인다.
        val inputStream = contentResolver.openInputStream(uri) ?: return null
        // 이미지 데이터를 다시 내보내면서 file 객체에  만들었던 경로를 이용한다. 
        val outputStream: OutputStream = FileOutputStream(file)
        val buf = ByteArray(1024)
        var len: Int
        while (inputStream.read(buf).also { len = it } > 0) outputStream.write(buf, 0, len)
        outputStream.close()
        inputStream.close()
    } catch (ignore: IOException) {
        return null
    }
    return file.getAbsolutePath()
}

 

Deperated된 부분 

val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)

-이 부분을 대체하기 위해서 코드를 변경함.

 

 

4)Bitmap을 사용해서 BLOB형식을 DB에 저장했을 때 생겼던 문제

1)이미지 용량 문제

이미지를 Bitmap방식으로 로딩 했을 경우 서버에 보내서 DB에 저장할 때 BLOB 형식으로 DB에 저장하는 방식을 사용하면 작은 이미지일 경우에는 상관이 없지만 이미지의 개수가 많거나 이미지의 용량이 엄청 큰 경우에는 DB에 저장했을 때 부하가 심해져서 저장 및 읽기가 느려지는 현상이 생겼었다.

 

 

비트맵 크기 조절 메서드

2)이미지 화질 문제

-비트맵을 16진법의 BLOB 형식으로 변환하는 과정에서 비트맵 크기가 크면 사이즈를 조절해주어야 하는데 이 과정에서 이미지가 손실되어서 화질이 안좋아지는 문제가 발생했었다. 

 

 

 

5)해결

-그래서 최종적으로는 이미지를 File형식으로 변환 후  Volley+라이브러리를 사용해서 서버에 File형식으로 이미지를 보낸 후에 서버에 이미지를 저장하고 저장한 이미지의 URI를 DB에 저장하는 방식으로 구현했습니다. 

 

 

 

2.구현 과정

1)권한 설정 및 Gradle 라이브러리 추가

 

2)intent와 MediaStore객체를 통해 에뮬레이터에 있는 이미지의 경로를 갖고온 후 ArrayList에 담는다.

 

3)ArrayList에 담긴 개수만큼 이미지의 경로를 절대경로로 바꾸어 준다.

 

4)절대경로로 바꾼 이미지를 File형식으로 변환한다.

 

5)File형식으로 변환한 이미지들을 Volley나 Retrofit을 사용해서 서버에 보낸다.

 -Volley의 경우 SimpleMultiPartRequest 객체를 사용한다.

val smpr: SimpleMultiPartRequest = object : SimpleMultiPartRequest(

 

6)보낸 File형식의 이미지들을 서버에서 받은 후 이미지의 개수만큼 서버에 다운받는다.

 

7)그리고 File형식의 이미지들의 url(경로를)를 JSON형식으로 DB에  저장한다.

 

8)이미지를 로딩할 때는 Glide라이브러리를 사용해서 로딩.

 

3.코드

1)권한 설정 및 Gradle 라이브러리 추가

-권한 설정

 

Manifest.xml

<!-- 읽기 쓰기 권한 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--Volley+ 사용시 필요 -->
<uses-library
    android:name="org.apache.http.legacy"
    android:required="false" />

 

-라이브러리 추가

Gradle.Module

//Volley+
implementation 'dev.dworks.libs:volleyplus:0.1.4'
//글라이드
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
//테드 퍼미션 (권한을 편하게 설정해주는 라이브러리)
implementation "gun0912.ted:tedpermission:2.1.0"
lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

Intent를 사용해서 갤러리에서 이미지의 Uri를 갖고오는 코드

//버튼 클릭 리스너 메서드
override fun onClick(v: View?) {
    when (v?.id) {
        //1. 이미지 추가 버튼
        binding.btnImageAddBulletin.id -> {
            //1번 -> 갤러리에서 이미지 정보 받아오기
            API.bltn_img_addr_Flag = 1
            //이미지의 URI를 intent를 사용해 가져오는 코드
            var intent = Intent(Intent.ACTION_PICK)
            intent.data = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            intent.type = "image/*"
            //다중이미지 허용
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
            activityResultLauncher.launch(intent)
        }

 

ActivityResultLauncher를 사용해서 이미지의 Uri를 갖고 온 후 ArrayList에 저장하는 코드

activityResultLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        //Sub 엑티비티에서 데이터를 갖고왔을 때만 실행

        //번호에 따라서 받아오는 정보를 구분한다
        //1번: 이미지추가 -> 이미지 정보 받아오기
        //2번: 관심운동  -> 관심운동 정보 받아오기

        if (it.resultCode == RESULT_OK) {
            //1번 -> 선택한 이미지 추가
            if (API.bltn_img_addr_Flag == 1) {
                //기존에 선택된 이미지 초기화
                list_imgUri.clear()
                //1)사진을 여러장 선택한 경우
                if (it.data?.clipData != null) {
                    //선택된 이미지의 개수를 변수에 저장
                    //count: 선택된 이미지의 개수
                    val count = it.data!!.clipData!!.itemCount
                    //5장 이상 선택한 경우
                    if (count > 5) {
                        Toast.makeText(
                            applicationContext,
                            "사진은 최대 5장까지 선택 가능합니다.",
                            Toast.LENGTH_SHORT
                        ).show()
                        return@registerForActivityResult
                    }

                    //선택한 이미지의 갯수 만큼 반복
                    for (i in 0 until count) {
                        //선택한 이미지의 갯수만큼 이미지의 uri를 추출해서 리스트에 저장
                        val imageUri = it.data?.clipData!!.getItemAt(i).uri
                        list_imgUri.add(imageUri)
                    }
                }


                //2) 단일 선택(한 장의 사진만 선택한 경우)
                else {
                    it.data?.data?.let { uri ->
                        //이미지 uri
                        val imageUri: Uri? = it.data?.data
                        //이미지 uri가 null 이 아닐때만 리스트에 추가.
                        if (imageUri != null) {
                            list_imgUri.add(imageUri)
                        }
                    }
                }
                rv_Adapter_img.notifyDataSetChanged()
            }

            //2번 -> 관심운동 정보 View에 입력
            if (API.bltn_img_addr_Flag == 2) {
                //체크한 운동 View에 입력
                binding.edExerAddBulletin.setText(API.bltn_exer)
            }
        }
    }

 

 

가상디바이스에 있는 이미지 경로를 절대경로로 바꿔주는 메서드 

-이미지를 File형식으로 변환하기 위해서 필요

// 절대경로 파악할 때 사용된 메소드
@Nullable
fun createCopyAndReturnRealPath(context: Context, uri: Uri): String? {
    val contentResolver: ContentResolver = context.getContentResolver() ?: return null


    // 파일 경로를 만듬
    val filePath: String = (context.getApplicationInfo().dataDir + File.separator
            + System.currentTimeMillis())
    val file = File(filePath)
    try {
        // 매개변수로 받은 uri 를 통해  이미지에 필요한 데이터를 불러 들인다.
        val inputStream = contentResolver.openInputStream(uri) ?: return null
        // 이미지 데이터를 다시 내보내면서 file 객체에  만들었던 경로를 이용한다. 
        val outputStream: OutputStream = FileOutputStream(file)
        val buf = ByteArray(1024)
        var len: Int
        while (inputStream.read(buf).also { len = it } > 0) outputStream.write(buf, 0, len)
        outputStream.close()
        inputStream.close()
    } catch (ignore: IOException) {
        return null
    }
    return file.getAbsolutePath()
}

 

Volley라이브러리를 사용해서 File형식으로 변환한 이미지를 서버에 보내는 코드 

private fun volley_add_Bulletin(context: Context, url: String) {
    // RequestQueue 생성 및 초기화
    val requestQueue = Volley.newRequestQueue(context)

    val smpr: SimpleMultiPartRequest = object : SimpleMultiPartRequest(
        Request.Method.POST, url,
        Response.Listener { response ->
            //요청 성공
            //응답 값
            Log.d(Tag.TAG, "응답: " + response.trim())
            if (response.trim().equals("저장성공")) {
                Toast.makeText(applicationContext, "모집 글 작성완료", Toast.LENGTH_SHORT).show()
                //메인엑티비티(전체 모집 글 엑티비티)로 이동
                val intent = Intent(applicationContext, MainActivity::class.java)
                startActivity(intent)
                finish()
            }
        }, //요청 실패
        Response.ErrorListener { error ->
            Toast.makeText(context, error.toString(), Toast.LENGTH_LONG).show()
        }) {

    }
    //1.모집 글 정보 (서버에 보낼)
    smpr.addStringParam("user_idx", Util_KEY.user_idx) //1)회원 인덱스 번호
    smpr.addStringParam("bltn_title", binding.edTitleAddBulletin.text.toString()) //2)모집 글 제목
    smpr.addStringParam(
        "bltn_content",
        binding.edContentAddBulletin.text.toString()
    ) //3)모집 글 내용
    smpr.addStringParam("bltn_exer", binding.edExerAddBulletin.text.toString()) //4)관심운동
    smpr.addStringParam("bltn_addr", binding.edAddrAddBulletin.text.toString()) //5)원하는 지역
    smpr.addStringParam("image_cnt", list_imgUri.size.toString()) //6)업로드 이미지 개수
    //2.모집 글 이미지 (서버에 업로드할 이미지)
    for (i in list_imgUri.indices) {
        val realpath = getRealpath(list_imgUri.get(i))
        smpr.addFile("image$i", realpath)
    }

    // 서버에 요청 보내기
    requestQueue.add(smpr)
}

 

-클라이언트에서 받은 File형식의 이미지 Uri를 서버에 업로드 한 후 이미지의 URi를 DB에 저장하는 PHP 코드

   //1.업로드하는 이미지가 있는 경우에만
    if($filltered['image_cnt'] != 0){
      //서버에 저장한 이미지 경로 배열
      $ar_imageUri = array();
     
      //클라이언트에서 가져온 이미지를 담는 배열
      $ar_image = array();
       for($i=0; $i < $filltered['image_cnt']; $i++) {
       // 7)업로드 해야하는 이미지 받기
        $ar_image[] = $_FILES['image'.$i];
             //배열안에 배열 넣기
                //등록한 이미지 정보
          $img_name = $_FILES['image'.$i]['name'];
          $img_size = $_FILES['image'.$i]['size'];
          $tmp_name = $_FILES['image'.$i]['tmp_name'];
          $error = $_FILES['image'.$i]['error'];


         $img_ex = pathinfo($img_name, PATHINFO_EXTENSION);//확장자 저장
         $img_ex_lc = strtolower($img_ex);
         //파일 지원하는 확장자 형식
         $allowed_exs = array("jpg", "jpeg", "png");
        //지원 형식과 맞는 이미지인 경우
        if (in_array($img_ex_lc, $allowed_exs)) {
          //4-6)등록한 이미지의 이름  
           $new_img_name = uniqid("IMG-", true).'.'.$img_ex_lc;
            //4-6)이미지 업로드 url 생성
           $img_upload_path = '../app_image/'.$new_img_name;
            //4-7)지정한 디렉토리 경로에 이미지 업로드(저장)
            if(move_uploaded_file($tmp_name, $img_upload_path)){
            //Json으로 변환할 2차원 배열 생성
            array_push($ar_imageUri,
            array(
              'image'.$i => $new_img_name,
              ));
              // echo "$new_img_name";
               }else{
                echo "실패";
               }
              }else{
               echo "파일없음";
              }
    }//for문 종료

 

 

 

 

댓글