본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[kotlin] 스프링 프레임워크 개발

스프링 IOC - 의존성 주입

package com.example.KotlinLabSpring

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class KotlinLabSpringApplication

fun main(args: Array<String>) {
    SpringApplication.run(KotlinLabSpringApplication::class.java, *args)
}

run 함수에 대입되는 클래스에 @SpringBootApplication 어노테이션은 @Configuration, @EnableAutoConfiguration, @ComponentScan을 묶어놓은 어노테이션이다. 코틀린은 자바와 100% 호환되며 스프링 프레임워크 5 버전부터 공식적으로 코틀린을 지원한다.

 

코틀린으로 DI에 의한 객체 이용 구현은 기존 자바로 구현했던것과 크게 차이는 없다. 단, 코틀린에서는 프로퍼티를 선언하면서 초기화해야 하는데 이 프로퍼티는 개발자가 초기화하는게 아니라 스프링 프레임워크가 대입되는 객체로 초기화하므로 lateinit 예약어를 추가해야 한다.

@RestController
class IOCTestController {
    
    @Autowired
    lateinit var service: IOCTestService
    
    @GetMapping("/ioc")
    fun hello(@RequestParam(value = "name") name: String) = service.sayHello(name)
}

 

생성자 주입

스프링 DI를 이용하면서 위의 소스처럼 클래스 프로퍼티에 @Autowired를 추가해 이용할 수 있다. 그런데 DI는 생성자 주입(Constructor Injection)과 Setter 주입도 있다. 생성자 주입은 말그대로 객체를 생성자를 통해 주입받는 것을 의미하며 Setter 주입은 setter 함수의 매개변수를 이용하여 객체를 주입받는 것을 의미한다. 이부분을 코틀린에서도 지원한다.

@RestController
class IOCTestController2 @Autowired constructor(val service: IOCTestService) {
    
    @GetMapping("/ioc2")
    fun hello(@RequestParam(value = "name") name: String) = service.sayHello(name)
    
}

 

위의 소스는 생성자의 매개변수로 객체를 주입받는 생성자 주입을 테스트한 코드이다. 코틀린은 생성자가 주 생성자와 보조 생성자로 구분된다. 보조 생성자는 constructor 예약어 클래스로 {} 안에 추가하지만, 주 생성자는 클래스 선언 영역에 ()로 추가한다. 보통 주 생성자는 constructor 예약어를 생략하여 선언하지만 주생성자에 접근 제한자, 어노테이션 등을 함께 선언해야 할 때는 constructor 예약어를 함께 사용하기도 한다.

 

주 생성자 앞에 @Autowired 어노테이션을 추가하면 되는데, 주 생성자의 매개변수가 val로 선언되어 있으면 생성자의 지역변수가 아니라 클래스의 프로퍼티가 된다. 따라서 이렇게 주 생성자를 통해 주입받은 service 객체를 클래스 내부에서 바로 이용할수 있게 되는 것이다.

 

@RestController
class IOCTestController3 {
    
    lateinit var service: IOCTestService
    
    @Autowired constructor(service: IOCTestService) {
        this.service = service
    }
    
    @GetMapping("/ioc3")
    fun hello(@RequestParam(value = "name") name: String) = service.sayHello(name)
}

클래스 선언 부분이 아닌 클래스 몸체에 constructor 예약어로 추가하는것이 보조 생성자인데, 생성자 선언에 @Autowired 어노테이션으로 DI를 명시했다. 단지, 보조 생성자의 매개변수 var, val로 선언할 수 없어 그 자체로 클래스의 프로퍼티가 되지 않는다. 따라서 매개변수로 주입받은 객체를 다시 클래스의 프로퍼티에 대입해 사용한다. 주 생성자를 이용할 수 있다면 주 생성자를 이용해 객체를 주입받는게 조금 더 편해보인다.

 

Setter 주입

Setter 주입이란 클래스의 setter 함수를 이용해 객체를 주입받는 부분이다. 그런데 자바는 크래스의 변수가 필드라서 setter 함수를 명시적으로 선언하고 그 함수 위에 어노테이션을 추가해 객체를 주입받지만, 코틀린은 필드가 아니라 프로퍼티이다. 즉, 변수 자체가 set(), get() 함수를 내장하게 된다. 따라서 자바처럼 명시적으로 setter 함수를 추가할 필요는 없다. 단지, 프로퍼티의 set() 함수를 통해 객체를 주입해 달라는 식으로 표현만 해주면 된다.

@RestController
class IOCTestController4 {
    
    @set:Autowired lateinit var service: IOCTestService
    
    @GetMapping("/ioc4")
    fun hello(@RequestParam(value = "name") name: String) = service.sayHello(name)
}

프로퍼티에 @Autowired를 투가하는 것은 같은데 단지 프로퍼티의 set() 함수를 이용한다는 의미에서 @set:Autowired로 추가한 것만 차이가 있다.


스프링 AOP - 관점지향 프로그래밍

관심들은 핵심 관심과 보조 관심으로 나누어 생각해볼 수 있다. 핵심 관심은 애플리케이션의 주 업무로 보면 되고 보조 관심은 핵심 관심을 위한 부가적인 처리로 보면 된다.

 

예를 들어 은행 업무를 처리하는 시스템을 생각해보면 핵심 관심은 예금 입출금, 계좌 간 이체, 이자 계산, 대출 처리 등이며 보조 관심은 로그 작성(logging)과 보안/인증(security/authentication), 트랜잭션(transaction), 리소스 풀링(resource pooling), 에러 검사(error checking), 정책 적용(policy enforcement), 멀티스레드 안전 관리(multithread safety), 데이터 퍼시스턴스(data persistence) 처리 등을 이야기한다.

 

핵심 관심 처리를 위해 보조 관심이 필요한 것이므로 이를 하나의 코드 내에 작성해야 하는데 AOP의 개념은 이 관심들을 분리해서 로그 처리, 보안, 트랜잭션 관리 그리고 예외사항 처리 등의 코드를 단일 모듈로 각각 작성하고, 필요한 시점에 핵심 코드를 삽입하여 동작하게 하고자 하는 프로그래밍 기법이다.

package com.example.KotlinLabSpring.aop

import org.springframework.stereotype.Service

@Service
class AOPTestService {
    fun sayHello(name: String): String {
        return "Hello $name"
    }
}

위 소스는 Controller에 DI로 이용하고자 하는 주 업무가 구현된 코드이다. 클라이언트의 요청이 있을때 이곳의 sayHello() 함수가 호출되는데, 이 함수 호출 시 특정 로그를 남기고자 한다. 로그 출력을 AOP를 이용하여 분리해 작성하고 실제 sayHello() 함수 호출 때는 로그가 자동으로 출력되게 해본다.

 

package com.example.KotlinLabSpring.aop

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.stereotype.Component

@Aspect
@Component
class LoggingAOP {
    @Before("execution(* com.example.KotlinLabSpring.aop.AOPTestService.sayHello(..))")
    fun beforeLogging() {
        println("beforeLogging......")
    }
}

스프링에서 Advice 선언은 @Aspect 어노테이션을 이용한다. 또한, 스프링 프레임워크에 의해 자동 스캔이 되도록 @Component 어노테이션을 추가한다. 스프링 프레임워크에서 AOP 적용은 내부적으로 CGLIB을 이용한다. CGLIB(Code Generator Library)는 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. 즉, sayHello() 함수 호출때 beforeLogging() 함수가 호출되는 것은 CGLIB에 의해 서브 클래스가 만들어지고 서브 클래스가 이 작업을 대행해주기 때문에 가능하다. 개발자 코드에서는 단순히 @Aspect와 @Before 어노테이션만 추가해주면 되지만, 실제 작업은 CGLIB에 의해 동적으로 만들어지는 서브 클래스가 프록시 역할을 해주기때문에 가능한 것이다.

 

@Aspect 어노테이션이 추가된 class LoggingAOP {} 클래스의 서브 클래스가 자동으로 만들어지게 된다. 코틀린의 클래스는 기본이 final이라 open을 명시적으로 선언하지 않는한 서브 클래스를 만들수 없다. 즉, open을 명시적으로 선언하지 않는 한 서브 클래스를 만들 수 없으며 함수도(위 소스의 beforeLogging)도 open이 추가되어 있지 않으면 서브 클래스에서 재정의할 수 없다.

 

그러므로 원래는 Advice 클래스를 정의하면서 클래스와 함수에 open 예약어를 추가해주어야 한다. 그런데 AOP의 Advice 클래스는 그 자체로 서브 클래스가 동적으로 만들어져 동작하게 하는게 목적이므로 개발자가 일일이 open 예약어를 추가해주는 부분이 상당히 귀찮을 수 있다. 이를 해결하기 위해 allopen을 제공한다. 빌드 환경파일을 보면 allopen이 dependency로 선언되어 있는것을 확인할 수 있다.

dependencies {
    // ...
    classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}

apply plugin: 'kotlin-spring'

build.gradle 파일을 보면 allopen이 dependencies에 선언되어 있는 것을 확인할 수 있다. 그리고 kotlin-spring 플러그인이 등록되어 있는데 이 플러그인이 등록되어 있는데 이 플러그인이 allopen을 내장한 플러그인이다. 이런 설정 때문에 open으로 처리하지 않은 클래스, 함수가 서브 클래스로 만들어지고 함수를 재정의하게 된다. 

 

포인트컷

위에 선언한 @Before는 Advice가 적용될 특정 시점을 정의하기 위한 어노테이션으로 이를 포인트컷(pointcut)이라고 부른다.

@Before("execution(* com.example.KotlinLabSpring.aop.AOPTestService.sayHello(..))")

포인트컷을 지정할 때 앞부분(*)은 호출되는 함수의 반환 타입이다. *로 표현했으므로 모든 반환 타입을 의미한다. com.example.KotlinLabSpring.aop 부분은 패키지명이고 AOPTestService는 클래스명이다. 그리고 sayHello가 함수명이며 (..)부분이 매개변수이다. (..)으로 표현했으므로 매개변수가 어떻게 선언되었든 상관하지 않겠다는 의미다.

@Aspect
@Component
class LoggingAOP {
    @Before("execution(* com.example.KotlinLabSpring.aop.AOPTestService.*(..))")
    fun beforeLogging() {
        println("beforeLogging......!!!")
    }
    
    @Before("execution(* com.example.KotlinLabSpring.aop.AOPTestService.*(String))")
    fun beforeLogging2() {
        println("beforeLogging2......!!!")
    }
}

beforeLogging() 함수의 조건은 AOPTestService 클래스의 모든 함수 호출을 의미하며 beforeLogging2()의 조건은 *(String)으로 작성되어있고 이는 AOPTestService의 함수 중 매개변수가 문자열 타입 하나가 선언된 함수를 지칭한다.

 

포인트컷 지정은 @Before만 있는 것이 아니라 @AfterReturning, @AfterThrowing, @Around 등이 있다. @Before는 함수 호출 전을 지칭하며 @AfterReturning은 함수 호출 후, @AfterThrowing은 함수에서 예외를 전달받았을 때(Exception throw), @Around는 함수 호출 자체를 가로채기 위한 어노테이션이다.

@AfterReturning(pointcut = "execution(* com.example.KotlinLabSpring.aop.AOPTestService.*(..))",
    returning = "retVal")
fun afterLogging(retVal: Any) {
    println("afterLogging $retVal")
}

@AfterThrowing("execution(* com.example.KotlinLabSpring.aop.AOPTestService.*(..))")
fun afterThrowing() {
    println("afterThrowing....")
}

@Around("execution(* com.example.KotlinLabSpring.aop.AOPTestService.*(String))")
fun around(pjp: ProceedingJoinPoint): Any {
    println("around before....")
    val returnVal = pjp.proceed()
    println("around after....")
    return returnVal
}

@AfterReturning 어노테이션을 지정하면서 포인트컷으로 적용할 함수의 조건을 명시했다. 이 어노테이션이 함수 호출 후에 실행되므로 호출한 함수의 반환값을 받아서 처리하는 것이 일반적이다. 이를 위해 어노테이션 정보에 returning 속성으로 반환값을 저장할 변수명을 지정했다. 이렇게 선언해 놓으면 함수 호출 후 반환되는 값이 returning에 지정한 이름의 변수에 대입된다.

 

@Around 어노테이션으로 지정하면 Target 클래스 내에 함수 호출 시 무조건 이 함수가 실행되고, 이 함수의 실행 구문에 의해 Target 클래스의 함수가 실행될 수도 있고 안될 수도 있다. 그러므로 @Around 함수 내에서 실제 Target 클래스를 실행해야 하는데 이 작업을 위해서 ProceedingJointPoint 객체가 매개변수로 전달된다. 만약 코드 내에서 Target 클래스의 실제 호출된 함수를 호출하려면 pjp.proceed(); 라는 구문을 써야 한다.


스프링 MVC

MVC는 웹 애플리케이션에 유명한 모델이다. 웹 애플리케이션에서만 사용되는 모델은 아니며, 애플리케이션이 화면 출력을 목적으로 하는 곳에 널리 사용된다. MVC 모델은 서버 측 웹 애플리케이션에서도 널리 사용된다.

 

MVC 모델은 하나의 업무 처리를 Model, View, Controller로 역할을 분리해서 개발하고자 하는 모델이다.

  - Model : 비즈니스 업무로직과 업무에 의한 데이터 표현이 주목적

  - View: 화면을 목적. 서버측 웹 애플리케이션이라면 View는 대부분 HTML 파일로 만들게 된다.

  - Controller: 클라이언트 요청 분석. 대표적인 분석이 요청 URL 분석이 된다.

 

스프링 MVC를 이용하여 클라이언트 전송 결과를 HTML 파일로 전송하는 방법에 대해 살펴본다. 스프링 개발환경에 Web과 Thymeleaf를 포함한다.

dependencies {
    // ...
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
}

spring-boot-starter-web은 스프링 프레임워크에서 제공하는 웹개발을 위한 풀스택(full stack)이며 thymeleaf는 템플릿 엔진이다. 클라이언트 요청으로 정적인 HTML 서비스를 한다면 필요가 없지만, 동적 데이터를 HTML 페이지에 출력하겠다면 템플릿 엔진이 필요하며 thymeleaf는 그중 하나의 엔진이다. HTML, XML, javascript, CSS 등을 동적 데이터를 포함하여 생성할 수 있는 템플릿 엔진이며 스프링 프레임워크와 연동 모듈을 제공한다. 스프링 프레임워크를 이용한 MVC 구현 시 필수라고 볼 수는 없지만 HTML 페이지를 양산하는 템플릿 엔진은 필요하므로 thymeleaf를 이용하여 쉽게 HTML 페이지를 만들어 낼 수 있다. thymeleaf를 사용하지 않고 JSP를 이용하여 HTML을 만들어 낼 수도 있다.

 

package com.example.KotlinLabSpring.mvc

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
class MVCController {
    
    @RequestMapping("/mvc")
    fun doIndexGet(): String {
        return "index"
    }
}

@Controller 어노테이션에 의해 클래스가 스프링의 Bean으로 등록된다. @RestController 어노테이션은 Restful 서비스를 목적으로 하는 Bean에 추가되는 어노테이션이며, 클라이언트 요청에 의한 HTML 전송이 주목적인 스프링 MVC 테스트이므로 @Controller 어노테이션을 추가했다.

 

Thymeleaf에 의한 동적 데이터 출력

간단하게 스프링 MVC를 이용하여 HTML 파일을 클라이언트에 서비스하는 방법을 살펴보았는데 이번에는 소스의 데이터를 HTML에 출력하는 방법을 살펴보도록 한다. 간단하게 클라이언트에서 넘어오는 데이터를 받아 결과 HTML에 출력하는 테스트이다.

package com.example.KotlinLabSpring.mvc

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping

data class MyRequestParams(var no: Int = 0, var name: String = "")

@Controller
class MVCController {
    
    @RequestMapping("/mvc")
    fun doIndexGet(params: MyRequestParams, model: Model): String {
        
        model.addAttribute("params", "Hello ${params.no} - ${params.name}")
        return "index"
    }
}

위의 소스는 클라이언트 요청시 실행되는 Controller 클래스이다. 클라이언트 요청시 전달되는 데이터를 받기 위해 data 클래스를 선언했는데, 원래 클라이언트 데이터를 하나하나 받아 data 클래스에 대입해야 하는데 만약 클라이언트의 데이터 key 값과 data 클래스의 프로퍼티 이름이 같다면 data 클래스의 객체를 클라이언트 요청시 실행되는 함수의 매개변수로 지정만 해주면 된다. 그러면 자동으로 클라이언트 데이터가 data 클래스의 객체가 생성되면서 대입된다.

 

클라이언트 데이터를 받는 data 클래스를 만들때 주의할 점은 data 클래스의 프로퍼티를 val로 선언하면 안되고, var로 선언해야 한다. 그리고 반드시 프로퍼티의 default 값을 선언해야 한다.

 

HTML에 출력할 데이터를 담을 Model 객체를 자동으로 지원한다. Model 객체는 클라이언트 요청시 실행되는 함수의 매개변수를 통해 받을 수 있다. HTML에 출력할 데이터가 있다면 Model 객체에 addAttribute() 함수를 이용하여 대입만 해놓으면 된다. Controller에서 Model에 출력 데이터를 담아 주었다면 HTML에서 thymeleaf 표현식을 이용하여 HTML에 데이터를 출력하면 된다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
</head>
<body>
    <h1>Hello Spring MVC with Kotlin</h1>
    <h3 th:text="${params}"></h3>
</body>
</html>

Thymeleaf를 이용하기 위해 namespace를 선언하고 선언된 namespace를 이용하여 데이터 출력 표현식을 사용하면 된다. namespace 명에 th로 선언되었으므로 th:text="${params}"로 사용했다. ${}는 Controller 클래스에서 Model에 담아놓은 데이터의 key 값이다. 이렇게 하면 이 위치에 key 값으로 등록된 value의 값이 출력된다.

 

위에서 클라이언트로부터 넘어오는 데이터를 data 클래스에 담고 그 데이터를 그대로 HTML에 출력했다. 이번에는 Controller 클래스의 객체를 그대로 HTML에 출력하는 방법에 대해 살펴본다. 데이터 출력이라는 측면에서 같은 형태이지만 Model에 객체 자체를 담아 HTML에서 활용할 수 있다.

package com.example.KotlinLabSpring.mvc

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping

data class MyRequestParams(var no: Int = 0, var name: String = "")

data class MyModelData(var data1: String = "", var data2: String = "")

@Controller
class MVCController {
    
    @RequestMapping("/mvc")
    fun doIndexGet(params: MyRequestParams, model: Model): String {
        model.addAttribute("params", "Hello ${params.no} - ${params.name}")
        model.addAttribute("myModel", MyModelData("hello", "world"))
        return "index"
    }
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
    <h1>Hello Spring MVC with Kotlin</h1>
    <h3 th:text="${params}"></h3>
    
    <table th:object="${myModel}">
        <tr>
            <th>Id</th>
            <th>Name</th>
        </tr>
        <tr>
            <td th:text="*{data1}"></td>
            <td th:text="*{data2}"></td>
        </tr>
    </table>
</body>
</html>

th:object="${myModel}" 로 객체명을 명시하고 하위 태그에서 이 객체의 프로퍼티명을 *{} 표현식으로 출력하는 구조이다.