@OneToMany에서 의도하지 않은 중간 테이블이 생성된 문제
목차
정상 상태
게시글(Board)과 첨부파일(BoardImage)은 1:N 관계예요.
Board 하나에 여러 개의 BoardImage가 연결되며, 데이터베이스에는 board 테이블과 board_image 테이블만 존재하고, board_image 테이블의 외래키(board_id)로 관계를 표현하는 것이 정상이에요.
문제 상황
Board 엔티티에서 BoardImage에 대한 참조를 @OneToMany로 설정하고 프로젝트를 실행했더니, 예상과 다른 테이블이 생성됐어요.

board와 board_image 외에 **board_image_set이라는 중간 테이블(조인 테이블)**이 추가로 생성된 거예요.
원인 분석
JPA 스펙(JSR 338)과 Vlad Mihalcea의 분석 글을 확인해보니, @OneToMany의 기본 동작이 원인이었어요.
@OneToMany만 선언하고 mappedBy나 @JoinColumn을 지정하지 않으면, JPA는 양쪽 엔티티가 독립적인 테이블을 갖고, 그 사이를 중간 테이블로 연결하는 전략을 기본으로 사용해요.
이는 객체 지향 관점에서는 자연스럽지만, 데이터베이스 관점에서는 불필요한 테이블이 생기고 조인 비용이 증가해요.
중간 테이블을 제거하는 방법은 두 가지예요.
- 단방향
@OneToMany에@JoinColumn을 추가하는 방법 - 양방향 매핑에서
mappedBy속성을 사용하는 방법
해결: mappedBy 적용
2번 방법을 선택했어요. 이유는 다음과 같아요.
- 게시물(Board) 관점에서 보면, 첨부파일은 별개의 존재예요. 단방향
@OneToMany+@JoinColumn을 사용하면 부모 엔티티가 자식 테이블의 외래키를 관리하게 되는데, 이 경우 INSERT 후 별도의 UPDATE 쿼리가 추가로 발생해요. - 첨부파일(BoardImage) 관점에서 보면, 하나의 게시물을 참조하는
@ManyToOne관계가 자연스럽거든요.mappedBy를 사용하면 외래키의 주인이 BoardImage 쪽이 되어, INSERT 한 번으로 관계가 설정돼요.
mappedBy는 “이 컬렉션의 매핑 주인은 상대 엔티티의 이 필드다”라고 선언하는 거예요.
Baeldung - @JoinColumn vs mappedBy에서도 양방향 @OneToMany에서는 mappedBy 사용을 권장하고 있어요.
적용 후 프로젝트를 실행하니, 중간 테이블 없이 board_image 테이블에 board_id 외래키가 생성됐어요.

@ManyToOne 구조처럼 외래키 기반의 테이블이 정상적으로 생성된 것을 확인할 수 있어요.
정리
| 항목 | mappedBy 적용 전 | mappedBy 적용 후 |
|---|---|---|
| 테이블 수 | 3개 (board, board_image, board_image_set) | 2개 (board, board_image) |
| 관계 표현 | 중간 테이블의 두 외래키 | board_image.board_id 외래키 |
| 조인 비용 | 2번 조인 필요 | 1번 조인으로 충분 |
| 데이터 정합성 | 중간 테이블까지 관리 필요 | 외래키 제약으로 자동 보장 |
@OneToMany는mappedBy나@JoinColumn없이 단독 사용하면 기본적으로 중간 테이블을 생성해요. 이것이 JPA의 기본 전략이에요.mappedBy로 연관관계의 주인을 명시하면, 중간 테이블 없이 외래키 기반의 자연스러운 테이블 구조를 만들 수 있어요.- 중간 테이블 제거는 단순히 테이블 수를 줄이는 것이 아니라, 조인 연산 복잡도와 데이터 정합성 관리 비용을 줄이는 것이에요.
JPA 연관관계 매핑은 어노테이션 하나로 끝나는 게 아니라, 도메인 관계의 방향성과 데이터베이스 설계 원칙을 함께 고려해야 해요.
Reference
- Vlad Mihalcea - The best way to map a @OneToMany association
- Baeldung - @JoinColumn vs mappedBy
- Thorben Janssen - Best Practices for Many-To-One and One-To-Many Association Mappings
Normal Behavior
Board (post) and BoardImage (attachment) have a 1:N relationship. Multiple BoardImages are linked to a single Board, and the database should only contain board and board_image tables, with the relationship expressed through a foreign key (board_id) in the board_image table.
The Problem
After setting up the reference from Board to BoardImage with @OneToMany and running the project, unexpected tables were created.

In addition to board and board_image, a join table called board_image_set was created.
Root Cause Analysis
After checking the JPA spec (JSR 338) and Vlad Mihalcea’s analysis, the default behavior of @OneToMany turned out to be the cause.
When @OneToMany is declared without mappedBy or @JoinColumn, JPA defaults to a strategy where both entities have independent tables connected by a join table. This is natural from an object-oriented perspective, but from a database perspective, it creates unnecessary tables and increases join costs.
There are two ways to eliminate the join table:
- Adding
@JoinColumnto a unidirectional@OneToMany - Using the
mappedByattribute in a bidirectional mapping
Solution: Applying mappedBy
Option 2 was chosen for the following reasons:
- From the Board’s perspective, attachments are separate entities. Using unidirectional
@OneToMany+@JoinColumnmeans the parent entity manages the child table’s foreign key, which generates an additional UPDATE query after INSERT. - From the BoardImage’s perspective, referencing a single post with
@ManyToOneis natural. UsingmappedBymakes BoardImage the owner of the foreign key, so a single INSERT establishes the relationship.
mappedBy declares “the mapping owner of this collection is this field in the other entity.” Baeldung - @JoinColumn vs mappedBy also recommends using mappedBy for bidirectional @OneToMany.
After applying the fix and running the project, the board_image table was created with a board_id foreign key without any join table.

The foreign key-based table structure, similar to a @ManyToOne setup, was successfully created.
Summary
| Aspect | Before mappedBy | After mappedBy |
|---|---|---|
| Table count | 3 (board, board_image, board_image_set) | 2 (board, board_image) |
| Relationship | Two foreign keys in join table | board_image.board_id foreign key |
| Join cost | 2 joins required | 1 join sufficient |
| Data integrity | Join table management needed | Automatically guaranteed by FK constraint |
@OneToManyused alone withoutmappedByor@JoinColumncreates a join table by default. This is JPA’s default strategy.- Specifying the relationship owner with
mappedBycreates a natural foreign key-based table structure without a join table. - Eliminating the join table isn’t just about reducing table count — it’s about reducing join operation complexity and data integrity management costs.
JPA relationship mapping isn’t just about a single annotation — it requires considering both the directionality of domain relationships and database design principles.