본문 바로가기

끄적끄적

[끄적끄적] SVG를 img태그로 불러올 경우 발생하는 문제와 해결

[끄적끄적]  SVG를 img태그로 불러올 경우 발생하는 문제와 해결

프로젝트 진행중에 SVG에 관련된 기술적인 문제에 직면했다.

유저의 이미지 카드를 SVG형태로 만들어서 다른페이지에 삽입(예를들어, img태그를 사용해서)하는 서비스를 만들어야했는데, SVG를 <img>태그로 불러올경우, SVG내부의 하이퍼링크로 불러오는 이미지가 동작하지 않았다.

(하이퍼링크 뿐만아니라 자바스크립트도 동작하지 않는다.)

(모종의 이유로 <img>태그 사용이 강제되는 상황이여서 object나 embedded는 사용하지 못한다)

 

원인

보안적인 이유 때문에 <img>로 불러온 하이퍼미디어 안의 <img>는 적용되지 않는게 원인이였고, SVG관련 서비스를 프론트엔드에서 처리하는방식에서 백엔드로 바꿔, 서버에서 SVG의 모든 요소를 채워넣은 후, 클라이언트에게 전달해주는 방식으로 해결하기로 결정했다.

(서버 리소스, 네트워크 대역폭 사용량은 늘어나겠지만 디자인을 바꾸지 않는 한, 이렇게 해결하는 것이 최선이라 생각했다.)

* 이 글에서 말하는 SVG내부에서 불러오는 이미지는 모두 같은서버에 있는게 아닌 다른 서버에서 불러오는것이다.

 

고려한 해결법

이미지를 base64인코딩해서 SVG의 <image>태그안에 data-url형식으로 넣어주는 방식으로 해결했다.

구현방식은 크게 2가지를 고려했는데,

  1. base64인코딩된 이미지를 DB에 저장 후 요청시 base64 문자열을 DB에서 꺼내서 제공한다. 2. 이미지 카드 요청시 프로필 이미지를 base64로 인코딩해서 제공한다
장점 1. 이미지가 변경되었을때만 새로운 이미지를 저장후 인코딩한다.

2. 이전에 이미 인코딩된 이미지 라면, 사용자의 요청에 빠르게 응답가능하다.

3. 외부에서 이미지를 다운받는 과정이 최소화되어서 네트워크 대역폭을 비교적 적게 사용할 수 있다.
1. DB에 저장할 필요가 없어, 저장공간을 적게 차지한다. (ec2에 서버가 있어서 저장공간 가격을 무시하지 못한다)

2. DB와 통신과정이 한 번 이상 줄어든다.

3. base64인코딩된 이미지를 포함하는 거대한 객체를 영속성 컨텍스트에 저장할 필요가 없어, 메모리를 적게 사용한다.
단점 2번 방식의 모든 장점을 거꾸로하면 1번 방식의 단점이 된다. 1번방식의 모든 장점을 거꾸로하면 2번방식의 단점이 된다.

 

선택한 해결법

해결법 선택에 있어서 가장 중요하다고 생각한것은 이미지를 인코딩했을 때의 크기였다. 이 크기가 작다면, 1번방식의 모든 단점이 상쇄된다. 만약 인코딩된 이미지의 크기가 크다면 서버 성능(ec2.micro)과 가격을 고려했을때, 더 느리더라도 2번방식을 선택하는것이 더 효과적일수도 있다.

실제로 사용될 이미지를 base64로 인코딩해서 크기를 측정해보자.

 

인코딩 및 압축코드

public interface ImageEncoder{
	
	String encode(String url);
	
}
@Service
public class Base64ImageEncoder implements ImageEncoder{
	
	@Override
	public String encode(String url){
		try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){
			return encodeRealTask(url, byteArrayOutputStream);
		}catch(Exception e){
			e.printStackTrace();
		}
		return "";
	}
	
	private String encodeRealTask(String url, ByteArrayOutputStream byteArrayOutputStream) throws Exception{
		BufferedImage bufferedImage = ImageIO.read(new URL(url));
			
		bufferedImage = optimizeImage(bufferedImage);
		
		ImageIO.write(bufferedImage, "JPEG", byteArrayOutputStream);
		return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
	}
	
	private BufferedImage optimizeImage(BufferedImage bufferedImage){
		BufferedImage ans = new BufferedImage(100, 100, bufferedImage.getType());
		
		Graphics2D graphics2D = getGraphics2DfocusQuality(ans);
		graphics2D.drawImage(bufferedImage, 0, 0, 100, 100, null);
		graphics2D.dispose();
		
		return ans;
	}
	
	private Graphics2D getGraphics2DfocusSpeed(BufferedImage bufferedImage){
		Graphics2D graphics2D = bufferedImage.createGraphics();
		graphics2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
		graphics2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
		graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
		graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		return graphics2D;
	}
	
	private Graphics2D getGraphics2DfocusQuality(BufferedImage bufferedImage){
		Graphics2D graphics2D = bufferedImage.createGraphics();
		graphics2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		graphics2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
		graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		return graphics2D;
	}
	
}

(압축과정에서는 사용가능한 모든 최적화 방식을 적용했다.)

압축 결과

이미지를 최적화시켜서 base64인코딩한 결과, 2KB내외로 압축되었다. 

2KB정도라면(인코딩된 데이터만 생각했을때) 10,000명의 유저가 동시에 몰려도, JVM에 추가로 올라오는 메모리는 2mb정도다. (반면 2번방식으로 외부서버에서 이미지를 받아올시, 40KB의 데이터가 네트워크대역폭을 타고 전송된다.)

 

이제, 1번 방식이 갖고있는 단점과 해결법을 나열해보자.

 

1. DB에 추가로 저장해야해서, 저장공간을 많이 차지한다.

-> 유저가 갖고있을수 있는 이미지는 최대 1개이고, 가용 저장공간이 넉넉히 5기가라는것을 고려했을때, 문제되지 않는다. 

 

2. DB와 통신과정이 한 번 이상 늘어난다.

-> 2차 캐시를 이용해서 이 과정을 최소화 시킬수 있다.

-> 2차 캐시를 이용할 경우, 메모리에 인코딩된 데이터를 쌓아놔야한다. 차라리 통신 한 번 더 하는것이 더 효율적일수도 있는데, 이는 운영하면서 천천히 조율하면될것같다.

-> 웹 캐시를 사용해서, 서버로 오는 요청을 최소화 시킨다.

 

3. base64 인코딩 된 데이터를 영속성 컨텍스트에 올려서 관리시켜야한다.

-> 트랜잭션 범위를 최소화 시킨다. (현재 프로젝트에서 사용하는 영속성 컨텍스트 전략은 트랜잭션당 영속성 컨텍스트이다.)

-> base64 인코딩 된 데이터가 영속성 컨텍스트에 남아있는 시간을 최소화 시킨다.

-> 자바기본형으로 조회하여 영속성 컨텍스트의 관리를 받지 않도록 우회하는것도 좋은 방법일 것 이라고 생각한다.(이건 실제로 적용은 하지않았다.)

소스코드

https://github.com/gitofolio/gitofolio 

 

GitHub - gitofolio/gitofolio: 💎 Github, Notion, Blog... 를 자신의 이력 카드로 꾸미세요

💎 Github, Notion, Blog... 를 자신의 이력 카드로 꾸미세요. Contribute to gitofolio/gitofolio development by creating an account on GitHub.

github.com