[Spring Data] JPA (2) - CRUD 기능을 위한 JPA Repository 만들기

k·2024년 2월 7일
0

spring data

목록 보기
2/5

이전 글 - [Spring Boot] JPA (1) - 환경셋팅 / Entity 클래스 만들기

이전에 Entity클래스까지 만들었다.

이번에는 JPA Repository를 활용해서 CRUD 기능을 구현해 보려한다.

Repository 인터페이스 구성

Repository 인터페이스는 JpaRepository<Your_Entity, PK_Type>를 상속받아서 만들 수 있다.

//UserRepository.java

public interface UserRepository extends JpaRepository<User, Integer>{
	//추가적인 메소드를 만들 수 있다.
}

위의 코드처럼 구성하게 되면 JPA는 자동으로 해당 인터페이스의 구현체를 생성하고 어플리케이션 컨텍스트에 빈으로 등록하게 됩니다.

근데 왜 @Repository는 안쓰세요?

일반적으로 이 부분이 궁금할 수도 있다. 나도 처음 공부를 진행 할 때, @Repository를 남발했었다.

하지만 DOCS를 읽어보면, @Repository는 인터페이스가 아닌 구현체에 적어줘야한다. 그러나 JpaRepository를 상속받게 되면 자동으로 구현체가 만들어지기 때문에 @Repository 가 불필요하다.

아래에 소개되는 CustomRepository를 만들 때, @Repository를 명시적으로 쓰는 것을 확인할 수 있다.

Custom Repository 를 만들어보자

CustomRepository 인터페이스

//CustomRepository.java

public interface CustomRepository{
	//메소드를 만들어준다.
    List<User> findUsers;
}

CustomRepository 구현체

//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을 추가해주면 된다.

@Repository 쓰지않고 Bean등록

//JpaConfig.java
@Configuration
public class JpaConfig{
	//bean은 한번만 호출된고, bean 저장소에이있는 것을 지속 참조한다.
    @Bean
    public CustomRepository customRepositoryConfig(){
    	return CustomRepositoryImpl();
    }
}

커스텀을 만들게 되면, 만들어지는 요소가 많아지고 유지보수 측면에서 불리할 수 있다.

그렇기 때문에 조금 더 편리하게 할 수 있도록 지금부터는 JpaRepository를 상속받아서 사용하는 형태로 진행할 것이다.

커스텀을 하는 방법을 굳이 정리한 이유는 어떤 동작이 일어나는지 알지 못하면, 실수가 많이 발생 할 수 있기 때문에 한번 짚고 넘어가는 것이다. 얼마나 편리한 기능을 사용하고 있는지 알기 위함도 있다.

Entity Class 수정

기존 User Entity Class

@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;
}

수정된 User Entity Class

@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);
    }
}
  • @Getter
    모든 필드에 대한 Getter함수를 생성한다. 값을 뱉어낼 수있다.
  • @NoArgsConstructor
    기본 생성자(매개변수가 없는 생성자)를 만들어준다.
  • @Setter
    변경되지않고, 불변이여야하는 필드를 제외한 나마지에 접근할 수 있도록 한다.
  • public User(String userId, String userPw, String userName)
    추후 Insert 작업 시, User 객체를 생성하기 위한 필드를 생성자로 지정한다.
  • equals() 와 hashCode()
    테스트 작성이나, 기타 기능에서 객체 간에 동등성을 비교하기 위해서 사용된다.

    id가 같으면 동일한 객체로 취급
    참조하는 데이터를 최소화 시킴

위의 형태로 CRUD 기능을 위한 Entity 수정이 완료되었다.

UserRepository 커스텀 메소드 추가

JpaRepository를 상속받으면, Jpa가 메소드이름을 통해 자동적으로 쿼리를 생성한다.

테스트를 위해서 아래의 함수원형을 UserRepository 인터페이스내에 추가해줄 것이다.

	Optional<User> findByUserId(String userId);

Jpa 상에서 자동으로 구현체를 만들어주기 때문에 메소드 이름을 규칙에 따라서 잘 지어주면 된다.

테스트케이스 구성 및 동작

우리는 현재 컨트롤러를 따로 구성하여 만들지 않았다. 그렇기에 테스트케이스를 만들어서 CRUD 기능에 대한 동작이 잘 진행되는지 확인해 볼 것이다.

build.gradle 추가

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 구현체 인스턴스를 주입하여 사용할 수 있다.

    의존성 주입시에 자주 쓰이는 개념이다.

    테스트케이스 결과


    실행 후, 위의 결과와 동일하면 정상적으로 작동 되며, 성공적으로 테스트케이스 작성까지 마무리 된 것이다.

profile
You must do the things you think you cannot do

0개의 댓글