Spring Data JPA

|

Spring Data JPA

JPA(Java Persistence API)는 Java를 이용해서 데이터를 관리하는 기법을 스펙으로 정리한 표준입니다. 데이터베이스와 관련된 기술과 스펙은 오랫동안 이슈가 되어왔고, Java 진영에서는 EJB라는 기술 스펙으로 Entity Bean이라는 데이터 처리 스펙을 정했으며 이게 JPA의 시초라고 볼 수 있습니다.


ORM

ORM(Object Relation Mapping)은 데이터베이스의 개체와 객체지향에서의 객체가 아주 유사하기 때문에 데이터베이스와 객체 지향을 한 번에 처리하기 위해서 나온 개념입니다.

ORM은 특정 언어에 종속적인 개념이 아니기 때문에 다양한 언어에서 ORM 지원 프레임워크를 많이 볼 수 있습니다.


JPA와 ORM

JPA는 ORM의 개념을 Java에서 구현하기 위한 스펙이라고 볼 수 있습니다. 기존의 JDBC 등을 이용해서 SQL 쿼리 등을 직접 작성했다면, ORM을 통해서 추상화된 객체 형태로 데이터베이스를 관리할 수 있습니다.


스프링 부트와 JPA

JPA는 하나의 스펙이기 때문에 다양한 프레임워크들에서 개별로 구현을 하고 있습니다. 스프링부트에서는 Hibernate를 이용해서 JPA를 지원하고 있습니다.


JPA의 특징

JPA를 이용하면 다음과 같은 장점이 있습니다.

  • 데이터베이스 관련 코드에 대한 유연함 획득
  • DB 설계와 Java 설계를 동시에 할 수 있음
  • 데이터베이스 종류와 독립적 - 특정 벤더에 종속적이지 않음

반대로 다음과 같은 단점도 있습니다.

  • 높은 러닝 커브(Learning Curve)
  • 객체지향 설계 사상이 반영되어야 함
  • 특정 데이터베이스에 최적화된 방법을 사용할 수 없음

Error 처리하기

|

Error 처리하는 방법

Json 파싱 에러 처리

ErrorHandler.kt 파일을 만들고 다음과 같이 작성하면 됩니다.

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<String> {
        return ResponseEntity("JSON Parsing Error", HttpStatus.BAD_REQUEST)
    }
}

하지만 이 경우는 String으로 리턴하기 때문에 RESTful API에는 어울리지 않습니다. 에러도 JSON 포맷으로 리턴하는 것이 좋습니다.


ErrorResponse.kt

data class ErrorResponse(val error: String, val message: String)


ErrorHandler.kt

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("JSON Parsing Error", exception.message ?: ""),
                HttpStatus.BAD_REQUEST)
    }
}


다양한 Exception 처리

여기에 좀 더 다양한 Exception 처리를 하려면 다음과 같이 추가로 예외 처리를 작성할 수 있습니다.

ErrorResponse.kt

data class ErrorResponse(val error: String, val message: String)


UserNotFoundException.kt

class UserNotFoundException(message: String) : Exception(message)


ErrorHandler.kt

@ControllerAdvice
class ErrorHandler {

    @ExceptionHandler(JsonParseException::class)
    fun JsonParseExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("JSON Parsing Error", exception.message ?: ""),
                HttpStatus.BAD_REQUEST)
    }

    @ExceptionHandler(UserNotFoundException::class)
    fun UserNotFoundExceptionHandler(servletRequest: HttpServletRequest, exception: Exception)
            : ResponseEntity<ErrorResponse> {
        return ResponseEntity(
                ErrorResponse("User Not Found", exception.message ?: ""),
                HttpStatus.NOT_FOUND)
    }
}


UserController.kt

@RestController
class UserController {

    @Autowired
    private lateinit var service: UserService

    @GetMapping("/user/{id}")
    fun getUser(@PathVariable id: Int): ResponseEntity {
        val user = service.getUser(id) ?: throw UserNotFoundException("user($id) does not exist.")
        return ResponseEntity(user, HttpStatus.OK)
    }
}
</pre>

간단한 Restful Server 만들기(리팩토링 버전)

|

Simple Restful Server

기존에 UserApplication.kt 내부에서 만들었던 User 리스트 정보를 UserService 클래스로 이동시킵니다. 그리고 UserService 클래스를 직접 참조하는게 아니라 인터페이스를 통해 접근하기 위해서, IUser 인터페이스와 UserService 클래스로 분리를 합니다.


UserApplication.kt

@SpringBootApplication
class UserApplication

fun main(args: Array<String>) {
    runApplication<UserApplication>(*args)
}


IUser.kt

interface IUser {
    fun getUser(id: Int): User?
    fun searchUser(nameFilter: String): List<User>
    fun createUser(user: User)
    fun updateUser(id: Int, user: User)
    fun deleteUser(id: Int)
}


UserService.kt

@Service
class UserService : IUser {
    private val userMap = ConcurrentHashMap<Int, User>()

    init {
        userMap[1] = User(1, "snowdeer")
        userMap[2] = User(2, "ran")
    }

    override fun getUser(id: Int): User? {
        return userMap[id]
    }

    override fun searchUser(nameFilter: String): List<User> {
        return userMap.filter {
            it.value.name.contains(nameFilter, true)
        }.map(Map.Entry<Int, User>::value).toList()
    }

    override fun createUser(user: User) {
        userMap[user.id] = user
    }

    override fun updateUser(id: Int, user: User) {
        userMap[id] = user
    }

    override fun deleteUser(id: Int) {
        userMap.remove(id)
    }
}


UserController.kt

@RestController
class UserController {

    @Autowired
    private lateinit var service: UserService

    @GetMapping("/user/{id}")
    fun getUser(@PathVariable id: Int): ResponseEntity<User> {
        return ResponseEntity(service.getUser(id), HttpStatus.OK)
    }

    @GetMapping("/users")
    fun userList(): ResponseEntity<List<User>> {
        return ResponseEntity(service.searchUser(""), HttpStatus.OK)
    }

    @GetMapping("/search")
    fun search(@RequestParam(required = false, defaultValue = "") nameFilter: String): ResponseEntity<List<User>> {
        return ResponseEntity(service.searchUser(nameFilter), HttpStatus.OK)
    }

    @PostMapping("/create")
    fun createUser(@RequestBody user: User): ResponseEntity<Unit> {
        service.createUser(user)
        return ResponseEntity(Unit, HttpStatus.OK)
    }

    @DeleteMapping("/delete/{id}")
    fun deleteUser(@PathVariable id: Int): ResponseEntity<Unit> {
        service.deleteUser(id)
        return ResponseEntity(Unit, HttpStatus.OK)
    }

    @PutMapping("/update/{id}")
    fun updateUser(@PathVariable id: Int, @RequestBody user: User): ResponseEntity<Unit> {
        service.updateUser(id, user)
        return ResponseEntity(Unit, HttpStatus.OK)
    }
}


POST 함수 테스트하기

터미널에서 curl 명령어를 이용해서 간단하게 테스트할 수 있습니다.

curl -X POST "http://localhost:8080/create" \
    -H "content-type: application/json" \
    -d '{ 
        "id": 3, 
        "name": "yang" 
        }'

간단한 Restful Server 만들기

|

Simple Restful Server

스프링부트에서 간단히 @RestController 어노테이션이 붙은 클래스를 추가하는 것만으로 간단한 Restful 서버가 만들어집니다.

그리고 data class 클래스를 만들어서 해당 인스턴스를 반환하면 자동으로 JSON 포맷으로 전송됩니다.

간단한 예제는 다음과 같습니다.


User.kt

data class MobileDevice(var id: Int = 0, var name: String = "")


UserController.kt

@RestController
class UserController {

    @RequestMapping("/user")
    fun user(): User {
        return User(1, "snowdeer")
    }
}


이 상태에서 http://localhost:8080/user 주소로 접속하면 다음과 같은 결과가 출력됩니다.

{"id":1,"name":"snowdeer"}


경로 및 매개변수

이제 URL에 추가 매개변수를 넣어 필터링을 할 수가 있습니다.

매개변수에 따라 필터링되는 결과를 확인하기 위해, UserApplication.kt 파일에 다음과 같은 리스트를 추가합니다.


UserApplication.kt

@SpringBootApplication
class UserApplication {
    companion object {
        val userList = ConcurrentHashMap<Int, User>()
    }

    init {
        userList[1] = User(1, "snowdeer")
        userList[2] = User(2, "ran")
        userList[3] = User(3, "yang")
        userList[4] = User(4, "downy")
        userList[5] = User(5, "john")
    }

    @Bean
    fun userList(): ConcurrentHashMap<Int, User> {
        return userList
    }
}

fun main(args: Array<String>) {
    runApplication<UserApplication>(*args)
}


UserController.kt

@RestController
class UserController {

    @Autowired
    private lateinit var userList: ConcurrentHashMap<Int, User>

    @RequestMapping("/user/{id}")
    fun user(@PathVariable id: Int): User? {
        return userList[id]
    }

    @RequestMapping("/users")
    fun userList(): List<User> {
        return userList.map(Map.Entry<Int, User>::value).toList()
    }
}


검색어를 이용한 필터링

UserController 클래스에 search() 메소드를 추가합니다.

@RestController
class UserController {

    @Autowired
    private lateinit var userList: ConcurrentHashMap<Int, User>

    @RequestMapping("/user/{id}")
    fun user(@PathVariable id: Int): User? {
        return userList[id]
    }

    @RequestMapping("/users")
    fun userList(): List<User> {
        return userList.map(Map.Entry<Int, User>::value).toList()
    }

    @RequestMapping("/search")
    fun search(@RequestParam(required = false, defaultValue = "") nameFilter: String): List {
        return userList.filter {
            it.value.name.contains(nameFilter, true)
        }.map(Map.Entry<Int, User>::value).toList()
    }
}
</pre>

그런 다음, `http://localhost:8080/find?nameFilter=an`과 같이 요청을 날리면 다음처럼 결과가 나옵니다.

~~~
[{"id":2,"name":"ran"},{"id":3,"name":"yang"}]
~~~

Application Properties를 이용한 프로파일 사용하기

|

프로파일 사용하기

하나의 서비스 인터페이스와 이를 구현하는 서비스 구현체가 여러 개가 있을 때 프로파일을 이용해서 원하는 서비스가 연결되도록 할 수 있습니다.

HelloService.kt

interface HelloService {
    fun hello(name: String): String
}


HelloServiceImpl.kt

class HelloServiceImpl : HelloService {
    override fun hello(name: String): String {
        return "Hello, $name"
    }
}


GoodMorningServiceImpl.kt

class HelloServiceImpl : HelloService {
    override fun hello(name: String): String {
        return "Hello, $name"
    }
}

기존에는 각 서비스 구현 클래스에 @Service 어노테이션을 붙였지만, 여기서는 붙이지 않았습니다. 대신 Application 클래스안에 Bean 클래스를 명시적으로 생성하도록 합니다.


HelloApplication.kt

@SpringBootApplication
class HelloApplication {
    @Bean
    fun helloService(): HelloService = HelloServiceImpl()

//    @Bean
//    fun goodMorinigServie(): HelloService = GoodMorningServiceImpl()
}

fun main(args: Array<String>) {
    runApplication<HelloApplication>(*args)
}

위의 주석 부분을 제거하면 서비스가 2개가 되어 실행시 오류가 발생합니다. 이제 Application Properties를 이용해서 선택적으로 인스턴스를 생성하는 부분을 적용합니다.


application.yaml

service:
  greet: "goodmorning"


HelloApplication.kt(수정 후)

@SpringBootApplication
class HelloApplication {
    @Bean
	@ConditionalOnExpression("#{'\${service.greet}'=='hello'}")
    fun helloService(): HelloService = HelloServiceImpl()

    @Bean
	@ConditionalOnExpression("#{'\${service.greet}'=='goodmorning'}")
    fun goodMorinigServie(): HelloService = GoodMorningServiceImpl()
}


fun main(args: Array<String>) {
    runApplication<HelloApplication>(*args)
}