|  스프링 부트 Test 중, 아래와 같이 오류가 나타났다.

C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java:33: warning: @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final.
    private List<ProductItem> productItems = new ArrayList();
                              ^
Note: C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

- 테스트 코드 

@Test
void addProduct() {

    // given
    Long sellerId = 1L;

    AddProductForm form = makeProductForm("나이키 에어포스", "신발", 3);

    // when
    Product p = productService.addProduct(sellerId, form);

    // then
    Product result = productRepository.findById(p.getId()).get();

    Assertions.assertNotNull(result);
    Assertions.assertEquals(result.getSellerId(), 1L);
    Assertions.assertEquals(result.getName(), "나이키 에어포스");
    Assertions.assertEquals(result.getDescription(), "신발");
    Assertions.assertEquals(result.getProductItems().get(0).getName(), "나이키 에어포스0");
    Assertions.assertEquals(result.getProductItems().get(0).getPrice(), 10000);

}

private static AddProductForm makeProductForm(String name, String description, int itemCount) {
    List<AddProductItemForm> addProductItemForms = new ArrayList<>();
    for (int i = 0; i < itemCount; i++) {
        addProductItemForms.add(makeProductItemForm(null, name + i));
    }
    return AddProductForm.builder()
            .name(name)
            .description(description)
            .addProductItemForms(addProductItemForms)
            .build();
}

> 원인 :

- @OneToMany의 default fetch type이 LazyLoading이기 때문에, Proxy로 id 값만 담은 ProductItem들을 가져오고, 실제 내용은 들어있지 않았기 때문이다.

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited  // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long sellerId;

    private String name;
    private String description;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductItem> productItems = new ArrayList();


    public static Product of(Long sellerId, AddProductForm form) {
        return Product.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .description(form.getDescription())
                .productItems(form.getAddProductItemForms().stream()
                        .map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
                ).build();
    }
}

> 해결 :

이를 해결하기 위해서는 두가지 방법을 사용할 수 있는데

(1) fetch type을 EAGER로 변경한다. --> 이 방법은 그러나 불필요하게 매번 DB 조회 시 모든 데이터를 한 번에 가져오게 함으로 좋지 않다.

(2) JPA의 @EntityGraph와 findWith 함수를 통해 속성을 지정(ex. productItems)할 때, fetch type을 변경시킨다.

EntityGraphType.LOAD attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 default fetch type으로
EntityGraphType.FETCH attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 LAZY로
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    @EntityGraph(attributePaths = {"productItems"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<Product> findWithProductItemsById(Long id);

}

 

[ 출처 ]

부트캠프 수업 내용 정리

|  CascadeType

     
ALL - 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 모두 전파
PERSIST - 상위 엔터티에서 저장을 하면 하위 엔터티도 저장 (영속성 전파) x.persist() 전파
MERGE - 하위 엔터티까지 병합 작업을 지속 ...(?) x.merge() 전파
REMOVE - 하위 엔터티까지 제거 작업을 지속 x.remove() 전파
REFRESH - 하위 엔터티까지 인스턴스 값 새로 고침 (다시 조회) x.refresh() 전파
DETACH - 하위 엔터티까지 엔터티 제거 x.detach() 전파

* persist() 는 리턴값이 없는 insert, merge() 는 리턴값이 없는 update

@Transactional
    public <S extends T> S save(S entity) {
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

* save() 는 리턴값이 있는 insert, update이다.

 

|  예시 코드

- Product (상품) 하위에 옵션으로 들어가는 Item들이 있다고 할 때,

- Product의 List<ProductItem> productItems에 @OneToMany(cascade = CascadeType.ALL)을 달아주어, 상품에 대한 CRUD가 이루어질 때 하위 엔터티인 productItems의 ProductItem까지 영향을 받는다. 

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited  // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long sellerId;

    private String name;
    private String description;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductItem> productItems = new ArrayList();


    public static Product of(Long sellerId, AddProductForm form) {
        return Product.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .description(form.getDescription())
                .productItems(form.getAddProductItemForms().stream()
                        .map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
                ).build();
    }
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited
public class ProductItem extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long sellerId;

    @Audited
    private String name;

    @Audited
    private Integer price;

    private Integer count;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private Product product;

    public static ProductItem of(Long sellerId, AddProductItemForm form) {
        return ProductItem.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .price(form.getPrice())
                .count(form.getCount())
                .build();
    }
}

>> 이슈! : 하위에서 삭제시, 상위로 CasecadeType.ALL이 전파되었다. (전부 다 삭제됨)

>>> 수업에서 해결할 때에는, 하위의 CascadeType.ALL을 지웠더니 해결되었다.

 

[ 출처 및 참조 ]

부트캠프 수업 내용 정리

https://data-make.tistory.com/668

 

[JPA] Spring JPA CascadeType 종류

JPA Cascade Types Spring JPA CascadeType 종류 javax.persistence.CascadeType JPA Cascade Type ALL PERSIST MERGE REMOVE REFRESH DETACH CascadeType.ALL 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 @Entity public class Person { @Id @Gen

data-make.tistory.com

https://gimmesome.tistory.com/207

 

[JPA] save와 persist차이 (save, persist, merge개념)

persist()는 리턴값이 없는 insert다. merge()는 리턴값이 없는 update다. save()는 리턴값이 있는 insert, update다. save 메소드를 호출하면.... entityInformation에서 새로운 entity이면 persist()를 그게 아니면 merge()

gimmesome.tistory.com

https://umanking.github.io/2019/04/12/jpa-persist-merge/

|  관련 용어

용어 What? How?
Lazy Loading 사용자가 보지 않는 리소스는 차후에 로딩하는 기술 - 프론트 : 무한 스크롤, placeholder 등
- 백엔드 : JPA의 지연 로딩

 

|  JPA의 Lazy Loading 

- 개발자는 JPA를 통해 프록시를 만들 수 있다.

- 프록시란, 가짜 객체를 말하는 것으로, 실제 엔터티에 대한 참조값을 가진다.

- 하나의 엔터티가 다른 엔터티와 연관관계를 맺고 있을 때 (oneToMany) , 

  연관된 객체들을 처음부터 DB에서 조회하지 않고, 실제 사용하는 시점에 DB에서 조회하면 속도를 향상시킬 수 있다.

- JPA에서는 프록시가 참조하는 객체들의 데이터 조회 시점을 정하는 타입을 Fetch Type이라 하는데, 

즉시로딩 - 한 객체를 조회할 때, 참조 객체들까지 전부 읽어온다.(EAGER),
지연로딩 - 한 객체를 조회할 때, 참조 객체들은 무시하고 해당 객체의 엔터티 데이터만 가져온다.(LAZY)

- 그 타입 중에서도 지연로딩(LAZY)이 바로 Lazy Loading이다.

 

|  General한 Lazy Loading 용어 의미

- Lazy Loading은 JPA 뿐만 아니라, 프론트에서도 사용하는 용어이다. 

- 프론트에서는 이미지나 동영상 등의 리소스들을 전부 다 올리지 않고 사용자가 필요로 할 때에만 로딩하기도 하는데,

- 이 또한 페이지의 로딩이 너무 느려지는 현상을 방지하기 위해서이다.

- 결국 정리하자면, 지연 로딩은 사용자가 당장 쓰지 않는 불필요한 데이터의 로딩을 지연시키겠다는 의미이다.

 

 

[ 참고 및 출처 ]

Lazy Loading,

https://scarlett-dev.gitbook.io/all/it/undefined-1

https://programmer-chocho.tistory.com/81

https://victorydntmd.tistory.com/210

https://velog.io/@bread_dd/JPA%EB%8A%94-%EC%99%9C-%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

|  관련 SQL 개념

(1) Unique Key : 중복 데이터 저장을 방지하기 위한 제약조건

- 단일 칼럼 뿐 아니라, 복합 칼럼을 지정할 수도 있다.

- 아래와 같이 복합 키를 생성할 경우, DB 조회 속도를 향상시킬 수 있다. 

-- MySQL 기준
create table member(
    email varchar(50),
    name  varchar(255),
    unique key member_uk_email (email, name)
)

- 중복 데이터 저장을 막기 위한 방법으로 아래 두가지를 사용할 수 있다.

  * 단순 insert into 문의 경우 에러를 발생하지만, 아래 두 가지는 에러를 발생시키지 않는다.

INSERT IGNORE unique key가 걸린 칼럼에 중복 데이터가 이미 있는 경우 삽입 X
INSERT ~~~ ON DUPLICATE KEY UPDATE unique key가 걸린 칼럼에 중복 데이터가 이미 있는 경우 업데이트
insert ignore into member values ('jamie@gmail.com','JAMIE');

-- 0 raw affected (error x)
insert ignore into member values ('jamie@gmail.com','JAMIE');
insert ignore into member values ('jamie@gmail.com','JAMIE');
insert ignore into member values 
   on duplicate key update ('jamie@gmail.com','JAMIE');

-- 2 raw affected (error x)
insert ignore into member values 
   on duplicate key update ('jamie@gmail.com','JAMIE');
   
insert ignore into member values 
   on duplicate key update ('jamie@gmail.com','JAMIE');

(2) Index : 추가적인 쓰기 작업과 저장공간을 활용해 DB table의 검색 속도를 향상시키기 위한 자료구조

- Index는 다양한 자료구조를 통해 만들 수 있다.(ex. Hashtable)

- 가장 많이 사용되는 자료구조는, B+Tree 구조이다.

  * B+Tree 구조란, DB인덱스를 위해 자식노드가 2개 이상인 B-Tree를 개선시킨 자료구조를 말한다.

  * 일반적으로 조회시 O(logN)의 시간을 가진다.

- Index는 아래처럼 두 가지로 종류를 나눌 수 있다.

cluster index primary key를 통해 인덱스를 설정 (ex. 처음부터 정렬된 영어사전)
보조 index unique key를 통해 인덱스를 설정 (ex. 책 뒤의 찾아보기)

  * 참고 ) 

   cluster와 보조index를 같이 쓸 경우 INDEX로 입력할 수 있지만, 보조 index만 쓸 경우 UNIQUE INDEX를 써야한다.

  * 자세한 내용 : https://enterkey.tistory.com/417

- Index는 아래와 같이 테이블을 생성할 때 만들 수도 있고, 별도로 alter문으로 제약조건을 붙일 수도 있다.

create table member(
    id    bigint  primary key,  -- cluster key 
    email varchar(50),
    name  varchar(255),
    
    unique key member_uk_email (email, name),
    INDEX  member_idx (id, email)
)

- Index의 장단점

장점 - Index를 사용하면 조회 속도를 향상시킬 수 있어, DB 서버의 부하를 줄일 수 있다.
단점 - 하지만 추가 저장공간을 사용해야하고,
- insert, update, delete 같은 데이터 변경 쿼리가 자주 사용되는 경우에 인덱스를 쓰면
   paging이 빈번해져 성능이 악화될 수 있다.(== 조회 보다 db 변동이 많은 경우 불리)
- 추가적으로 cardinality가 낮은 경우(= 중복 데이터가 많은 경우), 인덱스를 사용하는 것이 비효율적일 수 있다.

   * Selectivility : 데이터 집합에서 특정 값을 얼마나 잘 골라낼 수 있는지에 대한 지표

      >> Selectivility = Cardinality / Total number of Records

      >> Selectivility = 1 <-- 모든 레코드가 유니크하다.

   * cardinality : 특정 데이터 집합의 유니크(Unique)한 레코드의 개수 

- Index를 설계할 때 알아둘 점

[효율적인 인덱스 설계 ]
- Where절에 사용되는 열
- Select절에 자주 등장하는 칼럼을 잘 조합해 Index로 만들면 조회 시간을 줄일 수 있다.
- JOIN절에 자주 사용되는 열
- Order by 절에 사용되는 열은 클러스터 인덱스가 유리하다.

[금지해야할 인덱스 설계]
- 대용량 데이터가 자주 입력되는 경우 primary보다 unique를 설정한다.
- 데이터 중복도가 높은 열은 인덱스 효과가 없다. (ex. 성별)
- 자주 사용되지 않으면 성능 저하를 초래할 수 있다.

 

|  스프링에서 중복 데이터 저장을 방지하기

1. Primary key 설정하기

-  사용할 칼럼에 @Id 를 넣어준다.
- @GeneratedValue 를 통해 auto_increment가 가능하게 한다.

  * auto : 자동선택, identity : db identity 칼럼 사용, sequence : 시퀀스를 쓰는 db vendor에서 사용

2. Unique Key & Index 설정

- @Entity 클래스에서, @Table(uniqueConstraints = {}) 를 통해 unique 제약조건을 설정한다.

- Unique key를 아래와 같이 설정하게 되면 해당 키를 곧 인덱스로 인식한다.

@Entity(name = "DIVIDEND")
@NoArgsConstructor
@Getter
@ToString
@Table( // 복합 unique키 설정 (중복 저장 X)
        uniqueConstraints = {
                @UniqueConstraint(
                        columnNames = {"companyId", "date"}
                )
        }
)
public class DividendEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long companyId;

    private LocalDateTime date;

    private String dividend;

    public DividendEntity(Long companyId, Dividend dividend) {
        this.companyId = companyId;
        this.date = dividend.getDate();
        this.dividend = dividend.getDividend();
    }
}

 

[ 참고 및 출처]

* 부트캠프 수업 내용 정리

* 인덱스 개념 참조 : https://mangkyu.tistory.com/96

* Cardinality와 Selectivility : https://soft.plusblog.co.kr/87

* 인덱스 핵심 설계 문법 : https://inpa.tistory.com/entry/MYSQL-%F0%9F%93%9A-%EC%9D%B8%EB%8D%B1%EC%8A%A4index-%ED%95%B5%EC%8B%AC-%EC%84%A4%EA%B3%84-%EC%82%AC%EC%9A%A9-%EB%AC%B8%EB%B2%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

* 유니크 키를 통한 중복데이터 관리방법 https://umanking.github.io/2021/07/05/mysql-duplicate-record/

|  개요

- 서비스에서 @Transactional 을 사용할 때와 사용하지 않을 때 수정과 삭제 시 차이를 보이는 걸 경험했다.

@Transactional update delete
사용 repository.save() 불필요 repository.delete() 시 삭제됨
미사용 repository.save() 필요 repository.delete() 시 삭제되지 않음

- 더불어 테스트에서 아래와 같이 @Transactional을 작성할 때, DB에 영구반영이 되지 않는 걸 확인했었다.

// 아래는 TEST 코드
@SpringBootTest
@Transactional
public class JpaMemoRepositoryTest{

    // 테스트 작성
    
}

- 왜 그럴까? 

(1) 수정 시 save하지 않아도 수정되는 이유

: @Transactional이 붙여진 메소드는 spring boot에서 트랜잭션으로 인식된다.  해당 메소드에서 DB의 row를 조회한 후, setter로 row에 변화를 준다고 하자. Hibernate는 persistent entities의 변동사항을 자동적으로 확인하고, 그에 따라 DB에 업데이트를 해준다. 

// default : dbms의 격리성 수준을 따르며, 부모와 별개로 실행되며, 읽기전용이 아님
@Transactional(isolation = Isolation.DEFAULT,
               progagation = Propagation.REQUIRED,
               readOnly = false){
    
}

(2) 삭제 시 @Transactional을 쓰지 않으면 삭제되지 않는 이유

: 이 부분은 정확하게는 모르겠지만, JPA의 PersistenceContext와 연관되어 있다고 한다. @Transactional을 명시하지 않을 경우, 해당 메소드는 트랜잭션으로 인식되지 않는다. 이 부분은 좀 더 알아보아야 할 것 같은데 save와 달리 delete는 삭제되지 않는다는 점만 일단 기록해두려 한다.

(3) 테스트에서 @Transactional을 쓸 경우 

: 스프링부트의 test로 지정된 패키지 내부의 클래스에 @Transactional을 쓰게 되면 모든 트랜잭션이 롤백되도록 되어 있다.

  따라서, 테스트에서 @Transactional을 쓸 경우 DB에 변동사항이 반영되지 않는다. 그럼에도 쓰는 이유는 서비스 로직을 파악하기 간편하기 때문. 검색해보면 일부 일부 사람들은 테스트에서는 해당 어노테이션을 쓰지 말 걸 권장하기도 하는데, 케바케인 것 같다.

(4) @Transactional와 연관해서 지연로딩과 즉시로딩에 대한 오류에 대해서도 알아볼 필요가 있다.

Lazy Loading 가능한 객체의 초기화를 지연시킨다. (참조하는 엔터티를 당장 쓰지 않을 경우)
Eager 객체의 초기화를 지금 당장 한다. (참조하는 엔터티를 지금 쓰는 경우)

-  A엔터티 내부에 @OneToMany 라는 Annotation으로 참조하는 B엔터티 데이터가 있다고 할 때, @OneToMany의 fetch type은 디폴트로 Lazy로 되어있다. 

- 이는 지연 로딩을 의미하는데, 지연 로딩의 경우, 참조된 B엔터티의 데이터들을 모두 읽지 않고 Proxy 객체(id값만 있음)를 통해 가져온다. 이 경우, 아래와 같이 코드를 작성하게 되면 오류를 발생시킨다. 

@RestController
class Controller{

    @GetMapping
    void findOneUserAndPetInfo(long userId){
    
    	User user = userService.findByUserId(userId);       
        List<Pet> pets = user.getPet(); // error!!
                
    }
    
}

- 이를 해결하는 방법은 서비스 메소드에 @Transactional을 붙이는 것이다. 그렇게 하면, Lazy Loading을 하더라도, 해당 메소드를 트랜잭션으로 인식해 필요한 데이터를 적시에 초기화한다. 

pf. https://zzang9ha.tistory.com/347

 

트랜잭션 개념

트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위

[출처] 코딩팩토리, https://wonit.tistory.com/462

- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.

(1) 트랜잭션의 연산 : Commit과 Rollback

- 트랜잭션은 순차적으로 Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.

- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.

(2) 트랜잭션의 상태

- 트랜잭션은 실행중(Active)이거나

  - 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)

  - 실행중 오류 발생으로 실패(Failed), 비정상적 종료로 rollback 수행(Aborted)

- 하는 총 5가지의 상태를 가질 수 있다.

(3) 트랜잭션의 특징

- 트랜잭션에는 4가지의 특징이 존재하는데,

ACID 내용
Atomic (원자성) All or Nothing : 모든 작업이 실행되거나 or 모두 실행되지 않아야 한다.

rf. 결제서비스에 관한 트랜잭션 중 갑자기 오류발생으로 끊긴다면? 
    ㄴ 결제서비스에 관한 트랜잭션 전체는 실행되지 않게 된다. 
Consistency (일관성) 트랜잭션 작업 결과는 항상 일관적이어야 한다.

* 모든 트랜잭션이 종료된 후엔 모든 DB의 제약조건을 지키고 있는 상태여야 한다.

rf. 웹툰 결제를 할 때 최소 결제 단위는 100원이어야 한다면, 
    결제 서비스 트랜잭션이 실행될 때 내 계정엔 100원 이상이 남아있어야 한다.
Isolation (격리성/독립성) 트랜잭션은 다른 트랜잭션과 독립적이어야 한다.

rf. 현실적으로 트랜잭션의 격리성을 지키기가 어렵다 (성능과 안정성의 trade-off)
- 격리성의 단계 ) 
  READ_UNCOMMITED > READ_COMMITED > REPEATABLE_READ > SERIALIZABLE
- 대체로 현장에서는 REPEATABLE_READ 를 한다고 한다.
Durability (지속성) 트랜잭션이 완료되면 영구적으로 결과에 반영되어야 한다.

* 트랜잭션은 순차적으로 commit되며, commit이 실패해도 모든 로그를 남겨 db에 반영된다.

 

|  트랜잭션을 사용해보기

1. @EnableTransactionManagement 달기

- 일일히 @Transactional 메소드에 붙이지 않고, @SpringBootApplication이 있는 클래스에 붙인다.

@SpringBootApplication
@EnableTransactionManagement
class XXXX{

}

2. 서비스 클래스 또는 각각의 메소드에 @Transactional 달기

 

|  여러 트랜잭션이 경쟁하면 생길 수 있는 문제

1. Diry Read

: 어떤 트랜잭션이 Row를 수정하는 중에 다른 트랜잭션이 해당 Row를 읽는 경우

* 계좌 정보를 읽고 잔액을 수정하는 중에 만약 다른 트랜잭션이 진입한다면?

작업1   작업2  
게시글 1번 조회 A    
게시글 1번 수정중 B 게시글 1번 조회 A
게시글 1번 반영 B 게시글 1번 수정중 C
    게시글 1번 반영 C

2. Non-repeatable Read

: 시간 간격 두고 특정 Row를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번 조회 A    
    게시글 1번 조회 A
    게시글 1번 수정중 C
게시글 1번 조회 C 게시글 1번 반영 C

3. Phantom-read

: 시간 간격을 두고 특정 범위를 조회한 결과가, 다른 트랜잭션의 개입으로 달라지는 것 * 일관성을 위배

작업1   작업2  
게시글 1번~4번 조회 A,B,C,D    
    게시글 1번 조회 A
    게시글 1번 수정중 E
게시글 1번~4번 조회 E,B,C,D 게시글 1번 반영 E

 

|  스프링에서 트랜잭션 처리를 지원하는 방식

- 여러가지 방식이 있으나, 가장 자주 사용하는 방식은 @Transactional이다.

- 이를 선언적 트랜잭션 방식이라고도 부른다.

(1) 서비스 메소드에 @Transactional 을 넣어 해당 작업이 트랜잭션임을 명시

(2) 트랜잭션 기능이 적용된 프록시 객체 생성

(3) PlatformTransaction manager가 트랜잭션 전체가 성공했는지 확인하고 Commit 또는 Rollback

- 스프링 트랜잭션의 세부 설정들

* 모든 트랜잭션을 동기화처리(synchronized) 하게 되면 속도가 너무 느려진다.

* 적절한 설정을 통해 속도와 안전성 두 마리 토끼를 잡자

pf. 전파 수준이 아직 이해가 잘 가지 않는다.. 관련 자료를 차후에 더 보고 정리하자.

https://n1tjrgns.tistory.com/266

https://www.baeldung.com/spring-transactional-propagation-isolation

세부 설정 내용
Isolation(격리수준) 트랜잭션에서 일관성이 없는 데이터를 허용하는 수준

- DEFAULT : DATABASE의 기본 격리성 레벨 
- READ_UNCOMMITED : 트랜잭션 commit 전 다른 트랜잭션 조회 가능 (Dirty Read 발생)
- READ_COMMITED      : 트랜잭션 commit 후 다른 트랜잭션 조회 가능 (Dirty Read 방지)
- REPEATABLE_READ  : 트랜잭션이 조회중인 특정 Row에 대해 shared lock을 걸어, 다른 트랜잭션이 해당 Row을 조회하지 못하도록 하는 것 (Non-repeatable Read 방지)
- SERIALIZABLE            : 트랜잭션이 사용하는 모든 데이터에 대해 shared lock을 걸어, 다른 트랜잭션의 개입을 완전 봉쇄하는 것 (Phantom read 방지)

// 현장에선 REPEATABLE_READ을 주로 사용한다고 한다.

@Transactional(isolation = Isolation.REPEATABLE_READ)
Propagation(전파수준) 트랜잭션 동작 중 다른 트랜잭션을 호출하는 상황에서, 
트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값

* 트랜잭션 A 안에 트랜잭션 B를 호출하는 경우,
  트랜잭션 A는 부모 트랜잭션 , B는 자식 트랜잭션이 된다.

* 참여한다 = 부모 트랜잭션의 흐름 안에 포함되어 동작한다.


- REQUIRED             
  : 디폴트 속성. 
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션 뒤에 붙는다.
  : 새로운 트랜잭션을 생성한다.
  : 예외가 발생하면 롤백되며 호출한 곳에도 롤백이 전파된다.
   
- SUPPORT
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션을 계속 진행한다.
  : 실행 중인 트랜잭션(부모)이 없으면, 트랜잭션이 아닌 일반 로직으로써 동작한다.

- REQUIRES_NEW
  : 실행 중인 트랜잭션(부모)이 있으면, 해당 트랜잭션은 대기하고, 새 트랜잭션을 만든다.
  : 예외가 발생해도 호출한 곳에 롤백이 전파되지 않는다.

- NESTED
  : = 중첩 트랜잭션.
  : 실행 중인 트랜잭션(부모)이 있으면, save point를 지정하고 새로운 트랜잭션을 생성한다.
  : 만약 자식 트랜잭션에서 예외가 발생할 경우, save point까지 롤백된다.
  : 만약 실행 중인 트랜잭션(부모)이 없을 경우, REQUIRED와 동일하게 작동한다.

ex. 일기작성을 부모 트랜잭션으로, 로그 저장을 자식 트랜잭션으로
- 로그 작성을 실패해도 일기 작성은 롤백되지 않는다.
- 일기 작성이 실패하면 로그 작성도 롤백된다.

rf. 예제
https://oingdaddy.tistory.com/28
ReadOnly 트랜잭션을 읽기 전용 속성으로 지정

* 성능 최적화, 특정 트랜잭션 안에서 읽기 외 작업이 일어나는 것을 방지할 때 사용

@Transaction(readOnly=true)
트랜잭션 롤백 예외 예외 발생했을 때 트랜잭션 롤백시킬 경우를 설정

@Transaction(rollbackFor=Exception.class) : 예외 상황 발생 시 롤백된다
@Transaction(noRollBackFor=Exception.class) : 어떤 예외가 되더라도 커밋된다

* 디폴트 : RuntimeException, Error
timeout 일정 시간 내에 트랜잭션 끝내지 못하면 롤백 

@Transaction(timeout=10)

 

|  정리

- 트랜잭션의 개념

  (1) 트랜잭션은, DB의 상태를 변화시키는 하나의 작업 단위, 다시 말해 CRUD 행위를 말한다.

  (2) 트랜잭션은, 커밋과 롤백이라는 두 가지 연산을 통해,
       총 5가지의 상태 (실행중, 일부 커밋, 완전 커밋, 실패, 롤백) 를 가질 수 있다.

  (3) 트랜잭션의 특징으로는 ACID가 있는데,

      - 원자성은 All or Nothing을 말하며,
      - 일관성은 작업 처리 결과는 늘 일관적이어야 한다는 의미이며, 
      - 독립성은 한 트랜잭션이 실행되고 다른 트랜잭션이 수행되어야 한다는 의미이며,
      - 지속성은 한 번 변화를 시켰으면 그것이 영구적으로 반영되어야 한다는 걸 의미한다. 

 

[ 참고 및 출처 ]

- 부트캠프 수업 내용을 정리

- https://stackoverflow.com/questions/8190926/transactional-saves-without-calling-update-method

- https://kafcamus.tistory.com/31

- https://stackoverflow.com/questions/32269192/spring-no-entitymanager-with-actual-transaction-available-for-current-thread

- https://stackoverflow.com/questions/26601032/default-fetch-type-for-one-to-one-many-to-one-and-one-to-many-in-hibernate

- 기타 출처의 경우, 페이지 내 링크 참조

https://gmlwjd9405.github.io/2019/10/28/intellij-jpa-erd.html

 

[IntelliJ] intellij에서 JPA Entitiy 기반의 ERD 그리기 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

|  상황

- repository.save()를 하면 Entity타입의 객체가 반환되어야 하는데 아래와 같이 null이 반환됐다.

/*Q14*/
// 트러블 슈팅 : 어? 뭐지? 데이터가 저장은 됐는데, Response값이 안온다.
// -- 원인 ) Entity 인스턴스 비교(equals)를 위해 equals, hashcode 필요
@PostMapping("/api/notice4")
public Notice addNotice4(@RequestBody NoticeRegister noticeRegister){
    return noticeRepository.save(
        Notice.builder()
                .title(noticeRegister.getTitle())
                .contents(noticeRegister.getContents())
                .build()
    );
}

>> Response

{}

 

|  해결방법

- Entity 객체에 @EqualsAndHashCode가 추가되면 된다.

- 나는 그냥 @Data를 넣어주었다.

 

[참고]

https://stackoverflow.com/questions/68429396/jparepository-save-methods-returns-null

 

|  JPA

- JPA란, 자바에서 사용하는 ORM 기술 표준을 말한다.

* ORM : 객체와 RDBMS를 매핑해주는 기술

- JPA는 인터페이스의 모음으로, Hinernate / EclipseLink / DataNucleus가 이 명세를 구현했다.

 

|  JPA를 왜 쓸까?

1. SQL 중심 개발 -> Object 중심 개발

2. 생산성 : 데이터의 CRUD가 훨씬 쉬워진다.

저장 : jpa.persist(entity)

조회 : jpa.find(pk)

수정 : entity.setName(" ")

삭제 : jpa.remove(entity)

3. 유지보수 : 필드 변경 시 모든 SQL 수정 --> 필드를 하나만 더 추가

4. 패러다임 불일치 해결

rf. 객체와 관계형 DB의 차이

  객체 SQL
상속 객체 상속 관계 Table 슈퍼 타입 - 서브 타입 (Pk - Fk)
연관관계 참조 사용 (ex. member.getTeam()) 외래 키를 사용 (양방향 조인이 가능)
객체 그래프 탐색 상속 관계에서 부모 타입 사용 가능 서로 pk,fk를 통해 조인된 테이블끼리만 가능
비교하기 같은 참조값을 가진 객체는 서로 == 같다고 표현 동일한 트랜잭션에서 조회한 엔터티여야만 같다고 표현

5. 성능 :

(1) 1차 캐시와 동일성 보장

: JPA를 쓸 경우에 하나의 트랜잭션에서 다음 트랜잭션으로 넘어가기 전에 잠시 동안 캐시를 해주는 기능이 있다.

(2) 트랜잭션을 지원하는 쓰기 지연

insert JDBC BATCH SQL 기능을 통해 commit() 전까지 insert를 모아 한번에 전송한다.
update/delete update, delete로 인한 row 락 시간 최소화 - update, delete 후 바로 commit()

(3) 지연 로딩

* 일반적으로 지연 로딩으로 먼저 코딩 후, 최적화를 위해 자주쓰는 두 객체간 연관관계를 즉시 로딩으로 변경한다.

지연 로딩 객체가 실제 사용될 때에 로딩한다.

-- A 테이블을 조회, B 테이블을 조회 ... 식으로 지연하여 조회
즉시 로딩 Join을 통해 연관 객체를 미리 묶어 조회

-- A join B 를 통해 바로 조회 

6. 데이터 접근 추상화와 벤더 독립성

7. 표준

 

|  역사

- JPA는 Java Persistence API 의 줄임말

- 이클립스 재단에서 가져가면서 Jakarta Persistence API 로 명칭 변경

- 과거에 EJB ORM이 있었다. --> 하이버네이트 (오픈 소스) --> JPA (자바 표준)

 

|  특징

- Annotation을 통해 매핑

- 기본형 타입에 대한 매핑 지원

- 밸류 타입에 대한 매핑 가능

- 클래스 간 연관관계 : 1:1, 1:N, N:1, N:M

- 상속 매핑 지원

 

 

[ 출처 ]

인프런 강의, [ 자바 ORM 표준 JPA 프로그래밍 - 기본편 ] 을 들은 후 정리한 내용입니다.

 

 

 

 

+ Recent posts