[Spring][쇼핑몰 프로젝트] 24. 상품 이미지 업로드(3)

YB·2023년 2월 26일
0

쇼핑몰

목록 보기
37/40

목표

이미지 저장까지 구현을 하였습니다. 앞으로 저장한 이미지를 호출하여 홈페이지 화면에 띄우는 것을 구현을 할 것입니다. 하나의 페이지에 저장된 이미지가 한 개인 경우 크게 문제는 없습니다. 하지만 이미지가 여러 개인 경우 특히 저장된 이미지가 용량이 큰 경우 홈페이지를 운영하는 사람과 홈페이지를 사용하는 사람 모두에게 부담이 됩니다.

홈페이지를 운영하는 사람의 입장에서는 이미지를 많이 호출하는 것은 트래픽 량을 많이 소모하는 원인이 됩니다. 서버를 대여해서 운영하는 운영자 입장에서 트래픽 량에 따라 비용이 달라지기 때문에 민감한 사항입니다.

홈페이지를 사용하는 사람입장에서 데스크톱, 노트북으로 이용하는 경우는 부담이 좀 덜하겠지만, 휴대폰을 통해 특히 와이파이를 통하지 않고 이미지를 많이 호출하는 페이지로 이동할 경우 로딩 시간과 데이터 소모량이 많게 됩니다.

원본 이미지를 보여주는 것이 아니라 원본보다 크기를 줄인 이미지, 즉 썸네일 이미지를 홈페이지에 보여준다면 이러한 문제점을 다소 보완할 수 있습니다. 원본을 사용자가 보길 원하는 경우 이미지를 클릭하는 것과 같은 이벤트를 통해 볼 수 있도록 해주면 될 것입니다.

추후 이미지 호출 시 썸네일 이미지를 호출할 수 있도록 원본 이미지를 통해 썸네일 이미지를 생성하고 저장하는 것을 목표로 합니다. 썸네일 이미지를 만들기 위해 Java에서 자체적으로 제공하는 ImageIO를 이용하거나 간단히 썸네일 이미지를 만들 수 있도록 도와주는 Scalar, Thumbnailator등을 이용하는 방법이 있습니다. ImageIO와 Thumbnailator을 통한 썸네일 이미지 작업을 해 볼 것입니다.

0. 방향

썸네일 이미지를 저장을 실행하는 코드는 작성한 이미지 파일을 저장하는 코드에 이어서 작성할 것입니다.

저장될 썸네일 이미지의 경우 기존 원본 파일 이름("uuid원본파일이름")에서 앞에 "s"를 붙여줄 것입니다. 다시 말해 썸네일 이미지는 다음과 같은 형태의 구성을 가집니다.

"s" + "uuid" + "원본파일 이름. 이미지 타입"

1. 썸네일 생성 및 저장 (ImageIO)

ImageIO를 통해서 썸네일을 만들기 위해서 ImageIO, BufferedImage, Graphics2D 클래스를 사용할 것입니다. 각 클래스에 대해 간략히 설명하면 ImageIO 클래스는 이미지를 읽어오거나 생성할 수 있도록 도와주는 메서드입니다. BufferedImage의 경우 이미지 데이터를 처리하거나 조작에 필요한 값과 메서드를 제공합니다. Graphics2D의 경우 그림을 그리는데 필요로 한 설정값과 메서드를 제공하는 클래스입니다.

ImageIO도 결국 입출력에 관한 클래스 이기 때문에 해당 클래스의 메서드들은 illegalArgumentException, IOException을 일으킬 가능성이 있습니다. 때문에 try catch문 구현부에 이어서 작성할 것입니다.

ImageIO를 통해 썸네일을 만들기 위해선 먼저 원본 파일의 File객체와 썸네일 이미지 파일의 File객체가 필요로 합니다. 원본 이미지의 File객체는 앞서 작성해 놓았기 때문에(File saveFile)썸네일 이미지 파일 객체를 만들어 줄 것입니다.

  • File타입의 참조 변수를 선언하여 썸네일 이미지 File객체로 초기화해줍니다. 썸네일 이미지는 "s_" + "이미지 이름"으로 작성되도록 하였기 때문에 다음과 같이 작성하였습니다.

  • 원본 이미지 파일을 ImageIO의 read()메서드를 호출하여 BufferedImage타입으로 변경 해준 뒤 BufferedImage타입의 참조 변수를 선언하여 해당 변수에 대입해줍니다.

  • BufferdImage생성자를 사용하여 썸네일 이미지인 BufferedImage객체를 생성해주고 참조 변수에 대입합니다. (사용할 BufferedImage생성자는 매개변수로 '넓이', '높이', '생성될 이미지의 타입'을 작성하면 됩니다. '생성될 이미지의 타입' 경우 int형 값인데 BufferedImage클래스가 가지고 있는 필드 값 들 중 자신이 필요로 한 타입을 선택해서 작성을 하면 됩니다. Java BufferedImage API)

  • 썸네일 BufferedImage객체(btimage에서 createGraphics()메서드를 호출을 통해 Graphic2D객체를 생성 해준 후 Graphic2D타입의 참조 변수에 대입합니다. 썸네일 BufferedImage객체에 그림을 그리기 위해 Graphic2D객체를 생성하였습니다. Graphic2D메서드를 통해 조작을 하게 되면 그 결과가 썸네일 BufferedImage객체 적용이 됩니다.

  • drawImage메서드를 호출하여 원본 이미지(원본 BufferedImage)를 썸네일 BufferedImage에 지정한 크기로 변경하여 왼쪽 상단 "0, 0" 좌표부터 그려줍니다.

    • 첫 번째 인자는 그려놓고자 하는 이미지입니다.
    • 두 번째, 세 번째 인자는 그림을 어느 좌표로부터 그릴 것인지에 대한 x, y값입니다.
    • 네 번째, 다섯 번째 인자 값은 첫 번째 인자로 작성한 이미지의 '넓이'와 '높이'입니다. 확대 및 축소의 의미입니다.
    • 여섯 번째 인자는 ImageObserver객체입니다. ImageObserver는 이미지의 정보를 전달받아서 이미지를 업데이트시키는 역할을 합니다. 일반적인 경우 null을 전달하면 됩니다.
  • 썸네일 이미지(bt_image)를 이제 만들어줄 ImageIO의 write메서드를 호출하여 파일로 저장합니다.

    • 첫 번째 인자는 파일로 저장할 이미지입니다.
    • 두 번째 인자는 어떠한 이미지 형식으로 저장할 것(jpg, png)인지 String타입으로 작성합니다.
    • 세 번째 인자는 우리가 앞서 썸네일 이미지가 저장될 경로와 이름으로 생성한 File객체를 부여하면 됩니다.
				/* 썸네일 생성(ImageIO) */
				File thumbnailFile = new File(uploadPath, "s_" + uploadFileName);
				
				BufferedImage bo_image = ImageIO.read(saveFile);
				BufferedImage bt_image = new BufferedImage(300, 500, BufferedImage.TYPE_3BYTE_BGR);
								
				Graphics2D graphic = bt_image.createGraphics();
				
				graphic.drawImage(bo_image, 0, 0,300,500, null);
					
				ImageIO.write(bt_image, "jpg", thumbnailFile);

2. 비율 축소 적용

원본 이미지 크기를 개발자가 지정한 비율에 따라 축소되도록 보완하고자 합니다.

  • 먼저 변수 bo_image를 선언 및 초기화 한 코드 다음 칸에서 비율을 저장할 변수를 선언합니다. 비율을 소수점 값으로도 지정할 수 있도록 double타입을 지정하였습니다.

  • 원본 이미지 비율을 지정한 비율로 줄이기 위해선 원본 이미지 높이 넓이를 지정한 비율값으로 나누어주면 됩니다. 이러한 작업을 해주기 위해선 이미지의 넓이와 높이를 알아내야 하는데 BufferedImage클래스의 getWidth(), getHeight()메서드를 사용하면 됩니다.

  • 썸네일 이미지가 가져야 할 넓이와 높이를 얻었기 때문에 해당 값들이 필요로 한 파라미터에 인자로 부여합니다.

					/* 비율 */
					double ratio = 3;
					
					/*넓이 높이*/
					int width = (int) (bo_image.getWidth() / ratio);
					int height = (int) (bo_image.getHeight() / ratio);
                    
				BufferedImage bt_image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
				graphic.drawImage(bo_image, 0, 0, width, height, null);

3. 이미지 생성 및 저장(thumbnailator 라이브러리 사용)

maven repository에서 thumbnailator를 검색하여 해당 라이브러리를 찾은 후 pom.xml에 추가해줍니다.

		<!-- thumbnail 라이브러리 추가 -->
		<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
		<dependency>
		    <groupId>net.coobird</groupId>
		    <artifactId>thumbnailator</artifactId>
		    <version>0.4.13</version>
		</dependency>

ImageIO를 통한 코드 작성보다 훨씬 간단히 생성할 수 있습니다. thumbnailator라이브러리는 이미지 생성에 세부적 설정을 할 수 있도록 몇몇 메서드들을 제공하고 있습니다. thumbnailator 홈페이지에서 다양한 예시들이 있기 때문에 공식 홈페이지와 API문서를 참고하여 필요한 설정들을 추가하시면 됩니다.

ImageIO를 통한 썸네일 생성 코드처럼 try catch문 안에 작성을 하면 됩니다. 썸네일을 생성하는 코드를 아래와 같이 작성을 하면 됩니다.

				/* 방법 2 */
				File thumbnailFile = new File(uploadPath, "s_" + uploadFileName);	
				
				BufferedImage bo_image = ImageIO.read(saveFile);

					//비율 
					double ratio = 3;
					//넓이 높이
					int width = (int) (bo_image.getWidth() / ratio);
					int height = (int) (bo_image.getHeight() / ratio);					
				
				
				Thumbnails.of(saveFile)
		        .size(width, height)
		        .toFile(thumbnailFile);

4. 고려사항

1) 왜 정보를 보내려 하는가?

첫 번째 목적은 업로드한 이미지 정보를 DB에 저장을 하기 위해서입니다. 상품 등록 과정에서 업로드한 이미지에 대한 정보도 함께 저장하기 위해서입니다.

두 번째 목적은 업로드한 이미지를 사용자가 미리 볼 수 있도록 하기 위해서입니다.

2) 어떠한 정보를 보낼 것인가?

DB에 업로드 이미지 정보를 저장한다는 것은 결국 이미지를 화면에 출력시키기 위해서입니다. 그렇다면 이미지를 화면에 출력시키기 위해 필요로 한정보는 무엇일까요? 이미지가 저장된 경로와 파일의 이름 일 것입니다.

뷰로 전송할 이미지 관련 정보는 곧 DB에 저장될 데이터 이기 때문에 이미지 정보를 어떠한 방식으로 보관할 것인지 정해야 합니다. 파일의 경로와 파일의 이름을 보관하는 방식엔 크게 두 가지 방식이 있다고 생각합니다.

먼저 첫 번째 방식은 경로와 파일 이름 전체를 보관하는 것입니다. 이러한 방식의 경우 원본 이미지와 썸네일 이미지 두 개의 데이터를 보관하면 될 것입니다.

D:\upload\2021\05\13\이미지이름.png
D:\upload\2021\05\13\썸네일이미지이름.png

두 번째 방법은 파일 경로와 파일 이름 데이터를 따로 보관 및 관리하는 것입니다.

파일 경로는 다시 나뉠 수 있는데 고정적인 경로와 유동 적인 경로입니다. 예를 들어서 이미지를 저장을 하게 되면 "c:\upload\" 경로의 경우 고정적입니다. 하지만 "\2021\05\03\" 경로의 경우 파일이 저장되는 날자에 따라 유동적으로 변합니다. 따라서 이미지에 대한 정보에서 유동적인 경로는 반드시 보관되어야 할 정보입니다.

파일 이름의 경우 '썸네일 이미지'와 '원본 이미지' 이름을 두 개를 보관합니다. 하지만 썸네일 이미지 이름의 경우 "s_"+"원본 이름" 이기 때문에 이 규칙만 안다면 '원본 이미지' 이름만 보관을 하면 됩니다.

'원본 이미지'에 대한 데이터를 조금 더 세분화하면 'uuid'와 '원본 이름'입니다. 지금 우리가 구현하고자 하는 목표는 이미지를 출력시키는 것이기 때문에 이렇게 꼭 세분화할 필요는 없습니다. 하지만 만약 홈페이지 사용자가 파일을 다운로드할 수 있도록 하는 기능 구현의 경우, 사용자가 다운로드하였을 때 파일 이름에 uuid가 안 붙도록 코드를 작성해야 하는데 'uuid'와 '원본 이름' 따로 관리를 한다면 한결 편하게 코드를 작성할 수 있을 것입니다.

따라서 두 번째 방식은 '유동적인 경로', 'uuid', '원본 파일 이름'을 보관합니다.

'유동적 경로', 'uuid', '원본 파일 이름'


원본 파일 : c:\upload + '유동적 경로' + 'uuid' + '_' + '원본 파일 이름'
썸네일 이미지 : c:\upload + '유동적 경로' + 's_' + 'uuid' + '_' + '원본 파일 이름'

저는 두 번째 방식으로 작업을 진행할 것입니다.

3) 어떠한 방식으로 뷰에 전송할 것인가?

우리가 지금 구현 중인 기능은 화면의 이동 없이 서버와 뷰가 정보를 주고받는 비동기 방식입니다. 이러한 비동기 방식의 서버에서 뷰로 데이터를 전송하기 위해선 Controller의 url 매핑 메서드에 @ResponseBody 어노테이션을 붙여 주거나 메서드의 반환 타입을 ResponseEntity 방식으로 하면 됩니다. (혹은 두 가지 동시에 사용 할 수도 있기는 합니다.)

@Responsebody와 ResponseEntity 모두 서버에서 뷰로 전송하는 Http body에 뷰로 전달하고 싶은 데이터를 포함시켜서 보낸 다는 점을 동일합니다.

다른 점은 ResponseEntity의 경우 뷰로 전송될 HTTP의 세부 정보를 조작하여 뷰로 전송할 수 있다는 점입니다. HTTP는 크게 'status', 'header', 'body' 3개의 범주로 구성되어 있는데 ResponseEntity 클래스에는 'status'와 'header'의 속성들을 조작할 수 있는 메서드들을 제공하고 있습니다.

※ 크롬의 경우 서버와 뷰(웹 브라우저)가 주고받는 Http 데이터를 볼 수 있는데 '크롬 개발자 도구 창(f12)' => 상단 'Network' 클릭 => 'Network' 창 리스트 중 자신이 세부적인 정보를 보고자 하는 http 통신 클릭을 하면 아래와 같이 세부 정보를 볼 수 있습니다.

저는 ResponseEntity 방식으로 구현해보겠습니다.

5. AttachImageVO

이미지에 대한 정보 중 '경로', 'uuid', '파일 이름'에 대한 데이터를 DB에 보관할 것이라고 하였습니다. 따라서 앞서 말한 3가지 데이터를 뷰로 전송을 해주어야 합니다. 저는 이 데이터들을 클래스에 정의하여서 해당 클래스의 객체를 뷰에 전송할 것입니다. 따라서 뷰로 전송될 객체의 클래스를 작성하겠습니다.

'com.test.model'패키지에 "AttachImageVO"클래스를 생성하고 아래와 같이 변수를 선언합니다. 작성한 변수들 중 앞서 언급한 3가지 정보이외의 변수 'bookId"를 추가해주었습니다. 이 변수는 이미지 정보가 어떠한 상품의 이미지인지에 관한 정보인 '상품 id'를 위한 변수입니다. 추가한 변수들에 대한 getter/setter/toString메서드를 추가해줍니다.

	/* 경로 */
	private String uploadPath;
	
	/* uuid */
	private String uuid;
	
	/* 파일 이름 */
	private String fileName;
	
	/* 상품 id */
	private int bookId;

6. 라이브러리 추가

이미지에 관한 정보가 담긴 AttachImageVO 객체를 뷰에 전송하도록 코드를 작성할 것인데, 뷰에서는 해당 객체 데이터가 JSON 타입으로 전달받아야만 해당 데이터를 활용할 수 있습니다. 그렇기 때문에 ajax 코드에서도 전달받을 데이터 타입을 지정하는 'datatype'속성의 속성 값을 'json'으로 지정했었습니다.

따라서 스프링에서 Java의 객체 데이터를 JSON타입의 데이터로 변환하여 뷰로 전송을 해주어야 하는데, 이를 위해 Jackson-databind관련 라이브러리를 추가해주어야합니다.

pom.xml에 아래의 두 개의 Jackson-databind라이브러리를 추가해줍니다.(jackson-databind의 경우 카테고리 리스트 출력 구현 때 추가를 해주었기 때문에 'jackson-dataformat-xml'만 추가해주시면 됩니다.)

		<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml -->
		<dependency>
		    <groupId>com.fasterxml.jackson.dataformat</groupId>
		    <artifactId>jackson-dataformat-xml</artifactId>
		    <version>2.12.1</version>
		</dependency>

7. 이미지 정보 객체에 저장

이미지 한 개만 처리하는 경우 AttachImageVO를 인스턴스화하여 각 정보를 바로 담으면 됩니다. 하지만 현재의 업로드를 수행하는 url매핑 메서드의 경우 여러 개의 이미지 파일을 전달받고 처리할 수 있도록 설계되어 있습니다. 따라서 ArrayList자료구조를 활용할 것입니다.

  • for문 밖 상단에 List타입의 참조 변수를 선언하고 ArrayList생성자로 초기화해줍니다.
  • for문 내부에 AttachImageVO타입의 참조 변수를 선언하고 생성자로 초기화해줍니다.
  • setter메서드를 사용하여 각 정보를 AttachImageVO객체에 저장합니다.
  • for문 내부에 이미지 정보가 저장된 AttachImageVO객체를 List의 요소로 추가해줍니다.

		/* 이미지 정보 담는 객체 */
		List<AttachImageVO> list = new ArrayList();
        
		/* 이미지 정보 객체 */
		AttachImageVO vo = new AttachImageVO();

		vo.setFileName(uploadFileName);
		vo.setUploadPath(datePath);
        vo.setUuid(uuid);
        
        list.add(vo);

8. ResponseEntity 리턴

업로드를 수행하는 url매핑 메서드의 리턴 타입을 아래와 같이 변경해줍니다. 의미는 반환 타입이 ResponseEntity객체이고 Http의 Body에 추가될 데이터는 List<AttachImageVO>라는 의미입니다. 그리고 구현부 최하단에 아래와 같이 ResponseEntity참조 변수를 선언하고 생성자로 초기화합니다. 작성한 생성자를 통해서 Http의 바디에 추가될 데이터는 list<AttachImageVO>이고 상태 코드가 OK(200)인 ResponseEntity객체가 생성됩니다. 그리고 return문을 추가하여 생성한 ResponseEntity객체를 반환합니다.

뷰(view)에서 ajax를 통해 요청할 때 JSON타입의 데이터를 요청하였습니다. 따라서 뷰로 List객체를 반환하는 과정에서 List객체를 JSON형식의 문자열로 변환을 해주어야 합니다. 이 때 클래스패스(classpath)에 Jackson이 존재해야만 JSON형식으로 스프링이 변환을 시켜줍니다.

클래스패스(classpath)에 Jackson을 추가시켜주기 위해서 우리는 Jackson-databind 라이브러리를 추가시켜주었습니다. Jackson-databind 라이브러리는 jackson-annotations, jackson-core, jackson-datatbind를 클래스 패스에 자체적으로 추가시켜주기 때문입니다.

ResponseEntity<List<AttachImageVO>>

ResponseEntity<List<AttachImageVO>> result = new ResponseEntity<List<AttachImageVO>>(list, HttpStatus.OK);

return result;

9. RequestMapping produces 속성 추가

완성한 업로드 url매핑 메서드는 이미지 파일 이름이 영어인 경우 업로드 요청을 하였을 때 정상적으로 업로드를 수행 후 뷰로 업로드한 이미지 정보를 반환하게 됩니다. 하지만 이미지 파일 이름이 한글인 경우 업로드는 정상적으로 수행을 하겠지만 뷰로 반환되는 이미지 정보의 파일 이름(fileName) 데이터가 깨져 있을 수가 있습니다.

이를 방지하기 위해선 서버에서 뷰로 반환하는 데이터가 UTF8로 인코딩 되어야 합니다. 이를 위해서 RequestMapping어노테이션 produce속성을 추가하여 전송되는 JSON데이터가 UTF8인코딩이 된 채로 전송되도록 속성값을 부여해주어야합니다. RequestMapping어노테이션의 produces속성은 서버에서 뷰로 전송되는 Response의 Content-type을 제어할 수 있는 속성입니다.

업로드 url매핑 메서드의 RequestMapping어노테이션을 아래와 같이 변경해줍니다.

@PostMapping(value="/uploadAjaxAction", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

10. ajax success속성 추가

뷰에서 서버에서 전송한 이미지 정보를 전달받기 위해 기존 ajax 코드에 success속성을 추가하였습니다. success속성 값으로 콜백 함수를 부여한 뒤 전달받은 객체 데이터를 console에 찍히도록 코드를 작성하였습니다.

    	success : function(result){
    		console.log(result);
    	}

11. 테스트


profile
개인이 공부한걸 작성하는 블로그입니다..

0개의 댓글