Spring Boot 1.4 Test

이번 시간에는 Spring Boot 1.4부터 추가된 Test를 알아보자

Spring Boot 1.3 에서는 이런 어노테이션을 붙어서 테스트를 진행했다.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class SimpleTestCase {
}

하지만 1.4.부터는 좀더 심플하게 바뀌었다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleTestCase{
}

좀더 간판해졌다는걸 알수 있다.
1.4 부터 추가된 @MockBean 이라는 어노테이션이 있다. 가짜 객체를 만들어 테스트할 수 있게 만들어준다. 한번 코드를 보자.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootTestApplicationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @MockBean
    public UserService userService;

    @Test
    public void userRestTemplateTest() throws Exception {
        User user = new User(1L, "wonwoo", "wonwoo@test.com", "123123");
        given(userService.findOne(1L)).willReturn(user);
        ResponseEntity<User> userEntity = this.restTemplate.getForEntity("/findOne/{id}", User.class, 1);
        User body = userEntity.getBody();
        assertThat(body.getName()).isEqualTo("wonwoo");
        assertThat(body.getEmail()).isEqualTo("wonwoo@test.com");
        assertThat(body.getPassword()).isEqualTo("123123");
        assertThat(body.getId()).isEqualTo(1);
    }
}

@Autowired 와 비슷하게 사용하면 된다. @MockBean 어노테이션만 붙어주면 된다. 그럼 실제 객체가 아닌 가짜 객체가 주입된다.
그러고나서 호출할 때와 리턴값을 가짜 객체를 만들어서 넣어주고 호출해주면 된다. 그러면 실제 UserService 호출하는 어느곳에서 가짜객체가 만들어져서 위와 같은 리턴값을 받게된다.
위의 예제는 TestRestTemplate을 이용했는데 MockMvc를 이용해도 된다.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

  @Autowired
  private MockMvc mvc;

  @MockBean
  public UserService userService;

  @Test
  public void userTest() throws Exception {
    given(userService.findOne(1L)).willReturn(new User(1L, "wonwoo", "wonwoo@test.com", "123123"));
    mvc.perform(get("/findOne/{id}", 1L).accept(MediaType.APPLICATION_JSON))
      .andDo(print())
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.id", is(1)))
      .andExpect(jsonPath("$.name", is("wonwoo")))
      .andExpect(jsonPath("$.email", is("wonwoo@test.com")))
      .andExpect(jsonPath("$.password", is("123123")))
      .andExpect(content().json("{'id':1,'name':'wonwoo','email':'wonwoo@test.com','password':'123123'}"));
  }
}

아까의 예제와 비슷하게 UserService에 가짜객체를 주입해서 테스트를 했다. 뭐 나머지는 예전에 테스트 포스팅을 할때 했던거라 굳이 설명하지는 않겠다.

다음은 @DataJpaTest 인데 Jpa를 테스트 할 때 사용하면 될듯하다. 바로 예제를 보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private UserRepository repository;

  @Test
  public void findByNameTest() {
    this.entityManager.persist(new User("wonwoo", "wonwoo@test.com","123123"));
    User user = this.repository.findByname("wonwoo");
    assertThat(user.getName()).isEqualTo("wonwoo");
    assertThat(user.getEmail()).isEqualTo("wonwoo@test.com");
    assertThat(user.getPassword()).isEqualTo("123123");
    assertThat(user.getId()).isEqualTo(1);
  }
}

TestEntityManager 라는 클래스는 Spring 1.4부터 추가 되었다. 이것은 주입받아서 테스트를 할 수 있다. persist를 해도 실제 디비에는 저장 되지 않는듯 하다. 정말 Test를 위해서 만들어진듯하다. 필자는 @DataJpaTest를 잘 사용하지 않겠지만 나머지 두개는 많이 사용할 듯하다.
이외에도 Json을 테스트 할 수 있는 것들도 존재 한다. 딱히 필자는 필요 없어서 하지는 않았다.
@JsonTest 어노테이션과 JacksonTester클래스를 한번 살펴보면 되겠다. 관심있는 분들은 한번 살펴보면 좋을 것 같다.
필자는 @MockBean 어노테이션이 젤 맘에 든다. 좀 더 쉽게 테스트를 할 수 있어 좋다.
간단한 소스도 여기에 올려놨으니 한번 살펴보면 도움이 많이 될듯하다.

Spring Jpa java8 date (LocalDateTime) 와 Jackson

제목이 거창하지만 별거 없다.
Spring data jpa와 java8에 추가된 LocalDateTime 설정과 그에 맞게 json으로 보낼 Jackson 설정을 알아볼 예정이다.
현재 JPA2.1은 java8의 date(LocalDateTime) 를 지원하지 않는다. 아마도 java8이 릴리즈 되기 전에 JPA2.1이 먼저 나와서 일것이다.
java8 이전의 Date API는 엉망진창이다.(물론 내가 잘만드는건 아니지만..) thread-safe 하지도 않고 API가 직관적이지도 않다. 그래서 우리는 Third-party 라이브러리인 Joda-time을 많이 쓰곤했다. 하지만 java8에 추가된 date는 Joda-time의 저자와 오라클 주도하에 JSR310 표준에 맞춰 개발 하였다.
아무튼 이걸 말하고자 하는건 아닌데.. 그럼 한번 살펴보자.

일단 우리는 java8의 추가된 LocalDateTime과 DB와의 연결고리를 만들어야 한다.
직접적으로 컨버터를 만들어도 되지만 Spring에서 만든 컨버터가 있다. 필자는 거의 최신이라 버전이 낮으면 없을 수도 있으니 여기를 참고해서 만들자. 아마도 직접적으로 만들면 딱히 설정할 필요가 없는데 만약 Spring에서 만든 컨버터를 사용하려면 약간의 설정이 필요하다.

@SpringBootApplication
@EntityScan(basePackageClasses = {Application.class, Jsr310JpaConverters.class} )
public class Application {

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

EntityScan에 위와 같이 넣어주면 끝난다. 그럼 일단 DB와는 컨버터가 되었다.
한번 데이터베이스에 잘 들어가나 확인해보자. 아마도 문제 없이 잘 들어갈 것이다. 왜냐면 필자가 잘 되었기 때문이다. ㅎㅎ
컨버터는 되었으니 View에 뿌려줄 json 데이터가 문제다. 처음엔 그냥 있는 그대로 출력해보자.
출력한 결과는 다음과 같다.

{  
  "id":1,
  "name":"tt",
  "password":"111",
  "email":"123",
  "localDateTime":{  
    "hour":22,
    "minute":47,
    "second":34,
    "nano":158000000,
    "dayOfYear":194,
    "dayOfWeek":"TUESDAY",
    "month":"JULY",
    "dayOfMonth":12,
    "year":2016,
    "monthValue":7,
    "chronology":{  
      "id":"ISO",
      "calendarType":"iso8601"
    }
  }
}

조금 그렇다. 물론 이렇게 써도 되겠지만(?) 필자는 컨버터가 하고 싶었다. 컨버터 방법역시 간단하다.
일단 메이븐에 아래와 같이 추가하자.

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

필자는 Spring Boot를 쓰는데 저것 역시 Version을 Boot에서 관리해준다. 그래서 명시 하지 않았다. Boot를 쓰지 않는다면 버전을 명시해야 한다. 버전은 인터넷에서.. 현재 필자의 버전은 2.8.0이다.
Spring boot의 경우에는 일단 메이븐에만 추가해도 내용이 바뀌어 출력된다. Boot가 아닌경우에는..잘… 따로 설정을 해야 하나?..

메이븐을 추가하고 다시 테스트를 해보면 아래와 같이 출력된다.

{
  "id": 1,
  "name": "tt",
  "password": "111",
  "email": "123",
  "localDateTime": [
    2016,
    7,
    12,
    22,
    47,
    34,
    158000000
  ]
}

아까보다는 나아졌다고 할 수 있지만 그래도 영 시원찮다. 좀 더 깔끔한 방법으로 출력하고 싶다. 그럼 아래와 같이 조금 설정을 해줘야한다.

@Bean
public ObjectMapper objectMapper() {
  return Jackson2ObjectMapperBuilder
    .json()
    .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .modules(new JavaTimeModule())
    .build();
}

Spring boot를 쓰면 위와 같이 하지 않고 properties나 yaml 파일에 아래와 같이 넣자.

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false

그럼 아래와 같이 ISO 8601 포멧으로 바뀐다. 요즘 Rest API에 많이 쓴다고 한다.

{
  "id": 1,
  "name": "tt",
  "password": "111",
  "email": "123",
  "localDateTime": "2016-07-12T22:47:34.158"
}

이제 깔끔하게 되었다. LocalDateTime도 알아봤으니 나머지 Date(LocalDate, LocalTime) 등들도 어떻게 출력 되는지 한번 살펴 보자.


private ZonedDateTime zonedDateTime; private LocalDate localDate; private LocalTime localTime; private OffsetDateTime offsetDateTime;

이 외에도 좀 더 존재하는거 같지만 위에 4개만 더 테스트 해보자.

메이븐에 jackson에 추가 히지 않았을 경우 (아무 설정 하지 않았을경우)

{
  zonedDateTime: {
    offset: {
      totalSeconds: 32400,
      id: "+09:00",
      rules: {
        fixedOffset: true,
        transitions: [

        ],
        transitionRules: [

        ]
      }
    },
    zone: {
      id: "Asia/Seoul",
      rules: {
        fixedOffset: false
      }
     //.. zonedDateTime long
     //..
     //..
    }
  },
  localDate: {
    year: 2016,
    month: "JULY",
    dayOfYear: 194,
    dayOfWeek: "TUESDAY",
    leapYear: true,
    dayOfMonth: 12,
    monthValue: 7,
    chronology: {
      id: "ISO",
      calendarType: "iso8601"
    },
    era: "CE"
  },
  localTime: {
    hour: 23,
    minute: 35,
    second: 30,
    nano: 847000000
  },
  offsetDateTime: {
    offset: {
      totalSeconds: 32400,
      id: "+09:00",
      rules: {
        fixedOffset: true,
        transitions: [

        ],
        transitionRules: [

        ]
      }
    },
    dayOfYear: 194,
    dayOfWeek: "TUESDAY",
    month: "JULY",
    dayOfMonth: 12,
    year: 2016,
    monthValue: 7,
    hour: 23,
    minute: 35,
    second: 30,
    nano: 847000000
  }
}

zonedDateTime 은 너무 길어서 일부만 가져와서 보여줬다.

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS 설정을 제거 했을 경우

{
  id: 1,
  name: "tt",
  password: "111",
  email: "123",
  localDateTime: [
    2016,
    7,
    12,
    23,
    35,
    30,
    847000000
  ],
  zonedDateTime: 1468334130.847,
  localDate: [
    2016,
    7,
    12
  ],
  localTime: [
    23,
    35,
    30,
    847000000
  ],
  offsetDateTime: 1468334130.847
}

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS 설정을 사용 했을 경우

{
  id: 1,
  name: "tt",
  password: "111",
  email: "123",
  localDateTime: "2016-07-12T23:35:30.847",
  zonedDateTime: "2016-07-12T23:35:30.847+09:00",
  localDate: "2016-07-12",
  localTime: "23:35:30.847",
  offsetDateTime: "2016-07-12T23:35:30.847+09:00"
}

참고로 아무 설정 하지 않았을 경우 zonedDateTime, offsetDateTime 은 DB에 BLOB로 매핑된다.
원래는 금방 포스팅했는데 컴터가 맛탱이가서 좀 오래 걸렸다. 컴터를 바꿀때가 된듯하다…

Spring data JPA

JPA에 대해서 많은 포스팅은 한듯하다. 하지만 계속 계속 봐야겠다. 할때마다 까먹는다.
오늘은 Spring data jpa를 포스팅해보자.

Spring data JPA

Spring data jpa는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도로고 제공해주는 프로젝트이다. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 조금더 세련된 방법으로 해결한다.

public class AccountRepository {
  public void save(Account account){
   ...
  }
  public Member findOne(Long id){
  ...
  }
  //update
  //delete
  //findAll
}

public class ItemRepository {
  public void save(Item item){
   ...
  }
  public Member findOne(Long id){
  ...
  }
  //update
  //delete
  //findAll
}

우리는 대게 이런작업을 일삼았다. 여기서 보면 두개는 비슷한 일을 한다. save, findOne, update, delete .. 기타 등등 그래서 이것을 해결하고자 우리는 제네릭 DAO를 만들어서 사용했다. 하지만 이방법은 공통 기능을 구현한 부모 ㅋ르래스에 너무 종속되고 구현 클래스 상속이 가지는 단점에 노출된다.

Spring data jpa는 CRUD를 처리하기 위한 공통 인터페이스가 존재 한다. 개발할때 인터페이스만 작성하면 실행 시점에 Spring data jpa가 구현 객체를 동적으로 생성해서 주입해준다. 그래서 우리는 구현 클래스 없이 인터페이스만 작성해도 개발할 수 있다.

public interface ItemRepository extends JpaRepository<Item, Long>{
}

위 처럼 작성하면 간단한 CRUD가 완성 된다.
일반적인 CRUD 메서드는 JpaRepository 인터페이스가 공통으로 제공하므로 문제가 될 건 없다. 예를들어 findByusername(String username) 처럼 메서드의 이름을 분석해서 JPQL를 실행한다.

spring data jpa Query creation
위에 링크에 가면 자세히 알 수 있다.

Spring data jpa는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나이다. JPA 말고도 여러가지 다양한 데이터 저장소를 제공해준다.

쿼리 메서드

쿼리 메서드 기능은 스프링 데이터 JPA가 제공하는 마법 같은 기능이다. 대표적으로 메서드 이름만으로 쿼리를 생성하는 기능이 있다.
이외에도 NamedQuery, @Query 어노테이션 등을 활용해 좀더 세부적으로 개발 할 수 있다.

위에서 메서드명으로 쿼리를 생성하는 것은 잠깐 봤으니 생략 하겠다. 위의 문서에 더 자세히 나와있으니 참고하길 바란다.

JPA NamedQuery

JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다. 한번 살펴보자.

@Entity
@NamedQuery(
  name = "Account.findByusername",
  query = "select a from Account a where a.name = :name"
)
public class Account {

  @Id
  @GeneratedValue
  @Column(name = "ACCOUNT_ID")
  private Long id;

  private String name;

  private String password;

  private String email;
  ...getter setter ... etc
}

@NamedQuery 어노테이션으로 Named 쿼리를 지정하였다.

@Repository("accountRepositoryImplCustom")
public class AccountRepositoryImplCustom {

  @PersistenceContext
  private EntityManager entityManager;

  public List<Account> findByname(String name){
    return entityManager.createNamedQuery("Account.findByusername", Account.class)
      .setParameter("name", name)
      .getResultList();
  }
}

그리고 createNamedQuery에 위에 해당하는 Name을 지정해주면 된다.
위는 스프링 데이터 JPA를 쓰지 않았을 경우이고 Spring data jpa를 쓰면 더 간단해진다.

public interface AccountRepository extends JpaRepository<Account, Long> {

  List<Account> findByusername(@Param("name") String name);
}

스프링 데이터 JPA는 선언한 도메인 클래스 +.(점) + 메서드 이름으로 Named쿼리를 찾는다. 만약 실행한 Named 쿼리가 없다면 메서드 이름으로 쿼리 생성 전략을 사용한다.

@Query

레파지토리 메서드에 직접 쿼리를 정의하려면 Spring에 있는 @Query 어노테이션을 사용하면 된다. 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다. 또한 JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견 할 수 있는 장점이 있다.

@Query("select a from Account a where a.name = ?1")
List<Account> findByusernames(String name);

위에는 JPQL을 사용했을 경우이다 기본으로 JPQL을 사용한다. 네이티브 쿼리를 사용하고 싶다면 아래와 같이 하면 된다.

@Query(value = "select * from Account where name = ?1", nativeQuery = true)
List<Account> findByusernamesQueryNative(String name);

책에는 네이티브 쿼리시 위치 기반 파라미터가 0부터 시작한다고 했지만 필자는 1부터 시작했다. 버전이 올라가면서 바뀐듯하다?

파라미터 바인딩

Spring data jpa는 위치기반 파라미터와 이름 기반 파라미터 바인딩을 모두 지원한다.

select a from Account a where a.name = ?1 //위치 기반
select a from Account a where a.name = :name //이름 기반

기본값은 위치 기반인데 파라미터 순서로 바인딩한다. 이름 기반 파라미터 바인딩을 사용하려면 spring에 있는 @Param 어노테이션을 사용하면 된다.

@Query(value = "select a from Account a where a.name = :name")
List<Account> findByusernamesNamedQueryNative(@Param("name") String name);

코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자!