본문 바로가기

Study/개발일지

[백엔드스터디WIL]9주차 학습일지

- 학습한 내용 

 - @Transactional 

- @MappedSuperClass

- queryDsl 개념

 

@Transactional

1. 트랜잭션의 성질 (ACID)

  • 원자성 : 한 트랜잭션 내에서 실행한 작업들은 하나로 간주한다. 즉, 모두 성공 또는 모두 실패
  • 일관성 : 트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
  • 격리성 : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
  • 지속성 : 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다.



2. 스프링에서 트랜잭션 처리 방법

스프링에서 트랜잭션 처리를 지원하는데 그 중 어노테이션 방식으로 @Transactional을 선언하여 사용하는 방법이 일반적이며, 선언적 트랜잭션이라 부른다.

클래스, 메서드 위에 @Transactional이 추가되면, 이 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성된다.

이 프록시 객체는 @Transactional이 포함된 메소드가 호출될 경우, PlatformTransaction을 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 한다.



3. @Transactional의 작동 원리와 흐름

@Transactional이 클래스 내지 메서드에 붙을 때, Spring은 해당 메서드에 대한 프록시를 만든다. 프록시 패턴은 디자인 패턴 중 하나로, 어떤 코드를 감싸면서 추가적인 연산을 수행하도록 강제하는 방법이다.

트랜잭션의 경우, 트랜잭션의 시작과 연산 종료 시의 커밋 과정이 필요하므로, 프록시를 생성해 메서드의 앞뒤에 트랜잭션의 시작과 끝을 추가하는 것이다. 이러한 로직은 AOP에 바탕을 두고 설계되었다.

또한, 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 서비스 클래스에서 @Transactional을 사용할 경우, 해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생긴다는 뜻이다. 영속성 컨텍스트는 트랜잭션 AOP가 트랜잭션을 시작할 때 생겨나고, 메서드가 종료되어 트랜잭션 AOP가 트랜잭션을 커밋할 경우 영속성 컨텍스트가 flush되면서 해당 내용이 반영된다. 이후 영속성 컨텍스트 역시 종료되는 것이다.

이러한 방식으로 영속성 컨텍스트를 관리해 주기 때문에, @Transactional을 쓸 경우 트랜잭션의 원칙을 정확히 지킬 수 있다.

유의할 원칙

  • 만약 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도, 이는 같은 영속성 컨텍스트를 사용한다.
  • 같은 EntityManager를 쓰더라도, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.


4. @Transactional과 지연로딩

조회한 엔티티가 Service/Repository 계층에서는 영속성 컨텍스트에서 관리되며 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다!

준영속 상태는 엔티티가 영속성 컨텍스트에서 분리된 것을 말한다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. 그 중 하나가 지연로딩이다.

프록시를 사용해서 조회할 경우, 해당 객체에 접근 시 조회 요청을 보내야 한는 것이 지연로딩이다. 영속성 컨텍스트가 이미 종료된 상태라, 엔티티와 DB를 이어줄 매개가 없기 때문에 준영속 상태에서 엔티티는 이 기능을 쓰지 못한다.

ex)

@Entity
public class Book {
	@Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 전략
    private Author author;
    ...
}

작가와 책이 서로 연관 관계를 맺고 있으며, 지연 로딩을 쓴다.

class BookController {
    public String view(Long bookId) {
    	Book book = bookService.findBook(bookId);
        Author author = book.getAuthor();
        author.getName(); //여기서 예외 발생!!!
        ...
    }
}

findBook 메서드가 종료되면서 영속성 컨텍스트가 닫혔고, 반환된 book 엔티티가 준영속 상태가 된 것이다.
author는 지연로딩 전략을 사용했으므로 비어있는 프록시 객체로 존재했는데, 해당 객체에서 실제로 값을 뽑아 쓰려고 하니 예외가 발생한다.


5. 해결책

  1. 글로벌 페치 전략 수정
  2. JPQL 페치 조인
  3. 강제 초기화
  4. FACADE 계층 추가
  5. DTO 사용







내가 접한 문제에서는 값을 가져오고 나서 또 지연로딩 부분을 찾으려니 준영속성 컨텍스트로 넘어가 문제가 생겼던 것이었다. 그리고 @Transacational을 사용하면 영속성 컨텍스트가 끝나지 않아 해당 칼럼을 조회시에도 문제가 발생하지 않았던 것이었다.

 

 

 

@MappedSuperclass를 사용하면 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공한다.

비유를 하자만 추상 클래스와 비슷한데 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과 매핑되지 않는다. 단순히 매핑 정보를 상속할 목적으로만 사용된다.

이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.

객체 상속 관계

@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue
    @Column(name = "ID")
    private long id;
    @Column(name = "NAME")
    private String name;
}

@Entity
public class Member extends BaseEntity {
    @Column(name = "EMAIL")
    private String email;
}

@Entity
public class Seller extends BaseEntity {
    @Column(name = "SHOPNAME")
    private String shopName;
}

 

위에 코드는 BaseEntity의 id, name 두 공통 속성을 부모클래스로 모으로 객체 상속 관계로 만들었다.

자식 엔티티에서 공통으로 사용되는 매핑 정보만 제공하면 된다.

부모 엔티티로부터 물려받은 매핑 정보를 재정의 하려면 @AttributeOverrides, @AttributeOverride를 사용하고,

연관 관계를 재 정의하려면 @AssociationOverrides, @AssociationOverride를 사용한다.

@Entity
@AttributeOverrides({
        @AttributeOverride(name = "ID", column = @Column(name = "MEMBER_ID")),
        @AttributeOverride(name = "NAME", column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity {
    @Column(name = "EMAIL")
    private String email;
}

 

간단히 정리하자면 @MappedSuperclass는 테이블과 관련이 없고 단순히 공통으로 사용하는 매핑 정보만 제공한다.

ORM에서 이야기하는 진정한 상속 매핑은 객체 상속을 데이터베이스의 슈퍼타입 서브타입 관계와 매핑하는 것이다.

 

 

 

QueryDSL 이란?

정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있게 지원하는 프레임워크

문자열이나 XML파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 Fluent API를 활용해 쿼리 작성 가능

장점

  • IDE에서 제공하는 코드 자동완성 기능 사용 가능
  • 문법적으로 잘못된 쿼리 허용 X
  • 고정된 SQL쿼리를 작성하지 않기에 동적으로 쿼리 생성 가능
  • 코드를 작성하므로 가독성 + 생산성 향상
  • 도메인타입과 프로퍼티를 안전하게 참조 가능

QueryDSL을 사용하기 위한 프로젝트 설정

의존성 추가

<dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <scope>provided</scope>
</dependency>
<dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
</dependency>

< plugins > 태그에 APT 플러그인 추가

<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>
                  <options>
                      <querydsl.entityAccessors>true</querydsl.entityAccessors>
                  </options>
              </configuration>
           </execution>
        </executions>
</plugin>

JPAAnnotationProcessor은 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입 생성

이후 maven lifecycle -> compile 단계 클릭해 빌드 수행

그러면 Q 도메인 클래스가 생성된 것을 볼 수 있다

QueryDSL은 지금까지 작성했었던 엔티티 클래스와 QDomain이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용
-> 이를 통해 SQL과 같은 쿼리를 생성하여 제공

generated-sources를 Mark as Sources로 하여 IDE에서 소스 파일로 인식할 수 있게 해주어야 함


기본적인 QueryDSL 사용

	@PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest(){
        JPAQuery<Product> query = new JPAQuery(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = query
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for(Product product : productList){
            System.out.println("-----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price :" + product.getPrice());
            System.out.println("Product Stock :" + product.getStock());
            System.out.println();
            System.out.println("-------------------");
        }
    }

위 코드는 QueryDSL에 의해 생성된 Q도메인 클래스를 활용하는 코드

Q도메인 클래스와 대응되는 테스트 클래스가 없으므로 엔티티 클래스에 대응되는 리포지토리의 테스트 클래스에 포함해도 무관

QueryDSL을 사용하기 위해선 JPAQuery 객체를 사용
-> EntityManager를 활용하여 생성

생성된 JPAQuery는 9~13번 줄 같이 빌더 형식으로 쿼리 작성

List로 반환 받기 위해서는 fetch() 메서드를 사용해야 함

반환 메서드 종류

  • List< T > fetch() : 조회 결과를 리스트로 반환
  • T fetchOne : 단 한건의 조회 결과 반환
  • T fetchFirst() : 여러 건의 조회 결과중 1건을 반환
  • Long fetchCount() : 조회 결과의 개수를 반환
  • QueryResult< T > fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults 반환

QueryDSL을 사용하기 위해 JPAQueryFactory를 사용하는 경우도 있음

JPAQueryFactory를 활용한 QueryDSL 테스트 코드

@Test
void queryDslTest2(){
    JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
     QProduct qProduct = QProduct.product;
        
      List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        
     for(Product product : productList){
            System.out.println("-----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price :" + product.getPrice());
            System.out.println("Product Stock :" + product.getStock());
            System.out.println();
            System.out.println("-------------------");
        }
    }

JPAQuery를 사용했을 때와 달리 JPAQueryFactory에서는 select 절부터 가능

일부만 조회하고 싶다면 selectFrom()이 아닌 select()와 From() 메서드 구분해서 사용하자

@Test
    void queryDslTest3(){
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        
        for(String product : productList){
            System.out.println("-------------------");
            System.out.println("Product Name : " + product);
            System.out.println("------------------");
        }
        
        List<Tuple> tupleList = jpaQueryFactory
                .select(qProduct.name, qProduct.price)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        
        for(Tuple product : tupleList){
            System.out.println("----------");
            System.out.println("Product Name :" + product.get(qProduct.name));
            System.out.println("Product Name :" + product.get(qProduct.price));
            System.out.println("----------");
        }
    }

productList는 select 대상이 하나인 경우

TupleList처럼 select 대상이 여러개인 경우 쉼표로 구분해서 작성하면 되고, 리턴 타입을 List< tuple > 로 작성


실제 비즈니스 로직에 QueryDSL 활용

컨피그 클래스 생성

QueryDSLConfig.java

@Configuration
public class QueryDSLConfiguration {
    
    @PersistenceContext
    EntityManager entityManager;
    
    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

위와 같이 JPAQueryFactory 객체를 @Bean 객체로 등록시 앞선 방법처럼 JPAQueryFactory를 초기화 하지 않고 스프링 컨테이너에서 가져다 쓸 수 있음

ProductRepositoryTest

	@Autowired
    JPAQueryFactory jpaQueryFactory;
    
    @Test
    void queryDslTest4(){
        QProduct qProduct = QProduct.product;
        
        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        
        for(String product : productList){
            System.out.println("-----------------");
            System.out.println("Product Name : " + product);
            System.out.println("-----------------");
        }
    }
Hibernate: 
    select
        product0_.name as col_0_0_ 
    from
        product product0_ 
    where
        product0_.name=? 
    order by
        product0_.price asc
-----------------
Product Name : 펜
-----------------
-----------------
Product Name : 펜
-----------------
-----------------
Product Name : 펜
-----------------
728x90