Clean Code that Works.

정말 처음 봤던 내용.

=======================================================================================================================

Spring에서는 비동기 처리 및 스케쥴링 처리를 위해서 @Async, @Scheduled 를 지원하는데요.
아래처럼 간단한 설정으로 사용 하기도 합니다.

<task:annotation-driven />

위 설정으로 세팅하고 @Scheduled를 사용하는데, 사용하는 입장에서는 당연히 비동기로 동작할 것이라 생각합니다.
(특정 시간에 동작하도록 하거나, fixedDelpay를 사용할 때)

하지만! 아무 설정을 하지 않을 경우에는 실제 작업을 수행하는 bean이 ConcurrentTaskScheduler로 설정되어 concurrent 하게 수행됩니다!

해서 비동기로 수행하고 싶으면 아래 처럼 설정을 추가해주시면 됩니다.

<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
    <property name="poolSize" value="10"/>
</bean>
// or
<task:scheduler id="scheduler" pool-size="10" />

그럼 비동기로 동작하는 것을 확인 할 수 있습니다.

참고로 디폴트로 ConcurrentTaskScheduler를 등록해 주는 부분은
ScheduledAnnotationBeanPostProcessor.onApplicationEvent 에서 맨 마지막에 this.registrar.afterPropertiesSet() 를 호출해주는데
여기 들어가 보면 taskScheduler 가 null 일때 ConcurrentTaskScheduler로 세팅해주는것을 확인할 수 있습니다.

public void afterPropertiesSet() {
        if (this.taskScheduler == null) {
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
        }
        if (this.triggerTasks != null) {
            for (Map.Entry<Runnable, Trigger> entry : this.triggerTasks.entrySet()) {
                this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), entry.getValue()));
            }
        }
        if (this.cronTasks != null) {
            for (Map.Entry<Runnable, String> entry : this.cronTasks.entrySet()) {
                this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), new CronTrigger(entry.getValue())));
            }
        }
        if (this.fixedRateTasks != null) {
            for (Map.Entry<Runnable, Long> entry : this.fixedRateTasks.entrySet()) {
                this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(entry.getKey(), entry.getValue()));
            }
        }
        if (this.fixedDelayTasks != null) {
            for (Map.Entry<Runnable, Long> entry : this.fixedDelayTasks.entrySet()) {
                this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(entry.getKey(), entry.getValue()));
            }
        }
    }

감사합니다. :)


스프링 버전 : 4.0.0.RELEASE
스프링 시큐리티 버전 : 3.2.5.RELEASE

아직 root-context를 xml 설정을 사용하고 있어 시큐리티 부터 java config를 사용하기 위해 시작.

스프링 시큐리티 java config 링크

3년전만해도 스프링 시큐리티 문서 진짜 부족한게 많았는데.. 
이렇게 좋아 지다니.. 놀랍구나.

기존 XML 설정 

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="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.xsd
           http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security-3.2.xsd">

    <global-method-security pre-post-annotations="enabled" />
    <http use-expressions="true" authentication-manager-ref="authManager">
        <intercept-url pattern="/index.jsp" access="permitAll" />
        <intercept-url pattern="/" access="permitAll" />
        <intercept-url pattern="/home" access="permitAll" />
        <intercept-url pattern="/favicon.ico" access="permitAll" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/publish/**" access="permitAll" />

        <intercept-url pattern="/j_spring_security_logout" access="isAuthenticated()" />

        <intercept-url pattern="/user/updateUser" access="isAuthenticated()" />
        <intercept-url pattern="/user/info" access="isAuthenticated()" />

        <intercept-url pattern="/login/**" access="isAuthenticated()" />

        <intercept-url pattern="/gas/**" access="isAuthenticated()" />
        <intercept-url pattern="/payment/**" access="isAuthenticated()" />
        <intercept-url pattern="/etc/**" access="isAuthenticated()" />
        <intercept-url pattern="/stats/**" access="isAuthenticated()" />

        <intercept-url pattern="/secure/**" access="isAuthenticated()" />
        <intercept-url pattern="/manage/**" access="hasRole('ROLE_ADMIN')" />
        <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
        <intercept-url pattern="/comment/admin/**" access="hasRole('ROLE_ADMIN')" />

        <form-login login-page="/user/loginForm"    authentication-success-handler-ref="loginSuccessHandler" />

        <logout logout-url="/logout" logout-success-url="/home" />
    </http>

    <authentication-manager id="authManager">
        <authentication-provider user-service-ref="userDetailService">
            <password-encoder ref="passwordEncoder" />
        </authentication-provider>
    </authentication-manager>
</beans:beans>

거의 기본 설정이라서 아주 심플하다. 
딱히 특별한것도 없음..;

Java Config로 설정

@Configuration @EnableWebSecurity public class SecurityWebApplicationInitializer extends WebSecurityConfigurerAdapter { @Autowired private UserDetailService userDetailService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/index.jsp", "/home", "/favicon.ico", "/resources/**", "/publish/**").permitAll() .antMatchers("/secure/**", "/manage/**", "/admin/**", "/comment/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/user/loginForm") .loginProcessingUrl("/j_spring_security_check") .usernameParameter("j_username") .passwordParameter("j_password") .successHandler(loginSuccessHandler) .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/home") .and() .csrf().disable() .httpBasic(); } @Override protected UserDetailsService userDetailsService() { return userDetailService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

위에 링크해둔 스프링 시큐리티 java config 문서에도 친절하게 설명이 잘 나와 있다.
문서 보면서 천천히 해보면 더 좋을 듯.

딱히 특별한건 없고 xml 설정에서 사용하던 authentication-manager를 사용하기 위해서 configre(AuthenticationmanagerBuilder auth)를 세팅했다.

.formLogin() 부분에 보면 processingUrl이랑 username, password 파라미터를 세팅하도록 되어 있는데 이게 원래 기본으로 설정되어 있던거였는데 안되더라..

HttpSecurity.java 의 formLogin()을 보면 FormLoginConfigurer를 사용하는데 여기서 사용하는 default 값이 "username", "password" 이 두가지로 세팅되어 있다.
UsernamePasswordAuthenticationFilter.java 에선 여전히 "j_username", "j_password"를 사용하고 있어서 위처럼 값을 세팅해줘야 사용 가능하다.

그리고 .csrf() 이 설정은 CSRF 공격을 막기위해 세팅되어있는데 .disable()을 세팅해 놓지 않으면 해당 작업을 수행하기 위한 파라미터가 없다고 에러가 발생하기 때문에 .disable()을 해주도록 한다.

csrf().disable()을 안해주면 만나는 에러 
HTTP Status 403 - Expected CSRF token not found. Has your session expired?

이렇게 설정을 바꿈으로서 xml 설정보다 코드를 약간 더 줄일 수 있었고, 조금더 읽기 쉬운 코드가 됬다.



http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/remoting.html#remoting-httpinvoker

요약하면, HTTP Invoker를 사용하면 직렬화를 이용해서 원격 호출을 지원할 수 있다.

클라이언트 쪽에서 사용하기 위해서는 당연히 원격 호출을 위한 인터페이스가 필요하다. 이 인터페이스를 포함한 파일들을 서비스를 노출하는 서버에서 제공 해야 된다.(maven으로 jar 파일로 묶어서 클라이언트에 배포, impl은 노출시킬 필요가 없고 필요한 인터페이스만 노출 하면 된다.) 

위에 설정에서 처럼 httpInvokerProxy가 example.AccountService 타입의 bean을 생성해 주기 때문에(프록시로) 클라이언트로 사용할 때는

@Autowired private AccountService service; 형식으로 일반 Spring bean을 사용하는것처럼 사용하면 된다.

물론 내부적으로는 HTTP POST를 통해서 사용되는 거기 때문에 이를 인지하고 있어야 되고, 세부적인 설정이 필요 하다면 commons HttpClient를 httpInvokerRequestExecutor로 지정한 후에 여러가지 세팅을 지정할수 있다(timeout, connection pooling, 인증 등)

스프링에서 제공하는 remoting 기능중에 HTTP invokers 를 사용하는 방법이 있다.(위 문서를 간단히 번역, 날림 번역)

Burlap 과 Hessian을 사용하여 remoting 기능을 사용할 수 있지만 이 방법은 표준이 아닌 가벼운 프로토콜을 사용하여 자기들만의 직렬화 방식을 사용한다.

Spring Http Invokers HTTP를 통해 서비스를 노출하기 위해 자바 표준 직렬화 기능을 사용한다. 이것은 파라미터나 리턴타입이 복잡하고 Hessian 이나 Burlap의 직렬화 기법이 동작하지 않을때 큰 이점이 있다. 

스프링은 J2SE에서 제공하는 표준 HTTP 호출이나 Commons HttpClient 를 사용한다. Commons HttpClient를 사용하는게 기능적으로 쉽고 좀 더 많은 이점이 있다. 이 링크를 참조 jakarta.apache.org/commons/httpclient 

서비스 오브젝트 노출하기.

스프링 HttpInvoker는 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter를 제공한다. 

AccountService 를 스프링 웹 MVC DispatcherServlet을 통해 노출시키기 위해서는 dispatcher의 application context에 아래와 같은 설정을 추가 해야 된다.

<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/>     <property name="serviceInterface" value="example.AccountService"/> </bean>

이 방식은 DispathcerServlet의 표준 매핑 방식에 따라 매핑된다.

다른 방법은 HttpInvokerServiceExporter를 root application에 만들 수 있다(e.g 'WEB-INF/applicationContext.xml')

<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">     <property name="service" ref="accountService"/>     <property name="serviceInterface" value="example.AccountService"/> </bean>

추가적으로 이것과 통신하기 위한 exporter를 'web.xml'에 정의 해야 하고, 서블릿의 이름은 exporter와 일치 해야 한다. 

<servlet>
    <servlet-name>accountExporter</servlet-name>
    <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>accountExporter</servlet-name>
    <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>

만약 서블릿 컨테이너를 사용하지 않고 Sun's Java 6를 사용한다면 내장된 HTTP servier implementation을 사용할수 있다.(이런것도 있었나) SimpleHttpServerFactoryBean과 함계 SImpleHttpInvokerServiceExporter를 아래의 예제처럼 사용하면 된다.

<bean name="accountExporter"
    class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
    <property name="service" ref="accountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

<bean id="httpServer"
        class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
    <property name="contexts">
        <util:map>
            <entry key="/remoting/AccountService" value-ref="accountExporter"/>
        </util:map>
    </property>
    <property name="port" value="8080" />
</bean>


서비스를 클라이언트와 연결하기.

프록시를 사용해서, 스프링은 당신의 요청을 HTTP POST requests로 변환하여 노출된 서비스를 가르키는 URL로 가게 해준다.

<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

전에 언급한 바와 같이 HTTP client는 원하는 것을 사용할 수 있다. J2SE HTTP 를 사용하는 HttpInvokerProxy가 기본으로 지정되어 있지만 httpInvokerRequestExecuttor 속성에 Commons HttpClient를 세팅해서 사용할 수도 있다.

<property name="httpInvokerRequestExecutor">
    <bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>


기술을 선택할때의 고려사항

여기에서 제시된 각각의 기술은 단점이 있다. 

RMI를 사용할때 RMI 트래픽을 터널링 하지 않는한  HTTP protocol을 통해 객체에 접근할수 없다. RMI는 완전한 객체 직렬화를 지원하는 상당히 무거운 프로토콜 이지만 정보를 통해 직렬화가 필요한 복합 데이터 모델을 사용할때 중요하다. 하지만, RMI-JRMPs는 Java 클라이언트에 종속적이 된다. 이것은 Java-to-Java간의 원격 호출 방식이다.

Spring에서 제공하는 HTTP invoker는 HTTP 기반의 원격호출을 사용하지만 자바 직렬화가 필요할때 사용할 수 있는 좋은 선택이다. 이것은 RMI invoker의 기본 구조를 공유하고 HTTP를 전송하기 위해서만 사용된다. HTTP invoker는 Java-to-Java 에 제한을 두진 않지만 클라이언트와 서버 둘다 스프링을 사용해야 된다.

Hessian 이나 Burlap은 이기종 환경에서 사용할때 큰 이점이 있다. 왜냐하면 이들은 명시적으로 Java Clinet가 아닌것도 사용가능하기 때문이다. 하지만 Java Clinet가 아닌것을 지원하는데는 여전히 제한이 있다. 알려진 이슈로는 늦은 초기화 콜렉션이 포함된 하이버네이트 객체를 직렬화 하는것에대한 문제가 있다. 만약 이런 구조의 데이터 모델을 가지고 있다면 Hessian의 대용으로 RMI나 HTTP invoker를 사용하는것을 고려할 수 있다.

JMS는 서비스들을 클러스터링하기에 용이하고 JMS 브로커를 통해 로드 벨런싱을 하게 하거나 자동 오류극복을 하게 할 수 있다. 기본적으로 JMS를 사용할때 자바의 직렬화를 사용하도록 되어 있지만 JMS provider는 XStream을 구현하거나 다른 방식을 구현한 것처럼 다른 메커니즘을 가진 방식을 사용할 수 있다.

마지막으로 가장 적게 사용되는건 아니지만 EJB는 RMI를 넘어서는 표준 role-based 인증민 인가, 원격 트랜잭션 전파를 지원하는 장점이 있다. 스프링의 핵심 기능에서 제공되지는 않지만 원격 보안 컨텍스트 전파를 지원하는 RMI Invoker나 HTTP Invoker를 통해 얻을 수 있다. 





스프링 시큐리티에선 패스워드 인코딩 방식을 몇가지 지원 하는데 

로그인 시도시 입력한 패스워드를 인코딩 하고, db에 저장된 인코딩된 패스워드와 비교를 한다.


이것을 수행하는 부분은

org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)


이 부분으로 여기서 인코딩 하는데 사용된 패스워드 인코더를 가지고 패스워드를 확인한다.


참고 할 것.



http://stackoverflow.com/questions/4624250/where-to-put-default-servlet-handler-in-spring-mvc-configuration

저기를 보도록 하자.

기본적으로, 스프링은 HandlerMapping은 order 프로퍼티에서 낮은 숫자가 우선 순위가 높다. dispatcher에서 처음으로 설정들을 불러들일 때 DispatcherServlet은 이 order 값을 사용해서 HandlerMapping 들을 정렬한다.

만약 order값이 명시적으로 세팅되어있지 않을 경우에는 모든 핸들러들은 Integer.MAX_VALUE 값으로 동일하게 설정된다. 그러므로 정렬 뒤에 빈이 정의된 순서대로 정해지게 된다.

따라서 handler mapping의 값을 명시적으로 세팅해주어야 <mvc:default-servlet-handler /> 태그를 설정파일 아무 위치에다가 두어도 된다.


보통 프로퍼티 값을 Service나 Repository에서 가져다 쓸 때

<!-- Properties Files -->
<context:property-placeholder location="classpath:properties/*.properties"/>

처럼 프로퍼티 파일을 정의 해 두고,
@Value("${jdbc.username}") String userName;
이렇게 사용한다.

하지만, Controller에서 @Value("${jdbc.username}")로 주입을 받을려고 해 보면 안되고
값을 출력 해 보면 "${jdbc.username}" 값이 출력 된다.

왜 그럴까 해서 구글링을 좀 해 봤더니

http://www.dotkam.com/2008/07/09/spring-web-application-context-visibility/

아래에서 답을 알 수 있었다.

Here is the semi-official version “why” from Juergen (Spring Lead Developer):

“PropertyPlaceholderConfigurer is an implementation of the BeanFactoryPostProcessor interface: This interface and its sibling BeanPostProcessor just apply to the BeanFactory that defines them, that is, to the application context that defines them.

If you combine multiple config files into a single contextConfigLocation, a PropertyPlaceholderConfigurer defined in any of the files will apply to all of the files, because they are loaded into a single application context.

However, a DispatcherServlet has its own application context, just using the root web application context as parent. Therefore, it needs to define its own BeanFactoryPostProcessors and/or BeanPostProcessors, in this case its own PropertyPlaceholderConfigurer.”

Happy Springing!


알다 시피 웹 환경에서
부모(보통 applictionContext) - 자식(dispathcerServlet)으로 구성을 한다.
컨텍스트 영역이 다르 다는 말이다.

PropertyPlaceholderConfigurer는 BeanFactoryPostProcessors and/or BeanPostProcessors 이것인데
부모와 자식의 생성 주기가 틀리므로 부모의 PropertyPlaceholderConfigurer를 적용할 수 없다.
그래서 각자 자신의 PropertyPlaceholderConfigurer를 만들어서 써야 한다는 말



http://www.springsource.org/node/2966..

More content from SpringOne 2GX 2010 has just been posted on the InfoQ media library.

In this session Juergen Hoeller gives a presentation covering Spring and Java EE 6: Synergy or Competition?. This has been a topic of great interest recently and the presentation provides a good complement to Juergen's earlier blog post on a similar topic.

이번세션에서 유겐 휄러는 "스프링과 Java EE 6 : 시너지 인가 경쟁인가?"에 대한 발표를 했다.
이것은 최근에 큰 관심을 끄는 주제이고 이 프레젠테이션은 유겐이 예전에 포스팅한 비슷한 주제를 보완하고있다.

Juergen starts with a review of Spring's general deployment approach and then provides a brief review of some of the Java EE 6 specifications and how they relate to the Spring projects. Finally Juergen discusses some of the key considerations that developers and architects should think about when making a technology choice for their applications.

유겐은 스프링의 일반적인 배포 방법에 대해서리뷰를 하고 Java EE 6 명세와 이것이 어떻게 스프링 프로젝트들과 관계가 있는지 리뷰를 한다. 마지막으로 유겐은 개발자와 아키텍쳐가 그들의 어플리케이션을 만들 때 선택할 기술에 대해서 생각해야 할 몇몇 핵심 고려사항을 설명 한다.

Again, many thanks to InfoQ who was at SpringOne 2GX to capture so many of the great presentations and provided fantastic coverage for the show.

이런 발 번역 -_-


스프링에서 MappingJacksonHttpMessageConverter를 사용할 때
Date 포맷은 json값이 "createdAt":1287041156338 이렇게 들어 온다.

이게 멍미!!
하여 포맷을 정해줘야 하는데...

아주 유용한 블로그를 발견 했다.
how-to-serialize-java-util-date-with-jackson-json-processor-spring-3-0
오우 very good!!

하는 방법은
jackson 요녀석이 시리얼 라이즈를 하는데..
Date형식에 대해서는 특정 시리얼라이저(
JsonDateSerializer)를 정해줘서.
이 녀석을 getDate 하는데다가
@JsonSerialize(using=JsonDateSerializer.class) 이렇게 해주는거다.
JsonSerializer<Date>이것을 상속하여 JsonDateSerializer를 구현해서 사용.

그러면 jackson 이것이 시리얼라이즈를 할 때 저것을 사용하려 시리얼 라이즈를 하게 된다.

쭉 살펴보면..
댓글에도 유용한 답변이 있다.
바로 전역적인 dateformat을 사용할 때는 어떻게 하는가.

이때는
MappingJacksonHttpMessageConverter의 ObjectMapper를 구현하여 set 해주면 된다.
org.codehaus.jackson.map.ObjectMapper를 상속하여 CustomObjectMapper 를 만들고,
CustomObjectMapper클래스 안의 메서드에 @PostConstruct 를 붙여 주고
getSerializationConfig().setDateFormat(new SimpleDateFormat(“yyyy-MM-dd’T'HH:mm:ss.SZ”));
이 스타일로 포맷을 지정해 주면 된다.

그다음엔 스프링 설정에서 빈으로 만들고 MappingJacksonHttpMessageConverter에다가 주입!!

<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="objectMapper" ref="CustomObjectMapper" />
</bean>

UrlLogging 화면

Java/Spring2010. 3. 30. 13:05
지금 만들고 있는 UrlLogging 화면, 달력은 jQuery UI Datepicker로 했고
그래프는 jquery Float chart 플러그인으로 사용.


UI 조금 더 수정 하고, 그래프 검색 결과도 수정하면 좀더 볼만 해 질듯.



간단한 Url Logger를 작성 하기 위해서, 해당 url에 대한 @RequestMapping에 url값이 필요했다.

구글링을 해서 찾아봤었는데.. http://lizdouglass.wordpress.com/2009/12/16/adding-a-spring-aspect/

여튼..
@Before aspect로 할 경우 Joinpoint에서 getClass 한 후
여기서 getMethods[] 한다. 메서드 들 중에서 방금 호출한 메서드를 가져오고
가져온 메서드와 호출한 메서드 정보가 일치 하면 그 메서드에서 어노테이션 정보를 가져오고
어노테이션 정보에서(대부분 url을 첫번째 값이니) 첫번째 값을 가져온다.

@Before(value="execution(* net.study.spring.controller.WelcomeController.index())")
    public void doBeforeProfiling( JoinPoint jp) throws SQLException
    {
        UrlLogger urlLogger = new UrlLogger();
        Method[] methods =  jp.getTarget().getClass().getMethods();
        for ( Method method : methods) {
            if ( method.getName().equals( jp.getSignature().getName()))
            {
                String[] values = method.getAnnotation( RequestMapping.class).value();
                urlLogger.setTargetUrl( values[0]);
            }
        }
        urlLoggerDAO.insert( urlLogger);
    }

더 낳은 방법도 있을 것 같은데..
토비님 책 나오면 다시한번 집중하고 봐야겠다.

추가로..
aspect표현식을 수정 하면. 아래 한줄임!! ..;
@Before(value="execution(* net.study.spring.bbs.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")

이렇게 된다. controller클래스에 있는 모든 메서드에 걸긴 거는데 거기서 @annotation이 RequestMapping인 것만!!
왜냐.. url로깅이기 때문에 @_@
만약 dao관련 로깅을 하고 싶다면..@Transactional을 하면 되겠죵!!