다음은 Java 기반 템플릿엔진인 Thymeleaf의 튜토리얼 및 자습서의 번역본이다.

모쪼록 이 강력한 녀석을 바탕으로, SI 뿐 아니라 SM 시장도 광명을 찾게 되기를 바랄 뿐이다.

혹자는 이 템플릿엔진의 무용론을 주장하지만, 

템플릿엔진 하나 갖고 개발 상의 모든 문제를 해결할 수 있을 것처럼 얘기한 적도 없거니와

세상에 그런게 있기는 하냐고 되묻고 싶다.

그리고 이 템플릿엔진의 효용에 대해 - 아는 만큼 보인다는 말을 해 주고 싶다.



여튼 번역본 다운로드는 아래 링크를 누르면 번역본을 다운로드 할 수 있다.

(게으른 탓에 향후 증보는 없을 것이다)



thymeleaf_Tutorial_번역본_v1.1_20131213.pdf


thymeleaf_InteractiveTutorial_자습서_v1.0_20131213.pdf




이제 막 활성화 되어 계속 버전업 되는 템플릿엔진이다 보니 매뉴얼이 계속 변경되고 있다.

자세한 원문 정보는 Thymeleaf의 홈페이지에서 확인하기 바란다.

http://www.thymeleaf.org/documentation.html



당연히 카피레프트이고, 맘대로 퍼가고 읽고 써도 좋다.

물론 출처를 밝히지 않아도 상관 없지만, 밝혀 주면 기분은 좋을 것 같다.



-----------------------------------------------


지난 해 이런 저런 이유로 Thymeleaf 에 대해 공부 하게 됐다.

무척 막강한 템플릿 엔진임에도 불구하고, 국내에는 소개된 자료가 거의 없다.

튜토리얼을 더듬더듬 읽어 내리다..

화딱지가 나 번역을 해 버렸다.


번역하는 동안 쓰고 읽다 보면 좀 더 공부가 잘 되겠지, 하는 생각으로.

그런데 기본적으로 워낙 사용법이 간단하다 보니, 이 녀석을 쓰는 법을 익히는 데 꼭 번역이 필요했던 것은 아닌 것 같다.


뭐 어떻든.. 

기껏 번역을 했으니 좀 더 많은 사람들이 봤으면 하는 바람으로 번역본을 공개~!

전문 번역가도 아니고, 영어에 탁월한 재능이 있기는 커녕~ 이므로..

오역과 의역이 넘나 들 것이다.


그래도 자습서는 꽤 볼 만 할 것이라 자평한다. (응?)

저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. kkanding

    와 이걸 번역해주시다니 정말감사합니다! 잘보겠습니다!

    2014.04.02 10:36 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 멋진인생

    번역 감사합니다 ^^

    2014.04.10 10:24 신고 [ ADDR : EDIT/ DEL : REPLY ]
  3. 감사드립니다^^; 큰절 드리고 싶네요 ㅠ

    2015.03.14 17:27 신고 [ ADDR : EDIT/ DEL : REPLY ]
  4. 아직 열어보진 않았는데 먼저 감사인사드리고 싶습니다.^^ 저 또한 Thymeleaf의 효용에 의문점을 가지고 있긴 한데, 아직 배움과 경험이 많이 부족한 듯 합니다. 여튼 잘보겠습니다!

    2015.04.01 11:21 신고 [ ADDR : EDIT/ DEL : REPLY ]
  5. 유상훈

    번역 해주셔서 감사합니다~ㅎ

    2015.04.02 09:40 신고 [ ADDR : EDIT/ DEL : REPLY ]
  6. mazdah

    감사합니다. 유용하게 사용하도록 하겠습니다.

    2015.04.13 16:22 신고 [ ADDR : EDIT/ DEL : REPLY ]
  7. 유상훈

    안녕하세요 혹시 타임 리프 사용하시나요??

    제 메일로 답변 주세요 ㅎ

    hue@hellomarket.com 이에요~

    2015.04.17 11:30 신고 [ ADDR : EDIT/ DEL : REPLY ]
  8. jooni06

    진짜 감사합니다^^

    2015.11.06 16:30 신고 [ ADDR : EDIT/ DEL : REPLY ]
  9. 이진욱

    너무 감사합니다. 잘 보겠습니다~ ^^

    2017.04.01 19:08 신고 [ ADDR : EDIT/ DEL : REPLY ]
  10. 로미

    번역 감사합니다 ㅠㅠ~!!! 너무나 필요했던 자료예요~!!!ㅠㅠㅠ

    2017.07.14 11:04 신고 [ ADDR : EDIT/ DEL : REPLY ]



1. EC2 신청

2. OS/APM 설치

   - 관리자 비밀번호 변경

   - mysql 비번 변경

3. Elastic IP 신청

4. Elastic IP 인스턴스에 연동

5. 보안 그룹 설정

6. Wordpress 설치, k-news 플러그인 설치

7. SES 신청

8. SES에 verifying 용 메일 등록

9. SES에 메일 발송 도메인 등록

   도메인에 TXT, CNAME, DKIM 추가

   (SMTP로 구글메일 사용할 경우에도 마찬가지)

10. SES SMTP 설정

   Create my SMTP Credentials 후 SMTP User / Password

11. 메일 송신 부에 SMTP 설정

12. Wordpress 메일 플러그인 이용 시 크론탭 설정

    - crontab -e

13. 메일링 리스트 뉴스레터 발송 테스트




이거 다 하는데 반나절이면 뚝딱.


정말 세상 좋아졌다...



저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

Java Source deCompiler인 Jad 를 STS(Eclipse)에서 플러그인 설치

 

1. Jad 다운로드 후 시스템 path 적용.

   귀찮으니 그냥 Java\bin 에..

 

2. STS - Help - Install new software - source 추가 후 Jadclipse 설치: 그냥 jadclipse의 jar를 설치하면 STS에서 오류가 나는 경우가 있다.

   Jadclipse - http://jadclipse.sourceforge.net/update

 

3. STS - Window - Preference - Java - Decompiler - Jad 에서 설치 경로 및 temp 확인: 위 2번 방법으로 설치하면 따로 확인 안 해도 된다.

 

4. 소스에서 한글이 깨져 보이는 경우를 방지하기 위해 STS - Window - Preference - Java - Decompiler - Jad - Misc 에서 Convert Unicode strings into ANSI strings 를 체크해 준다.

 

 

저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요


책 696 페이지 이하를 보면, 스프링 개발 환경 셋팅에 대한 이야기가 있다.
본문 정리에서는 대충 스킵했지만 개발환경 셋팅에 대해서도 언급해야 할 것 같다.
환경 셋팅이라고 해 봐야 별 거 없지만.. 이 정도는 해 줘야지.



스프링소스 다운로드 - eclipse 포함

책에는 이클립스를 설치하고 업데이트를 통해 플러그인을 설치하라고 하지만 사이트에 가 보면 통합버전을 다운로드 할 수 있다.
물론 플러그인만 따로 설치할 수도 있다.
http://www.springsource.com/products/springsource-tool-suite-download
 



Spring Framework 업데이트

최신 버전을 확인해 봤더니 3.2 까지 나왔다. 업데이트 방법은 다음과 같다.

위에서 다운로드 한 STS를 실행시켜 Help - Install New Software를 선택한 후 Work with: 에
SpringSource Update Site for Eclipse 3.7 - http://dist.springsource.com/release/TOOLS/update/e3.7 를 선택한다.

여기서 Core / Spring IDE, Extensions / Spring IDE 를 선택한 후 업데이트 하면 된다.



내용이 부실하지만, 딱히 더 적을만한 것도 없다.
앞으로 필요할 때 마다 update 하기로 하자. 


저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. jaeger

    스프링 개발자로 거듭나시는군요! ㅋㅋㅋ

    2012.02.14 17:45 신고 [ ADDR : EDIT/ DEL : REPLY ]

드디어 13장의 마지막 편이다.
그런데 낭패다. 이 장의 남은 부분은 정리라는 것이 별로 필요하지 않은 내용이기 때문이다.
그래도 어떡하나. 해야지 뭐.


13장의 나머지 부분은 JSP 뷰와 태그 라이브러리, 메시지 컨버터와 Ajax, OXM 등의 연계 방법 등에 대해 아주 살짝.. 아주 아주 살짝 건드리고 있다.
수박 겉을 핥아도 이보다는 맛있을 것(!) 같다.

뭐 아무튼 시작해보자.

1. JSP EL

일반적인 JSP 페이지에서 name 이라는 변수를 출력하기 위해서는 다음과 같이 코딩해야 한다.

<%
User user = new User();
user = user.getUser(userid);
out.println(user.name)
%>


하지만 JSP 뷰 페이지에서는(스프링에서 ModelAndView로 리턴된 뷰페이지에서는) 이렇게만 하면 된다.

${user.name}


물론, 이렇게 사용하기 위해서는 당연히 뷰를 호출하는 메소드에서 user 오브젝트에 값을 넣어둬야 한다.
여기에 fmt나 <c:if>, <c:when>, <c:choose> 등을 함께 사용해 필요한 처리를 할 수도 있다.


2. 스프링 Sp EL 

스프링의 SpEL을 사용하기 위해서는 다음과 같은 Spring 태그 라이브러리를 추가해야 한다.

<% taglib prefix="spring" uri="http://www.springframework.org/tags" %>


위 JSP EL에서 했던 것과 같이, user 오브젝트의 name을 출력할 때 SpEL은 다음과 같다.

<spring:eval expression="user.name" />


SpEL은 JSP EL에 비해 자유도가 높아 메소드 호출이나 연산, 심지어 오브젝트 생성도 가능하다.
하지만 애초에 JSP 뷰 단에서 복잡한 로직을 처리하는 것은 MVC 패턴 전략에 맞지 않으므로, SpEL에서 아무리 대단한 기능을 지원한다고 해도 그걸 막 써대면 안 될 것 같다.

하지만 이거 하나는 매우 맘에 든다.
메소드에서 특정 변수에 포맷터를 지정한 경우 이 변수를 출력하면 포맷팅이 자동으로 따라 온다는 것.

예를 들어 JSP 뷰 단에서 <% %> 이나 JSP EL을 이용해 통화 표현이나 날짜 표현을 하려면 매우 번거롭다.
하지만 SpEL 에서는 메소드에서 해 놓은 포맷터를 그대로 상속하므로 그냥 쓰기만 하면 된다.
이건 매우 좋다.


3. 지역화 메시지 출력

messages.properties 파일을 이용한 메시지 출력이다.
messages_en.properties, messages_ko.properties, messages_jp.properties 등의 파일을 복수로 만들어 놓고 LocaleResolver의 결정에 따라 서로 다른 메시지를 뿌려 줄 수도 있다.

이렇게 messages.properties 를 이용하기 위해서는 어플리케이션 컨텍스트 설정에서 다음과 같은 빈 설정을 해 줘야 한다.

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">

<property name="basenames">

<list>

<value>com.kunner.common.messages.default_msg</value>
// messages.properties 라는 기본 이름을 쓰지 않고 별도로 지정해 줄 수도 있다. 
// 이 경우 메시지 파일은 WEB-INF/classes/com/kunner/common/messages/default_msg.properties 가 될 것이다.
 

</list>

</property>

</bean>



아래는 실제로 메시지를 적용해 본 것이다.

messages.properties

greeting=Hello, {0}! Have a nice {1}!


뷰 페이지

<spring:message code="greeting" arguments="${user.name},${dayOrNight}" text="Hello, there!" />
 


만약 위 소스의 결과에서 user.name이 "Kunner" 이고, dayOrNight가 "day" 라면 화면에 출력되는 내용은 다음과 같다.

Hello, Kunner! Have a nice day!


그리고 만약 greeting 이라는 인수로 메시지를 찾을 수 없을 경우 화면에 출력되는 내용은 text의 속성값 "Hello, there!" 가 될 것이다. 자바스크립트의 alert 메시지 뿐 아니라 HTML 바디에 들어갈 텍스트에도 모두 사용 가능하다. 



4. 폼 태그 라이브러리 <form: />

HTML의 폼태그를 써도 입출력하는데는 크게 문제가 없다.
하지만 최근의 웹 개발 트렌드를 생각해 볼 때 이렇게 하는 경우 번거로운 처리를 함께 해 줘야 한다.

책에서 회원가입의 예를 잘 들었으니 굳이 또 예를 들어가며 설명할 건 없겠고..

간단히 말해 폼을 서밋 했다가 오류나 기타 다른 이유로 다시 폼을 리턴 시킬 때 이미 입력한 정보가 그대로 들어 오도록 한다거나, 입력 오류가 난 항목에 별도의 오류 메시지를 출력한다든가 하는 걸 처리하기 위해서는 일반적인 HTML의 폼태그만으로는 매우 귀찮고 번거로운 작업을 해야만 한다.

하지만 스프링의 폼이나 폼 태그 라이브러리를 이용하면 쉽다.
여기서는 스프링 폼은 생략하고 폼태그라이브러리에 대한 용례만 몇가지 짚고 넘어 가기로 한다.

먼저 폼 태그 라이브러리 <form: /> 을 사용하기 위해서는 다음과 같이 태그 라이브러리를 추가해 줘야 한다.

<% taglib prefix="form" uri="http://springframework.org/tags/form" %>


<form:form> 을 이용해 폼을 만들면 폼을 전송할 때 사용할 객체를 지정해 줄 수 있다.
또 폼 전송 후 오류를 리턴 받았을 때 자동으로 오류 메시지를 출력하게 할 수 있는데, 이 때 사용할 css의 class 를 별도로 지정해 줄 수도 있다.
그 외 일반적인 사항은 거의 다를 게 없다.
그냥 아래 코드를 보고 이해하자.

user.java

public class User(){
//생성자 지시자 생략
...
String name;
@NumberFormat(pattern="###,##0") 

Integer point;
...
}

@RequestMapping("/user/add")
public String add(){
// 입력 처리
return "addResult";
}


View - add.jsp

<form:form id="userform"[각주:1] commandName="user"[각주:2] action="/user/add" method=post>
<form:label path="name" cssClass="inputText" cssErrorClass="inputTextError">이름: </form:label>
<form:input path="name" cssClass="inputForm" cssErrorClass="inputFormError" />
<form:errors path="name" cssClass="inputTextError" />
//path를 지정해 놓으면 해당 입력값에 입력 오류나 바인딩 오류가 있을 때 에러를 표시할 수 있다.
//일반적인 상황에서는 cssClass에 선언된 css가 적용되고, 오류상황에서는 cssErrorClass의 css가 적용된다.
//<form:errors>는 오류상황에서만 출력되므로 cssClass가 다른 개체의 cssErrorClass와 같은 기능을 한다.


<form:label path="point" cssClass="inputText" cssErrorClass="inputTextError">포인트: </form:label>
<form:input path="point" cssClass="inputForm" cssErrorClass="inputFormError" />
<form:errors path="point" cssClass="inputTextError" /> 
//모델에 포맷터를 적용한 경우 해당 포맷팅이 자동으로 처리된다!!!

<input type="submit" value=" ok "> // HTML 태그를 혼용해도 상관 없다.

</form:form>



그 외 체크박스나 라디오버튼과 같이 컬렉션을 연동해 사용하는 것도 있다.
어렵지 않으니 필요할 때마다 찾아서 쓰다보면 익숙해 질 것이다.


그 외 메시지 리솔버나 OXM, Ajax 의 사용법등을 간략하게 언급하고 있으나 정리로서의 의미가 크게 없으므로 생략한다.
자, 이렇게 13장을 끝내자.


뿌리 깊은 나무를 보고 있는데.. 정말 한글은 위대한 거 있지.
우린 한글 창제 때와 전혀 다른 세상에서 살고 있는데도 한글로 표현 안 되는게 없으니..
역사를 풍미한 불세출의 천재, 하면 카이사르나 나폴레옹을 꼽곤 했는데 우리 세종대왕도 충분히 어께를 견줄만 하지 않은가!
아니, 어쩌면 더 위대할지도 모르겠다.



  1. id는 HTML 폼에서의 name과 같다 [본문으로]
  2. 폼의 각 값을 모델 오브젝트와 연동시켜준다. modelAttribute로 바꿔 쓸 수 있다. [본문으로]
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. 한글날을 공휴일로 만드는 일이 시급합니다! ㅋㅋ

    2011.12.07 14:43 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 댓글은 언제 달고 간거냐 ㅋㅋ
      난 왜 댓글이 잘 안 보이지? 흣.

      분명 어렸을 때는 한글날이 휴일이었는데..
      노태우 이 !@#$%.. -ㅅ-;

      2011.12.15 11:59 신고 [ ADDR : EDIT/ DEL ]


앞서 13장은 3부분으로 나누어 정리하겠다고 말한바 있다. 사실 바로 정리를 했어야 했는데.. 이전 글을 쓴 지 거의 일주일이 다 되었다. 게으름은 인류의 적, 아니 나의 적이다.


이번 편에서 정리할 내용은 '모델 바인딩과 검증' 이다. 책에서는 13-3 로 분류되어 있다.

앞서 살펴봤던 @ModelAttribute 애노테이션을 사용하면 크게 세 가지의 작업이 자동으로 진행된다.

@ModelAttribute 선언 후 자동으로 진행되는 작업들

첫째, 파라미터로 넘겨 준 타입의 오브젝트를 자동으로 생성한다. 예를 들어 @ModelAttribute User user 라고 인수를 넘기면 User 타입의 user 오브젝트를 자동으로 생성한다.
둘째, 생성된 오브젝트에 HTTP로 넘어 온 값들을 자동으로 바인딩한다. HTTP 파라미터는 문자열이기 때문에 오브젝트에 맞는 형 변환이 필요하다. 이때 스프링의 기본 프로퍼티 에디터를 사용한다. 변환 중 오류가 생겨도 작업은 중단되지 않는다.
셋째, 오브젝트로 넘어 온 값을 검증한다. 사용자가 자체적으로 검증기를 등록해 Validation 체크를 진행할 수 있다.


위에서 첫번째로 진행되는 작업 - 즉, 오브젝트를 자동으로 생성하는 부분은 앞선 정리에서 살펴 보았다. (뭐 대단한 작업 없이 그냥 애노테이션 선언만 하면 알아서 만든다!)

이번에는 두번째와 세번째 내용을 학습할 차례다.


@ModelAttribute를 이용해 넘어 온 파라미터를 오브젝트에 자동으로 바인딩 할 때 기본 프로퍼티 에디터를 사용한다고 했는데, 꼭 여기 뿐 아니라 XML 설정 파일에서 빈에 대한 값 주입을 할 때도 마찬가지의 동작이 일어난다. 또 @RequestParam 이나 @PathVariable 파라미터의 바인딩에도 마찬가지의 동작이 일어난다. 스프링에서 바인딩이 일어날 때 값에 적절한 형 변환이 일어나도록 하는 방법에는 PropertyEditor를 이용하는 방식과 Converter를 이용하는 방식 두 가지가 있다.


1. PropertyEditor

스프링이 기본적으로 제공하는 바인딩용 타입(형) 변환 API 다.
애초에 GUI 개발도구에서 사용자가 입력한 값을 적절한 형태로 형 변환하기 위해 제공되는 도구로 시작했다는데, 마찬가지의 필요에 의해 만들어졌다는 걸 알 수 있다.

자바에서 기본으로 지원하는 타입들은 자동 형 변환이 일어난다.
Integer와 같은 기본적인 오브젝트는 물론이요, 책에서 예로 든 Charset 와 같은 타입도 자동으로 형 변환된다. IDE 도구에서 자동 볼드 처리되는 모든 타입이라 봐도 무방하겠지.

그런데 개발자가 필요에 따라 만든 타입이라면 자동으로 형 변환이 일어나지 않을 것이다.(당연하게도)
이런 경우 커스텀 프로퍼티 에디터가 필요하다.


먼저 간단한 형태의 커스텀 프로퍼티 에디터를 만들어 보자.
커스텀 프로퍼티 에디터를 만들 때는 PropertyEditorSupport 클래스를 상속해서 필요한 메소드(getAsText, setAsText)만 구성하면 된다.

Level.java - Level에 대한 도메인 오브젝트

public enum Level {

GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);  

private final int value;

private final Level next; 

Level(int value, Level next) {  

this.value = value;

this.next = next; 

}

public int intValue() {

return value;

}

public Level nextLevel() { 

return this.next;

}

public static Level valueOf(int value) {

switch(value) {

case 1: return BASIC;

case 2: return SILVER;

case 3: return GOLD;

default: throw new AssertionError("Unknown value: " + value);

}

}

}



levelPropertyEditor.java - 커스텀 프로퍼티 에디터

static class LevelPropertyEditor extends PropertyEditorSupport {

public void setAsText(String text) throws IllegalArgumentException {

this.setValue(Level.valueOf(Integer.parseInt(text.trim())));

}


public String getAsText() {

return String.valueOf(((Level)getValue()).intValue()); 

}

}



코드를 보면, level 타입은 1,2,3 의 숫자를 Basic, Silver, Gold 로 각각 해석하도록 되어 있다. 반대로 Basic, Silver, Gold 의 level 타입은 숫자 1,2,3 과 대응한다.
이미 Level 도메인 오브젝트에서 Level 타입에 대한 해석과 적용에 대한 코드가 만들어져 있는 것이다.
이것을 levelPropertyEditor.java 에서 가져다 쓰는데 - PropertyEditorSupport 클래스를 상속해 getAsText와 setAsText를 오버라이드해서 쓰면 된다.
여기까지 볼 때는 그냥 메소드 하나 만들어서 형 변환하는 거랑 차이가 없다.


2. InitBinder

이제 스프링에서 자동으로 형 변환이 일어날 때 우리의 levelPropertyEditor를 불러들이도록 해 보자.
스프링이 형 변환을 할 때 여러가지 작업이 일어나지만, 이를 다 열거할 필요는 없을 것 같고(궁금하면 책 봐! p.1186) - 우리가 알아 둘 것은 WebDataBinder 가 이런 기능을 하는데 개발자가 직접 WebDataBinder를 건드리지는 못 하지만, 이런 경우를 위해 스프링이 특별히 제공하는 WebDataBinder 초기화 메소드를 사용해야 한다는 것이다.

아.. 말이 중언부언하는데, 결국 WebDataBinder 초기화 메소드를 사용해서 우리의 커스텀 프로퍼티 에디터를 등록할 수 있다는 것이다.

사용 방법은 아래와 같다.

@InitBinder 메소드

@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.registerCustomEditor(Level.class, new LevelPropertyEditor());
//dataBinder에 커스텀에디터를 등록한다. Level 오브젝트를 만나면 LevelPropertyEditor와 연결된다.
}
 



위처럼 @InitBinder 애노테이션을 걸어 주면 level 타입을 만나면 무조건 LevelPropertyEditor와 연결된다. 이와 달리 특정 조건에서만 커스텀 형 변환을 하게 할 수도 있다.

특정 조건에서만 실행되는 @InitBinder

@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.registerCustomEditor(int.class"age"new MinMaxPropertyEditor(0,200));
//dataBinder에 커스텀에디터를 등록한다. 숫자면서 age 라는 이름을 가진 파라미터에 대해서만 형 변환을 시도한다.
}


보다시피 역시 간단하다. 오브젝트와 커스텀 에디터 사이에 파라미터 이름을 문자열로 던져 주면 된다.



3. WebBindingInitializer

자주 쓰이는 커스텀 형 변환의 경우 매번 쓸 때 마다 선언할게 아니라 한번 선언으로 계속 쓸 수 있게 해 줄 수도 있다. 이럴 때 쓰이는 것이 WebBindingInitializer 다.

사용 방법 역시 엄청 간단하다.
스프링의 미학은 단순함에 있는 것 같다.

먼저 커스텀 프로퍼티 에디터 클래스를 만든다.
다음으로 WebBindingInitializer 를 구현해서 커스텀 프로퍼티 에디터를 특정 오브젝트와 연결하는 클래스를 만든다.
이 클래스를 빈으로 등록한 다음 AnnotationMethodHandlerAdapter 핸들러의 webBindingInitializer 프로퍼티에 DI 해 주면 된다.
어차피 이 WebBindingInitializer 는 다른 곳에서 참조해 쓸 일이 없으므로 그냥 바로 프로퍼티로 주입하는 편이 좋다.

WebBindingInitializer를 구현해 커스텀프로퍼티 에디터와 연결하는 클래스

public class MyWebBindingInitializer implements WebBindingInitializer{
public void initBinder(WebDataBinder binder, WebRequest request){
binder.registerCustomEditor(Level.class, new LevelPropertyEditor());
}
}


설정 파일에서 빈에 값 주입

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="webBindingInitializer">
<bean class="MyWebBindingInitializer" />
</property>
</bean> 


하지만 이 경우 어쨌든 리소스의 낭비가 예상되므로, 빈번히 쓰이는 게 아니면 꼭 이렇게 할 필요는 없을 것 같다.


4. 프로토타입 프로퍼티 에디터

어떤 파라미터의 값과 타입(형)이 규정된 하나의 형식이 아니라 DB에서 관리되는 동적인 형태의 경우 위에서 살펴본 바와 같이 처리하기가 어렵다.

이해를 위해 책에서 들은 구체적인 예를 보자.
회원관리 기능에서, 회원의 권한 Code userType을 코드 테이블에서 가져 온다고 하자. 코드 테이블은 id(Integer) 와 name(String)으로 구성되어 있고 동적으로 관리된다.
코드의 아이디는 1,2,3 이고 각각 관리자,회원,손님으로 매핑된다.
이때 Code 와 코드테이블의 id 가 어떤 상관관계가 있는지 모르기 때문에 파라미터로 userType이 1 이라고 넘어 왔을 때 자동으로 형 변환하지 못한다.

책에서는 이런 경우에 대해 세가지 방법을 제시하고 있다.

첫째는 코드테이블의 id 값과 1:1 로 매핑되는 int userTypeId 를 선언해 request 된 파라미터를 받아 들인 후 userTypeId 를 코드테이블과 대조해 name을 Code와 연결하는 것.
기존의 노가다 방식과 전혀 다르지 않다. 그러므로 별로 눈여겨 볼 필요가 없다.

둘째는 모조 프로퍼티 에디터를 사용하는 방법.
스프링에서는 id 값만 덜렁 하나 가진 프로퍼티 에디터를 모조(Fake) 프로퍼티 에디터라고 한다.
파라미터로 userType이 1 이라고 넘어 온 경우 이를 Code의 id 값에 그냥 1 이라고 넣어 주는 역할만 한다.
특정 화면에서 잠깐잠깐 갖다 쓸 때는 유용하겠지만, 이 역시 기존 방식과 크게 달라 보이지는 않는다.

셋째는 바로 프로토타입 프로퍼티 에디터를 이용하는 것이다.
스프링에서 자동으로 형 변환이 일어나는 과정을 자세히 보면, getAsText 와 setAsText가 번갈아 일어나는데, 이때 아주 잠깐이기는 하지만 값이 저장되는 - 상태가 유지되는 순간이 있다. 만약 프로퍼티 에디터를 빈으로 등록해 여럿이 공유하게 되면, 이 상태유지 때문에 큰 문제가 일어날 수 있다.
개발자 혼자 테스트 할 때는 전혀 문제가 없는데, 서비스에 올려 놓고 돌리면 알 수 없는 문제가 생길 때가 있는데 그 중 하나가 바로 이런 경우이다.
따라서 프로퍼티 에디터를 빈으로 등록해 재사용하는 것은 기본적으로 불가능하다. 

하지만 이런 경우 빈의 생명주기를 제어하면 된다고 우리는 배운 바 있다.

먼저, 코드 테이블의 모든 레코드를 커스텀 프로퍼티 에디터에 등록해 놓고 계속해서 재사용한다고 생각해 보자.
이러려면 이 커스텀 프로퍼티 에디터는 DB로부터 코드 테이블을 뒤져서 Code 오브젝트를 가져올 수 있어야 한다.
따라서 codeDao나 codeService 같은 빈을 DI 받아야 하는데, DI 받기 위해서는 자신도 빈으로 등록되어야 한다.
물론, 앞서 얘기한대로 반드시 프로토타입의 생명주기를 가지고 있어야 한다.

빈으로 사용될 프로퍼티 에디터

@Component
@Scope("prototype") // 빈의 생명주기를 프로토타입으로 설정한다.
 static class CodePropertyEditor extends PropertyEditorSupport {

@Autowired CodeService codeService; //빈에서 주입해도 되지만, 이렇게 오토와이어 해도 된다.


public void setAsText(String text) throws IllegalArgumentException {

setValue(codeService.getCode(Integer.parseInt(text)));

}

public String getAsText() {

return String.valueOf(((Code)getValue()).getId()); 

}

}


컨트롤러에서 사용하기

@Controller
static class UserController2 {

@Inject Provider<CodePropertyEditor> codeEditorProvider;

@InitBinder public void initBinder(WebDataBinder dataBinder) {

dataBinder.registerCustomEditor(Code.class, codeEditorProvider.get());
// codeEditorProvider.get() - Provider<> 를 통해 프로토타입 빈을 새로 가져온다. 

}

@RequestMapping("/add") public void add(@ModelAttribute User user) {

System.out.println(user);

}

}




5. Converter

PropertyEditor는 파라미터를 자동으로 바인딩하는 매우 유용하고 강력한 도구다. 하지만 매번 필요할 때 마다 새로 생성해줘야 하는 단점으로 인해 관리가 어렵고 작긴 하지만 리소스의 낭비가 예상된다.
이런 단점을 극복하기 위해 스프링 3.0 에서는 Converter 라는 기술을 도입했다.

Converter의 인터페이스는 다음과 같이 구성되어 있다.

public interface Converter<S,T>{
T convert(S source);


Converter 인터페이스가 <S,T> 라는 제네릭스 타입으로 생성되어 있기 때문에 모든 타입에서 그냥 가져다 쓰기만 하면 된다.
그리고 Converter는 단방향변환만 지원하므로, S -> T 와 T -> S 두가지를 함께 등록해야 프로퍼티 에디터와 같은 기능을 수행할 수 있다.
예제 코드를 보자.

LevelToStringConverter.java

public class LevelToStringConverter implements Converter<Level, String>{
public String convert(Level level){
return String.valueOf(level.intValue());
//level.intValue 와 같은 메소드는 앞서 생성한 Level.class에 있다.
}
}
 

StringToLevelConverter.java

public class StringToLevelConverter implements Converter<String, Level>{
public Level convert(String text){
return Level.valueOf(Integer.parseInt(text));
}



이번에는 이렇게 만든 컨버터를 바인딩 할 때 자동으로 쓸 수 있도록 해 보자.
바인딩 할 때 컨버터를 사용하도록 하는 방법에는 @InitBinder를 이용하는 방법과 ConfigurableWebBindingInitializer를 이용하는 방법 두가지가 있다.


@InitBinder 를 이용하면 필요할 때 마다 매번 생성해서 쓸 수 있다.

ConverstionServiceFactoryBean에 빈 등록

<bean class="org.springframework.context.support.ConversionServiceFactoryBean" >
<property name="converters">
<set>
<bean class="LevelToStringConverter">
<bean class="StringToLevelConverter">
</set>
</property>
</bean> 


컨트롤러에서 컨버터 등록

@Controller public static class SearchController{
@Autowired ConversionService conversionService; //컨버전서비스를 주입받는다.

@InitBinder
public void initBinder(WebBinder dataBinder){
dataBinder.setConversionService(this.conversionService);
 
}



또 ConfigurableWebBindingInitializer를 이용해 일괄등록하는 방법도 있다.

설정파일에서 일괄 등록

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
<property name="webBindingInitializer" ref="webBindingInitializer" />
</bean>

<bean id="webBindingInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" >

<property name="conversionService" ref="conversionService" />
</bean>

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" >

<property name="converters">
<set>
// 일괄 적용할 컨버터 목록
<bean class="LevelToStringConverter">
<bean class="StringToLevelConverter">
</set>
</property>
</bean> 




6. Formatter

원래 포맷터는 바인딩 시 자동 형 변환을 하기 위해 만들어 진 기능은 아니지만 이를 이용하면 컨버터와는 다르게 특정 오브젝트 포맷에 최적화 된 형태의 형변환을 할 수 있다. 
이를테면 숫자를 통화 형태로 변경한다든가 할 때 쓰면 되는데, 사실 형 변환이라는 것이 로직 설계 상 반드시 필요한 것이 아니라면 굳이 써야 하나, 하는 생각이 들기도 한다. 
따라서 책에 나온 사용법만 간단히 소개하는 정도로 마치기로 한다.

통화표현의 예

class product{
...
@NumberFormat("$###,##0.00") // 아래 선언된 price를 이와 같은 통화 표현으로 포맷팅한다.
BigDecimal price;



날짜 표현의 예

system.out.println(org.joda.time.format.DatetimeFormat.patternForStyle("SS",Locale.KOREA));
// 앞의 인수는 날짜 표현 방식인데, S(Short), M(Medium), L(Long), F(Full) 네개의 문자를 날짜+시간 표현으로 정의한다. 

@DateTimeFormat(style="F-") //애노테이션으로 특정 오브젝트만 형 변환할 수도 있다. F- 라고 하면 시간은 생략된다.
Calendar birthday

@DateTimeFormat(pattern="yyyy/MM/dd") //아예 날짜 표현 방식을 직접 정의할 수도 있다.
Date orderDate;




7. WebDataBinder 설정

- AllowedFields, disAllowedFields : 파라미터의 자동 바인딩을 허용/비허용 설정할 수 있다.

@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.setAllowedFields("name","email","tel"); // 정의된 세 파라미터만 바인딩 허용


- requiredFields : 필수 값인 경우 값이 없으면 예외를 발생

@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.setRequiredFields("name"); // 정의된 파라미터는 필수값


user.setName(ServletRequestUtils.getRequiredStringParameter(request,"name"); // 값이 없으면 예외 발생


- fieldMarkerPrefix : HTML의 체크박스의 경우, 체크해제를 하면 값 자체가 없어지기 때문에 자동 바인딩 때 예외 오류가 생긴다. 이럴 때는 HTML의 폼에서 체크박스 아이템의 이름과 같은 아이템을 히든 필드로 만들되 앞에 _ 를 붙여 주면 자동으로 체크해제 값으로 인식한다.

<form>
<input type="checkbox" name="check" value="true" />
<input type="hidden" name="_check" value="false" />
</form> 


- fieldDefaultPrefix : fieldMarkerPrefix와 거의 비슷한 기능을 하지만, 단순히 true, false 가 아니라 디폴트 값을 ! 로 표시해 설정할 수 있다.

<form>
<input type="checkbox" name="usertype" value="admin" /> 관리자일 경우 체크
<input type="hidden" name="!usertype" value="member" />
</form> 




8. 값의 검증 - Validation Check

파라미터를 바인딩할 때 값에 대한 검증은 Validator - 검증기 를 통해 처리할 수 있다.
자세한 설명은 생략하고 그냥 예제 코드를 보자.

Validator 메소드 

public void validate(Object target, Errors errors){
User user = (User)target;
if (user.getName() == null || user.getName().length() == 0){
errors.rejectvalue("name", "field.required"); // name 파라미터가 해당 조건인 경우 "field.required" 예외 메시지를 리턴한다.
// 예외 메시지는 message.property 에 정의되어있다. 

}
}


컨트롤러 메소드 내에서 사용하기

@Controller
public class UserController{
@Autowired UserValidator validator; // Validator 메소드를 빈으로 주입

@RequestMapping("/add")
public void add(@ModelAttribute User user, BindingResult result){
this.validator.valodate(user, result); // validator를 선언한다.
if (result.hasErrors()){
// 오류가 발생한 경우
}else{
// 오류가 없는 경우
}
}

}


@Valid 를 이용한 자동 검증

@Controller
public class UserController{
@Autowired UserValidator validator; // Validator 메소드를 빈으로 주입

@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.serValidator(this.validator); // @InitBinder에서 validator를 선언할 수 있다.
}

@RequestMapping("/add")
public void add(@ModelAttribute User user, BindingResult result){
}

}




또 JSR-303의 빈 검증 기능을 이용할 수도 있다.

먼저 아래 코드처럼 설정 파일에서 빈을 생성한다.

<bean id="lovalValidator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />


그리고 이 빈을 @Autowired 해서 쓰면 된다. 적용 방법은 위와 동일하다.

JSR-303 빈 검증 예제 코드

public class User {
...
@NotNull
String name;

@Min(0)
int age


그 외 JSR-303에 대한 정보는 따로 찾아 봐야 겠다.



9. 모델의 일생

무슨 삼류 소설 제목 같은 챕터명이다. 모델의 일생..
어쩐지 처연한 느낌도 나고.

응? ;;;


책에서도 간단히 그림으로 설명했다. 나도 그리자.


- HTTP 요청으로부터 컨트롤러 메소드까지

그림 13-4. HTPP 요청으로부터 컨트롤러 메소드까지의 과정(p.1235)



그림 13-5. 컨트롤러 메소드로부터 뷰까지의 과정(p.1237)




흑.. 그림 그리는 건 너무 귀찮은 일이다.



저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. takuma99

    좋은 정보 정말 감사합니다.

    2012.05.07 12:55 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 도움이 됐다니 기쁘네요. 스프링3의 강력함으로, 부디 takuma99 님의 삶에 야근이 사라지기를 바랍니다. ㅋㅋ

      2012.05.13 14:54 신고 [ ADDR : EDIT/ DEL ]
  2. core..

    정말 깔끔한 정리입니다. 책으로 보아선 잘 이해가 가지 않더군요 ㅋ 이것을 한번 쭉~ 흩어보고 난 후 책을보니 개념이 잘잡히네요.
    그림도 너무잘그리셧음요!

    2013.02.26 20:57 신고 [ ADDR : EDIT/ DEL : REPLY ]


13장에서는 12장 내용보다 한 발 더 나아가, 실무에서 실제 빈번하게 쓰이는 @애노테이션 MVC 기법을 상세하게 다루고 있다.
개인적으로 매우 재미있게 읽은, 또 매우 유용했던 챕터다. 그동안 애노테이션을 쓰면서도 각 애노테이션의 하위 정보와 이 애노테이션이 영향을 미치는 범위를 정확하게 알지 못해 소극적으로(매번 쓰는 부분만 쓰는) 사용하곤 했는데, 그런 부분에 대해 상당부분 명쾌하게 정의가 되어 있었다.
13장은 160페이지가 넘는 매우 방대한 분량이고, 대부분의 내용이 용례에 가깝기 때문에 한 번에 정리하기가 어렵다.
따라서 내용에 따라 크게 세 부분으로 나누어 정리해보도록 하자.


12장에서 한참 떠들었던 내용인 Controller 타입 클래스는 스프링 3.0 에서는 @MVC로 대체되고 있다.
그러므로 12장의 내용을 잘 숙지하지 못했다고 해도, 좀 답답하기는 하겠지만 크게 슬퍼할 필요는 없다. 13장을 잘 익히면 된다.
이게 '혹시 걷지 못해도 상관없다, 뛸 줄 알면 된다' 따위의 말로 들릴 수도 있겠으나.. 뭐 크게 상관은 없다.
왜냐면 대부분 작업을 하다보면, 고급/최신 정보를 몰라서 삽질을 하기는 해도 과거에 쓰이던 기술을 몰라서 난감해지는 건 별로 없기 때문이다.
물론 스프링 2.x로 개발된 사이트의 유지보수가 주된 업무라면 얘기가 좀 달라지겠지만, 애초에 그런 상황에서 Controller의 쓰임을 모른 채 @MVC를 공부하고 있다면 그 자체로 말이 안 되는 일이다. 서두가 지나치게 길다. 이만 본론으로 가도록 하자.


1. URL 매핑

Controller 인터페이스와 메소드를 연결하는 방식에서는 대개 URL이 하나의 메소드와 연결된다. 따라서 매 URL 마다 Controller 인터페이스를 계속 만들어줘야 하는 부담이 있다. 물론 특정 URL 밑에 파라미터 값에 따라 하위 메소드를 연결하는 방법도 있긴 하지만, 이건 그다지 좋은 코드가 되지 못한다. 일단 알아 보기 힘들고 번거롭기 때문이이다.
하지만 @MVC에서는 메소드 단위 별로 URL을 매핑할 수 있다. 아래 예시를 보자.

... (중략) ...

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;


... (중략) ...


@RequestMapping("/member")

static class MemberController {

@RequestMapping("/edit") public String edit() { return "edit.jsp"; }

@RequestMapping("/delete") public String delete() { return "delete.jsp"; }

@RequestMapping("/add", method=RequestMethod.GET) public String get() { return "get.jsp"; }

@RequestMapping("/add", method=RequestMethod.POST) public String post() { return "post.jsp"; } 

}


예제코드만 봐도 뭔 얘긴지 확 감이 올 것이다. 
부연 설명이 필요하지는 않을 것 같지만 그래도 하나 덧붙이자면, 위와 같이 상위 메소드의 RequestMapping 정보를 하위 메소드가 상속하는 경우는 별 문제가 없지만 슈퍼클래스를 구현하는 서브클래스에서 RequestMapping를 설정할 때는 주의해야 한다. 하위 타입에서의 RequestMapping 정보는 항상 상위 타입의 정보를 대체하게 되는데, 오버라이드한 경우에는 @RequestMapping 애노테이션만 붙였을 때 상위 타입의 정보가 그대로 상속이 된다는 점이다. 이는 특수한 경우이고 추후 어떻게 변할지 모르기 때문에 추천하지 않는다고 한다.


그런데 보통 작업을 하다보면, URL 매핑 작업은 대개 정해진 네이밍에 따라 진행되곤 한다.
작업 대상이 게시판이든, 회원가입이든 대개 regist나 add 와 같은 이름은 뭔가를 추가 하는 페이지로 연결되고, modify나 edit와 같은 것들은 대개 정보를 수정하는 페이지로 연결된다는 것이다. 그리고 보통 이런 네이밍 규칙은 작업을 시작할 때 공통으로 정해두기 마련이다.
그렇다면 이런 규칙에 따라 각 메소드들을 보다 쉽게 URL로 매핑하는 방법이 있다면 쉬울 것이다. 아래 코드를 보자.

<추상클래스>
public abstract class GenericController<T, K, S>{

S service;

@RequestMapping("/add") public void add(T entity){}

@RequestMapping("/update") public void update(T entity){}

@RequestMapping("/view") public T view(K id){}

@RequestMapping("/delete") public void delete(K id){}

@RequestMapping("/list") public List<T> list(){}

}


<개별 컨트롤러>
@RequestMapping("/user")
public class UserController extends GenericController<User, Integer, UserService>{
public void add(User user){
// 상위 메소드 오버라이드 : 개별 메소드 로직이 들어간다.
// 별도의 설정이 없는 경우 추상클래스에서 정의된 RequestMapping 정보가 상속된다. 

}
@RequestMapping("/login")
public String login(String userId, String userPassword){
// 상위 메소드에서 규정되지 않은 메소드는 자유롭게 추가해서 쓸 수 있다.
// 당연히 별도의 RequestMapping 이 필요하다.

}
}

쉽다. 별다른 설명이 필요없을 것 같다.



2. 메소드 파라미터

@MVC에서는 매핑된 URL의 request 파라미터나, Session 정보와 같은 다양한 정보를 메소드에 넘겨 줄 수 있다.
이때 넘겨 줄 수 있는 메소드 파라미터의 종류는 다음과 같다.

- HttpServletRequest, HttpServletResponse : 말 안해도 뻔하다. 예전에 작업한 소스를 가져다 쓰고 싶을 때 크게 수정하지 않고 사용할 수 있을 것이다.
- HttpSession : 세션이다. 역시 뻔하다.
- WebRequest, NativeWebRequest : 서블릿에 종속적이지 않은 오브젝트라고 하는데, 일단은 이런 종류가 있다는 것만 알아두고 넘어 가자.
- Locale : 지역정보를 받아 온다. 책에는 DispatcherServlet의 Locale 오브젝트를 상속한다고 적혀 있는데, 그 대상이 서버인지 클라이언트인지는 확인이 필요하다.
- InputStream, Reader 
- OutpitStream, Writer
- @PathVariable : 다중패스 변수를 사용할 수 있다.

다중패스변수

@RequestMapping("/member/{membercode}/order/{orderid}")  // /member/kunner/order/1/ 과 같은 URL이 매핑될 것이다.

public String lookup(

@PathVariable("membercode") String code, 

@PathVariable("orderid") int orderid){

//메소드 로직

}


- @RequestParam : HTTP의 Request 파라미터를 메소드 파라미터에 넘겨준다. 설정된 @RequestParam는 필수 정보이므로, 반드시 HTTP 요청으로 넘어와야 한다. 만약 생략 가능한 파라미터라면 @RequestParam의 두번째, 세번째 인수를 생략하면 안 된다.

HTTP 파라미터 전달

public String view(@RequestParam("id") int id, @RequestParam("name") String name){}
public String view(@RequestParam(value="id", required=false, defaultValue=-1) int id){}
public String view(@RequestParam Map<String, String> params{} 


- @CookieValue : 쿠키 값을 메소드 파라미터에 넘겨 준다. 쿠키값이 생략 가능할 때는 역시 두번째, 세번째 인수를 넣어 줘야 한다. 

쿠키 정보 전달

public String check(@CookieValue("auth")  String auth){}
public String check(@CookieValue(value="auth",required=false,defaultValue="NONE")  String auth){}


- Map, Model, ModepMap : 말 그대로 해당 콜렉션들을 넘겨 준다.
- @ModelAttribute : HTTP 요청을 통해 넘어 온 값들을 자동으로 모델에 추가해 View에 넘길 때 쓴다. 개별 파라미터를 각각 정의해야 하는 @RequestParam에 비해 매우 유용하고 강력하다.

HTTP 요청 파라미터를 자동으로 모델로 생성해 전달

public class UserSearch{ //search 에서 연결될 모델

int id;

String name;

int level;

String email;

// 수정자, 접근자 생략

}

@RequestMapping("/user/search")

public String search(@ModelAttribute UserSearch userSearch){

List<User> list = userService.search(userSearch);

model.addAttribute("userlist",list);

}


public class User{ //add에서 연결될 모델

int id;

String name;

int level;

String email;

// 수정자, 접근자 생략

}

@RequestMapping("/user/add", mothod=RequestMethod.POST) 

public String add(@ModelAttribute User user){

//폼으로 넘어 온 정보를 자동으로 모델과 매핑한다.
userService.add(user);
//만약 user 말고 다른 이름으로 모델을 만들고 싶다면 @ModelAttribute("원하는이름") 으로 설정하면 된다.

}


- Errors, BindingResult : 파라미터 검증 오류 시 전달받을 리턴값. @ModelAttribute를 쓸 때 반드시 함께 사용해야 한다. 자세한 내용은 검증 부분에서 다시 다룬다.
- @RequestBody : XML이나 JSON 기반의 메시지를 사용하는 경우 Body의 값을 그대로 전달.
- @Value : 빈의 값 주입에서 사용하던 @Value 애노테이션과 동일.
- @Valid : JSR-303의 빈 검증기를 사용하도록 하는 지시자. 자세한 내용은 검증 부분에서 다시 다룬다.


3. 리턴 타입

@Controller의 리턴타입은 일부 예외가 있지만 결국 ModelAndView 라고 봐도 무방하다. 메소드의 다양한 정보가 Model로 취합되고 View로 리턴된다는 것이다.

다음 정보는 모델에 자동으로 추가된다.

- @ModelAttribute 로 넘어 온 메소드 파라미터의 모델
- Map, Model, ModelMap 파라미터 
- @ModelAttribute 메소드 : 모델에만 붙이는 게 아니라 메소드에도 붙일 수 있다. 어떤 메소드가 실행될 때 같이 실행되어야 하는 메소드가 있는 경우 해당 메소드에 @ModelAttribute 애노테이션을 부여하면 함께 실행되어 모델로 추가된다. 회원가입 화면에서 주소 지역 필드를 DB에서 조회해 폼에 출력해야 할 때, 유용하게 사용할 수 있다.
- BindingResult : 검증 결과를 모델로 담아 리턴한다. org.springframework.validation.BindingResult.모델이름 으로 리턴된다. 



그 외 리턴 타입은 다음과 같다.

- ModelAndView : 전통적인 스프링 MVC에서처럼 ModelAndView를 리턴한다. 하지만 @Controller에서는 ModelAndView를 직접 리턴하는 방법을 잘 쓰지 않는다.
- String : 모델은 파라미터로 맵을 가져와 넣고, 리턴값은 뷰 이름을 스트링으로 선언하는 방식이다. @Controller에서 가장 흔하게 사용되는 방법이다. 촌스럽게 ModelAndView 를 왜 리턴하지 않느냐고 당황해 하지 말자.

@RequestMapping("/user/search")
public String hello(@RequestParam String name, Model model){
model.addAttribute("name",name); // 모델에 추가
return "hello"; // View 이름 리턴


- void : RequestToViewNAmeResolver 전략을 통해 메소드 이름이 곧 리턴할 view의 이름이 된다. 메소드 이름과 view의 이름을 통일할 수 있는 경우 string을 리턴할 것도 없이 바로 void를 써도 된다. 역시 리턴값이 없다고 당황하지 말자.

- 모델 오브젝트 : 위의 void에서처럼 뷰 이름은 메소드 이름과 같고, 모델에 추가해야 할 오브젝트가 하나 뿐이라면(즉, 전달할 파라미터가 한개라면) 바로 모델 자체를 리턴할 수 있다. 모델을 리턴하는 경우에도 실제로는 메소드 이름과 같은 뷰가 리턴된다.

@RequestMapping("/view")
public String hello(@RequestParam int id){
return userService.getUser(id); // 모델에 연결되는 파라미터 값이 하나일 때 


- Map/Model/ModelMap : 각 콜렉션을 모델로 만들어서 리턴하는 경우에도 모델 오브젝트를 리턴하는 것과 같은 효과를 낸다. 단, 맵 타입의 단일 오브젝트 리턴은 절대 해서는 안 된다.

Map 오브젝트 리턴의 잘못된 사용

@RequestMapping
("/view")
public String hello(@RequestParam int id){
Map userMap = userService.getUserMap(id);
return userMap; // 이렇게 맵을 리턴하면 맵을 다시 모델로 감싸서 리턴하기 때문에 안 된다.



Map 오브젝트 리턴의 올바른 사용 

@RequestMapping
("/view")
public void hello(@RequestParam int id, Model model){
model.addAttribute("userMap", userService.getUserMap(id)); // 아예 모델로 싸서 리턴하면 된다.


- View : XML 뷰어와 같이 별도의 뷰 오브젝트로 넘기고 싶을 때 리턴타입을 View로 설정한다.
- @ResponseBody : 메소드의 실행 결과가 바로 HTTP Response로 출력된다.

@RequestMapping("/view")
@ResponseBody
public String hello(){
return "<html>블라블라블라</html>"; // 원래 String 리턴타입은 뷰 이름을 말하지만, @ResponseBody 애노테이션 때문에 바디에 출력되는 스트림으로 인식된다.

 



4. 세션을 이용한 상태 저장

아다시피 웹은 비동기식 프로그램이라, 상태를 저장하지 않는다.
좀 더 쉽게 말하면 웹에서는 클라이언트가 서버와 완전히 동기화된 채 작업하는게 아니라는 뜻이다. 그 말이 그 말인가? -ㅅ-;
부연하면 - 웹 환경에서는 클라이언트가 서버에 정보를 요청하면, 서버는 해당 요청에 따른 작업결과를 클라이언트에게 전달한다. 이렇게 요청 - 전달의 순간에만 서버와 클라이언트가 연결되어 있고, 그 후에는 연결이 끊긴다. 비동기식 프로그램이란 바로 이런 뜻이다. 뭐 대단한 얘기는 아니다. 그냥 그렇구나, 하고 끄덕이면 된다.

아무튼 누구나 다 아는 이런 얘기를 하려는게 아니고..
이렇게 상태가 유지되지 않는 웹프로그램의 특성 상 여러 화면이 하나의 결과를 만드는 - 즉, 위저드 방식으로 제작되어 폼 페이지가 여러 번 나오고 Submit은 최종적으로 한번만 해야 되는 경우 처리하기가 매우 곤란하다. 꼼수를 부려 처리하는 여러가지 방법이 있긴 하지만, 그건 깔끔하지 못하다. 

이런 경우 세션을 이용해 상태를 저장하면 쉽게 처리할 수 있다.

세션을 이용한 상태 저장

@Controller

@SessionAttributes("user") // 메소드에서 다룰 세션 정보를 지정한다. 복수로 지정될 수 있다.

static class UserController {

@RequestMapping(value="/user/edit", method=RequestMethod.GET) 

public User form(@RequestParam int id) {

return new User(1, "Spring", "mail@spring.com"); // 특별한 지시가 없어도 리턴된 모델은 세션으로 저장된다.

}

@RequestMapping(value="/user/edit", method=RequestMethod.POST) 

public void submit(@ModelAttribute User user, SessionStatus sessionStatus) {

sessionStatus.setComplete(); // 이 메소드에서 사용된 세션 정보를 비운다.

}





p.1173을 보면, 세션을 이용한 폼 모델의 저장/복구 과정을 이해하기 쉽게 도식으로 설명해 놓았다.
뭐 사실 그림으로 보면서까지 이해를 해야 할 내용은 아니지만, 각 작업의 시점을 명확하게 아는 것은 중요하다.


여기까지가 13장의 1/3 정도 분량이다.
나머지는 #2, #3으로 나누어 정리할 예정이다.


저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요


이번 장에서는 스프링을 웹 프레젠테이션 계층에서 어떻게 사용하는가에 대해 이야기하고 있다.

예전에 얘기했던 바와 같이 웹 기술이라는 것은 결국 무엇을 어떻게 보여줄 것인가에 관한 이야기라 할 수 있다.
그러므로 스프링 웹기술이라는 것 또한 그 범주에 있는 일 들이다.

스프링에서 사용하는, 또는 스프링과 함께 사용할 수 있는 웹 기술, 프레임워크는 여러가지가 있다.
Struts니 SWF니 하는 것들이 그것이다.
책에서는 스프링에서 직접 제공하는 스프링 MVC를 소개하고 있는데, 실제로 실무에서 많이 쓰인다.

MVC는 구성요소Model, 화면출력View, 제어로직Controller 으로 이루어진다.
스프링 MVC도 마찬가지여서 이 세가지 요소가 서로 주거니 받거니 하며 요청을 처리하고 응답한다.
MVC는 보통 프론트 컨트롤러front controller[각주:1]와 함께 사용되는데 스프링 MVC에서는 DispatcherServlet이 그 역할을 담당한다.

아래 도식은 DispatcherServlet이 프론트 컨트롤러로 MVC 컴포넌트들과 어떻게 동작하는지를 보여주고 있다.

스프링의 DispatcherServlet과 MVC. (p.1022)


그림에서 보다시피 (1)HTTP 요청 및 (7)응답과 같은 클라이언트와의 상호작용 뿐 아니라 컨트롤러, 뷰의 동작 제어 모두 DispatcherServlet이 도맡아 처리하고 있다. 사실 상 스프링 MVC의 처음이자 마지막이다. 각 단계에 대한 세세한 설명은 책을 참고하자.

또 다음과 같이 DI를 통해 DispatcherServlet을 확장할 수 있다. 

Dispatcher의 DI 가능한 7가지 전략

- HandlerMapping
  요청에 따라 어떤 컨트롤러에 연결할 것인지 제어할 수 있다.

- HandlerAdapter
  핸들러 매핑으로 선택한 컨트롤로러를 DispatcherServlet이 호출할 때 사용한다.

- HandlerExceptionResolver
  예외가 발생했을 때 처리 방법을 정의한다.

- ViewResolver
  컨트롤러가 반환한 뷰 이름을 참고해 적절한 뷰 오브젝트를 찾아 준다. 또는 연결될 뷰 오브젝트를 정의할 수 있다.

- LocaleResolver
  지역 정보를 결정한다. HTTP 헤더 뿐 아니라 세션, URL 파라미터, 쿠키 등 다양한 방식으로 결정 가능하다.

- ThemeResolver 
  사이트에 테마를 선택 적용하는 경우 테마 적용에 관한 정의를 할 수 있다.

- RequestToViewNameTranslator
  컨트롤러에서 뷰 이름이나 뷰 오브젝트를 지정해 주지 않은 경우 자동으로 뷰 이름을 생성한다.



처음 스프링으로 작업된 소스를 봤을 때 혼란스러웠던 기억이 난다.
ModelAndView 라는 리턴타입이 뭘 의미하는지 몰랐기 때문이다.
그리고 그 동작방식을 알게 됐을 때 얼마나 놀랐던지... 
세상이 많이 달라졌구나, 하고 느꼈던 기억이 난다. ㅎㅎ


스프링의 강력함은 DispatcherServlet과 컨트롤러를 이용해 얼마든 새로운 프레임워크를 만들어 낼 수 있는 유연함으로 설명할 수 있다. 따라서 그저 스프링 MVC에서 제공하는 기능들을 갖다 쓰는 것만이 아니라 작업환경과 업무 내용에 맞게 커스터마이징하고 더 나은 퍼포먼스를 낼 수 있도록 발전시켜 나가는 것이 필요하다.

아무튼 이번 장에서 다룬 중요한 내용은 DispatcherServlet의 동작방식과 순서, 사용법과 확장 전략에 대한 이야기들이다.
지난 몇몇 프로젝트에서 개발한 소스를 가져다 놓고 설명하면 더 좋겠지만, 그러다간 책 한 권 쓰게 될지도 모르므로.. 두리뭉실하지만 이 정도로 마치자.

다음 장은 스프링 MVC의 본격적인 개발법을 다룰 예정이니, 그때 좀 더 자세히 얘기할 수 있을 것이다.


  1. 프론트 컨트롤러란 프레젠테이션 계층의 제일 앞에서 서버로 들어오는 모든 요청을 받고, 공통적인 작업을 수행하고, 요청의 성격에 따라 처리 대상 컨트롤러로 분배하고, 다시 취합하여 화면에 출력하는 중앙집중형 컨트롤러를 말한다. 또한 작업도중 예외가 발생하면 예외의 처리 방법까지도 프론트 컨트롤러를 통해 제어할 수 있다. (본문 p.1021) [본문으로]
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요


예전에.. 적어도 "웹프로그램" 이란 것은 DB 기술에 의존하는 부산물 같은 것이라고 생각했다. 실제로 중요한 것은 DB 기술이고, 웹프로그램에서 제어하는 것은 프레젠테이션 수준의 것이라고 생각했던 것이다. 사실 90년대 말에서 2000년 대 초반까지 주류를 이루던 asp니 jsp니 php니 하는 것들에 대해서는 크게 틀린 말도 아니었을 것이다. 



물론 지금도 일반적인 의미에서 "웹프로그램"이 하는 주요 역할은 DB(뿐 아니라 어떤 형태든)의 데이터를 특정한 방법으로 가공해 사용자의 브라우저에 표시해주고, 다시 사용자의 입력을 받아 DB에 저장하는 일련의 상호작용일 것이다.

그렇지만 최근 들어 각광받고 있는 여러 ORM 기술들 덕분에 개발에서 차지하는 DB제어 기술의 비중이 예전의 그것과 사뭇 달라진 것 같다. 꼭 그거 아니더라도, iBatis만 놓고 봐도 예전에 소스에다 컨트롤 C 컨트롤 V 하면서 갖다 박았던 것과는 정말 많이 다르다. 세상은 점점 편해져 간다 - 이렇게 프로그램 개발에서조차도.


웹프로그램에서 DB와 연동하는데는 크게 몇가지 중요한 개념이 있다.

1. DB 연결.
2. DB 조작/제어.
3. 트랜잭션

예전 같으면 DB를 연결하고, DB를 조작/제어 하고 여기에 트랜잭션 기술을 걸고 하는데 참 많은 손이 갔다. 특히 똑같은 코드를 페이지마다 계속 반복적으로 갖다 붙여야 했던 점, 아주 간단한 기능에도 코드가 주르륵 다 따라 붙어야 했던 점이나, 트랜잭션과 같은 부분에서 안정성과 유연성을 동시에 보장하기 어려웠던 점 등 불편했던 점이 한 두가지가 아니다.

그런데 스프링에서는 스프링 자체에서 지원하는 기술과 ORM 등 외부 기술과의 연계를 통해 이런 과정을 획기적으로 편리하게 해 준다.

DB를 연결하고 트랜잭션을 거는 일 따위를 환경설정 몇 줄로 다 끝낼 수 있다. 한번 제대로 설정해 놓으면, 그 다음부터는 딱히 신경 쓸 필요가 없다. 그리 어렵지도 않다. 놀라운 일이다.

비록 DB데이터를 조작하고 제어하는 일은 여전히 손이 많이 가는 일이고, 메소드마다 다 제각각일 수 밖에 없는 건 매한가지지만.. 그 적용 방법은 ORM 기술을 통해 획기적으로 편리해졌다. 감탄하는 식의 얘기가 계속되는데.. 예전의 노가다식 프로그램을 생각해 보면 이렇게 감탄하지 않을 수가 없다. 

개발을 하다보면 반복적이고 기계적인 적용이 필요한 곳들을 발견하게 된다. 그게 한번이 되고, 두 번이 되고 세번이 되면.. 어떻게 하면 이런 반복을 줄일 수 있을까 고민한다. 때문에 어지간한 개발자들은 완성도가 어떻든 저마다 일종의 프레임워크를 만들어 사용하게 된다. 그러니 이런 개념은 사실 완전히 새롭거나 완전히 획기적인 것은 아니다. 그렇지만 프레임워크의 완성도나 적용의 용이함, 그리고 실제 개발 시 시스템 전체를 아우르는 프레임워크의 커버리지 등은 정말 놀랍다. 이런거 진작 좀 나오지, 하는 생각 안 들 수가 없다. 쩝...


여튼, 이번 장에서는 DB를 연결하고 트랜잭션을 관리하는 기술들을 소개한다.
(DB의 조작과 제어는 SQL이나 ORM 기술 등 스프링이 하는 일과는 조금 거리가 있기 때문에 제외되었다)


책에서는 전통적인 JDBC는 물론, iBatis, JPA, 하이버네이트, JTA 등 여러가지 기술을 다루고 있다.
DB를 연결하고 자바 코드에서 불러와 사용하는 방법, 트랜잭션을 관리하는 법 등 굉장히 세밀하게 기술하고 있는데, 그걸 일일히 다 읽고 외울 필요는 없을 것이다.

일단은 빈번하게 쓰이는 것들을 눈여겨 보고, 나중에 필요할 때 또 들춰 보는 식으로 익히는게 좋을 것 같다.


아래는 지난 10장에서 소개한 applicationContext.xml 샘플 중 DB 관련 내용이 있는 부분이다.

이 소스는 iBatis 와 연동해 개발된 것이다.
이해를 돕기 위해 아래 코드에 주석과 각주를 달아 보자.
서로 연결된 오브젝트는 같은 색깔로 되어 있다. 그 표시들을 주의깊게 보면 이해가 더욱 쉬울 것이다.


applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:p="http://www.springframework.org/schema/p"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans   

        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  

        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd

        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">


        <!-- ... 중략 ...  -->



        <!--트랜잭션 적용을 위한 AOP 설정 -->

<aop:config>

<aop:pointcut id="defaultServiceOperation"

expression="execution(* classpathRoot.*.service.*Service.*(..))" /> // 클래스패스 하위의 service 패키지에 있는 개체 중 이름이 Service로 끝나는 것들에 적용한다. 스프링에서는 보통 인터페이스에 적용해야 한다.[각주:1] 

<aop:advisor pointcut-ref="defaultServiceOperation"

advice-ref="defaultTxAdvice" order="2"/>
                 <!--
                 // 위 두 <aop:pointcut />과 <aop:advisor />를 아래처럼 하나의 태그로 합칠 수 있다. 
<aop:advisor advice-ref="defaultTxAdvice" order="2" pointcut="execution(* classpathRoot.*.service.*Service.*(..))"  />

                 --> 

</aop:config>


<!-- 실제 메소드에 트랜잭션 적용하기 -->

<tx:advice id="defaultTxAdvice" transaction-manager="transactionManager">

<tx:attributes>

<tx:method name="update*[각주:2]" propagation="REQUIRED"[각주:3] rollback-for="Exception"[각주:4]/>

<tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="insert*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="process*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="perform*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="get*" propagation="REQUIRED" read-only="true"[각주:5] />

<tx:method name="set*" propagation="REQUIRED" read-only="true" />

<tx:method name="find*" propagation="REQUIRED" read-only="true"/>

<tx:method name="*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"[각주:6] />

</tx:attributes>

</tx:advice> 


<!-- DB 연결 설정 -->

<bean id="nativeJdbcExtractor" class="org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor" lazy-init="true"/>


<bean id="oracleLobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true">

   <property name="nativeJdbcExtractor"><ref local="nativeJdbcExtractor"/></property>

  </bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean" >

<property name="configLocation">

<value>/WEB-INF/config/sqlmap-config.xml</value> //iBatis Sql Mapping 연결설정

</property>

<property name="dataSource">

<ref bean="dataSource" />

</property>

<property name="lobHandler" ref="oracleLobHandler" />

</bean>

<!-- jndi -->

<bean id="jnditemplate" class="org.springframework.jndi.JndiTemplate" >

    <property name="environment">

       <props>

          <prop key="java.naming.factory.initial">jeus.jndi.JNSContextFactory</prop>

          <prop key="java.naming.provider.url">localhost:9736</prop>

       </props>

    </property>

</bean>

<!-- Jeus Server use ConnectionPoolingDataSource -->

<bean id="transactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager"

p:dataSource-ref="dataSource" />

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName" value="JNDI_NAME"/>

<property name="jndiTemplate" ref="jnditemplate"/>

</bean> 

</beans>



위 소스는 사실 어느 정도는 기계적인 적용이 가능하다.
JNDI와 데이터소스, 트랜잭션 매니저를 설정하는 부분은 해당 환경에 맞게 한번만 바꿔주면 되는 것이니 한번 샘플을 제대로 만들어 놓으면 쉽게 적용해 쓸 수 있을 것이다.

또 트랜잭션을 설정하는 부분 역시 프로그램마다 크게 다를리 없다.
데이터액세서란 결국 CRUD[각주:7] 안에서 움직이기 때문이다.
따라서 트랜잭션 설정 부분의 readOnly 애트리뷰트를 놓고 보면 언제나 Read 에서는 true, 그 외 CUD에서는 false가 될 것이다.

트랜잭션을 설정하는 방법은 이렇게 applicationContext에서 미리 설정해 두는 방법 외에도 자바 소스에서 메소드 앞에 @Transactional 애노테이션을 적용하는 방법이 있다.
애노테이션으로 설정된 트랜잭션은 applicationContext에서 정의한 트랜잭션에 우선한다. 이 애노테이션을 사용하고 싶으면 applicationContext에 다음과 같은 선언을 해 주면 된다.

<tx:annotation-driven />


@Transactional 애노테이션은 메소드 단위로 적용할 수 있다.
이때, 애노테이션을 클래스 앞에 붙여 놓으면 하위 메소드에 일괄 적용된다.

아래는 트랜잭션 설정에 관한 애트리뷰트의 종류와 설정값에 대한 정리이다.

트랜잭션 설정에 관한 애트리뷰트의 종류와 설정값

propagation

  - REQUIRED: 기본 속성. 모든 트랜잭션 매니져가 지원함. 이미 시작된 트랜잭션이 있으면 참여, 없으면 새로 생성. 
  - SUPPORTS: 이미 시작된 트랜잭션이 있으면 참여. 없으면 그냥 진행.
  - MANDATORY: 이미 시작된 트랜잭션이 있으면 참여. 없으면 예외 오류.
  - REQUIRES_NEW: 항상 새로운 트랜잭션 시작. 이미 시작된 트랜잭션은 일시중지됨.
  - NOT_SUPPORTED: 사용안함. 이미 시작된 트랜잭션은 일시중지됨.
  - NEVER: 트랜잭션을 사용하지 않음. 이미 시작된 트랜잭션이 있으면 예외 오류.
  - NESTED: 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션 실행. 일련의 처리 중 트랜잭션을 롤백시킬 만큼의 중대한 처리가 아닌 부분에 한시적으로 사용할 수 있음. ex) 로그 기록 중 오류가 나는 경우 업무 처리는 그대로 진행하도록 함. 단, 업무 처리에서 오류가 난 경우 해당 로그 기록도 함께 삭제됨.


isolation

  - DEFAULT: DB 드라이버의 기본 설정에 따른다.
  - READ_UNCOMMITTED: 가장 낮은 격리수준으로 트랜잭션 커밋 여부와 관계 없이 다른 트랜잭션에 노출.
  - READ_COMMITTED: 가장 많이 쓰임. 일반적으로 얘기하는 트랜잭션. 커밋되지 않은 정보는 다른 트랜잭션에서 보이지 않는다.
  - REPEATABLE_READ: 다른 트랜잭션에서 읽은 로우의 경우 수정 불가. 단, 새로운(다른 트랜잭션에서 읽히지 않은) 로우에 대해서는 수정 가능하다.
  - SERIALIZABLE: 읽기만 해도 수정 불가. 가장 강력하고 가장 낮은 성능. MSSQL의 쿼리어낼라이저에서 트랜잭션 걸어봤다면 확 감이 오겠지.


read-only, readOnly
  - true / false: 추가 설명이 필요 없을 것 같다.

트랜잭션 롤백 예외
  - rollback-for, rollbackForClassName, rollbackFor: 역시 추가 설명이 필요 없을 것 같다.

트랜잭션 커밋 예외
  - no-rollback-for, noRollbackForClassName, noRollbackFor : 마찬가지로 추가 설명이 필요 없을 것 같다.




이제 실제 java 소스코드에서 어떻게 쓰이는지 보자.

먼저 iBatis 를 쓰기 위해 임포트하자.

import com.ibatis.sqlmap.client.SqlMapClient;



다음은 각 DAO 에서 SqlMapClient를 구현한다.

private SqlMapClientTemplate sqlMapClientTemplate;

public void setSqlMapClient(SqlMapClient sqlMapClient) {

sqlMapClientTemplate = new SqlMapClientTemplate(sqlMapClient);

}


// 아래는 DAO에서 구현한 메소드이다. 
public void insert(Member m) { sqlMapClientTemplate.insert("insertMember", m); }

public void deleteAll() { sqlMapClientTemplate.delete("deleteMemberAll"); }

public Member select(int id) { return (Member)sqlMapClientTemplate.queryForObject("findMemberById", id); }

public List<Member> selectAll() { return sqlMapClientTemplate.queryForList("findMembers"); }


여기서 주의깊게 볼 것은 sqlMapClientTemplate의 사용법이다. 좀 더 자세히 살펴 보자.

sqlMapClientTemplate 사용법

1. 등록/수정/삭제

insert()

  - Object insert(String statementName): 여기서 statementName은 XML 매핑파일에서 규정된 쿼리의 id 값이다. 이하 동일함.
  - Object insert(String statementName, Object parameterObject): parameterObject는 넘겨 줄 파라미터 값이다. SQL 매핑 파일에서 #파라미터#와 연동된다. 이하 동일함.


update()
  - int update(String statementName): 반환되는 int 값은 결과행의 수이다. 이하 동일함.
  - int update(String statementName, Object parameterObject)
  - void update(String statementName, Object parameterObject, int requiredRowsAffected): 반환값이 없는 대신 예상 결과행 수와 맞지 않으면 예외 오류 발생.

delete()
  - int delete(String statementName): 반환되는 int 값은 결과행의 수이다. 이하 동일함.
  - int delete(String statementName, Object parameterObject)
  - void delete(String statementName, Object parameterObject, int requiredRowsAffected): 반환값이 없는 대신 예상 결과행 수와 맞지 않으면 예외 오류 발생.


2. 조회

단일 로우 조회: queryForObject() - 실행된 결과가 단일행인 경우 사용한다.
  - Object queryForObject(String statementName)
  - Object queryForObject(String statementName, Object parameterObject)
  - Object queryForObject(String statementName, Object parameterObject, Object resultObject): 특정 오브젝트로 결과값을 반환할 경우 resultObject로 정의한다.

다중 로우 조회: queryForList() - 실행된 결과가 한건 이상일 경우 List로 반환
  - List queryForList(String statementName)
  - List queryForList(String statementName, Object parameterObject)
  - List queryForList(String statementName, int skipResults, int maxResults): 몇번째 행부터, 최대 몇개 행을 가져 올 것인지 규정할 수 있다.
  - List queryForList(String statementName, Object parameterObject, int skipResults, int maxResults)

다중 로우 조회: queryForMap() - JDBC에서의 querForMap과는 전혀 다르므로 주의해야 한다.
  ※ JDBC에서는 하나의 Row를 대상으로 각 필드명을 key로 하여 필드 데이터를 맵에 담는데 반해, iBatis에서는 여러개의 Row를 대상으로 하여 지정된 컬럼을 key로 하여 각 로우의 모든 데이터를 맵에 담는다.
  - Map queryForMap(String statementName, Object parameterObject, String keyProperty): keyProperty는 key값이 될 필드를 말한다. 이하 동일함.
  - Map queryForMap(String statementName, Object parameterObject, String keyProperty, String valueProperty): valueProperty는 로우 전체가 아닌 특정 필드값만 맵에 담고 싶을 때 적어준다.

다중 로우 조회: queryWithRowHandler() - JDBC의 RowMapper와 비슷한 기능을 한다.
  ※ SQL 결과를 루프를 돌면서 각 로우마다 콜백 오브젝트를 호출한다.
  - void queryWithRowHandler(String statementName, RowHandler rowHandler): 각 로우마다 호출될 rowHandler는 미리 구성해 놓아야 한다.
  - void queryWithRowHandler(String statementName, Object parameterObject, RowHandler rowHandler)

rowHandler 구성 방법 예시[각주:8]

// RowHandler import
import com.ibatis.sqlmap.client.event.RowHandler;

// row를 받아 오기 위해 Entity import
import com.lg.g4c.capp.lib.entity.Entity;

//  실제 rowHandler 구현

public class ResultRowHandler implements RowHandler{

private List<Entity> returnList = new ArrayList<Entity>() ;


public ObjectRowHandler(){

}


public void handleRow(Object rowObject){

Entity result = new Entity() ;

try{

result.parseResultSet((ResultSet)rowObject) ;

returnList.add(result) ;

}catch(Exception e){

e.printStackTrace();

}

}

public List<Entity> getReturnList(){

return returnList ;

}




마지막으로 iBatis SQL 매핑 파일 및 SQL 파일을 보자.


sqlmap-config.xml

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE sqlMapConfig

PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"

"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">


<sqlMapConfig>

<settings enhancementEnabled="true"[각주:9] useStatementNamespaces="true"[각주:10] />


<!-- common sqlMap -->

<sqlMap resource="xmlfile.xml" />[각주:11]

... (필요한 수 만큼 반복) ... 

</sqlMapConfig> 




xmlfile.xml

<?xml version="1.0" encoding="UTF-8"?>


<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"

    "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="BoardFile">

<typeAlias alias="fileVo" type="projectName.model.FileVo"/> //조회된 값과 연결될 오브젝트

<typeAlias alias="map" type="java.util.Map"/>

<resultMap id="_resultMap" class="fileVo" >

<result column="FILE_NM" property="fileNm" />

<result column="FILE_PATH" property="filePath" />

</resultMap>

<select id="fileInfo" resultMap="_resultMap" parameterClass="fileVo"> // 파라미터와 결과값에 대한 정의를 설정한다.

SELECT *

FROM

TABLE_NAME

WHERE

SEQ = #seq#

<isNotEmpty property="requestParameter"> // 넘겨 받은 값에 의한 판별

AND MENU_CD = #requestParameter#  // #~#로 된 부분이 파라미터와 연결되는 부분이다. 이름이 같으면 자동 연결된다.

</isNotEmpty>

</select>

  
<!-- 반복되는 코드는 아래와 같이 include 해서 쓸 수 있다. -->
<sql id="page-header">

SELECT PAGINGSELECT.*

FROM (SELECT TMP.*,

CEIL(ROWNUM/#pageSize#) AS TMPPAGE

FROM (

</sql>

<sql id="page-footer">

) TMP

) PAGINGSELECT

WHERE PAGINGSELECT.TMPPAGE = #pageNo#

</sql>
<!-- 반복되는 코드 정의 -->

  <select id="selectInfoDivList" resultMap="infoResult">

<include refid="page-header"/> // 미리 정의해 둔 코드 인클루드

SELECT PRT_ORDER

,BBS_DIV_CD

,BBS_NM

,USE_YN

FROM INFOVIO_DIV

WHERE INSTR(BBS_DIV_CD, 'COM') = 0

<isNotEmpty prepend="AND" property="searchType">

USE_YN = #searchType#

</isNotEmpty>

ORDER BY BBS_DIV_CD, PRT_ORDER

<include refid="page-footer"/> // 미리 정의해 둔 코드 인클루드

</select>

 </sqlMap> 




코드만 봐도 참 쉽지 않은가?

이거 원.. 밥로스 라도 소환해야 할 것 같다.





- JPA와 하이버네이트, JTA 에 대한 내용은 책에 나온 내용이 좀 부실하여 따로 공부를 좀 더 해야 할 것 같다.





 
  1. 만약 클라이언트에서 인터페이스가 바라보는 실제 메소드를 직접 호출한 경우 트랜잭션 적용이 안 될 수 있다. 이런 경우에는 트랜잭션 전파 범위 설정을 클래스 단위로도 할 수 있지만, 이는 스프링이 지향하는 개발 방식과 배치되므로 주의해야 한다. 일단은 인터페이스에 적용하고 인터페이스를 불러다 쓰는 것이 좋다는 정도로 정리하자. (클래스 프록시: 본문 p988 ~) [본문으로]
  2. 이름이 update로 시작되는 모든 메소드에 적용한다는 뜻이다. [본문으로]
  3. 트랜잭션의 전파 범위를 설정한다. [본문으로]
  4. 롤백 예외: 롤백이 필요한 경우에 대한 설정. rollback-for, rollbackFor, rollbackForClassName이 있다. rollback-for, rollbackForClassName에서는 예외 종류를, rollbackFor에서는 클래스를 지정해 준다. 반대 개념으로 no-rollback-for, noRollbackFor, noRollbackForClassName 애트리뷰트가 있다. [본문으로]
  5. 읽기 전용 속성을 설정한다. read-only, readOnly 애트리뷰트에 true/false 를 설정한다. 기본은 false다. [본문으로]
  6. 트랜잭션 격리수준을 말한다. 자세한 설명은 아래 따로 정리한다. [본문으로]
  7. Create / Read / Update / Delete [본문으로]
  8. http://blog.naver.com/brainkorea/150099231928 참조 [본문으로]
  9. 바이트코드의 기능 향상. [본문으로]
  10. SQL 매핑 정보에서 nameSpace를 사용할 것인지 여부. 만약 true인데 SQL 파일에는 namespace 정보가 없으면 오류가 나므로 주의. [본문으로]
  11. 관리의 편의를 위해 xml 파일을 여러개로 쪼갠 경우 개수만큼 행을 반복한다. [본문으로]
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. jaeger

    참 쉽죠잉~

    2012.02.14 17:49 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. jaeger

    7월 예정이에요..^^

    2014.01.22 13:56 신고 [ ADDR : EDIT/ DEL : REPLY ]


이번 장의 분량은 꽤 많다. 약 140p - 장수로는 70장 - 소설처럼 술술 읽히지 않는 특성 상 글자를 읽는데만 서너시간씩 걸린다.
눈꺼풀은 어찌나 무겁던지.. 

처음 이 책을 들었을 때 철저히 숙독해야겠다고 마음 먹었다.
먹고 살기 위한 공부가 아니라 공부를 위한 공부를 해 보겠다고 말이지.
사실 이미 아는 내용이거나, 실무에서 빈번하게 쓰이지 않는 부분은 그냥 대충 읽고 넘어가도 좋을텐데.. 얼마간은 바보 같은 짓인것 같기도 하다.

이 책은 한번 읽고 따라해보는 스타일이 아니라 원리를 이해하고 계속해 두고 보는 바이블 스타일이다.
그래서 단시간에 읽고 이해하기도 어렵지만 정리하기는 더욱 어려운 것 같다.



이번 장에서는 스프링 컨테이너에서 Bean을 DI하는 법에 대해 말하고 있다.
사실 실무에서 늘 하던거지만, 이렇게 체계적으로 파고 들어 본 적은 없었다.
그저 기계적인 접근(Ctrl + C, Ctrl + V)만이 있었을 뿐.

그래서 이번 장은 참 따분하기도 했고[각주:1], 또 참 흥미로운 내용[각주:2]이기도 했다.
 
70페이지나 되는 방대한 분량을 게시물 하나로 정리하기는 좀 어렵겠고..
하나 하나 세세하게 짚어야 할 이유도 별로 없을 것 같다.


IoC 컨테이너를 통해 애플리케이션이 만들어지는 방식 (본문 p772)



IoC에 대해서는 이미 앞선 1부에서 충분히 이야기 했던 내용이니 이론적인 이야기는 그림을 통해 설명하고 넘어가자.
귀찮아서라기보다, 딱히 더 설명할 것이 없다.

대신 실무에서 빈번하게 사용되는 방식인 XML을 이용한 applicationContext 설정을 가져다 review 하는 방식으로 이 장을 정리하고자 한다.
예제로 제시되는 applicationContext 는 얼마 전에 실제로 개발한 프로젝트에서 사용한 것 중 일부이다.
원본 applicationContext는 분량이 꽤 방대해 그 중 일부를 발췌했다.
따라서 정상 동작하는 소스가 아니므로, 어떻게 하는지만 대충 살펴보기 바란다.


applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:p="http://www.springframework.org/schema/p"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans   

        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  

        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd

        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">


        <!-- 동적 URL Mapping : 각 url에 오브젝트를 연결한다. -->

<bean id="defaultUrlMapping" 

class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

<property name="order" value="1"/>

<property name="interceptors"> //정의된 url에 인터셉트를 적용한다.

                    <list>

                        <ref bean="serviceSessionInterceptor"/> //인터셉터의 예. list 로 복수를 지정할 수 있다.

                        <ref bean="menuNodeInterceptor"/>

                    </list>

                </property>

<property name="mappings">

<props>

<prop key="/serviceName#1.do">serviceController#1</prop> //서블릿(do)에 연결될 오브젝트(Controller)

<prop key="/serviceName#2.do">serviceController#2</prop>
                                ... (연결될 수 만큼) ...
</props> 

</property>
</bean>
 

        <!-- 정적 URL Mapping : 각 url에 staticUrlController를 연결한다. 단순 html 페이지에서 사용 -->

<bean id="staticUrlMapping" 

class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

<property name="alwaysUseFullPath" value="true" />

<property name="order" value="6"/>

<property name="interceptors"> //정의된 url에 인터셉트를 적용한다.

                    <list>

                        <ref bean="serviceSessionInterceptor"/> //인터셉터의 예. list 로 복수를 지정할 수 있다.

                        <ref bean="menuNodeInterceptor"/>

                    </list>

                </property>

<property name="mappings">

<props>

<prop key="/serviceName#1.do">staticUrlController</prop> //서블릿(do)에 연결될 staticUrlController

<prop key="/serviceName#2.do">staticUrlController</prop>
                                ... (연결될 수 만큼) ...
</props> 

</property>
</bean>

 

<bean id="staticUrlController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/> // 위에서 호출한 정적(static) 페이지를 보여주는 서블릿 컨트롤러


        <!--JSP View Mapping : JSP페이지를 view로 연결 -->

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" 

p:prefix="/WEB-INF/jsp/"

p:suffix=".jsp">

<property name="viewClass"

           value="org.springframework.web.servlet.view.JstlView" />

</bean>

 

        <!--트랜잭션 적용을 위한 AOP 설정 -->

<aop:config>

<aop:pointcut id="defaultServiceOperation"

expression="execution(* classpathRoot.*.service.*Service.*(..))" />

<aop:advisor pointcut-ref="defaultServiceOperation"

advice-ref="defaultTxAdvice" order="2"/>

</aop:config>


<!-- 실제 메소드에 트랜잭션 적용하기 -->

<tx:advice id="defaultTxAdvice" transaction-manager="transactionManager">

<tx:attributes>

<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/> //update로 시작하는 이름을 가진 모든 메소드에 적용한다. 이하 동일.

<tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="insert*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="process*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="perform*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="get*" propagation="REQUIRED" read-only="true" />

<tx:method name="set*" propagation="REQUIRED" read-only="true" />

<tx:method name="find*" propagation="REQUIRED" read-only="true"/>

<tx:method name="*" propagation="REQUIRED" read-only="true"/>

</tx:attributes>

</tx:advice> 

 

<!-- Bean을 등록하고 해당 Bean에 DI 하기 -->

<bean id="codeService" class="mest.eks.common.service.CodeService">

<property name="zipCodeService" ref="zipCodeService"/> // 아래 오렌지색 처리된 zipCodeService를 참조함

</bean>

<bean id="zipCodeService" class="projectName.common.service.ZipCodeService">

<property name="zipCodeDao" ref="zipCodeDao"/>

</bean>


<!-- DB 연결 설정 -->

<bean id="nativeJdbcExtractor" class="org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor" lazy-init="true"/>


<bean id="oracleLobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true">

  <property name="nativeJdbcExtractor"><ref local="nativeJdbcExtractor"/></property>

  </bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean" >

<property name="configLocation">

<value>/WEB-INF/config/sqlmap-config.xml</value> //iBatis Sql Mapping 연결설정

</property>

<property name="dataSource">

<ref bean="dataSource" />

</property>

<property name="lobHandler" ref="oracleLobHandler" />

</bean>

<!-- jndi -->

<bean id="jnditemplate" class="org.springframework.jndi.JndiTemplate" >

  <property name="environment">

      <props>

        <prop key="java.naming.factory.initial">jeus.jndi.JNSContextFactory</prop>

        <prop key="java.naming.provider.url">localhost:9736</prop>

      </props>

  </property>

</bean>

<!-- Jeus Server use ConnectionPoolingDataSource -->

<bean id="transactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager"

p:dataSource-ref="dataSource" />

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName" value="JNDI_NAME"/>

<property name="jndiTemplate" ref="jnditemplate"/>

</bean> 

</beans>



sqlmap-config.xml

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE sqlMapConfig

PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"

"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">


<sqlMapConfig>

<settings enhancementEnabled="true" useStatementNamespaces="true" />


<!-- common sqlMap -->

<sqlMap resource="xmlfile.xml" />

... (필요한 수 만큼 반복) ... 

</sqlMapConfig> 




xmlfile.xml

<?xml version="1.0" encoding="UTF-8"?>


<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"

    "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="BoardFile">

<typeAlias alias="fileVo" type="projectName.model.FileVo"/>

<typeAlias alias="map" type="java.util.Map"/>

<resultMap id="_resultMap" class="fileVo" >

<result column="FILE_NM" property="fileNm" />

<result column="FILE_PATH" property="filePath" />

</resultMap>

<select id="fileInfo" resultMap="_resultMap" parameterClass="fileVo">

SELECT *

FROM

TABLE_NAME

WHERE

SEQ = #seq#

<isNotEmpty property="requestParameter"> //넘겨 받은 값에 의한 판별

AND MENU_CD = #requestParameter#

</isNotEmpty>

</select>

</sqlMap> 



applicationContext 를 설명할 때, 단순히 기술 방식만 언급하는게 아니라 이를 클래스나 메소드에서 어떻게 활용하는지를 함께 언급해야 할 것이다.


위에서 보여준 그림을 다시 보면서 얘기하자면..

IoC 컨테이너를 통해 애플리케이션이 만들어지는 방식 (본문 p772)



위에서 잔뜩 나열한 xml 파일들은 모두 메타정보리소스에 해당하는 내용들이다. 이를 스프링의 메타정보리더가 읽어들여 설정메타정보로 바꾼다. 이 정보가 IoC 컨테이너가 되려면 POJO 클래스와 함께 사용되어야 한다. 즉, applicationContext에서 설정된 내용들을 POJO 클래스에서 불러와 활용해야 한다.

하지만 POJO 클래스에서 메타정보를 활용하는 방법에 대해서는 이미 1부에서 학습했기 때문에[각주:3] 실제 적용된 applicationContext 파일을 소개하는 선에서 마무리하자.



마지막으로, 이 장에서 언급된 개념 중 특히 중요한 항목들을 나열해보자.


1. 스프링의 빈 등록 방법: XML, 빈 자동인식, 자바 코드에서 등록(XML + 자동인식이 자주 쓰인다)

2. 스프링의 빈 의존관계 설정 방법: XML, 애노테이션, 자바 코드에서 등록(XML + 애노테이션이 자주 쓰인다)

3. 스코프: 빈의 존재 범위. 싱글톤, 프로토타입, 기타 스코프(요청request, 세션session, 글로벌세션globalSession, 애플리케이션application : 모두 웹 전용이다)
 




아.. 졸리다.


  1. 대부분 아는 내용이었으니까 [본문으로]
  2. 스프링의 작동 원리에 대해 이야기하고 있었으므로 [본문으로]
  3. 더구나 자바 클래스 파일의 작성법을 다룰 수는 없지 않은가? 이것은 어디까지나 스프링에 대한 학습서 내용을 간추린 것이니까. ㅎㅎ;; [본문으로]
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

이 책은 1장 부터 8장까지를 1부로, 나머지 9장부터 16장까지를 2부로 하고 있다.
1부에서 자바와 스프링 프레임워크에 대한 이론적인 접근을 하고 있다면, 2부에서는 이를 실전에서 어떻게 활용하는가를 다루고 있다.
그러므로 8장과 9장을 한번에 정리한다는 것은 일견 잘못된 것이 아닌가 생각할 수 있다.
하지만 1부 마지막 장에서 스프링 프레임워크에 대해 정리하고, 이 정리된 내용을 2부 첫 장에서 다시 다루고 있기 때문에 이 두 장을 한번에 정리하는 게 좋을 것이다. 


8장. 스프링이란 무엇인가?

'스프링이란 무엇인가?' 라는 화두를 놓고 책에서 수십쪽에 걸쳐 다루었는데, 이를 다시 몇개의 문장으로 정리한다는 것은 쉬운 일이 아니다.
하지만 누군가, 스프링이 뭐냐고 물었을 때 『toby의 스프링』8장(649p)을 참조하라고 대답해 줄 수는 없지 않은가?
그래서 대충이라도 정리를 해 놓자면..

책에서 언급한대로 스프링이란 자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크 이다.

물론 이 문장을 듣고 바로 이해하는 사람이라면 스프링이 뭐냐고 물을 일도 없었을 것이다.
실제로 저 말을 제대로 알아 들으려면 아주 긴 학습의 시간이 필요할 것이다.
또한 나를 비롯해 스프링을 이용해 개발하는 많은 개발자들 또한 스프링의 개념을 완벽히 숙지하고 있지는 못 할 것이다.(안타깝게도)

어떻든.. 정리를 위한 정리라도 해 보자면.

스프링이 도입되기 전 기존 자바 개발 환경은 매우 복잡하고 어려웠다. 실무에서는 비즈니스 로직 처리하기도 어렵지만, 이 비즈니스 로직을 트랜잭션이나 동기화, XML 인터페이스 등 다양한 외부 기술과 접목하고 시스템의 각 장치와 연결하는 등의 타겟 애플리케이션 外적인 부분의 처리까지 개발, 관리해야 했기 때문이다. 이에 대항하기 위해 EJB 가 도입되었지만, 이는 EJB의 통합 환경에만 국한되어 사용할 수 있어 폐쇄성의 문제 때문에 근본적인 해결이 될 수 없었다. 


지나치게 장황한 설명이지만, 실무를 해 본 사람이라면 충분히 이해할 수 있는 이야기일 것이다.
하다 못해 아주 간단한 개발에서라도 DB 연결, 트랜잭션, 레코드셋의 연결과 이를 다시 개체에 담고 비즈니스 로직에 따라 사용자에게 전달하기까지 얼마나 많은 것들을 신경써야 했는가?

무엇보다 개발은 어찌어찌 됐다 쳐도 나중에 유지보수 할 때의 고달픔은 겪어보지 않은 사람은 상상도 할 수 없다.
말이 유지보수지 실은 재개발이었던 것이다.
그 때문에 얼마나 많은 밤들을 새웠던가, 망할...[각주:1]


이를 해결하기 위해 고안된 것이 스프링이다.

스프링의 핵심 기술은 IoC/DI, AOP, 서비스 추상화를 통해 POJO(Plain Old Java Object)를 구현하는 것이다. 
이는 앞장에서 누누히 말했던 내용들로, 결합도는 낮추고 응집도는 높인다는 자바 개발의 기본 원리에 충실하게 설계된 자바 오브젝트를 가지고 개발하는 것이다.

이렇게 스프링을 활용하면 개발자가 업무 외 다른 기술이나 기능들에 대해 신경쓰지 않고 오로지 개발에만 집중할 수 있다.



9장. 스프링 프로젝트 시작하기

본격적으로 스프링 프로젝트를 시작하기 앞서, 개발 도구에 대한 안내 및 1부에서 얘기한 주요 원리들을 어떻게 설계에 반영시켜야 하는가에 대해 이야기하고 있다.

개발도구와 개발환경에 대한 이야기는 딱히 정리할 필요가 없을 것 같아 skip 하기로 한다.

다음으로 스프링 프레임워크를 이용해 개발할 때 설계 시 고려할 점에 대해 많은 이야기들을 하고 있다.
이를 간략하게 정리하면 이런 것이다.

Application을 개발할 때는 Application이 다루는 정보를 관점으로 데이터 중심 설계와 오브젝트 중심 설계로 나눌 수 있다. 스프링 프레임워크를 이용해 개발 할 때는 데이터 중심 설계가 아닌 오브젝트 중심 설계를 이용해야 한다.


다시 말하면..

모든 Application은 결국 DB의 정보나 기타 특정 정보를 가공하여(또는 그대로), 비즈니스 로직에 따라 사용자에게 전달하는 것을 목표로 하고 있다.

그런데 스프링을 이용하지 않던 기존의 개발에서는 데이터를 중심으로 설계를 한다.
보통 DB를 사용한다면 메소드에서 SQL 질의를 통해 얻어 낸 정보를 List나 기타 오브젝트에 담는데 비즈니스 로직에 따라 그때 그때 다른 SQL 질의를 날리거나, 아니면 정보를 잔뜩 가져와서 오브젝트에 담은 다음 그걸 가지고 재처리를 한다거나 하는 방식이다. 

전자의 경우에는 DB에 너무 종속된 개발을 하게 될 우려가 있어 사후 변경이나 유지보수가 어려워지는 단점이 있다. 또 프로그램 규모가 커지면 커질 수록 SQL 질의의 수는 폭발적으로 증가하는 특성 상 프로그램 자체가 몹시 복잡해지기도 한다.
또 후자의 경우에는 자칫 리소스를 낭비하게 될 우려가 크다. 애플리케이션의 각 기능들에서 다루는 정보들은 모두 같지 않기 때문이다.

이를테면 게시판을 만든다고 할 때 목록을 표시할 때의 정보와 내용보기를 할 때의 정보는 서로 다를 수 밖에 없는데 이를 매번 각각의 오브젝트로 만들어 낸다면 전자의 경우일 것이고, 몽땅 한 오브젝트에 담아 목록 표시할 때도 쓰고, 내용보기 할 때도 쓴다면 리소스의 낭비를 부르는 후자의 경우가 될 것이다. 수만건의 게시물이 있는 게시판의 목록을 표시할 때 이런 방식을 사용한다고 생각해 보자. 그야말로 끔찍한 일이 될 것이다.[각주:2]

이에 대한 대안으로 지연된 로딩 기법(lazy loading)[각주:3]을 이용한 오브젝트 중심 설계를 들 수 있다. 하이버네이트와 같은 ORM 기술을 이용하면 쉽다. 



이렇게 오브젝트 중심 설계는 다시 오브젝트의 활용 방법을 기준으로 빈약한(anemic) 도메인 오브젝트 방식과 풍성한(rich) 도메인 오브젝트 방식이 있는데 이에 대해서는 그림을 참조하기로 한다.

그림 9-20. 빈약한 도메인 오브젝트 방식 (본문 p744)



그림 9-21. 풍성한 도메인 오브젝트 방식. 



풍성한 도메인 오브젝트 방식을 쉽게 설명하면, DB에서 조회한 정보를 그대로 오브젝트에 담는 것이 아니라, 서비스 상 필요한 다양한 파생 정보들을 미리 도메인 오브젝트에 할당해 놓는 것을 말한다. 이를테면 어떤 쿼리의 결과에 대한 목록 수를 집계할 때, 별도로 count(*) 질의를 날리거나 불러 온 목록의 length를 집계하거나 하는 방법을 사용한다. 이를 코드화하면 비즈니스 로직에서 많은 별도의 처리가 필요해질 것이다.[각주:4] 하지만 이를 도메인 오브젝트로 처리한 후 서비스 계층에서 .getCount() 와 같이 오브젝트의 속성값을 불러 오는 정도로 쓸 수 있다면 코드가 간결해지고 소스 관리가 간편해질 것이다.

하지만 풍성한 도메인 오브젝트 방식의 경우 설계 전에 이런 것들이 모두 고려되고, 각 개발자가 이렇게 사전에 정의된 오브젝트를 숙지하지 않는다면 각 모듈에서 계속 새로운 메소드나 오브젝트를 생성하게 되어 소스의 복잡도만 가중시키는 결과를 초래할 수도 있다. 이래서는 풍성한 도메인 오브젝트를 사용하는 것이 프로젝트의 발목을 잡는 이유가 될 수도 있다.


이런 한계들을 극복하기 위한 대안으로, 도메인을 계층으로 끌어 올리는 방안이 있다.

스프링은 도메인 오브젝트를 빈으로 관리하지 않으므로 AOP를 이용해 DI를 적용하고, 이렇게 생성된 도메인 오브젝트에 도메인과 관련된 제처리를 하게 한다. 도메인 오브젝트를 DI받을 수 있으므로 3계층과 도메인 계층은 서로 원활하게 소통할 수 있게 된다. 따라서 도메인 계층이 서비스 계층의 일부 처리를 떠안기도 하고, 서비스 계층과 협력하여 보다 복잡한 처리를 할 수도 있다. 개발자는 자바의 오브젝트를 불러들이는 것만으로 DB의 정보를 조작할 수 있게 된다.

이런 경우 자바 오브젝트에서 그저 Object.addObject(ObjectData); Object.deleteObject(ObjectKey); 하는 것만으로 DB 정보의 저장이나 변경, 삭제도 가능해진다. (권한이 있는 경우) 그런데 각 개발자가 모두 이런 접근권한을 가지게 된다면, 또 모든 소스에서 공통적으로 DB에 같은 권한으로 접속을 하게 된다면 이는 보안 상 심각한 위협이 될 수도 있다. 물론 개발자가 보안 관련 규칙을 준수한다면 문제가 없겠지만, 모두가 규정을 준수할 것이라고 믿는 것은 지나치게 낙관적인 일이다.

따라서 AspectJ 를 이용해 특정 계층의 오브젝트가 사용할 수 있는 메소드의 범위를 지정해 주거나, 도메인 오브젝트가 도메인 계층을 벗어나지 못하도록 제한하는 방법이 있다. 도메인 오브젝트를 제한할 때는 읽기 전용의 DTO(Domain Transfer Object) - 정보 전달용 도메인 오브젝트를 생성해 이 오브젝트가 도메인 오브젝트를 대신해 정보를 전달하도록 한다. 실제 DB 정보를 안전하게 보호하기 위함이다.


이런 기술들이 실제 개발에서 어떻게 활용되는지는 11장 이후에서 다뤄질 것이다.






매번 학습 내용을 정리할 때 마다 느끼는거지만...
프로그램을 말로 표현하는 것, 그것도 짧게 정리한다는 건 참 어려운 일이다.
다섯살 아이도 이해할 수 있도록 할 수 없다면 그건 확실히 아는게 아니라고 했던가?
하지만 그렇다면 그건 이 책의 저자도 마찬가지일 것이라는.. 궁색한 변명을 해 본다. -ㅅ-;










  1. 물론 스프링이 이 모든 것들을 확 해결해 줄 리는 없다. 그 정도를 기대하는 것은 아니다. 다만 조금 더 편리해 지기를바라는 것이지. 더구나 실제 현업에서 개발할 때는 스프링을 그냥 공식처럼, 일종의 패턴에 따라 기계적으로 적용하기도 한다. 실제로 나는 왜 그렇게 하는지도 모르고 그냥 Ctrl + C, Ctrl + V로 사이트 하나를 구축해 내는 사람을 보기도 했다. 아무리 좋은 연장이라도 그걸 쓰는 사람에 따라 보도(寶刀)가 되기도 하고 부엌의 식칼이 되기도 하는 법 - 대개의 경우 문제는 기술이 아니라 사람에게 있지 않을까? [본문으로]
  2. 한 10년도 더 된 옛날, 실제로 이렇게 프로그램을 짜던 사람들이 있었다. 목록을 불러 올 때 select * from ~ 해 버리는 용자들이.. -ㅅ-; [본문으로]
  3. 지연된 로딩 기법이란 일단 최소한의 정보를 가져온 후 필요에 따라 추가 정보를 다시 가져오는 방법을 말한다. [본문으로]
  4. 도메인 오브젝트는 스프링에서 관리하는 빈이 아니므로 3계층의 오브젝트를 사용할 수 없기 때문에 데이터 액세스 계층이나 서비스 계층과 정보를 주고 받기 위해서는 비즈니스 로직에서 직접 DAO를 건드려야 하는 부담이 따른다. [본문으로]
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요


이전 장까지 스프링의 핵심 기술인 IoC/DI, 추상화, AOP에 대해 학습했다. 그리고 이번 장에서는 이러한 스프링의 제반 기술들을 응용하는 방법을 소개한다.
누차 강조하지만, 스프링이 만들어진 이유, 스프링을 사용해야 하는 이유는 결국 OOP를 위함이다.
따라서 모듈을 설계할 때는 반드시(라 불러도 좋을 만큼) 인터페이스를 기본으로 하는 DI를 적용하고, 특정 기술이나 특정 상황에 한정해 작성하지 않고 추상화를 적용한다.

특히 이 장에서는 스프링에 기본적으로 내장되어 있는 여러가지 내장 기술들을 적용하는 방법을 제시한다.


1. SQL을 코드에서 분리하기

먼저 SQL 구문을 코드에서 분리해 별도의 파일로 저장해 두는 상황을 가정해 보자.

아주 무식하게 생각하면.. 각 SQL 구문을 탭과 캐리지리턴으로 Key와 SQL로 구분하여 txt 파일로 저장한 다음 해당 텍스트를 스트림으로 읽어들인다음 배열이나 Map에 저장하는 방법이 있을 것이다. 실제로 ASP에서는 이러한 시도들이 있었고, SQL이 아니더라도 개발자마다 자신들의 개발 환경이나 기타 중요한 정보 등을 텍스트로 저장해 둔 다음 불러 들여 사용하곤 했다.

그런데 텍스트 파일은 관리와 식별이 용이하지 않다. 이런 경우에는 당연하게도 XML이 제격이다.

XML을 불러 들이는 방법에는 여러가지가 있는데 책에서는 JAXB를 먼저 소개한다. (JAXB를 사용하여 XML을 불러 들이는 방법에 대해서는 책에 비교적 자세히 나와 있다.)

여기서, 서버가 XML을 로드하는 시점이 언제인지를 생각해 볼 필요가 있다. 만약 각 메소드에서 SQL을 요청할 때 마다 XML을 로드한다면 자원의 낭비가 막심할 것이다. 코드 내에 if 명령을 이용해 내용이 있으면 불러 들이고, 없으면 로드하라는 식으로 기술할 수도 있지만 이런 경우 코드가 복잡해지고, 매번 XML을 로드하는 생성자가 호출됨에 따라 오류가 생길 여지가 높아진다. 따라서 XML의 로드는 별도로 처리될 필요가 있다. 그리고 이렇게 생성된 빈은 스프링이 초기화될 때 자동으로 로드하게 하면 된다.(앞선 6장에서 언급한 프록시 자동생성기와 같은 '후처리기'를 이용한 것이다.)

스프링의 환경설정 문서에 다음과 같이 코드를 변경한다.

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/beans 

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

http://www.springframework.org/schema/aop 

http://www.springframework.org/schema/aop/spring-aop-3.0.xsd

http://www.springframework.org/schema/context 

http://www.springframework.org/schema/context/spring-context-3.0.xsd

http://www.springframework.org/schema/tx 

http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

<context:annotation-config /> // 이 태그를 통해 빈 후처리기가 동작하고, 이에 따라 새로운 애노테이션을 사용할 수 있게 된다.
...(중략)


이후 사용 방법은 간단하다.
아래와 같이 SQL이 기술된 XML을 로드해야 하는 메소드의 바로 위에 @PostConstruct 라는 애노테이션을 추가하기만 하면 된다.

...(중략)
@PostConstruct

public void loadSql() {

this.baseSqlService.setSqlReader(this.oxmSqlReader);

this.baseSqlService.setSqlRegistry(this.sqlRegistry);

this.baseSqlService.loadSql();

}

...(중략)




2. OXM 서비스 추상화

자바에는 JAXB 외에도 XML을 자바오브젝트와 연결하는 다양한 방법이 있다. 
또, XML 파일의 위치는 언제든 변경될 수 있기 때문에 이런 상황에 대비한 모듈의 설계가 필요하다.

바로 추상화가 필요하다는 뜻이다.

먼저 JAXB를 대체하는 다른 기술들을 손쉽게 적용할 수 있도록 만들기 위해서는 XML을 로드하는 부분을 따로 추출해 클래스로 만들고 다른 클래스에서 이를 사용할 때는 인터페이스를 통해 사용하도록 만든다. 그렇게 n개의 기술에 대해 n개의 클래스 또는 인터페이스로 분리하면 특정 기술에 종속되지 않는 유연한 코드를 작성할 수 있게 된다.

장황하고 막연한 이야기처럼 보이지만, 이 역시 경험을 통해 직관으로 이해할 수 있는 내용이다.


또 XML 파일의 위치를 자유롭게 지정하기 위해서는 Resource 추상화가 필요하다. 자바에서는 리소스에 대해 단일화된 접근 인터페이스를 제공하지 않지만, 스프링에서는 제한적이나마 이에 대한 인터페이스를 제공한다. 

자세한 이야기를 파고 들어 가다보면 의외로 좀 복잡한데.. 적용 예시를 보면 아주 간단하다.


...(중략)

import
javax.annotation.PostConstruct;

import javax.xml.transform.Source;

import javax.xml.transform.stream.StreamSource;


import org.springframework.core.io.ClassPathResource;

import org.springframework.core.io.Resource;    //스프링의 Resource 를 가져온다.

import org.springframework.oxm.Unmarshaller;



...(중략)

private class OxmSqlReader implements SqlReader {

private Unmarshaller unmarshaller;

private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);


public void setUnmarshaller(Unmarshaller unmarshaller) {

this.unmarshaller = unmarshaller;

}


public void setSqlmap(Resource sqlmap) {    // 자료 타입으로 Resource 를 쓰기만 하면 된다. 

this.sqlmap = sqlmap;

}


public void read(SqlRegistry sqlRegistry) {

try {

Source source = new StreamSource(sqlmap.getInputStream());

Sqlmap sqlmap = (Sqlmap)this.unmarshaller.unmarshal(source);

for(SqlType sql : sqlmap.getSql()) {

sqlRegistry.registerSql(sql.getKey(), sql.getValue());

}

} catch (IOException e) {

throw new IllegalArgumentException(this.sqlmap.getFilename() + "을 가져올 수 없습니다", e);

}

}

}



그리고 sqlmap의 문자열은 어플리케이션컨텍스트에 아래와 같이 지정한다.

applicationContext.xml

...(중략)
<bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">

<property name="unmarshaller" ref="unmarshaller" /> 

<property name="sqlmap" value="classpath:/springbook/user/dao/sqlmap.xml" />    // sql 구문이 저장되어 있는 경로를 기술한다. 

</bean>

<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">

<property name="contextPath" value="springbook.user.sqlservice.jaxb" />

</bean>
...(중략) 



이때 sqlmap의 문자열에 대한 접두어는 다음과 같다.

 접두어  예시
 file: file:c:/temp/file.txt 
 classpath: classpath:file.txt (클래스패스의 루트를 말한다.)
 http: http:www.domain.com/file.txt 
 없음 접두어가 없는 경우에는 서블릿컨텍스트의 루트를 기준으로 한다. 따로 지정도 가능하다.


또 XML을 저장할 때 HashMap이 아니라 ConcurrentHashMap을 사용하면 SQL 코드의 수정이 자유로워진다.


3. 내장 데이터베이스 사용하기

XML 파일을 불러 들이는 방법 외에도, 내장 데이터베이스를 사용하는 방법도 있다. SQL 구문 내용의 업데이트와 이에 대한 트랜잭션 등의 기능을 기본적으로 지원한다는 점에서 내장 DB의 사용은 권장될 만 하다.

먼저 SQL, 또는 기타 주요 정보를 저장해 놓을 테이블의 생성 스크립트를 .sql 파일로 만들어 서버의 특정 위치에 저장한다.
또 해당 테이블에 들어갈 초기 정보 역시 insert 구문으로 저장한다.
JUnit 테스트 시에는 스프링이 제공하는 내장DB를 호출해 사용한다. 사용 후 내장DB를 닫아줘야 하는 경우에는 .close() 구문을 이용해 DB를 닫는다.

applicationContext.xml

<bean id="sqlRegistry" class="springbook.user.sqlservice.updatable.EmbeddedDbSqlRegistry">

<property name="dataSource" ref="embeddedDatabase" />

</bean>


<jdbc:embedded-database id="embeddedDatabase" type="HSQL">

<jdbc:script location="classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql"/>

</jdbc:embedded-database>



EmbeddedDbSqlRegistry.java

 public class EmbeddedDbSqlRegistry implements UpdatableSqlRegistry {

SimpleJdbcTemplate jdbc;

TransactionTemplate transactionTemplate;

public void setDataSource(DataSource dataSource) {

jdbc = new SimpleJdbcTemplate(dataSource);

transactionTemplate = new TransactionTemplate(

new DataSourceTransactionManager(dataSource));

transactionTemplate.setIsolationLevel(TransactionTemplate.ISOLATION_READ_COMMITTED);

}

public void registerSql(String key, String sql) {

jdbc.update("insert into sqlmap(key_, sql_) values(?,?)", key, sql);

}


public String findSql(String key) throws SqlNotFoundException {

try {

return jdbc.queryForObject("select sql_ from sqlmap where key_ = ?", String.class, key);

}

catch(EmptyResultDataAccessException e) {

throw new SqlNotFoundException(key + "에 해당하는 SQL을 찾을 수 없습니다", e);

}

}

}

...(중략) 


복잡한 설명을 다 이해하지 못하더라도, 최종 적용된 코드를 보면 누구나 '음, 이렇게 흘러가는구나' 하고 알 수 있을 것이다.

아마 이런 것이 객체지향설계의 매력이 아닐까?
 
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. 재인

    음.. 이렇게 왜 코드가 짜진거지... 라고 생각이 드는건..

    2012.10.19 09:52 신고 [ ADDR : EDIT/ DEL : REPLY ]


이전 장에서 말한 것처럼 Spring에서 가장 중요한 3가지 개념 중 하나인 AOP(Aspect Oriented Programming:애스펙스 지향 프로그래밍) 이다.
계속 말하지만, Spring이 추구하는 가장 중요한 개념은 서비스 추상화이다. (이것은 결국 객체지향 프로그램의 근본적인 목표이기도 하다.)
그리고 AOP 역시 서비스 추상화의 한 축을 담당하고 있는 것이다.

일반적으로 서비스 추상화를 위해서는 IoC/DI 개념을 바탕으로 인터페이스를 사용하면 된다.
하지만 책에서 소개된 바와 같이 트랜잭션과 같은 경우 각 메소드에 강한 응집력을 가지고 있지만, 해당 메소드의 코드 내에 트랜잭션 관련 코드를 매번 삽입해야 하므로 낮은 결합도를 유지할 수 없다. 따라서 일반적인 방식의 서비스 추상화를 이루기가 어렵다.
AOP는 이에 대한 효과적인 대안을 제시한다.
다시 말해, AOP란 "전통적인 객체지향기술의 설계방법으로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가 기능을 어떻게 모듈화 할 것인가"(본문 p.506)를 고민한 끝에 나온 OOP(Object Oriented Programming)의 보완적 개념이다.
 
사실 말로 하면 참 어렵게 느껴지지만.. 실제로 개발 경험이 있는 사람들은 이게 뭔 소린지, 그리고 이 개념이 얼마나 중요한 일인지 알 것이다.
이런 기술이 없는 전통적 방식의 개발에서는 Copy & Paste 와 Find & Replace, 그리고 "개발자의 기억력"이 프로젝트에 지대한 영향을 미칠 수 밖에 없었다.
하지만 Spring에서 제시하는 방법들을 사용하면 코드를 획기적으로 개선할 수 있다. 


<그림 6-21> 독립 애스펙트를 이용한 부가기능의 분리와 모듈화. (본문 p.507)



어렵게 들리지만, 사실 실제 개발에 들어가면 어느 정도 기계적 적용이 가능한 부분들일 것이다. 
책에서 다루고 있는 AOP를 이용한 트랜잭션 적용 방법 역시.

처음 Spring 책을 고를 때, 중급 이상 개발자들에게 추천한다는 어떤 독자의 서평이 생각이 난다. 꼭 중급일 필요는 없겠지만, 개발 경험이 없거나 한 사람들에게는 참 어렵지 않을까 싶다. 물론.. 개발 경험이 있다고 다 쉽게 읽힐 리는 없다. 응? -ㅅ-;


이 장에서는 프록시, 다이내믹 프록시, 프록시 패턴, 프록시 팩토리 빈, 포인트 컷, AspectJExpressionPointcut(AspectJ 포인트컷 표현식), 타입 패턴 및 클래스 이름 패턴, 어드바이스, 조인 포인트, 어드바이저, 애스펙트, AOP 네임스페이스 등 다양한 용어와 개념, 기술이 쏟아져 나온다. 이를 모두 완벽히 숙지한다면 참 좋겠지만, 주관식 시험 보는 것도 아니고, 그럴 필요는 없을 것이다. 각 개념들이 AOP에서 어떻게 작동하여 OOP를 보완하는가를 이해하는 것이 더욱 중요할 것이다.
물론 사용법을 숙지해야 한다는 것은 두 말 할 나위가 없을 것이다.



이번 장은 워낙 내용도 많고 다뤄진 개념도 방대하다.
블로그에 올리는 학습정리는 이 정도면 충분하지 않을까 한다.
각 기술의 실제 적용 코드는 학습이 진행될 수록 알아서 정리될 것이다.

 
저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. 쌍용

    쌍용 왔다감~

    2011.11.10 14:48 신고 [ ADDR : EDIT/ DEL : REPLY ]


Spring에서 중요한 3가지 개념이 있는데, 바로 IoC/DI, 서비스 추상화, AOP 이다.
이 중 JAVA라는 객체지향 프로그램과 이를 이용한 Spring 프레임워크가 지향하는 가장 중요한 개념을 꼽으라면 바로 서비스 추상화일 것이다.
(심지어 IoC/DI는 이 서비스 추상화를 용이하게 하기 위한 부차적인 개념이라고 봐도 무방하다는 생각이다.)

추상화란 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수가 있다.(본문 p.371)

다시 말해 서비스 추상화를 적용한다는 것은, 코드들을 비슷한 것들끼리 모으고(강한 응집력) + 서로 다른 코드들이 서로를 구속하지 않는(낮은 결합도) 프로그램 개발을 말하는 것이다. 그리고 이를 위해 Spring은 쉽고 편리한 여러가지 방법들을 제공한다.

그 중 가장 중요한 것은 이미 앞에서 학습했던 IoC/DI이다.
이 의존주입을 이용해 각 코드들을 독립적으로 개발하고, 이 코드들이 프레임워크 상에서 서로 연결되도록 하여 서비스 추상화를 제공하는 것이다.

이 장에서는 예제로 Spring에서 제공하는 트랜잭션을 이용하는 방법, 개발 중 메일발송 테스트를 간단하게 적용하는 방법 등 IoC/DI를 이용해 서비스 추상화를 용이하게 하는 방법들에 대해서 다루고 있다.

※ 트랜잭션의 개념이나, 동작원리, Spring 트랜잭션의 사용방법 등에 대해서는 책에서 다룬 usage 외 별도의 설명은 생략한다.

UserService.java

package springbook.user.service;

import java.util.List;

import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.transaction.PlatformTransactionManager;    //Spring에서 제공하는 트랜잭션을 사용하기 위해 반드시 선언해야 한다.
import org.springframework.transaction.TransactionStatus;    //Spring에서 제공하는 트랜잭션을 사용하기 위해 반드시 선언해야 한다.
import org.springframework.transaction.support.DefaultTransactionDefinition;    //Spring에서 제공하는 트랜잭션을 사용하기 위해 반드시 선언해야 한다.

//다른 class import

더보기


public class UserService {
    public static final int MIN_LOGCOUNT_FOR_SILVER = 50; //여러 클래스에서 자주 사용되는 값은 스태틱 변수에 담아 처리하는 것이 좋다.
    public static final int MIN_RECCOMEND_FOR_GOLD = 30;

    //UserDao 및 MailSender, PlatformTransactionManager 등 외부 클래스를 사용하기 위해 생성자 및 수정자 메소드를 생성한다.
    private UserDao userDao;
    private MailSender mailSender;
    private PlatformTransactionManager transactionManager;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
 
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }
    //수정자 메소드 생성 완료


    public void upgradeLevels() {
        TransactionStatus status =  this.transactionManager.getTransaction(new DefaultTransactionDefinition());    //트랜잭션 선언
        try {
            List<User> users = userDao.getAll();
            for (User user : users) {
                if (canUpgradeLevel(user)) {
                    upgradeLevel(user);    //코드의 시인성을 높이기 위해 실제 처리는 upgradeLevel() 에서 진행한다.
                }
            }
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);    //트랜잭션이 실패한 경우 롤백한다.
            throw e;
        }
    }
 
    private boolean canUpgradeLevel(User user) {
        Level currentLevel = user.getLevel();
        switch(currentLevel) {                                  
            case BASIC: return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
            case SILVER: return (user.getRecommend() >= MIN_RECCOMEND_FOR_GOLD);
            case GOLD: return false;
            default: throw new IllegalArgumentException("Unknown Level: " + currentLevel);
        }
    }

    protected void upgradeLevel(User user) {
        user.upgradeLevel();
        userDao.update(user);    //실제 DB입력 처리는 UserDao에서 한다.
        sendUpgradeEMail(user);
    }
 
    private void sendUpgradeEMail(User user) {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(user.getEmail());
        mailMessage.setFrom("useradmin@ksug.org");
        mailMessage.setSubject("Upgrade 안내");
        mailMessage.setText("사용자님의 등급이 " + user.getLevel().name());
  
        this.mailSender.send(mailMessage);
    }
 
    public void add(User user) {
        if (user.getLevel() == null) user.setLevel(Level.BASIC);
        userDao.add(user);
    }
}


이때 UserDao 클래스는 DB입출력 로직 외 특별한 처리가 없으므로 생략한다.
아래 applicationContext.xml 에서 굵게 처리된 부분이 새로 추가된 내용이다.

test-applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.springframework.org/schema/beans 
                                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/springbook?characterEncoding=UTF-8" />
        <property name="username" value="spring" />
        <property name="password" value="book" />
    </bean>
 
    <bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
        <property name="dataSource" ref="dataSource" />
    </bean> 
 
    //아래와 같은 방법으로 userService 에서 DI받을 다른 클래스들을 정의한다.
    <bean id="userService" class="springbook.user.service.UserService">
        <property name="userDao" ref="userDao" />

        <property name="transactionManager" ref="transactionManager" />
        <property name="mailSender" ref="mailSender" />
    </bean>


    //메일 발송을 위한 bean 등록이다.
    <!-- 
    //실제 운영서버에서는 주석으로 표시된 부분을 사용한다.
    <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="mail.server.com" />
    </bean>
     -->
    <bean id="mailSender" class="springbook.user.service.DummyMailSender" />
  
 
    //Spring에서 제공하는 트랜잭션매니저를 사용하기 위한 bean 등록이다.
    <bean id="transactionManager
             class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" /> 
    </bean>

</beans> 


소스가 많이 추가된 것 같지만 결국 applicationContext.xml을 통해 서비스 추상화를 구현한다는 것이 핵심이다.
이때 중요한 것은 높은 응집도와 낮은 결합도를 준수하는 것이다.
이것이 핵심이다.

저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. 스프링... 공부하시나요?ㅋ return to the basic?
    스프링+하이버네이트가 짱인듯 ㅋㅋ

    2011.09.28 07:53 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 리턴은 애초에 거기 있었다는 걸 전제로 하는거 아니냐? ㅋㅋ

      2011.09.28 10:22 신고 [ ADDR : EDIT/ DEL ]
    • 제가 말한 의미의 Basic = Development

      2011.09.28 15:11 신고 [ ADDR : EDIT/ DEL ]
    • 쉬는 동안 생존을 위한 개발 말고.. 제대로 함 배워 볼까 하고 책 들춰 보고 있는 중이야.

      엉아한테 도움이 될만한 관련 소스나 자료, 언제든지 환영이다 케군아 ㅎㅎ

      2011.10.05 23:57 신고 [ ADDR : EDIT/ DEL ]


지난 번에 다니던 회사에서는..
경우에 따라(라고 쓰고 매번이라 읽어야 한다) 다른 사람들이 만들어 놓은 소스를 수정해야 할 필요가 있었다.
보통 프로젝트를 진행한 사람들은 따로 있고, 이 사람들이 일을 대충 해 놓고 사라지면 그 일을 마무리 하거나 버그를 수정해야 하는 건 모조리 내 몫이었기 때문이다.
대개 이런 경우 - 일이 하기 싫었는지, 아니면 시간이 턱없이 부족했는지, 그것도 아니면 애초에 실력이 부족한건지, 아니면 그 모든 이유가 복합적으로 작용했든지..
상식적으로 이해하기 어려운 코드들이 작성된 경우가 많았다.

그 중에서도 특히 잦은 것은, 예외처리가 되어 있지 않은 경우였다.
다수의 메소드가 하나의 프로세스를 이룰 때, 어느 하나에서 오류가 나는데도 그냥 프로그램은 흘러간다거나..
아니면 아예 별다른 처리를 하지 않아 Internal Server Error를 뿌리거나 하는 등 말이다.

서비스 중인 프로그램이 오류가 나는 경우, 애초에 내가 작성한 코드들이 아니다보니 오류의 원인을 찾기란 무척 어렵다.
프로세스를 하나하나 짚어 가며, 해당 프로세스에 엮여 있는 클래스며 메소드들을 하나씩 짚어 가며 오류를 찾는데..
일정 규모 이상의 프로젝트에서는 이게 보통 일이 아니다.
실제 소스 수정에 들어가는 시간이 1분인데, 이를 위해 한 시간 내내 오류의 원인을 찾아야 할 때도 부지기수다.

굳이 이런 기억들을 꺼내지 않더라도, 프로그램을 개발할 때 예외처리가 중요하다는 것은 두 말 할 나위가 없다.
하지만 다른 모든 것들이 그렇듯, 세상에 몰라서 못 하는 일은 별로 없다.
알아도 못 하는 일들이 많은 것이 문제일 뿐이다.
프로젝트를 진행하면서 개발하는 수많은 코드들에 대해 일일히 예외처리를 꼼꼼히 한다는게 쉬운 일은 아니기 때문이다.
(물론 저 위에서 말한 것처럼 아예 안 되어 있기도 어렵다. 저건 아예 프로그래머로서 기본이 안 된 문제다.)

서두가 좀 길었지만.. 아무튼 이 장에서는 올바른 예외처리란 무엇인가를 이야기 하고 있다.

책에서는 예외처리에 대해 이야기를 하기 전에, 자바 시스템에서 예외란 무엇이고 어떤 것들이 있는지를 설명한다.
이는 자바에 대한 이론적인 내용으로 이 학습정리노트에서 자세한 내용을 언급할 필요는 없어 보인다.
간단히 정리하면 예외란 체크예외언체크예외(런타임예외)가 있고, 체크예외는 자바 문법 상 무조건 예외처리를 해야 한다.
우리가 코드 개발 시 만나는 일반적인 예외들은 대부분 체크예외다.

보통 프로젝트를 진행할 때 자주 사용되는 예외처리를 하는 방법은 다음과 같다.

1. 메소드를 try{}catch{}로 감싼 후 에러메시지를 시스템 로그에 찍는 방법.
2. 메소드 선언부에 throw Exception 으로 예외를 던지는 방법.

하지만 이것은 그야말로 예외처리를 위한 예외처리일 뿐인 경우가 많다.

catch된 예외를 제대로 처리해 주지 않고 그냥 시스템 로그에 찍기만 하는 경우에는 오류가 났는데도 다음 프로세스로 이동해 버리는 문제가 있기 때문에 주의를 기울이지 않으면 상황이 심각해진 후에야 오류를 파악할 수 있기 때문이다. 또 메소드 선언부에서 예외를 던지도록 해 놓은 경우 예외를 받아 들이는 상위 메소드에서는 오류가 있었다는 사실 외 어떤 오류가 났다는 것을 파악하기가 몹시 어려운 문제가 있다. 이 역시 예외처리를 위한 예외처리일 뿐 예외상황에 대한 대비로는 적절치 않다.

저자는 예외 처리를 위해(특히 체크 예외에 대해) 다음과 같은 전략을 제시한다. (4장의 정리 내용 중 일부 발췌)

 
  • 예외를 잡아서 아무런 조취를 취하지 않거나, 의미없는 throws 선언을 남발하는 것은 위험하다.
  • 예외는 복구하거나 예외처리 오브젝트로 의도적으로 전달하거나 적절한 예외로 전환해야 한다.
  • 좀 더 의미있는 예외로 변경하거나, 불필요한 catch/throws를 피하기 위해 런타임 예외로 포장하는 두가지 방법의 예외 전환이 있다.
  • 복구할 수 없는 예외는 가능한 한 빨리 런타임 예외로 전환하는 것이 바람직하다. //포기하면 편하다.
  • 애플리케이션의 로직을 담기 위한 예외는 체크예외로 만든다. //로직 상에서의 문제는 복구가 가능하거나, 최소한 이런 이런 오류가 있다는 것을 관리자/개발자에게 알리고 사용자에게 양해를 구하는 메시지를 전달하는 정도의 예외처리는 반드시 있어야 한다는 것이다.
  • JDBC의 SQLException은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다. //쿼리문이 틀렸거나, 기타 다른 이유로 DB를 사용할 수 없을 때 이를 어플리케이션 단에서 스스로 예외를 복구한다는 것은 애초에 불가능하기 때문이다.
  • SQLException의 에러 코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환될 필요가 있다. //이를 위해 아래서 얘기하는 DataAccessException이 필요하다.
  • 스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다. //이번 장에서는 이 내용에 대해 대충 훑어보는 정도로 넘어 갔다. 11장에서 다시 자세히 설명한다고 한다.
  • DAO를 데이터 엑세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화된 예외로 전환이 필요하다. //간단히 말해 인터페이스로 구현해 스프링에서 DI를 관리할 수 있도록 하라는 뜻이다.


그 외 이 장에서 사용된 소스 코드 예제 등에 대해서는 별도로 언급할 필요가 없을 것 같다.

저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요


이번 장에서는 템플릿과 콜백의 개념, 그리고 이것들을 적용해야 할 상황과 이를 구현하는 방식에 대해 설명한다.
템플릿과 콜백은 객체지향 설계의 핵심 원칙인 개방폐쇄원칙과 깊은 관계가 있다.

개방폐쇄원칙

개발된 코드 중 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있을 때 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어 주는 원칙을 말한다. (본문 211p)

한마디로 말해서, 반복적으로 재사용 되는 코드를 별도로 묶어서 언제든 재활용할 수 있도록 만들어야 한다는 것이다.
사실 이게 객체지향의 핵심인데, 그 중 템플릿/콜백은 메소드나 클래스와는 달리 특정 코드의 일부분만 재사용한다. (엄밀히 말하면 '메소드나 클래스와는 달리'라는 표현 역시 잘못된 것이다.)
책에서는 자바의 동작 원리에 따라 템플릿/콜백의 구현에 대한 자세한 이야기가 나오지만, 이미 현업에서 대부분 체득하여 사용되는 개념이므로 대체로 한번 쭉 훑어 보면서 이해하고 넘어 갈 성질의 것이라 본다.

이번 장의 대부분이 위와 같은 내용에 대한 것이므로 별도로 축약하기는 어렵다. 따라서 맨 마지막 부분의 정리 내용을 한번 적어 내려가면서 마무리 하고자 한다. 앞으로 학습을 진행하면서 이와 같이 정리 부분만 복기하고 넘어가는 장이 종종 나올지도 모르겠다.


정리
  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
  • 일정한 작업흐름이 반복되면서 그 중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분은 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
  • 같은 애플리케이션 안에서 여러가지 종류의 전략을 다이내믹하게 구성하고 사용해야 한다면 컨텍스트를 이용하는 클라이언트 메소드에서 직접 전략을 정의하고 제공하게 만든다.
  • 클라이언트 메소드 안에 익명 내부 클래스를 사용하여 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다.
  • 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리해서 공유하도록 만든다.
  • 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용한다. 클래스 내부에서 컨텍스트를 사용할 때 컨텍스트가 의존하는 외부의 오브젝트가 있다면 코드를 이용해서 직접 DI 해 줄 수 있다.
  • 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.
  • 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.
  • 스프링은 JDBC 코드 작성을 위해 JdbcTemplate 을 기반으로 하는 다양한 템플릿과 콜백을 제공한다.
  • 템플릿은 한번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
  • 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고 받는 정보에 관심을 둬야 한다.


또한 이번 장에서 새로 언급된 내용 중 다음과 같은 개념이 있다.

제네릭스 타입 파라미터

제네릭스 타입 파라미터는 처음 인수를 선언할 때 이게 String 인지, Integer 인지 구분하지 않고 실제 사용할 때 정의할 수 있도록 하는 파라미터이다.
VB에서 사용하던 개념을 차용한 게 아닐까 싶다. 같은 기능을 수행하지만 인수나 반환값이 달라서 몇개씩 만들었던 메소드들을 하나로 합칠 수 있을 것이다.


JdbcTemplate

Spring에서 제공하는 JDBC의 표준 API(에 가까운) 템플릿이다.

jdbcTemplate.update(SQL_Statement) : 단순 구문 실행
jdbcTemplate.queryForInt(SQL_Statement) : Integer 반환. count(), sum()...
jdbcTemplate.queryForObject(SQL_Statement) : 1개 행의 RecordSet 반환
jdbcTemplate.query(SQL_Statement) : 다수 행의 RecordSet 반환

굳이 책에서의 설명이 없더라도, 코드 개발 시 중복 코드의 제거를 통한 코드 재사용은 매우 중요한 개념이다.
이때 제네릭스 타입 파라미터는 매우 유용한 수단이 될 것이다.
JdbcTemplate은 대표적인 템플릿/콜백 적용의 예가 될 것인데, 사실 현업에서는 거의 사용되지 않는다. Spring에서 이에 대한 확장 API로 SimpleJdbcTemplate를 사용하기 때문이다.

저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

프로그램을 개발할 때 테스트가 얼마나 중요한지에 대해서는 말해봐야 입만 아프다.
하지만 테스트가 얼마나 중요한지 아는 것과, 테스트를 실제로 수행하는 것과는 약간의 차이가 있다.
프로젝트를 진행할 때 당장 코드 써내려가기만도 바빠 죽겠는데 테스트까지 면밀하게 하기가 여간 어렵지 않기 때문이다.

통상적으로 웹프로그램 테스트를 한다고 하면 WEB UI위에 올려 놓고 하는 경우가 많다.
하지만 이는 클래스나 모듈에 대한 단위테스트로는 매우 부적절하다.
프로그램이 오류가 났을 때 이게 지금 개발한 클래스나 모듈의 문제인지, 브라우저의 문제인지, 서버의 문제인지 기타 다른 부분의 문제인지 알기가 쉽지 않기 때문이다.

따라서 작성코드에 대한 단위테스트는 WEB UI가 아닌 실행 가능한 자바 클래스 자체에서 수행해야 한다.
또 기존 코드와 구분하기 위해 별도의 클래스를 만들어서 진행하는 것이 좋다.
DB의 입출력 테스트라면, 개발된 클래스로 입력과 출력을 테스트해 보고 제대로 수행이 됐는지 알아 보는 식이다.
이러한 테스트를 각각 void main() 메소드를 이용해 진행할 수도 있겠지만 프로그램 규모가 커질 수록, 테스트할 대상이 많아질 수록 개별적으로 테스트 하는 것은 매우 어려운 일이다.
따라서 테스트를 할 때는 테스트 자동화 도구를 이용하는 것이 좋다.

대표적인 테스트 자동화 도구에는 JUnit 이 있다.
테스트 자동화 도구 JUnit을 이용할 때의 장점은 아래와 같다.

1. 테스트의 수행과 결과 확인이 용이하다.
2. 테스트를 위한 자원의 할당과 반환이 효율적이다.
3. 다수의 테스트에서 동일한 명령을 수행해야 할 경우, 해당 명령을 매번 복기하지 않고 미리 정의해 둔 것으로 재사용할 수 있다.


아래는 JUnit을 이용해 Spring 프레임워크에서 ApplicationContext가 하나만 만들어져서 재사용되는지 확인해보는 테스트이다.

package springbook.learningtest.junit;

더보기


@RunWith(SpringJUnit4ClassRunner.class)        //테스트 중 함께 수행할 class 파일이다.
@ContextConfiguration("junit.xml")         //ApplicationContext를 지정해 줄 수 있다.

public class JUnitTest {
        @Autowired ApplicationContext context;        //Autowired 명령을 통해 자동으로 할당한다.

        static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();
        static ApplicationContext contextObject = null;        //위에서 Autowired 된 context 와 비교를 위한 임시 변수

        @Test public void test1() {        //@Test Annotation을 삽입하면 JUnit에서 해당 메소드를 테스트 단위로 인식한다.
                assertThat(testObjects, not(hasItem(this)));        //assertThat()의 두 인수가 서로 같지 않으면 false를 반환한다.
                testObjects.add(this);
  
                assertThat(contextObject == null || contextObject == this.context, is(true));        //둘 중 어떤 것이라도 성립하면 true
                contextObject = this.context;
        }

        @Test public void test2() {
                assertThat(testObjects, not(hasItem(this)));
                testObjects.add(this);
  
                assertTrue(contextObject == null || contextObject == this.context);        //해당 조건이 맞으면 true, 아니면 fasle
                contextObject = this.context;
        }

       @Test public void test3() {
                assertThat(testObjects, not(hasItem(this)));
                testObjects.add(this);
  
                assertThat(contextObject, either(is(nullValue())).or(is(this.contextObject)));        //either(().or())는 둘 중 어떤 것을 만족해도 true
                contextObject = this.context;
        }

}


JUnit을 이용한 테스트든, 전통적인 방식의 WEB UI를 이용한 테스트든..
테스트의 중요성은 따로 말할 필요가 없다.

더 나아가 개발이 잘 됐는지 확인하기 위한 테스트로서가 아니라 TDD(Test Driven Development)로서의 테스트로까지 테스트의 역할을 확장할 필요가 있다.
TDD에 대해서는 따로 언급하지 않는다.




저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

클래스간에 의존관계를 주입하는 방법에 대해서 설명하고 있다.

클래스 모델이나 코드에서 각각의 클래스들간의 관계를 직접 지정하게 되면, 향후 클래스나 코드의 변경이 일어날 때 마다 관련된 모든 코드를 손봐야 한다.
이는 소프트웨어 개발 시 높은 응집도와 낮은 결합도를 고려해야 한다는 원칙에 위배된다.
따라서 클래스를 개발 할 때 IoC(Inversion of Control)과 DI(Dependency Injection)을 고려해 개발한다.

사실 뭐 이 정도는 그리 대단한 내용이 아니다.
스프링 아니어도 기존에 개발할 때 마다 늘 하던 일이기도 하다.(xml이나 배열등의 노가다로)
하지만 스프링을 쓰면 이런 작업들을 하나하나 수동으로 하는 대신, 스프링의 특정 기술을 쓰면 된다.
거기에 더해, 싱글톤 레지스트리를 자동으로 구현해 주는 덕분에 서버 자원 관리에 이점이 있다.(한번 로드 된 ApplicationContext는 계속 재사용된다.)


스프링에서 DI를 구현하는 방법은 여러가지가 있지만, 그 중에 가장 많이 사용 되는 것은 역시 XML을 이용한 방식이다.
XML의 샘플은 아래와 같다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
        <bean id="dataSource" class="org.spring.jdbc.datasource.SimpleDriverDataSource">
                <property name="driverClass" value="com.mysql.jdbc.Driver">        //JDBC로 사용할 클래스 이름
                <property name="url" value="jdbc:mysql://localhost/springbook">        //DB 연결 url 
                <property name="username" value="spring">        //DB 사용자 이름
                <property name="password" value="book">        //DB 사용자 비밀번호
        </bean>
        <bean id="userDao" class="springbook.user.dao.UserDao">        //프로그램 전체에서 불러 쓸 수 있다. id에는 중복값이 올 수 없다.
                <property name="dataSource" ref="dataSource">        //다른 빈과의 관계를 주입할 수 있다. (붉은 색으로 표시된 부분)
        </bean>
</beans>


그리고 각 클래스에서 이러한 의존성을 인식하게 하기 위해 아래의 라인을 추가한다.

ApplicationContext context= new GenericXmlApplicationContext("applicationContext.xml");        //xml의 경로는 클래스 root를 기준으로 한다.

만약 root에서부터 xml 파일의 경로를 찾기가 번거로운 경우 다음과 같은 방법을 사용해도 된다.

ApplicationContext context= new ClassPathXmlApplicationContext("applicationContext.xml","UserDao.class");        //UserDao.class 와 같은 경로라는 뜻


이렇게 하면 각 클래스 파일간의 의존성 관계를 스프링이 제어하도록 할 수 있다.



저작자 표시 비영리 변경 금지
신고
Posted by Kunner

댓글을 달아 주세요

  1. 정진철

    <property name="url" value="jdbc:mysql://localhost/springbook"> //JDBC JAR파일의 경로
    jar파일 경로가 아니라 DB url 아닌가요

    2011.11.08 15:45 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 네 말씀하신게 맞습니다.
      보통 써놓고 다시 안 읽어 보다보니.. 말씀하시기 전엔 미처 몰랐네요.
      고쳐 놓겠습니다. 감사합니다. ^^

      2011.11.08 20:28 신고 [ ADDR : EDIT/ DEL ]

사가현 LMS 건으로 다국어 관련 처리를 해야 했다.
요즘의 나는 나홀로 개발 중... 에효


Struts 에서 다국어를 지원할 수 있도록 만드는 message.properties 

JDK의 native2ascii.exe 를 이용해 간단히 글자를 유니코드로 컨버팅 할 수 있는데..

c:\> native2ascii.exe  원본글자파일명  생성대상파일명


일단 해당 언어의 OS 에서 해야 한다는게 포인트다.
일어를 지원하기 위해서는 반드시 일어 OS에서 해야 제대로 된 아스키코드가 생긴다.
다른 방법이 있을지도 모르겠으나.. 일단은 이렇게 처리.


이때 web-inf 설정은 다음과 같다.

<context-param>
    <description>Current language setting</description>
    <param-name>lang</param-name>
    <param-value>언어파일명</param-value>
</context-param>


내 참.. 이걸 몰라서 대체 얼마나 삽질을 했던지...
신고
Posted by Kunner

댓글을 달아 주세요

연휴가 끝나고 난 후 지금껏 한국인터넷진흥원의 프로젝트를 마무리 하느라 바빴다.
원래 내가 하던 프로젝트가 아닌데, 어떻게 하다보니.. 엉겹결에 프로젝트를 마무리 하는 일이 내 손에 들어왔지.
남이 하던 프로젝트, 그것도 끝물에, 인수인계도 제대로 못 받고 덜컥 투입된 덕분에 버벅임과 삽질의 연속이었지만..
어떻든 오늘로 고비는 모두 넘었다.

새벽 3시가 넘었다.
원래 내일 오전까지 일을 마치면 되니 내일 오전에 해도 되는 건데..
막상 내일 오전이 되면 맘급하고 할 것 같아 밤 늦게까지 작업해 그냥 끝내 버렸다.
이렇게 밤샘을 했으니 내일은 좀 늦게 일어나도 되겠지.
잘했다.

이제 남은 일은 산출물 정리 후 납품, 끝~ ^_^

이렇게 얼마 안 남은거 다 하고 가면 오죽 좋았을까. 쩝..




*
그래도 참 다행인건, 이 기간 동안 내가 쥐고 있던 다른 프로젝트들에서 별다른 이슈가 생기지 않았다는 거다.
행안부의 까다로운 고객도 어쩐 일인지 조용조용하고..
새로 제안 공고가 나올 사업도 내부 결재가 길어진 탓에 전혀 연락 없고.
덕분에 제대로 집중할 수 있었다.
일들이 모조리 겹쳐서 한숨 푹푹 쉬고 있었는데, 결국 죽으란 법은 없는거구나.


**
하지만 이렇게는 더 일하고 싶지 않다.
목구멍이 포도청인 탓에 당장은 어쩔 수 없다고 생각하지만..
이래갖고는 회사가 살기 전에 내가 먼저 죽을 것 같다.

회사 근무를 학업과 병행할 수 있다는 것은 대단한 배려인게 맞긴 하지만..
이런 강도의 업무는 나 아니라 누구라도 버텨내지 못할 것 같다.
턱없이 부족한 자원으로 온통 망쳐 놓은 일을 떠맡아 처리하는 일 - 한번이면 족한데 벌써 몇번이나 하고 있으니..

결국 사장님께 사직하겠다는 장문의 메일을 보내 놓았다.
이런저런 생각을 많이 하게 하는 요즘이다.

자꾸 위염이 심해진다.




아무튼, 수고했어!

신고
Posted by Kunner

댓글을 달아 주세요