Develop/spring-data

[JPA] 프로젝트에 JPA를 적용하며.... ( save편 )

에디개발자 2020. 11. 26. 07:00
반응형

이번 프로젝트를 진행하면서 JPA와 Querydsl을 도입하였습니다. 

 

 

SI 회사에서 근무했을 당시에는 mybatis를 사용했었습니다. SI 프로젝트는 90%이상이 mybatis일 것입니다. 그리고 스타트업으로 이직한 후 첫 프로젝트에서도 역시 mybatis를 사용했습니다. 미리 공부해뒀다면 jpa를 썼을텐데... 아쉬운 마음에 학습하고 이번 프로젝트를 진행할 떄 도입하기로 결정이 났습니다. ( 드디어..! )

 

Repository를 이용하여 findById(), save(), 그외 등등.. 을 사용하였습니다. 

혹시나 프로젝트 기술스택에 대해서 궁금해하시는 분들을 위해서 적어봅니다.  - 
  - Querydsl-jpa
  - p6spy ( log설정 )
  - pinpoint ( 모니터링 )
  - flyway ( 기술검토 중 )
  - spring boot, gradle ... 

해당 기술을 선택한 이유와 간단하게 설정하는 방법은 블로그에 작성해놓았습니다.

 

이슈

insert와 update를 할 때 모든 필드를 세팅하지 않으면 세팅되지 않은 필드들은 null로 sql문이 작성됩니다.

@Getter
@Setter
@Entity
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicUpdate  // 변경된 필드만 적용
@DynamicInsert  // 같음
public class Store {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String address;
    
    @Builder
    public Store(Long id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

}
@Service
@RequiredArgsConstructor  // 의존성 주입
public service {

    private final StoreRepository storeRepository;

    public Store insert() {
        Store store = Store.builder()
            .name("용태스토어")
            .build();

        return storeRepository.save(store);
    }
    
    public Store update(final Long id) {
        Store store = Store.builder()
            .id(id)
            .name("용태스토어")
            .build();
            
        return storeRepository.save(store);    
    }
}

이렇게 insert와 update를 하면 어떻게 나올까? 

insert는 Entity Store객체에 @DynamicInsert 를 붙혔으니 name 만 세팅할 것입니다.

INSERT INTO PLAYER (name)
VALUES ('용태스토어');

update도 Entity Store 객체에 @DynamicUpdate를 붙혔으니 name만... 세팅되지 않는다! 

UPDATE store
SET name = '용태스토어'
    , address = null
WHERE id = 1

 

왜 동일한 Annotation을 달았는데 다르게 작동하는걸까? 정답은 영속성 때문입니다.

영속성 개념이 쉽진 않다. 아주 간단하게 요약하자면 db의 데이터와 연결중 ~ 이라고 생각하면 쉽다!

 

insert 같은 경우는 db의 데이터와 상관없이 새로운 데이터를 만들어서 넣는 것이다. 그렇기때문에 @DynamicInsert 에서 영속성이 없어도 null 필드는 제외하고 insert 쿼리를 실행합니다.

하지만 update는 다릅니다. db의 데이터를 변경하는 것이기에 영속성이라는 개념이 필요합니다. 위에서 행한 경우는 비영속성으로 db와 연결중이 아닙니다. 그 상태에서 update하면 당연히 변경감지를 할 수 없습니다. 

update는 insert와 다르게 null이 무조건 제외되는 경우가 아닙니다. 
실제 데이터가 1이었다가 null로 변경되면 null도 변경된 필드이기 때문에 순수하게 null로 세팅되었다고 제외되면 안됩니다. 그래서 실제 db의 row 데이터와 비교해야합니다.

영속성을 유지하려면 디비에서 조회를 해오면 영속성이 유지됩니다. ( 1차 캐시 )

Store store = storeRepository.findById(id);    // 조회된 Entity는 영속성 유지중

그럼 update를 하기위해 소스를 수정해보겠습니다.

@Service
@RequiredArgsConstructor  // 의존성 주입
public service {

    private final StoreRepository storeRepository;

    public Store save(final Long id) {
        Store store = storeRepository.findById(id);    // 1번 조회

        store.changeName("용태스토어");
        return storeRepository.save(store);            // 1번 조회, 1번 업데이트
    }
}

그리고 실행!! 그러나 또 예상과는 다른 결과!

 

당연히 findById 할때 조회 1번 save할 때 업데이트 한번이라고 생각했습니다. 그런데 예상과는 다르게 조회를 2번하였습니다. 뭘까??

 

구글링을 해보니 JPA는 save()만 있습니다. 다르게 말하자면 insert와 update가 구분되어 있지 않습니다.

Entity에 @Id 어노테이션이 붙어있는 필드의 값을 세팅하고 save를 하면 insert, update 여부를 판단하기 위해 findById로 조회를 해옵니다. 값이 있으면 update, 없으면 insert를 실행합니다. 그래서 조회가 2번 일어나고 있었습니다.

 

그럼 어떻게할까? 그대로 진행하면 update를 할때마다 조회를 해야하는데 너무 자원낭비이지 않나?? 

다시 구글링하여 답을 얻었습니다. JPA의 영속성 개념을 이용하여 update를 하면 되겠구나!

영속성이 유지되는 Entity는 Transaction이 종료되면 변경된 필드를 자동으로 감지하여 db에 commit을 해줍니다. 이와 같은 개념이 Dirty Checking 입니다.

 

영속성 개념을 적용하도록 소스를 수정해보겠습니다.

@Service
@RequiredArgsConstructor  // 의존성 주입
public StoreService {

    private final StoreRepository storeRepository;

    @Transactional
    public Store save(final Long id) {
        Store store = storeRepository.findById(id);    // 1번 조회

        store.changeName("용태스토어");
        return store;    // transaction이 종료되는 시점에 변경 필드 업데이트
    }
}

이렇게하면 update 시 select를 해오지 않아도 됩니다. 여기서 데이터 일관성의 중요성이 보여지네요. 만약에 @Setter가 Entity에 붙어있었다면 개발자들이 무분별하게 set을 사용하는 환경을 만들어주는 것입니다. 그럼 Transaction이 종료되면 Entity에 set을 한 모든 필드를 감지하여 update를 치게 될 것입니다. 그럼 왜 데이터가 바뀌었는지 찾는데 한참 걸리겠죠?

 

결론

  • 영속성을 파악하고 상황에 맞게 사용하자! 의미없는 자원을 줄이자!
  • @Setter와 생성자를 제외하고 의미있는 메서드, Builder를 사용하여 일관성을 보존하면서 Entity를 사용하는 것을 권해드리고 싶습니다. ( 누구나 실수는 한다!! )

 

 

 

 

 

반응형