✔️ 사용된 엔티티
1. QuestionBoard
@Getter
@Setter
@Table(name = "question_board")
@ToString(callSuper = true)
@Entity
public class QuestionBoard extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// UsreAccount와 연결 -> JoinColumn을 통해 user_id와 연결
@JoinColumn(name = "user_id")
@ManyToOne
private UserAccount userAccount; // 유저 정보
@Column(nullable = false, name = "title")
private String title; // 제목
@Column(nullable = false, name = "content")
private String content; // 내용
@Enumerated(EnumType.STRING)
private QuestionBoardTag tag; // 태그
@Column(name = "view_count")
private Integer viewCount; // 조회수
// 뒤는 생략
}
2. study group
@Getter
@Setter
@Table(name = "study_group")
@Entity
public class StudyGroup extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "study_group_name")
private String StudyGroupName;
@Column(name = "description")
private String description;
// 생략
}
3. study group board
@Getter
@Setter
@Table(name = "study_group_board")
@Entity
public class StudyGroupBoard extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private UserAccount userAccount;
@ManyToOne
@JoinColumn(name = "study_group_id")
private StudyGroup studyGroup;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "content", nullable = false)
private String content;
// 생략
}
✔️ 발생한 문제
@Test
void 질문_게시글_삭제_성공한_경우() {
// Given
String userId = "userId";
Long questionBoardId = 1L;
QuestionBoard questionBoard = QuestionBoardFixture.get(userId);
UserAccount userAccount = questionBoard.getUserAccount();
// When
when(userAccountRepository.findById(userId)).thenReturn(Optional.of(userAccount));
when(questionBoardRepository.findById(questionBoardId)).thenReturn(Optional.of(questionBoard));
// Then
assertDoesNotThrow(() -> questionBoardService.deleteQuestionBoard(userId, questionBoardId));
}
// 사용된 fixture
public class QuestionBoardFixture {
public static QuestionBoard get(String userId) {
UserAccount userAccount = UserAccount.of(userId, "test", "test", "test", null);
QuestionBoard questionBoard = QuestionBoard.of(userAccount, "title", "content", QuestionBoardTag.Free, 0);
return questionBoard;
}
public static QuestionBoard get(String userId, String title, String content) {
UserAccount userAccount = UserAccount.of(userId, "test", "test", "test", null);
QuestionBoard questionBoard = QuestionBoard.of(userAccount, title, content, QuestionBoardTag.Free, 0);
return questionBoard;
}
}
위 테스트는 통과가 잘되나
// study group
@Test
void 스터디_그룹_삭제가_성공한_경우() {
// Given
String userId = "userId";
StudyGroup studyGroupFixture = StudyGroupFixture.get();
UserStudyGroup userStudyGroupLeaderFixture = UserStudyGroupFixture.getLeader(userId, studyGroupFixture);
UserAccount userAccount = userStudyGroupLeaderFixture.getUserAccount();
// When
when(userAccountRepository.findById(userId))
.thenReturn(Optional.of(userAccount));
when(userStudyGroupRepository.findByUserAccount(userAccount))
.thenReturn(Optional.of(userStudyGroupLeaderFixture));
// Then
assertDoesNotThrow(() -> studyGroupService.deleteStudyGroup(userId));
}
// study group fixture
public class StudyGroupFixture {
public static StudyGroup get() {
return StudyGroup.of("StudyGroupName", "description");
}
public static StudyGroup get(String studyGroupName, String description) {
return StudyGroup.of(studyGroupName, description);
}
}
// study group board
@Test
void 스터디_그룹_게시글_삭제가_성공한_경우() {
// Given
String userId = "userId";
Long studyGroupBoardId = 1L;
StudyGroupBoard studyGroupBoard = StudyGroupBoardFixture
.get(userId, "title", "content");
UserAccount userAccount = studyGroupBoard.getUserAccount();
// When
when(userAccountRepository.findById(userId))
.thenReturn(Optional.of(userAccount));
when(studyGroupBoardRepository.findById(studyGroupBoardId))
.thenReturn(Optional.of(studyGroupBoard));
// Then
assertDoesNotThrow(() -> studyGroupBoardService.deleteStudyGroupBoard(userId, studyGroupBoardId));
}
// study group board fixture
public class StudyGroupBoardFixture {
public static StudyGroupBoard get(String userId, String title, String content) {
UserAccount userAccount = UserAccount.of(userId, "test", "test", "test", null);
StudyGroup studyGroup = StudyGroup.of("test", "test");
return StudyGroupBoard.of(userAccount, studyGroup, title, content);
}
}
위의 study group과 study group board는 오류가 생겨서 무엇이 문제인지 찾아 보았습니다.
🏷️ 엔티티 간 연관 관계 및 영속성 관리
✅ 연관 관계
▪️ StudyGroupBoard는 StudyGroup 및 UserAccount와 @ManyToOne 관계를 가집니다.
▪️ UserStudyGroup도 StudyGroup 및 UserAccount와 @ManyToOne 관계를 가집니다.
이러한 연관 관계에서는 각 엔티티가 데이터베이스에서 영속화될 때 관련된 엔티티가 함께 영속화되지 않으면 문제가 발생할 수 있습니다. 특히 @ManyToOne 관계에서 연관된 엔티티가 null이거나 ID가 없는 상태로 영속성 컨텍스트에 등록되면 Hibernate는 TransientObjectException을 발생시킵니다.
✅ ID 설정 문제
▪️ StudyGroup, StudyGroupBoard, UserStudyGroup의 경우 ID는 @GeneratedValue(strategy = GenerationType.IDENTITY)로 자동 생성됩니다. 따라서 데이터베이스에 저장되기 전까지는 ID 값이 null입니다.
▪️ 그러나, 테스트 중에는 실제로 데이터베이스에 저장하지 않기 때문에, ID가 수동으로 설정되지 않으면 영속성 컨텍스트에서 관리되지 않아 문제가 발생할 수 있습니다.
❗ 왜 일부 테스트에서는 문제가 발생하지 않는가?
- 단순 연관 관계: QuestionBoard의 경우, 연관된 엔티티가 비교적 단순하여 영속성 관리가 간단합니다. 특히 ID가 필요하지 않은 단순한 관계에서는 Hibernate가 자동으로 연관된 객체를 처리할 수 있습니다.
- Mocking: 테스트에서 리포지토리 메서드 (findById 등)를 Mocking하여 사용하기 때문에, 실제 데이터베이스 접근 없이도 연관된 객체를 생성하여 테스트를 진행할 수 있습니다. 그러나 이 경우에도 연관된 엔티티의 ID가 필요하거나 영속성 컨텍스트에 있어야 하는 경우에는 수동으로 ID를 설정해주는 것이 필요할 수 있습니다.
✅ 해결 방법
1. ID 수동 설정:
테스트에서 엔티티를 생성할 때, ID 값을 수동으로 설정해줍니다. 예를 들어, StudyGroup을 생성할 때 ID를 수동으로 설정하여 문제를 해결할 수 있습니다.
StudyGroup studyGroup = StudyGroup.of(1L, "StudyGroupName", "description");
2. Cascade 옵션 사용
StudyGroupBoard와 UserStudyGroup에서 연관된 엔티티가 영속화될 때 자동으로 연관된 객체도 영속화되도록 CascadeType 옵션을 설정할 수 있습니다. 예를 들어, @ManyToOne 관계에서 CascadeType.PERSIST 또는 CascadeType.ALL을 추가할 수 있습니다.
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "study_group_id")
private StudyGroup studyGroup;
3. TestEntityManager 사용
Spring Boot에서 TestEntityManager를 사용하여 테스트 중에 엔티티를 명시적으로 영속화할 수 있습니다. 이를 통해 엔티티 간의 관계를 올바르게 설정하고 테스트할 수 있습니다.
@Autowired
private TestEntityManager entityManager;
@Test
void test() {
StudyGroup studyGroup = StudyGroup.of("StudyGroupName", "description");
entityManager.persistAndFlush(studyGroup); // 영속화
StudyGroupBoard studyGroupBoard = StudyGroupBoard.of(null, userAccount, studyGroup, "title", "content");
entityManager.persistAndFlush(studyGroupBoard); // 영속화
}
🔹요약
StudyGroup, StudyGroupBoard, UserStudyGroup 간의 복잡한 연관 관계에서, ID 값이 없거나 연관된 엔티티가 영속화되지 않았을 때 발생할 수 있는 문제를 방지하기 위해, 테스트 시 ID를 수동으로 설정하거나 Cascade 옵션을 사용하거나 TestEntityManager를 사용하는 것이 중요합니다.
따라서, 위와 같은 방법을 통해 테스트에서 발생하는 TransientObjectException과 같은 영속성 관련 예외를 예방할 수 있습니다.
'코딩 공부 > web & Java' 카테고리의 다른 글
[Spring / Redis] Redis LocalDateTime 역직렬화 오류 (2) | 2024.10.03 |
---|---|
[Spring / Vue] Spring과 Vue를 연동시 주소로 입력하면 오류나는 문제 (1) | 2024.10.03 |
[JWT] JWT 0.12.5 최신화 문제 해결 (0) | 2024.07.20 |
[Java] 팩토리 메소드 (0) | 2024.07.13 |
[JPA] N + 1 문제 (0) | 2024.04.23 |