스프링내맘대로정리하기 1-2편 (환경 구성, 연관 관계)
서론
Spring JPA를 학습하면서 궁금하거나 잘 알지 못했던 부분들을 정리하려고 합니다.
1편
application.properties vs application.yml
Spring Boot는 크게 application.properties , application.yml 와 같은 파일 형식으로 환경 설정 파일을 구성할 수 있습니다.
결론
application.properties는 Key-Value 형식으로 직관적이고 간단하기 때문에 간단한 설정에 용이하며
application.yml은 YAML 형식으로 들여쓰기를 통해 계층적인 구조와 중복되는 속성을 표현하기에 용이하다는 특징이 있습니다.
application.properties
Key-Value 형식을 사용합니다.
간단하고 직관적인 구조기 때문에 작성 문법을 배울 필요 없이 간단한 프로젝트인 경우 환경 설정하기 편하다는 장점을 가지고 있습니다.
하지만 한 줄에 하나의 설정 값을 작성하기 때문에 계층적인 구조를 표현하기 어려우며 중복 속성을 여러 번 작성해야 된다는 단점을 가지고 있습니다.
# application.properties
server.port=8080
spring.datasource.url=jdbc:h2:mem:testdb
application.yml
YAML (YAML Ain't Markup Language) 형식을 사용합니다. 들여쓰기 방식을 통해 계층 구조를 표현합니다.
들여쓰기를 통해 계층적인 구조로 설정을 값을 작성하므로 구조를 쉽게 파악할 수 있으며 중복되는 속성을 효율적으로 작성할 수 있다는 장점이 있습니다.
하지만 들여쓰기에 주의해야 하고, YAML의 문법을 학습해야 합니다.
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:testdb
2편
연관 관계 정리
테이블 연관 관계와 객체 연관 관계
관계형 데이터베이스에서는 외래 키 하나로 두 테이블을 연관 관계를 관리하며, 외래 키는 항상 한쪽 테이블에만 존재합니다. 외래 키 하나로 양쪽 테이블에서 데이터를 쉽게 조회할 수 있습니다.
그렇다면 테이블을 객체로 매핑하는 JPA에서는 어떻게 테이블 간의 연관 관계를 설정할 수 있을까요?
객체의 참조를 통해서 테이블 간의 연관 관계를 설정할 수 있습니다.
연관 관계의 방향
연관 관계에는 단방향 연관 관계와 양방향 연관 관계가 존재합니다.
엔티티를 단 방향으로 매핑하는 경우에는 객체의 참조를 한 곳에서만 사용하기 때문에 외래 키를 두는 엔티티에서만 외래 키를 관리하면 되며 이를 단방향 연관 관계라고 합니다.
두 엔티티에서 해당 외래 키에 대해서 참조를 하고 싶은 경우에는 양방향 연관관계로 설정하여 양쪽에 외래 키를 관리하도록 하면 됩니다.
즉 양방향 연관 관계를 설정하게 된다면 외래 키는 하나 이지만 객체의 참조는 둘이 됩니다. 엄밀히 말하자면 양방향 연관 관계는 특별한 무언가가 아닌 서로 다른 단반향 연관 관계 2개를 엮어서 양방향 인 것처럼 보이게 할 뿐입니다.
두 엔티티가 서로를 참조하면서 데이터의 일관성과 무결성을 유지하기 위해서 외래 키를 관리할(등록, 수정, 삭제) 연관 관계 주인을 설정해주는 것이 중요합니다. 연관 관계의 주인이 아닌 엔티티에서는 읽기만 할 수 있습니다.
연관 관계의 주인
연관 관계의 주인을 정한다는 것은 외래 키를 관리할 엔티티를 선택하는 것입니다.
주인은 항상 외래 키가 있는 곳으로
간단한 팁으로, 1대 다 관계에서는 '다' 쪽을 연관 관계의 주인으로 사용하면 됩니다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 됩니다.
- 주인은 mappedBy 속성을 사용하지 않습니다.
- 주인이 아니라면 mappedBy 속성을 사용하여 속성의 값으로 연관관계의 주인을 지정해야 합니다.
- mappedBy를 사용하는 경우, 반대쪽 매핑의 필드 이름을 값으로 주면 됩니다.
연관 관계
JPA에서 연관 관계는 크게 3가지로 분류 할 수 있습니다. 다만 조심해야 될 것은 관계는 크게 3가지 분류이지만, 단방향 연관 관계로도 양방향 관계로도 설정할 수 있다는 것입니다.
- 일대일 관계 (@OneToOne)
한 엔티티가 다른 엔티티와 1:1로 매핑되는 관계입니다.
다음 예시는 일대일 관계를 외래 키를 Member 엔티티에서 관리하는 단뱡향 연관 관계로 매핑한 것입니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "address_id") // 외래 키 매핑
private Address address;
// getters and setters
}
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String street;
private String city;
// getters and setters
}
- 일대다, 다대일 관계 (@ManyToOne, @OneToMany)
일대다 관계는 하나의 엔티티가 여러 개의 엔티티를 참조하는 관계입니다.
다음은 하나의 Team이 여러 Member를 참조하는 예시입니다.
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
private String name;
}
다대일 관계는 여러 개의 엔티티가 하나의 엔티티를 참조하는 관계입니다.
다음 예시는 여러 Member가 하나의 Team을 참조하는 경우입니다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
private String name;
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
- 다대다 관계 (@ManyToMany)
다대다 관계는 여러 엔티티가 다른 여러 엔티티와 매핑되는 관계입니다. 관계형 데이터베이스에서 N:N 관계를 1:N 관계와 N:1 관계 사이에 중간 테이블에 외래 키를 두어 해소하는 것처럼, 다대다 관계인 경우 직접 다대다 관계로 설정하는 것은 지양하고 1:N, N:1 관계로 해소하는 것이 중요합니다.
다음 예시는 다대다 관계를 매핑합니다.
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_subject",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "subject_id")
)
private List<Subject> subjects = new ArrayList<>();
// getters and setters
}
@Entity
public class Subject {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "subjects")
private List<Student> students = new ArrayList<>();
// getters and setters
}
Fetch Type
연관 관계를 설정할 때 Fetch Type을 지정할 수 있습니다. Fetch Type은 연관된 엔티티의 로드 시점을 결정합니다.
결론부터 말하자면 JPA를 사용하면서 겪는 성능 상의 문제는 대부분 지연 로딩을 사용하는 것으로 해결할 수 있습니다.
- EAGER: 즉시 로딩. 연관된 엔티티를 즉시 함께 로드합니다.
- LAZY: 지연 로딩. 연관된 엔티티를 실제로 사용할 때 로드합니다
연관 관계에서 값은 어디에 저장해야 할까요?
정말 연관 관계의 주인에만 값을 저장하고, 주인이 아닌 곳에는 값을 저장하지 않아도 될까요?
객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전합니다.
public void 양방향_매핑_예시() {
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);
}
연관 관계 편의 메소드의 필요성
양방향 연관관계는 결국 양쪽 모두를 신경 써야 합니다.
예를 들어, member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향 관계가 깨질 수 있습니다.
따라서 양방향 관계에서는 두 코드를 한번에 호출하는 연관 관계 편의 메소드를 통해 안전하게 관리할 수 있습니다.
연관 관계 메소드는 연관 관계의 주인인 엔티티에서 작성해주는 것이 좋습니다.
public class Member {
private Team team;
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team =team;
team.getMembers().add(this);
}
}
간단한 3줄 요약
- 단방향 매핑만으로 테이블과 객체의 연관 관계 매핑은 이미 완료되어 있습니다.
- 단방향을 양방향으로 만들면 반대 방향으로 객체 그래프 탐색 기능이 추가됩니다.
- 양방향 연관 관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 합니다. 연관 관계 메소드를 통해 안전하게 관리할 수 있습니다.