Database

PK 생성 전략 선택하기

KAispread 2023. 6. 19. 00:37
728x90
반응형

6월부터 본격적으로 본 과정에 들어가게 되면서, 프로젝트 개발을 위해 필요한 여러 설계를 진행하였다. 그 중에서도 데이터베이스 물리적 설계도 진행하였는데, 담당 멘토님께서 PK를 int 로 잡은 이유에 대해서 여쭤보셨다. 그 이유에 대해 PK 생성 방법을 MySQL의 AUTO_INCREMENT를 사용할 예정이기 때문이라고 답했고 멘토님께서 다시 AUTO_INCREMENT로 PK를 생성하는 이유에 대해 여쭤보셨는데 여기에 제대로 답을 하지 못했다.

단순히, AUTO_INCREMENT 전략을 주로 사용하여 PK를 생성한다는 것은 알고 있었지만 왜 쓰는지에 대해서 깊게 생각해본적이 없었다는 생각이 들었다. 따라서 이번 포스팅에서 PK 생성 전략을 살펴보려 한다.

 


 

식별자 (PK) 선택 전략

데이터베이스 기본 키는 다음 3가지 조건을 모두 만족해야한다.

  1.  null 허용 X
  2. 유일해야 한다. (UNIQUE)
  3. 변해선 안된다.

위와 같은 조건에서 기본 키를 선택하는 전략은 크게 2가지가 있다.

 

자연 키 (Natural Key)

  • 비즈니스적으로 의미가 있는 키
  • 주민등록번호, 이메일, 전화번호, 주문번호, 고객번호 등

자연 키는 비즈니스적으로 의미가 있는 키를 말한다. 비즈니스적으로 의미가 있다는 말은 곧 비즈니스에 이용될 수 있는 데이터이거나 해당 데이터에 대한 정보를 알 수 있다는 의미와 비슷하다. 이러한 자연 키는 기본 키로 선택하지 않는 것이 좋다.

이유는 데이터베이스 기본키의 조건인 유일성과 불변성을 지키는 자연 키를 쉽게 정의할 수 없기 때문이다. 만약, 이메일을 기본키로 선택한다고 해보자. 해당 기본 키는 앞서 언급한 3가지 조건을 만족할 수 있을까? 쉽게 만족한다고 단언 할 수 없을 것이다. 우선 개인 이메일이 필수적으로 있어야하고 다른 이메일로 변경할 수 없으며, 유일한 이메일 주소여야한다. 유일하다는 항목은 지켜질 것으로 보여지지만, 먼 훗날 중복된 이메일을 생성할 수 있도록 정책이 바뀐다면 해당 테이블과 연관된 수많은 로직을 변경해야 할 것이다. 그럴 일은 발생하지 않는다고 말할수도 있겠지만, 외적 요인에 의해 영향을 받을 가능성이 있다는 것 자체가 리스크이다. 예시로 개인 정보 보호법이 바뀌면서 주민등록번호를 기본 키로 설정했던 곳에서 많은 고충을 겪었던 사례가 존재한다. 

테이블은 한 번 정의하면 변경하기 어렵기 때문에 외적 요소에 흔들릴 수 있는 자연 키는 UNIQUE 옵션을 부여하여 따로 저장하는 것이 좋다. 기본 키를 선택하는 대안으로는 대리 키가 있다.

 

대리 키 (Surrogate Key)

  • 비즈니스와 상관 없이 임의로 만들어진 키
  • 오라클 시퀀스, AUTO_INCREMNET, 키 생성 테이블 사용

대리 키는 비즈니스와 상관 없이 식별을 위해 임의로 만들어진 키를 말한다. MySQL에서 대리 키를 생성하는 방법으로는 AUTO_INCREMENT, UUID, 시퀀스 등이 있다.

먼저 AUTO_INCREMENT는 MySQL에서 PK를 지정할 때 가장 많이 사용되는 방법으로, 숫자 값을 자동으로 증가시키고 해당 값을 기본 키로 사용하는 방식이다. 데이터베이스 자체적으로 값을 관리해주기 때문에 편리하고 기본 키가 중복되는 것을 크게 걱정하지 않아도 된다는 장점이 있다. 하지만 분산형 시스템의 경우 AUTO_INCREMENT 로 인해 생성된 키값이 중복될 수 있다는 문제점이 있다. 또한, 기본 키가 정수형이기 때문에 특정 테이블의 튜플 개수를 쉽게 파악 할 수 있고, SQL injection 과 같은 공격에도 취약해질 수 있다.

 

UUID 사용

이러한 문제에 착안한 대안으로는 UUID를 사용하는 방법이 있다. UUID를 사용하면 다음과 같은 형태의 무작위 값이 생성된다.

UUID 340,282,366,920,938,463,463,374,607,431,768,211,456개의 경우의 수가 존재하기 때문에 사실상 중복되지 않는 유일한 값을 생성한다고 볼 수 있다. MySQL에서는 UUID_SHORT() 또는 UUID() 함수를 사용하여 UUID 값을 생성할 수 있다.

하지만 UUID는 다음과 같은 문제를 가지고 있다. 

  • 의미적으로 알아보기 힘들다.
  • 정렬이 안된다.
  • 용량이 너무 크다.

이와 같은 문제를 해결하기 위해  Youtube UID나 상대적으로 더 적은 용량의 8바이트 정수를 사용하는 방법이 있다. 하지만 이렇게 byte 수를 줄이면 중복의 위험도가 올라가고 AUTO_INCREMENT 에 비해 용량을 많이 차지한다는 단점은 해결할 수 없다.

이처럼 PK 생성 전략을 데이터베이스에 위임하는 경우도 있지만 DB에서 관리하는 공통코드의 경우는 서비스 자체적으로 그룹코드를 정의하고 해당 코드를 PK로써 사용할 수도 있다. 예를 들어 '사내 부서 코드' 테이블의 경우 그룹코드 + 코드를 'GR0001'로 정의하여 PK로 넣고 이름에 '인사과'와 같은 텍스트를 넣어 관리할 수 있다.

 

 

JPA 키 생성 전략 종류

추가적으로 JPA에서 제공하는 PK 생성 전략을 간단하게 살펴보자. JPA에서는 @GeneratedValue를 통해 자동 PK 생성 전략을 제공한다. GeneratedValue에서 제공하는 전략은 다음과 같다.

  • strategy = GenerationType.AUTO >> DB 방언에 맞춰서 자동 생성.
  • strategy = GenerationType.IDENTITY >> 기본 키 생성을 데이터베이스에 위임. (DB별로 자동생성 전략이 다름)
  • strategy = GenerationType.SEQUENCE >> sequence 오브젝트 통해 값을 가져오고, 세팅. (필드값을 정수형으로 바꿔주자. Long 형 권장) / 직접 Sequence를 지정할 수도 있음 Entity에 @SequenceGenerator 을 사용하여 세팅.
  • strategy = GenerationType.TABLE >> 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략. Entity에 @TableGenerator을 사용하면 따로 Table을 생성할 수 있다. 

@GeneratedValue의 기본 전략은 AUTO이다. AUTO는 IDENTITY / SEQUENCE / TABLE 세가지 방법중 선택한 데이터베이스가 권장하는 방법을 자동으로 선택한다. 이 방식은 사용하는 DB가 변경되어도 큰 문제가 없기 때문에 개발단계에서 주로 사용하는 방식이다. 다만 Hibernate 5부터 MySQL와 AUTO를 사용할 경우 TABLE 전략으로 PK를 생성하므로 Product 환경에서는 꼭 전략을 바꿔주자.

IDENTITY 전략은 기본 키 생성을 데이터베이스에 위임하는 방식이다. MySQL의 경우 AUTO_INCREMENT를 사용하는데, 현재 데이터가 몇 번째인지 알아야 PK를 할당해줄 수 있으므로 DB와 connection이 일어나기 전까지 PK값을 알 수 없다. 따라서, 이와 같은 문제를 방지하기 위해 IDENTITY 전략은 Entity를 영속성 컨테이너에 저장할 때 바로 DB에 INSERT 쿼리를 날려버린다. 따라서 영속성 컨텍스트의 특징 중 하나인 '쓰기 지연'이 발생하지 않는다. 이러한 특성 때문에 JPA에서 자동으로 제공하는 Bulk Insert는 사용할 수 없다. (JdbcTemplate나 Spring Batch를 사용하여 극복할 수 있다.)

SEQUENCE 전략은 데이터베이스의 객체인 Sequence를 사용하는 방식이다. (MySQL에서는 사용 X) DB Sequence는 DB에서 사용하는 유일한 값을 순서대로 생성하는 특별한 DB 객체다. SEQUENCE 전략을 사용할 경우, 이 DB Sequnece를 이용해서 기본 키를 생성한다. 이 방식은 미리 DB에서 일정량의 PK값을 가져와서 메모리에 저장해두고 사용하는 방식이다. 따라서 Sequence 전략은 쓰기 지연이 가능하고, 옵션 중 하나인 allocationSize를 통해 성능을 최적화할 수 있다. 

TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. SEQUENCE 전략과는 다르게, 테이블을 사용하는 방식이기 때문에 모든 데이터베이스에 적용할 수 있다. 하지만, 테이블을 통해 PK 값을 할당하므로 추가적인 SELECT, UPDATE 쿼리가 발생한다. 해당 쿼리 발생 주기는 @TableGenerator.allocationSize 를 사용하여 최적화할 수 있다.

 

결론

결론적으로 자연키, 대리키, AUTO_INCREMENT를 쓸지, UUID를 쓸지 또는 시퀀스를 사용하거나 직접 PK를 할당해줄지는 각각의 trade-off가 있기 때문에 비즈니스에 따라 다르게 선택해야한다는 것이다. 다만, 특별한 이유가 없다면 예상치 못한 변수를 생각했을 때 자연키 보다는 대리키를 사용하는 것이 더 낫다고 생각된다.

대리키를 생성하는 방법 또한, AUTO_INCREMENT 를 사용하는 것이 UUID를 사용하는 것 보다 비용이 훨씬 작으므로 AUTO_INCREMENT를 Default로 두고 성능, 데이터, 보안에 따라 UUID를 비롯한 다른 방식의 키 생성 전략을 사용하는 것이 좋다고 생각한다. 예를 들어 IDENTITY를 전략을 사용하는 특정 도메인에서 하나의 트랜잭션 안에 여러 데이터가 삽입되는 경우가 많다면 성능을 위해 다른 전략을 고려해볼 수 있겠다.

확실한 것은 테이블 설계시 무작정 대리 키 - AUTO_INCREMENT 를 남발하면 안된다는 것이다. 모든 테이블에 대리키를 사용하는 경우는 거의 없다고 생각되므로 적절한 PK 생성 방식에 대해 한 번 더 생각해보는 습관을 기르자. 

 

 

참고

https://www.bytebase.com/blog/choose-primary-key-uuid-or-auto-increment/

 

SQL Primary Key - UUID or Auto Increment Integer / Serial?

> tl;dr choosing Auto Increment Integer 95% of the time for readability. One of the first things when designing a new SQL database schema is to decide which typ...

www.bytebase.com

https://ojt90902.tistory.com/912

 

JPA : PK 생성 전략

이 글은 자바 ORM 표준 JPA 프로그래밍을 학습하며 필요한 내용을 작성한 글입니다. @Id 해당 어노테이션을 특정 필드 위에 사용하면 이 필드는 앞으로 DB에서 PK로 사용하겠다는 것을 의미한다. DB

ojt90902.tistory.com

 

728x90
반응형