본문 바로가기

3.구현/Java or Kotlin

[kotlin] JPA 1: Spring Boot에서 간단한 CRUD

들어가기

Spring Boot 기반의 Spring Data JPA를 사용하여 API 서버 구축할려고 한다. 복잡한 JPA 개념을 건너뛰고 바로 Spring Boot에서 기본적인 CRUD 부터 시작해보자.

작성자: http://ospace.tistory.com/ (ospace114@empal.com)

기본환경

먼저 kotlin과 Spring Boot에서 Maven 빌더를 사용한다고 가정하겠다. 기본적인 Spring Boot 프로젝트가 구성되었다면 아래와 같은 dependency을 추가해줘야 한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

spring-boot-starter-data-jpa에 의해서 기본적인 JPA 관련 라이브러리가 포함된다. 그리고 사용할 데이터베이스로 간편하게 테스트용으로 사용할 수 있는 h2를 사용했다.

이외에 웹 환경 사용을 위해 아래 dependency을 추가한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

그외의 dependency들은 각자 환경에 맞게 추가하면 된다.

다음으로 application.properties을 설정하겠다. 사용할 데이터 베이스 설정이 필요하다.

spring.datasource.url=jdbc:h2:./storage.h2
spring.h2.console.enabled=true
spring.h2.console.path=/h2

h2는 파일 DB를 사용하고 사용할 파일 명은 “storage.h2”로 했다. console 사용하도록 했고 경로를 “/h2”로 했다. 실행 후에 http://localhost:8080/h2 로 접속하면 h2을 관리할 수 있는 웹페이지을 사용할 수 있다.

JPA 관련 설정이다.

spring.jpa.hibernate.ddl-auto=create
# spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

dll-auto에 의해 엔티티와 데이터베이스간 변경에 따른 정책을 사용할 수 있다. 총 5가지 정보 옵션을 제공한다.

  • create: 기준 테이블 있으면 제거하고 새로 생성
  • create-drop: 기준 테이블 있으면 제거하고 새로 생성, 종료시 테이블 삭제(임베딩 DB인 경우 기본값)
  • update: 테이블이 없으면 새로 만들고 추가되거나 삭제된 컬럼은 반영(컬럼 속성 변경은 반영 안됨)
  • validate: 엔티티와 테이블이 동기화되었는지만 확인
  • none: 사용하지 않음 (기본값)

database-platform에 의해 데이터베이스 방언(Dialect)을 지정한다. 물론 datasource에 설정된 값에 의해 자동으로 감지해서 적용한다. 어떤 데이터베이스는 여러 방언 모듈이 있기에 다른 방언 모듈을 사용하려면 설정하면 된다.

개발 과정에서 데이터베이스에 실행하는 SQL 쿼리를 출력할 경우에 사용하는 설정이다.

# SQL 쿼리 출력
spring.jpa.show-sql=true
# SQL 쿼리 이쁘게 포멧팅
spring.jpa.properties.hibernate.format_sql=true

한줄로 SQL 쿼리 출력이 보기좋게 포멧팅되서 출력된다.

Entity 정의

다음으로 객체인 엔티티(Entity)을 정의해보자. 엔티티는 특정 대상을 표현하는 객체라고 보면 된다. 추후 데이터베이스 테이블과 매핑된다. Member 엔티티를 정의해보자.

@Entity
open class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private var id:Long? = null;
    private var name:String? = null;

    open fun getId(): Long? = id
    open fun setId(id:Long?) {
        this.id = id
    }
    open fun getName(): String? = name
    open fun setName(name:String?) {
        this.name = name
    }
}

Member라는 엔티티는 어떤 조직에 소속된 사용자라고 할 수 있다. 그리고 Member 식별을 위한 식별자와 이름이 포함된다. 물론 그외에 다양한 속성을 정의할 수 있다. 사용하고 있는 어노테이션을 살펴보자.

@Entity

엔티티를 지정할때 사용하는 어노테이션이다. 엔티티 정의할 때에 주의사항이다.

  • 기본 생성자가 필수이다. 가급적 기본생성자를 정의해놓는게 좋다.
  • final 클래스, enum, interface, inner 클래스에서는 사용할 수 없다.
  • final 속성 필드는 사용할 수 없다.

그리고 @Table을 같이 사용한다. @Table은 테이블 명과 같이 사용한다. 사용하지 않으면 클래스 명이 테이블 명이 된다.

@Id

엔티티에 식별자를 지정한다. 식별자 생성 전략을 @GeneratedValue 어노테이션으로 추가 설정할 수 있다. GenerationType.IDENTITY은 데이터베이스에서 기본키를 생성하는 전략이다. 보통 데이터베이스의 AUTO_INCREMENT를 사용한 기본키 생성 방식이다. 나머지 추가 속성에 대한 자세한 설정은 차후에 살펴볼 예정이다.

속성 중에 id만 어노테이션으로 기본키를 정의했지만 별도 매핑 정보가 없는 나머지 속성은 자동으로 기본 매핑된다. 이정도만 해도 기본적인 엔티티 정의는 끝이다.

Repository 정의

실제 데이터베이트와 연동하기 위한 인터페이스를 정의해야 한다. 이를 위해 Repository을 선언해야 한다. Spring Data JPA에서 제공되는 Repository에는 몇가지가 존재한다. 여기서 사용할 Repository는 JpaRepository이다.

Fig 01. Repository HIerarchy

JpaRepository 인터페이스는 JPA를 위한 Repository로 ListCrudRepository와 ListPagingAndSortingRepository를 상속한다. 우리가 할 부분은 JpaRepository을 상속한 인터페이스를 정의하면 된다.

interface MemberRepository : JpaRepository<Member, Long> {
}

JpaRepository에서 사용할 엔티티로 Member을 사용하고 해당 엔티티의 기본키 타입을 지정하면 된다. 이미 JpaReposiotry에서 기본 CRUD와 페이징과 정렬을 할 수 있는 인터페이스가 제공된다. 여기서 사용할 메소드는 다음과 같다.

  • Optional findById(ID id): 주어진 ID의 엔티티 조회
  • save(S entity): 엔티티 저장
  • deleteById(ID id): 주어진 ID의 엔티티 삭제

이름에서도 이미 어떤 역할을 하는지 직관적으로 알 수 있다.

이렇게 MemberRepository 인터페이스를 정의해두면 실행할때 자동으로 빈객체로 인스탄스화되고 메소드에 대한 구현체도 포함된다.

Controller 정의

서비스를 정의해야하지만 여기서는 맞보기로 JPA을 연동하는 부분에 집중할려고 한다. 때문에 서비스를 생략하고 바로 Controller에서 Repository을 사용해보자.

@RestController
@RequestMapping("/api/members")
class MemberController(
    private val memberRepo: MemberRepository
) {
    @GetMapping("/{id}")
    fun get(@PathVariable id: Long): Member {
        val ret = memberRepo.findById(id)
        return ret.get();
    }

    @PostMapping
    fun create(@RequestBody member: Member): Member {
        return memberRepo.save(member)
    }

    @PutMapping("/{id}")
    fun update(@PathVariable id: Long, @RequestBody member: Member): Member {
            member.setId(id)
        return memberRepo.save(member)
    }

    @DeleteMapping("/{id}")
    fun delete(@PathVariable id: Long) {
        memberRepo.deleteById(id)
    }
}

기본적인 Member에 대한 CRUD용 API를 정의했다. 별다른게 없다. 별도 DTO 같은 객체로 전달해서 처리 형태를 권장한다. 아무튼 복잡해지면 이해하기 힘들기 때문에 최대한 단순하게 바로 엔티티 객체로 처리하고 있다. REST API로 제공하고 있다.

테스트

간단한 테스트용 화면이다. POST, GET, PUT, DELETE를 호출하는 있는 버튼이 있다. 그리고 GET, PUT, DELETE 버튼 오른쪽에는 ID값을 입력할 수 있는 텍스트 박스가 있다.

<button onclick="onStep1()">POST</button> /
<button onclick="onStep2()">GET</button>
<input type="text" id="idGet" value="1" style="width:20px;"> /
<button onclick="onStep3()">PUT</button>
<input type="text" id="idPut" value="1" style="width:20px;"> /
<button onclick="onStep4()">DELETE</button>
<input type="text" id="idDelete" value="1" style="width:20px;">
<script>
function onStep1() {
    fetch('/api/members', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: '{"name":"foo"}',
    }).then(res=>
        res.json()
    ).then(json=>{
        alert("Success: " + JSON.stringify(json));
    }).catch(err=>{
        alert("ERROR");
    });
}

function onStep2() {
    fetch(`/api/members/${idGet.value}`)
    .then(res=>res.json())
    .then(json=>{
        alert(JSON.stringify(json));
    }).catch(err=>{
        alert("ERROR");
    });
}

function onStep3() {
    fetch(`/api/members/${idPut.value}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: '{"name":"bar"}',
    }).then(res=>{
        alert("Success!");
    }).catch(err=>{
        alert("ERROR");
    });
}

function onStep4() {
    fetch(`/api/members/${idDelete.value}`, {
        method: 'DELETE',
    }).then(res=>{
        alert("Success!");
    }).catch(err=>{
        alert("ERROR");
    });
}
</script>

자바스크립트 자체는 크게 어렵지 않기 때문에 설명은 생략한다. 아래는 결과 화면이다.

Fig 02. 테스트 화면

아래 API를 호출를 통해 확인할 수 있다.

  1. POST http://localhost:8080/api/members/
    • BODY { “name”:”foo” }
  2. GET http://localhost:8080/api/members/1
  3. PUT http://localhost:8080/api/members/
    • BODY { “name”:”bar” }
  4. GET http://localhost:8080/api/members/1
  5. DELETE http://localhost:8080/api/members/1

Github

https://github.com/ospace/jpa-works/tree/main/project-jpa-kotlin-01

결론

지금까지 Spring Data JPA를 사용한 기본적인 CRUD를 살펴보았습니다. SQL 쿼리도 없이 엔티티를 만들어서 바로 사용할 수 있다는 장점이 있다. 지금까지는 간단한 맛보기로 이제 부터 본격적으로 엔티티 표현과 엔티티와 엔티티간 관계에 대한 복잡한 사용법에 대한 내용을 살펴볼려고 한다.

부적한 글이지만 여러분에게 도움이 되었으면 하네요. ^^ 모두 즐프하세요. ospace.

참고

[1] 김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘, 2015

[2] Spring Boot Reference Guide, Data access, https://docs.spring.io/spring-boot/docs/2.1.13.RELEASE/reference/html/howto-data-access.html

[3] Interface JpaReposotyr, https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html, 2024.03.30

반응형