영속성 컨텍스트의 특징

영속성 컨텍스트는 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분합니다.
따라서 영속 상태는 식별자 값이 반드시 있어야 합니다. 식별자 값이 없으면 예외가 발생하는걸 알아두셔야 합니다.
영속성 컨텍스트에 엔터티를 저장하면 이 엔터티는 바로 데이터베이스에 저장되지 않습니다. 트랜잭션 커밋이 수행되기 전까지 영속성 컨텍스트에 있다가 커밋을 수행하면 플러시라는걸 호출하여 쓰기지연 SQL 저장소라는 메모리 공간에 쌓인 sql문을 데이터베이스에 동기화하는 작업을 수행하고 이것을 플러시라고 합니다. 플러시는 아래서 더 자세하게 다루어 보겠습니다.

영속성 컨텍스트(Persistence Context)의 장점

1차 캐시
동일성 보장
트랜잭션을 지원하는 쓰기 지연
변경 감지
지연 로딩
위에서 말한 영속성 컨텍스트의 장점에 대해서 리뷰하고 어떤 이점이 있는지 엔터티를 CRUD하는 예제를 보면서 하나씩 살펴보겠습니다.

엔터티 조회

Member member = new Member();
member.setId("member1");
member.setUsername("회원");

em.persist(member);

이 코드를 실행하면 영속 컨텍스트 내부에 존재하는 1차 캐시에 회원 엔터티를 저장합니다.
회원 엔터티는 아직 데이터베이스에 저장되지 않는 상태입니다. 1차 캐시는 Collection인 Map 형태의 자료구조로 Key와 value 값을 가지고 있는 영역이라고 생각하시면 됩니다. Key는 식별자 값으로 @Id이고, value 값으로는 해당 member 인스턴스 즉 엔터티 값이 들어가 있습니다. 그리고 식별자 값은 데이터베이스 기본 키와 매핑되어 있습니다.

따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스의 기본 키 값입니다.

엔터티 조회는 아래코드와 같습니다.

Member member = em.find(Member.class, "member1");

find() 메소드를 보면 첫 번째 파라미터는 엔터티 클래스 타입이고, 두번째는 조회할 엔터티의 식별자 값 입니다. 이 메소드를 호출하면 먼저 1차 캐시에서 엔터티를 찾고 만약 존재하지 않는다면 데이터베이스에서 조회를 하여 엔터티를 생성합니다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔터티를 반환합니다.

스크린샷 2019-10-04 오전 1 26 19

이러한 방식으로 한번 DB에서 조회를 한 후에 영속 컨텍스트에 저장하면 메모리에 있는 1차 캐시에서 바로 불러오기 때문에 성능상의 이점을 누릴 수 있습니다.

엔터티 등록

엔터티 매니저를 사용해서 엔터티를 영속성 컨텍스트에 등록하는 과정을 살펴보겠습니다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔터티 매니저는 데이터 변경 시 트랜잭션을 시작합니다.
transaction.begin(); // 트랜잭션 시작

em.save(memberA);
em.save(memberB);
//여기까지 Insert SQL을 데이터베이스에 보내지 않습니다.

transaction.commit(); // 트랜잭션 커밋

엔터티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔터티를 저장하지 않고 내부 쿼리를 쓰기지연 SQL저장소에 Insert 쿼리문들을 차곡차곡 쌓아두었다가 실제로 트랜잭션을 커밋하는 시점에서 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기지연이라고 합니다.

트랜잭션을 커밋하는 시점에 엔터티 매니저는 우선 영속성 컨텍스트를 플러시를 합니다. 전에 올렸던 포스팅에서 플러시를 언급했었었는데요. 플러시란 영속성 컨텍스트의 변경 내용을 데이터 베이스에 동기화하는 작업입니다. 이때 등록, 수정, 삭제한 엔터티를 데이터베이스에 반영합니다. 한마디로 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보내는 작업이라고 생각하시면 됩니다. 이렇게 영속성 컨텍스트 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋합니다.

어차피… 데이터를 저장하는 즉시 등록 쿼리를 데이터베이스에 보내거나 데이터를 저장하면 등록 쿼리를 데이터베이스에 보내지않고 메모리에 모아두다가 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는 방법 모두 다 트랜잭션 범위 안에서 실행되므로 둘의 결과는 같습니다. 결과적으로… 트랜잭션을 커밋하기 전까진 아무 소용이 없는거죠 어떻게든 커밋 직전에만 데이터베이스에 SQL문을 전달하기만 하면 됩니다. 이것이 트랜잭션을 지원하는 쓰기 지연이 가능한 이유입니다.

이러한 기능을 잘만 활용한다면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화 할 수 있습니다.

엔터티 수정

SQL을 사용하면 수정 쿼리를 직접 작성해야 합니다. 그런데 프로젝트가 점점 커지고 요구사항이 늘어나면서 수정 쿼리도 점점 추가됩니다. 다음 아래와 같은 수정쿼리가 있습니다.

update MEMBER
SET
    NAME=?
    AGE=?
WHERE
    id=?

회원의 이름과 나이를 변경하는 기능을 개발했는데 회원의 등급을 변경하는 기능이 추가되면 회원의 등급을 다음과같이 수정 쿼리를 추가로 작성해야합니다.

UPDATE MEMBER
SET
   GRADE=?
WHERE
   id=?

보통은 이렇게 2개의 수정 쿼리를 작성한다. 물론 둘을 합쳐서 다음과 같이 하나의 수정 쿼리만을 사용해도 됩니다.

UPDATE MEMBER
SET 
   NAME=?
   AGE=?
   GRADE=?
WHERE
   id=?

하지만 이렇게 합쳐진 쿼리를 사용해서 이름과 나이를 변경하는 데 실수로 등급 정보를 입력하지 않거나, 등급을 변경하는데 실수로 이름과 나이를 입력하지 않을 수도 있습니다.
이런 개발방식의 문제점은 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 합니다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 됩니다.

이러한 문제점을 해결하기 위해서 JPA에서는 변경감지 기능을 제공합니다.
JPA로 엔터티를 수정할 때는 단순히 엔터티를 조회해서 데이터만 변경하면 됩니다.
엔터티의 데이터만 변경했는데 어떻게 데이터베이스에 반영이 될까… 이렇게 엔터티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경감지라고 합니다.

JPA는 엔터티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 합니다. 그리고 플러시 시점에 스냅샷과 엔터티를 비교하여 변경된 엔터티를 찾습니다.

스크린샷 2019-10-04 오전 1 26 28

위의 그림이 가장 잘 설명하는거 같아서 첨부하였습니다 ㅎㅎ… 변경 감지는 영속성 컨텍스트가 제공하는 기능으로… 엔터티가 영속성 컨텍스트에 존재하지 않으면 당연히 변경된 데이터를 가진 엔터티는 DB에 변경된 상태로 저장되지 않습니다. 오직 영속상태의 엔터티만 적용됩니다.

엔티티 삭제

엔터티를 삭제하려면 먼저 삭제 대상 엔터티를 조회해야 합니다.

Member memberA = em.find(Member.class, "memberA); //삭제 대상 엔터티 조회
em.remove(memberA);

em.remove();

em.remove()에 삭제 대상 엔터티를 넘겨주면 엔터티를 삭제합니다. 물론 엔터티를 즉시 삭제하는 것이 아니라 엔터티 등록과 비슷하게 삭제쿼리를 쓰기 지연 SQL 저장소에 등록합니다.
트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달합니다. 참고로 em.remove(memberA)를 호출하는 순간 memberA는 영속성 컨텍스트에서 제거됩니다.
이렇게 삭제된 엔터티는 재사용하지 말고 자연스럽게 GC(가비지 컬렉션)의 대상이 되도록 두는 것이 좋습니다.

플러시란?

플러시는(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다. 플러시를 실행하면 구체적으로 다음과 같은 일이 일어납니다.

  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔터티를 스냅샷과 비교해서 수정된 엔터티를 찾습니다. 수정된 엔터티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록합니다.

  2. 쓰기 지연 SQL 저장소의 쿼리를 데이터 베이스에 전송합니다.(등록, 수정, 삭제, 쿼리)

영속성 컨텍스트를 플러시하는 방법은 총 3가지 입니다.

  1. em.flush()를 직접 호출합니다.

  2. 트랜잭션 커밋 시 플러시가 자동으로 호출됩니다.

  3. JPQL 쿼리 실행 시 플러시가 자동 호출 됩니다.

직접 호출

엔터티 매니저의 flush()의 메소드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시 합니다.
테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용하지 않습니다.

트랜잭션 커밋 시 플러시 자동 호출

데이터베이스에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않습니다. 따라서 트랜잭션을 커밋하기 전에 꼭 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 합니다.
JPA는 이런 문제를 예방하기 위해서 트랜잭션을 커밋할 때 플러시를 자동으로 호출합니다.

JPQL 쿼리 실행 시 플러시 자동 호출

JPQL이나 Criteria같은 객체지향 쿼리를 호출할 때도 플러시가 실행된다.
예시를 통해서 한번 알아보겠습니다.

//save 메소드는 JPA 구현체인 하이버네이트에서 제공해주는 함수로 persist처럼 해당 엔터티를 영속화 시킵니다.
em.save(memberA);
em.save(memberB);
em.save(memberC);

//중간에 JQPL 실행
query = em. createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

먼저 em.save()나 em.persist()를 호출해서 엔터티 memberA, memberB, memberC를 영속 상태로 만들었습니다. 이 엔터티들은 영속성 컨텍스트에는 있지만 아직 데이터베이스에는 반영되지 않았습니다. 이때 JPQL를 실행하면 JQPL은 SQL로 변환되어 데이터베이스에서 엔터티를 조회합니다. 그런데 memberA, memberB, memberC는 아직 데이터베이스에 없으므로 쿼리 결과로 조회되지 않습니다. 따라서 쿼리를 실행하기 직전에 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영해야 합니다.
마찬가지로 JPA는 이런 문제를 예방하기 위해 JPQL을 실행할 때도 플러시를 자동 호출합니다.

지금까지 영속 컨텍스트가 제공하는 기능과 엔터티를 조회, 등록, 수정, 삭제 작업을 수행할 때 어떠한 과정을 거치는지… 그 과정속에서 플러시를 호출하는 메커니즘까지 살펴보았습니다.

출저: 자바 ORM 표준 JPA 프로그래밍

'SpringFramework > JPA' 카테고리의 다른 글

스프링 부트 테스트 : @DataJpaTest  (0) 2019.10.09
JPA 다양한 연관관계 매핑  (0) 2019.10.04
JPA 연관관계 매핑기초  (0) 2019.10.04
JPA가 지원하는 엔티티 매핑  (0) 2019.10.04
JPA를 이용한 영속성 관리  (0) 2019.10.04

JPA 그게 뭔데?

JPA(Java Persistence API)는 ORM(Object Relational Management) 기술 표준으로 스프링 부트 교재를 보면 대부분 실려있는 기술 스펙 입니다.

JPA가 어떤기술인지 알아보기전에 대체 왜 JPA가 세상에 나오게 됬는지에 대해서나 알게되면 확실히 기억에도 오래남는거 같아서 저의 얄팍한 지식으로 살펴보겠습니다.
스프링 프레임워크로 프로젝트를 해본 사람이라면 간단하든 복잡하든 DB를 사용하면서 대부분의 객체들을 테이블에 매핑하는 데이터베이스 관련 작업을 Spring JDBC Template이나 Mybatis를 이용하여 해보셨을 겁니다.
하지만 프로젝트 규모가 커지면서 객체와 테이블을 매핑하기 위해서 반복적이고 지루한 JDBC API를 호출하게 되고 이 과정에서 코드의 가독성과 유지보수적 측면에서 많은 효율성이 떨어지게 됩니다. 간단하게 개발자들이 해당 객체를 DB에 저장하기 위해서 테이블을 설계하는데 많은 시간을 소모하게 됩니다.
이렇게 되면 핵심 비즈니스 로직을 테스트하는데 소모하는 시간도 줄어들기 때문에 심각한 에러를 초래할 수 있고, 이러한 상황이 계속 무한루프 되는 악순환이 발생하는 것이죠.

이러한 문제점을 해결하기 위해서 JPA라는 걸출한 기술이 등장하게 되었습니다. JPA는 위에서 설명한것처럼 ORM을 정의하는 자바진영의 기술 표준입니다. 이러한 JPA 구현체는 여러개가 있지만 제가 아는 대표적인 구현체는 Hibernate 입니다.
저는 Hibernate를 기준으로 이야기 하겠습니다.

스크린샷 2019-10-04 오전 1 09 22

JPA가 제공하는 기능은 크게 두가지가 있습니다.

엔터티와 테이블을 매핑하는 설계하는 부분
매핑한 엔터티를 실제 사용하는 부분
위에 말하는 엔터티는 쉽게말해서 클래스이지만 그냥 클래스가 아니고 JPA에게 테이블과 매핑한다고 알려주고 클래스 위에 @Entity가 적용된 클래스를 엔터티라고 부릅니다.
이렇게 매핑한 엔터티를 엔터티 매니저를 통해서 영속성 컨테스트(Persistence Context)에 관리하게 됩니다.

스크린샷 2019-10-04 오전 1 11 07

엔터티 매니저는 엔터티를 저장, 수정, 삭제, 조회하는 등 엔터티와 관련된 모든 일을 처리합니다.

즉, 엔터티를 관리하는 관지라인 샘이죠 개발자 입장에서는 엔터티 매니저는 엔터티를 저장하는 가상의 데이터베이스로 생각하면 이해하기 쉽습니다.

엔티티 매니저 팩토리와 엔티티 매니저

스크린샷 2019-10-04 오전 1 11 23

데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성합니다. EntityManager를 생성하기 위해서는 META-INF/persistence.xml이라는 클래스 패스에서 설정 정보를 사용해서 EntityManagerFactory를 먼저 생성해야 합니다.

스크린샷 2019-10-04 오전 1 11 33

EntityManagerFactory는 EntityManager를 생산하는 공장이라고 생각하시면 됩니다. 공장을 만드는 비용은 상당히 크기때문에 한개만 만들어서 애플리케이션 전체에서 공유도록 설계되어 있습니다. 반면에 EntityManager를 생산하는 비용은 거의 들지 않습니다.

EntityManager는 Entity를 영속성 컨텍스트에 관리하고 있습니다. 이것은 JPA를 이해하는데 가장 중요한 이유입니다. 영속성 컨텍스트는 엔터티를 영구 저장하는 환경이라는 뜻입니다.

엔터티의 생명주기

스크린샷 2019-10-04 오전 1 19 30

비영속일때는 엔터티 객체를 생성했했지만 아직 순수한 객체 상태이며 아직 저장하지 않았습니다.
따라서 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없기때문에 이것을 비영속 상태라고 합니다.

ex) 비영속 상태

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

영속상태는 엔터티 매니저를 통해서 엔터티를 영속성 컨텍스트에 저장합니다. 이렇게 영속성 컨텍스트가 관리하는 엔터티를 영속 상태라고 합니다. 이제 회원 엔터티는 비영속 상태에서 영속상태가 되었습니다.

ex) 영속 상태

//비영속 상태인 member 인스턴스를 영속상태로 변경
em.persist(member);

em.find()나 JPQL을 사용해서 조회한 엔터티도 영속성 컨텍스트가 관리하는 영속상태입니다.

스크린샷 2019-10-04 오전 1 22 37

결국 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻입니다.

준영속은 영속성 컨텍스트가 관리하던 영속상태의 엔터티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 됩니다. 특정 엔터티를 준영속 상태로 만들려면 em.detach()와 영속성컨텍스트를 닫는 em.close(), 영속성 컨텍스트를 초기화하는 em.clear()를 호출하면 영속상태의 엔터티는 준영속상태가 됩니다.

ex) 준영속 상태

em.detach(member);

삭제는 엔터티를 영속성 컨텍스트와 데이터베이스에서 삭제합니다.
ex) 삭제 상태
em.remove(member);

오늘은 간단하게 JPA의 개념과 등장이유 그리고 엔터티 매니저를 생성하는 엔터티매니저 팩토리,엔터티를 관리하는 엔터티 매니저, 영속성 컨텍스트의 개념, 엔터티 생명주기에 대해서만 리뷰하였습니다.

다음 포스팅에서는 영속성 컨텍스트의 특징과 영속성 컨텍스트에서 관리하는 엔터티를 데이터 베이스에 동기화하는 플러시(flush)에 대해서 더 자세하게 다루어 보겠습니다.

출저: 자바 ORM 표준 JPA 프로그래밍

안녕하세요
이번 포스팅은 Spring Data JPA를 이용하여 동적으로 SQL을 처리하는 Querydsl에 대해 진행하겠습니다.

Querydsl이란?

쿼리를 처리하다 보면 다양한 상황에 맞게 쿼리를 생성하는 경우가 많습니다.
대표적인 케이스가 다양한 검색 조건에 대해서 쿼리를 실행해야 하는 경우라고 할 수 있습니다.
쿼리 메소드나 @Query를 이용하는 경우에 개발 속도는 좋지만 고정적인 쿼리만을 생산합니다.
이러한 이유로 동적인 상황에 대한 처리를 위해서 Querydsl이라는 것을 이용합니다.

단순 문자열(JDBC, Mybatis, JPQL)과 비교해서 Fluent API(Querydsl)를 사용할 때의 장점은 다음과 같습니다.

  1. IDE의 코드 자동 완성 기능 사용
  2. 문법적으로 잘못된 쿼리를 허용하지 않습니다.
  3. 도메인 타입과 property를 안전하게 참조할 수 있습니다.
  4. 도메인 타입의 리팩토링을 더 잘 할 수 있습니다.

요약하면 Querydsl은 타입에 안전한 방식으로 쿼리를 실행하기 위한 목적으로 만들어졌습니다.
즉, Querydsl의 핵심 원칙은 타입 안전성(Type safety)입니다.
그것이 가능한 이유는 문자열이 아닌 메서드 호출로 쿼리가 수행되기 때문입니다.

저는 Maven을 이용해서 Querydsl에 대해 의존성과 플러그인을 설정하겠습니다.
Querydsl을 이용하기 위해서는 다음과 같은 과정을 수행해야 합니다.

  • pom.xml의 라이브러리와 Maven 설정의 변경 및 실행
  • Predicate의 개발
  • Repository를 통한 실행
  1. Querydsl 의존성 라이브러리를 추가합니다.
    <dependency>
     <groupId>com.querydsl</groupId>
     <artifactId>querydsl-apt</artifactId>
     <version>${querydsl.version}</version>
     <scope>provided</scope>
    </dependency>
    <dependency>
     <groupId>com.querydsl</groupId>
     <artifactId>querydsl-jpa</artifactId>
     <version>${querydsl.version}</version>
    </dependency>
    


2. pom.xml에 Querydsl 플러그인을 추가합니다.
~~~xml
 <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <goals>
                <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
 </plugin>
~~~

플러그인에서 outputDirectory 태그에 경로를 적어주었는데요. Querydsl에서는 JPA를 처리하기 위해서 엔터티 클래스를 생성하는 방식을 이용합니다. 이를 'Qdomain'이라고 하는데,
이 도메인 클래스는 위의 해당경로에 생성이 됩니다.

정상적으로 설정이 되었다면 프로젝트 내에 target/generated-sources/java 디렉토리에 Qdomain 클래스가 생성되는 것을 볼수 있습니다.
이때 주의할 사항은 해당 경로는 패스에 존재하지 않기 때문에 Qdomain을 찾지 못한다고 에러가 발생 할 수 있습니다. 따라서 아래와 같이 해당경로를 패스에 추가를 해줘야 합니다.

![스크린샷 2019-06-01 오후 11 53 22](https://user-images.githubusercontent.com/22395934/58750072-85862f00-84c8-11e9-877f-099e106548c2.png)

저는 현재 해당경로를 소스 폴더에 추가한 상태입니다. IntelliJ에서는 File>Project Structure>Modules에 들어가면 위 화면 같이 구성되어있습니다.
추가방법은 좌측 트리에 target/generated-sources/java 디렉토리를 우클릭하여 Sources를 선택하면 우측 Source Folders에 추가가 됩니다.

이제 간단하게 엔터티 클래스를 이용하여 동적으로 쿼리를 생성하는 예제를 보여드리겠습니다.

먼저, Repository 코드에서 QueryDslPredicateExcutor<T> 인터페이스를 상속하도록 아래와 같이 변경해 줍니다.
```java
public interface BoardRepository extends CrudRepository<Board, Long>, QuerydslPredicateExecutor<Board> {

}

QueryDslPredicateExcutor 인터페이스는 다음과 같은 메소드들이 선언되어 있습니다.

스크린샷 2019-10-03 오후 3 58 49

엔터티 클래스

@Getter
@Setter
@ToString
@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long bno;

    private String title;

    private String writer;

    private String content;

    @CreationTimestamp
    private Timestamp regdate;

    @UpdateTimestamp
    private Timestamp updatedate;
}

Predicate를 생성 및 테스트를 수행하는 코드입니다.
Predicate는 '확신한다', '단언하다'라는 뜻으로 이 조건이 맞다고 판단하는 근거를 함수로 제공하는 것입니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTests {

    @Autowired
    private BoardRepository boardRepository;

    @Test
    public void testPredicate(){

        String type = "t";
        String keyword = "17";
        BooleanBuilder builder = new BooleanBuilder();


        QBoard board = QBoard.board;

        //메소드를 통해서 쿼리문을 생성함.
        if(type.equals("t")){
            builder.and(board.title.like("%" + keyword +"%"));
        }

        //bno 값이 0보다 큰 값을 조건에 추가
        builder.and(board.bno.gt(0L));
        //페이징 처리에 정보를 가진 객체 생성
        Pageable pageable = PageRequest.of(0,10);

        Page<Board> result = boardRepository.findAll(builder, pageable);

        System.out.println("PAGE SIZE: " + result.getSize());
        System.out.println("TOTAL PAGES: " + result.getTotalPages());
        System.out.println("TOTAL COUNT: " + result.getTotalElements());
        System.out.println("NEXT: " + result.nextPageable());

        List<Board> list = result.getContent();

        list.forEach(b -> System.out.println(b));
    }

위의 코드를 보면 상황에 따라 조건문이 달라집니다.

  • type이 오면 where type = type
  • keyword가 오면 where keyword = keyword

즉, 파라미터가 어떻게 오는지에 따라 where의 조건이 변경되는 것입니다.
이를 해결하기 위한 방법으로 BooleanBuilder를 자주 사용합니다.

QBoard 객체는 엔터티 클래스인 Board 객체를 참조해서 생성이 됩니다.
만약 위와 같이 설정파일과 패스경로에 추가했음에도 생성이 되지 않는다면 pom.xml을 우클릭하여 아래 화면처럼 Generate Sources and Update Folders를 클릭해줍니다.

스크린샷 2019-06-02 오전 12 23 50

if문에서 필요한 부분만을 BooleanBuilder에 추가하면서 쿼리를 만들었습니다.
메소드로 쿼리를 생성하기 때문에 위에서 언급한것 처럼 안정적이고 문법적인 오류를 허용하지 않습니다. 하지만 where문의 조건을 한눈에 보기 어렵습니다. 지금은 조건문을 많이 추가를 안했지만 조금만 조건이 까다로워지면 추측하기도 힘든 쿼리가 될 것입니다.

and() 메소드를 이용하여 조건을 추가하는 것을 볼 수가 있습니다.
QBoard는 Board의 속성을 이용해서 다양한 SQL에 필요한 구문을 처리할 수 있는 기능이 추가된 형태이므로, like(), get()등을 이용해서 원하는 SQL을 구성하는데 도움을 줍니다.

테스트 코드를 실행하면 다음과 같은 결과를 볼 수 있습니다.
스크린샷 2019-06-02 오전 12 34 55

리턴타입을 Page로 설정했기 때문에 데이터를 추출하는 SQL과 개수를 파악하는 SQL이 실행되고, 이때 필요한 조건들이 지정되는 것을 볼 수 있습니다.

안녕하세요. 이전 포스팅에서 스프링-Mybatis 모듈을 연동하는 간단한 실습을 진행했다면,이번엔 @Aspect를 이용한 스프링 AOP의 개념과 구동원리에 대해서 공부를 하였습니다.
웹 서비스를 운영하면서 다양한 핵심 비즈니스 로직이 존재하게 되는데 그때마다 고객이 해당 비즈니스 서비스를 호출할때마다 보안인증, 트랜잭션 처리, 로깅같은 꼭 필요하지만 중요하지 않는 공통기능의 코드를 작성할 필요성을 느끼게 됩니다. 그때마다 비즈니스 로직을 가진 메소드에 해당 공통기능의 코드를 넣게 된다면 가독성과 유지보수성 측면에서 좋지 않다고 생각하기 때문에 이런경우 스프릥 AOP를 이용하면 좋을거 같다 생각하여 리뷰하게 되었습니다.

스프링 AOP(Aspect Oriented Programming)

AOP는 관점지향 프로그래밍이라는으로 "기능을 핵심 비즈니스 기능과 공통기능으로 '구분'하고,모든 비즈니스 로직에 들어가는 공통기능의 코드를 개발자의 코드 밖에서 필요한 시점에 적용하는 프로그래밍 방식입니다. 이게 무슨 강아지같은 소리인지... 처음에는 이해가 안됬지만.. 역시 코딩은 해보는게 답인거 같아서... 그냥 한번 뭐라도 해보자는 마인드로 간단하게 구글링해본 결과 AOP에 대한 정말 퀄리티가 훌륭한 포스팅들이 넘쳐 흘렀습니다.
컨시브이가 주특기인 저한테는 그중에서 정말 따라하기 쉬운 간단한 코드를 실습하면서 AOP에 대한 기본적인 개념에 대해서 공부하였습니다.

AOP란?

  • 로깅, 예외, 트랜잭션 처리 같은 코드들은 횡단 관심(Crosscutting Concerns)
  • 핵심 비즈니스 로직은 핵심 관심(Core Concerns)

스크린샷 2019-05-25 오후 10 54 53

AOP는 핵심관심과 횡단 관심을 완벽하게 분리할 수 없는 OOP의 한계를 극복하도록 도와줍니다.

AOP 용어

AOP 소스예제를 살펴보기 전에 간단하게 용어정리를 해보았습니다.

조인포인트(Joinpoint): 클라이언트가 호출하는 모든 비즈니스 메소드, 조인포인트 중에서 포인트컷이 되기 때문에 포인트컷의 후보라고 할 수 있습니다.

포인트컷(Pointcut): 특정 조건에 의해 필터링 된 조인포인트, 수많은 조인포인트 중에 특정 메소드에서만 공통기능을 수행시키기 위해 사용됩니다.

어드바이스(Advice): 공통기능의 코드, 독립된 클래스의 메소드로 작성합니다.

위빙(Weaving): 포인트컷으로 지정한 핵심 비즈니스 로직을 가진 메소드가 호출될 때, 어드바이스에 해당하는 공통기능의 메소드가 삽입되는 과정을 의미합니다. 위빙을 통해서 공통기능과 핵심 기능을 가진 새로운 프록시를 생성하게 됩니다.

Aspect: 포인트컷과 어드바이스의 결합입니다. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정합니다.

아래 표는 어드바이스의 동작 시점입니다.

동작시점 설명
Before 메소드 실행 전에 동작
After 메소드 실행 후에 동작
After-returning 메소드가 정상적으로 실행된 후에 동작
After-throwing 예외가 발생한 후에 동작
Around 메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작

포인트컷 표현식

포인트컷을 이용하면 어드바이스 메소드가 적용될 비즈니스 메소드를 정확하게 필터링 할 수 있습니다.

지시자(PCD, AspectJ pointcut designators)의 종류

execution: 가장 정교한 포인트컷을 만들수 있고, 리턴타입 패키지경로 클래스명 메소드명(매개변수)
within: 타입패턴 내에 해당하는 모든 것들을 포인트컷으로 설정
bean: bean이름으로 포인트컷

표현식 설명
* 모든 리턴타입 허용
void 리턴타입이 void인 메소드 선택
!void 리턴타입이 void가 아닌 메소드 선택

패키지 지정

표현식 설명
com.jun.demo.controller 정확하게 com.jun.demo.controller 패키지만 선택
com.jun.demo.controller.. com.jun.demo.controller 패키지로 시작하는 모든 패키지 선택

클래스 지정

스크린샷 2019-10-03 오후 3 22 02

메소드 지정

스크린샷 2019-10-03 오후 3 22 12

매개변수 지정

스크린샷 2019-10-03 오후 3 22 23

JoinPoint 인터페이스

어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요합니다. 예를들면 예외가 발생하였는데, 예외발생한 메소드의 이름이 무엇인지 등을 기록할 필요가 있을 수 있습니다. 이럴때 JoinPoint 인터페이스가 제공하는 유용한 API들이 있습니다.

스크린샷 2019-10-03 오후 3 22 38

Sinature API

스크린샷 2019-10-03 오후 3 22 48

AOP 코드예제

간략하게 스프링 AOP 관련해서 용어정리를 하였습니다. 이제 스프링 AOP가 적용된 간단한 소스코드를 살펴보겠습니다.

해당코드는 간단하게 @RestController를 어노테이션을 적용한 controller에서 클라이언트로부터 요청이오면 해당요청에 매핑되는 메소드를 호출할때마다 얼마나 빠르게 응답하는지 확인하기 위해서 공통모듈로 로깅과 StopWatch 객체를 이용하여 메소드 리턴 시간을 측정하는 것을 구현해보았습니다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
public class LogAspect {

    private static Logger logger = LoggerFactory.getLogger(LogAspect.class);

    //ProceedingJoinPoint 클래스는 JoinPoint 인터페이스를 상속하는 인터페이서.. 인자는 스프링 컨테이너가 넘겨준다.
    @Around("execution(* com.jun.demo.controller.HelloController.*(..))") //포인트컷
    public Object logging(ProceedingJoinPoint pjp) throws Throwable{

        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        logger.info("start -" + pjp.getSignature().getDeclaringTypeName() + " / " + pjp.getSignature().getName());
        Object result = pjp.proceed();
        logger.info("finished -" + pjp.getSignature().getDeclaringTypeName() + " / " + pjp.getSignature().getName());

        stopWatch.stop();
        logger.info("Timer Stop - Elapsed time :" + stopWatch.getTotalTimeMillis());

        return result;
    }
}
@RestController
public class HelloController {

    @GetMapping("/sample")
    public SampleVO makeSample(){

        SampleVO vo = new SampleVO();
        vo.setVal1("v1");
        vo.setVal2("v2");
        vo.setVal3("v3");
        System.out.println(vo);

        return vo;
    }

}

결과화면

스크린샷 2019-05-25 오후 11 45 37

LogAspect는 AOP를 정의하는 클래스로 @Aspect, @Componet로 이클래스가 AOP가 바라보는 관점을 정의하고 스프링 컨테이너가 관리하는 bean으로 등록하는 것을 정의하였습니다.

@Around는 위의 AOP용어 설명처럼 어드바이스 동작 시점을 정의하였습니다.
저는 메소드 실행 전/후에 공통기능을 핵심 비즈니스 로직에 적용하였습니다.

 @Around("execution(* com.jun.demo.controller.HelloController.*(..))")

@Around에 표현식을 사용하였는데 지시자로 execution을 사용하여 정교한 포인트컷을 만들었습니다. 먼저 *은 리턴타입을 의미하는데 모든 리턴타입을 허용한다는 의미이고, 두번째로는 패키지를 지정, 세번째는 클래스 HelloController로 지정하였습니다.
그리고 포인트컷으로 지정할 메소드와 매개변수를 지정하였는데 *은 해당 클래스의 모든 메소드를 포인트컷으로 지정하고, (..)은 매개변수의 개수와 상관없이 모든 매개변수를 지정한다는 의미입니다.

공통기능을 정의한 메소드 logging에 매개변수로 ProceedingJoinPoint 객체는
타켓대상의 핵심 관심에 대한 정보를 제공하는 역할을 하고 있습니다. 그리고 JoinPoint 인터페이스를 상속하는 인터페이스로 스프링 컨테이너에서 제공하고 있습니다.
이때 Around 어드바이스만 다른 어드바이스와 약간 다른데, ProceedingJoinPoint 객체를 인자로 선언해야합니다. 그렇지 않으면 에러가 발생합니다.

위의 코드는 Object 객체를 리턴하도록 하였는데, 그 이유는 스프링 AOP 동작원리에 있습니다. 스프링 AOP는 Proxy(대행자)를 통해서 수행하게 됩니다.
즉 proceed()에서 정상적으로 메서드를 실행한 후 리턴 값을 주는데 가로채서 어떤 action 을 한 후에 기존 리턴 값을 되돌려 주지 않으면 가로챈 프록시가 결과 값을 변경하거나 지워버린것과 다름이 없습니다. 위의 코드는 단순하게 전/후로 시간을 측정하여 로깅을 찍어주고 기존 비즈니스 로직이 실행될 수 있게 pjp.proceed();를 호출하였습니다.

AOP 동작원리

프록시(Proxy)를 이용하여 AOP를 구현

스크린샷 2019-05-25 오후 11 21 28

프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑(Wrapping) 오브젝트입니다.
호출자(클라이언트)에서 타겟을 호출하게 되면 타겟이 아닌 타겟을 감싸고 있는 프록시가 호출되어, 타겟 메소드 실행전에 선처리, 타겟 메소드 실행 후, 후처리를 실행시키도록 구성되어있습니다.

스프링 AOP에서는 런타임시에 Weaving을 통해서 프록시 객체를 생성하게 됩니다.
생성방식으로는 첫번째로 JDK Dynamic Proxy가 있는데 타겟대상이 Interface를 구현하는 클래스면 인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되지 않는 메서드에 대해서는 AOP가 적용되지 않는 단점이 있습니다.

두번째로는 CGLIB가 있는데 타켓대상이 인터페이스를 구현하고 있지 않고 바로 클래스를 사용한다면, 스프링은 CGLIB를 이용하여 클래스에 대한 프록시 객체를 생성합니다. CBLIB는 대상 클래스를 상속받아 구현합니다. 따라서 클래스가 final인 경우에는 프록시를 생성할 수 없습니다.

좀더 구체적으로 설명하기에는 아직 공부를 못했기 때문에 이 부분에 대해서는 다음에 더 준비해서 포스팅하겠습니다.

프록시 객체를 자세하게 까보지 않아서 구체적인 동작원리는 모르겠지만 맴버로 타켓객체와 Aspect로 정의된 공통모듈을 가지는 객체를 가지고 았지 않을까 뇌피셜로 생각만 해봤습니다. 클라이언트의 요청이 오면 포인트컷과 어드바이스가 결합하는 Weaving과정에서 새로운 Proxy객체가 생성되면서 공통기능과 타켓의 핵심 비즈니스로직을 수행하지 않을까 생각도 해봤습니다.

프록시를 이용해서 보조업무를 처리하는 예제 포스팅을 보게 되면서 조금이나마 프록시에 대해서 이해하게 되었고, AOP 개념이 스프링에 한정되지 않는것을 알게 되었습니다.

결론은.. AOP의 장점은 이렇습니다.

  • 단순 복사 붙여넣기 -> 핵심 비즈니스 로직에 공통기능의 코드 중복이 많아져 코드분석과 유지보수를 어렵게 만듭니다.
  • AOP를 통해 부가적인 공통코드를 효율적으로 관리합니다.

참조: https://ooz.co.kr/201
참조: https://jeong-pro.tistory.com/171

안녕하세요. 

웹개발자를 준비하고 있는 주니어 개발자 임준영이라고 합니다.

스프링부트랑 ORM기술에 관심이 많아서 개발공부를 열심히 하고 있습니다.

hexo를 쓰다가 티스토리로 넘어오게 되었는데 앞으로 글 꾸준히 올리겠습니다.

 

'잡다한 것' 카테고리의 다른 글

Git- 실전 가이드  (0) 2019.11.29
IntelliJ 자주 사용하는 단축키 정리  (0) 2019.11.24
Dev Festival을 다녀와서...  (0) 2019.11.17

+ Recent posts