Web/JPA 2019. 2. 12. 00:19

JPA - 영속성 컨텍스트와 JPQL



쿼리 후 영속 상태인 것과 아닌것

-JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입 같이 다양하다. 하지만 엔티티를 조회하면 영속성 컨텍스트에 관리되지만 나머지는 관리되지 않는다. 예를 들어 임베디드 타입만 참조해서 조회했을 경우에 값을 변경해서 플러시 시점에 반영되지 않는다. 하지만 엔티티를 조회하여 엔티티가 가지고 있는 임베디드타입을 변경했을 경우에는 플러시 시점에 변경이 반영이 된다.



JPQL로 조회한 엔티티와 영속성 컨텍스트

-JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스에서 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다. 이때 식별자 값을 이용하여 비교한다.


여기서 동일하다는 것은 엔티티의 모든 필드값이 동일하다는 것을 의미하는 것이 아닌 엔티티의 식별자값이 동일함을 의미한다. 즉, 만약 1차 캐시의 member1의 이름이 "여성게"이고 , 디비에 있는 member1의 이름이 "소라게"여도 식별자 값이 같음으로 "여성게"로 조회가 되게 된다. (물론 이런 상황 자체가 있는 것이 잘못된 것이긴함) 하지만 만약 영속성컨텍스트에 디비에서 조회한 엔티티가 존재하지 않으면 디비에서 가져온 엔티티를 영속성 컨텍스트에 넣는다. 왜 그런가?


1. 새로운 엔티티를 영속성 컨텍스트에 하나 더 추가한다. -> 식별자값은 유일함으로 이것은 말이안됨.

2.기존 엔티티를 새로 검색한 엔티티로 대체한다. -> 그럴싸 하지만 만약 엔티티가 수정중에 있다면 아주 큰문제가됨.

3.기존 엔티티는 그대로 두고 새로 검색한 엔티티를 버린다. -> 이 규칙을 따르게 되는 것이다. 영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장해야하기 때문 




find() vs JPQL

-em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 따라서 해당 엔티티가 메모리에 존재한다면(이미 이전에 동일한 엔티티를 조회한 적이 있기때문) 성능상 이점이 있다.

JPQL은 항상 디비에서 찾는다. 왜냐하면 JPQL은 SQL로 변환되어 데이터베이스에 바로 날려보기 때문이다. 항상 디비에서 찾지만 디비에서 찾은 엔티티가 영속성 컨텍스트에 존재한다면 디비에서 찾은 엔티티를 버리고 영속성 컨텍스트에 있는 것을 반환값으로 넘긴다.



JPQL과 플러시 모드

-플러시는 영속성 컨텍스트의 변경 내역을 디비에 동기화하는 것이다. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 적절한 SQL을 만들어 디비에 반영한다.

플러시 모드
1)FlushModeType.AUTO : 커밋 또는 쿼리 실행 시 플러시(기본값)
2)FlushModeType.COMMIT : 커밋시에만 플러시


쿼리와 플러시모드

JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 디비에서 데이터를 조회한다. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 디비에 반영해야한다. 그렇지 않으면 의도하지 않은 결과가 발생할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void batchQuery() {
     JPAQuery query = new JPAQuery(em);
     QMemberJPQL qMember = QMemberJPQL.memberJPQL;
     tx.begin();
     JPAUpdateClause updateBatch = new JPAUpdateClause(em, qMember);
     long count = updateBatch.where(qMember.username.eq("여성게1"))
                             .set(qMember.username,"바보멍청이").execute();
               
     tx.commit();
     if(count == 1) {
             MemberJPQL member = query.from(qMember)
                                      .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
            System.out.println("=====================batchQuery======================");
             if(!ObjectUtils.isEmpty(member)) {
                     System.out.println(member.toString());
             }
             System.out.println("=====================batchQuery======================");
     }
}
cs


이 예제는 QueryDSL의 수정 배치 쿼리이다. 이 예제의 결과가 어떻게 될것인가? 결과는 정말 의도치 않게 나오게 된다. QueryDSL의 배치쿼리는 영속성컨텍스트와 관계없이 직접 디비로 요청을 보내게 된다. 이러는 순간 디비와 영속성컨텍스트의 동기화는 깨진것이다. 변경을 완료했고 나는 변경된 이름으로 쿼리를 날렸는데, 이게 뭐람 변경전의 "여성게1"이란 결과가 나오게 된다. 이유는 위에서 설명한 것과 같이 영속성컨텍스트에 이미 존재하기 때문에 디비에서 가져온 엔티티가 데이터가 달라도 식별자 값이 같기 때문에 버려지게 되고 수정전에 이름이 출력되게 되는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void batchQuery2() {
        JPAQuery query = new JPAQuery(em);
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        tx.begin();
        JPAUpdateClause updateBatch = new JPAUpdateClause(em, qMember);
        long count = updateBatch.where(qMember.username.eq("여성게1"))
                                                .set(qMember.username,"바보멍청이").execute();
        MemberJPQL member2 = query.from(qMember)
                                .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
        member2.setUsername("바보멍청이");
        
        tx.commit();
        if(count == 1) {
                
                MemberJPQL member = new JPAQuery(em).from(qMember)
                                    .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
                System.out.println("=====================batchQuery======================");
                if(!ObjectUtils.isEmpty(member)) {
                        System.out.println(member.toString());
                }
                System.out.println("=====================batchQuery======================");
        }
        
}
cs


이 코드는 실무에서는 사용할 수없는 코드이다. 하지만 직관적으로 영속성 컨텍스트의 값을 수정배치쿼리와 동일하게 변경한것을 보여주기 위해 짠것이다. 이 예제의 결과는 수정배치에서 변경된 값과 동일한 엔티티가 반환되게 된다.

posted by 여성게
:
Web/JPA 2019. 2. 11. 23:43

JPA - JPAQuery와 Q클래스


긴 포스팅은 아니지만 예제로 소스를 짜는 도중에 몰랐던 내용을 알게되어서 작성한다. QueryDSL로 예제중 나온 상황이다.



1
2
3
4
5
6
7
8
9
10
public List<MyEntity> findMyDumbEntities(long id) {    
      QMyEntity qMyEntity = QMyEntity.myEntity;
      JPAQuery jpaQuery = new JPAQuery(entityManager);
      MyEntity myFirstEntity = jpaQuery.from(qMyEntity)
                                        .where(qMyEntity.id.eq(id)).uniqueResult(qMyEntity);
      MyEntity mySecondEntity = jpaQuery.from(qMyEntity)
                                        .where(qMyEntity.id.eq(id+1)).uniqueResult(qMyEntity);
 
      return Arrays.asList(myFirstEntity, mySecondEntity);    
}
cs


언뜻 보기에는 크게 문제가 없는 듯 싶다. 하지만 이 소스를 실행하면 이러한 예외가 발생한다.



java.lang.IllegalStateException: qMyEntity is already used


즉, 같은 JPAQuery 객체로 같은 Q타입클래스를 사용할 수없는 것이다. 사실 내부를 까보지는 않아서 원인파악은 해보아야겠지만 일단 결론은 그렇다. 그렇기 때문에 소스가 이런식으로 변경되어야한다.


1
2
3
4
5
6
7
8
9
public List<MyEntity> findMyDumbEntities(long id) {        
          QMyEntity qMyEntity = QMyEntity.myEntity;
          MyEntity myFirstEntity = new JPAQuery(entityManager).from(qMyEntity)
                                    .where(qMyEntity.id.eq(id)).uniqueResult(qMyEntity);
          MyEntity mySecondEntity = new JPAQuery(entityManager).from(qMyEntity)
                                    .where(qMyEntity.id.eq(id+1)).uniqueResult(qMyEntity);
 
          return Arrays.asList(myFirstEntity, mySecondEntity);        
}
cs


이런식으로 JPAQuery 객체를 만드는 메소드를 하나 정의하는 것이 좋을 것같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
 
protected JPQLQuery jpaQuery() {
    return new JPAQuery(entityManager);
}
 
@Override
public List<Article> findAll() {
      return jpaQuery().from(article).fetchAll().list(article);
}
 
public Article findById(@Nonnull final long id) {
      return jpaQuery().from(article).where(article.id.eq(id)).uniqueResult(article);
}
cs

언젠가는 이런 상황이 닥칠수도 있을 것같아서 포스팅한다..

posted by 여성게
:
Web/JPA 2019. 2. 11. 22:43

JPA - NativeQuery ( SQL ) 네이티브 SQL 사용하기!


JPA는 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 잘 지원하지 않는다. 하지만 때론 특정 데이터베이스에 종속적인 기능이 필요할 수도 있다. 다양한 이유로 JPQL을 사용할 수 없을 때, JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 네이티브 SQL(네이티브쿼리)라고 한다. 즉, 사용자가 직접 데이터베이스에 날리는 쿼리를 작성하는 것이다. 그렇다면 JPA가 지원하는 네이티브 SQL과 JDBC API를 직접 사용하는 것에는 어떤 차이가 있냐? 그것은 바로 네이트브 쿼리는 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다는 것이다!



Native Query API



  1. //결과 타입 정의
  2. public Query createNativeQuery(String sqlString, Class resultClass) {}
  3. //결과 타입을 정의할 수 없을 때
  4. public Query createNativeQuery(String sqlString) {}
  5. //엔티티 타입이 아닌 결과 값일때
  6. public Query createNativeQuery(String sqlString
  7.                         ,String resultSetMapping) {}

 이렇게 세가지의 오버로딩된 메소드를 사용한다.




  1. /*
  2.  * 네이티브쿼리는 위치기반 파라미터 바인딩만 지원한다.(구현체중 일부는 이름기반 파라미터 바인딩도 지원함)
  3.  */
  4. public void nativeQuery() {
  5.         String sql = "SELECT MEMBER_JPQL_ID,USERAGE,USERNAME,TEAM_ID FROM MEMBER_JPQL WHERE USERAGE> ?";
  6.         
  7.         Query nativeQuery = em.createNativeQuery(sql,MemberJPQL.class).setParameter(135);
  8.         
  9.         List<MemberJPQL> resultList = nativeQuery.getResultList();
  10.         
  11.         System.out.println("====================nativeQuery=====================");
  12.         
  13.         for(MemberJPQL m : resultList) {
  14.                 System.out.println(m.toString());
  15.         }
  16.         
  17.         System.out.println("====================nativeQuery=====================");
  18. }

리턴타입이 엔티티일 경우이다. em.createNativeQuery(SQL문장,리턴타입클래스)로 Query객체를 만들어낸다. 위의 예제를 보면 JPQL과 다를 것은 쿼리를 직접작성하냐 안하냐의 차이지 대부분의 로직은 비슷하다.(JPA가 자동으로 생성해주는 SQL를 사용자가 직접 작성한 것뿐 더이상의 차이는 거의없다.)

그리고 Native Query에서는 반환타입이 명시되어도 Query 객체로 반환받는다는 점을 유의하자.





다음은 결과 매핑 사용 예제이다.


  1. @Table(name = "MEMBER_JPQL")
  2. //@Getter
  3. //@Setter
  4. @ToString
  5. @SqlResultSetMapping(name = "memberWithOrderCount",
  6.                                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
  7.                                         columns = {@ColumnResult(name = "ORDER_COUNT")})
  8. public class MemberJPQL {....}


  1. /*
  2.  * ResultMapping
  3.  */
  4. public void nativeQuery2() {
  5.         String sql = "SELECT M.MEMBER_JPQL_ID AS ID,USERAGE AS AGE,USERNAME,TEAM_ID,I.ORDER_COUNT "
  6.                                 + "FROM MEMBER_JPQL M JOIN  "
  7.                                                 + "(SELECT IM.MEMBER_JPQL_ID, COUNT(*) AS ORDER_COUNT "
  8.                                                 + "FROM ORDER_JPQL O, MEMBER_JPQL IM "
  9.                                                 + "WHERE O.MEMBER_JPQL_ID = IM.MEMBER_JPQL_ID) I "
  10.                                 + "ON M.MEMBER_JPQL_ID = I.MEMBER_JPQL_ID";
  11.                                 
  12.         Query nativeQuery = em.createNativeQuery(sql,"memberWithOrderCount");
  13.         
  14.         List<Object[]> resultList = nativeQuery.getResultList();
  15.         
  16.         System.out.println("====================nativeQuery2=====================");
  17.         
  18.         for(Object[] m : resultList) {
  19.                 System.out.println(m[0].toString());
  20.                 System.out.println(m[1]);
  21.         }
  22.         
  23.         System.out.println("====================nativeQuery2=====================");
  24. }

특별한 것은 없다. 엔티티클래스에 네이티브 쿼리로 받을 반환 타입을 명시해주었고, 네이티브 쿼리를 사용한 쪽에서는 반환타입을 엔티티클래스에 작성한 반환타입 이름 값을 문자열로 넘겨주었다. 그래서 반환된 Object[]에서 Object[0]은 회원엔티티로 매핑되고, Object[1]은 스칼라타입으로 ORDER_COUNT를 담고있다.

어노테이션을 보면 속성 값이 entities,columns로 된것으로 보아 여러개의 엔티티,칼럼을 매핑할 수 있다. 그리고 @SqlResultSetMappings 어노테이션이 있기 때문에 여러개의 매핑타입을 만들 수 있다.



그런데 대부분 엔티티 클래스의 id클래스의 필드명은 "id"로 거의 동일하게 하는 경우도 많다. 그래서 조회를 하면 칼럼명이 중복나는 경우가 생기게 된다. 그때는 필드매핑까지 포함하여서 조회가능하다.


  1. @Table(name = "MEMBER_JPQL")
  2. //@Getter
  3. //@Setter
  4. @ToString
  5. @SqlResultSetMappings({
  6.         @SqlResultSetMapping(name = "memberWithOrderCount",
  7.                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
  8.                         columns = {@ColumnResult(name = "ORDER_COUNT")}
  9.         )
  10.         ,@SqlResultSetMapping(name = "memberInfo",
  11.                         entities = {
  12.                                         @EntityResult(entityClass=MemberJPQL.class,
  13.                                         fields = {
  14.                                                         @FieldResult(name = "id",column = "member_id")
  15.                                                         ,@FieldResult(name = "age",column = "client_age")
  16.                                         })
  17.                         }
  18.         )
  19.         
  20. })
  21. public class MemberJPQL {...}
  1. public void nativeQuery3() {
  2.         String sql = "select MEMBER_JPQL_ID as member_id,USERAGE as client_age "
  3.                         + "from member_jpql where USERNAME = '여성게1'";
  4.         
  5.         Query nativeQuery = em.createNativeQuery(sql,"memberInfo");
  6.         
  7.         List<MemberJPQL> resultList = nativeQuery.getResultList();
  8.         
  9.         System.out.println("====================nativeQuery3=====================");
  10.         
  11.         for(MemberJPQL m : resultList) {
  12.                 System.out.println(m.toString());
  13.         }
  14.         
  15.         System.out.println("====================nativeQuery3=====================");
  16. }

부득이하게 조회할 컬럼명을 별칭으로 주었을 경우, 엔티티 클래스와 매핑이 되지 않는다. 그럴때 엔티티클래스에 필드매핑을 이용한다.


@FieldResult(name = "id",column = "member_id")


name이 엔티티클래스의 필드명이고, column이 조회하였을 때 반환되는 컬럼명이다.



마지막으로 이전에 다루었던 정적쿼리(@NamedQuery)처럼 네이티브 쿼리로 정적쿼리처럼 엔티티클래스에 정의할 수 있다.




▶︎▶︎▶︎JPA - @NamedQuery , 정적 쿼리



    1. @Table(name = "MEMBER_JPQL")
    2. //@Getter
    3. //@Setter
    4. @ToString
    5. @SqlResultSetMappings({
    6.         @SqlResultSetMapping(name = "memberWithOrderCount",
    7.                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
    8.                         columns = {@ColumnResult(name = "ORDER_COUNT")}
    9.         )
    10.         ,@SqlResultSetMapping(name = "memberInfo",
    11.                         entities = {
    12.                                         @EntityResult(entityClass=MemberJPQL.class,
    13.                                         fields = {
    14.                                                         @FieldResult(name = "id",column = "member_id")
    15.                                                         ,@FieldResult(name = "age",column = "client_age")
    16.                                         })
    17.                         }
    18.         )
    19.         
    20. })
    21. @NamedNativeQueries({
    22.         @NamedNativeQuery(name = "Member.memberSQL",
    23.                                                 query = "select member_jpql_id,userage,username,team_id "
    24.                                                                 + "from member_jpql "
    25.                                                                 + "where username = ?",
    26.                                                 resultClass=MemberJPQL.class)
    27.         ,@NamedNativeQuery(name = "Member.memberSQL2",
    28.                         query = "select MEMBER_JPQL_ID as member_id,USERAGE as client_age "
    29.                                         + "from member_jpql where USERNAME = ?",
    30.                         resultSetMapping = "memberInfo")
    31. })
    32. public class MemberJPQL {}


    1. public void nativeQuery4() {
    2.         
    3.         Query nativeQuery = em.createNamedQuery("Member.memberSQL",MemberJPQL.class)
    4.                         .setParameter(1"여성게1");
    5.         
    6.         List<MemberJPQL> resultList = nativeQuery.getResultList();
    7.         
    8.         System.out.println("====================nativeQuery4=====================");
    9.         
    10.         for(MemberJPQL m : resultList) {
    11.                 System.out.println(m.toString());
    12.         }
    13.         
    14.         System.out.println("====================nativeQuery4=====================");
    15. }
    16. public void nativeQuery5() {
    17.         
    18.         Query nativeQuery = em.createNamedQuery("Member.memberSQL2")
    19.                         .setParameter(1"여성게1");
    20.         
    21.         List<MemberJPQL> resultList = nativeQuery.getResultList();
    22.         
    23.         System.out.println("====================nativeQuery5=====================");
    24.         
    25.         for(MemberJPQL m : resultList) {
    26.                 System.out.println(m.toString());
    27.         }
    28.         
    29.         System.out.println("====================nativeQuery5=====================");
    30. }


    em.createNativeQuery()에서 다루던 것을 어노테이션으로 다 가져다 놓았다고 보면된다. 그리고 조금 특이한 것은 @NamedQuery와 같이 em.createNamedQuery()를 사용한다는 것이다.

    posted by 여성게
    :
    Web/Spring 2018. 8. 12. 13:17

    @Inject / @Named 어노테이션


    JSR 330(자바 의존성 주입)은 자바 플랫폼을 위한 의존성 주입 어노테이션을 표준화해서 스프링의 @Autowired 및 @Qualifier 어노테이션과 비슷한 @Inject@Named 어노테이션을 정의하고 있다.




    @Service("abc")

    public class A{

    @Autowired

    @Qualifier("bb")

    private B b;

    }


    이러한 클래스와 대비해서 @Inject & @Named 어노테이션을 적용해보면


    @Named("abc")

    public class A{

    @Inject

    @Named("bb")

    private B b;

    }


    이와 같이 동일하게 적용할 수 있다.


    즉, @Named 같은 경우 클래스 레벨에 적용되면 @Component,@Service 등등과 같은 어노테이션과 동일하게 적용되며, 인스턴스 및 메소드,생성자 레벨에 적용된다면 @Qualifier 어노테이션과 동일하게 이름 기준으로 의존성을 주입하는 기능을 하게된다. @Inject 같은 경우는 @Autowired 와 거의 동일한 기능을 하며 변수의 형식을 기준으로 자동으로 빈주입을 하는 역할을 담당한다.



    @Resource / @PostConstruct / @PreDestroy 어노테이션


    @Service("abc")

    public class A{

    @Autowired

    @Qualifier("bb")

    private B b;

    }


    이러한 정의를 @Resource 어노테이션을 이용한다면


    @Service("abc")

    public class A{

    @Resource(name="bb")

    private B b;

    }


    이렇게 적용할 수 있다. 즉, @Resource 어노테이션은 이름을 기준으로 의존성을 주입할 때 사용 할 수 있는 어노테이션이다. 만약 이름을 기준으로 의존성을 주입할 경우에는 @Autowired & @Qualifier 를 이용하는 방법과 @Resource 를 이용하는 두가지 방법이 있지만, @Resource 어노테이션을 이용하는 것이 좋다. 전자와 같은 경우는 우선 형식이 동일한 빈을 찾는 과정이 선행된 후에 이름과 같은 빈을 찾지만, 후자 같은 경우는 바로 해당이름과 동일한 빈을 찾는 과정을 들어가기 때문이다.



    @PostConstruct & @PreDestroy 어노테이션은 xml 설정에서 빈의 속성 중에 init-method와 destroy-method 와 비슷한 역할을 하는 어노테이션이다. 즉, 사용자 빈정의에서 초기화과정에서 어떠한 행위를 원한다면 행위를 정의한 메소드에 @PostConstruct를, 빈의 생명주기가 다해서 GC되기 전에 어떠한 행위를 원한다면 행위를 정의한 메소드에 @PreDestroy 어노테이션을 붙여주면 빈 초기화과정과 삭제과정에서 어떠한 행위를 정의 할 수 있다.

    posted by 여성게
    :
    Web/Spring 2018. 7. 28. 21:57

    <bean>의 depends-on 속성




    applicationContext.xml에 빈정의에 대한 설정을 모두 마치고 실행하게 되면 빈의 생성은 선언한 순서대로 생성이 된다.(만약 의존하는 빈이 다른 위치에 있다면 그 빈을 먼저 생성하고 다시 순서대로 내려온다.) 하지만 명시적으로 의존성이 들어나지 않고 암시적으로 빈이 어떠한 다른 빈을 의존하고 있는 경우가 있을 수 있다. 만약 명시적으로 의존이 들어나 있다면 스프링컨테이너는 알아서 순서에 맞춰 빈을 생성해 주지만 암시적으로 겉으로 들어나지 않은 의존성 같은 경우는 컨테이너도 어쩔수가 없다. 


    예를 들면, 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <bean id="service"
            class="sample.spring.chapter04.bankapp.service.FixedDepositServiceImpl">
            <property name="fixedDepositDao">
                <bean class="sample.spring.chapter04.bankapp.dao.FixedDepositDaoImpl" />
            </property>
            <constructor-arg index="0"
                value="META-INF/config/appConfig.properties" />
        </bean>
     
        <bean id="eventSenderSelectorService"
            class="sample.spring.chapter04.bankapp.service.EventSenderSelectorServiceImpl">
            <constructor-arg index="0"
                value="META-INF/config/appConfig.properties" />
        </bean>
    cs

     

    eventSenderSelectorService라는 빈이 생성될때, appConfig.properties 파일에 어떠한 정보를 남겨준다. 그러면 service 빈이 그 appConfig.properties 파일에 작성된 정보를 이용하여 빈생성과정이 이루어진다. 위의 설정파일에서는 암시적으로 service 빈이 eventSenderSelectorService 빈을 의존하고 있는 것이다. 그래서 service 빈이 먼저 생성되는 과정이 된다면? 빈생성 과정에서 오류가 생기고 말것이다. (eventSenderSelectorService 빈 생성이 먼저 이루어져야함으로) 여기서 사용할 수 있는 속성이 depends-on 속성이다.




    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <bean id="service"
            class="sample.spring.chapter04.bankapp.service.FixedDepositServiceImpl"
               depends-on="eventSenderSelectorService ">
            <property name="fixedDepositDao">
                <bean class="sample.spring.chapter04.bankapp.dao.FixedDepositDaoImpl" />
            </property>
            <constructor-arg index="0"
                value="META-INF/config/appConfig.properties" />
        </bean>
     
        <bean id="eventSenderSelectorService"
            class="sample.spring.chapter04.bankapp.service.EventSenderSelectorServiceImpl">
            <constructor-arg index="0"
                value="META-INF/config/appConfig.properties" />
        </bean>
    cs

    이렇게 depends-on으로 의존하고 있는 빈을 들어내줌으로써 eventSenderSelectorService 빈을 먼저 생성한 후에 service 빈을 생성한다.



    depends-on속성에는 ","을 구분자로 여러개의 빈을 의존하고 있음을 명시할 수 있다.

    하지만 이 속성은 자식 빈정의에는 상속되지 않는 속성임에 주의해야한다.(부모빈에 이 속성이 정의되어 있어도 자식에는 상속되지 않음)

    posted by 여성게
    :
    Web/Spring 2018. 7. 10. 20:13

    스프링프레임워크 p & c schema


    servlet context & servlet xml 설정파일에서 <property>,<constructor-arg> 로 속성을 정의하지 않고 간결한 표현을 제공하는 것이 p와 c schema이다.

    말로 설명하는 것보다는 간단한 예제를 통해서 보는 것이 이해가 빠르다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    package sample.spring.chapter03.beans;
     
    import java.util.Currency;
    import java.util.Date;
     
    public class BankDetails {
        private String bankName;
        private byte[] bankPrimaryBusiness;
        private char[] headOfficeAddress;
        private char privateBank;
        private Currency primaryCurrency;
        private Date dateOfInception;
        private Address branchAddresses;
     
        public String getBankName() {
            return bankName;
        }
     
        public void setBankName(String bankName) {
            this.bankName = bankName;
        }
     
        public byte[] getBankPrimaryBusiness() {
            return bankPrimaryBusiness;
        }
     
        public void setBankPrimaryBusiness(byte[] bankPrimaryBusiness) {
            this.bankPrimaryBusiness = bankPrimaryBusiness;
        }
     
        public char[] getHeadOfficeAddress() {
            return headOfficeAddress;
        }
     
        public void setHeadOfficeAddress(char[] headOfficeAddress) {
            this.headOfficeAddress = headOfficeAddress;
        }
     
        public Currency getPrimaryCurrency() {
            return primaryCurrency;
        }
     
        public void setPrimaryCurrency(Currency primaryCurrency) {
            this.primaryCurrency = primaryCurrency;
        }
     
        public Date getDateOfInception() {
            return dateOfInception;
        }
     
        public void setDateOfInception(Date dateOfInception) {
            this.dateOfInception = dateOfInception;
        }
     
        public char getPrivateBank() {
            return privateBank;
        }
     
        public void setPrivateBank(char privateBank) {
            this.privateBank = privateBank;
        }
     
        public Address getBranchAddresses() {
            return branchAddresses;
        }
     
        public void setBranchAddresses(Address branchAddresses) {
            this.branchAddresses = branchAddresses;
        }
     
        @Override
        public String toString() {
            return "BankDetails [bankName=" + bankName + ", bankPrimaryBusiness="
                    + new String(bankPrimaryBusiness) + ", headOfficeAddress="
                    + new String(headOfficeAddress) + ", primaryCurrency="
                    + primaryCurrency + ", dateOfInception=" + dateOfInception
                    + ", privateBank=" + privateBank + ", branchAddresses="
                    + branchAddresses + "]";
        }
    }
     
    cs


    setter를 이용해 빈주입을 하는 예제이다. xml 파일에서는 원래라면 <property> tag를 이용하여 빈의 속성을 주입해줄 것이다.

    하지만 p schema를 이용하면 훨씬 간결한 표현이 된다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:c="http://www.springframework.org/schema/c"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
     
        <bean id="bankDetails" class="sample.spring.chapter03.beans.BankDetails"
            p:bankName="My Personal Bank" p:bankPrimaryBusiness="Retail banking"
            p:headOfficeAddress="Address of head office" p:privateBank="Y"
            p:primaryCurrency="INR" p:dateOfInception="30-01-2012"
            p:branchAddresses-ref="branchAddresses" />
    </beans>
    cs


    여기서 p schema를 사용하기 위해 네임스페이스를 추가해주는 설정이 필요하다. 단순 값은 p:bankName="value" 형태이고, 다른 빈을 참조할 경우는 

    p:branchAddress-ref="빈id"이다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package sample.spring.chapter03.beans;
     
    import java.beans.ConstructorProperties;
    import java.util.Date;
     
    public class BankStatement {
        private Date transactionDate;
        private double amount;
        private String transactionType;
        private String referenceNumber;
     
        @ConstructorProperties({ "transactionDate""amount""transactionType",
                "referenceNumber" })
        public BankStatement(Date transactionDate, double amount,
                String transactionType, String referenceNumber) {
            this.transactionDate = transactionDate;
            this.amount = amount;
            this.transactionType = transactionType;
            this.referenceNumber = referenceNumber;
        }
     
        @Override
        public String toString() {
            return "BankStatement [transactionDate=" + transactionDate
                    + ", amount=" + amount + ", transactionType=" + transactionType
                    + ", referenceNumber=" + referenceNumber + "]";
        }
    }
    cs

    생성자로써 의존주입을 할때 예시이다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:c="http://www.springframework.org/schema/c"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
            
     
               <bean id="bankStatement" class="sample.spring.chapter03.beans.BankStatement"
            c:transactionDate="30-01-2012" c:amount="1000" c:transactionType="Credit"
            c:referenceNumber="1110202" />
    </beans>
    cs


    p schema와 방법은 차이가 없다. 하지만 여기서는 constructor 매개변수들의 이름을 지정하였다.(@ConstructorProperties 어노테이션을 이용하여 디버그플러그를 비활성화 시켰을 때, 생성자의 매개변수 이름을 지정하여 참조할 수 있게 설정하였다.) 하지만 index로 참조하려고 할 경우도 있을 것이다. 그럴경우에는 c:_0-ref="bean id" 이런 식으로 작성하면 된다. _를 포함하는 인덱스넘버를 넣어줘야함에 유의하면 된다.


    posted by 여성게
    :
    Web/Spring 2018. 3. 7. 16:28

    Spring Transaction(트랜잭션) 범위 설정하기



    1.예외 상황(트랜잭션 범위설정이전)


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public void insertUserTest() {
            
            UserDTO user1=new UserDTO();
            UserDTO user2=new UserDTO();
            UserDTO user3=new UserDTO();
            
            user1.setId("test@test.com");
            user1.setPw("1111");
            user1.setNickName("tester");
            user2.setId("test1@test.com");
            user2.setPw("1111");
            user2.setNickName("tester1");
            user3.setId("test@test.com");
            user3.setPw("1111");
            user3.setNickName("tester2");
            
            userService.insertUser(user1);
            userService.insertUser(user2);
            userService.insertUser(user3);
            
        }
    cs

    예를 들어 이런 코드가 있다고 가정해보자. 여기서 보면 user3이라는 객체의 인스턴스변수를 설정해줄때, user3.setId() 메소드 부분에 user1과 같은 id가 들어가서 duplicationkey라는 예외가 발생하여 user3은 DB에 정상적으로 데이터가 삽입되지 않을 것이다. 여기서 transaction manager를 사용한다고 가정했을 때,



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
        <!-- Transaction Manager -->
        <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
        <!-- txManager Advice -->
        <tx:advice id="txAdvice" transaction-manager="txManager">
            <tx:attributes>
                <tx:method name="get*" read-only="true"/>
                <tx:method name="*" propagation="REQUIRED"/>
            </tx:attributes>    
        </tx:advice>
        <!-- txManagerAdvice aop -->
        <aop:config>
            <aop:pointcut expression="execution(* com.web.nuri..*(..))" id="txPointCut"/>
            <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
        </aop:config>
    cs

    위에 보이는 대로 transaction manager를 설정하면 user1, user2는 정상적으로 DB에 삽입되고 user3는 DB에 저장되지 않고 rollback 될 것이다. 즉, UserDAO(userService)의 메소드 단위로 트랜잭션이 적용되어 user3에서 예외가 발생하여도 user1,user2는 정상적으로 DB에 삽입이 되는 것이다. 본인은 트랜잭션의 범위의 설정을 메소드 단위가 아닌 내가 직접 범위를 설정하여 user3에서 예외가 발생하였다고 하면 이전의 user1,user2의 데이터도 DB에 삽입되지 않게 하고 싶은 것이다.



    2.트랜잭션 범위 설정


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class UserDAOTest {
        @Autowired
        UserService userService;
        @Autowired
        PlatformTransactionManager transactionManager;
        
        //트랜잭션 경계설정 코드
        public void insertUserTest() {
            //트랜잭션의 시작을 알린다.(트랜잭션을 얻어오는 시점이 시작)
            TransactionStatus status=this.transactionManager.getTransaction(new DefaultTransactionDefinition());
            UserDTO user1=new UserDTO();
            UserDTO user2=new UserDTO();
            UserDTO user3=new UserDTO();
            try {
                user1.setId("test@test.com");
                user1.setPw("1111");
                user1.setNickName("tester");
                user2.setId("test1@test.com");
                user2.setPw("1111");
                user2.setNickName("tester1");
                user3.setId("test@test.com");
                user3.setPw("1111");
                user3.setNickName("tester2");
            
                userService.insertUser(user1);
                userService.insertUser(user2);
                userService.insertUser(user3);
                this.transactionManager.commit(status);
            }catch (RuntimeException e) {
                // TODO: handle exception
                this.transactionManager.rollback(status);
                throw e;
            }        
        }
    }
    cs



    여기에 적용되는 것은 스프링의 트랜잭션 서비스 추상화 기법이다. PlatformTransactionManager(트랜잭션매니저의 최상위 인터페이스) 라는 인터페이스에 각자의 DB에 해당되는 TransactionManager 클래스(여기서는 mybatis를 이용하므로 DataSourceTransactionManager가 된다)를 의존주입 해준 후에 TransactionStatus 객체를 생성해준다.( 이시점이 트랜잭션의 시작시점이 되는 것이다.)

    그리고 예외없이 메소드가 잘 수행되면 transactionManager.commit(status), 예외가 발생하였다면 transactionManager.rollback(status) 메소드를 수행시킨다. 이 시점이 트랜잭션 범위의 끝지점이 되는 것이다. 이렇게 범위를 설정하게 되면 user3 객체의 삽입에서 예외가 발생하면 user1,user2의 객체는 이전에 수행됬던 것들이 모두 rollback 된다.




    posted by 여성게
    :