AXBoot를 IntelliJ IDEA에서 처음 사용할 때 유의할 점

Java|2018. 3. 24. 15:15

AXBoot Initializer로 생성한 프로젝트를 IntelliJ IDEA에서 열고 실행하려고 하면, QMenu, QCommonCode 등의 JPA 도메인 클래스 관련 오류가 발생하면서 빌드가 실패한다.  

이럴 때는 "View > Tools Windows > Maven Projects" 메뉴를 이용해서 Maven 패널을 열고, Lifecycle 하위의 clean, package 골을 차례로 실행한다.  


이 오류는 Querydsl 관련 클래스가 없어서 발생하는데, 위의 과정을 통해서 생성하는 것이다.


EOF


댓글()

Spring Data JPA - LazyInitializationException

Java|2015. 4. 14. 10:00

개요

요즘 Spring Boot 저장소에 있는 Spring Data JPA 샘플 프로젝트를 살펴 보고 있습니다.

테스트 코드를 작성하면서 하나하나 파악하고 있는데, OneToMany로 엮인 다른 엔티티의 목록을 가져오려고 하면 아래와 같은 오류가 발생합니다.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.javaworld.jpa.domain.Person.cars, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:575)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:214)
    at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:155)
    at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:278)
    at org.hamcrest.collection.IsCollectionWithSize.featureValueOf(IsCollectionWithSize.java:21)
    at org.hamcrest.collection.IsCollectionWithSize.featureValueOf(IsCollectionWithSize.java:14)
    at org.hamcrest.FeatureMatcher.matchesSafely(FeatureMatcher.java:40)
    at org.hamcrest.TypeSafeDiagnosingMatcher.matches(TypeSafeDiagnosingMatcher.java:55)
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:12)
    at org.junit.Assert.assertThat(Assert.java:956)
    at org.junit.Assert.assertThat(Assert.java:923)
    at kr.co.javaworld.jpa.service.PersonRepositoryIntegrationTests.testFindCars(PersonRepositoryIntegrationTests.java:46)
    ..............
    .... 중략 ....
    ..............
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)

늦은 초기화(Lazy initialization)에 관련된 예외 상황인데, 이에 대한 해결 방법을 정리해 봅니다.

예외가 발생하는 코드

아래의 테스트 코드를 실행하던 중 예외가 발생했습니다. testFindCars 메소드는 Person 객체를 하나 찾은 후, 다시 그 Person이 가진 Car 목록을 조회하는 테스트 코드입니다.

Person.getCars() 호출 후 목록(cars)의 크기를 얻으려고 하니까(assertThat(cars, hasSize(4))) 오류가 발생했습니다.

package kr.co.javaworld.jpa.service;

import kr.co.javaworld.jpa.domain.Car;
import kr.co.javaworld.jpa.domain.Person;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import sample.data.jpa.SampleDataJpaApplication;

import javax.transaction.Transactional;
import java.util.List;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleDataJpaApplication.class)
public class PersonRepositoryIntegrationTests {

    @Autowired
    PersonRepository repository;

    ..............
    .... 중략 ....
    ..............

    @Test
    public void testFindCars() {
        Person person = repository.findOne(1L);
        List<Car> cars = person.getCars();

        assertThat(cars, hasSize(4));
        assertThat(cars.get(0).getName(), is("Mercedes"));
    }
}

Person 엔티티의 코드입니다.
cars@OneToMany annotation이 붙어있을 뿐 특별한 점은 없습니다.

package kr.co.javaworld.jpa.domain;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "TB_PERSON_02837")
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "PERSON_NAME", length = 100, unique = true, nullable = false)
    private String name;

    @OneToMany
    private List<Car> cars;

    ..............
    .... 중략 ....
    ..............

}

해결 방법

@OneToMany로 엮을 경우 JPA는 늦은 초기화를 통해 Personcars 목록을 가져오는게 기본 설정입니다. 즉, 위 코드의 @OneToMany annotation 선언은 @OneToMany(fetch = FetchType.LAZY)로 대체해도 동일합니다.

따라서 해결 방법은 간단합니다. 패치 타입을 FetchType.EAGER로 변경하여 늦은 초기화 대신 이른 초기화(Eager initialization)를 사용하면 됩니다.

아래 코드와 같이 @OneToMany(fetch = FetchType.EAGER)로 annotation 선언을 변경해 주면 오류가 사라집니다.

package kr.co.javaworld.jpa.domain;

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "TB_PERSON_02837")
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "PERSON_NAME", length = 100, unique = true, nullable = false)
    private String name;

    @OneToMany(fetch = FetchType.EAGER)
    private List<Car> cars;

    ..............
    .... 중략 ....
    ..............

}

다른 해결 방법

앞서의 해결 방법은 People 엔티티를 얻어 올 때는 언제나 Car 목록도 함께 가져오도록 강제합니다. 따라서 가끔씩만 Car 목록이 필요하다면 불필요한 메모리 낭비나 서버 성능 저하 등의 문제가 발생할 수 있습니다.

늦은 초기화를 사용하면서도 문제를 해결하려면 아래와 같이 트랜잭션을 Person.getCar() 호출 후 관련 동작을 완료할 때까지 유지하면 됩니다.

아래에서는 testFindCars 메소드에 @Transactional annotation을 붙여서 메소드 전체를 하나의 트랜잭션으로 감쌌습니다.

실무에서는 서비스 클래스의 메소드 단위로 트랜잭션을 잡아 주거나, 뷰 단의 서블릿 필터에서 UserTransaction을 이용하여 요청 단위로 트랜잭션을 처리하는 방법을 사용하면 됩니다.

package kr.co.javaworld.jpa.service;

import kr.co.javaworld.jpa.domain.Car;
import kr.co.javaworld.jpa.domain.Person;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import sample.data.jpa.SampleDataJpaApplication;

import javax.transaction.Transactional;
import java.util.List;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleDataJpaApplication.class)
public class PersonRepositoryIntegrationTests {

    @Autowired
    PersonRepository repository;

    ..............
    .... 중략 ....
    ..............

    @Test
    @Transactional
    public void testFindCars() {
        Person person = repository.findOne(1L);
        List<Car> cars = person.getCars();

        assertThat(cars, hasSize(4));
        assertThat(cars.get(0).getName(), is("Mercedes"));
    }
}

EOF

댓글()

Spring Shell Command 구현

Java|2015. 1. 7. 22:16

개요

HelloWorldCommands 클래스를 통해서 Spring Shell 커맨드 구현 방법을 살펴 보겠습니다.

Marker interface

HelloWorldCommands는 org.springframework.shell.core.CommandMarker 인터페이스를 구현하고 있습니다.

@Component
public class HelloWorldCommands implements CommandMarker {

}

CommandMarker는 마커 인터페이스라서 커맨드 구현체라는 것을 알려주기 위해서 implements 할 뿐입니다. 따라서 구현할 메소드는 없습니다.
@Component annotation은 component-scan을 통해 bean으로 등록되라고 붙여 줬습니다.

CLI Annotation

HelloWorldCommands는 모두 3개의 annotation을 사용하여 쉘에서의 동작을 제어합니다.

CliAvailabilityIndicator

어떤 커맨드가 쉘에서 사용 가능한지를 알려주기 위해서 사용합니다. 이 annotation은 boolean 값을 반환하는 메소드에 붙이며, 이 메소드는 annotation에 명시한 커맨드의 사용 가능 여부를 반환합니다.
HelloWorldCommands에서는 hw simple, hw complex, hw enum 커맨드의 사용 가능 여부를 알려 주기 위해서 사용하였습니다.

    private boolean simpleCommandExecuted = false;

    @CliAvailabilityIndicator({"hw simple"})
    public boolean isSimpleAvailable() {
        //always available
        return true;
    }

    @CliAvailabilityIndicator({"hw complex", "hw enum"})
    public boolean isComplexAvailable() {
        if (simpleCommandExecuted) {
            return true;
        } else {
            return false;
        }
    }

위에서 isSimpleAvailable() 메소드는 언제나 true를 반환하여, hw simple 커맨드가 언제나 사용 가능하다고 알려주고 있습니다.
isComplexAvailable 메소드는 simpleCommandExecuted 변수의 상태에 따라서 hw complex, hw enum 커맨드의 사용 가능 여부를 알려줍니다.

댓글()

Spring Shell 소개

Java|2015. 1. 2. 11:17

소개

스프링 프로그래밍 모델을 기반으로 손쉽게 커맨드라인 애플리케이션(interactive shell)을 만들 수 있도록 도와주는 프로젝트입니다.

홈페이지(GitHub)

https://github.com/spring-projects/spring-shell

샘플 애플리케이션

Spring Shell은 샘플 애플리케이션을 제공합니다. HelloWorld 애플리케이션은 부트스트랩(Bootstrap)/커맨드(Command)/베너(Banner)/프롬프트(Prompt)의 기본 구현 방법을 보여줍니다.
샘플을 통해 Spring Shell을 이해하는게 효과적이기 때문에 이를 통해 Spring Shell 사용 방법을 하나하나 설명토록 하겠습니다.

Clone

먼저 Spring Shell 프로젝트를 Clone합니다.

~$ git clone git@github.com:spring-projects/spring-shell.git

빌드 및 실행

Clone이 완료되면, samples/helloworld 디렉토리로 이동한 후 ./gradlew installApp 명령으로 HelloWorld 샘플을 빌드합니다.

~$ cd spring-shell/samples/helloworld
~/spring-shell/samples/helloworld$ ./gradlew installApp
:compileJava
:processResources
:classes
:jar
:startScripts
:installApp

BUILD SUCCESSFUL

./gradlew -q run 명령 혹은 build/install/helloworld/bin/helloworld 스크립트를 실행하면 HelloWorld 샘플을 실행할 수 있습니다.
build/install/helloworld 디렉토리에는 실행 스크립트(bin/helloworld)와 의존 라이브러리(lib/*.jar)가 모두 들어 있기 때문에 이 디렉토리만 배포하면 누구든 이 애플리케이션을 사용할 수 있게 됩니다.
사실 실행만 원한다면 빌드 없이 ./gradlew -q run 명령만 사용해도 됩니다.

~$ cd spring-shell/samples/helloworld
~/spring-shell/samples/helloworld$ ./gradlew -q run
=======================================
*                                     *
*            HelloWorld               *
*                                     *
=======================================
Version:1.2.3
Welcome to HelloWorld CLI
hw-shell>

구현 상세

HelloWorld는 아래 5개의 클래스로 구현되어 있었습니다.

  • org.springframework.shell.samples.helloworld.Main
  • org.springframework.shell.samples.helloworld.commands.HelloWorldCommands
  • org.springframework.shell.samples.helloworld.commands.MyBannerProvider
  • org.springframework.shell.samples.helloworld.commands.MyHistoryFileNameProvider
  • org.springframework.shell.samples.helloworld.commands.MyPromptProvider

Main 클래스는 org.springframework.shell.Bootstrap 클래스의 main 메소드를 호출하여 애플리케이션을 실행합니다.

    /**
     * Main class that delegates to Spring Shell's Bootstrap class in order to simplify debugging inside an IDE
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        Bootstrap.main(args);
    }

이 때 "/META-INF/spring/spring-shell-plugin.xml" 파일을 이용해 ApplicationContext를 생성합니다. spring-shell-plugin.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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springframework.shell.samples.helloworld.commands" />

</beans>

component-scan을 통해 bean을 찾기 때문에 Main 이외의 모든 클래스는 Component annotation을 달고 있습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyPromptProvider extends DefaultPromptProvider {

    @Override
    public String getPrompt() {
        return "hw-shell>";
    }


    @Override
    public String getProviderName() {
        return "My prompt provider";
    }

}

위의 MyPromptProvider 클래스는 Order annotation 또한 달고 있는데, 이는 혹 클래스 패스에 다른 plugin이 Provider(Banner/Prompt/HistoryFileName) 구현체를 가지고 있더라도 내가 구현한 Provider를 우선하도록(Ordered.HIGHEST_PRECEDENCE) 합니다. Provider에 대해서는 곧 설명합니다.

Provider는 Spring Shell이 쉘을 커스터마이징 할 수 있도록 제공하는 확장 포인트로 모두 org.springframework.shell.plugin.NamedProvider 인터페이스를 상속한 인터페이스입니다.

HelloWorld가 사용하는 구현 클래스는 모두 Spring Shell이 제공하는 각 Provider의 기본 구현체(DefaultXXXProvider)를 상속하고 있습니다. HelloWorld는 총 3개의 Provider 구현 클래스를 가지고 있습니다.

  • MyBannerProvider는 이름이 알려주듯이 배너 정보를 담고 있는데, 이외에 버전정보(getVersion()), 환영 메시지(getWelcomeMessage()) 또한 제공합니다.
  • MyPromptProvider는 프롬프트 모양을 정의(getPrompt())합니다.
  • MyHistoryFileNameProvider는 쉘의 동작 히스토리를 담을 로그파일 이름(my.log)을 정의합니다.

MyHistoryFileNameProvider의 코드는 다음과 같습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyHistoryFileNameProvider extends DefaultHistoryFileNameProvider {

    public String getHistoryFileName() {
        return "my.log";
    }

    @Override
    public String getProviderName() {
        return "My history file name provider";
    }

}

MyBannerProvider의 코드는 다음과 같습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyBannerProvider extends DefaultBannerProvider  {

    public String getBanner() {
        StringBuffer buf = new StringBuffer();
        buf.append("=======================================" + OsUtils.LINE_SEPARATOR);
        buf.append("*                                     *"+ OsUtils.LINE_SEPARATOR);
        buf.append("*            HelloWorld               *" +OsUtils.LINE_SEPARATOR);
        buf.append("*                                     *"+ OsUtils.LINE_SEPARATOR);
        buf.append("=======================================" + OsUtils.LINE_SEPARATOR);
        buf.append("Version:" + this.getVersion());
        return buf.toString();
    }

    public String getVersion() {
        return "1.2.3";
    }

    public String getWelcomeMessage() {
        return "Welcome to HelloWorld CLI";
    }

    @Override
    public String getProviderName() {
        return "Hello World Banner";
    }
}

HelloWorld를 실행하면 배너와 버전 정보, 환영 메시지가 출력된 후 hw-shell> 프롬프트 옆에서 커서가 깜빡입니다.
앞에서 설명했듯이 이 때 출력되는 모든 정보는 MyBannerProvider가 제공하며, hw-shell> 프롬프트는 MyPromptProvider에서 정의하고 있습니다.
그리고 HelloWorld를 실행한 디렉토리를 탐색해 보면 MyHistoryFileNameProvider에서 정의한 my.log 파일이 생성된 것을 확인할 수 있습니다.

=======================================
*                                     *
*            HelloWorld               *
*                                     *
=======================================
Version:1.2.3
Welcome to HelloWorld CLI
hw-shell>

지금까지 Spring Shell에 대한 기본적인 내용과 Provider에 대해 알아봤습니다.
가장 중요한 커맨드 클래스에 대해서는 따로 글을 올리겠습니다.

댓글()

Spring Tool Suite(STS)에 Subversion Plugin 설치

Developer Tools|2014. 10. 14. 13:42

Spring Tool Suite(STS)는 간편하게 Subversion(SVN) 플러그인을 설치하는 방법을 제공합니다.
아래의 방법으로 Subversive 혹은 Subclipse를 설치합니다.


먼저 Dashboard를 엽니다.

sts subversive dashboard menu


Dashboard가 열리면, Extensions 탭을 선택합니다.

sts subversive select tab


Find 필드에 Subversive/Subclipse를 입력하여 플러그인을 찾습니다(둘 중 하나만 설치하면 됩니다). 저는 Subversive를 찾았습니다. 검색 결과가 나오면 앞쪽의 체크박스를 선택합니다.

sts subversive find subversive


Install 버튼을 눌러서 설치를 진행합니다. 이후 검증/재시작 등에 대한 경고가 나오면 모두 Yes를 선택하여 설치를 완료합니다.

sts subversive install subversive

댓글()

Spring MVC의 CommonsMultipartResolver를 사용하여 업로드한 임시파일은 지워질까?

Java|2010. 2. 23. 23:30

Spring MVC는 Commons FileUpload 패키지를 이용한 파일 업로드를 지원한다.


아무 생각 없이 사용하고 있다가 문득 FileUpload를 Spring 없이 사용할 때는 FileCleanerCleanup 리스너를 web.xml에 등록해서 temporary 파일을 자동으로 삭제하게 한다는게 기억났다.


아차! 지금이라도 등록해 줘야하는건가? 하는 생각에 확인차 서버의 temporary 경로를 살펴봤다.

그런데 이상하게도 파일 업로드 후에 남아 있었어야할 임시 파일이 하나도 없었다.


CommonsMultipartResolver가 뭔가 알아서 지우는건가 해서 뒤져봐도 관련 코드는 없었다.

이해가 안가서 좀 더 뒤지다가 결국 DispatcherServlet에서 삭제 코드를 찾았다. doDispatch 메서드의 finally 절에 cleanupMultipart 메서드를 호출하는 부분이 있었던 것이다.

public class DispatcherServlet extends FrameworkServlet {


    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

        ......

        try {

            ......

        finally {

            // Clean up any resources used by a multipart request.

            if (processedRequest != request) {

                cleanupMultipart(processedRequest);

            }


            // Reset thread-bound context.

            RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable);

            LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable);


            // Clear request attributes.

            requestAttributes.requestCompleted();

            if (logger.isTraceEnabled()) {

                logger.trace("Cleared thread-bound request context: " + request);

            }

        }

    }


    /**

     * Clean up any resources used by the given multipart request (if any).

     * @param request current HTTP request

     * @see MultipartResolver#cleanupMultipart

     */

    protected void cleanupMultipart(HttpServletRequest request) {

        if (request instanceof MultipartHttpServletRequest) {

            this.multipartResolver.cleanupMultipart((MultipartHttpServletRequest) request);

        }

    }

    ......

}




댓글()