Spring Data JPA는 "JpaRepository"라는 기능을 사용하면 매우 간단히 데이터를 검색 할 수 있도록 된다. 그 기본적인 사용법을 설명한다.


JpaRepository 인터페이스 생성

이전에 기본적인 데이터베이스 액세스 대해 대충 설명을 했었다. 이 때, 데이터의 검색에 대해서는 "JPQL"라는 쿼리 문을 사용 할 수 있다는 것을 설명했었는데 기억하는가?

이 설명을 읽고 본인도 모르게 한숨을 쉰 사람도 분명 많았을 것이다. SQL이라는 것을 보고 싶지 않아서 프레임워크를 사용하려고 했는데, 설마 SQL 같은 것이 있을 거라고는 생각하지 못했을 것이다.

그래도 아직 안심해도 된다. Spring Framework에는 더 똑똑하게 데이터 검색을 할 수 있는 구조가 제대로 준비되어 있다. 그것이 "JpaRepository"라는 것이다.

JpaRepository는 인터페이스이다. 인터페이스에 미리 검색 메소드를 정의 해 두는 것으로, 메소드를 호출하는 것만으로 스마트한 데이터 검색을 할 수 있게 되는 것이다. "결국, 그 메소드 중에는 JPQL를 사용하여 검색 처리를 쓰는 거지 ......"라고 생각했다면 조금 더 참아주기 바란다. 결코 손해가되지 않을 것이다.

우선 JpsRepository 인터페이스를 만들어 보자. com.devkuma.spring.db 패키지에 "SampleEntityRepository.java"라는 파일명으로 소스 코드 파일을 작성하자. 그리고 아래와 같이 인터페이스를 작성하자.

package com.devkuma.spring.db;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface SampleEntityRepository
    extends JpaRepository <SampleEntity, Long> {
}

JpaRepository 인터페이스는 org.springframework.data.jpa.repository 패키지의 "JpaRepository"라는 인터페이스를 상속하여 만든다. 이 인터페이스는 범용적으로 사용한다. 코드를 보면 이런 형태로 쓰여져 있는 것을 알 수 있을 것이다.

public interface 이름 extends JpaRepository <엔티티 ID 유형>

<>안에는 엔티티 클래스 이름과 ID 필드 타입이 지정된다. 주의할 점은 "기본형의 경우, 래퍼 클래스를 지정한다는 점이다. 샘플 SampleEntity 클래스는 long 형을 ID를 지정하고 있기 때문에, 여기에서는 <SampleEntity, Long>라고 작성을 한다.

또 하나 주의해야 할 것은 어노테이션이다. 클래스의 선언 앞에 "@Repository"라는 어노테이션이 붙여져 있다. 그러면 이 인터페이스가 JpaRepository임을 나타낸다. 이는 반드시 붙여 두어야 하는 것이다.


Bean설정 파일 준비

그럼 Bean 설정 파일을 준비해 보자. 먼저, dbbean.xml라는 파일을 준비하고 있다. 그걸 그대로 다시 사용하도로 하겠다.

아래와 같이 dbbean.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:jpa="http://www.springframework.org/schema/data/jpa"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd  
        http://www.springframework.org/schema/data/jpa 
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
 
    <jdbc:embedded-database id="dataSource" type="H2" />
 
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true" />
            </bean>
        </property>
    </bean>
         
    <jpa:repositories base-package="com.devkuma.spring.db" />
     
    <bean id="transactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
</beans>

이번에는 Bean 관련 태그가 2개가 추가되어 있다. 먼저 JpaRepository에 대한 태그가 하나 포함되어 있다.

<jpa:repositories base-package="com.devkuma.spring.db"/>

이 <jpa:repositories> 태그에는 base-package라는 속성이 포함되어 있다. 이제 검색할 위치(패키지)를 지정한다. 이렇게 하면 지정된 패키지 내에서 JpaRepository을 찾아서 Bean 화한다.

다른 하나는 "트랜잭션 관리자"라는 Bean이다. 이것은 트랜잭션을 관리하기 위한 것으로, 다음과 같이 작성되어 있다.

<bean id="transactionManager"
    class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</ bean>

여기에 정의되어 있는 것은 "transactionManager"라는 이름의 Bean으로써, class에는 JpaTransactionManager 클래스를 지정한다. 다른 "entityManagerFactory"라는 속성이 있는데, 이것은 작성하는 Bean에서 사용하는 EntityManagerFactory를 지정한다 (위에 있는 id="entityManagerFactory"를 ref로 지정되어 있다).

이것으로 JpaRepository를 이용하는데 필요한 Bean의 준비가 되었다. "어? 아까 준비한 SampleEntityRepository은 Bean으로 준비해야 하지 않나?"라고 생각할 수 있는데, 이것은 필요하지 않다. <jpa:repositories>에 의해 지정된 패키지를 검색하고 @repository가 지정된 JpaRepository를 Bean으로 자동 등록하게 되는 것이다. 따라서 별도로 <bean>를 준비 할 필요는 없다.


어플리케이션에서 JpaReposiory을 이용

그럼 JpaRepository를 이용해 보기로 하자.

com.devkuma.spring.db 패키지에 있는 App 클래스를 아래와 같이 고쳐 보자.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
    private static EntityManager manager;
     
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
         
        SampleEntityRepository repository = app.getBean(SampleEntityRepository.class);
 
        // make dummy
        makeDummyData();
         
        // get list
        List list = repository.findAll();
        printList(list);
         
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData() {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower"));
        manager.persist(new SampleEntity("taro", "taro@yamada"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy"));
        manager.flush();
        transaction.commit();
    }
 
    // print all entity.
    public static void printList(List list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
}

그리고 실행하자. 그러면 더미로 만들고 있는 모든 엔티티가 표시된다.

여기에서는 먼저 EntityManager로 부터 SampleEntityRepository 인스턴스를 얻는다.

SampleEntityRepository repository =
    app.getBean (SampleEntityRepository.class);

이것으로 Bean을 가져옵니다. dbbean.xml에는 Bean의 정의는 포함되어 있지 않지만, 이렇게 제대로 Bean을 꺼낼 수 있다. 이것도 @Repository 덕분이다.

List list = repository.findAll ();

그리고 "findAll"메소드를 호출해서, 엔티티 목록을 검색한다. "이런 메소드 도대체 어디에 있는거야?"라고 생각한 사람이 있을 것이다. 이는 SampleEntityRepository에서 상속하는 JpaRepository에 준비되어 있기 때문이다.

클래스는 도대체 어디에?

그런데, 여기에 코드를 보고 좀 이상하게 생각한 사람도 있을 것이다. SampleEntityRepository 인터페이스이다. 클래스가 아니다. 그런데 당연한 듯이 Bean을 얻어 오는 것이 가능하다.

JpaRepository는 인터페이스를 준비하기만 하면, 자동으로 클래스를 만들고 Bean을 생성하는 것이다. 필요한 것은 인터페이스 뿐이다.



Bean 설정 클래스를 이용

Bean은 설정 파일을 사용하지 않고, 클래스로 정의 할 수 있다. 그럼 이것도 해보자.

com.devkuma.spring.db 패키지의 "SampleEntityConfig"클래스 소스 코드를 아래와 같이 고쳐 보자.

package com.devkuma.spring.db;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
 
@Configuration
@EnableJpaRepositories("com.tuyano.libro.db")
@EnableTransactionManagement
class SampleEntityConfig {
 
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.H2).build();
    }
 
    @Bean
    public EntityManagerFactory entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
 
        LocalContainerEntityManagerFactoryBean factory = new
            LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.tuyano.libro.db");
        factory.setDataSource(dataSource());
        factory.afterPropertiesSet();
 
        return factory.getObject();
    }
 
    @Bean
    protected JpaTransactionManager transactionManager
            (EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

그리고 App에서 다음의 문장을 수정하여 해당 Bean 설정 파일에서 이 SampleEntityConfig 클래스를 이용하도록 변경하자.

ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");

    ↓

ApplicationContext app = new  AnnotationConfigApplicationContext(SampleEntityConfig.class);

수정하면 App을 실행하여 보자. 앞서 살펴본 바와 같이 더미 데이터가 나열된다. 여기에서는 우선 눈길을 끄는 것은 클래스 선언 앞에 있는 어노테이션이다.

@EnableJpaRepositories("com.devkuma.spring.db")

이것은 JpaRepository를 ON으로 하기 위한 것이다. 이 어노테이션을 작성하는 것으로, 지정된 패키지를 검색하고 @Repository를 붙인 클래스를 Bean으로 등록한다. 괄호 () 내에는 검색할 패키지를 지정해야 한다. <jpa:repositories>에 해당하는 것으로 생각해도 된다.

@EnableTransactionManagement

이것은 TransactionManagement를 사용하기 위한 것이다. 이것을 준비해 두는 것으로 TransactionManager가 ON된다.

@Bean 어노테이션을 붙인 Bean 생성을 위한 메소드으로는 "JpaTransactionManager"라는 메소드가 추가되어 있다. 이것으로 JpaTransactionManager 인스턴스를 만들고 return한다. 보면 알 수 있는 거처럼, new JpaTransactionManager 한 것을 돌려주고있을 뿐이다. 인수는 EntityManagerFactory 인스턴스를 전달하도록 하고 있다.

보면 알 수 있듯이, 준비되어있는 Bean 생성 메소드에는 레파지토리 생성을 위한 것은 아니다. 레파지토리는 자동으로 Bean 화되는 것을 잘 알것이다.


JpaRepository 표준 방법

여기에서는 SampleEntityRepository 인터페이스를 준비했지만, 이것의 내용은 텅 빈 채였다. 그래도 findAll라는 메소드를 호출하여 전체 엔터티를 검색 할 수 있었다.

상속 받을 JpaRepository에는 표준에서 몇 개의 메소드가 정의되어 있다. 우선 그들을 사용하여 데이터베이스의 기본적인 조작이 가능하다. 여기서 주요 방법에 대해 간단하게 정리하겠다.

findAll()

이미 등장했었다. 전체 엔터티를 정리 한 List를 돌려준다.

getOne("ID")

ID를 지정하여 엔터티를 하나를 얻어온다. 인수에는 그 엔터티의 ID에 지정된 형식의 값이 들어간다.

saveAndFlush(엔티티)

인수에 지정된 엔티티를 데이터베이스에 저장한다.

delete("ID")

인수에 지정된 ID의 엔티티를 데이터베이스에서 삭제한다.

count()

엔티티의 수를 int 값으로 반환한다.

검색 대해서는 findAll 밖에 없지만, 엔티티의 저장과 삭제 등의 메소드도 갖추어져 있다. 이 메소드들은 트랜잭션 처리를 시작할 필요도 없다. 단지 메소드를 호출하는 것만으로 간단히 저장하거나 제거 할 수 있다.




JpaRepository에 메소드 추가

데이터베이스의 핵심이 되는 것은 뭐니 뭐니해도 "검색"이다. 필요에 따라 데이터를 범위를 지정하여 검색이 가능할 수 있어야 데이터베이스의 위력을 발휘할 수 있다.

그런데 JpaRepository에는 검색 관련 메소드는 "findAll"밖에 없다. 이걸로는 사용을 할 수 없다라고 생각했을지도 모르겠다. 하지만 실은 JpaRepository의 진정한 위력은 검색에 있는 것이다. 인터페이스에 검색을 위한 메소드를 추가하는 것으로, 필요한 검색 메소드를 늘릴 수 있다.

실제로 해보자. 아래를 참고하여 SampleEntityRepository 인터페이스를 수정하자.

package com.devkuma.spring.db;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface SampleEntityRepository
        extends JpaRepository<SampleEntity, Long> {
 
    public List<SampleEntity> findByNameLike(String name);
    public List<SampleEntity> findByMailEndingWith(String mail);
}

여기에서는 다음의 두 문장을 추가되었다.

public List <SampleEntity> findByNameLike(String name);
public List <SampleEntity> findByMailEndingWith(String mail);

첫 번째 "findByNameLike"는 name의 값을 사용하여 like 검색하기 위한 것이다. 두 번째 "findByMailendingWith"는 mail 값이 지정된 텍스트로 끝나는 엔티티를 검색한다.

그저 메소드 선언문을 작성했을 뿐이지만, 주의해야 하는 것은 메소드 이름입니다. 이것은 마음대로 변경하거나 하지 말아야 한다. 반드시 여기에 올린 이름대로 메소드 선언을 작성하자. 이런 메소드 이름에는 의미가 있다. 변경하면 잘 작동하지 않는다.



JpaRepository의 메소드 이용

그럼 아래를 참고하여 App.java을 변경하도록 하자.

package com.tuyano.libro.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
    private static EntityManager manager;
 
    public static void main(String[] args) {
        //ApplicationContext app = new 
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
        ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
        makeDummyData();
         
        // get repository
        SampleEntityRepository repository = app.getBean(SampleEntityRepository.class);
         
        // get list
        List list1 = repository.findByNameLike("%ko");
        System.out.println("*Find By Name*");
        printList(list1);
        List list2 = repository.findByMailEndingWith(".com");
        System.out.println("*Find By Mail*");
        printList(list2);
         
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData() {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower.uk"));
        manager.persist(new SampleEntity("taro", "taro@yamada.jp"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy.com"));
        manager.flush();
        transaction.commit();
    }
 
    // print all entity.
    public static void printList(List list) { ...생략... }
}

코드를 보면 간단히 필요한 엔티티를 검색 할 수 있다는 것을 알 수 있다.

name으로 검색

List list1 = repository.findByNameLike("%ko");

검색 결과 :

SampleEntity [id=2, name=hanako, mail=hanako@flower.uk]
SampleEntity [id=4, name=sachiko, mail=sachico@happy.com]

mail로 검색

List list2 = repository.findByMailEndingWith(".com");

검색 결과 :

SampleEntity [id=1, name=tuyano, mail=syoda@tuyano.com]
SampleEntity [id=4, name=sachiko, mail=sachico@happy.com]

findByNameLike("%ko")는 name 값이 "ko"로 끝나는 엔티티가 검색된다. 또한 findByMailEndingWith("com")에서는 mail 값이 ".com"으로 끝나는 엔티티가 검색되는 것을 알 수 있다. 제대로 필요한 것만 검색이 가능하다.

여기서 주의 깊게 봤으면 하는 것은 "SampleEntityRepository에는 메소드의 선언을 했을 뿐이다"라는 점이다. 즉, 이러한 메소드에서 실제로 수행하는 처리는 전혀 쓰여 있지 않다는 것이다. 단지 메소드의 선언만으로 제대로 움직이는 메소드가 자동 생성된다.

이것이 JpaRepository의 위력이다. JpaRepository는 미리 정해진 형식의 메소드 이름을 작성하면, 그 이름을 바탕으로 실제로 움직이는 메소드가 자동 생성된다.



JpaRepository의 메소드 명명 규칙

JpaRepository는 메소드 이름 작성 방법만 알다면, 필요한 메소드를 빠르게 쓰고 추가할 수 있다. 처리는 코드 일체 필요 없다. 단지, 메소드 선언문만 있으면 된다. 단지, 미리 정해진 룰에 따라 제대로 메소드 이름을 붙이기만 하면 된다. 적당히 이름을 붙이는 것만으로 메소드가 자동 생성이 되는 것이다.

그럼, 어떤 형태로 메소드 이름을 붙이면 되는지 대해, 여기에 그 이름 지정 규칙에 대해 간략하게 정리하면 아래와 같아

findByXX

기본은 이것입니다. "findBy" 이후에 엔티티의 속성 이름을 붙이다. 이 속성 이름은 첫 글자는 대문자로 한다. 예를 들어, name 검색한다면 "findByName"이며, mail에서 찾는다면 "findByMail"가 된다.

이 다음에는 기본형인 'findByXX" 이후에 계속 이어서 쓰면 된다.

Like / NotLike

"퍼지 검색"에 관한 것이다. Like를 붙이면, 인수에 지정된 텍스트를 포함하는 엔티티를 검색한다. 또한 NotLike을 쓰면 인수의 텍스트를 포함하지 않는 것을 검색한다. "findByNameLike"이라면, name에서 인수의 텍스트를 퍼지 검색한다.

StartingWith / EndingWith

텍스트 값에서 인수에 지정된 텍스트로 시작하거나 끝나는 것을 검색하기 위한 것이다. findByNameStartingWith("A")이라면, name의 값이 "A"로 시작하는 항목을 검색한다.

IsNull / IsNotNull

값이 null이 거나, 혹은 null이 아닌 것을 검색한다. 인수는 필요없다. "findByNameIsNull()"이라면, name의 값이 null의 것만 검색한다.

True / False

부울 값으로 true 인 것, 혹은 false 인 것을 검색한다. 인수는 필요없다. "findByCheckTrue()"이라면, check라는 항목이 true 인 것만을 검색한다.

Before / After

시간 값으로 사용한다. 인수에 지정한 값보다 이전의 것, 혹은 이후 것을 검색한다. "findByCreateBefore(new Date())"라고 하면, create라는 항목의 값이 현재보다 이전의 것만을 찾는다 (create가 Date 인 경우).

LessThan / GreaterThan

숫자 값으로 사용한다. 그 항목의 값이 인수보다 작거나 큰 것을 검색한다. "findByAgeLessThan(20)"이라면, age의 값이 20보다 작은 것을 찾는다.

Between

두 값을 인수로 가지고 그 두 값 사이의 것을 검색한다. 예를 들어, "findByAgeBetween(10, 20)"라고 한다면 age 값이 10 이상 20 이하인 것을 검색한다. 수치뿐만 아니라 시간의 항목 등에도 사용할 수 있다.

이 외에도 아직 더 있지만, 일단 이것들을 사용할 수 있다면 기본적인 검색은 대체로 가능하게 될 것이다. JpaRepository를 잘 다룰 수 있게 되면 코딩이 많이 없어도 다양한 데이터 검색을 할 수 있다. 매우 간단하므로 샘플을 기반으로 여러가지 시도해 보길 바란다.

'Spirng' 카테고리의 다른 글

[Spring] JpaRepository 이용  (1) 2017.12.10
[Spring] CRUD 기본  (0) 2017.12.10
[Spring] Spring Data JPA 이용  (0) 2017.12.10
[Spring] AspectJ 이용  (0) 2017.12.10
[Spring] AOP 이용  (0) 2017.12.10
[Spring] 어노테이션으로 DI 구현  (0) 2017.12.10
  1. 맹구Kim 2019.01.21 19:44 신고

    감사합니다. 많은 도움되었습니다!

데이터 생성(Create), 검색(Read), 갱신(Update), 삭제(Delete)의 4 가지 데이터베이스 액세스의 기본이라고 할 수 있다. 이러한 기본 작업에 대해 설명한다.


Query로 데이터의 목록 조회

데이터베이스 조작의 기본은 "CRUD"라고 한다. 이전에 더미 데이터 몇개를 저장하여 데이터 목록을 표시하는 예제를 만들었다.

(참고 : Spring Data JPA 이용 - 데이터베이스를 이용해 보자.)

예제를 보면 'Create'와 'Read'의 기본은 이미 만들어져 있을 것이다. 그럼 어떻게 했는지 살펴 보자.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        EntityManager manager = factory.createEntityManager();
 
        makeDummyData(manager);
 
        // get list
        Query query = manager.createQuery("from SampleEntity");
        List list = query.getResultList();
        printList(list);
 
        System.out.println("...ok.");
    }
 
    public static void makeDummyData(EntityManager manager) { ...생략... }
 
    public static void printList(List list) { ...생략... }
}

먼저 "데이터 목록 가져 오기"이다. 여기에는 "Query"라는 클래스를 이용하고 있다.

Query query = manager.createQuery("from SampleEntity");

이것은 "Query"라는 클래스의 인스턴스를 만들고, 해당 메서드를 호출하여 전체 엔터티 목록을 검색한다. Query라는 것은 SQL 쿼리 문장의 단순화 된 버전과 같은 문장을 사용하여 데이터베이스에 액세스하기 위한 것이다.

인스턴스 생성은 new 대신 "createQuery"라는 메소드를 이용한다. 인수는 "from SampleEntity" 텍스트을 넣었다. 이것이 SampleEntity 엔티티를 모두 취득하는 것을 나타내는 구문이다.

List list = query.getResultList();

이렇게 작성한 Query의 "getResultList"을 호출하면, 가져온 엔티티를 목록으로 얻을 수 있다. 그 다음, l결과 목록(list)에서 부터 차례로 엔티티를 꺼내 처리하면 된다.

createQuery 인수로 어떤 텍스트를 지정하면 어떤 결과를 얻을 수 있을까? 라는 것이 제일 문제이다. 우선 여기에서는 "from 엔티티"라는 형태로 쓰면 그 엔터티 목록을 얻을 수 있다는 사실만 기억하자.


새로운 엔티티 추가

이어서 신규 엔터티를 저장한다. 이는 생각하기에는 매우 간단하다. 엔티티 클래스의 인스턴스를 만들고 EntityManager에 있는 저장용의 메소드를 호출하기만 하면 된다.

다만! 데이터베이스 조작은 단순히 데이터를 읽기만 한다면 간단하지만 데이터를 갱신하는 경우에는 주의가 필요하다. 데이터베이스는 동시에 여러 곳에서 액세스되는 경우도 있다. 따라서 다른 데이터가 액세스되는 동안에 데이터가 갱신이 되거나 한다면 문제가 일어날 수 있다.

따라서 데이터의 갱신이 필요한 경우에는 "트랜잭션"이라는 것을 사용해야 한다. 트랜잭션은 여러 처리를모와서 실행 할 수 있도록하는 기능이다.

트랜잭션을 지정하는 동안에는 그 처리를 실시하고 있는 해당 데이터에 대해서, 외부에서 액세스를 할 수 없게 된다. 트랜잭션 처리가 완료된 후에는 외부에서 부터 데이터 액세스 가능해 진다.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        EntityManager manager = factory.createEntityManager();
 
        makeDummyData(manager);
 
        // get list
        Query query = manager.createQuery("from SampleEntity");
        List list = query.getResultList();
        printList(list);
 
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData(EntityManager manager) {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower"));
        manager.persist(new SampleEntity("taro", "taro@yamada"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy"));
        manager.flush();
        transaction.commit();
    }
 
    public static void printList(List list) { ...생략... }
}

트랜잭션에 의해서 특정 작업을 수행하는 동안, 외부에서 액세스 할 수 없게 하여 안전하게 데이터베이스를 업데이트 할 수 있다는 것이다. 그럼, 처리의 흐름을 살펴 보자.

EntityTransaction transaction = manager.getTransaction ();

EntityManager의 "getTransaction"은 EntityTransaction라는 클래스의 인스턴스를 가져온다. 이것이 트랜잭션 클래스이다.

transaction.begin ();

"begin"메소드를 실행하여 트랜잭션 처리를 시작한다.

manager.persist (new SampleEntity ( "tuyano", "syoda@tuyano.com"));
manager.persist (new SampleEntity ( "hanako", "hanako @ flower"));
manager.persist (new SampleEntity ( "taro", "taro @ yamada"));
manager.persist (new SampleEntity ( "sachiko", "sachico @ happy"));

엔티티의 신규 추가는 EntityManager의 "persist"라는 메서드를 사용한다. 인수에 엔티티 클래스의 인스턴스 (여기에서는 SampleEntity)을 지정하여 호출하는 것으로, 그 엔티티를 데이터베이스에 저장한다.

manager.flush ();
transaction.commit ();

마지막으로 EntityManager의 'flush'을 호출해서 버퍼를 비우고, EntityTransaction의 "commit"메서드를 호출하여 커밋하면 persist한 엔티티가 전부 데이터베이스에 저장된다. 커밋을 하면 바로 트랜잭션 처리는 종료되고, 데이터베이스는 개방된다.

데이터 추가는 트랜잭션에 대한 사용법을 모르면 안된다. 이것은 데이터베이스 갱신의 기본이므로 여기에서 기본적인 사용법을 꼭 기억해 두도록 하자.


엔티티 수정

이어 엔터티의 변경이다. 이미 데이터베이스에 저장되어 있는 데이터를 업데이트하려면 해당 데이터 엔티티를 가져와서, 그 값을 변경해서 저장하는 작업을 해야한다.

아래와 같이 간단한 예제를 작성해 놨다.

package com.devkuam.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class App {
    private static EntityManager manager;
 
    public static void main(String[] args) {
        ApplicationContext app = new
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
 
        // make dummy
        makeDummyData();
         
        // update entity
        updateEntity(1L);
        updateEntity(3L);
 
        // get list
        Query query = manager.createQuery("from SampleEntity");
        List list = query.getResultList();
        printList(list);
 
        System.out.println("...ok.");
    }
 
    public static void makeDummyData() { ...생략... }
    public static void printList(List list) { ...생략... }
     
    // update entity
    public static void updateEntity(long id) {
        SampleEntity entity = manager.find(SampleEntity.class, id);
        entity.setName("**update name**");
        entity.setMail("**update@mail**");
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.merge(entity);
        manager.flush();
        transaction.commit();
    }
}

실행을 하면 4개의 더미 데이터 중에 2개의 내용을 변경해서 출력하고 있다.

변경 처리는 updateEntity 메서드로 정리하고 있다. 이는 데이터를 변경하는 ID 번호를 인수에 전달할 수 있게끔 되어 있다.

그럼 처리의 흐름을 살펴보자.

EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
manager = factory.createEntityManager();

먼저, EntityManager 인스턴스를 얻어오고, 그것을 사용하여 처리를 하고 있다.

SampleEntity entity = manager.find (SampleEntity.class, id);

SampleEntity 인스턴스를 얻어온다. 이것은 new로 새롭게 만드는 것이 아니라 EntityManger의 "find"메소드를 호출한다. find는 특정 ID의 엔티티를 검색하는 것이다. 이것은 인수에 엔티티 클래스의 class 값과 검색 엔티티의 ID 값을 각각 지정한다.

먼저 목록 리스트를 얻어오는 경우에는 다음 처리 등을 생각하면 귀찮긴 하지만, 이렇게 EntityManager 의 메소드를 사용하면 ID를 지정하는 것만으로 엔티티를 얻을 수 있다. (다만, ID를 모르는 상태에서는 이를 사용할 수 없다.)

다음에 얻어온 엔티티의 메소드를 호출하여 내용을 업데이트한다.

entity.setName ( "** update name **");
entity.setMail ( "** update @ mail **");

편집이 끝나면 신규 작성과 같이 트랜잭션을 사용하여 업데이트 처리를 한다. 이것은 기본적으로 신규 작성을 하는 흐름과 같지만, 갱신하는 부분만 다르다.

EntityTransaction transaction = manager.getTransaction ();
transaction.begin ();
manager.merge (entity);
manager.flush ();
transaction.commit ();

이미 저장되어 있는 엔터티의 변경은 "merge"라는 메소드를 사용해 실시한다. 인수에 업데이트하는 엔터티 클래스의 인스턴스를 지정하면 해당 엔티티의 값을 최신 상태로 업데이트한다.

그 후에는 EntityManager를 flush하고 EntityTransaction을 commit하면 작업 완료이다. 업데이트는 신규 작성과 달리, find에서 특정 ID의 엔티티를 얻어서, merge 업데이트 처리를 하면 된다.



엔티티 삭제

남은 것은 엔티티의 삭제이다.

삭제도 기본적으로는 엔티티의 업데이트와 동일하다. ID를 지정하여 엔티티의 인스턴스를 얻어와서, 삭제하고 트랜잭션을 커밋하는 방식을 한다.

아래와 같이 간단한 예제를 작성해 놨다.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class App {
    private static EntityManager manager;
 
    public static void main(String[] args) {
        ApplicationContext app = new
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
 
        // make dummy
        makeDummyData();
         
        // delete entity
        deleteEntity(1L);
        deleteEntity(3L);
 
        // get list
        Query query = manager.createQuery("from SampleEntity");
        List list = query.getResultList();
        printList(list);
 
        System.out.println("...ok.");
    }
 
    public static void makeDummyData() { 略 }
    public static void printList(List list) { 略 }
     
    // delete entity
    public static void deleteEntity(long id) {
        SampleEntity entity = manager.find(SampleEntity.class, id);
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.remove(entity);
        manager.flush();
        transaction.commit();
    }
}

실행하면 ID=1, 3의 요소를 제거하고 남은 2 개를 표시한다. 그럼 이것도 흐름을 정리해 보자.

SampleEntity entity = manager.find (SampleEntity.class, id);

먼저 EntityManager의 "find"를 사용하여 지정된 ID의 엔티티 (여기에서는 SampleEntity)를 얻어온다.

EntityTransaction transaction = manager.getTransaction ();
transaction.begin ();

이어서 EntityTransaction 인스턴스를 얻어와서 트랜잭션 처리를 시작한다.

manager.remove(entity);

엔티티를 삭제한다. 이는 EntityManager의 "remove"메서드를 사용한다. 인수에 데이터베이스에서 검색한 엔티티의 인스턴스를 지정하면, 데이터베이스에서 해당 엔티티를 제거한다.

manager.flush ();
transaction.commit ();

마지막으로 EntityManager를 flush하고 EntityTransaction을 commit하면 데이터베이스 갱신이 완료 데이터가 삭제된다. 엔터티의 변경을 알면 제거도 거의 같은 느낌으로 실행할 수 있다.



JPQL 쿼리 문장의 기본

이것으로 CRUD 기본 조작은 할 수 있게 되었다. 여기서 조금 더 알아 두지 않으면 안되는 것은 기본 중에 기본인 "Read"부분이다.

앞의 예제에서는 모든 엔티티를 얻어오는 간단한 샘플이 있었다. 이보다 실제 처리는 더 조건을 세밀하게 지정하여 엔티티를 검색할 필요가 있을 것이다.

엔티티를 얻어올 때, Query라는 클래스를 했었다. 이 Query 인스턴스를 만들 때 "from SampleEntity"라는 문장을 인수로 넣었다. 이 문장은 SQL쿼리 문장과 같은 기능을 하는 것으로, "JPQL"이라고 한다. 다만, SQL과 약간 작성이 다르다. 우선, 엔티티 검색의 기본적인 작성법에 대해 설명을 하겠다.

JPQL의 검색의 기본

from 엔티티 where 조건식

SQL에서도 친숙한 where 절을 사용하면 기본적인 검색은 거의 할 수 있다. 조건이 되는 수식은 등호 등의 비교 연산자 ( =<> )를 사용하여 작성한다.

ID가 1 인 엔티티를 반환

from SampleEntity where id = 1

ID가 2보다 큰 엔터티를 반환

from SampleEntity where id > 2

또한, 텍스트를 검색하려면 퍼지(fuzzy) 검색 "like"를 사용할 수 있다. %기호를 사용하여 와일드 카드의 지정도 가능합니다.

name이 'K'로 시작하는 엔터티 반환

from SampleEntity where name like 'K %'

여러 조건식을 설정하려면, "and", "or"를 사용할 수 있다. where는 "조건식 and 조건식'와 같은 형식으로 여러 조건식을 and/or로 이어 간다.

id가 10이상 20이하의 엔티티를 취득

from SampleEntity where id> = 10 and id <= 20

우선, 이러한 기본적인 검색 방법을 알면 초보적인 데이터 검색의 처리는 대체로 수 있게 될 것이다. 실제로 JPQL 쿼리 문장을 만들어서 실행해 보면 재미있다.

'Spirng' 카테고리의 다른 글

[Spring] JpaRepository 이용  (1) 2017.12.10
[Spring] CRUD 기본  (0) 2017.12.10
[Spring] Spring Data JPA 이용  (0) 2017.12.10
[Spring] AspectJ 이용  (0) 2017.12.10
[Spring] AOP 이용  (0) 2017.12.10
[Spring] 어노테이션으로 DI 구현  (0) 2017.12.10

데이터베이스 액세스에 대한 프레임워크로 Spring Framework에 포함되어 있는 것이 "Spring Data JPA "이다. 이것은 문장 그대로 JPA를 이용하여 데이터 액세스를 위한 것이다. 그 기본적인 사용법에 대해 설명한다.


Spring Data JPA와 pom.xml

데이터베이스 관련은 다양한 라이브러리와 프레임워크가 가장 많이 포함되어 있는 분야이다. Hibernate 등 ORM(Object Relational Mapping)은 SQL 등을 사용하는 데이터베이스의 액세스와 Java의 Objective 코드와 일관성을 위한 기술로 널리 사용되고 있다.

그러한 ORM 관련 기술 중에서도 Java의 순정 기술으로 스며들어 있는 것이 "JPA(Java Persistence API)"이다. 아무튼, JPA 자체는 ORM을 위한 기술이라기 보다는 SQL을 Java 나름대로 쉽게 사용할 것이라는 느낌이지만, 데이터베이스 및 Java 오브젝트의 중개 역할을 위한 기본적인 기술로 많은 프레임워크에서도 사용되고 있다.

Spring Framework에도 JPA를 이용하여 데이터베이스에 액세스를 하는 'Spring Data JPA'라는 라이브러리가 포함되어 있다. 이것은 "Spring Data"라는 데이터베이스 액세스 관련 라이브러리 중 하나이다. 이 밖에 비(非)SQL인 MongoDB를 이용하는 "Spring Data MongoDB"와 Hadoop 이용을 위한 "Spring Data Hadoop"등의 것이 준비되어 있다. Spring Data JPA는 일반적인 SQL 의한 관계형 데이터베이스 전반을 이용하기 위한 것으로, Spring Data의 기본이 되는 라이브러리라고 해도 좋을 것이다.

그럼, 이 Spring Data JPA를 이용하여 보기로 하자. 우선 pom.xml에 라이브러리 추가한다. 아래와 같이 소스 코드를 재 작성한다. 예제로, Spring Framework 4.1.7에 맞는 버전으로 구성되어 있다.

<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.springframework.samples</groupId>
    <artifactId>MySpringApp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <properties>
        <java.version>1.6</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
    <dependencies>
 
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.7.RELEASE</version>
        </dependency>
 
        <!-- Database (H2) -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.187</version>
        </dependency>
 
        <!-- JPA Provider (Hibernate) -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.10.Final</version>
        </dependency>
 
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.8.1.RELEASE</version>
        </dependency>
 
    </dependencies>
</project>

이번에는 총 3가지의 <dependency> 태그를 새롭게 추가하였다. 각각 간단히 설명을 하겠다.

H2

그룹 ID에 "com.h2database" 아티팩트(Artifact) ID에 "h2"를 지정하고 있다. H2는 Java SQL 데이터베이스 엔진이다. 데이터베이스 파일에 직접 액세스하여 데이터를 저장할 수 있다. 보통 데이터베이스라고 하면 MySQL이나 PostgreSQL 등이 떠오를 것이다. Java 라이브러리로 구현된 H2와 같은 데이터베이스 엔진은 데이터베이스 서버의 설치 등이 번거로운 일을 할 필요가 없고, 부담없이 이용할 수 있으므로, 학습용으로 적합하다. 그래서 이번에는 H2를 데이터베이스에 이용하여 Spring Data JPA를 사용한다.

Hibernate EntityManager

JPA에는 "엔티티"라는 형태로 데이터베이스의 데이터를 오브젝트화한다. 이 엔티티를 관리하는 것이 엔티티 관리자(EntityManager)이다. 이것은 여러 가지가 있지만, 스스로 사용하기 쉬운 것을 선택해서 구성 이용 가능하도록 되어 있다. 이번에는 Hibernate가 생성하는 엔티티 관리자를 사용하고 있다. 그룹 ID "org.hibernate" Artifact ID은 "hibernate-entitymanager"로 지정한다.

Spring Data JPA

이것이 Spring Data JPA의 본체 라이브러리이다. 그룹 ID는 "org.springframework.data", 아티팩트 ID가 "spring-data-jpa"을 각각 지정한다.

이상 3개의 라이브러리를 pom.xml에 추가하여 Spring Data JPA를 사용할 수 있게 된다. H2는 다른 데이터베이스 서버를 사용하는 경우는 물론 필요하지 않는다.



persistence.xml 생성

우선 Bean 설정 파일을 이용하는 형태로 Spring Data JPA를 사용해보기로 하자.

Spring Data JPA를 사용하려면 프로젝트에 몇 가지 준비해야 하는 것이 있다. 그 중 하나가 "persistence.xml"라는 파일이다. 이 파일은 "지속성 단위(Persistence unit)"이라고하는 정보를 포함하고 있다.

JPA에서 데이터베이스 액세스를 실행하기 위하여 "지속성"이라는 것을 제공한다. 즉, 이 persistence.xml 안에는 액세스하는 데이터베이스에 대한 정보를 기술해 두는 것이라고 생각하면 된다.

이 파일은 Web 어플리케이션의 "META-INF" 폴더에 저장한다. Maven 프로젝트인 경우에는 "src"의 "main"에 있는 "resources"폴더가 Web 어플리케이션의 디렉토리이다. 이 "resources"폴더에 "META-INF"라는 폴더를 생성하고, 그 안에 "persistence.xml '라는 이름으로 파일을 만들고 아래 목록 아래의 내용을 작성하자.

<?xml version="1.0" encoding="UTF-8"?>
<persistence
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence.xsd">
 
    <persistence-unit name="persistence-unit"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.diarect" value="${hibernate.dialect}" />
            <property name="hibernate.hbm2ddl.auto" value="${hibernate.hbm2ddl.auto}" />
            <property name="javax.persistence.jdbc.driver" value="${db.driver}" />
            <property name="javax.persistence.jdbc.url" value="${db.url}" />
            <property name="javax.persistence.jdbc.user" value="${db.user}" />
            <property name="javax.persistence.jdbc.password" value="${db.password}" />
        </properties>
    </persistence-unit>
     
</persistence>

이 persistence.xml에서 <persistence> 태그 내에 <persistence-unit> 태그를 넣는다. 이것이 지속성 단위의 정보를 기술하는 태그이다. 이 안에 <provider> 태그에 "공급자" 클래스를 지정하고, 추가로 <properties> 태그에 필요한 정보를 속성으로 기술하고 있다. 기술되어 있는 속성은 다음과 같다.

hibernate.diarect
hibernate.hbm2ddl.auto

이것들은 HibernatePersistence에 필요하다. Dialect는 데이터베이스와 상호 작용을 하는 클래스이며, 시작 및 종료시 데이터베이스 처리에 대한 설정이다. 여기에서는 H2를 이용하기 위한 Dialect와 시작할 때 새로운 데이터베이스를 준비하고, 종료시 삭제하는 설정을 하고 있다.

javax.persistence.jdbc.driver
javax.persistence.jdbc.url
javax.persistence.jdbc.user
javax.persistence.jdbc.password

이러한 데이터베이스 드라이버, 액세스 URL, 액세스에 사용하는 사용자 이름과 비밀번호 등을 각각 설정해 두는 것이다.

준비되어있는 속성은 사용하는 엔티티 관리자에 의해 다소 달라질 수 있다. 이번에는 Hibernate 라이브러리를 이용하고 있기 때문에, 그것은 전용 값이 속성에 추가되어 있다. 다른 엔티티 관리자를 사용하는 경우에는 또 다른 속성을 사용할 필요가 있을지도 모른다.



application.properties 작성

생성한 persistence.xml의 내용을 살펴보면 <property> 태그에 포함되어있는 속성의 값은 ${XX} 형태로 쓰여 있다. 이것은 속성 파일에 포함되어 있는 값을 집어 넣어서 사용하게 되어있는 것이다.

프로젝트는 "resources"폴더에 "application.properties"라는 속성 파일이 생성하다. 여기에 persitence.xml에 필요한 값을 기술하는 것이다. 아래와 내용으로 작성 하자.

#Database Configuration
db.driver=org.h2.Driver
db.url=jdbc:h2:mem:datajpa
db.username=sa
db.password=
  
#Hibernate Configuration
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create-drop

여기에서는 "db. XX"와 같은 항목과 "hibernate. XX"와 같은 항목이 작성되어 있습니다. db로 시작하는 항목은 데이터베이스 액세스에 대한 기본 설정이다. 데이터베이스 드라이버, 클래스 액세스하는 URL, 사용자 이름, 비밀번호가 있다.

H2에 액세스의 경우, 드라이버는 org.h2.Driver라는 클래스를 지정한다. 또한 액세스 URL은 jdbc:h2:mem:datajpa로 하였다. 이것은 H2 메모리에 "datajpa '라는 이름으로 데이터베이스를 준비하는 것을 의미한다. 메모리에 만드는 프로그램 종료 후에는 깨끗이 데이터베이스는 사라진다. 학습용으로는 최적의 사용 방법이다.

만약 메모리가 아닌 하드 디스크에 파일로 저장하고 싶다면 "jdbc:h2:경로"라는 형태로 쓰면 된다. 예를 들어, jdbc:h2:/db/dbdata으로 쓰게 되면 디스크의 "db"폴더에 "dbdata"라는 파일을 만들고 거기에 데이터를 저장한다.

Hibernate 관련은 Diarect 값으로 org.hibernate.dialect.H2Dialect를 지정하고 있다. 이것은 H2용 Dialect 클래스이다. 또 hibernate.hbm2ddl.auto 값에 create-drop 지정있어서, 프로그램 시작시 데이터베이스를 생성하고 종료할 때 삭제하는 것을 의미한다. 이번은 H2의 메모리에 저장하고 있기 때문에, 스펙에 관계없이 종료하면 데이터베이스가 삭제된다.


Bean 설정 파일 생성

다음 준비하는 것은 Bean 설정 파일이다. Spring Data JPA를 사용하는 경우에는 "엔티티 관리자"라는 것을 사용하여 액세스한다. 이것을 생성하기 위해 "엔티티 관리자 팩토리'라는 Bean을 설정 파일에서 추가해야 한다..

그럼 "resources"폴더에 새로운 "dbbean.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:jpa="http://www.springframework.org/schema/data/jpa"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd  
        http://www.springframework.org/schema/data/jpa 
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
 
    <jdbc:embedded-database id="dataSource" type="H2" />
 
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true" />
            </bean>
        </property>
    </bean>
         
</beans>

이번에는 JPA와 JDBC 태그를 사용하기 위한 속성이 <beans> 태그에 여러가지가 추가되어 있다. 이 태그에는 2개의 Bean 설정 태그가 준비되어 있다.

<jdbc:embedded-database> 태그

이것은 데이터베이스의 데이터 정보(데이터 소스) 오브젝트에 대한 것이다. id="dataSource"라고 명시되어 있고, 그 엔티티 관리자 팩토리에서 이 Bean을 사용하기 위한 ID이다. 또한 type="H2"은 데이터베이스로 H2를 사용하고 있음을 나타낸다.

<bean id="entityManagerFactory"> 태그

엔티티 관리자 팩토리의 Bean 정의한다. 클래스에 org.springframework.orm.jpa 패키지의 LocalContainerEntityManagerFactoryBean라는 클래스를 지정한다. 속성으로 다음과 같은 항목을 사용할 수 있어야 한다.

  • dataSource : <jdbc : embedded-database> 마련한 Bean이 지정된다.
  • jpaVendorAdapter : JPA 벤더 어댑터라는 것을 지정한다. 여기에서는 HibernateJpaVendorAdapter 클래스의 Bean을 사용할 수 있어야 한다. 또한, 이 Bean에는 generateDdl이라는 속성을 제공하고 데이터베이스를 자동으로 생성할 수 있도록 해야 한다.

여러가지 Bean이 조합되어 있지만, 결국 모두가 "엔티티 관리자"라는 것을 사용할 수 있도록 하기 위한 것이다라는 점을 잘 이해하도록 하자. 또한 entityManagerFactory 속성은 우리는 Hibernate의 것을 이용하고 있지만, 다른 라이브러리를 사용할 수도 있다. 그런 경우 사용하는 업체 어댑터에 맞추어 주어야 한다.



엔티티 클래스 생성

자, 이제 드디어 Spring Data JPA를 이용하기 위한 사전 준비가 되었다. 여기에서 실제로 Spring Data JPA를 이용하기 위한 코딩에 들어간다.

Spring Data JPA는 데이터베이스에 작성되는 테이블을 다루기 위하여 "엔티티"라는 클래스를 사용한다. 엔티티는 테이블의 구조를 Java 클래스로 재구성 한 것이다. 동시에 테이블에서 검색되는 데이터들도 엔티티의 인스턴스로 처리된다.

아래에 간단한 엔터티 클래스의 샘플로 com.devkuma.spring.db 패키지 "SampleEntity"라는 클래스로 작성하자.

package com.devkuma.spring.db;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class SampleEntity {
     
    @Id
    @Column
    @GeneratedValue(strategy=GenerationType.AUTO)
    private long id;
     
    @Column(length=50, nullable=false)
    private String name;
     
    @Column(length=100, nullable=true)
    private String mail;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getMail() {
        return mail;
    }
 
    public void setMail(String mail) {
        this.mail = mail;
    }
 
    public long getId() {
        return id;
    }
     
    public SampleEntity() {
        super();
    }
     
    public SampleEntity(String name, String mail) {
        this();
        this.name = name;
        this.mail = mail;
    }
     
    public String toString() {
        return "SampleEntity [id=" + id + ", name=" + name + ", mail=" + mail + "]";
    }
}

엔티티 클래스에서는 테이블에 포함되는 항목을 그대로 필드로 제공한다. 즉, "테이블=클래스", "컬럼=필드 "라는 느낌으로 정의한다. 주의 할 점은 오직 정의 할뿐만 아니라 각각에 어노테이션을을 붙여 있다는 것이다. 이는 다음에 정리하였다.

@Entity

엔티티 클래스에 붙인다. 이 어노테이션을 붙이는 것으로,이 클래스가 엔터티 클래스임을 나타낸다.

@Column

필드에 붙인다. 테이블의 칼럼을 나타내기 위한 것이다. 이것을 붙인 필드는 그 이름과 같은 칼럼의 값을 저장하는 것으로 간주된다. 따라서, 칼럼의 형태와 같은 형태의 값이 아니면 안됩니다. 또한 샘플은

@Column(length=50, nullable=false)

이런 식으로 있다. 이것은 ( ) 안에 칼럼 특성을 기술하고 있는 것이다. 이런 방식으로 그 칼럼에 설정해야 속성도 함께 어노테이션을 작성할 수 있다.

@Id

이 어노테이션은 최초의 id 필드에만 붙일 수 있다. 기본 키를 나타 내기위한 것이며, 이것을 붙인 필드는 그 테이블의 기본 키 컬럼임을 나타낸다.

@GeneratedValue (strategy = GenerationType.AUTO)

이것도 id 필드에 붙여 있다. 이것은 값의 자동 생성에 관한 것이다. strategy=GenerationType.AUTO 값을 지정하면 값을 자동으로 설정하도록 한다.

추가되어 있는 필드는 모두 private으로되어 있으며, Setter/Getter 메소드에 액세스 할 수 있게 되어 있다. 그 외에 두 종류의 생성자와 toString 메소드가 있지만, 이들은 엔티티에 필수는 아니다. 필요에 따라 준비하면 된다.


데이터베이스 이용

그러면 실제로 데이터베이스를 이용하여 보기로 하자. com.devkuma.spring.db 패키지에 "App"클래스를 만들고 아래와 같이 소스 코드를 작성한다.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        EntityManager manager = factory.createEntityManager();
 
        makeDummyData(manager);
 
        // get list
        Query query = manager.createQuery("from SampleEntity");
        List list = query.getResultList();
        printList(list);
 
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData(EntityManager manager) {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower"));
        manager.persist(new SampleEntity("taro", "taro@yamada"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy"));
        manager.flush();
        transaction.commit();
    }
 
    // print all entity.
    public static void printList(List list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
}

완성되면 실제로 App 클래스를 실행하자. 그러면 다음과 같이 텍스트가 출력되는 것을 볼 수 있다.

SampleEntity [id = 1 name = tuyano, mail=syoda@tuyano.com]
SampleEntity [id = 2 name = hanako, mail = hanako @ flower]
SampleEntity [id = 3 name = taro, mail = taro @ yamada]
SampleEntity [id = 4 name = sachiko, mail = sachico @ happy]
... ok.

이것은 더미(dummy)로 마련한 엔티티를 데이터베이스에서 검색하여 표시하고 있는 것이다. 여기에서는 더미 데이터를 생성하기 위한 makeDummyData 메소드와 취득한 목록의 내용을 출력하는 printList 메소드가 준비되어 있다. 그럼 간단히 정리해 보자.

1. ApplicationContext 생성

ApplicationContext app =
    new ClassPathXmlApplicationContext( "dbbean.xml");

우선은, 언제나 만들었던 ApplicationContext 인스턴스를 준비한다. 여기에서는 dbbean.xml 파일 지정했다..

2. EntityManagerFactory 인스턴스의 취득

EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);

Bean 설정 파일에 준비해 놓은 엔티티 관리자 팩토리 Bean을 얻어 온다. 여기에서는 EntityManagerFactory 클래스의 class 값으로 인수를 지정했다.

3. EntityManager 작성

EntityManager manager = factory.createEntityManager();

준비한 EntityManagerFactory에서 EntityManager를 가져온다. 이것은 createEntityManager라는 메소드를 호출할 뿐이다.

이것으로 EntityManager가 준비되었다. 다음에는 이를 사용하여 더미 데이터를 저장하거나 혹은 전체 엔티티를 목록으로 얻어 올수 있다.

우선, 이번은 "데이터베이스 이용의 기본"이라고 할 있는 것으로, 중요한 EntityManager를 꺼낼 오는 것까지 알면 된다. 구체적인 데이터베이스 액세스는 다음에 설명 할 예정이다.



Bean설정 클래스 이용

Bean 설정 파일을 사용하지 않고 클래스로 정의할 수 있다. 이전에 dbbean.xml에서 엔티티 관리자 팩토리를 준비했지만 클래스를 사용하여 같은 것을 해보자.

여기에서는 com.devkuma.spring.db 패키지에 "SampleEntityConfig"라는 클래스를 만들고, 거기에 Bean 생성 메소드을 준비하도록 하자. 아래와 같이 샘플로 아래와 같이 소스 코드를 재 작성하자.

package com.devkuma.spring.db;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
 
@Configuration
class SampleEntityConfig {
 
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = 
            new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.H2).build();
    }
 
    @Bean
    public EntityManagerFactory entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = 
            new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
 
        LocalContainerEntityManagerFactoryBean factory = 
            new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.devkuma.spring.db");
        factory.setDataSource(dataSource());
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
}

그리고 App 클래스에 있는 다음의 문장을 수정한다.

ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");

    ↓

ApplicationContext app = new AnnotationConfigApplicationContext(SampleEntityConfig.class);

이것으로 SampleEntityConfig 클래스를 사용하여 Bean을 작성 할 수 있다. dbbean.xml은 불필요해 졌다. 그뿐만 아니라 persistence.xml도 application.properties도 사실 필요가 없어졌다 (application.properties없이 클래스에 직접 값을 쓰고 있기 때문이다). 클래스를 사용하는 것이 보다 깔끔하게 엔티티 관리자를 준비할 있다는 것을 알 수 있다. 그럼 포인트를 정리해 보자.

DataSource의 작성

EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.H2).build();

DataSource는 EmbeddedDatabaseBuilder라는 클래스의 인스턴스를 만든다. setType에서 데이터베이스 종류을 설정하고, build를 호출하면 생성된다.

EntityManagerFactory 만들기

이것은 조금 귀찮은 작업을 해야 한다. 여기에서 하고 있는 작업을 대충 정리하면 이런 느낌이 될 것이다.

  • HibernateJpaVendorAdapter 인스턴스를 생성한다.
  • LocalContainerEntityManagerFactoryBean 인스턴스를 생성한다.
  • setJpaVendorAdapter에서 LocalContainerEntityManagerFactoryBean에 HibernateJpaVendorAdapter를 설정한다.
  • 그밖에 setPackagesToScan, setDataSource 같은 방법으로 속성을 설정한다.
  • afterPropertiesSet를 호출한다.
  • getObject에서 EntityManager 인스턴스를 가져온다.

이것 저것 해야 할 일이 있어서 귀찮은 느낌이 있긴 있지만, Java 코드만으로 (XML 파일 등) 다른 아무것도 필요없이 Bean 생성 할 수있는 것을 생각하면, Java 프로그래머으로써는 이쪽 방식이 더 편할 것이다.

'Spirng' 카테고리의 다른 글

[Spring] JpaRepository 이용  (1) 2017.12.10
[Spring] CRUD 기본  (0) 2017.12.10
[Spring] Spring Data JPA 이용  (0) 2017.12.10
[Spring] AspectJ 이용  (0) 2017.12.10
[Spring] AOP 이용  (0) 2017.12.10
[Spring] 어노테이션으로 DI 구현  (0) 2017.12.10

AOP를 실현하는 것으로, Java에서는 "AspectJ"라는 라이브러리가 널리 사용되고 있다. Spring AOP에서이 AspectJ를 이용한 AOP 처리에 대해 설명한다.


AspectJ와 pom.xml 수정

AOP에 대해 조사해 보면, 아마 "AspectJ"라는 소프트웨어에 대해 많이 찾게 될것이다. AspectJ는 Java AOP 소프트웨어의 사실상의 표준이라고 해도 될 정도로 널리 사용되고 있는 소프트웨어이다.

Spring AOP에도 이 AspectJ를 이용하여 AOP 구현을 위한 기능이 포함되어 있다. 마지막으로 사용한 Spring AOP와는 또 다른 형태로 AOP를 구현할 수 있기에, 이쪽의 사용법에 대해서도 배워보도록 하자.

그럼 먼저 AspectJ를 이용하기 위한 준비를 하자. pom.xml을 열고 <dependencies> 태그 안에 아래와 같이 내용을 추가하자 (이미 기술되어 있는 spring-core와 spring-aop 태그는 삭제하지 않아야 한다!).

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.6</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.6</version>
</dependency>

예제로, 여기에서는 Spring Framework 4.1.7에 맞는 버전을 지정하고 있으므로 다른 버전을 사용하는 경우는 거기에 아울러 <version>을 조정하도록 하자.

여기에는 2 개의 라이브러리를 추가한다. "AspectJ RT"는 AspectJ 런타임 프로그램이다. 이 라이브러리는 추가하면 AspectJ의 기능을 사용할 수 있게 된다. 또한 "AspectJ Weaver"는 aspect의 정보를 바탕으로 aspect를 구성한 코드를 생성하는데 필요한 유틸리티 프로그램이다. Spring AOP에서 AspectJ를 사용을 하려면, 이 두 가지를 세트로 준비해야 한다.



Aspect 클래스 생성

Aspect으로 삽입하는 처리되는 클래스를 만들어 보자. 이번에는 AspectJ를 이용하기 때문에 이전과는 다른 형태이다.

아래 코드가 작성한 예제이다. com.devkuma.spring.aop 패키지에 "SampleAspect"라는 클래스 이름으로 코드를 작성한다.

package com.devkuma.spring.aop;
 
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class SampleAspect {
 
    @Before("execution(* com.devkuma.spring.aop.SampleAopBean.*(..))")
    public void before() {
        System.out.println("before:");
    }
 
    @After("execution(* com.devkuma.spring.aop.SampleAopBean.*(..))")
    public void after() {
        System.out.println("after:");
    }
}

보면 알 수 있듯이, 이것은 매우 일반적인 POJO 클래스이다. 어떤 인터페이스도 구현하지 않고, 어떤 클래스도 상속받지 않는다. 다른 것은 어노테이션이 부여되었다는 것뿐이다.

@Aspect

이 클래스가 Aspect 클래스 임을 나타낸다. AspectJ에서 사용하는 클래스는 이 어노테이션을 부여한다.

@Before

이 어노테이션은 메소드의 앞에 넣는다. 메소드의 실행 전에 삽입하는 처리임을 나타내는 어노테이션이다. Spring AOP에 있는 "before"과 유사한 것으로 생각해도 된다.

@After

이 어노테이션은 메소드의 앞에 넣는다. 이것은 메소드의 실행 후에 삽입되는 처리인 것을 나타내는 어노테이션이다. Spring AOP에 있는 "afterRunning"에 해당하는 것으로 생각하면 된다.

어노테이션 자체는 간단하다. 알기 어려운 것은 @Before와 @After 뒤의 괄호()안에 기술된 내용이다. 이것은 다음과 같이 기술이 되어 있다.

("execution (...... 할당되는 메소드의 지정 ...)")

execution 후의 괄호()에 어떤 메소드에 이 메소드 삽입 할 것인지를 지정한다. 이것은 패키지 이름부터 제대로 정확하게 메소드를 지정해야 한다. 단, 모든 이름을 다 기술해야 한다는 뜻은 아니다.

여기에는 와일드 카드(*)를 사용할 수 있기 때문에, 그것을 이용하여 특정 패키지와 클래스의 모든 메소드 등을 지정할 수 있다. 또한 지정하는 메소드의 인수도 ".." 기호로 불특정 인수를 지정할 수 있다.

여기에 작성된 것을 보면, 다음과 같이 되어 있다.

* com.devkuma.spring.aop.SampleAopBean.*(..)

알기 쉽게 말해, "불특정 값"을 XX라고 기술한다면, 이런 식으로 적혀있는 것을 알 수 있다.

XX com.devkuma.spring.aop.SampleAopBean.XX(XX)

맨 처음에있는 XX는 public이나 private 같은 것이 지정된 경우를 생각하여 붙여 있다. 그리고 SampleAopBean의 후에 붙은 XX는 이 클래스 내에 있는 어떤 방법도 모두 대상으로 지정하는 것이다. 또, (XX)는 인수가 어떤 형태여도 해당되도록 하기 위한 것이다.

이 execution 작성 방법을 어느 정도 알게 된다면, 원하는대로 AOP 처리 대상이 되는 메소드를 지정할 수 있게 될 것이다.



aopbean.xml 작성

이어서 Bean 설정 파일을 준비하다. 이번에는 "aopbean.xml"라는 파일을 새로 추가하도록 하자.

"resources"폴더에 "aopbean.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"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd 
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop.xsd">
 
    <bean id="sampleAspect" class="com.devkuma.spring.aop.SampleAspect" />
    <bean id="sampleAopBean" class="com.devkuma.spring.aop.SampleAopBean">
        <property name="message" value="this is AOP bean!" />
    </bean>
 
    <aop:aspectj-autoproxy />
     
</beans>

이번에는 지금까지 사용해 온 bean.xml에 비해 여러가지 기술해야 하는 것이 많다. 내용에 대해서 아래와 같이 정리를 해보자.

<beans> 태그의 속성

먼저 <beans> 태그 부분을 보자. 다음 속성이 추가되어 있다.

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

그리고, 이 스키마 위치(schema location)를 나타내는 값이 xsi:schemaLocation 값에 추가되어 있다. 구체적으로는 아래와 같은 부분이다.

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

이것은 설정 파일에서 사용되고 있는 <aop:aspectj-autoproxy/>를 사용할 수 있도록 하기 위한 것이다. 다소 길지만, 반드시 추가 기입해야 한다.

Bean 등록

<bean id = "sampleAspect"class = "comdevkuma.spring.aop.SampleAspect"/>
<bean id = "sampleAopBean"class = "com.devkuma.spring.aop.SampleAopBean">
    <property name = "message"value = "this is AOP bean!"/>
</ bean>

이것은 이미 여러 번 작성해 봤었다. Bean 등록 태그이다. SampleAspect 클래스와 SampleAopBean 클래스를 각각 Bean으로 등록한다.

AspectJ 자동 프록시

<aop:aspectj-autoproxy/>

이것은 AspectJ를 위한 태그이며, 먼저 Spring AOP 때 이용한 ProxyFactoryBean에 해당하는 것을 자동으로 생성하는 태그이다. 이를 기술하게 되면 ProxyFactoryBean으로 준비된 기능이 자동으로 포함된다.



프로그램을 실행

이것으로 AspectJ를 사용할 준비가 되었다. 그럼 Bean을 이용하여 보자. com.devkuma.spring.aop 패키지의 App 클래스 (전에 사용 했던 거)을 아래와 같이 수정한다.

package com.devkuma.spring.aop;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("aopbean.xml");
         
        SampleAopBean bean = (SampleAopBean) app.getBean("sampleAopBean");
        String msg = bean.getMessage();
        bean.setMessage("<<" + msg + ">>");
        bean.printMessage();
    }
 
}

이것을 실행하면 다음과 같이 출력이 될 것이다.

before :
after :
before :
after :
before :
message : [<< this is AOP bean! >>
after :

여기에서는 getMessage로 메시지를 얻어서, 그것을 바탕으로 setMessage 메시지를 설정하고, 그것으로 부터 printMessage 내용을 출력한다. 각각의 메소드를 호출마다 그 전과 후에 before / after가 출력되는 것을 볼 수 있다.

이전에 Spring AOP와 결정적인 차이는 getBean으로 얻어 실행하는 클래스이다. Spring AOP에서는 ProxyFactoryBean를 통해서 수정된 SampleAopBean를 얻을 수 있게 되어 있었다.

이번에 getBean으로 얻은 것은 SampleAopBean이다. 즉, SampleAopBean 클래스 자체가 변경된 메소드가 삽입되어 있는 것이다.

동작을 확인했다면 aopbean.xml을 열고 <aop : aspectj-autoproxy/> 태그를 삭제하자. 그리고 실행하면 이번에는 "message:[<< this is AOP bean! >>]"만 표시된다. before / after에 삽입된 것 메소드가 사라진 것을 알 수 있다.

이처럼 AspectJ를 사용하면 자동 프록시의 ON / OFF만으로 화면 처리의 삽입과 제거가 간단히 할 수 있다. 사용하는 Bean을 프록시 Bean에서 원래대로 되돌릴 수는 없다.


Bean 설정 클래스 이용

이제 이것으로 AspectJ의 기본적인 이용 방법은 알았다. Spring Framework에서는 Bean은 설정 파일이 아닌 클래스를 사용하여 정의 할 수 있었다. AspectJ에도 정의 클래스로 설정을 해 보자.

com.devkuma.spring.aop 패키지에 "SampleAspectConfig"라는 클래스를 작성한다. 그리고 아래와 같이 코드를 작성하자.

package com.devkuma.spring.aop;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
 
@Configuration
@EnableAspectJAutoProxy
public class SampleAspectConfig {
     
    @Bean
    SampleAopBean sampleAopBean() {
        return new SampleAopBean("this is AspectJ bean.");
    }
     
    @Bean
    SampleAspect sampleAspect() {
        return new SampleAspect();
    }
 
}

이것이 AspectJ 설정 클래스이다. 작성한 후 App.java을 열고 다음 문장을 수정한다.

App 수정 전

ApplicationContext app =
    new ClassPathXmlApplicationContext ( "aopbean.xml");

App 수정 후

ApplicationContext app =
    new AnnotationConfigApplicationContext (SampleAspectConfig.class);

이제 실행하면 이전과 마찬가지로 AspectJ를 이용한 방법의 삽입이 이루어 메소드 호출 전후에 처리가 실행되게 된다. 이 클래스에는 설정 클래스임을 나타내는 @Configuration 외에,

@EnableAspectJAutoProxy

이러한 어노테이션이 클래스에 추가되어 있다. 이것은 Bean 정의 파일에 넣었던 <aop:aspectj-autoproxy/> 태그에 해당하는 것이다. 이것을 기술하는 것으로 AspectJ의 자동 프록시 기능이 ON이 되고, 자동으로 Aspect 클래스의 메소드 삽입이 이루어지게 된다.

Bean의 정의는 지금까지와 변함이 없다. 다만, @EnableAspectJAutoProxy를 추가하는 것만으로 AspectJ의 기능이 ON이 된다는 점이다. 그래도 설정 클래스으로 변경을 한다고 해도 AspectJ 이용의 간단함은 변함이 없다는 것을 알 수 있다.



'Spirng' 카테고리의 다른 글

[Spring] CRUD 기본  (0) 2017.12.10
[Spring] Spring Data JPA 이용  (0) 2017.12.10
[Spring] AspectJ 이용  (0) 2017.12.10
[Spring] AOP 이용  (0) 2017.12.10
[Spring] 어노테이션으로 DI 구현  (0) 2017.12.10
[Spring] Dependency Injection(DI)와 Bean  (0) 2017.12.10

DI와 더불어 Spring Framework의 핵심 기능이 되는 것이 "AOP"라는 기술이다. 클래스 안에 외부에서 "처리"를 삽입하는 AOP의 구조와 기본적인 사용법에 대해 설명한다.


AOP이란?

Spring Framework에 있어서 DI(Dependency Injection 의존성 주입)와 더불어 중요한 근간이 되는 "AOP"이라는 기술이 있다.

AOP는 "Aspect Oriented Programming (관점 지향 프로그래밍)"의 약자이다. Aspect이라는 것은 일반적으로 "횡단적 관심사"라는 것이다.

객체 지향 프로그램은 "클래스"를 기준으로 작성된다. 각각의 클래스마다, 그 클래스에 필요한 기능을 메서드로 구현하는 것이다. 이 수법은 사고방식으로써는 잘되어 있지만, 반대로 "클래스마다 완벽하게 결정되어 있어야 한다"것이 몹시 번거려워질 수도 있다.

예를 들어, 프로그램의 개발 중에 작동 상황을 확인하기 위해 곳곳에 System.out.println 문장을 쓰고 값을 출력시키는 것은 누구든지 흔하게 하는 방법이다. 그런데, 이것은 생각해 보면 굉장히 귀찮은 방법이다. 다수의 클래스가 있으면, 각 클래스의 각 메소드마다 println을 쓰고 나가지 않으면 안된다. 또한, 그렇게 프로그램이 완성된 후에는 모든 println을 제거하지 않으면 안된다.

이런 "다수의 클래스에 걸쳐 공통적으로 필요한 처리"가 횡단적 관심사이다. 만약 여러 클래스의 메서드에 println 문장을 자동으로 삽입 할 수 있는 기능이 있으면 상당히 편리 아니지 않을까? 그리고 필요가 없어지면 자동으로 삭제 할 수 있다면? 이것이 바로 AOP의 개념이다.

DI가 "의존성(값) 주입"이라면, AOP는 "처리 주입"이라고 해도 좋을 것이다. 외부에서 클래스의 특정 부분에 미리 준비 해둔 처리를 삽입하거나 제거하거나 하는 것이 AOP에서 실현되는 것이다.

pom.xml 준비

우선 프로젝트에 AOP 관련 라이브러리를 추가하자. pom.xml을 열고 ≶dependencies> 태그 안에 아래에 있는 내용을 추가하자.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.10.RELEASE</version>
</dependency>

여기에 추가하는 것은 Spring AOP 라이브러리이다. groupId에 org.springframework를 지정하고, artifactId에 spring-aop를 지정한다. 또한 버전은 Spring Framework 4.3.10으로 이용하도록 맞춰 지정하고 있다. Spring Framework의 버전이 다르면 거기에 맞춰서 버전을 조정하면 된다.



AOP를 이용하는 Bean 클래스 생성

그럼 AOP를 이용해 보자. AOP는 특정 처리를 외부에서 클래스에 삽입하는 기능을 한다. 이를 위해서는 다음과 같은 준비해야 한다.

  • AOP의 대상이 되는 클래스. 가장 일반적인 Bean 클래스를 준비한다.
  • AOP에 삽입하는 처리를 하는 클래스. 여기에 삽입하는 작업을 준비한다.
  • AOP에 대한 설정 정보. 이것은 Bean 설정 파일 또는 설정 클래스를 사용하여 준비한다.

먼저 AOP 대상이되는 클래스를 준비하자. 이번에는 com.devkuma.spring.aop라는 패키지를 준비하고, 이 안에 필요한 클래스들을 모으도록 하자. "SampleAopBean"라는 클래스를 아래와 같이 작성한다.

package com.devkuma.spring.aop;
 
public class SampleAopBean {
    private String message;
 
    public SampleAopBean() {
        super();
    }
    public SampleAopBean(String message) {
        this.message = message;
    }
     
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public void printMessage() {
        System.out.println("message:[" + message + "]");
    }
}

이것은 먼저 만든 SampleBean과 거의 동일하다. 메시지를 저장하는 message 속성과 생성자, 그리고 printMessage라는 메소드를 만들었다. 이렇게, 사용하는 Bean 자체는 극히 간단한 POJO 클래스로써 Spring Framework의 특징이다.



MethodBeforeAdvice 클래스 생성

이어서 SampleAopBean에 AOP에 삽입하는 작업을 준비하자. 이것도 물론 Java 클래스로 정의한다.

com.devkuma.spring.aop 패키지에 "SampleMethodAdvice"라는 이름으로 클래스를 만들어 보자. 그리고 아래와 같이 코드를 작성한다.

package com.devkuma.spring.aop;
 
import java.lang.reflect.Method;
 
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
 
public class SampleMethodAdvice 
        implements MethodBeforeAdvice, AfterReturningAdvice {
 
    @Override
    public void before(Method method, Object[] args, 
            Object target) throws Throwable {
        System.out.println("*before: " + method.getName() + "[" + target + "]");
    }
 
    @Override
    public void afterReturning(Object returnValue, Method method, 
            Object[] args, Object target) throws Throwable {
        System.out.println("*after: " + method.getName() + "[" + target + "]");
    }
 
}

이번 작성한 SampleMethodAdvice는 두 개의 인터페이스를 구현하고 있다. 이 인터페이스는 처리의 삽입에 대한 메소드를 추가한다. 각각 간단히 정리를 하면 아래와 같다.

MethodBeforeAdvice

이것은 메소드가 실행하기 전에 처리를 삽입하기 위한 인터페이스이다. 이것은 'before'라는 메소드를 하나 가지고 있으며 다음과 같이 정의되어 있다.

public void before (Method method, Object [] args, Object target)
    throws Throwable

Method는 대상 메소드, args는 그에 대한 인수, target은 대상이 되는 객체(인스턴스)가 각각 전달된다. 이러한 인수들은 어떤 인스턴스의 어떤 메서드를 호출하기 전에 이 처리를 수행했는지를 알 수 있다.

AfterReturningAdvice

이것은 메소드의 실행이 끝나고, 호출을 하고 원래대로 돌아오게 될 때 삽입하는 처리의 인터페이스이다. "afterReturning"라는 메소드가 준비되어 있다. 이것은 다음과 같이 정의되어 있다.

public void afterReturning (Object returnValue, Method method,
    Object [] args, Object target) throws Throwable

메소드의 반환 값, 메소드, 메소드에 전달된 인수, 대상 인스턴스가 인수로 전달된다. 반환 값 이외는 위 before와 동일하므로 거의 같은 감각으로 처리하는 것이 가능하다.

여기에서는 각 메소드과 타겟을 System.out.println에서 출력하고 있을 뿐이다. AOP는 "처리 삽입"라고 했지만 어디라도 마음대로 삽입되는 것은 아니다. "이 타이밍에 삽입한다"는 것이 미리 몇 가지 준비되어 있을 것이다.

우선, 이 2개의 인터페이스를 익히면 "메소드를 호출하기 전과 호출 후"에 처리를 삽입할 수 있다. AOP의 기본을 알게 된것으로는 충분하다.



bean.xml 생성

다음에 해야 하는 것은 필요한 Bean의 설정을 준비하는 것이다. 우선 Bean 설정 파일을 사용해 보기로 하자.

먼저 "resources"폴더에 생성한 "bean.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.xsd">
     
    <!-- aop bean... -->
    <bean id="sampleAopBean" class="com.devkuma.spring.aop.SampleAopBean">
        <property name="message" value="this is AOP bean!" />
    </bean>
        
    <bean id="sampleMethodAdvice"
        class="com.tuyano.libro.aop.SampleMethodAdvice" />
 
    <bean id="proxyFactoryBean"
            class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="sampleAopBean"/>
        <property name="interceptorNames">
            <list>
                <value>sampleMethodAdvice</value>
            </list>
        </property>
    </bean>
             
</beans>

이번에는 총 3개의 Bean을 등록한다. 각각 다음과 같은 내용으로 되어 있다.

SampleAopBean

먼저 만든 Bean이다. 여기에는 id="sampleAopBean"라고 이름을 지정해서 준비해 둔다.

SampleMethodAdvice

방금 만든 AOP 처리 클래스이다. 이것은 id="sampleMethodAdvice"라는 이름으로 준비해 둔다.

ProxyFactoryBean

이것이 여기에서의 포인트이다. 이것은 org.springframework.aop.framework 패키지에 포함되어 있는 Spring AOP의 클래스이다. 이렇게 라이브러리에 포함되어 있는 클래스도 Bean 설정 파일에 의해 인스턴스를 자동 생성할 수 있다.

이 ProxyFactoryBean은 <property> 태그를 사용하여 2개의 프로퍼티를 추가하고 있다. 각각 다음과 같다.

target : AOP의 대상이 되는 Bean을 지정한다. 여기에서는 sampleAopBean (<bean id = "sampleAopBean">에서 준비한 것)를 지정하고 있다.

interceptorNames : 이것은 AOP에 삽입하는 처리 Bean을 지정한다. 복수를 지정할 수 있도록 <list>라는 목록 태그를 지정하고 그 안에 <value> 태그에서 Bean 이름을 지정한다. 여기에서는 그 앞에서 만들었던 sampleMethodAdvice를 지정하고 있다.

따라서, AOP의 대상이 되는 Bean, AOP 처리을 수행하는 Bean, 그리고 이러한 관계를 속성으로 설정한 ProxyFactoryBean까지 3개가 필요하게 된다.



AOP 실행하기

자, 이제 겨우 준비가 되었다. 그러면 실제로 AOP를 사용하여 보도록 하자. com.devkuma.spring.aop 패키지에 "App"클래스를 만들고 아래와 같이 소스 코드로 작성하자.

package com.devkuma.spring.aop;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
         
        SampleAopBean bean1 = (SampleAopBean) app.getBean("sampleAopBean");
        bean1.printMessage();
 
        System.out.println("--------------------");
 
        SampleAopBean bean2 = (SampleAopBean) app.getBean("proxyFactoryBean");
        bean2.printMessage();
    }
 
}

실행하면 bean.xml에서 SampleAopBean를 얻어 printMessage을 실행하는데, 자세히 보면 2번 반복하고 있다.

첫 번째 SampleAopBean는 getBean("sampleAopBean")에서 Bean을 얻고 있다. 이것은 지금까지대로의 방식이다. 그리고 두 번째는 getBean("proxyFactoryBean")에서 Bean을 얻고 있다. 이것으로얻어지는 Bean이 ProxyFactoryBean처럼 생각되지만, 정확히 SampleAopBean에 캐스팅하고 SampleBean으로서 기능을 한다.

이것은 ProxyFactoryBean의 신기한 기능이다. 이 Bean은 target 속성에 지정된 Bean으로 얻어 올 수 있는 것이 가능하다는 것이다.

그럼 얻어온 SampleAopBean의 printMessage를 호출 처리는 어떤 출력을하고있는 것일까? 보면 이렇게 되어 있는 것이다.

message : [this is AOP bean!]
--------------------
* before : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]
message : [this is AOP bean!]
* after : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]

첫 번째 SampleAopBean은 단순히 printMessage의 출력이 될뿐이다. 하지만 두 번째 SampleAopBean는 printMessage의 실행 전후에 SampleMethodAdvice 클래스에 제공되는 before / afterReturning의 실행 결과가 삽입되어 있는 것을 알 수 있다. 메소드의 실행 전후에 자동으로 다른 처리가 추가되어 있는 것이다.

이것이 AOP의 위력이다. getBean로 취득하는 Bean을 ProxyFactoryBean로 하는 것은 이런 방식으로 자동 처리가 추가 될 수 있다. 불필요하게 되면 getBean 인수를 SampleAopBean으로 돌아가면 된다.



어노테이션으로 AOP 설정 클래스 생성

이것으로 기본은 알았다. 이번에는 bean.xml를 클래스에 고쳐 써 보자. Spring Framework에서는 Bean 설정 파일을 사용하지 않고, 설정을 위한 클래스으로 같은 것이 가능하다.

그럼 com.devkuma.aop 패키지 "SampleAopConfig"라는 클래스를 만들어 보자. 그리고 아래와 같이 소스 코드를 작성한다.

package com.devkuma.spring.aop;
 
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class SampleAopConfig {
    private SampleAopBean sampleAopBean = 
        new SampleAopBean("this is message bean.");
    private SampleMethodAdvice sampleMethodAdvice = 
        new SampleMethodAdvice();
     
    @Bean
    SampleAopBean sampleAopBean() {
        return sampleAopBean;
    }
     
    @Bean
    SampleMethodAdvice sampleMethodAdvice() {
        return sampleMethodAdvice;
    }
     
    @Bean
    ProxyFactoryBean proxyFactoryBean() {
        ProxyFactoryBean bean = new ProxyFactoryBean();
        bean.setTarget(sampleAopBean);
        bean.setInterceptorNames("sampleMethodAdvice");
        return bean;
    }
     
}

작성을 하면 bean.xml를 사용하던 것을 SampleAopConfig 클래스를 사용하도록 App 클래스의 코드를 수정한다. 다음 문장을 바꾸면 된다.

App 수정

ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");

    ↓

ApplicationContext app = new AnnotationConfigApplicationContext (SampleAopConfig.class);

이것으로 App을 실행하면 방금처럼 출력이 된다. bean.xml에 기술된 것과 동일한 Bean이 SampleAopConfig에서 취 할 수 있도록 되어있는 것을 알 수 있다.

여기에서 @Configuration 어노테이션을 붙여 SampleAopConfig 클래스를 선언하고, Bean 인스턴스를 반환하는 메서드에 @Bean 어노테이션 붙여서 Bean을 취득할 수 있도록 하고 있다. 주목을 해야 하는 것을 SampleMethodAdvice 인스턴스를 작성하고 있는 sampleMethodAdvice 메소드이다. 인스턴스 작성 후에 다음과 같이 필요한 속성을 설정하고 있다.

bean.setTarget(sampleAopBean);
bean.setInterceptorNames ("sampleMethodAdvice");

이는 "setTarget"과 "setInterceptorNames"가 먼저 bean.xml에서 기술하고 있었던 <property name="target">와 <property name="interceptorNames">에 해당하는 처리한다는 것이다.




'Spirng' 카테고리의 다른 글

[Spring] Spring Data JPA 이용  (0) 2017.12.10
[Spring] AspectJ 이용  (0) 2017.12.10
[Spring] AOP 이용  (0) 2017.12.10
[Spring] 어노테이션으로 DI 구현  (0) 2017.12.10
[Spring] Dependency Injection(DI)와 Bean  (0) 2017.12.10
[Spring] Spring 프로젝트 생성  (0) 2017.12.10

+ Recent posts