이전 글 - [Spring Boot] JPA (1) - 환경셋팅 / Entity 클래스 만들기
이전에 Entity클래스까지 만들었다.
이번에는 JPA Repository를 활용해서 CRUD 기능을 구현해 보려한다.
Repository 인터페이스는 JpaRepository<Your_Entity, PK_Type>를 상속받아서 만들 수 있다.
//UserRepository.java
public interface UserRepository extends JpaRepository<User, Integer>{
//추가적인 메소드를 만들 수 있다.
}
위의 코드처럼 구성하게 되면 JPA는 자동으로 해당 인터페이스의 구현체를 생성하고 어플리케이션 컨텍스트에 빈으로 등록하게 됩니다.
일반적으로 이 부분이 궁금할 수도 있다. 나도 처음 공부를 진행 할 때, @Repository를 남발했었다.
하지만 DOCS를 읽어보면, @Repository는 인터페이스가 아닌 구현체에 적어줘야한다. 그러나 JpaRepository를 상속받게 되면 자동으로 구현체가 만들어지기 때문에 @Repository 가 불필요하다.
아래에 소개되는 CustomRepository를 만들 때, @Repository를 명시적으로 쓰는 것을 확인할 수 있다.
//CustomRepository.java
public interface CustomRepository{
//메소드를 만들어준다.
List<User> findUsers;
}
//CustomRepositoryImpl.java
@Repository
public class CustomRepositoryImpl implements CustomRepository{
@PersistenceContext
private EntityManager em;
@Override
public List<User> findUsers(){
return em.createQuery("select * from USERS",User.Class).getResultList();
}
}
@Repository를 인터페이스의 구현체에 적어주면 된다. 이 때 EntityManager를 통해서 데이터베이스와 상호작용 할 수 있다.
그리고 @Repository를 사용하면 Bean으로 자동 등록된다. 만약 수동으로 Bean등록을 하고 싶으면 아래와 같이 Configuration을 추가해주면 된다.
//JpaConfig.java
@Configuration
public class JpaConfig{
//bean은 한번만 호출된고, bean 저장소에이있는 것을 지속 참조한다.
@Bean
public CustomRepository customRepositoryConfig(){
return CustomRepositoryImpl();
}
}
커스텀을 만들게 되면, 만들어지는 요소가 많아지고 유지보수 측면에서 불리할 수 있다.
그렇기 때문에 조금 더 편리하게 할 수 있도록 지금부터는 JpaRepository를 상속받아서 사용하는 형태로 진행할 것이다.
커스텀을 하는 방법을 굳이 정리한 이유는 어떤 동작이 일어나는지 알지 못하면, 실수가 많이 발생 할 수 있기 때문에 한번 짚고 넘어가는 것이다. 얼마나 편리한 기능을 사용하고 있는지 알기 위함도 있다.
@Entity
@Table(name = "USERS", indexes = {
@Index(name="idx_userid_username", columnList = "u_id, u_name", unique= true),
@Index(name="idx_username", columnList="u_name")
})
class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
@Column(name = "u_id", nullable = false, unique = true, length = 255)
String userId;
@Column(name = "u_pw", nullable = false, length = 255)
String userPw;
@Column(name = "u_name", nullable = false, length = 255)
String userName;
}
@Entity
@Table(name = "USERS", indexes = {
@Index(name="idx_userid_username", columnList = "u_id, u_name", unique= true),
@Index(name="idx_username", columnList="u_name")
})
@Getter
@NoArgsConstructor
class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
@Setter
@Column(name = "u_id", nullable = false, unique = true, length = 255)
String userId;
@Setter
@Column(name = "u_pw", nullable = false, length = 255)
String userPw;
@Setter
@Column(name = "u_name", nullable = false, length = 255)
String userName;
public User(String userId, String userPw, String userName) {
this.userId = userId;
this.userPw = userPw;
this.userName = userName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
id가 같으면 동일한 객체로 취급
참조하는 데이터를 최소화 시킴
위의 형태로 CRUD 기능을 위한 Entity 수정이 완료되었다.
JpaRepository를 상속받으면, Jpa가 메소드이름을 통해 자동적으로 쿼리를 생성한다.
테스트를 위해서 아래의 함수원형을 UserRepository 인터페이스내에 추가해줄 것이다.
Optional<User> findByUserId(String userId);
Jpa 상에서 자동으로 구현체를 만들어주기 때문에 메소드 이름을 규칙에 따라서 잘 지어주면 된다.
우리는 현재 컨트롤러를 따로 구성하여 만들지 않았다. 그렇기에 테스트케이스를 만들어서 CRUD 기능에 대한 동작이 잘 진행되는지 확인해 볼 것이다.
dependencies{
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.21.0'
}
aassert를 활용한 테스트를 진행하기 위한 의존성을 추가해준다.
//UserRepositoryTest.java
@SpringBootTest
@Transactional
public class UserRepositoryTest {
@Autowired
UserRepository userRepository;
private List<User> dummyUsers;
@BeforeEach
public void setUp() {
// 각 테스트 메서드에서 독립적으로 사용할 데이터 초기화
dummyUsers = List.of(
new User("k", "1234", "k"),
new User("k12", "1234", "kk"),
new User("k32", "1234", "kkk"),
new User("k442", "1234", "kkkk"),
new User("k552", "1234", "kkkkk")
);
}
@Test
@DisplayName("Create User Test")
public void createUserTest() {
User newUser = new User("kang", "1234", "k");
userRepository.saveAndFlush(newUser);
Optional<User> foundUser = userRepository.findById(newUser.getId());
assertThat(foundUser).isPresent();
assertThat(foundUser.get()).isEqualTo(newUser);
}
@Test
@DisplayName("Select All Users Test")
public void selectAllUsersTest() {
dummyUsers.forEach(user -> userRepository.saveAndFlush(user));
List<User> foundAllUser = userRepository.findAll();
assertThat(foundAllUser).isNotNull();
assertThat(dummyUsers.size()).isEqualTo(foundAllUser.size());
foundAllUser.forEach(user ->
assertThat(dummyUsers.contains(user)).isTrue()
);
}
@Test
@DisplayName("Find User By UserId Test")
public void selectUserByUserIdTest() {
dummyUsers.forEach(user -> userRepository.saveAndFlush(user));
Optional<User> foundUser = userRepository.findByUserId("k");
Optional<User> foundUser2 = userRepository.findByUserId("b");
assertThat(foundUser2).isEmpty();
assertThat(foundUser).isNotEmpty();
assertThat(foundUser.get()).isEqualTo(dummyUsers.get(0));
}
@Test
@DisplayName("Update User By UserId Test")
public void updateUserByUserIdTest() {
// 사용자 저장
userRepository.saveAndFlush(dummyUsers.get(0));
// 사용자 조회
Optional<User> foundUser = userRepository.findByUserId("k");
// 사용자가 존재하는지 확인
assertThat(foundUser).isNotEmpty();
User userToUpdate = foundUser.get();
userToUpdate.setUserId("updatedUserId");
userRepository.saveAndFlush(userToUpdate);
// 다시 사용자 조회
Optional<User> updatedUser = userRepository.findById(userToUpdate.getId());
// 업데이트된 사용자가 존재하는지 확인
assertThat(updatedUser).isNotEmpty();
assertThat(updatedUser.get().getUserId()).isEqualTo("updatedUserId");
}
@Test
@DisplayName("Delete User By UserId Test")
public void deleteUserByUserIdTest() {
userRepository.saveAndFlush(dummyUsers.get(0));
Optional<User> foundUser = userRepository.findById(dummyUsers.get(0).getId());
assertThat(foundUser).isNotEmpty();
userRepository.deleteById(dummyUsers.get(0).getId());
Optional<User> foundUser2 = userRepository.findById(dummyUsers.get(0).getId());
assertThat(foundUser2).isEmpty();
}
}
TestCase에 대해서는 추후에 게시글이 올라올 예정이라서 중요한 기능적 포인트만 소개하려한다.
@SpringBootTest
이를 통해서 실제 어플리케이션과 비슷한 환경에서 테스트되는 효과를 얻을 수 있다.
@Transactional
현재는 H2 In-Memory를 사용하기 때문에 큰 의미는 없지만, Mysql 같은 DB를 사용할 때, Test 내용이 올라가는 것을 원치 않을 수도 있다. 또한 작업 도중 문제 발생 시 돌아가고 싶을 수도 있다.
@Transactional는 트랜잭션 처리를 지원하도록하고, 문제 발생시에 자동으로 Rollback과 같은 작업을 허용해준다.
@Autowired
매우매우 중요한 개념이다.
초반 내용에서 @Repository를 사용함으로써 해당 Repository구현체가 자동으로 bean으로 등록된다고 했다.
이 때, bean에 등록된 구현체 인스턴스를 주입받게 된다.
이로써 Repository의 구현체 인스턴스가 여러 번 생기지않고, 단 한번 bean이 반환한 Repository 구현체 인스턴스를 주입하여 사용할 수 있다.
의존성 주입시에 자주 쓰이는 개념이다.
실행 후, 위의 결과와 동일하면 정상적으로 작동 되며, 성공적으로 테스트케이스 작성까지 마무리 된 것이다.