국제화Internationalization, i18n

여러 국가에서 사용할 수 있도록 설계하는 것

지역화Localization, l10n

사용 국가별 환경에 대해 지원하는 것

다국어화Multilingualization, m17n

사용 국가별 다양한 언어를 지원하는 것


국제화, 지역화(또는 현지화)의 영역[각주:1]

  1. 언어
    - 컴퓨터의 문자 인코딩
    - 글자의 그림 표현(인쇄물이나 텍스트를 담고 있는 이미지)
    - 녹음된 음성
    - 영상물의 자막
  2. 날짜/시간 형식, 다른 역법의 사용
  3. 시간대
  4. 통화(Currency)
  5. 이름과 직함
  6. 정부 지정번호(주민등록번호, 미국의 사회보장번호 등)와 여권
  7. 전화 번호, 주소, 국제 우편 번호
  8. 중량과 치수
  9. 종이 크기

지역화에만 적용되는 내용

  1. 번역
  2. 동아시아 언어(한자 문화권) 등에 대한 특별한 지원
  3. 지역의 관습
  4. 기호
  5. 정렬의 순서
  6. 미학(미적 감각)
  7. 문화적인 값어치, 사회적인 환경


시차 표현을 위해서 사용되는 규격? 뭐라고 하지 표현? 기준?

UTCCoordinated Universal Time, 협정 표준시

PSTPacific Standard Time, 태평양 표준시

GMTGreenwich Standard Time, 그리니치 표준시

CSTCentral Standard Time, 중부 표준시

KSTKorean Standard Time, 한국 표준시

DSTDaylight Saving Time

PDTPacific Daylight Time

ESTEastern Standard Time, 동부 표준시



참고 사이트

한글 위키피디아 국제화와 지역화 - http://ko.wikipedia.org/wiki/국제화와_지역화

영어 위키피디아 List of UTC time offsets - http://en.wikipedia.org/wiki/List_of_UTC_time_offsets

The Time Now - http://ko.thetimenow.com/pst/pacific_standard_time

영어 위키피디아 List of time zones by country - http://en.wikipedia.org/wiki/List_of_time_zones_by_country

영어 위키피디아 Daylight saving time by country - http://en.wikipedia.org/wiki/Daylight_saving_time_by_country

ISO 3166 Codes (Countries) - http://userpage.chemie.fu-berlin.de/diverse/doc/ISO_3166.html


자바 타임 존 변경 예제

http://mwultong.blogspot.com/2006/11/java-world-time-timezone.html


 gentoo가 뭐지..? https://wiki.gentoo.org/wiki/Localization/HOWTO



자바의 시간 표현

TimeZone, DateFormat, Locale, Calendar, Time

sun.util 패키지..

Joda Time - http://www.joda.org/joda-time/quickstart.html

  1. http://ko.wikipedia.org/wiki/국제화와_지역화 [본문으로]

'밤을 지새다' 카테고리의 다른 글

웹 다이어그램 드로잉 툴  (0) 2014.07.08
Google Drive를 이용한 파일 버전 관리  (0) 2013.05.24
Linux Kernel Map  (0) 2013.03.24
C# ListBox Double Click 이벤트  (0) 2012.06.08
네이트온 광고와 팝업 없애기  (3) 2012.05.16

개발 환경

- IDE : Spring Tool Suite 3.3.0.RELEASE

- Spring Framework 3.1.1 (Web MVC 포함)

- OS : Max OSX 10.8.5


연습으로 해본 이미지 업로드 프로젝트입니다. 잘못된 내용이 있거나 개선할 내용이 있으면 댓글 남겨주시면 배우도록 하겠습니다 :)



먼저 STS에서 Spring MVC Web 프로젝트를 하나 만듭니다.








프로젝트 탐색기에 보이는 프로젝트의 모습입니다. 위 단계까지 따라오셨으면 아래에 보이는 파일 중에 없는 것 몇가지가 있습니다. 이 글을 끝까지 따라가면 보이는 프로젝트 파일의 모습입니다.

예제 프로젝트에서는 패키지를 따로 나누지 않고 통패키지(..)에 모든 클래스 파일을 담았습니다.






먼저 servlet-context.xml 파일에 Multipart 사용을 위해서 멀티파트 리졸버를 추가합니다.


<!-- Multipart를 받기 위한 리졸버 추가 -->
<beans:bean id="multipartResolver"
	class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>




MultipartResolver를 사용하게 되면 두개의 라이브러리가 추가적으로 필요합니다. Commons File Upload와 IO입니다.

메이븐을 사용한다면 의존 라이브러리를 추가해주고, 그 외 빌드 툴이나, 혹은 사용하지 않는다면 환경에 맞게, 두가지의 라이브러리를 추가해줍니다.

<!-- Apache Commons File Upload -->
<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.2</version>
</dependency>

<!-- Apache Commons IO 추가 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-io</artifactId>
	<version>1.3.2</version>
</dependency><






이미지 업로드 컨트롤러 클래스 내용입니다.

이미지 업로드를 위한 폼 페이지와 결과 페이지 및 이미지를 얻기 위한 URL을 매핑합니다.

package com.nnoco.example;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class ImageUploadController {
	/*
	 * ImageView는 파일 시스템에 있는 이미지 파일을 응답으로 돌려주는 역할을 합니다.
	 * 뒷 부분에서 ImageView 클래스를 작성하게 됩니다.
	 */
	@Resource(name="imageView") ImageView imageView;

	/**
	 * 이미지를 관리하는 서비스 계층 클래스입니다. 예제에서는 디비를 사용하긴 버거워지므로
	 * 서비스 클래스를 따라하는 모양만 서비스인 클래스입니다.
	 */
	@Autowired ImageService imageService;
	
	/**
	 * 이미지 업로드를 위한 페이지 매핑 
	 */
	@RequestMapping("/uploadPage")
	private String uploadView() {
		return "upload";
	}
	
	/**
	 * 이미지 업로드 페이지의 폼에서 전송 시 받게 되는 메서드 
	 */
	@RequestMapping(value="/upload", method=RequestMethod.POST)
	private String upload(@RequestParam MultipartFile imageFile, ModelMap modelMap) {
		ImageFile fileInfo = imageService.save(imageFile);
		
		modelMap.put("imageFile", fileInfo);
		
		return "uploadComplete";
	}
	
	@RequestMapping("/image/{imageId}")
	private ImageView getImage(@PathVariable String imageId, ModelMap modelMap) {
		ImageFile imageFile = imageService.get(imageId);
		
		modelMap.put("imageFile", imageFile);
		
		return imageView;
	}
}




업로드한 이미지 정보를 담고있는 ImageFile 클래스입니다.

package com.nnoco.example;

public class ImageFile {
	/**
	 * 업로드한 이미지 파일이 저장될 경로
	 */
	public static final String IMAGE_DIR = "/web/upload_images/";

	private String id;
	private String contentType;
	private int contentLength;
	private String fileName;

	public ImageFile(String id, String contentType, int contentLength,
			String fileName) {
		this.id = id;
		this.contentType = contentType;
		this.contentLength = contentLength;
		this.fileName = fileName;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getContentType() {
		return contentType;
	}

	public void setContentType(String contentType) {
		this.contentType = contentType;
	}

	public int getContentLength() {
		return contentLength;
	}

	public void setContentLength(int contentLength) {
		this.contentLength = contentLength;
	}

	public String getFileName() {
		return fileName;
	}

	public void setFileName(String fileName) {
		this.fileName = fileName;
	}

}






이미지 뷰 클래스입니다.

package com.nnoco.example;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;


@Component("imageView")
public class ImageView extends AbstractView{
	@Override
	protected void renderMergedOutputModel(Map model,
			HttpServletRequest req, HttpServletResponse res) throws Exception {
		ImageFile imageFile = (ImageFile)model.get("imageFile");
		
		// 응답 메시지에 파일의 길이를 넘겨줍니다.
		res.setContentLength(imageFile.getContentLength());

		// 응답의 타입이 이미지임을 알려줍니다.
		res.setContentType(imageFile.getContentType());
		
		// 파일로부터 byte를 읽어옵니다.
		byte[] bytes = readFile(imageFile.getFileName());
		write(res, bytes);
	}

	/**
	 * 파일로부터 byte 배열 읽어오기 
	 */
	private byte[] readFile(String fileName) throws IOException {
		String path = ImageFile.IMAGE_DIR + fileName;
		
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
		int length = bis.available();
		byte[] bytes = new byte[length];
		bis.read(bytes);
		bis.close();
		
		return bytes;
	}

	/**
	 * 응답 OutputStream에 파일 내용 쓰기
	 */
	private void write(HttpServletResponse res, byte[] bytes) throws IOException {
		OutputStream output = res.getOutputStream();
		output.write(bytes);
		output.flush();
	}
}




이미지 서비스 클래스입니다.

모양만 서비스이고, 구현은 전혀 아닙니다. 맵 자료구조를 사용해서 DB를 대신했습니다.

package com.nnoco.example;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public class ImageService {
	private Map imageFilesMap;
	
	public ImageService() {
		init();
	}
	
	/**
	 * 초기화
	 */
	private void init() {
		imageFilesMap = new HashMap();
	}
	
	/**
	 * ID로 이미지 파일 가져오기
	 */
	public ImageFile get(String id) {
		return imageFilesMap.get(id);
	}
	
	/**
	 * Multipart File을 파일로 저장하고 DB(를 빙자한 맵)에 업로드 파일 정보 저장, 실패하는 경우 null리
	 */
	public ImageFile save(MultipartFile multipartFile) {
		// UUID로 유일할 것 같은 값 생성.. 낮은 확률로 중복 가능성이 있음
		String genId = UUID.randomUUID().toString();
		ImageFile imageFile = null;
		
		try {
			String savedFileName = saveToFile(multipartFile, genId);
			
			imageFile = new ImageFile(genId, 
					multipartFile.getContentType(),
					(int)multipartFile.getSize(),
					savedFileName);
			
			imageFilesMap.put(genId, imageFile);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return imageFile;
	}
	
	/**
	 * Multipart File의 내용을 파일로 저장, 저장 후 저장된 파일 이름을 반환
	 */
	private String saveToFile(MultipartFile src, String id) throws IOException {
		String fileName = src.getOriginalFilename();
		byte[] bytes = src.getBytes();
		String saveFileName = id + "." + getExtension(fileName);
		String savePath = ImageFile.IMAGE_DIR + saveFileName;

		/* 파일 쓰기 */
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(savePath));
		bos.write(bytes);
		bos.flush();
		bos.close();
		
		return saveFileName;
	}
	
	/**
	 * 파일이름으로부터 확장자를 반환하는 메서드
	 * 파일이름에 확장자 구분을 위한 . 문자가 없거나. 가장 끝에 있는 경우는 빈문자열 ""을 리턴
	 */
	private String getExtension(String fileName) {
		int dotPosition = fileName.lastIndexOf('.');
		
		if (-1 != dotPosition && fileName.length() - 1 > dotPosition) {
			return fileName.substring(dotPosition + 1);
		} else {
			return "";
		}
	}
}


파일 업로드를 위한 JSP 페이지와 결과 확인 페이지입니다.

upload.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>이미지 업로드</title>
</head>
<body>
	<form action="./upload" method="post" enctype="multipart/form-data">
		<input type="file" name="imageFile"><br>
		<input type="submit" value="전송">
	</form>
</body>
</html>


uploadComplete.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>업로드 결과 페이지</title>
<style type="text/css">
	.failed {
		color: red;
		font-style: bold;
		font-size:18pt;
	}
</style>
</head>
<body>
	<c:choose>
		<c:when test="${imageFile != null }">
		파일 업로드 완료
		<ul>
			<li>파일 ID : ${imageFile.id }</li>
			<li>저장된 파일 이름 : ${imageFile.fileName }</li>
			<li>파일 길이 : ${imageFile.contentLength }</li>
			<li>MIME 타입 : ${imageFile.contentType }</li>
		</ul>
		
		<img src="${pageContext.request.contextPath}/image/${imageFile.id}">
		</c:when>
		<c:otherwise>
		<span class="failed">파일 업로드 실패</span>		
		</c:otherwise>
	</c:choose>
</body>
</html>




여기까지 필요한 설정과 파일을 추가하고, 서버에 올려 접속을 하면 아래와 같은 아주 간단한 폼과 함께 파일을 업로드 해볼 수 있습니다.

이미지 업로드라고 하긴 했지만 사실 이미지 업로드를 위해 한 일은 없고, 파일 업로드를 하는 작업만 한 것 같네요.





위 예제 프로젝트는 아래 파일을 다운로드하시어 살펴보실 수 있습니다.

ImageUploadExample.zip


어떤 URL로 리다이렉트하거나, 링크를 클릭했을 때 이동하지 않고 다운로드 하게 하려면,

브라우저는 응답 헤더의 내용을 보고 URL로 이동할지 다운로드를 할지 결정하기 때문에

서버측에서 아래와 같은 헤더 정보를 담아 주어야 한다.


Content-Type: application/octet-stream

Content-Disposition: attachment; filename=<브라우저에서 저장할 파일 이름>


지금 개발하고 있는 Touch me는 이벤트 광고 플랫폼으로 앱 사용자가 특정 광고에 참여 또는 공유하는 경우 일정 포인트가 쌓이게 된다. 참여 또는 공유 정보는 서버로 전송되고 유효한 액션인 경우에 포인트가 쌓이도록 해야하는데,

포인트는 현금과 1:1의 가치를 갖기 때문에 보안이 중요하다.

헌데 지금까지 보안을 고려한 개발을 제대로 해본적이 없어서, 어떻게 보호를 해야하고, 무엇을 보호해야할 지에 대해 모르는 상태다. 이 때문에 기본적인 암호화에 대한 공부를 하게 되었다.

지금까지는 인터넷에서 찾은 정보로 알게된 것이 전부이고, 더 공부를 하고 싶어서 '알아야 막는다 - 자바 JSP 해킹과 보안'과 '실용 암호학 : 보안 실무자를 위한 정보보호와 암호화 구현' 두 권의 책을 구매한 상태이다.

4학년 수업 중에 교재로 사용하는 정보 보호와 보안 이라는 책이 있었던 것 같은데 정확한 책 제목과 저자를 알지 못해 함께 구매하진 못했다.


앱을 통해 포인트를 적립하라는 HTTP 메시지의 암호화를 위해서는 무엇을 암호화 해야할까?

굳이 포인트 적립에 대한 메시지를 암호화하는 까닭은 현금과 포인트는 같은 가치를 가지기 때문에 유효하지 않은 포인트 적립, 즉 포인트 적립을 비정상적인 방법으로 적립할 수 있기 때문에 포인트 적립 메시지의 암호화를 총해 유효함을 검증하기 위함이다. 또한 비정상적인 광고 참여, 공유는 광고 효과와 서비스의 신뢰성과도 직결되기 때문에 서비스에서 중요한 부분일 수 있다.

사용자에게 광고 효과를 얻기 위해서 참여와 공유를 하도록 하고 이에 대한 보상으로 포인트를 지급하는 것인데, 광고 효과가 없는 포인트 지급은 의미 없는 지출일 뿐이니 말이다.


포인트 적립에 필요한 정보는 관련 광고, 유저 정보, 포인트 정보이다.

포인트 적립이 '유효함'을 보장하기 위해서는 사용자가 직접 어떤 액션을 해야한다는 것인데,

쓰다보니 굳이 암호화 하지 않더라도 서버에서 유효함을 보장하기 위한 1회성 토큰을 전송하는 것도 괜찮을 듯하다..


이 부분에 대해서는 아직 정리되지 않은 부분이 많아, 프로토콜에 대한 내용을 다시금 정리해봐야겠다.


서론이 너무 길었던 것 같다. 어쨌든.. 위의 고민을 하면서 알아봤던 RSA 암호화 알고리즘에 대해서 포스팅 해야겠다.

다시 잡설이지만 무엇인가를 배운 후에, 정리하거나 누군가에게 설명하는 일은 내가 다시 공부하는 것이라고 생각한다.


RSA 비대칭키 암호화 알고리즘

RSA 알고리즘은 공개키 암호 알고리즘의 하나로 이를 연구하였던 Ron Rivest, Adi Shamir, Leonard Adleman의 이름 첫글자를 따서 지어진 이름이다.

한국 위키피디아의 RSA 암호 문서의 내용에 따르면 RSA 암호 체계의 안정성은 큰 숫자를 소인수분해하는 것이 어렵다는 것에 기반을 두고 있다고 한다. 허나 1993년 피터 쇼어가 쇼어 알고리즘을 발표하면서 양자 컴퓨터를 이용하여 임의의 정수를 다항 시간 안에 소인수분해하는 방법을 발표하였다. 즉 양자 컴퓨터가 활성화된다면 RSA 알고리즘은 쉽게 깨지게 된다. 암호로써의 역할을 잃게 되는 것이다.

RSA 비대칭키 암호화 알고리즘은 위키피디아 문서에 잘 정리되어 있다.



비대칭키 암호화 알고리즘

암호화 알고리즘에는 대칭키 알고리즘과 비대칭키 알고리즘이 있다. 먼저 대칭키 알고리즘은 암호화/복호화에 사용되는 키가 동일한 알고리즘을 의미하고, 비대칭키 암호화 알고리즘은 암호화/복호화에 사용되는 키가 서로 다른 것을 의미한다. 여기서 암호화에 사용되는 키를 공개키(Public Key)라고 하고, 복호화에 사용되는 키를 비밀키(Private Key)라고 한다. RSA 알고리즘은 공개키, 비밀키 두개의 키를 생성하지만 암호화/복호화에 있어서 어느 하나만을 사용해야한다는 제약은 없다. 즉 암호화 시에 공개키를 사용하고 비밀키를 통해서 복호화를 할 수 있지만,  반대로 암호화 시에 비밀키를 사용하고 공개키를 통해서 복호화할 수도 있다.


일반적으로 대칭키 알고리즘이 비대칭키 알고리즘에 비해서 성능이 좋다. 따라서 대량의 데이터를 암호화 하기 위해서는 비대칭키 알고리즘의 성능은 떨어지게 된다. 성능상의 보정을 위해서 데이터는 대칭키 알고리즘으로 암호화 하고, 대칭키의 키만 비대칭키 알고리즘으로 암호화하는 방법을 사용하기도 한다.



자바에서 RSA 알고리즘의 사용

자바에서는 java.crypto 패키지 내에 암호화와 관련한 구현이 되어있다. Touch Me 에서 RSA 알고리즘 사용을 위해 간략하게 유틸리티 클래스를 작성해봤다.

주요 사용 클래스는 java.crypto 패키지의 Cipher, java.security 패키지의 KeyFactory, KeyPair, KeyPairGenerator, PrivateKey, PublicKey, RSAPrivateKeySpec, RSAPublicKeySpec이다.

package com.touchme.web.security;

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.codec.binary.Base64;

/**
 * RSA 비대칭키 암호화 알고리즘 사용을 위한 유틸리티 클래스
 * @author 이준영
 *
 */
public class RSAUtils {
	public static final String RSA = "RSA";
	public static KeyPair generateKeyPair() {
		KeyPair keyPair = null;
		try {
			keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return keyPair;
	}
	
	/**
	 * Public Key로 암호화한 후 결과로 출력된 byte 배열을 Base64로 인코딩하여 String으로 변환하여 리턴함
	 * @param text 암호화할 텍스트
	 * @param publicKey RSA 공개키
	 * @return Base64로 인코딩된 암호화 문자열
	 */
	public static String encrypt(String text, PublicKey publicKey) {
		byte[] bytes = text.getBytes();
		String encryptedText = null;
		try {
			Cipher cipher = Cipher.getInstance(RSA);
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			encryptedText = new String(Base64.encodeBase64(cipher.doFinal(bytes)));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}
		
		return encryptedText;
	}
	
	/**
	 * Base64로 인코딩된 문자열을 받아 decode 시킨 후 RSA 비밀키(Private Key)를 이용하여 암호화된 텍스트를 원문으로 복호화
	 * @param encryptedBase64Text Base64로 인코딩된 암호화 문자열
	 * @param privateKey RSA 비밀 키
	 * @return 복호화된 텍스트
	 */
	public static String decrypt(String encryptedBase64Text, PrivateKey privateKey) {
		byte[] bytes = Base64.decodeBase64(encryptedBase64Text.getBytes());
		String decryptedText = null;
		try {
			Cipher cipher = Cipher.getInstance(RSA);
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			decryptedText = new String(cipher.doFinal(bytes));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}
		
		return decryptedText;
	}
	
	/**
	 * RSA 공개키로부터 RSAPublicKeySpec 객체를 생성함
	 * @param publicKey 공개키
	 * @return RSAPublicKeySpec
	 */
	public static RSAPublicKeySpec getRSAPublicKeySpec(PublicKey publicKey) {
		RSAPublicKeySpec spec = null;
		try {
			spec = KeyFactory.getInstance(RSA).getKeySpec(publicKey, RSAPublicKeySpec.class);
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return spec;
	}
	
	/**
	 * RSA 비밀키로부터 RSAPrivateKeySpec 객체를 생성함
	 * @param privateKey 비밀키
	 * @return RSAPrivateKeySpec
	 */
	public static RSAPrivateKeySpec getRSAPrivateKeySpec(PrivateKey privateKey) {
		RSAPrivateKeySpec spec = null;
		
		try {
			spec = KeyFactory.getInstance(RSA).getKeySpec(privateKey, RSAPrivateKeySpec.class);
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return spec;
	}
	
	/**
	 * Moduls, Exponent 값을 이용하여 PublicKey 객체를 생성함
	 * @param modulus RSA Public Key Modulus
	 * @param exponent RSA Public Key exponent
	 * @return PublicKey 객체
	 */
	public static PublicKey getPublicKey(String modulus, String exponent) {
		BigInteger modulus_ = new BigInteger(modulus);
		BigInteger exponent_ = new BigInteger(exponent);
		PublicKey publicKey = null;
		
		try {
			publicKey = KeyFactory
					.getInstance(RSA)
					.generatePublic(new RSAPublicKeySpec(modulus_, exponent_));
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return publicKey;
	}
	
	/**
	 * Modulus, Exponent 값을 이용하여 PrivateKey 객체를 생성함
	 * @param modulus RSA private key Modulus
	 * @param privateExponent RSA private key exponent
	 * @return PrivateKey 객체
	 */
	public static PrivateKey getPrivateKey(String modulus, String privateExponent) {
		BigInteger modulus_ = new BigInteger(modulus);
		BigInteger privateExponent_ = new BigInteger(privateExponent);
		PrivateKey privateKey = null;
		
		try {
			privateKey = KeyFactory
					.getInstance(RSA)
					.generatePrivate(new RSAPrivateKeySpec(modulus_, privateExponent_));
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return privateKey;
	}
}


Base64

Touch Me에서는 문자열로 서버와 안드로이드 애플리케이션 간에 데이터를 주고받게 된다.

만약 JSON 포맷의 데이터라면 UTF-8로 인코딩된 문자열을 주고 받으므로 사람의 눈으로 문자를 확인하면 정상적인 JSON 문자열을 확인할 수 있다. 이 JSON 문자열을 암호화 하게 된다면 문자 인코딩이 적용되지 않은 바이트배열로 구성되어 있으므로 이를 문자열로 변환하여 사람의 눈으로 확인한다면 의미없는 문자 또는 폰트로 표현되지 않는 문자로 보여질 수 있다. 이 때 정상적인 문자로 보여주기 위해서 Base64로 인코딩한다.

간단하게 말하자면 Base64는 64진수를 의미한다. 6bit의 이진 데이터를 ASCII 문자로 매핑하여 변환하는데 숫자 0~1, a~z, A~Z, +, / 로 표현하게 된다. 이진 데이터가 6bit로 딱 떨어지지 않는 경우가 발생할 수 있다. 이를 표현하기 위해서 패딩(Padding) 문자 = 를 사용한다.

이진데이터의 길이 n (1bit를 1로 했을 때) 을 3으로 나눈 나머지 즉 r = n % 3 에서

r이 0인 경우 패딩문자를 붙이지 않는다.

r이 1인 경우 패딩문자 = 하나를 붙인다.

r이 2인 경우 패딩문자 = 두개를 붙인다.

Base64 인코딩 순서는 아래와 같다.

1. ASCII 테이블 매핑

2. 2진수 변환

3. 6bit 단위로 분할

4. 10진수 변환

5. Base64 테이블 매핑

6. 패딩 연산

문자 인코딩 방법은 Base64외에도 ASCII, URL, HTML 등이 있으며, allstar927님의 블로그에 '인코딩이란? (ASCII, URL, HTML, Base64, MS Script 인코딩)'이란 제목으로 깔끔하게 정리되어 포스팅이 되어있다.

Base64의 자바구현은 다양한 벤더에서 제공하고 있지만 Touch Me 에서는 Apache Commons Codec을 사용하였다.


내일이면 주문한 도서가 도착할 듯하다. 터치미 서비스의 신뢰성과 안전성을 위해서 체계적으로 정리해서 적용해봐야겠다.




* 참고 글

1. allstar927님 - 인코딩이란? (ASCI, URL, HTML, Base64, MS Script 인코딩)

2. www.java2s.com - Generate a RSA public key with given modulus and public/private exponent

3. www.javamex.com - RSA ecryption in Java

4. 剛宇님 - 자바 암호화 - RSA

5. 류지현(瀏智賢)님 - 자료구조 8. 대칭형 암호화, 비대칭형 암호화

6. 권남님 - RSA 기반 웹페이지 암호화 로그인

7. 오늘도 커피 3잔? - AES를 이용한 128비트 공통키의 생성과 암호화

8. 위키피디아 - RSA 암호 (영문, 한글)

Netty를 이용해서 웹 소켓 서버를 만들고 클라이언트에서 웹 소켓을 열고 통신을 하다보니

웹 소켓에서는 아직 텍스트 통신만 지원한다.


따라서 양쪽에서 서로 쓰기 좋은 Json 포맷으로 통신하다보니 서버쪽은 자바로 구성되어 Gson 라이브러리를 쓰고,

클라이언트에서 자바스크립트로 Json <-> Object 가 필요해서 찾아보니 내장 객체로 JSON이 있다는 것을 알게 되었다.

(관련 Stack Overflow 글)


JSON Parsing 내장 객체를 지원하는 브라우저는 다음과 같다


(http://caniuse.com/json)


JSON 객체의 함수로는 Object를 Json 문자열로 변환하는 stringify 와 반대로 Json 문자열을 객체로 변환하는 parse 함수가 있다.


JSON에 관한 설명은 MSDN에 잘 나와있어 사용방법에 관한 내용은 생략하고 매개변수에 대한 내용만 스크랩했다.

http://msdn.microsoft.com/ko-kr/library/ie/cc836466(v=vs.94).aspx


1. JSON.stringify(value [, replacer] [,space])

value

필수 요소. JavaScript 값은 일반적으로 변환할 개체 또는 배열입니다.

replacer

선택 사항입니다. 결과를 변환하는 함수 또는 배열입니다.

replacer가 함수이면 JSON.stringify는 키와 각 멤버의 값을 전달하여 함수를 호출합니다. 반환 값은 원본 값 대신 사용됩니다. 함수가 undefined를 반환하면 멤버가 제외됩니다. 루트 개체의 키는 빈 문자열인 ""입니다.

replacer가 배열이면 배열에 키 값이 있는 멤버만 변환됩니다. 멤버가 변환되는 순서는 배열의 키 순서와 같습니다. replacer 배열은 value 인수도 배열인 경우 무시됩니다.

space

선택 사항입니다. 읽기 쉽도록 들여쓰기, 공백, 줄 바꿈 문자를 반환 값 JSON 텍스트에 추가합니다.

space가 생략되면 반환 값 텍스트가 추가 공백 없이 생성됩니다.

space가 숫자이면 반환 값 텍스트가 각 수준의 지정된 공백 수로 들여쓰기됩니다. space가 10보다 크면 텍스트가 10칸 들여쓰기됩니다.

space가 '\t'와 같이 빈 문자열이 아니면 반환 값 텍스트가 각 수준의 문자열의 문자로 들여쓰기됩니다.

space가 10자보다 긴 문자열이면 처음 10자가 사용됩니다.

변환할 객체에 toJSON(key) 함수가 있는 경우 해당 키를 가진 값을 변환하여 반환한다.



2. JSON.parse(value [,reviver])

value

필수 요소. JavaScript 값은 일반적으로 변환할 개체 또는 배열입니다.

replacer

선택 사항입니다. 결과를 변환하는 함수 또는 배열입니다.

replacer가 함수이면 JSON.stringify는 키와 각 멤버의 값을 전달하여 함수를 호출합니다. 반환 값은 원본 값 대신 사용됩니다. 함수가 undefined를 반환하면 멤버가 제외됩니다. 루트 개체의 키는 빈 문자열인 ""입니다.

replacer가 배열이면 배열에 키 값이 있는 멤버만 변환됩니다. 멤버가 변환되는 순서는 배열의 키 순서와 같습니다. replacer 배열은 value 인수도 배열인 경우 무시됩니다.

space

선택 사항입니다. 읽기 쉽도록 들여쓰기, 공백, 줄 바꿈 문자를 반환 값 JSON 텍스트에 추가합니다.

space가 생략되면 반환 값 텍스트가 추가 공백 없이 생성됩니다.

space가 숫자이면 반환 값 텍스트가 각 수준의 지정된 공백 수로 들여쓰기됩니다. space가 10보다 크면 텍스트가 10칸 들여쓰기됩니다.

space가 '\t'와 같이 빈 문자열이 아니면 반환 값 텍스트가 각 수준의 문자열의 문자로 들여쓰기됩니다.

space가 10자보다 긴 문자열이면 처음 10자가 사용됩니다.



금방까지 작업하던 이클립스를 종료하고 다시 시작했더니,


로딩 중에 응답이 없어지면서 더 이상 진행이 되질 않는다.

로딩 중인 패키지는 org.eclipse.mylyn.task.ui





키워드를 org.eclipse.mylyn.task.ui 그대로 검색해보니 이클립스의 버그인 듯 하다

참고 : http://eclipse.1072660.n5.nabble.com/Workbench-fails-to-start-Plug-in-org-eclipse-mylyn-tasks-ui-was-unable-to-load-class-org-eclipse-mylt-td134522.html


고치는 방법은 실행 시 워크스페이스 입력 대화상자에서 다른 워크스페이스를 선택해서 한번 실행했다가

File 메뉴에서 Switch Workspace를 이용해 원래의 워크스페이스를 선택하면

이클립스가 재시작되면서 정상적으로 동작한다.



Android SDK Tool 20 버전 이상부터 Library Project의 AndroidManifest.xml 의 내용을 애플리케이션 프로젝트의 Manifest에 병합할 수 있게 되었다 (관련 링크 : http://tools.android.com/download/adt-20-preview)


  • Build System
    • Automatic merging of library project manifest files into the including project's manifest. Enable with the manifestmerger.enabled property.


이전에는 Library Project의 매니페스트에 서비스나 액티비티, 퍼미션 등을 등록하더라도 해당 Library Project를 사용하는 애플리케이션 프로젝트에는 아무런 적용이 되지 않았는데, ADT 20 부터는 이를 활용할 수 있게 된것이다.


지금 소마에서 개발하고 있는 Salina의 안드로이드 SDK에서 퍼미션이나, 서비스 사용을 위해서 애플리케이션 프로젝트에서 이래저래 설정해줘야 할 것들이 많았는데, 이것으로 SDK의 적용이 아주 쉬워졌다. 라이브러리를 개발하는 입장에서는 아주 큰 이점이 아닌가 싶다.


라이브러리 프로젝트와 애플리케이션 프로젝트의 Manifest의 내용이 중복되는 경우의 충돌은 확인해 봐야할 듯 하다.


적용방법은 간단하다.  프로젝트의 설정파일(project.preference)에서 manifestmerger.enabled=true 만 추가해주면 적용된다.

Salina Project에서 RatingBar가 아래 같은 모습으로 들어가야 하는데 기본 크기는 너무 크다






그렇다고 그냥 RatingBar의 layout_width, layout_height의 크기를 줄여버리면 이런 모습이 되어버린다.


ㅡㅡ...




검색 결과 중 대부분이 RatingBar를 커스터마이징 하기 위해서 style xml을 만들어내는 것이었는데,

(http://blog.naver.com/PostView.nhn?blogId=telsome&logNo=90094111681)

RatingBar를 커스터마이징 할 수도 있는 거지만 크기만 변경하는 되는 문제여서 더 간단한 것을 찾다 보니 

StackOverFlow에서 답을 찾을 수 있었다.


>> StackOverFlow : Decrease the size of Rating Bar in android 2.1


RatingBar의 xml에 속성에 style="?android:attr/ratingBarStyleSmall" 를 추가하면 보다 작은 크기로 적용이 된다.


<RatingBar
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    style="?android:attr/ratingBarStyleSmall"/>


다만 정확히 원하는 크기로 변경하기 위해서는 커스터마이징을 해야한다.

이미지뷰를 화면에 꽉 채우기에 앞서 뷰의 영역에 대해 먼저 설명하겠습니다.



그림이 다소 난잡하지만, 하나의 뷰는 크게 세 가지 영역으로 볼 수 있습니다.

뷰가 실제로 보이는 크기와는 상관이 없지만 다른 뷰와의 거리를 조절하는 외부 여백 margin

눈에 보이는 크기에 포함되는 내부 여백 padding,

전체 뷰 크기에서 위의 두 여백을 제외하고 내용이 들어가는 부분으로 나뉘어집니다.


margin과 padding은 둘다 여백이지만 뷰에 배경색상을 지정했을 때 margin 영역에는

배경색상이 지정되지 않고 공간만을 차지하고, padding 부분에는 배경색상이 적용됩니다.


왜 이 이야길 꺼냈느냐 하면, 액티비티에 이미지뷰를 삽입하려면 이미지뷰를 둘러싸고 있는 레이아웃이

padding의 값이 0이 아닌 경우가 있기 때문에 이미지뷰를 아무리 키운다고 한들 여백이 생기기 때문입니다.


IDE (Eclipse나 IntelliJ 기반의 Android Studio)에서 새로운 레이아웃 XML을 자동으로 생성하면

기본 레이아웃은 RelativeLayout이고 요런 속성이 주렁주렁 달려서 자동으로 xml 문서가 생성됩니다.

android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"


저렇게 자동으로 생성된 xml 레이아웃 파일의 경우 저 부분을 제거해주어야 원하는 대로 화면에 꽉 찬 이미지뷰를 만들 수 있습니다.



여백에 관한 얘기는 여기까지 하고, 이미지뷰에서 이미지를 채우기 관련 속성인 scaleType에 대해 알아보겠습니다.

scaleType 속성이 가질 수 있는 값은



위의 그림과 같이 matrix, fitXY, fitStart, fitCenter, fitEnd, center, centerCrop, centerInside 여덟가지입니다.


위 속성에 관해 녹두장군님의 포스트(http://mainia.tistory.com/473)에서 설명이 잘 되어있어 링크로 대체하도록 하고, 화면에 이미지를 맞추기 위해서는 scaleType의 값으로 fitXY를 사용하면됩니다.


위의 내용대로 한다면 xml의 내용은 아래와 같이 나오게됩니다.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".IntroActivity" >

    <ImageView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/salina_community_intro"
        android:scaleType="fitXY"/>

</LinearLayout>


추가적으로 Manifest에서 Activity의 theme 속성 수정을 통해서 타이틀바의 유무, 상태표시바 유무(풀스크린)를 수정할 수 있습니다. 어찌보면 별 내용도 없는 것을 횡설수설 괜히 길게 썼네요. 도움이 되셨길 바랍니다~!  :)

작업을 하다보면 파일 관리를 해야할 경우가 많습니다.

개발을 할 때는 소스코드 버전 관리를 위한 툴이 많이 있고 일반 문서나 이미지 등의 파일 버전 관리 또한

Git이나 Subverision 등의 기존 버전 관리 툴을 이용할 수는 있지만 얼마전 사업 계획서 작성을 하면서 Google Drive를 이용해서 팀 구성원 내에서 파일 공유를 하며 버전 관리를 쉽게 할 수 있어 사용 방법을 공유하고자 합니다.


구글 드라이브는 구글 드라이브 서비스 전 구글 Docs 기능을 함께 사용할 수 있기 때문에

파일 버전 관리뿐만 아니라 구글 Docs의 문서(문서, 스프레드시트, 프리젠테이션)에 대한 버전 관리도 가능합니다.

위 두 가지의 버전 관리 방법에는 차이가 있어 먼저 파일 버전 관리부터 살펴보겠습니다.


구글 드라이브를 처음 사용하는 경우 위와 같은 화면이 나타납니다.





왼쪽 상단의  버튼 옆의 화살표 버튼이 파일을 업로드하는 버튼입니다. 해당 버튼을 눌러 버전 관리를 하고 싶은 파일을 업로드 해보겠습니다.



해당 버튼을 누르면 파일 또는 폴더를 업로드 할 것인지 선택을 하게 되는데

파일을 업로드할 것이므로 파일...을 선택합니다.






파일을 선택한 후 업로드 하게 되면 화면 오른 쪽 하단에 업로드 상태와 함께 파일이 업로드 되고

파일 리스트에 반영이 됩니다.






버전 관리를 원하는 파일에서 마우스 오른쪽 버튼을 누르면 해당 파일과 관련한 메뉴가 나타나는데

이 중에서 버전 관리를 선택해서 파일 버전 관리 대화 상자를 띄웁니다.





버전 관리 대화창에서는 현재까지 관리되고 있는 버전이 리스트로 나타나게 됩니다.

현재는 파일을 업로드만 한 상태이므로 하나의 파일만 존재합니다.

이 상태에서 새로운 버전으로 업데이트 하고 싶은 경우 새 버전 업로드를 눌러 새로운 버전의 파일을 선택한 후 업로드를 실행합니다. 이 때 파일의 이름은 기존 파일과 같지 않아도 상관이 없습니다.


새로운 버전의 파일을 업로드 한 후의 버전 관리 대화상자의 모습입니다.



첫 번째 업로드와 새로 올린 버전이 리스트에 표시됩니다.

파일 버전 관리 시에 각 버전별로 Google Drive 의 저장 용량을 개별적으로 사용하게 됩니다.

위의 경우 8바이트의 파일 2개를 올렸으므로 해당 파일이 사용하는 전체 용량은 16바이트가 됩니다.


대화상자에 보면 [자동 삭제 안함] 부분에 체크가 되어있는데, 구글 드라이브에서 관리하는 파일의 버전은

100개 이상의 버전이 쌓이거나, 해당 파일의 버전이 30일 이상이 지나면 자동으로 삭제되도록 하는 기능이 있습니다.

기본 값으로 자동 삭제 안함이지만 이전 버전에 대해 특별히 보존할 필요가 없는 경우 자동 삭제가 되도록 하거나

X 버튼을 눌러 버전 정보를 삭제할 수 있습니다.




참고로 파일 버전 관리를 하기 위해서는 꼭 파일에서 마우스 오른 쪽 버튼을 눌러 버전 관리를 통해서만 할 수 있습니다.

같은 이름을 가진 파일을 업로드 하는 경우, 파일이 덮어 씌워지진 않지만 같은 파일 이름으로 리스트에 표시됩니다.



최종 수정 날짜를 보고 파일의 버전을 판단할 수도 있지만 아무래도 같은 파일이 여러개 표시되는 것 보단

잘 되어 있는 버전 관리를 이용하는 게 더 편리하겠죠?


구글 드라이브를 이용해서 파일의 버전을 관리하는 방법을 알아보았습니다.

한가지 아쉬운 점이라면 해당 버전에 대한 코멘트나 메모를 쓸 수 있었더라면 좋았을텐데 해당 기능이 지원이 되지 않는 다는 것 정도겠네요.


+ Recent posts