Java

JpaRepository를 활용한 CRUD 구현 방법 (자세한 설명)

15호의 개발자 2022. 2. 24. 22:01
반응형

JpaRepository를 활용한 CRUD 구현 방법 (자세한 설명)

 

이전 글

 

Lombok(롬복) & JPA 에 대한 간단한 설명, 기본 애너테이션(@) 몇 가지

Lombok(롬복) & JPA 에 대한 간단한 설명, 기본 애너테이션(@) 몇 가지 Lombok(롬복) 자바로 코딩을 할 때 가장 힘든 부분 중 하나는, 변수를 선언하고 나서 다음과 같은 것들을 일일이 만들어줘

unit-15.tistory.com

 

 

이전 편에서 알 수 있듯이, UserRepository는 JpaRepository를 상속받았다. 그러므로 기본적인 CRUD를 할 수 있는 환경이 마련되었고, 이번 편에서는 구체적인 실현 방법에 대해 알아본다.

 


 

1. C (Create)

 

@Autowired

@Autowired
private UserRepository userRepository = new UserRepository();

public void create() {
    userRepository.save();
    userRepository.count();
}

예전에는 이렇게 직접 객체를 생성한 다음에 이 userRepostory를 가지고, save()나 count() 등의 메서드를 사용했다.

 

@Autowired는 스프링 부트의 가장 큰 장점이기도 하며 대표적인 디자인 패턴이다. 이 어노테이션은 스프링의 특징인 DI(Dependency Injection)이며, 개발자가 객체를 직접 만들지 않고 스프링이 직접 객체를 만들어서 관리해주어 의존성을 주입시킨다는 의미이다. 어플리케이션이 실행될 때 스프링이 @Autowired가 붙어 있는 키워드를 찾아서 자동으로 생성자를 만들어주기 때문에, new를 통해 생성자를 만들어 줄 필요가 없다.

 

 

 

spring 외의 다른 데이터베이스에서 insert를 친다고 하면

String sql = insert into user ($s, $s, $d) value (account, email, age);

와 같이 쿼리문을 작성했을 것이다. 하지만 JPA는, 오브젝트를 가지고 데이터베이스를 관리할 수 있게끔 도와준다는 장점이 있기 때문에 JPA를 활용하면 아래와 같이 매우 편하게 이용할 수 있다.

 

 

우선, 스프링부트 테스트에 대해 공부하기 위해 아래와 같이 StudyApplicationTests를 하나 생성해보자.

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public
class StudyApplicationTests {

    @Test
    void contextLoads() {
    }

}

 

이제 이 StudyApplicationTests를 상속받아서 테스트를 진행할 것이다. repository를 이용해서 CRUD를 테스트 할 것이기 때문에 UserRepository에 @Autowired를 붙여준다.

import com.example.study.StudyApplicationTests;
import com.example.study.model.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;

public class UserRepositoryTest extends StudyApplicationTests {

    // Dependency Injection (DI)
    @Autowired
    private UserRepository userRepository;

    @Test
    public void create() {

        User user = new User();
        // user.setId();  // auto increment이므로 생략
        user.setAccount("TestUser01");
        user.setEmail("TestUser01@gmail.com");
        user.setPhoneNumber("010-1234-5678");
        user.setCreatedAt(LocalDateTiem.now());
        user.setCreatedBy("admin");
        
        User newUser = userRepository.save(user);
        System.out.println("newUser: " + newUser);
        
    }
    
}

우선, User라는 객체(=User 클래스)를 데이터베이스에 넣을 것이기 때문에 User 객체부터 생성해야 한다. 그런데 이때 User 객체는 DI로 관리하지 않는가?라는 의문이 생길 것이다. 하지만 DI의 핵심은 싱글톤(Singleton)이다. 싱글톤을 간단하게 설명하면, userRepository를 하나만 생성해 놓고서 @Autowired라고 작성해놓은 곳에서 다 같이 쓰겠다는 의미이다. 하지만 User는 생성할 때마다 값이 달라져야하기 때문에(ex, 홍길동 유저, 이성계 유저..) new를 사용해서 User 객체를 직접 생성해서 사용해야 한다.

 

Id의 경우 데이터베이스에서 auto increment로 지정해놓았으므로 setId()로 설정해 놓으면 안 된다. create 할 때마다 Id 값이 순차적으로 증가하는 것이지, 특정 Id 값을 넣어서 create 할 수 없다. 

 

save() 메서드는 user 타입을 넘겨서 저장했기 때문에 저장되는 타입도 User 객체여야 한다. 이렇게 save로 넘기게 되면 우리가 따로 지정하지 않은 Id 값도 알아서 지정된다.

 

위의 Test를 Run해보면 println으로 찍은 newUser를 콘솔창에서 확인할 수 있다. 그러면 프린트문의 포맷이 깔끔하게 나온다는 것을 알 수 있는데, 이는 User class에서 @Data 어노테이션을 붙임으로써 롬복이 자동으로 toString() 메서드를 설정해줘서 깔끔하게 뜨는 것이다. 또한, 실제 DB와 연동이 정상적으로 되어있다면 Test를 실행했을 때, 실제 DB에 해당 데이터가 잘 들어간 것을 확인할 수 있다.

 

cf.
application.properties에 spring.jpa.show-sql=true를 추가하면 create문을 실행했을 때 jpa가 실행한 sql문이 로그에 뜰 것이다.

 


 

2. R (Read)

아래는 가장 기본적인 방식으로 read() 메서드를 구현한 것이다.

@Test
public void read() {
    Optional<User> user = userRepository.findById(1L);	// id를 Long 타입으로 지정했으므로

    user.ifPresent(selectUser -> {
        System.out.println("user: " + selectUser);
        System.out.println("email: " + selectUser.getEmail());
    });
}

Id가 1인 user에 대한 user 객체와 email를 print문으로 보여준다. 이때, User 클래스에서 id를 Long 타입으로 지정해놓았기 때문에 findById에 "1L"과 같이 넣어줘야 한다.

 

참고로, findById() 메서드는 Optional을 반환하도록 설정되어있다. Optional은 말 그대로 값이 존재할 수도, 존재하지 않을 수도 있다는 의미이다.

 

 

 

cf.
CRUD와 REST API를 연결하면 아래와 같이 @RequestParam으로 id 값을 받는 방식으로 코딩할 수도 있다.
@Test
public void read(@RequestParam Long id) {
    Optional<User> user = userRepository.findById(id);	// id를 Long 타입으로 지정했으므로

    user.ifPresent(selectUser -> {
        System.out.println("user: " + selectUser);
        System.out.println("email: " + selectUser.getEmail());
    });
    
    return user.get();
}

 


 

3. U (Update)

update를 하기 위해서는 몇 가지 선행 조건이 있다. 

  1. 특정 데이터를 먼저 select해야 한다.
  2. 해당하는 데이터가 존재해야 한다.

 

@Test
public void update() {
    // 특정 데이터를 먼저 select한다.
    Optional<User> user = userRepository.findById(1L);

    // 해당하는 데이터가 존재해야 한다.
    user.ifPresent(selectUser -> {
        // selectUser.setId(2L); // 이 경우 id=2인 컬럼이 update 된다.
        selectUser.setAccount("UpdateUser01");
        selectUser.setUpdatedAt(LocalDateTime.now());
        selectUser.setUpdatedBy("update method()");

        userRepository.save(selectUser);
    });
}

id가 1인 user를 먼저 select 해놓고서 selectUser.setId(2L);와 같이 id가 2인 user를 또 select 해버리면, 결과적으로 id가 2인 user의 데이터가 update된다. 따라서 이에 주의해야 한다.

 

Test를 실행시키면 account, updatedAt, updatedBy 외에도 createdBy, email 등의 칼럼들도 update가 쳐지는 것을 볼 수 있다. 이는 JPA가 update를 하는 기본 동작 원리로써, update할 특정 컬럼을 선택하더라도 모든 컬럼이 update된다.

 

 


 

4. D (Delete)

delete를 하기 위해서는 update와 같이 몇 가지 선행 조건이 필요하다.

  1. 특정 데이터를 먼저 select해야 한다.
  2. 해당하는 데이터가 존재해야 한다.

 

@Test
public void delete() {
    // 특정 데이터를 먼저 select한다.
    Optional<User> user = userRepository.findById(1L);

    // 해당하는 데이터가 존재해야 한다.
    user.ifPresent(selectUser -> {
        // selectUser.setId(2L); // 이 경우 id=2인 컬럼이 delete 된다.
        userRepository.delete(selectUser);
    });

    // 데이터가 정상적으로 삭제되었는지 확인
    Optional<User> deleteUser = userRepository.findById(1L);

    if(deleteUser.isPresent()) {
        System.out.println("데이터 존재:" + deleteUser.get());
    } else {
        System.out.println("데이터 없음");
    }
}

delete는 말 그대로 데이터를 삭제하는 것이기 때문에 save 할 필요가 없다.

 

id가 1인 user를 먼저 select 해놓고서 selectUser.setId(2L);와 같이 id가 2인 user를 또 select 해버리면, 결과적으로 id가 2인 user의 데이터가 delete된다. 따라서 이에 주의해야 한다.

 

데이터가 정상적으로 삭제되었는지 확인하기 위해 deleteUser를 하나 생성한 후, deleteUser가 존재하는지 간단한 if-else문을 만들어서 확인한다. 정상적으로 삭제되었다면 "데이터 없음"이라는 문구가 뜰 것이다.

 

 

 

위 코드는 테스트 코드라고 부르기에는 사실 민망할 정도로 너무 간단한 코드이다. 아래와 같이 수정하면 좀 더 그럴싸한 테스트 코드가 된다.

import org.junit.jupiter.api.Assertions;

@Test
public void delete() {
    // 특정 데이터를 먼저 select한다.
    Optional<User> user = userRepository.findById(1L);

    Assertions.assertTrue(user.isPresent()); // true

    // 해당하는 데이터가 존재해야 한다.
    user.ifPresent(selectUser -> {
        userRepository.delete(selectUser);
    });

    // 데이터가 정상적으로 삭제되었는지 확인
    Optional<User> deleteUser = userRepository.findById(1L);

    Assertions.assertFalse(deleteUser.isPresent()); // false
}

assertions의 assertTrue 메서드와 assertFalse 메서드를 이용해서, select한 user가 존재하는지 확인하고, delete한 user가 정말 잘 delete 되었는지 확인하는 코드를 추가했다.

 

 

 

@Transactional

테스트 코드는 말 그대로 테스트 코드이기 때문에 DB에 들어있는 실데이터에는 영향을 끼치면 안 된다. 이처럼 코드는 돌려보고 싶지만 실제 DB의 데이터들 변경하고 싶지 않을 때는 @Transactional 애너테이션을 이용하면 된다.

 

import org.junit.jupiter.api.Assertions;

@Test
@Transactional
public void delete() {

    Optional<User> user = userRepository.findById(1L);

    Assertions.assertTrue(user.isPresent()); // true

    user.ifPresent(selectUser -> {
        userRepository.delete(selectUser);
    });

    Optional<User> deleteUser = userRepository.findById(1L);

    Assertions.assertFalse(deleteUser.isPresent()); // false
    
}

@Test 어노테이션 아래에 @Transactional 어노테이션을 추가했다. 이후 Test를 실행하면 기존과 마찬가지로 테스트 코드의 메소드들은 모두 실행되지만, 실제 데이터베이스에 있는 데이터는 하나도 변하지 않는다. 이 어노테이션을 붙여 놓으면 마지막에 트랜잭션을 롤백(roll back) 시켜주기 때문에 실제 데이터베이스의 데이터들은 원상복구가 된다.

 


 

다음 글

 

JPA 연관관계 설정 (@OneToOne/@OneToMany/@ManyToOne/@ManyToMany)

JPA 연관관계 설정 (@OneToOne/@OneToMany/@ManyToOne/@ManyToMany) 이 게시글은 아래 글과 이어지는 글이다. JpaRepository를 활용한 CRUD 구현 방법 (자세한 설명) JpaRepository를 활용한 CRUD 구현 방법 ..

unit-15.tistory.com

 

 

 

(출처: 패스트캠퍼스 Java & SpringBoot로 시작하는 웹 프로그래밍)

 

반응형