spring java config transaction

spring java config transaction

저번 포스팅에서 rollback이 안되는 문제 있었다. rollback도 안됐지만 service 계층의 영속성도 안됐다(물론 당연한 얘기지만)
해결은 했는데 잘 모르겠다.
RootContext를 버리니 잘 된다.
그래서 다시 책을 봤다. 하지만 안보인다.(대충 봐서)
다시 생각이 나서 김영한님의 github를 봤다.
RootContext가 없다.
솔직히 아직 잘 모르겠다. root와 servlet context의 대해.. 토비님 책을 다시 읽어야 겠다.
servlet이 하나라면 굳이 분리할 필요도 없다고 하는거 같기도 하고..
아무튼 지금은 잘모르겠다. 일단 나중에 다시 살펴보겠다.

이렇게 바꾸었다.

@Configuration
@EnableWebMvc
@ComponentScan(
  basePackages = "me.wonwoo.account"
)
@Import(RootConfiguration.class)
public class MvcConfiguration extends WebMvcConfigurerAdapter {
  //...
}
  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
//    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
//    rootContext.register(RootConfiguration.class);

    AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
    dispatcherServlet.register(MvcConfiguration.class);
    servletContext.addListener(new ContextLoaderListener(dispatcherServlet));

   //...
   //...
}

롤백도 되고 service에 영속성도 된다.
소스가 다시 개판 되었다..ㅡㅡ

save하면서 한건입력 후 강제로 에러를 냈다.
아무튼 소스는 다시 올려놔야 겠다.

@Transactional
public Account save(Account account) {
  accountRepository.save(account);
  throw new RuntimeException();
}

에러는 내는 부분

@Transactional(readOnly = true)
public Account findOne(Long id) {
  Account one = accountRepository.findOne(id);
  Product order = one.getOrder();
  order.getOrderedName();
  return one;
}
@OneToOne(fetch = FetchType.LAZY)

service 계층에 영속성 테스트 하는 부분

물론 일단 지금은 EAGER로 바꿔놨다.

spring java config

spring java config

맨날 스프링부트로 공부 하다 보니 자바 config가 기억이 잘 안난다.
그래서 대충 한번 해봤다.
일반 스프링과 jpa 구현체는 하이버네이트로 했다.

maven 프로젝트 webapp으로 만들었다.
pom.xml에 다음과 같이 추가를 했다.

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.5.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.2.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.1.5</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>log4j-over-slf4j</artifactId>
        <version>1.7.16</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.5</version>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.191</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>1.9.4.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.11.Final</version>
        <exclusions>
            <exclusion>
                <groupId>org.jboss.spec.javax.transaction</groupId>
                <artifactId>jboss-transaction-api_1.2_spec</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>javax.transaction</groupId>
        <artifactId>javax.transaction-api</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.6</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.2.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.2.0</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

스프링 컨텍스트랑 webmvc를 추가 하고 로그는 logback을 사용했다.
데이터베이스는 h2 메모리 디비로 일단.
스프링 부트를 안쓰니 디펜더시 설정이 많다.

ROOT 컨텍스트 부터 살펴보자.

@Configuration
@EnableJpaRepositories(basePackages = "me.wonwoo.account")
@EnableTransactionManagement
public class RootConfiguration {

  @Bean
  public DataSource dataSource() {
    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    EmbeddedDatabase db = builder
      .setType(EmbeddedDatabaseType.H2)
      .build();
    return db;
  }

  @Bean
  public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
    return entityManagerFactory.createEntityManager();
  }

  @Bean
  public FactoryBean<EntityManagerFactory> entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean containerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    containerEntityManagerFactoryBean.setDataSource(dataSource());
    JpaVendorAdapter adaptor = new HibernateJpaVendorAdapter();
    containerEntityManagerFactoryBean.setJpaVendorAdapter(adaptor);
    containerEntityManagerFactoryBean.setPackagesToScan("me.wonwoo.account");
    Properties props = new Properties();
    props.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
    props.setProperty("hibernate.show_sql", "true");
    props.setProperty("hibernate.hbm2ddl.auto", "create");
    containerEntityManagerFactoryBean.setJpaProperties(props);
    return containerEntityManagerFactoryBean;
  }

  @Bean
  public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
    return jpaTransactionManager;
  }
}

dataSource는 h2 메모리 디비를 썼고 하이버네이트 셋팅을 했다. jpa는 처음 설정하는 거라 틀린 거 일지도 모른다.
맨날 boot가 알아서 해주니 (실제 회사에선 개발할때 맨날 mybatis만 써갖고..)
아무튼 이제 webconfig를 보자. 별거 없다.

@Configuration
@EnableWebMvc
@ComponentScan(
  basePackages = "me.wonwoo.account"
)
public class MvcConfiguration extends WebMvcConfigurerAdapter {

  @Bean
  @Primary
  public ObjectMapper objectMapper() {
    return new ObjectMapper();
  }

  @Autowired
  private AccountRepository accountRepository;

  @Bean
  public InitializingBean initializingBean() {
    return () -> {
      Arrays.asList(
        new Account(1L, "wonwoo"),
        new Account(2L, "kevin")
      ).forEach(accountRepository::save);
    };
  }
}

ComponentScan 스캔만 하고 api서버를 만들 예정이라 view는 필요 없고 test에 쓰기 위해 ObjectMapper를 빈으로 등록했다.
그리고 초기 데이터를 넣기 위해 InitializingBean을 사용하였다.

설정 마지막으로 디스파처 설정이랑 context를 등록하자. web.xml이라고 생각하면 된다

public class DispatcherServletInitializer implements WebApplicationInitializer {

  @Override
  public void onStartup(ServletContext servletContext)
    throws ServletException {
    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
    rootContext.register(RootConfiguration.class);

    servletContext.addListener(new ContextLoaderListener(rootContext));

    AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
    dispatcherServlet.register(MvcConfiguration.class);

    ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(dispatcherServlet));
    dispatcher.setLoadOnStartup(1);
    dispatcher.addMapping("/");

    FilterRegistration.Dynamic filter = servletContext.addFilter("CHARACTER_ENCODING_FILTER", CharacterEncodingFilter.class);
    filter.setInitParameter("encoding", "UTF-8");
    filter.setInitParameter("forceEncoding", "true");

  }
}

context를 등록하고 디스파처를 등록했다.
일단 설정은 이것으로 끝났다.
일반으로 쓰듯이 controller service repository를 만들면 되겠다.
소스는 github 에 올려놨다. 참고만…

테스트 해보니 롤백이 안된다.ㅜㅜㅜㅜㅜㅜㅜㅜ ㅜㅜㅜ
다시 알아봐야 겠다.

spring @bean

spring @Bean

스프링에 자주 사용되는 어노테이션으로 @Bean에 대해 살짝 맛만 볼라고 한다.
저번에 한번 얘기를 했는데 ConfigurationClassParser클래스 doProcessConfigurationClass 메소드에 여러 메타 어노테이션을 파싱하는 부분이 있다.

...

// Process individual @Bean methods
Set<MethodMetadata> beanMethods = sourceClass.getMetadata().getAnnotatedMethods(Bean.class.getName());
for (MethodMetadata methodMetadata : beanMethods) {
    configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

...

doProcessConfigurationClass 메소드의 Bean을 파싱하는 부분이다.
하지만 메타 정보만 갖고 있고 이때 인스턴스는 하지 않는다.(이 부분은 저번에도 얘기 한듯 하다)
그럼 우리가 흔히 쓰는 @Bean 은 언제 인스턴스 하는지 알아보자

@Bean
public ModelClass modelClass(){
    return new ModelClass();
}

예로 위와 같은 빈이 있다 가정하자
저번에 얘기 했듯이 Spring은 내부적으로 getBean을 호출하면서 그때 인스턴스도 같이 한다.(물론 안하는 Context도 있다. 안한다는 것보다 할 필요가 없는 듯 해서 그런거 같다.)
그 중에 @Bean 어노테이션들은 BeanMethodInterceptor aop 프록시를 통해 해당 빈을 호출한다.
그리고 나서 DefaultSingletonBeanRegistry클래스에 싱글톤 빈이라고 저장해둔다.(아마도 여기에 모든 싱글톤이 있는 듯하다)

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

저기에는 실제 인스턴스화된 빈들이 저장 되어 있는 곳이다.
왜 이얘길 하냐면 우리들이 흔히 쓰는 getBean을 호출 할때 singletonObjects 멤버 변수에서 꺼내서 주는 거다.

Object sharedInstance = getSingleton(beanName);

getBean을 호출 할때 저 함수를 호출한다. getSingleton 함수를 보자

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

첫 줄만 봐도 알 것이다. Map으로 저장된 빈들을 꺼내서 사용한다.

그럼 Bean의 스코프가 prototype 일 경우는 어떠할까?
실질적으로 하는 행동은 같다.
AbstractBeanFactory 클래스의 일부분이다.

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
        @Override
        public Object getObject() throws BeansException {
            try {
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                // Explicitly remove instance from singleton cache: It might have been put there
                // eagerly by the creation process, to allow for circular reference resolution.
                // Also remove any beans that received a temporary reference to the bean.
                destroySingleton(beanName);
                throw ex;
            }
        }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
    Object prototypeInstance = null;
    try {
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        afterPrototypeCreation(beanName);
    }
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

코드만 봐도 비슷한 일을 하고 있다 대신 싱글톤일 경우엔 getSingleton 을 호출 하면서 싱글톤으로 등록 할 뿐이다.
저기 위에서 말했듯이 getBean을 호출 할 경우 getSingleton에서 싱글톤이 있는지 없는지 확인을 하는데 prototype일 경우엔 없으니 계속 Bean을 새로 호출 할 것이다.

스코프가 session이거나 request일 경우에도 비슷하게 동작 하지 않나 싶다. (확인을 안해서 믿지 마시길)

다 아는 내용 이겠지만 테스트를 해봤다.

  • singleton 일 경우
ModelClass hello = applicationContext.getBean("modelClass", ModelClass.class);
ModelClass hello2 = applicationContext.getBean("modelClass", ModelClass.class);
System.out.println(hello == hello2);
//true
  • prototype 일 경우
ModelClass hello = applicationContext.getBean("modelClass", ModelClass.class);
ModelClass hello2 = applicationContext.getBean("modelClass", ModelClass.class);
System.out.println(hello == hello2);
//false

이렇게 @Bean이 어떻게 동작하는지 조금 알아봤다.