SpringBoot에서 데이터베이스 영속성 처리하기

이전에 파라미터 전달에 대해 알아보았으니, 이제 SpringBoot에서 데이터베이스 영속성 작업을 다뤄보겠습니다. 여기서는 JPA를 활용하여 데이터베이스 작업을 수행합니다.

데이터베이스 작업을 위해 먼저 MySQL 드라이버를 추가해야 합니다. 또한 JdbcTemplate과 JpaRepository를 사용할 것이므로 관련 의존성을 함께 추가하겠습니다. 편의상 Alibaba의 fastjson도 포함시켰습니다:

<!-- mysql 연결 시작 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Spring Boot JDBC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql 연결 끝 -->

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.44</version>
</dependency>

다음으로 application.properties 파일에 데이터베이스 설정을 추가합니다:

spring.datasource.url=jdbc:mysql://localhost:3306/girl
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.show-sql:true
spring.jpa.hibernate.ddl-auto:update

물론 YAML 형식으로 설정해도 무방합니다.

이제 엔티티 클래스를 정의하여 테이블 매핑을 구성해보겠습니다:

@Entity
@Table(name = "girl")
public class Girl implements Serializable{

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Integer id;
    @Column(name = "name")
    private String name;
    @Column(name = "age")
    private Integer age;
    @Column(name = "birthday")
    private Date birthday;

    // getter 및 setter 메소드들...
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}

각 어노테이션의 의미는 검색을 통해 확인하시기 바랍니다.

엔티티 매핑이 완료되었으니 이제 영속성 작업을 진행할 차례입니다. 컨트롤러-서비스-DAO 구조로 작업을 나누어 구현해보겠습니다. 먼저 JdbcTemplate을 사용한 예제부터 살펴보겠습니다.

DAO 계층 로직 작성:

@Repository
public class GirlDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<Girl> retrieveAllGirls(){
        return jdbcTemplate.query("SELECT * FROM girl", new RowMapper<Girl>(){
            @Override
            public Girl mapRow(ResultSet rs, int rowNum) throws SQLException {
                Girl girl = new Girl();
                girl.setId(rs.getInt("id"));
                girl.setName(rs.getString("name"));
                girl.setAge(rs.getInt("age"));
                girl.setBirthday(rs.getDate("birthday"));
                return girl;
            }
        });
    }

}

각 어노테이션 설명: @Service는 서비스 컴포넌트, @Controller는 컨트롤러 컴포넌트, @Repository는 DAO 컴포넌트, @Component는 일반 컴포넌트를 나타냅니다.

서비스 계층 구현:

@Service
public class GirlService {

    @Autowired
    private GirlDao girlDao;

    public List<Girl> getAllGirls(){
        return girlDao.retrieveAllGirls();
    }

}

@Autowired 어노테이션으로 DAO 계층을 주입받습니다.

컨트롤러 계층:

@RestController
public class GirlController {

    @Autowired
    private GirlService girlService;

    @RequestMapping(value = "/getAllGirls", method = RequestMethod.GET)
    public String getAllGirls(){
        List<Girl> girls = girlService.getAllGirls();
        return JSON.toJSONString(girls);
    }

}

첫 번째 예제가 완성되었습니다. 프로젝트를 실행하고 http://localhost:8080/getAllGirls로 접속해보세요.

이번에는 JpaRepository를 이용한 영속성 작업을 살펴보겠습니다. JPA는 Hibernate 같은 Provider를 필요로 하는데, Hibernate는 가장 강력한 JPA Provider 중 하나입니다. 기능 면에서 보면 JPA는 Hibernate의 부분집합이라고 할 수 있습니다.

JpaRepository는 위의 DAO 계층과 유사한 역할을 하며, 이해를 돕기 위해 JpaRepository를 DAO 계층으로 취급하겠습니다:

public interface GirlJpaRepository extends JpaRepository<Girl, Integer> {

    List<Girl> findByGirlName(final String name);

    @Query("select g from Girl g where g.name = ?1")
    List<Girl> searchGirlsByName(String name);

}

놀랍게도 JpaRepository는 인터페이스이며 구현체가 필요 없습니다. 이것이 JpaRepository의 장점입니다.

서비스 계층 작성:

@Service
public class GirlJpaService {

    @Autowired
    private GirlJpaRepository girlRepo;

    public Girl addGirl(Girl girl){
        return  girlRepo.save(girl);
    }

    public List<Girl> getGirlsByGivenName(String name){
        return girlRepo.findByGirlName(name);
    }

    public List<Girl> searchGirlsBySpecificName(String name){
        return girlRepo.searchGirlsByName(name);
    }

    public List<Girl> getAllAvailableGirls(){
        return girlRepo.findAll();
    }

}

save와 findAll 메소드는 JpaRepository에서 기본 제공되므로 따로 선언하지 않아도 됩니다.

컨트롤러 로직:

@RestController
public class GirlJpaController {

    @Autowired
    private GirlJpaService girlJpaService;

    @RequestMapping(value = "/addNewGirl", method = RequestMethod.POST)
    public String addNewGirl(){
        Girl girl = new Girl();
        girl.setName("jpa test 1");
        girl.setAge(26);
        girl.setBirthday(new Date());
        girl = girlJpaService.addGirl(girl);
        if(null != girl && null != girl.getId()){
            return "추가 성공";
        }else{
            return "추가 실패";
        }
    }

    @RequestMapping(value = "/getGirlBySpecificName", method = RequestMethod.GET)
    public String getGirlBySpecificName(){
        List<Girl> girls = girlJpaService.getGirlsByGivenName("abc");
        return JSON.toJSONString(girls);
    }

    @RequestMapping(value = "/searchGirlByName", method = RequestMethod.GET)
    public String searchGirlByName() {
        List<Girl> girls = girlJpaService.searchGirlsBySpecificName("abc");
        return JSON.toJSONString(girls);
    }

    @RequestMapping(value = "/getAllExistingGirls", method = RequestMethod.GET)
    public String getAllExistingGirls(){
        List<Girl> girls = girlJpaService.getAllAvailableGirls();
        return JSON.toJSONString(girls);
    }
}

JPA 사용법에 대한 간단한 소개를 마쳤습니다. 몇 가지 추가 팁을 더 알아보겠습니다.

1. @Query 어노테이션 파라미터 사용:

@Query("select g from <strong>Girl</strong> g where g.name = :name")
List<Girl> searchGirlsByName(@Param("name") String name);

2. 네이티브 SQL 사용:

@Query(value = "select * from <strong>girl</strong> g where g.name like %:name%", nativeQuery = true)
List<Girl> searchGirlsByName(@Param("name") String name);

빨간색으로 표시된 부분이 두 방식의 차이점입니다.

JPA 페이징 조회에 대해 알아보겠습니다:

@Override
Page<Girl> findAll(@PageableDefault(page = 1, size = 20, sort = {"id"}, direction = Sort.Direction.ASC) Pageable pageable);

@PageableDefault는 pageable의 기본 설정을 커스터마이즈하는 데 도움이 됩니다. 예를 들어 page = 1, size = 20, sort = { "id" }, direction = Sort.Direction.ASC는 ID 오름차순으로 정렬하고 페이지당 20개씩 첫 페이지 데이터를 가져오도록 설정합니다.

Pageable은 인터페이스이므로 구현체를 만들어야 합니다:

public class CustomPageRequest implements Pageable {
    private int currentPage;
    private int itemsPerPage;
    private Sort sorting;

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public void setItemsPerPage(int itemsPerPage) {
        this.itemsPerPage = itemsPerPage;
    }

    public void setSorting(Sort sorting) {
        this.sorting = sorting;
    }

    @Override
    public int getPageNumber() {
        return currentPage;
    }

    @Override
    public int getPageSize() {
        return itemsPerPage;
    }

    @Override
    public int getOffset() {
        return (currentPage-1)*itemsPerPage;
    }

    @Override
    public Sort getSort() {
        return sorting;
    }

    @Override
    public Pageable next() {
        this.currentPage = this.currentPage+1;
        return this;
    }

    @Override
    public Pageable previousOrFirst() {
        this.currentPage = 0 < this.currentPage ? this.currentPage-1 : 1;
        return this;
    }

    @Override
    public Pageable first() {
        this.currentPage = 1;
        return this;
    }

    @Override
    public boolean hasPrevious() {
        return false;
    }
}

페이징 영속성 계층 로직이 준비되었으니 이제 서비스 계층 로직을 작성해보겠습니다:

public List<Girl> getGirlsInPages(Integer startIndex, Integer itemCount){
        CustomPageRequest customPage = new CustomPageRequest();
        customPage.setCurrentPage(startIndex);
        customPage.setItemsPerPage(itemCount);
        customPage.setSorting(new Sort(Sort.Direction.DESC, new String[]{"id"}));
        return girlRepo.findAll(customPage).getContent();
    }

기본 조회는 Page<Girl> 타입을 반환하므로 getContent() 메소드를 통해 List 데이터를 얻을 수 있습니다.

마지막으로 컨트롤러 계층:

@RequestMapping(value = "/getPagedGirls", method = RequestMethod.GET)
    public String getPagedGirls(){
        List<Girl> girls = girlJpaService.getGirlsInPages(1, 5);
        return JSON.toJSONString(girls);
    }

SpringBoot 관련 내용을 여기까지 함께 살펴보았습니다. 더 좋은 의견이 있으시면 댓글로 자유롭게 논의해주시기 바랍니다.

태그: SpringBoot jpa MySQL hibernate JDBC

7월 2일 20:49에 게시됨