[ 10주차 - 1023 ]
금일 커리큘럼
├ 09:00 ~ 12:00 backend 프로그래밍 (JUnit JPA Entity 테스트)
└ 13:00 ~ 18:00 backend 프로그래밍 (JUtil과 DAO 구현, 엔티티 매핑 심화)
1. JUnit - JPA Entity 테스트 방식
엔티티 테스트 시 기본 설정
EntityManagerFactory는 프로그램 내 한번만 생성이므로@BeforeAll,@AfterAll에서 생성 및 종료 처리EntityManager,EntityTransaction은 각 테스트 메서드마다 새로 생성해야 하므로@BeforeEach,@AfterEach에서 생성 및 종료 처리
import jakarta.persistence.*;
import org.junit.jupiter.api.*;
public class UserJpaTest {
// beforeAll, afterAll 은 static 메서드여야 하므로,
// static 필드로 선언
private static EntityManagerFactory emf;
private EntityManager em;
private EntityTransaction tx;
@BeforeAll // 전체실행 1번
public static void init() {
emf = Persistence.createEntityManagerFactory("lionPU");
}
@AfterAll // 전체종료 1번
public static void close() {
if(emf != null) emf.close();
}
@BeforeEach
public void setUp() {
if(emf != null) {
em = emf.createEntityManager();
tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
}
}
@AfterEach
public void tearDown() {
// 트랜잭션 커밋없이 마지막까지 돌아가고있으면 롤백
if(tx != null && tx.isActive()) tx.rollback();
if(em != null) em.close();
}
}
Assertions 검증
- Assertions : 테스트 결과가 기대값과 일치하는지 검증하는 기능
Assertions 매개변수 설명
exp(expected): 기대값act(actual): 실제값msg(message): 실패 시 출력할 메시지
Assertions 주요 검증 메서드
| 메서드 | 설명 | 비교 방식 |
|---|---|---|
| assertEquals(exp, act, msg) | 같은지 검증 | 값 비교 (.equals()) |
| assertNotEquals(exp, act, msg) | 다른지 검증 | 값 비교 (.equals()) |
| assertSame(exp, act, msg) | 같은 인스턴스인지 검증 | 참조 비교 (==) |
| assertNotSame(exp, act, msg) | 다른 인스턴스인지 검증 | 참조 비교 (==) |
import jakarta.persistence.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class UserJpaTest {
// ... 필드,메서드 생략 (위의 설정 코드 동일)
@Test
@DisplayName("persist 후 id 값 여부")
void insertUser() {
User user = new User("test01", "test01@test01.com");
em.persist(user);
// tx.commit(); //DB등록 안하고 종료시 롤백넘기기
assertNotNull(user.getId(), "못받았음");
}
@Test
@DisplayName("같은 인스턴스 여부")
void findId() {
User user = new User("test02", "test02@test02.com");
em.persist(user);
tx.commit(); // tx 1 end
tx.begin(); // tx 2 start
User find01 = em.find(User.class, user.getId());
User find02 = em.find(User.class, user.getId());
try {
// 전부다 같은 인스턴스인지?
assertSame(user, find01, "user != find01");
assertSame(find01, find02, "find01 != find02");
assertSame(find02, user, "find02 != user");
} finally {
// 테스트 실패와 관계없이 항상 데이터 정리
em.remove(user);
tx.commit(); // tx 2 end
}
}
}
2. JPA - JUtil과 DAO 구현
JPAUtil 싱글톤 패턴
EMF를 싱글톤 패턴으로 해야하는 이유 ?
- EntityManagerFactory 생성은 비용이 많이 들어서 하나로 관리하는 것이 좋음
- 여러개의 DAO에서 개별 생성시 리소스 낭비됨
싱글톤 패턴시 장점
- 싱글톤 인스턴스로 EntityManagerFactory 관리
- JVM 종료 시점에 EntityManagerFactory 자동 종료 처리
- -> Runtime.getRuntime().addShutdownHook() 사용
JPAUtil 구현
public class JPAUtil {
// 싱글톤 인스턴스
private static final EntityManagerFactory emf =
Persistence.createEntityManagerFactory("lionPU");
// JVM 종료 시 자동 close
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (emf != null) {
System.out.println("---- EntityManagerFactory 종료 ---");
emf.close();
}
}));
}
// 외부 인스턴스 생성 방지
private JPAUtil() {}
// EntityManagerFactory 반환
public static EntityManagerFactory getEntityManagerFactory() {
return emf;
}
}
UserDAO 구현
@Slf4j
public class UserDAO {
public UserDAO() {}
// insert (return id)
public Long addUser(User user) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
em.persist(user);
tx.commit();
log.info("addUser 성공: id={}", user.getId());
return user.getId();
} catch (RuntimeException e) {
if(tx.isActive()) tx.rollback();
log.error("addUser 실패: {}", e.getMessage());
throw e; // 호출자한테 넘김
} finally {
em.close();
}
}
// select
public User findUserById(Long id) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
try {
return em.find(User.class, id);
} finally {
em.close();
}
}
// update
public boolean updateUser(Long id, String name, String email) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
boolean result = false;
try {
tx.begin();
User user = em.find(User.class, id);
if (user != null) {
user.setName(name);
user.setEmail(email);
log.info("updateUser 성공: id={}", id);
result = true;
} else {
log.warn("updateUser 대상없음: id={}", id);
}
tx.commit();
return result;
} catch (RuntimeException e) {
if(tx.isActive()) tx.rollback();
log.error("updateUser 실패: {}", e.getMessage());
throw e;
} finally {
em.close();
}
}
// delete
public boolean deleteUser(Long id) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
boolean result = false;
try {
tx.begin();
User user = em.find(User.class, id);
if (user != null) {
em.remove(user);
log.info("deleteUser 성공: id={}", id);
result = true;
} else {
log.warn("deleteUser 대상없음: id={}", id);
}
tx.commit();
return result;
} catch (RuntimeException e) {
if(tx.isActive()) tx.rollback();
log.error("deleteUser 실패: {}", e.getMessage());
throw e;
} finally {
em.close();
}
}
}
해당 DAO 사용예시
import org.example.jpaexam.User;
public class UserDAORun {
private static UserDAO userDAO = new UserDAO();
public static void main(String[] args) {
// 사용자 생성
User user = new User("홍길동", "hong@example.com");
Long userId = userDAO.addUser(user);
// 사용자 조회
User foundUser = userDAO.findUserById(userId);
System.out.println("조회확인: " + foundUser);
// 사용자 수정
userDAO.updateUser(userId, "김길동", "kim@example.com");
// 사용자 삭제
userDAO.deleteUser(userId);
}
}
# 실행 결과
Hibernate: # ... // insert
2025-10-23 13:25:45 [main] INFO org.example.jpaexam._1023.UserDAO - addUser 성공: id=13
Hibernate: # ... // select
조회확인: User(id=13, name=홍길동, email=hong@example.com)
Hibernate: # ... // select
2025-10-23 13:25:46 [main] INFO org.example.jpaexam._1023.UserDAO - updateUser 성공: id=13
Hibernate: # ... // update
Hibernate: # ... // select
2025-10-23 13:25:46 [main] INFO org.example.jpaexam._1023.UserDAO - deleteUser 성공: id=13
Hibernate: # ... // delete
---- EntityManagerFactory 종료 ---
3. 엔티티 매핑 심화
기존에 쓰던 엔티티 매핑 외에 다양한 매핑이 존재
기존 매핑 정리
@Entity: JPA 엔티티 지정@Table: 엔티티와 테이블 매핑@Id: 기본 키 매핑@GeneratedValue: 기본 키 자동 생성 전략 매핑@Column: 필드와 컬럼 매핑
연관관계 매핑
- 연관관계 매핑이란 ? 두 개 이상의 엔티티 간의 관계를 정의하는 것을 의미한다.
- 1:1 (One-to-One), 1:N (One-to-Many), N:1 (Many-to-One), N:N (Many-to-Many) 관계를 나타냄.
| 관계 유형 | 예시 |
|---|---|
| 1:1 (One-to-One) | 사용자(User) ↔ 프로필(Profile) |
| 1:N (One-to-Many) | 게시글(Post) ↔ 댓글(Comments) |
| N:1 (Many-to-One) | 댓글(Comments) ↔ 게시글(Post) |
| N:N (Many-to-Many) | 학생(Students) ↔ 강의(Courses) |
연관관계 매핑 어노테이션
| 어노테이션 | 설명 |
|---|---|
@OneToOne |
1:1 관계 매핑 |
@OneToMany |
1:N 관계 매핑 |
@ManyToOne |
N:1 관계 매핑 |
@ManyToMany |
N:N 관계 매핑 |
- 그 외 연관 어노테이션
@JoinColumn: 외래 키 매핑@JoinTable: 조인 테이블 매핑 (N:N 관계에서 주로 사용)
연관관계 매핑 어노테이션 옵션
@OneToMany(옵션 = ..)
- cascade : 부모 엔티티 -> 자식 엔티티 전파 설정
CascadeType.PERSIST: 부모저장 시 자식도 함께 저장CascadeType.MERGE: 부모 update 시 자식도 함께 updateCascadeType.REMOVE: 부모 remove 시 자식도 함께 removeCascadeType.REFRESH: 부모 find 시 자식도 함께 findCascadeType.DETACH: 부모 준영속 시 자식도 함께 준영속CascadeType.ALL: 모든 옵션 적용
- fetch : 연관된 엔티티를 조회할 때의 로딩 전략 설정
FetchType.EAGER: 즉시 로딩 (부모 조회시 자식도 함께 조회)FetchType.LAZY: 지연 로딩 (자기만 조회, 필요시 자식 조회)
- mappedBy : 양방향 연관관계에서 주인 엔티티 지정
- 예: post <- comments (1:N) 관계에서 post 엔티티가 주인일 때,
mappedBy = "post"로 설정
- orphanRemoval : 고아 객체 제거 설정
= true: 부모 엔티티에서 참조가 제거된 자식 엔티티를 자동으로 삭제= false: 비활성화
연관관계 엔티티 (학교 <-> 학생들 1:N 관계)
// school 엔티티 - 부모
@Entity
@Getter
@Setter
@ToString(of= {"id", "name"})
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "schools")
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 1:N 학교 ↔ 학생들 관계
@OneToMany(
mappedBy = "school", // 연관관계의 주인이 아님을 지정
cascade = CascadeType.ALL, // 관련 엔티티의 생명주기를 함께 관리
orphanRemoval = true, // 삭제시 참조한 자식 엔티티도 삭제
fetch = FetchType.EAGER
// EAGER: 즉시로딩(연관 엔티티 조인 조회), LAZY: 지연로딩(자기만 조회)
)
private List<Student> students = new ArrayList<>();
public School(String name) {
this.name = name;
}
}
// student 엔티티 - 자식
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// N:1 학생들 ↔ 학교 관계
@ManyToOne(fetch = FetchType.LAZY) // LAZY: 지연로딩 (자기만 조회)
@JoinColumn(name = "school_id") // 외래키 매핑
private School school;
public Student(String name) {
this.name = name;
}
public Student(String name, School school) {
this.name = name;
this.school = school;
}
}
테스트 코드
@Slf4j
public class SchoolMain {
private static void find(Long id) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
try {
School school = em.find(School.class, id);
log.info("학교: {}", school.getName());
System.out.println("-".repeat(40));
for (Student student : school.getStudents()) {
log.info("[find] 학교:{{}} 인 학생: {}", school.getName(), student.getName());
}
System.out.println("-".repeat(40));
//id가 1L인 학생을 조회하고 싶다.
log.info("[find] id가 1L인 학생");
Student student = em.find(Student.class, 1L);
log.info("[find] 학생명: {}", student.getName());
log.info("[find] 학교명: {}", student.getSchool().getName());
log.info("[find] 학교id: {}", student.getSchool().getId());
} finally {
em.close();
}
}
private static Long create() {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
School school = new School("newSchool-A");
Student stuA = new Student("stu-A", school);
Student stuB = new Student("stu-B", school);
school.getStudents().add(stuA);
school.getStudents().add(stuB);
em.persist(school);
log.info("[create] 학교 : {}", school);
for (Student student : school.getStudents()) {
log.info("[create] 학교:{{}} 인 학생: {}", school.getName(), student.getName());
}
tx.commit();
return school.getId();
} catch (RuntimeException e) {
tx.rollback();
throw e;
} finally {
em.close();
}
}
private static void update(Long id) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
School school = em.find(School.class, id);
school.setName("update-A");
log.info("[update] id={} 인 학교 : {}", id, school);
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
} finally {
em.close();
}
}
private static void delete(Long id) {
EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
School school = em.find(School.class, id);
em.remove(school);
log.info("[remove] id={} 인 학교 : {}", id, school);
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
} finally {
em.close();
}
}
public static void main(String[] args) {
find(1L);
Long insertId = create();
if(insertId != null) {
update(insertId);
delete(insertId);
}
}
}
# 실행 결과
# select 실행
Hibernate: # select - left join (id)
localTime [main] INFO package - 학교: Greenwood High School
----------------------------------------
localTime [main] INFO package - [find] 학교:{Greenwood High School} 인 학생: Alice
localTime [main] INFO package - [find] 학교:{Greenwood High School} 인 학생: Bob
localTime [main] INFO package - [find] 학교:{Greenwood High School} 인 학생: Charlie
----------------------------------------
localTime [main] INFO package - [find] id가 1L인 학생
localTime [main] INFO package - [find] 학생명: Alice
localTime [main] INFO package - [find] 학교명: Greenwood High School
localTime [main] INFO package - [find] 학교id: 1
# insert 실행
Hibernate: # insert - school (name)
Hibernate: # insert - student (name, school_id)
Hibernate: # insert - student (name, school_id)
localTime [main] INFO package - [create] 학교 : School(id=6, name=newSchool-A)
localTime [main] INFO package - [create] 학교:{newSchool-A} 인 학생: stu-A
localTime [main] INFO package - [create] 학교:{newSchool-A} 인 학생: stu-B
# update 실행
Hibernate: # select - left join (id)
localTime [main] INFO package - [update] id=6 인 학교 : School(id=6, name=update-A)
Hibernate: # update - school (name)
# delete 실행
Hibernate: # select - left join (id)
localTime [main] INFO package - [remove] id=6 인 학교 : School(id=6, name=update-A)
Hibernate: # delete - student (id) ...
---- EntityManagerFactory 종료 ---