들어가며
업무를 하며 form 데이터를 서버에 보내야 할일이 생겼다.
form 에서 User 가 Sign 을 하면 그 Sign 한 값을 서버로 보내야 했다.
<canvas id="drawCanvas" style="position: relative;"></canvas>
위 사인한 값을 추출해서 서버로 보내야 했고, 서버로 값을 같이 보내기 위해 아래 방법을 사용했다.
<input type="hidden" id="signImg" name="sign">
function onSignSave() {
const canvas = document.getElementById('drawCanvas');
const dataUrl = canvas.toDataURL();
// 숨겨진 input 필드에 Data URL 저장
document.getElementById('signImg').value = dataUrl;
}
위 코드를 사용하고 사용하여 input 폼에 Value 를 줘서 서버로 보내지게 만들었다.
그리고 실제로 위 Sign 은 URL 형태로 되어 있다.
대충 요약하면 진짜 값이 끝도 없이 길다^^
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAA~"
대충 위 인코딩된 사인 URL 값을 추출해보면 위 줄에 한 10배가 넘는 길이에 String 이 나온다.
그리고 data:image/png;base64, 부분을 뺀 인코딩된 값을 URL 에 붙여넣으면 진짜로 사진이 뜬다!
살짝은 신기하지만 너무 길다..^^
나는 위 값을 디코딩하여 img 로 만들어야 한다.
그렇기 위해서는 일단 디코딩 하는 작업이 필요할 것 같다.
디코딩 작업을 많이 시도하기 위해 테스트 코드를 짜면서 시도를 했다.
본론
✅ 테스트 코드는 정확한 값을 비교하기 위해서 짜본게 아닌, 단순 메인 역할로 사용했습니다.
처음에 시도했던 기초적인 방법을 일단 나열해봤습니다.
1) Java 8 기본 Base64
기본 인코더는 모든 것을 간단하게 유지하고 줄 구분 없이 입력 내용을 그대로 인코딩합니다.
@Test
@DisplayName("Base64 디코팅 테스트")
void 이미지_디코딩() {
// given
String input = "data:image/png;base64,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";
String encodeString = Base64.getEncoder().encodeToString(input.getBytes());
System.out.println(encodeString);
byte[] decode = Base64.getDecoder().decode(encodeString);
String decodeString = new String(decode);
System.out.println(decodeString);
}
결과는 decode 한 값은 새로운 엄청난 긴 값이 나왔고, encode한 값은 처음이랑 당연하게 같게 나왔습니다.
근데 웃긴게 위 값을 디코딩할때 에러가 났습니다.
java.lang.IllegalArgumentException: Illegal base64 character a
라는 오류였고 위 오류가 발생하는 이유는 '줄바꿈' 때문에 나는 에러였다.
input = input.replaceAll("\\s+", "");
위 코드를 통해 줄바꿈을 전부 없애버렸습니다.
2) 문자열을 Byte 배열로 변환
@Test
@DisplayName("Base64 디코팅 테스트")
void Base64_디코딩() {
String input = "iVBORw0C==asdasd";
input = input.replaceAll("\\s+", "");
byte[] decodeBytes = Base64.getDecoder().decode(input);
String decodedString = new String(decodeBytes);
Assertions.assertThat(input).isEqualTo(decodedString);
}
위 코드를 통하여 Base64 로 인코딩된 문자열을 디코딩할 수 있습니다.
또는 라이브러리를 사용하여 인코딩 및 디코딩을 할 수 있습니다.
implementation 'commons-io:commons-io:2.13.0'
위 commons 라이브러리는 아파치에서 만들어둔 오픈소스인데 유용한 라이브러리들이 많이 있습니다.
개발을 할 때 위 라이브러리를 사용한다면 개발생산성에 분명 도움이 될 거라고 생각합니다.
String originalInput = "HelloSpringBase64";
Base64 base64 = new Base64();
// 인코딩
String encodedString = new String(base64.encode(originalInput.getBytes()));
// 디코딩
String decodedString = new String(base64.decode(encodedString.getBytes()));
하지만 내가 해결한 방법은 위 방법이 맞긴하지만, 다른 방법으로 문제를 해결했다.
일단 자체적으로 Base64로 인코딩된 ImageUrl 을 디코딩하는 함수를 만들었다.
private byte[] decodeBase64ImageUrl(String dataUrl) {
String base64Image = dataUrl.split(",")[1];
return Base64.getDecoder().decode(base64Image);
}
return 타입이 byte[] 인 이유는 나는 I/O 작업을 하고 있어, String 데이터를 byte 로 만들어야 했기 때문이다.
split 으로 쪼개서 배열에 두번째를 String 에 담은 이유는
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAA~"
인코딩된 문자열이 앞에 '이 데이터는 이미지이고 확장자는 png 이고 base64로 인코딩 되어있어'
라고 알려주는 문구가 있기 때문에 실질적인 데이터는 , 뒤에 있기에 짤라서 데이터를 담았다.
그리고 docx4j 라는 라이브러리를 사용해서 문제를 해결했다.
위 라이브러리는 Java 에서 word 파일 즉 확장자가 doc, docx, dotx 인 파일을
html 로 변환도 해주고, pdf 로 변환해주는 여러 기능을 제공해주는 convert 라이브러리이다.
나는 docx 파일 내부 내용을 수정해서 서버로 전송해야 했기에 위 라이브러리들을 사용했다.
그리고 아래 코드를 통해 docx 파일 내부 데이터를 replace 쳐서 문제를 해결했다.
public void updateContractFile(SignUpDTO signup, String folderName) throws Exception {
String filePath = "merchant/contract.docx";
WordprocessingMLPackage wordMLPackage = loadDocxFromS3(filePath);
VariablePrepare.prepare(wordMLPackage);
byte[] imageBytes = decodeBase64ImageUrl(signup.getSign());
replaceTextWithImage(wordMLPackage, "${sign}", imageBytes);
Map<String,String> mappings = new HashMap<>();
mappings.put("${merchantName}", signup.getMerchantName());
mappings.put("${yearMonthDay}", new SimpleDateFormat("yyyy년 MM월 dd일").format(new Date()));
mappings.put("${name}", signup.getName());
mappings.put("${number}", signup.getPhoneNumber());
replaceVariables(wordMLPackage, mappings);
uploadDocxToS3(folderName, wordMLPackage);
}
결론
Base64 인코딩 및 디코딩 방법까지 알게되는 시간이였고, 역시나 모르는게 너무 많아 조금씩 지식을 쌓는데 재미가 들리고 있는 것 같다.
ref
https://www.baeldung.com/java-base64-encode-and-decode