- 복합키 vs (대칭키 + UNIQUE 제약 조건) 회의록2025년 06월 20일 15시 24분 57초에 업로드 된 글입니다.작성자: do_hyuk728x90반응형
상황
북마크 테이블은 TIL Bookmark 와 CodePost Bookmark로 나뉘어 있기 때문에 팀원 A는 TIL을 담당하고 나는 CodePost를 담당하였다.
이 후 도메인 구조를 살펴보니 A는 post_id와 email을 통해 복합키로 구성하였고, 나는 auto_increment 대칭키와 post_id, member_id를 유니크 제약 조건으로 설정하여 구현하였다.
@EqualsAndHashCode public class TILBookmarkId implements Serializable { private String email; private Long postId; }
@IdClass(TILBookmarkId.class) @Entity public class TILBookmark { @Id @Column(name = "MEMBER_email") private String email; @Id @Column(name = "TILPOST_id") private Long postId; ... }
// service public Long saveOrUpdate(String email, Long postId) { return tilbookmarkCommand.saveOrUpdate(email, postId); }
@Table(uniqueConstraints = {@UniqueConstraint(name = "uq_email_codepost", columnNames = {"email", "code_post_id"})} ) @Entity public class CodePostBookmark extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne() private Membmer member; @Column(nullable = false) private Long codePostId; ... }
// service public Long saveOrUpdate(CodePostBookmarkSaveRequestDto requestDto, String email) { Member member = memberCommand.findByEmail(email); return codePostBookmarkCommand.saveOrUpdate(requestDto, member.getId()).orElseThrow( () -> new CustomRuntimeException(ErrorCode.NOT_FOUND_BOOKMARK) ).getId(); }
서로의 도메인 구조를 보고 어느 구조가 더 나은지에 대한 토론을 진행하였다.
첫 번째 안건
북마크 saveOrUpdate() 로직에서 member를 조회할 필요가 있는가?
codePostBookmark 에서 member_id를 외래키로 가지고 있기 때문에 request 값으로 받은 email로 member를 조회하는 쿼리가 필수적이다. member 조회 쿼리의 필요성을 판단하기 위해 확인해야할 요소는 다음과 같다.
1. member 조회 없이 email로 바로 진행해도 되는가?
현재 email은 JWT access token 에서 추출한 값으로 DB 접근 없이 받아온 값이다.
FK인 이유를 제외하고는 보통 member를 조회하는 이유는 DB에 해당 회원이 있는지 판단하기 위해서인데, 북마크는 사용자가 직접 호출하는 API이기 때문에 회원이 탈퇴한 상황에서는 일반적으로 호출될 수 없는 API이다.
만약 호출이 된다면 access token을 탈취한 특수한 상황일 텐데 access token의 유효기간은 30분이고, 북마크가 이미 생성되어 있다면 상태 변경으로 soft delete 되기 때문에 접근을 한다 해도 상관이 없다. 따라서 FK인 이유를 제외하고는 유효성 검사 목적으로서의 필요성은 없다.
2. 북마크 테이블에 member_id를 FK로 가지고 있을 필요가 있는가?
그렇다면 FK를 쓸 이유가 있을까?
member_id와 연관관계를 맺은 이유는 관리하기 쉬워서이다. cascade 옵션을 통해 회원이 탈퇴할 경우 한번에 삭제하기 위해서인데,
cascade 방식은 삭제할 entity들을 메모리에 한 번에 올려서 처리하기 때문에 성능 상 이슈가 발생할 수 있고, 또한 Ori 서비스는 soft delete 방식을 사용하기 때문에 cascade 방식 말고 bulk update 방식으로 상태 변경만을 할 것이다.
따라서 member FK는 필요하지 않다.
첫 번째 안건 채택
@Table(uniqueConstraints = {@UniqueConstraint(name = "uq_email_codepost", columnNames = {"email", "code_post_id"})} ) @Entity public class CodePostBookmark extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String email; // 연관매핑 삭제 후 email 컬럼으로 변경 @Column(nullable = false) private Long codePostId; ... }
public Long saveOrUpdate(CodePostBookmarkSaveRequestDto requestDto, String email) { // 불필요한 member 조회 로직 삭제 return codePostBookmarkCommand.saveOrUpdate(requestDto, email).orElseThrow( () -> new CustomRuntimeException(ErrorCode.NOT_FOUND_BOOKMARK) ).getId(); }
두 번째 안건
복합키(email, post_id) 방식이 더 나은지 대칭키 + UNIQUE(email,post_id) 방식이 더 나은지
saveOrUpdate() 로직에서는 upsert() 기능을 사용하는데 이 때 INSERT ... ON DUPLICATE KEY UPDATE ... 로 실행될 때
키가 중복인 것을 어떻게 판단하는지 부터 알아야한다.
현재 복합키는 PK 값으로 InnoDB에서 클러스터형 인덱스(Clustered Index)를 생성한다.
클러스터형 인덱스는 인덱스 자체가 row 데이터까지 함께 저장하기 때문에 pk로 접근 시 row 데이터로 바로 접근 가능하다.
1번의 I/O로 접근 가능한 것이다.
[클러스터 인덱스 구조 (id 기준)] (내부 노드) [50] / \ [10][30] [60][80] ← 리프 노드 (id 기준 정렬) ↓ ↓ Row(id=10, name=Alice) Row(id=30, name=Bob) Row(id=60, name=Tom)
하지만 UNIQUE 값은 보조 인덱스(Secondary Index)를 생성하기 때문에 인덱스에 row 데이터를 저장하는 것이 아니라 해당 PK에 접근하기 위한 주소값을 저장한다. 따라서 유니크 인덱스를 통해 row 데이터에 접근하기 위해서는 2번의 I/O가 필요한 것이다.
[보조 인덱스 구조 (email 기준)] (내부 노드) [m@x.com] / \ [a@x.com][k@x.com][z@x.com] ← 리프 노드 ↓ ↓ id=10 id=30 [→ 다시 Clustered Index에서 id=10/30 찾음]
그렇기 때문에 빈번히 발생하는 upsert 로직에서는 복합키 방식이 성능상 더 낫다고 판단이 되었다.
하지만,
복합키 방식은 email, post_id로 Varchar[255] + BIGINT 이기 때문에 인덱스의 깊이가 깊어지고 정렬 기준에 email이 포함되기 때문에 비용이 추가적으로 발생하게 된다.
또한 복합키 방식이 성능상 매우 유리하려면 upsert 로직이 정말 많이 그리고 동시에 발생해야한다.
그리고 확장성 측면에서 만약 북마크에 카테고리를 추가하게 된다면 도메인 IdClass에 카테고리 컬럼을 추가하고, DB 마이그레이션을 통해 기존 pk 값들을 모두 변경해줘야하는 것에 비해 대칭키 + UNIQUE 방식은 카테고리 컬럼 추가 및 유니크 제약 조건만 수정해주면 되기 때문에 설계 방식에 따라 달라질 것이다.
Ori 서비스는 대규모가 아닌 소규모 서비스이기 때문에 복합키로 수정하기에는 오버 엔지니어링이라 판단되었기 때문에 굳이 코드를 리팩토링하지 않고 서로의 방식 그대로 진행하게 되었다.
결론
[대규모]
성능 중심의 설계일 경우 복합키 방식이 낫다.
→ 특히 동시성이 높고, 정말 많은 upsert 로직 호출 서비스일 경우
[소규모]
유지 보수와 확장성 중심의 설계일 경우 대칭키 + UNIQUE 방식이 낫다.
728x90반응형'포트폴리오 > AutoReview' 카테고리의 다른 글
saveOrUpdate 구현 시 동시성 이슈 (1) 2025.06.02 Spring Boot 프로젝트에서 Github REST API로 Push 기능 구현하기 (0) 2025.05.17 🛠 GitHub 연동 기능 구현기 (0) 2025.05.15 Code Post에 북마크 기능 추가 (0) 2025.05.10 게시글 공개 여부 기능 추가 (0) 2025.05.08 댓글