간단한 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)
}

Application Properties 설정 방법

|

어플리케이션 설정 값 정의 방법

스프링 부트는 어플리케이션 설정 값을 정의하기 위해 다음과 같은 방법을 제공하고 있습니다.

  • application.properties 파일 이용
  • yaml 파일
  • CLI 명령어 매개변수


application.properties

SERVICE.MESSAGE.TEXT="hello"


application.yaml

service:
    message:
        text: "hello"

좀 더 복잡한 설정을 사용할 때는 yaml 파일을 이용하는 것이 가독성이나 관리가 더 쉽습니다.


CLI 매개 변수

java jar ./snowdeer.jar --service.message.text="hello"

의존 관계 자동 설정

|

의존 관계 자동 설정

앞서 포스팅한 PathVariable 간단한 예제의 예제 코드는 다음과 같습니다.


SimpleService.kt

@Service
class SimpleService {
    fun hello(name: String): String {
        return "hello $name"
    }
}


SimpleController.kt

@Controller
class SimpleController(val simpleService: SimpleService) {

    @RequestMapping(value = ["/user/{name}"], method = arrayOf(RequestMethod.GET))
    @ResponseBody
    fun hello(@PathVariable name: String): String {
        return simpleService.hello(name)
    }
}

SimpleController 클래스에서 SimpleService 인스턴스를 클래스 생성자의 매개변수로 전달하고 있습니다. 만약 해당 인자를 생성자의 매개 변수를 전달하고 싶지 않을 때는 다음과 같이 @Autowired 어노테이션을 이용할 수 있습니다.

@Controller
class SimpleController() {

    @Autowired
    private lateinit var service: SimpleService

    @RequestMapping(value = ["/user/{name}"], method = arrayOf(RequestMethod.GET))
    @ResponseBody
    fun hello(@PathVariable name: String): String {
        return service.hello(name)
    }
}

이와 같이 사용하는 것을 의존성 삽입(Dependency Injection)이라고 합니다.


인터페이스와 구현체 분리

위에서 만들었던 SimpleService 클래스를 다음과 같이 인터페이스와 구현체로 분리할 수 있습니다.

interface SimpleService {
    fun hello(name: String): String
}
@Service
class SimpleServiceImpl : SimpleService {
    override fun hello(name: String): String {
        return "hello $name"
    }
}

그리고 SimpleController도 다음과 같이 수정합니다.

@Controller
class SimpleController() {

    @Autowired
    private lateinit var service: SimpleService

    @RequestMapping(value = ["/user/{name}"], method = arrayOf(RequestMethod.GET))
    @ResponseBody
    fun hello(@PathVariable name: String): String {
        return service.hello(name)
    }
}

SimpleController 코드 안에는 SimpleServiceImpl과 관련된 코드가 하나도 없습니다. 그런데도 스프링 부트가 SimpleService 인터페이스를 구현하는 적절한 인스턴스를 연결해서 매핑하는 것을 확인할 수 있습니다. 만약 SimpleService 인터페이스를 구현한 SimpleServiceImpl2 클래스를 추가로 생성하면 실행할 때 다음과 같은 오류가 발생합니다.

Field service in com.snowdeer.board.controller.SimpleController required a single bean, but 2 were found:
	- simpleServiceImpl: defined in file [/Users/snowdeer/Workspace/SpringBoot/board/build/classes/kotlin/main/com/snowdeer/board/service/SimpleServiceImpl.class]
	- simpleServiceImpl2: defined in file [/Users/snowdeer/Workspace/SpringBoot/board/build/classes/kotlin/main/com/snowdeer/board/service/SimpleServiceImpl2.class]


의존 관계 자동 설정을 통한 장점

이와 같이 의존 관계 자동 설정을 이용하면 다음과 같은 장점을 얻을 수 있습니다.

  • 세부 구현 정보 숨기기: 서비스 구현체에 종속성이 없더라도 해당 인스턴스를 사용가능하며, 따라서 서비스 작동 방식을 외부에 노출하지 않음
  • 디커플링: 기존 서비스에 영향을 주지 않고, 새로운 서비스 변경이나 구현의 사용이 가능
  • 쉬운 변경 처리: 스프링 설정을 통해 코드 수정 없이 서비스 변경이 가능함