2021. 10. 11. 14:04ㆍJAVA/Spring
이전글
https://yonghwankim-dev.tistory.com/139
SpringBoot #5 다양한 연관관계 처리 #1 연관관계 처리 순서와 사전 설계
본 글은 스타트 스프링 부트 도서의 내용을 복습하기 위해 작성된 글입니다. 개요 관계형 데이터베이스를 설계할때 하나의 테이블로 처리되는 테이블은 거의 없습니다. 대부분의 경우 PK와 FK를
yonghwankim-dev.tistory.com
본 글은 스타트 스프링 부트 도서의 내용을 복습하기 위해 작성된 글입니다.
개요
데이터베이스 상의 설계로 보면 회원과 회원의 프로필은 전형적인 '일대일' 혹은 '일대다'의 관계로 설정될 수 있습니다. 예제에서는 회원이 과거의 회원 프로필들을 보관한다고 가정하고, 일대다의 관계로 설정하도록 하겠습니다.
위 그림을 보면 한 명의 회원이 여러 프로필(사진)을 가지고 있습니다. 그중에서 하나의 사진을 현재 자신의 프로필로 이용하는 경우에는 현재여부 칼럼의 값이 true로 지정됩니다.
본 글에서는 위와 같이 회원과 프로필의 일대다 관계에서 단방향 처리하는 것을 실습합니다.
- 객체 간 연관관계 설정
- 단방향, 양방향 관계의 이해 (현재)
- JPQL을 이용한 @Query 처리와 Fetch JOIN(스프링 부트 2.0.0)
1. 예제 프로젝트 생성
Spring Data JPA를 이용할 때 개발 순서는 다음과 같습니다.
- 각 엔티티 클래스 설계
- 각 엔티티 간의 연관관계 파악 및 설정
- 단방향, 양방향 설정
File->New->Spring Starter Project
Project 명 : boot04
Packaging : War
Spring Boot Version : 2.0.0 이상
application.properties 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jpa_ex?useSSL=false
spring.datasource.username=jpa_user
spring.datasource.password=mysql_jpa_user
#스키마 생성(create)
#spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.ddl-auto=update
#DDL 생성시 데이터베이스 고유의 기능을 사용하는가?
spring.jpa.generate-ddl=false
#실행되는 SQL문을 보여줄 것인가?
spring.jpa.show-sql=true
#데이터베이스는 무엇을 사용하는가?
spring.jpa.database=mysql
#로그 레벨
logging.level.org.hibernate=info
#MySQL 상세 지정
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
2. 각각의 엔티티 클래스 설계
org.zerock.domain.Member
@Getter
@Setter
@ToString
@Entity
@Table(name="tbl_members")
@EqualsAndHashCode(of="uid")
public class Member {
@Id
private String uid;
private String upw;
private String uname;
}
org.zerock.domain.Profile
@Getter
@Setter
@ToString
@Entity
@Table(name="tbl_profile")
@EqualsAndHashCode(of="fno")
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long fno;
private String fname;
private boolean current;
}
3. 연관관계의 설정과 단방향/양방향
위의 회원과 프로필의 관계는 일대다입니다. 반대로 '프로필 사진과 회원의 경우는 다대일'의 관계라는 것을 파악할 수 있습니다. 관계형 데이터베이스에서는 단순히 하나의 FK를 이용해서 지정되는 상황이 JPA를 이용하는 복잡해진 상황을 볼 수 있습니다. 이러한 관게에서 판단해야 하는 것은 다음과 같습니다.
- '회원'에서 '프로필'로의 접근만을 사용하는가?
- '프로필'을 통해서 '회원'정보를 조회할 필요가 있는가?
위의 질의를 다시 말하면 'Member' 클래스에만 Profile 타입의 인스턴스 변수를 추가하는가? 아니면 Profile 클래스에도 Member 타입의 인스턴스 변수를 추가할 것인가?'를 결정하는 것입니다.
회원과 프로필의 관계를 보면 프로필 테이블에 회원 정보가 입력되어야 하므로, 가장 쉽게 생각할 수 있는 구조는 Profile 클래스에 Member 클래스 타입을 인스턴스 변수로 설계하는 것입니다.
org.zerock.domain.Profile
@Getter
@Setter
@ToString(exclude = "member")
@Entity
@Table(name="tbl_profile")
@EqualsAndHashCode(of="fno")
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long fno;
private String fname;
private boolean current;
private Member member;
}
위의 코드는 이전 코드와 달리 필드멤버에 Member 타입의 필드 멤버를 추가하였고 @ToString 애너테이션에 member 필드 멤버를 제외시킨다고 설정하였습니다. 제외시킨 이유는 Lombok을 이용해서 toString() 생성할때 양방향으로 참조를 설정하면 문제가 생기기 때문입니다.
Member 클래스와 Profile 클래스의 관계
3.1 JPA의 연관관계 어노테이션 처리
Member와 Profile의 관계는 '일대다'이지만 Profile과 Member의 관계는 '다대일'입니다. 따라서 Profile 쪽에서 다음과 같이 내용을 추가함으로써 설정을 합니다.
org.zerock.domain.Profile
@Getter
@Setter
@ToString(exclude = "member")
@Entity
@Table(name="tbl_profile")
@EqualsAndHashCode(of="fno")
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long fno;
private String fname;
private boolean current;
@ManyToOne
private Member member;
}
3.2 생성된 테이블 구조 확인
boot04Application.java를 실행하여 테이블 생성을 확인합니다.
4. Repository 작성
연관관계 설정에서 고민해야 하는 내용은 'Repository를 몇개나 생성해야 하는가?' 입니다. 엔티티 클래스마다 Repository를 설계할 수 있지만 연관관계 설정에 따라서는 Repository를 설계할 필요가 없을 수도 있습니다.
위 예제에서 Member와 Profile은 Member를 처리하는 Repository를 생성해서 회원 데이터를 처리하는 것이 명확합니다. 반면에 Profile을 저장할 때는 Member 객체를 통해서 Profile을 처리할 수 없기 때문에 Profile을 처리하는 Repository를 설계하는 것이 보편적입니다.
org.zerock.persistence.MemberRepository
public interface MemberRepository extends CrudRepository<Member, String>{
}
org.zerock.persistence.ProfileRepository
public interface ProfileRepository extends CrudRepository<Profile, Long>{
}
5. 테스트를 통한 검증
src/test/java/org.zerock.ProfileTests 생성
@SpringBootTest
@Log
@Commit // 테스트 결과 commit
class ProfileTests {
@Autowired
MemberRepository memberRepo;
@Autowired
ProfileRepository profileRepo;
}
5.1 더미 회원 데이터의 추가
회원 데이터에 대한 더미 데이터를 추가합니다.
org.zerock.ProfileTests
// 100명의 사용자 생성
@Test
public void testInsertMembers() {
IntStream.range(1, 101).forEach(i->{
Member member = new Member();
member.setUid("user"+i);
member.setUpw("pw"+i);
member.setUname("사용자"+i);
memberRepo.save(member);
});
}
5.2 특정 회원의 프로필 데이터 처리
MemberRepository를 이용해서 실제 Member 객체를 가져와서 처리할 수 있지만 중요한 것은 Member의 식별자인 uid 속성 뿐이므로 Member 객체를 잠시 생성해서 uid 만을 지정하는 방식이 더 효율적입니다. 아래 예제는 user1의 프로필 사진 5장을 생성합니다.
org.zerock.ProfileTests
// user1이 사용하는 프로필 사진 5장 생성
@Test
public void testInsertProfile() {
Member member = new Member();
member.setUid("user1");
for(int i=1;i<5;i++)
{
Profile profile1 = new Profile();
profile1.setFname("face"+i+".jpg");
if(i==1)
{
profile1.setCurrent(true);
}
profile1.setMember(member);
profileRepo.save(profile1);
}
}
6. 단방향의 문제와 Fetch Join
Member의 인스턴스는 Profile 인스턴스와 아무런 관게가 없이 설정되어 있기 때문에 단순 CRUD 작업을 하기에 편리하지만 실제로는 여러 문제가 발생할 수 있습니다. 대표적인 것은 '회원 정보를 조회하면서 회원의 현재 프로필 사진도 같이 보여라'와 같은 요구사항이 존재할 수 있습니다. 회원에 대한 정보는 MemberRepository의 findById()를 이용해서 처리할 수 있지만 회원의 프로필 사진은 Member 인스턴스에서 알 수 없기 때문에 별도의 처리가 필요합니다.
단방향 문제의 해결책
- JPA에서 'Fetch Join'이라는 기법을 통하여 SQL에서 조인을 처리하는 것과 같은 유사한 작업을 처리
6.1 JPA의 Join 처리
스프링 부트 2.0이상에서는 Hibernate 5.2.x에서는 참조관계가 없어도 'ON' 구문을 이용해서 LEFT OUTER JOIN을 처리 가능합니다. 예를 들어 'uid가 'user1'인 회원의 정보와 더불어 회원의 프로필 사진 숫자를 알고 싶다'라는 요구상이 존재한다고 가정합니다. 이에 대한 순수 SQL은 다음과 같은 외부 조인을 이용할 수 있습니다.
select member.uid, count(fname)
from
tbl_members member LEFT OUTER JOIN tbl_profile profile
ON member.uid = profile.member_uid
where member.uid = 'user1'
group by member.uid
위의 그림과 같은 순수 외부조인 SQL문을 @Query 애노테이션에 적용하면 다음과 같이 작성할 수 있습니다.
public interface MemberRepository extends CrudRepository<Member, String>{
// 'uid가 특정한 회원의 정보와 더불어 회원의 프로필 사진 숫자를 출력'
@Query("SELECT m.uid, count(p) FROM Member m LEFT OUTER JOIN Profile p "
+ " ON m.uid = p.member WHERE m.uid = ?1 GROUP BY m")
public List<Object[]> getMemberWithProfileCount(String uid);
}
@Query 안의 JPQL의 경우 SQL과 유사하지만, 테이블 대신에 엔티티 클래스를 이용하는 것이 큰 차이입니다. 메소드의 리턴 타입은 List<Object[]>로 처리됩니다. JPQL에서는 엔티티 타입뿐 아니라 다른 자료형들도 반환할 수 있기 때문에 List는 결과의 Row 수를 의마하고, Object[]는 칼럼을 의미합니다.
org.zerock.ProfileTests 일부
// user1의 회원ID와 user1이 사용하는 프로필 사진 개수 출력
@Test
public void testFetchJoint1() {
List<Object[]> result = memberRepo.getMemberWithProfileCount("user1");
result.forEach(arr -> System.out.println(Arrays.toString(arr)));
}
'회원 정보와 현재 사용 중인 프로필에 대한 정보'를 출력하시오.
org.zerock.persistence.MemberRepository
public interface MemberRepository extends CrudRepository<Member, String>{
// '회원 정보와 현재 사용중인 프로필에 대한 정보를 출력'
@Query("SELECT m, p FROM Member m LEFT OUTER JOIN Profile p "
+ "ON m.uid = p.member WHERE m.uid = ?1 AND p.current = true")
public List<Object[]> getMemberWithProfile(String uid);
}
// user1의 정보 및 현재 사용하는 프로필 사진 정보 출력
@Test
public void testFetchJoint2() {
List<Object[]> result = memberRepo.getMemberWithProfile("user1");
result.forEach(arr -> System.out.println(Arrays.toString(arr)));
}
References
스타트 스프링 부트, 구멍가게코딩단 지음
'JAVA > Spring' 카테고리의 다른 글
SpringBoot #5 다양한 연관관계 처리 #4 양방향 처리 (0) | 2021.10.12 |
---|---|
SpringBoot #5 다양한 연관관계 처리 #3 단방향 처리2 (0) | 2021.10.11 |
SpringBoot #5 다양한 연관관계 처리 #1 연관관계 처리 순서와 사전 설계 (0) | 2021.10.11 |
Spring Data JPA #4 단순 게시글 처리 #4 Querydsl을 이용한 동적 SQL의 처리 (0) | 2021.10.06 |
Spring Data JPA #4 단순 게시글 처리 #3 @Query 애노테이션 이용하기 (0) | 2021.10.06 |