카테고리 없음

spring boot 와 scala의 만남

머룽 2023. 4. 18. 12:31

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와 스칼라의 만남은 여기서... 나중엔 블로그를 한개 만들어 봐야겠다.