일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 406 NOT_ACCEPTABLE
- JPA
- RunWith
- 컴포넌트 스캔
- Java Reflection API
- 스프링 컨테이너
- 기본 생성자
- jdbc template
- jdbc
- 가을야구
- 스프링
- 테스트 코드
- Controller
- 405 METHOD_NOT_ALLOWED
- 스프링 데이터 JPA
- 통합 테스트
- ResponseEntity
- 랜덤 포트
- testresttemplate
- 의존관계 자동 주입
- 정적 컨텐츠
- 의존성 주입
- 스프링 IoC 컨테이너
- 키움
- 좋은 객체지향 설계 원칙
- Not Acceptable
- SpringBootTest
- 빈 스코프
- restTemplate
- entity
- Today
- Total
코드네임 JY
[스프링 JPA] 연관관계 매핑 (1) 본문
🥯 객체 vs 관계형 데이터베이스 패러다임 차이
관계형 데이터베이스에서는 다음과 같은 방식으로 테이블 간 관계를 알 수 있다.
예를 들어, MUSIC 테이블은 어떤 가수의 노래인지에 대한 정보가 필요하므로 ARTIST 테이블과 연결되어 있다.
이는 MUSIC 테이블 내에서 ARTIST 테이블에 있는 컬럼들의 PK 값을 가지는 외래 키 방식 으로 구현할 수 있다.
public class Music {
private Long id;
private String title;
private Long artist_id; // Music → Artist
}
public class Artist {
private Long id;
private String name;
}
위 코드는 객체를 테이블에 맞추어 모델링한 결과이다. 하지만 이는 전혀 '객체지향' 스럽지 않다. 왜냐고?
// 찾은 Music을 통해 Artist를 가져오고 싶을 때
Music findMusic = em.find(Music.class, 1L);
Long findId = findMusic.getArtistId(); // Artist의 id 값을 찾아야 함
Artist findArtist = em.find(Artist.class, findId); // 찾은 값을 토대로 결과 반환
찾은 Music 객체에서 Artist 객체를 가져오고 싶다면? Music 객체 내에서 Artist의 id 값을 일단 가져온다.
그리고 해당 id 값을 활용해서 Artist 객체를 최종적으로 가져오는.. 꽤나 복잡한 구성을 가지고 있다.. 😟😟
여기서 객체와 관계형 데이터베이스 간의 패러다임 차이가 존재한다.
✅ 관계형 데이터베이스 : 외래 키(FK)를 매핑 하는 방식으로 연관성을 정의
✅ 객체(자바) : 객체 참조(Reference) 로 연관성을 정의
그래서 관계형 데이터베이스의 패러다임을 그대로 자바에서 객체로 구현한다면? 객체지향적인 코드를 작성할 수 없다.. 🥲🥲
그렇다면 객체지향적인 방식으로 테이블 간의 관계를 구현할 수는 없을까? 객체를 어떻게 사용한다면 되지 않을까?
정답은! 객체를 사용하면서, 서로 간 연관관계를 지정 하면 된다! 밑에서 단방향 및 양방향 연관관계를 설명할 것이다!
+ 추가적으로, 이런 식의 객체와 관계형 데이터베이스 간의 패러다임 차이를 해결하고자 나온 것이 ORM 기술이다!
엔티티와 테이블은 서로 모델 형태의 차이도 존재한다. 일례로, 객체를 바로 데이터베이스에 저장할 수는 없지 않나!
따라서 객체를 통해 데이터베이스를 다룰 수 있도록 하여 객체지향적인 코드로 개발자가 로직 부분에 더 집중할 수 있게 해준다.
🧇 단방향 연관관계
위에서 다룬 예시를 객체지향적인 코드로 다시 리팩토링 해보겠다. 이는 단방향 연관관계를 통해 구현할 수 있다!
public class Music {
@Id @GeneratedValue
@Column(name = "music_id")
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "artist_id")
private Artist artist; // Music → Artist
}
public class Artist {
@Id @GeneratedValue
@Column(name = "artist_id")
private Long id;
private String name;
}
아까 작성한 코드에 부수적인 것들을 조금 붙이기는 했는데, 중요한 건 artist_id가 아닌 Artist 객체를 그대로 가져온 것 이다!
기존에는 id 값을 가져와 FK 방식을 직접 구현했는데, 이제는 객체를 직접 가져와서 몇 가지 어노테이션을 설정해주면 된다.
@ManyToOne 어노테이션
✅ 하나의 Artist는 여러 Music을 가질 수 있다! 따라서 Artist (1 : N) Music 관계가 만들어진다.
✅ Music 입장에서는 Artist에게 多 : 1 관계이므로, 이를 어노테이션을 나타내면 'ManyToOne' 을 사용하면 된다.
@JoinColumn 어노테이션
✅ 외래 키가 어떤 정보를 담을건지 생각하면 된다! 직관적으로 해석하면 "Join하는 Column은 무엇인가?" 로 읽자!
✅ Artist 객체의 id 값(artist_id)을 외래 키의 값으로 담고 있으므로, 위처럼 써주면 된다!
이처럼 객체를 가져오게 되면, 객체지향적인 코드를 구성할 수 있다.
// 찾은 Music을 통해 Artist를 가져오고 싶을 때
Music findMusic = em.find(Music.class, 1L);
Artist findArtist = findMusic.getArtist(); // Music에서 바로 Artist 반환 가능
이렇게 바로 Getter를 사용해서 가져올 수 있다! 이는 단방향 연관관계이다!
정리하면! 키 값을 그대로 사용하는 것이 아닌, 객체를 가져와 관계를 설정하고 조인해야하는 컬럼을 나타내기 만 하면 된다!
🥞 양방향 연관관계
단방향은 'A → B' 처럼 한 쪽으로만 진행되는 것을 의미하는데, 양방향은 'A ↔ B' 처럼 서로 왔다갔다 할 수 있음을 뜻한다.
당연한거 아니냐고? 초딩도 아는거 아니냐고? ㅋㅋㅋ 맞다! 하지만 지금부터 '방향' 이라는 개념 자체를 들고 한 번 생각해보자.
위의 예시를 다시 한 번 들고 와보면, 관계형 데이터베이스의 경우 위 그림을 봤을 때 무언가 '단방향' 처럼 느껴진다.
화살표 때문에 그런가? 하지만 엄청난 사실.. 관계형 데이터베이스는 '뱡향' 이라는 개념이 없다!!
-- MUSIC 테이블에서 ARTIST 테이블 JOIN
SELECT * FROM MUSIC M
JOIN ON ARTIST A WHERE M.artist_id = A.artist_id;
-- ARTIST 테이블에서 MUSIC 테이블 JOIN
SELECT * FROM ARTIST A
JOIN ON MUSIC M WHERE A.artist_id = M.artist_id;
위 SQL 쿼리문처럼, 두 테이블 중 어느 테이블에서도 서로의 테이블을 조회 할 수 있다. 따라서 '방향성'을 가지고 있지 않다.
public class Music {
@Id @GeneratedValue
@Column(name = "music_id")
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "artist_id")
private Artist artist; // Music → Artist
}
public class Artist {
@Id @GeneratedValue
@Column(name = "artist_id")
private Long id;
private String name;
}
반면 위의 기존 자바 코드에서는 Music → Artist 방향의 접근이 가능하지만, Artist → Music 방향의 접근은 불가능하다.
만약 이를 구현하고 싶다면? 이럴 때 '양방향' 연관관계를 구현하면 된다!
public class Music {
@Id @GeneratedValue
@Column(name = "music_id")
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "artist_id")
private Artist artist; // Music → Artist
}
public class Artist {
@Id @GeneratedValue
@Column(name = "artist_id")
private Long id;
private String name;
@OneToMany(mappedBy = "artist")
private List<Music> musics = new ArrayList<>();
}
Artist 클래스 내에 자신이 부른 Music에 접근할 수 있는 리스트를 하나 만든다고 해보자. Music에 접근할 필드가 생겼다.
그렇다면 위 코드처럼 몇 가지 어노테이션을 설정해줘서 양방향 연관관계를 구현할 수 있다.
@OneToMany 어노테이션
✅ Artist 입장에서는 Music에 1 : 多 관계이므로, 이를 어노테이션을 나타내면 'OneToMany' 을 사용하면 된다.
mappedBy 옵션
✅ 접근할 반대편 클래스에 어떤 변수로 걸려 있는지 체크하면 된다!
✅ Music 객체가 artist 객체 인스턴스를 가지고 있는 상황이므로, "어떤 필드에 매핑되었나?" 라고 생각하면 된다!
어노테이션 사용 관련은, 천천히 코드를 읽어보면서 매칭해나가면 어렵지 않다!
🍰 연관관계 사용 시 주의점
#01. 연관관계의 주인은?
객체의 양방향 관계는 서로 다른 방향의 단방향 2개로 풀어낼 수 있다! (A ↔ B) = (A → B) + (B → A) 처럼!
그렇다면 양방향 관계일 때는 누가 연관관계의 주인을 가져가야 할까?
결론부터 말하면, 외래 키(FK)가 있는 곳, 즉 1 : N 관계에서 'N' 의 부분에서 연관관계의 주인을 가지고 있는 것 이 좋다!
예를 들어, Artist (1 : 多) Music 관계에서 Music 필드를 수정한다고 하면, 당연히 Music 필드에 쿼리가 나갈 것이다.
하지만 Artist 필드를 수정한다고 하면, FK는 Music 필드에 있으므로, Music 필드에도 쿼리가 나가야한다.
그렇기 때문에 FK가 있는 쪽(FK가 있으면 당연히 'N' 부분임)에 연관관계의 주인을 가지고 있으라는 것이다!
연관관계의 주인이라면, 주인만이 외래 키를 관리(등록 및 수정) 할 수 있고, 주인이 아니면 조회(읽기)만 가능 하다.
일례로 Music 클래스는 외래 키를 직접 수정할 수 있으나, Artist 클래스는 Music에 대한 조회만 할 수 있다.
#02. 양방향 연관관계 사용 시, 양쪽에 모두 값을 세팅하자!
이렇게 양방향 연관관계를 가지고 있다면, 반드시 양쪽에 모두 값을 세팅할 필요 가 있다.
Flush를 통해 DB에 쿼리를 보내주지 않는다면, 객체에 대한 데이터는 DB에는 아직 없고 영속성 컨텍스트에 들어있다.
Artist artist = new Artist();
artist.setName("ITZY");
Music music = new Music();
music.setTitle("달라달라");
music.setArtist(artist);
// == 현재 영속성 컨텍스트 내 데이터 상태 (id는 임의로 넣겠다) == //
// artist (id: 20L, name: ITZY, musics: NULL)
// music (id:1L, title: 달라달라, artist: artist 객체)
// 여기서 바로 Artist에 musics를 조회한다면?
System.out.println(artist.getMusics()); // output : (없음)
// 만약 DB에 한번 데이터가 담겼었다면?
em.flush(); // DB에 영속성 컨텍스트 반영
em.clear();
System.out.println(artist.getMusics()); // output : music 리스트 조회 가능
DB에 데이터가 반영되지 않은 상태에서 영속성 컨텍스트 내 1차 캐시를 조회하면 artist 내 musics 에는 값이 없다.
하지만 DB에 데이터가 한 번 저장되었다면, 이는 값을 불러올 수 있다. 어떻게 그게 가능하지? 🧐🧐
현재 위 코드만 보면 artist 내 musics 에는 값이 들어있지 않는데, 그 상태에서 바로 DB에 데이터를 넣어보자.
DB의 입장에서 Music 객체와 Artist 객체 사이 관계 를 알고 있기 때문에 어떻게든 값을 찾아올 수는 있다!
FK 방식으로 연결되어 있는 것 을 DB는 알고 있다! 하지만 그 전에는 직접 musics 데이터를 넣지 않았기 때문에 모를 수 밖에..
말이 조금 복잡해졌다 ㅠㅠ 결론은 애초부터 데이터를 양쪽에 다 넣어주는 것이 좋다!
#03. 단방향만으로 설계하는 것이 좋다!
양방향 설계는 단뱡향 2개가 합쳐진 것이라고 했다. 그리고 위에서 말한 주인 문제, 양쪽에 값을 모두 세팅해야하는 문제가 있다.
그래서 양방향보다는 되도록이면 단방향으로 설계를 진행하는 것이 좋다! 그리고 충분히 단방향만으로 설계를 할 수 있다!
일단 단방향 매핑으로 모든 설계를 마무리하고, 그 이후 개발단계에서 필요한 양방향 관계를 정의하는 것으로도 충분하다!
'백엔드 공부' 카테고리의 다른 글
[프로젝트] 요구사항 분석 및 구현 단계 (0) | 2023.02.14 |
---|---|
[스프링 JPA] 연관관계 매핑 (2) (0) | 2023.02.13 |
[스프링 JPA] 엔티티 매핑 (0) | 2023.02.09 |
[스프링 JPA] 영속성 컨텍스트 (0) | 2023.02.08 |
[오브젝트] 객체지향 설계 (0) | 2023.02.05 |