ABOUT ME

-

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

    댓글

Designed by Tistory.