[Spring][쇼핑몰 프로젝트] 25. 업로드 이미지 출력

YB·2023년 3월 1일
0

쇼핑몰

목록 보기
39/40
post-thumbnail

목표

비동기 방식으로 url을 호출하면 이미지를 반환해주는 url매핑 메서드를 구현할 것입니다. 이 메서드는 구현해야할 상품 등록 페이지에서의 미리 보기 이미지뿐만 아니라 앞으로 사용자가 검색을 하였을 때 볼 수 있는 이미지들을 출력하는데도 사용될 것입니다.

1. 전체적 방향

이번에 구현할 url메서드의 경우 '파일 경로(유동 경로)' + '파일 이름' 데이터를 파라미터로 전달받고, 해당 데이터에 맞는 이미지 파일을 찾아서 뷰에 이미지 데이터를 전송하도록 할 것입니다.

가장 먼저 생각 해볼 사항은 이미지는 어떠한 데이터 타입으로 주고받을 수 있는가입니다. 먼저 결론부터 말하자면 이미지 파일을 주고받기 위한 데이터 타입은 byte배열(byte [])입니다.

이미지 파일은 바이너리(binary)파일 범주에 들어갑니다. 바이너리 파일이란 쉽게 말해 0과 1로만 구성된 이진 데이터인데, 텍스트(text)파일을 제외 한 모든 파일을 지칭합니다. 이러한 바이너리 파일(데이터)를 주고 받을 때 바이트(혹은 옥텟(octet))단위로 데이터를 주고 받습니다. 그렇기 때문에 이미지 파일을 주고받기 위해서는 byte배열 타입을 사용해야 합니다.

그 다음 고려할 사항은 url매핑 메서드의 반환 타입입니다. 현재 구현할 메서드의 경우 화면의 이동 없이 특정 데이터를 반환하는 것을 목표로 하기 때문에 비동기 방식의 url매핑 메서드를 구현해야 합니다. 따라서 @ResponseBody 어노테이션과 ResponseEntity 객체 두 가지 선택 사항이 있습니다.

두 가지 모두 경우 http response메시지의 body에 데이터를 첨부한다는 점은 동일하지만, ResponseEntity의 경우 http response메시지의 header와 status(상태 코드)를 조작할 수 있다는 차이점이 있습니다.

ResponseEntity를 사용할 것이며 그 이유는 response메시지와 body에 이미지 데이터(byte 배열)을 전송할 것인데, 해당 데이터의 타입(header의 'content-type')이 이미지 파일임을 명확히 명시해주기 위해서입니다.

이미지 출력 메서드를 어떠한 방식으로 구현할지를 정리하면 파라미터로 전달받은 '파일 경로'와 '파일 이름'을 활용하여 대상 이미지 파일을 File객체로 생성을 합니다. 해당 File객체를 활용하여 MIME TYPE에 대한 정보를 알아냅니다. ResponseEntity에 대상 이미지 데이터를 복사하여 body에 추가해주고, header의 'Content Type'에 앞서 얻어낸 MIME TYPE으로 수정 해준 후 ResponseEntity객체를 호출한 뷰(View)로 전송해줍니다.

2. URL 매핑 메서드 적성 및 File 객체 생성

이미지 데이터를 전달해주는 매핑 메서드 선언부를 추가해주겠습니다. 지금 현재 관리자 관련 기능들을 구현을 하고 있어서 AdminController에 추가해주어야 한다고 생각을 할 수 있습니다. 하지만 AdminController에 접근하기 위해선 관리자 계정이 아닐 시 접근을 하지 못하도록 설정을 한 Interceptor필터를 거쳐야 하기 때문에 접근을 하는데 제한이 있습니다. 이미지는 로그인을 하든 안 하든 모든 곳에서 접근이 가능해야 하기 때문에 BookController.java에 작성을 하겠습니다.

BookController.java에 아래와 같이 url매핑 메서드를 작성합니다.

  • 반환 타입은 앞서 말했듯이 ResponseEntity객체를 통해 body에 byte[]데이터를 보내야 하기 때문에 ResponseEntity<byte[]>를 반환 타입으로 작성하였습니다.
  • 파라미터의 경우 '파일 경로' + '파일 이름'을 전달받아야 하기 때문에 String타입의 fileName변수를 파라미터로 부여하였습니다.
  • url의 경로를 통해 변수와 변수값을 부여할 수 있도록 GetMapping 어노테이션을 사용하였습니다.
  • 기본 경로 문자열 데이터와 전달받은 '유동 경로' + '파일 이름'을 활용하여 File객체를 생성해주고 File타입의 참조 변수에 대입합니다.
	@GetMapping("/display")
	public ResponseEntity<byte[]> getImage(String fileName) {
		
		logger.info("getImage()........." + fileName);
		
		File file = new File("d:\\upload\\" + fileName);
		
		return null;
	}

3. 'Content Type' 명시 & 데이터 파일 반환

ResponseEntity에 Response의 header에 대한 설정을 추가해주기 위해 아래의 생성자를 사용할 것입니다.

ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status)


ResponseEntity의 첫 번째 파라미터는 body에 첨부할 데이터를 추가합니다. 두 번째 파라미터의 경우 header의 설정이 부여된 객체를 추가합니다. MultiValueMap클래스 타입이어야 한다고 명시되어 있는데 MultiValueMap클래스를 상속한 HttpHeader클래스를 사용할 것입니다. 세 번째 클래스의 경우 전송하고자 하는 상태 코드와 관련된 코드를 추가해 줄 것입니다.

  • 뷰로 반환할 Response객체의 주소를 저장할 참조 변수를 선언하고 null로 초기화합니다.
  • 대상 이미지 파일의 MIME TYPE을 얻기 위해 File클래스의 proveContentType()메서드를 사용할 것입니다. 해당 메서드의 경우 IOException을 일으킬 가능성이 큰 메서드이기 때문에 try catch문을 작성해주어야 합니다.
  • ResponseEntity에 Response의 header와 관련된 설정의 객체를 추가해주기 위해서 HttpHeader를 인스턴스화 한 후 참조 변수를 선언하여 대입합니다.


add()메서드의 첫 번째 파라미터에는 Response header의 '속성명'을, 두 번째 파라미터에는 해당 '속성명'에 부여할 값(value)을 삽입하면 됩니다.

  • header의 'Content Type'에 대상 파일의 MIME TYPE을 부여해주는 코드를 추가해줍니다.
  • 대상 이미지 파일, header객체, 상태 코드를 인자 값으로 부여한 생성자를 통해 ResponseEntity객체를 생성하여 앞서 선언한 ResponseEntity참조 변수에 대입합니다.


copyToByteArray의 첫 번째 파라미터는 출력시킬 대상 이미지 데이터 파일이라고 말을 하였는데 FileCopyUtil.copyToByteArray(file)코드를 작성하였습니다. FileCopyTuils클래스는 파일과 stream복사에 사용할 수 있는 메서드를 제공하는 클래스입니다. 해당 클래스 중 copyToByteArray()메서드는 파라미터로 부여하는 File객체 중, 대상 파일을 복사하여 Byte배열로 반환해주는 클래스입니다. 두 번째 파라미터는 'Content-Type'속성을 지정 해준 HttpHeader객체(변수 header)를 인자로 부여하였습니다. 세 번째 파라미터는 성공 상태(status)코드 200이 전송되도록 인자 값을 작성하였습니다.

  • url 매핑 메서드의 return에 생성한 Response객체(변수 result)를 작성해줍니다.
		ResponseEntity<byte[]> result = null;
		
		try {
			
			HttpHeaders header = new HttpHeaders();
			
			header.add("Content-type", Files.probeContentType(file.toPath()));
			
			result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return result;

4. 이미지 확인

실제로 이미지가 잘 출력되는지 "d:\upload\"경로에 테스트 파일을 추가 해준 후 서버를 구동 시켜 웹브라우저에 출력시켜보겠습니다.


5. 태그 추가 및 css설정 추가

'goodsEnroll.jsp'에 업로드 파일 선택 <input>태그 바로 밑에 업로드 한 이미지를 볼 수 있도록 코드를 추가할 것입니다.

  • id속성이 uploadResult인 <div>태그를 추가하였습니다. 해당 <div>태그를 타겟으로 내부에 이미지를 출력시킬 태그가 추가되도록 Javascript코드를 작성할 것입니다.
  • <img>태그 src속성에 이미지를 출력을 요청하는 url을 작성하였습니다.
  • id속성 uploadResult인 <div>태그 내부에 아래의 태그들을 추가해줍니다. class속성명이 imgDeleteBtn인 <div>태그는 추후 구현할 이미지 삭제 기능 수행 버튼입니다.
									<div id="uploadResult">									
										<div id="result_card">
											<div class="imgDeleteBtn">x</div>
											<img src="/display?fileName=test.png">
										</div>
									</div>

css설정을 추가해주겠습니다. css코드는 편의를 위해 css파일에 작성하지 않고 해당 'goodsEnroll.jsp'파일에 추가해주겠습니다. jsp상단 <head>태그 내부에 <style>태그를 추가하여 아래의 css코드를 추가해줍니다.

<style type="text/css">
	#result_card img{
		max-width: 100%;
	    height: auto;
	    display: block;
	    padding: 5px;
	    margin-top: 10px;
	    margin: auto;	
	}
	#result_card {
		position: relative;
	}
	.imgDeleteBtn{
	    position: absolute;
	    top: 0;
	    right: 5%;
	    background-color: #ef7d7d;
	    color: wheat;
	    font-weight: 900;
	    width: 30px;
	    height: 30px;
	    border-radius: 50%;
	    line-height: 26px;
	    text-align: center;
	    border: none;
	    display: block;
	    cursor: pointer;	
	}
</style>

정상적으로 출력되는 것을 확인하였기 때문에 id속성이 'result_card'인 <div>태그와 그 내부 태그들을 지우거나 주석처리해줍니다.

6. 이미지 출력 메서드 작성(showUploadImage())

어떠한 형태로 출력할 것인지를 결정을 하였고 css설정도 추가를 하였습니다. 따라서 앞서 주석 처리한 태그들을 js코드를 통해 동적으로 추가되도록 해주어야 합니다. 동작할 시점은 이미지 업로드를 요청 후 성공적으로 업로드 한 이미지에 대한 데이터(path, filname, uuid)들을 전달받았을 때 입니다. 따라서 ajax success속성의 콜백 함수에 전달받은 이미지 데이털르 활용하여 이미지가 출력되도록 코드를 작성할 것입니다.

작성할 코드의 양이 많기 때문에 콜백 함수 내부에 바로 작성하지 않고, 따로 메서드를 선언하여 그 구현부에 이미지를 출력하는 코드를 작성 한 뒤 해당 함수를 호출하는 형태로 진행하겠습니다.

선언 및 호출할 메서드의 이름은 'showUploadImage'입니다. ajax의 success속성 속성값으로 작성한 콜백 함수에 앞으로 선언 및 구현할 showUploadImage()메서드를 ㅎ미리 호출하고 인자값으로 서버로 전달받은 result부여하였습니다.

showUploadImage(result);

<script>태그 제일 하단에 showUploadImage를 선언 및 구현하겠습니다.

  • 메서드를 선언하는 코드를 추가합니다.
  • success콜백 함수가 실행된다는 것은 업로드 이미지 메서드가 정상적으로 수행되었다는 것이기 때문에 반환 데이터(result)를 전달받지 못할 가능성은 낮습니다. 하지만 혹여나 데이터를 전달 받지 못했을 경우에는 이미지를 출력시키는 태그를 추가해선 안되기 때문에 데티러르 검증하는 코드를 추가하겠습니다.
  • id속성 uploadResult인 <div>태그 요소에 쉽게 접근하기 위해 변수를 선언 및 초기화하였습니다.
  • 서버에서 뷰로 반환할 때 List타입의 데이터를 전송했었고 뷰(view)에서는 해당 데이터를 배열 형태로 전달받습니다. 지금 우리는 한 개의 이미지 파일만 처리를 하기 때문에 이미지의 데이터(filename, path, uuid)에 쉽게 접근할 수 있도록 변수 obj를 선언하여 서버로부터 전달받은 배열 데이터의 첫 번째 요소로 초기화해주었습니다.
  • id속성 값 uploadResult인 <div>태그 내부에 이미지를 출력하는 태그 코드들을 추가할 차례입니다. 추가될 태그 코드를 저장할 변수를 선언하고 초기화해줍니다.
  • 이미지 출력을 요청하는 url매핑 메서드("/display")에 전달해줄 파일의 경로와 이름을 포함하는 값을 저장하기 위한 변수인 코드를 추가해줍니다.
  • str변수에 추가되어야 할 태그 코드인 문자열 값들을 저장해줍니다. 한 번에 값들을 다 넣어도 상관은 없습니다. 단지 추가될 코드 값들을 이해하기 편하도록 4번에 걸쳐서 값들을 추가해주었습니다.
  • 태그 코드가 담긴 문자열 값(str)을 uploadResult태그에 append()명령 혹은 html()메서드를 호출하여 추가해줍니다.
/* 이미지 출력 */
function showUploadImage(uploadResultArr){
	
	/* 전달받은 데이터 검증 */
	if(!uploadResultArr || uploadResultArr.length == 0){return}
	
	let uploadResult = $("#uploadResult");
	
	let obj = uploadResultArr[0];
	
	let str = "";
	
	let fileCallPath = obj.uploadPath.replace(/\\/g, '/') + "/s_" + obj.uuid + "_" + obj.fileName;
	
	str += "<div id='result_card'>";
	str += "<img src='/display?fileName=" + fileCallPath +"'>";
	str += "<div class='imgDeleteBtn'>x</div>";
	str += "</div>";		
	
	uploadResult.append(str);     
    
}

7. encodeURIComponent() 적용

파일 이름에 한글이 들어갔지만, 웹브라우저에서 자동으로 한글 문자를 UTF-8로 변환을 해주었기 때문에 정상적으로 동작을 하였습니다. 하지만 UTF-8로 인코딩을 자동으로 해주지 않는 웹브라우저가 있기 때문에 지금의 코드가 동작하지 않을 수도 있습니다. 따라서 encodeURIComponent() 메서드를 활용하여 코드를 보완해줄 것입니다.

encodeURIComponent() 메서드는 다음의 문자 A-Z a-z 0-9 - _ . ! ~ * ' ( )을 제외한 모든 문자를 UTF-8로 인코딩하여 이스케이프 문자로 변환을 해줍니다.

fileCallPath 변수를 선언 및 초기화 한 문자 코드를 아래와 같이 encodeURIComponent()를 적용한 코드로 변경을 해줍니다.

더불어서 encodeURIComponent() 메서드는 '/'와 '\'문자 또한 인코딩을 하기 때문에 replace() 메서드를 사용 안 해도 해당 URI로 동작이 됩니다.

	let fileCallPath = encodeURIComponent(obj.uploadPath.replace(/\\/g, '/') + "/s_" + obj.uuid + "_" + obj.fileName);
	//replace 적용 하지 않아도 가능
	//let fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName);
profile
개인이 공부한걸 작성하는 블로그입니다..

0개의 댓글