포스트

2022-02-15-TIL

Today I Learned

UPSERT (없으면 INSERT 있으면 UPDATE)

UPSERT는 UPDATE와 INSERT를 합친 말로, 같은 키의 행이 없으면 새로 넣고 있으면 고치는 동작이다. 구현 방법은 크게 세 가지다.

단일 쿼리

쿼리 하나로 다음과 같이 구현할 수 있다. MySQL의 INSERT ... ON DUPLICATE KEY UPDATE는 넣으려는 행이 PRIMARY KEY나 UNIQUE 인덱스에서 중복을 일으키면, INSERT 대신 뒤에 적은 UPDATE를 실행한다(MySQL 문서).

1
2
3
4
5
6
7
8
9
10
INSERT INTO USER (
    USER_ID,
    PASSWORD,
    CREATED_DATE
) VALUES (
    'august17',
    '1234',
    NOW()
) ON DUPLICATE KEY UPDATE
    PASSWORD = '1234'

처음 적었던 쿼리는 CREATED_DATE 뒤에 쉼표가 남아 있었고, ON DUPLICATE KEY UPDATE 뒤에 갱신할 컬럼이 빠져 있어 실행되지 않았다. 두 군데를 고쳤다. 중복이 났을 때 무엇을 갱신할지는 반드시 적어야 하고, 위 예에서는 비밀번호만 바꾸고 최초 생성일은 그대로 둔다.

같은 문서에 따르면 영향받은 행 수는 새로 넣으면 1, 기존 행을 고치면 2, 기존 값과 같아 바뀐 것이 없으면 0이다. 새로 넣으려던 값을 UPDATE 절에서 참조하던 VALUES() 함수는 MySQL 8.0.20부터 deprecated이고, 8.0.19부터 지원하는 행 별칭(AS new)을 쓰라고 안내한다.

PostgreSQL은 INSERT ... ON CONFLICT DO UPDATE로 같은 일을 한다. PostgreSQL 문서는 이 구문이 동시성이 높은 상황에서도 INSERT 또는 UPDATE 둘 중 하나의 결과를 원자적으로 보장한다고 설명한다.

여러 번의 쿼리

또는 애플리케이션 코드상에서 UPDATE 쿼리를 먼저 날리고, 갱신된 행이 없으면 INSERT 쿼리를 날리면 된다. 하지만 가독성이 매우 떨어져서 의도를 파악하기가 어렵다. 게다가 두 쿼리 사이에 다른 트랜잭션이 같은 키로 INSERT하면 중복 키 오류가 날 수 있어서, 동시성까지 고려하면 단일 쿼리보다 다룰 것이 많다.

JPA

여러 가지 방법들이 있겠지만 일반적으로는 findById 등으로 엔티티를 가져온 다음 save한다. save를 호출하면 해당 엔티티의 키가 없다면 추가하고, 있다면 수정한다.

Spring Data JPA 문서에 따르면 save()는 엔티티가 새것이면 EntityManager.persist()를, 아니면 merge()를 호출한다. 새것인지는 먼저 non-primitive 타입의 @Version 필드가 있으면 그 값이 null인지로, 없으면 식별자(id)가 null인지로 판단한다. 그래서 id를 직접 할당하는 엔티티는 처음 저장할 때도 “새것이 아님”으로 판단되어 persist()가 아니라 merge()가 호출된다. 문서는 이런 경우 Persistable을 구현해 isNew()로 새것인지 직접 알려 주는 방법을 안내한다.

findById 후 save하는 방식도 조회와 저장 사이에 다른 요청이 끼어들 수 있으므로, 동시에 같은 키가 들어올 수 있다면 유니크 제약으로 최종 방어선을 둔다.

  • https://kig6022.tistory.com/6
  • https://coding-factory.tistory.com/427
  • https://wiki.postgresql.org/wiki/UPSERT
  • https://www.baeldung.com/spring-data-partial-update
  • https://stackoverflow.com/questions/36241163/spring-jpa-how-to-upsert-without-losing-data

@Data 사용의 장단점

@Data는 @ToString, @EqualsAndHashCode, 모든 필드의 @Getter, final이 아닌 모든 필드의 @Setter, @RequiredArgsConstructor를 한 번에 붙이는 단축 애너테이션이다. 단순한 POJO에 필요한 반복 코드를 한 줄로 없앨 수 있다는 것이 장점이다.

단점은 너무 많은 것을 한꺼번에 만든다는 점이다.

  • 모든 필드에 setter가 생겨 객체가 어디서든 바뀔 수 있다. 불변이어야 하는 값 객체나 엔티티에는 맞지 않는다.
  • 모든 필드로 equals, hashCode가 만들어진다. JPA 엔티티처럼 연관 관계가 있는 객체에서는 양방향 연관이 서로를 계속 호출해 toString, hashCode가 무한 순환할 수 있다.
  • 문서에 따르면 @Data로는 callSuper, exclude 같은 세부 옵션을 지정할 수 없다. 옵션이 필요하면 개별 애너테이션을 직접 붙여야 한다.

그래서 DTO처럼 단순히 데이터를 담는 클래스에는 편하지만, 도메인 객체에는 필요한 애너테이션만 골라 붙이는 편이 안전하다.

  • https://projectlombok.org/features/Data
  • https://javabydeveloper.com/lombok-data-annotation/

객체 직렬화

자바 직렬화(serialization)는 객체를 바이트 스트림으로 바꿔 저장하거나 전송하고, 역직렬화로 다시 객체로 되살리는 기능이다. Serializable만 구현하면 쓸 수 있어 편하지만 보안 문제가 크다.

Oracle의 Secure Coding Guidelines for Java SE는 직렬화가 자바 언어의 필드 접근 제어를 우회하는 인터페이스를 제공한다고 지적하고, 신뢰할 수 없는 데이터를 역직렬화하는 것은 본질적으로 위험하므로 피해야 한다고 명시한다.

그 밖에도 직렬화된 형태가 클래스의 내부 구조에 묶여 클래스를 바꾸기 어려워지는 문제가 있다. 그래서 외부와 주고받는 데이터는 JSON 같은 명시적인 포맷을 쓰고, 자바 직렬화를 꼭 써야 한다면 지침이 안내하는 대로 제한적인 역직렬화 필터로 허용할 클래스를 좁힌다.

  • https://dzone.com/articles/object-serialization-evil
참고한 자료외부 출처 5

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다