spring boot 와 scala의 만남

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 JpaRepository's 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와 스칼라의 만남은 여기서…

나중엔 블로그를 한개 만들어 봐야겠다.

Spring boot와 Docker Postgresql

Spring boot와 Docker Postgresql

이번엔 docker와 posrgresql에 대해 알아보겠다.
1. centos 6에 docker를 설치해보자
2. Spring boot와 Docker
3. Spring boot와 Docker Mysql

mysql과 소스는 동일하다.

@SpringBootApplication
public class SpringBootDockerPostgresqlApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootDockerPostgresqlApplication.class, args);
    }

    @Autowired
    private AccountRepository repository;

    @Bean
    CommandLineRunner runner() {
        return args -> Arrays.asList(
                new Account(1L,"wonwoo","wonwoo@test.com"),
                new Account(2L,"kevin","kevin@test.com"),
                new Account(3L,"mink","mink@test.com")
        ).forEach(repository::save);
    }
}

Account class

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String email;

}

AccountController class

@Slf4j
@RestController
public class AccountController {

    @Autowired
    private AccountRepository repository;

    @RequestMapping(value = "/accounts", method = RequestMethod.GET)
    public ResponseEntity<?> getAccount() {
        List<Account> accounts = repository.findAll();
        log.debug("accounts : {} ", accounts);
        return new ResponseEntity<>(accounts, HttpStatus.OK);
    }
}

AccountRepository class

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
}

따로 설명은 하지 않겠다.
mysql과 다른점은 yml 파일과 maven이다.

application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    url: jdbc:postgresql://${POSTGRES_PORT_5432_TCP_ADDR}:${POSTGRES_PORT_5432_TCP_PORT}/docker
    username: dbuser
    password: dbpassword
    driver-class-name: org.postgresql.Driver

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.4-1200-jdbc4</version>
</dependency>

일단 postgresq pull 하자

docker run --name spring-boot-postgres -e POSTGRES_PASSWORD=dbpassword -e POSTGRES_DB=docker -d postgres

latest: Pulling from postgres
00b8e8227087: Pull complete 
1ad190f65f04: Pull complete 
60f2f5fb5bf9: Pull complete 
2d0c73368a74: Pull complete 
a786535117f1: Pull complete 
dd182ad340e4: Pull complete 
c05a4dbabb61: Pull complete 
d147dcf8581e: Pull complete 
358e57fb3884: Pull complete 
9c4b8b3535ba: Pull complete 
adf658a6b808: Extracting [==================================================>] 41.23 MB/41.23 MB
0fc677feeba3: Download complete 
0dceace87c23: Download complete 
23b5786d3dce: Download complete 
9b00a7295803: Download complete 
f88f356d246d: Download complete 
d6fb5e6e70f6: Download complete 
07e6c1333fa3: Download complete 
6f960967c8bf: Download complete 
d8bd0657b25f: Already exists 
a582cd499e0f: Already exists 

위와 같이 이미지를 생성한다.

docker images

이미지가 잘 있나 보자

postgres                    latest              6f960967c8bf        5 days ago          264.5 MB

잘있다.

다음으로 메이븐 빌드를 하자!

mvn clean package docker:build

빌드가 잘 되었다면

docker를 실행시키자

docker run --name spring-boot-docker-postgresql --link spring-boot-postgres:postgres -p 9000:8080 -d wonwoo/spring-boot-docker-postgresql

로그를 확인하기 위해 아래와 같이 입력하자

docker logs $CONTAINER_ID 

잘되었다면 브라우저를 열어 확인해보자
http://localhost:8080/accounts

그리고 postgres 클라이언트 연결도 가능하다.

docker run -it --link spring-boot-postgres:postgres --rm postgres sh -c 'exec psql -h "$POSTGRES_PORT_5432_TCP_ADDR" -p "$POSTGRES_PORT_5432_TCP_PORT" -U postgres'

위와 같이하면 접속도 가능하다.

postgres=# \l

                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
-----------+----------+----------+------------+------------+-----------------------
 docker    | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(4 rows)

디비 목록이다.

postgres=# \connect docker

선택을 해보자

docker=# \d

테이블을 보자. account 테이블이 잘 있다.

                 List of relations
 Schema |        Name        |   Type   |  Owner   
--------+--------------------+----------+----------
 public | account            | table    | postgres
 public | hibernate_sequence | sequence | postgres
(2 rows)

한번씩 해보자!

Spring boot와 Docker Mysql

Spring boot와 Docker Mysql

  1. centos 6에 docker를 설치해보자
  2. Spring boot와 Docker

이번엔 도커에 mysql을 연동해보자.
연동이 되는지 확인해보기 위해 몇개의 클래스를 생성하자
간단하게 만들자

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String email;

}

Account 엔티티 클래스다.

@Slf4j
@RestController
public class AccountController {

    @Autowired
    private AccountRepository repository;

    @RequestMapping(value = "/accounts", method = RequestMethod.GET)
    public ResponseEntity<?> getAccount() {
        List<Account> accounts = repository.findAll();
        log.debug("accounts : {} ", accounts);
        return new ResponseEntity<>(accounts, HttpStatus.OK);
    }
}

controller 클래스도 한개 만들자.
마지막으로 레파지토리도 생성하자.

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
}

그리고 메이븐에 mysql connector를 추가해야된다.

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

그리고 yml 파일에 아래와 같이 추가하자

spring:
  jpa:
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.MySQLDialect
  datasource:
    url: jdbc:mysql://${MYSQL_PORT_3306_TCP_ADDR}:${MYSQL_PORT_3306_TCP_PORT}/${MYSQL_ENV_MYSQL_DATABASE}
    username: ${MYSQL_ENV_MYSQL_USER}
    password: ${MYSQL_ENV_MYSQL_PASSWORD}
    driver-class-name: com.mysql.jdbc.Driver
    validation-query: select 1

마지막으로 테스트 데이터를 넣자 Main 클래스에 넣자

@Autowired
private AccountRepository repository;

@Bean
CommandLineRunner runner() {
    return args -> Arrays.asList(
            new Account(1L,"wonwoo","wonwoo@test.com"),
            new Account(2L,"kevin","kevin@test.com")
    ).forEach(repository::save);
}

일단 설정은 끝났다.
그담 할일은 mysql을 pull 해오자.

docker run -d --name spring-boot-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=docker -e MYSQL_USER=dbuser -e MYSQL_PASSWORD=dbpassword -d mysql:latest

mysql dockerhub 참고

그리고 이미지가 존재 하는지 확인해보자.

docker images

java                        8                   31e7de89e3f8        5 days ago          642.3 MB
mysql/mysql-server          latest              26d67fdd58d2        4 weeks ago         294.8 MB
hello-world                 latest              975b84d108f1        4 months ago        960 B

이런식으로 나올거다.
그럼 잘된거다.
그런 다음에 다시 빌드를 하자.

mvn clean package docker:build

이상하게 첨엔 잘안되더니 docker를 재시작하니 또 된다.
이상하다. 리눅스가 클라우드서버라 약간 이상한듯 싶기도 하고 커널 버전도 안올라가 있어서..영 찜찜…아무튼
만약 빌드가 잘 안된다며 이것저것 다 해봐야댄다. 리부팅도 하고 막 암튼..

이제 spring boot를 실행해 보자.
spring이랑 mysql이랑 link를 걸어줘야된다.

docker run --name spring-boot-docker --link spring-boot-mysql:mysql -p 9000:8080 -d wonwoo/spring-boot-docker

요런식의 형태다.아래와 같이 하면 된다.
name은 컨테이너 명 아무거나 넣으면 댄다.
link는 저기 mysql container 명이다.

docker run --name app-container-name --link my-container-name:mysql -d app-that-uses-mysql

근데 필자는 No route to host이라는 에러가 자꾸 떳다.
찾아보니까

iptables -t filter -A DOCKER -d 172.17.0.0/16 -i docker0 -j ACCEPT

이렇게 하니까 된다.
원래 도커가 iptables을 건드는것도 같은데…아닌가?
아무튼 일단 된다.

필자의 처음으로 만든 dockerhub다

docker pull wonwoo/spring-boot-docker

참고

docker inspect $CONTAINER_ID  | grep IPAddress #컨테이너 아이피를 알아내는거다.
docker push $user-name/$app-container-name #도커 hub에 푸시