spring boot 와 scala의 만남
스칼라를 공부할겸 겸사겸사 스칼라로 spring boot 프로젝트를 해봤다.
근데 딱히 스칼라를 제대로 쓰진 못한듯 하다. 흠 아직 왕초보라 그런지 그래도 나름 도움은 된듯 싶다.
뭔가를 만드니까 그래도 조금은 도움은 됐다.
한번 살펴보자
일단 메이븐을 추가 하자. 그래들은 잘 할 줄 몰라서.. 언젠가 공부를 해야겠다. 일단 나중에.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>0.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
메이븐은 일반 Spring Boot 프로젝트다. jpa와 메모리 디비를 사용할 거다.
일단 엔티티 클래스를 보자
@Entity
class Account{
@Id
@GeneratedValue
@BeanProperty
var id: java.lang.Long = _
@BeanProperty
@NotNull
@Size(max = 100, min = 3)
var name: String = _
@BeanProperty
var password: String = _
}
흠 딱히 눈에 띄는건 없고
@BeanProperty
만 보인다. 나머지는 JPA니 다 알듯 싶다. JPA도 공부해야되는데.
책만 사놓고 조금 읽다 실습도 조금하다...멈췄지만 다시 공부해야ㅜㅜ 어쩌됬건...
BeanProperty 는 좋은 어노테이션이다.
이것은 자바스타일의 getter setter를 만들어 주는 어노테이션이다.
자동으로 getter와 setter가 생겼다.
@RestController
class AccountController @Autowired()(accountRepository: AccountRepository, accountService: AccountService) {
@RequestMapping(value = Array("/accounts"), method = Array(RequestMethod.GET))
@ResponseStatus(HttpStatus.OK)
def accounts(pageable: Pageable) = accountRepository.findAll(pageable)
@RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.GET))
@ResponseStatus(HttpStatus.OK)
def account(@PathVariable id: Long) = accountService.account(id)
@RequestMapping(value = Array("/account/search/{name}"), method = Array(RequestMethod.GET))
@ResponseStatus(HttpStatus.OK)
def account(@PathVariable name: String) = accountRepository.findByName(name)
@RequestMapping(value = Array("/account"), method = Array(RequestMethod.POST))
@ResponseStatus(HttpStatus.CREATED)
def createAccount(@Valid @RequestBody account: Account, bindingResult: BindingResult) = {
if (bindingResult.hasErrors) throw BadRequestException(bindingResult.getFieldError.getDefaultMessage)
accountService.save(account)
}
@RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.PATCH))
@ResponseStatus(HttpStatus.OK)
def updateAccount(@PathVariable id: Long, @Valid @RequestBody account: Account, bindingResult: BindingResult) = {
if (bindingResult.hasErrors) throw BadRequestException(bindingResult.getFieldError.getDefaultMessage)
accountService.update(id, account)
}
@RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.DELETE))
@ResponseStatus(HttpStatus.NO_CONTENT)
def deleteAccount(@PathVariable id: Long) = {
accountService.delete(id)
}
}
다음은 컨트롤러다.
생성자에 Repository 와 Service를 DI 받았다.
일단 중요한건 메소드들이 짧다.
오히려 어노테이션이 더 많다. 컨트롤러에선 딱히 하는건 없어서 그런가부다. 파라미터 체크정도만 하고 서비스 혹은 레파지토리로 넘긴다.
어딜 봐도 다 스프링 코드다...ㅜㅜㅜㅜㅜㅜ 이래서 스칼라 코드가 별루 없다.흠
필자는 비지니스 로직이 딱히 없을때는 바로 레파지토리로 넘긴다. 물론 혼자 개발할때 이야기다. 그게 더 효율적이지 않나 싶다. (필자 생각)
다음은 서비스 코드다
@Service
@Transactional
class AccountService @Autowired()(accountRepository: AccountRepository) {
@Transactional(readOnly = true)
def account(id: Long): Account = {
Option(accountRepository.findOne(id)) getOrElse (throw AccountNotFoundException(s"account id $id not found"))
}
def save(account: Account) = {
accountRepository.save(account)
}
def update(id: Long, account: Account) = {
val oldAccount = this.account(id)
account.setId(oldAccount.getId)
if (!Option(account.getName).exists(_.nonEmpty))
account.setName(oldAccount.getName)
if (!Option(account.getPassword).exists(_.nonEmpty))
account.setPassword(oldAccount.getPassword)
accountRepository.save(account)
}
def delete(id: Long) {
accountRepository.delete(id)
}
}
getOrElse
는 null이 아닐경우 Option() 에 들어간아이를 리턴하고 아니면 getOrElse 뒤에 있는 아이를 리턴한다.
하지만 필자는 에러를 내뿜었다.
update같은 경우엔 PATCH 메소드를 사용했다 요즘은 PATCH도 많이 사용된다고 하길래 써봤다. 뭐 어차피 비슷한지 않나 싶다
부분 업데이트냐 전체 업데이트냐 그차이 뿐이지만
그래서 저렇게 구현했다.
null 이 아니거나 비어있지 않으면 기존꺼를 넣어주고 아니면 새로운거 업데이트를 한다.
@Repository
trait AccountRepository extends JpaRepository[Account, java.lang.Long] {
def findByName(name: String): Account
}
저기 Long 타입을 왜 자바꺼 썼냐면 JPA에서 에러를 내뱉는다.
... do not conform to trait JpaRepositorys type parameter bounds [T,ID <: java.io.Serializable]
Serializable
가 안되어 있다고 하는듯 하다. 그래서 자바껄로 했다.
기본적인 구현은 다 됐다.
테스트를 해보자
@RunWith(classOf[SpringJUnit4ClassRunner])
@SpringApplicationConfiguration(Array(classOf[SpringBootConfig]))
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)
class AccountTest {
var objectMapper: ObjectMapper = _
var mockMvc: MockMvc = _
@Autowired
var wac: WebApplicationContext = _
@Before
def before = {
objectMapper = new ObjectMapper
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build
}
@Test
def accountsTest: Unit = mockMvc.perform(get("/accounts")).andDo(print()).andExpect(status.isOk)
@Test
def accountTest: Unit =
mockMvc.perform(get("/account/1"))
.andDo(print())
.andExpect(status.isOk)
.andExpect(jsonPath("$.name", is("wonwoo")))
.andExpect(jsonPath("$.password", is("pw123000")))
@Test
def creatTest: Unit = {
val account = new Account
account.setName("create")
account.setPassword("create123")
mockMvc.perform(post("/account")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(account)))
.andExpect(status.isCreated)
.andDo(print())
}
@Test
def updateTest: Unit = {
val account = new Account
account.setName("wonwoo1")
mockMvc.perform(patch("/account/1")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(account)))
.andDo(print())
.andExpect(status.isOk)
.andExpect(jsonPath("$.name", is("wonwoo1")))
.andExpect(jsonPath("$.password", is("pw123000")))
}
@Test
def deleteTest: Unit =
mockMvc.perform(delete("/account/2").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status.isNoContent)
@Test
def accountNotFoundExceptionTest: Unit = mockMvc.perform(get("/account/10")).andDo(print()).andExpect(status.isBadRequest)
@Test
def accountBadRequestExceptionTest: Unit = {
val account = new Account
account.setName("wl")
mockMvc.perform(post("/account").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(account)))
.andDo(print())
.andExpect(status.isBadRequest)
}
}
그냥 기본적인 테스트 케이스다.
Junit
은 원래 자기 마음 대로 테스트 한다.
그래서 순서를 정하고 싶었다. 마침 있다. 이번에 새로 알았다.
@FixMethodOrder(MethodSorters.JVM)
요 어노테이션이다.
JVM
은 있는 메소드 순서대로 테스트 하는 모양이다.
이거 말고도 한개가 더 있다. DEFAULT도 있는데 이건 그냥 기본인듯 하다.
NAME_ASCENDING
속성이다. 이거는 메소드 명 순서대로 테스트 케이스를 수행한다.
보니까 스칼라 코드가 너무 없어서 아쉽다. 나중엔 좀더 복잡한거를 해봐야겠다.
그래도 많이 도움되어서 다행이다.
이 전체 코드는 github에 올라가 있다.
https://github.com/wonwoo/spring-boot-scala.git
docker 에도 올려놨다.
배운거 다 써먹는다.ㅎㅎㅎㅎ
docker pull wonwoo/spring-boot-scala
그럼 spring boot와 스칼라의 만남은 여기서...
나중엔 블로그를 한개 만들어 봐야겠다.