스프링으로 모바일 애플리케이션 개발하기

http://blog.springsource.com/2010/11/19/spring-into-mobile-application-development/

스프링 모바일(Spring Mobile) 프로젝트는 스프링 MVC 확장을 제공해 모바일 웹 개발을 도와주고 스프링 안드로이드(Spring Adroid) 프로젝느는 스프링 기반 백 엔드를 사용하는 네이티브 안드로이드 클라이언트 개발을 지원한다.

스프링 모바일

스마트폰에 웹 브라우저는 있지만 화면이 너무작다. 이 문제에 대한 해결 책 두가지.

1. 장치를 인식한 다음 웹 요청을 모바일 서비스용 사이트로 보내는 방법(서버단에서 감지)
2. 단일 사이트를 사용하되 CSS 3 미디어 쿼리와 자바스크립트를 사용해서 점진적으로 개선하는 방법(클라이언트단에서 감지)

Greenhouse 예제는 서버단 감지를 사용했다. 모바일 장치에서 온 요청은 전혀 다른 페이지 레이아웃으로 보여주는 걸 목표로 했다. 이 기능을 스프링 모바일 1.0.0.M1에서는 이 기능을 “Device Resolver Abstraction”이라는 이름하에 제공하며 다음과 같은 주요 기능을 제공한다.

  1. DeviceResolver를 사용해서 장치를 감지하고 현재 HttpServletRequest를 우회시킬 HandlerInterceptor 제공.
  2. 감지한 장치를 @Controller의 메서드와 뷰 템플릿에 주입하여 장치 종류에 따라 로직을 달라지도록 할 수 있다.

다음은 Greenhouse 예제에서 사용한 예제 코드다. 먼저 /WEB-INF/spring/appServlet/serlvet-context.xml의 인터셉터 정의를 살펴보자.

[xml]
<interceptors>
<!– On pre-handle, detect the device that originated the web request –>
<beans:bean class="org.springframework.mobile.device.mvc.DeviceResolvingHandlerInterceptor" />
</interceptors>
[/xml]

이번에는 현재 장치가 모바일 장치가 아닐경우다른 것을 출력하는 JSP 템플릿이다.

[xml]
Please try again<c:if test="${!currentDevice.mobile}"> or <a href="<c:url value="/signup" />">sign up</a></c:if>.
[/xml]

마지막으로 현재 장치가 모바일이면 바뀌는 타일즈 기반 페이지 레이아웃이다.

[xml]
<definition name="page" templateExpression="/WEB-INF/layouts/${currentDevice.mobile ? ‘mobile/’ : ‘standard/’}page.jsp" />
[/xml]

이런 기능과 더불어 스프링 모바일 1.0.0.M1에는 다음과 같은 기능이 들어있다.

  1. 모바일 장치를 사용한 방문객을 다른 URL로 보내는 HandlerInterceptor. 이것을 사용하면 완전히 별개의 애플리케이션으로 모바일 사이트를 제공할 수 있다. 예를 들어 플리커 같은경우 모바일로 www.flickr.com에 접속하면 m.flickr.com으로 리다이렉트 된다.
  2. 요청이 온 장치에 대한 정보를 감지할 수 있도록 WURFL을 사용한 DeviceResolver 구현체를 제공한다.

최근에는 클라이언트단 감지를 제공하기 위해 CSS 3 미디어 쿼리자바스크립트를 사용하는 방법도 연구중이다. 이 방법은 서버단 작업이 필요치 않다는 장점이 있지만 모든 브라우저에서 지원될 수 있는 방법은 아니다. 하지만 웹킷 기반 브라우저를 사용하는 스마트폰을 타겟으로 한다면 대안이 될 수 있다.

스프링 안드로이드

안드로이드 클라이언트로 오면 문제가 달라진다. HTTPS를 사용한 REST로 데이터를 가져오며 장치에다 username과 password를 저장해야 하는 Basic Auth 보다는 보통 OAuth를 사용한다.

OAuth는 토큰 기반 인증 방식을 제공한다. username과 password로 인증이 되면 access 토큰을 받게되고 이 access 토큰을 사용해 보안된 리소스를 요청하게 된다. 즉 모바일 장치에 access 토큰만 저장해 두면 “remember me” 같은 기능을 사용할 수 있는것이다. 또한 이 방법을 사용하면 클라이언트에서 username과 password를 알고 있을 필요가 없다. 마지막으로 사용자 모바일 장치를 도둑맞더라도 username과 password 노출 걱정할 필요없이 access 토큰만 유효하지 않도록 처리될 것이다.

REST API는 RestTemplate을 사용하고 OAuth 클라이언트는 스프링 시큐리티를 사용한다.

다음과 같이 RestTemplate을 안드로이드 애플리케이션에서 REST 클라이언트로 사용할 수 있다.

[java]
RestTemplate restTemplate = new RestTemplate(new CommonsClientHttpRequestFactory());
Event event = restTemplate.getForObject("https://myapp.com/event/{name}", Event.class, "springone2gx");
[/java]

이후에는 아마 스프링 시큐리티 OAuth 클라이언트 같은 안드로이드 환경에 필요한 스프링 프레임워크 기능을 제공할 것이다.

시작하기

Greenhouse 예제를 직접로컬개발 환경에서 동작시켜 보는 것이다. Greenhouse는 스프링 안드로이드, 스프링 모바일, 스프링 MVC, 스프링 시큐리티, 스프링 소셜, 스프링 인테그레이션을 사용하고 있다. 프로젝트 홈페이지에서 가이드를 따라 쉽게 웹 애플리케이션, 아이폰 클라이언, 안드로이드 클라이언트를 로컬 개발 환경에서 실행하는 가이드를 찾아 볼 수 있다.


SNS를 그만 둔다.

SNS는 솔직한 글을 적기가 어렵다.
SNS를 자기 PR용으로 사용하는 사람이 많다.
SNS나 허세월드나 그게 그거다.
SNS가 유행이라 따라야 하는가?
SNS는 인맥늘리기 수단이다.

위와 같은 이유들로 ‘간단한 자기 생각을 공유하며 다른 사람들과 친해질 수 있다는 SNS’에 대한 회의감이 들었다.

  1. 우선 생각이라는게 간단할 수 없으며..
  2. 웹에 공개되는 이상 솔직할 수 없다.
  3. ‘자기 생각’ 아니라 남의 생각을 광고해주는 글들이 태반이다. 한마디로 다 스팸 SNS다.
  4. ‘공유’하는 활동 자체가 ‘학식이 뛰어난거나 박식한 사람’의 인성은 모른채 그 사람의 피상적인 ‘글’ 자체에 감동해 ‘무조건적 추종’에 불과하지 않는 경우가 많다. 즉 매스미디어의 언론조작과 비슷한 효과를 다져다 준다.
  5. ‘다른 사람’을 자신과 비슷하게 여기는 일종의 정신병을 만끽할 수도 있다.
  6. ‘친해진다’는 의미가 댓글을 자주 주고 받는 다는 의미로 변질 된다. 인생이 참 싸구려 같다. 댓글 안 달면 안 친한건가.
  7. 가식과 광고로 쪄든 SNS를 보느라 낭비하는 시간이 아깝다.

SNS는 꼭 불량식품같다. 맛있지만 유익하진 않다. 그래서 난 그만둔다. 사실 블로그 보다 짧은 글을 정리하기 편하다는 이유로 SNS에 슬쩍 발을 담가봤지만 블로그에 비해서 너무 위와같은 쓰레기 SNS가 판치는 세상인것 같다. 나도 사실 별 쓰잘때기 없는 글들을 올린게 더 많다.

SNS 쓸 시간에 차라리 짤막하게 노트에 일기를 적는 습관을 들이는게 좋겠다. 이제부터는 그냥 일기를 쓰련다.

1. 솔직할 수 있다.
2. 내가 원하는 만큼 적을 수 있다.
3. 키보드 보다는 종이게 쓰는게 더 효과가 좋다.
4. SNS 하느라 인터넷 할 시간에 차라리 진짜 사람다운 인연을 만드는데 더 힘쓰자.
5. 사람은 실제로 만나봐야 한다.

훔,, SNS 사이트들 탈퇴나 쭉. 하고 자야겠다.


[제이쿼리] 셀렉터를 잘 활용합시다

http://api.jquery.com/category/selectors/

낮에 봄싹 그룹스에 자바스크립트 리팩토링을 부탁한다며 올렸던 코드입니다.

[javascript]
function showAttended() {
$(".memberItem").each(function(){
var arate = $(this).attr("arate");
var trate = $(this).attr("trate");
if(arate == 0 &amp;&amp; trate == 0) {
$(this).hide();
} else {
$(this).show();
}
});
$("#attention-title").text("참석자");
}
function showNotAttended() {
$(".memberItem").each(function(){
var arate = $(this).attr("arate");
var trate = $(this).attr("trate");
if(arate == 0 &amp;&amp; trate == 0) {
$(this).show();
} else {
$(this).hide();
}
});
$("#attention-title").text("불참자");
}
function showAll(){
$(".memberItem").each(function(){
$(this).show();
});
$("#attention-title").text("스터디 참석 신청자");
}
[/javascript]

원랜 올리고나서 번역에 집중을 할랬는데.. 갑자기 답멜로 코드가 쏟아지기 시작하더니.. 완전 멋진 코드까지 나오더군요.

[javascript]
function showAttended() {
$(".memberItem[arate=0][trate=0]").show();
$(".memberItem:not([arate=0][trate=0])").hide();
$("#attention-title").text("참석자");
}
function showNotAttended() {
$(".memberItem[arate=0][trate=0]").hide();
$(".memberItem:not([arate=0][trate=0])").show();
$("#attention-title").text("참석자");
}
function showAll() {
$(".memberItem").show();
$("#attention-title").text("스터디참석신청자");
}
[/javascript]

이용혁님께서 보내주신 코드인데 셀력터를 사용해서 제가 사용했던 each로 루프를 돌면서 li를 조적하던 코드 보다 훨씬 간결하고 코드도 직관적이라 이것으로 당첨!!

그런데 Outsider님께서 셀렉터 성능을 개선할 방법으로 find와 filter를 제시해 주셨죠. 그래서 결국 코드는 요렇게 바꼈습니다.

[javascript]
function showAttended() {
$(".memberItem").hide().filter(":not([arate=0][trate=0])").show();
$("#attention-title").text("참석자");
}
function showNotAttended() {
$(".memberItem").hide().filter("[arate=0][trate=0]").show();
$("#attention-title").text("불참");
}
function showAll() {
$(".memberItem").show();
$("#attention-title").text("스터디참석신청자");
}
[/javascript]

셀렉터를 잘 쓰면 each 같은걸 안써도 되니 더 직관적이고 깔끔한 코드를 만들 수 있더군요.

귿귿! 재미있는건 이 모든 피드백이 글을 올린지 불과 2시간 이내에 벌어졌다는거죠.

봄싹은 역시 멋져!!


[javascript]
$(".memberItem").each(function(){
var arate = $(this).attr("arate");
if(arate > 0){
$(this).css("border", "1px solid #4183C4");
}

var trate = $(this).attr("trate");
if(trate > 0){
$(this).css("background", "#E0F8F7 none repeat scroll 0 0");
}
});
[/javascript]

이 코드도 역시 셀력터를 사용하면 딱 두줄로 간추릴 수 있습니다. 고건.. 걍 퀴즈!

아내를 위한 선물 – 봄말이

요즘 임신중인 아내가 마치 슈퍼우먼처럼 경제활동, 집안일, 공부를 병행하고 있습니다. 저는 공식 실직자라 집에서 딩가딩가.. (물론 할일은 많지만;;)

‘톨스토이, 길’이라는 책에서 죽음을 대하는 자세와 인생에 대해 많은 걸 배울 수 있었는데 혹시라도 무슨 사고로 인해 내가 언제 죽을지도 모르는데 임신중인 아내에게 음식도 한번 제대로 해준적 없으면 나중에 너무 후회될까봐… 그래서 부랴부랴 간단한 요리를 검색했습니다.

등장한 것은 바로 스프링 롤.. 이름도 좋치. 봄말이! 도저어~~언!!

성공!!

뿌듯하군요. 음하하하핫!!


[봄싹] 비동기 처리 후유증 해결

어젯밤에 벌인일을 이제사 마무리했다. 덕분에 시원하게 자바 코딩좀 했더니 재미난다.

예전에 이미 스프링 3에 추가된 task 스키마를 사용해서 쓰레드풀을 사용한 executor와 scheduler를 사용할 수 있도록 빈 설정을 해뒀다.

그때 설정한 executor를 사용해서 노티 서비스와 캘린더 서비스를 비동기 호출로 처리할 계획이었고 어제밤에 비동기 호출 예외 템플릿을 만들어 비동기 호출을 하는 것 까지 했다.

그런데 비동기 처리에 익숙하지 않다보니 예상치 못한 문제가 생겼다.

  • 스터디 서비스 처리가 끝나면서 하이버네이트 세션이 닫히기 때문에 study.getMeetings() 식으로 OSIV를 믿고 작업했던 부분이 전부 에러가 발생한다.
  • 스프링 시큐리티의 기본 시큐리티 컨텍스트 홀더 전략이 쓰레드로컬 방식이기 때문에 SecurityContextHolder.getContext()를 가져와봤자 비어있다. getAuthentication()이 null이다. 비동기로 실행중인  쓰레드에는 아무 정보가 없기 때문이다.
  • 스프링 트랜잭션 역시 마찬가지로 쓰레드로컬을 사용하기 때문에 비동기로 호출한 녀석들 중에 SessionFactory.getCurrentSession()을 호출한 녀석들은 전부 에러가 발생한다.

하나씩 해결해 나가자. 먼저 OSIV를 믿고 코딩했던 부분은 DAO 쪽을 조정해서 쉽게 해결할 수 있다. 두가지 방법이 떠오르는데 하나는 Study를 가져올때 미리 join해서 manager, memers, meetings까지 가져오는 것이고 다른 방법은 manager, memebers, meetings가 필요할 때 그때그때 DAO에 물어봐서 가져오는거다. 난 일단 후자로 해놨는데 StudyRepository.getFatchedStudy() 같은 녀석을 추가해 첫번째 방법도 만들어둬야겠다.

두번째 문제를 해결하는 방법은 간단했지만 꼼수에 가까워서 권장하고 싶지는 않다. 비동기 호출 내부에서 필요하는 시큐리티 정볼르 미리 스터디 서비스에서 읽어두고 익명 내부 클래스 형태인 비동기 호출중인 콜백에서 참조해 쓰는것이다. 즉 비동기 호출내에서는 시큐리티 정보 참조를 포기한거나 마차가지가. 미리 스터디 서비스 쪽에서 시큐리리 객체를 읽어놨으니비동기 호출쪽에서는 그때가서 쓰레드 뒤져서 찾아올 필요없이 그냥 참조만 하면 된다. 하지만 스프링 시큐리티를 찾아보니 이미 쓰레드를 많이 쓰는 스윙을 대비해 글로벌 컨텍스트 홀더 전략을 만들어 뒀다. 시큐리티 컨텍스트 홀더가 그 전략을 사용하도록 빈설정으로 더 깔끔하고 안전하게 처리할 수 있을 것 같다.

세번째 이슈는 약간 귀찮다. 스프링의 선언적 트랜잭션은 스프링 AOP를 사용해야 된다. 스프링 AOP를 사용하려면 빈으로 등록헤야 한다. 그러나 내가 만든 예외 템플릿의 콜백은 빈으로 등록될 녀석이 아니다. 그 콜백에 트랜잭션이 걸려야 하는데 아무리 @Transactional을 붙여봤자 소용없는 짓이다. 그래서 난 프로그래밍 방식 트랜잭션처리를 선택했다. 스프링 트랜잭션이 제공하는 PSA API인 PlatformTransactionManager, TrnsactionDefinitino, TransactionStatus세개만 알면 코딩으로 트랜잭션 처리하는것도 매우 간편하다. 그래서 어제 만든 ExceptionTemplate은 SimpleExceptionTemplate으로 변경하고 ExceptionTemplate 인터페이스  하나 만들었다. 비동기 호출을 지원한 예외 템플릿은 AsyncExceptionTemplate으로 만들었고 오늘 아침 트랜잭션까지 지원한느 예외 템플릿은 TransactionalAsyncExceptionTemplate으로 만들었다.

결국.. 다 해결했다. 이제 시큐리티쪽을 글로벌 컨텍스트 홀더로 변경하고 마무리 하자.