Spring boot Properties

오늘은 Spring boot properties에 대해서 이야기 해보도록 하자.
Spring boot 에는 다양한 설정파일을 제공하고 있다. .properties, .yaml 파일뿐만 아니라 다른 여러방법도 존재한다.
이것뿐만 아니라 우선순위 등 잘 사용하는 방법을 알아보도록 하자.

Random

properties에 random 함수를 사용할 수 있다. 실제로는 자주 사용하지는 않겠지만 테스트할 경우에는 유용할 수 있는 랜덤함수이다. 하지만 필자도 한번도 사용한적이 없다는..
int, long, uuid, 원하는 범위의 int 등으로 설정할 수 있다.

name=${random.int}
name=${random.long}
name=${random.value}
name=${random.int(10)}
name=${random.int[1024,65536]}

위와 같이 random 이라고 prefix에 작성하면 된다. 실제 이것은 바꿀수는 없다. 위의 값을 꺼내보면 매번 다른 값이 출력 된다.
해당 클래스는 RandomValuePropertySource 클래스이니 참고하길 바란다.

xml?

우리가 흔히 알고 있는 .properties, .yaml 파일의 확장자뿐만 아니라 xml 파일도 설정파일로 작성할 수 있다. 그렇지만 잘 사용하지 않는 이유는 properties나 yaml이 더욱 간편해서? 아마 그런거 같은데 xml도 설정파일로도 가능하니 사용하고 싶으면 사용해도 된다.

application.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>comment</comment>
    <entry key="name">wonwoo</entry>
</properties>

별다른 설정하지 않아도 위와 같이 설정하면 name이라는 프로퍼티에 wonwoo 라는 값이 들어가 있을 것이다.
음 이것은 java의 Properties 클래스를 이용하는 거 보니까 java의 스펙인 것 같다. 더 자세한 정보들은 인터넷 또는 스펙을 찾아보면 될 것 같다.

또한 Spring boot 에서 지원해주는 것은 위와 같이 .properties, .yml, xml 이지만 만약 자신은 다른 파일, 예를들어 json 파일로 설정파일을 하고 싶다면 다음과 같이 구현만 해주면 된다.


public class JsonPropertySourceLoader extends YamlPropertySourceLoader { @Override public String[] getFileExtensions() { return new String[]{"json"}; } }
{
  "name": "blabla"
}

위와 같이 만들어서 설정해주면 된다. 어떤 설정파일이든 자신에 맞게 구현하면 쉽게 파일들을 설정정보로 넣을 수 있다.

우선순위

Spring boot properties는 우선순위가 존재한다. 예를들어 application.properties, 와 application-dev.properties 존재하는 가운데 profile을 dev로 한다면 어떻게 될까? 우선순위가 높은건 application-dev.properties 파일이 된다. 우선순위가 낮다고 해서 application.properties가 사라지는 건 아니고 application-dev.properties 파일을 먼저 읽고 그 다음에 application.properties를 읽게 된다.

applciation.properties
name=kevin
address=seoul
applcation-dev.properties
name=wonwoo

만약 파일이 위와 같다면 name=wonwoo가 우선순위가 높아 dev 파일을 읽지만 address는 dev에 존재하지 않으니 application.properties 파일에 있는 address를 읽게 된다. 우선순위가 높은게 오버라이딩되니 참고하면 되겠다.

Spring boot 에서는 classpath에 있는 properties 말고도 다른 파일들을 로드하려고 한다. 예를들어 classpath:config/ 위치에 properties가 존재해도 그 파일을 읽는다.
기본적으로는 네 군데의 위치에서 파일을 읽는다.

  1. 해당 프로젝트의 루트 /config/ file:./config/
  2. 해당 프로젝트의 루트 file:./
  3. 해당 프로젝트의 클래스패스의 /config/ classpath:/config/
  4. 해당 프로젝트의 클래스패스 classpath:/

위와 같이 네 군데에서 파일들을 읽어 온다. 여기서도 우선순위가 정해진다. 가장 우선순위가 높은 위치는 1번이고 우선순위가 낮은 위치는 4번이다. 하지만 필자는 보통 4번에 넣는다는 점..

기본적으로는 위의 네 군데에서 파일을 읽어드리지만 만약 변경하고 싶다면 다음과 같이 변경하면 된다.

spring.config.location=classpath:/test/

SPRING_CONFIG_LOCATION=classpath:/test/

위와 같이 변경하면 classpath 의 /test 폴더 아래의 프로퍼티를 읽게 된다. 이것은 기존의 classpath에 application.properties 파일이 있어도 그 파일은 읽지 않는다. 그냥 저 /test/ 폴더 밑의 파일들만 읽게 된다. 따락서 나머지 파일들은 해당 설정에 파일로 읽어드리지 않는다.

참고로 spring.config.location 프로퍼티는 시스템 환경설정이나 args로 넣어야 동작한다. 왜냐하면 그 파일을 읽기 위해 바로 로드되어야 하기 떄문이다.

또한 spring.config.additional-location 속성을 통해 확장할 수 있다. spring.config.location 은 해당 파일만 읽어들이는 거라면 spring.config.additional-location은 설정한 해당 파일을 읽고 나머지 기본설정도 읽는 기능이다.

spring.config.additional-location=classpath:/additional/ 

SPRING_CONFIG_ADDITIONAL_LOCATION=classpath:/additional/

만약 위와 같이 설정했다면 제일 우선순위가 높은 파일은 classpath:/additional/ 아래의 파일들 이다. 그리고 나머지는 기본값과 동일하다.

  1. classpath:/additional/
  2. file:./config/
  3. file:./
  4. classpath:/config/
  5. classpath:/

참고로 만약 classpath:/additional/ 와 file:./additional/ 같이 설정하였다면 file: 이 항상 우선순위가 높다. spring.config.location 도 동일하다.

properties와 yaml 의 우선순위는 properties가 높다. 아마도 파일명으로 정렬을 하는듯 싶다.

spring.application.json

Spring boot는 설정정보를 json으로 바로 작성해서 환경설정을 할 수 있다.

spring.application.json={"name":"won1"}
SPRING_APPLICATION_JSON={"name":"won1"}

이 또한 시스템환경변수나 commandLine args 로 작성해야 된다.

위에서 봤던 spring.config.additional-location 보다 spring.application.json이 우선순위가 더 높다. 만약 additional-location과 application.json을 같이 사용했을 경우에는 spring.application.json에 작성했던 json이 먼저 환경설정에 들어가게 된다.

오늘은 이렇게 Spring boot의 properties에 대해서 살펴봤다. 우선순위가 조금 헷갈려서 다시 한번 찾아보는 계기가 되었다.

JPA 까먹지 말자! (2)

오늘은 저번시간에 이어서 JPA 까먹지 말자! (2) 를 시작해보자. JPA라 했지만 구현체는 hibernate 기준으로 설명하니 다른 구현체들은 조금씩 다를 수도 있으니 참고하면 되겠다. 또한 종종 hibernate 이야기도 있을 수도 있다.

@GeneratedValue strategy

JPA에서는 @GeneratedValue 어노테이션의 strategy 속성으로 기본키 전략을 설정할 수 있다. 물론 직접 기본키를 생성해주는 것도 좋지만 그보다는 자동생성 전략도 나쁘지 않게 생각된다. 필요하다면 비지니스 키를 따로 만들어서 직접 생성해주는 방법도 생각해 볼 수 있다.

JPA에서는 기본키의 전략이 3가지가 있다. TABLE, SEQUENCE, IDENTITY 전략이다. AUTO 전략도 있지만 AUTO는 해당 벤더에 따라 위의 세가지중 하나를 선택하게 된다. 3가지의 전략은 따로 설명하지 않겠다.

근데 이때 알아야 할 것은 기본 키 전략이 IDENTITY 일경우에는 쓰기 지연이 동작하지 않는다. 예를들어 다음과 같다.

public class Account {

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
//..
}

@Transactional
public void saveAccount() {
 Account account = new Account();
 account.setName("wonwoo");
 entityManager.persist(account);
}

위와 같이 작성할 경우에는 entityManager.persist(account) 이 메서드를 호출 할때 바로 insert 쿼리라 작성되어 DB에 날라간다. 하지만 TABLE전략이나 SEQUENCE 전략은 Transaction이 끝나고 flush 혹은 commit이 호출 될 때 insert 쿼리가 만들어져서 날라 가게 된다. 그 이유는 아마 데이터베이스에서 ID를 가져와야 하므로 insert 쿼리라 먼저 만들어져서 DB에 날라가는 것 같다.

만약 쓰기 지연 효과를 얻고 싶다면 TABLE 또는 SEQUENCE 전략으로 설계를 해야 한다.

@Basic LAZY

JPA 에서는 @Basic 어노테이션이라는 것이 있다. 하지만 우리는 거의 사용하지 않는다. 왜냐하면 사용할 일이 없기 떄문이다.. 기본 타입을 말하는 건데. 굳이 쓰지 않아도 암시적으로 JPA가 이거슨 기본타입이라고 설정해 주기 때문이다.

public class Account {

 @Id
 @GeneratedValue
 @Basic
 private Long id;

 @Basic
 private String name;

}

public class Account {

 @Id
 @GeneratedValue
 private Long id;

 private String name;
}

위의 코드는 암묵적으로 동일하다. 그런데 우리는 기본타입을 Lazy 로딩 할 수 있다. @Basic 어노테이션에는 fetch 속성이 존재한다. @OneToMany@OneToOne 기타 매핑하는 어노테이션에서 사용하는 fetch와 동일하다.

@Basic(fetch = FetchType.LAZY)
@Lob
private String content;

위와 같이 fetch 속성에 FetchType을 LAZY 설정해 주면 된다. 그럼 실제 content가 사용 될때 쿼리가 작성되어 날라간다.

Account account = entityManager.find(Account.class, 1L);
System.out.println(account.getContent()); //LAZY

실제 쿼리를 보면 아래와 같다.

select
 account0_.id as id1_0_0_,
 account0_.name as name3_0_0_
from
 account account0_
where
 account0_.id=?

select
 account_.content as content2_0_
from
 account account_
where
 account_.id=?

위 처럼 content를 사용할 때 한번 더 쿼리를 날린다. 근데 사용할 일이 있을까 싶다. content의 크기가 어마어마하게 크면 모를까..
근데 바로는 되지 않고 설정을 조금 해야한다. 여러방법이 있는 것 같은데 그 중에서 가장 쉬운 방법은 메이븐을 사용한다면 아래와 같이 plugin을 작성해야 한다.

<plugin>
 <groupid>org.hibernate.orm.tooling</groupid>
 <artifactid>hibernate-enhance-maven-plugin</artifactid>
 <version>${hibernate.version}</version>
 <executions>
  <execution>
   <configuration>
    <failonerror>true</failonerror>
    <enablelazyinitialization>true</enablelazyinitialization>
   </configuration>
   <goals>
    <goal>enhance</goal>
   </goals>
  </execution>
 </executions>
</plugin>

근데 설정을 좀 잘해야 될거 같다. 다른 프록시들과 조금 꼬이는 듯하다. 만약 사용할 경우에는 다른 프록시들이 잘되는지 테스트를 많이 해봐야 될 것 같다.

Criteria

요즘의 개발자들은 거의 사용하지 않겠지만 (필자 역시도 요즘개발자라) 예전 개발자분들은 익숙한 클래스일 듯 싶다. 요즘 Criteria 보다는 훨씬 쉬운 Querydsl 이라는게 있어 대부분이 Querydsl을 사용하지 Criteria를 사용하지는 않는 것 같다. Criteria와 Querydsl은 동일하게 JPQL을 편하게 작성하도록 만든 빌더 클래스들이다. Criteria는 JPA표준이고 Querydsl은 비표준이지만 훨씬 간단하고 알아보기 좋고 쉽다. 하지만 이런게 있다고만 알아두면 좋을 것 같다.

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<account> criteria = builder.createQuery(Account.class);
Root<account> root = criteria.from(Account.class);
criteria.select(root);
criteria.where(builder.equal(root.get("id"), 1L));
Account account = entityManager.createQuery(criteria).getSingleResult();

위와 같이 조금 어렵다. 뭔가 많은 작업을 해야 된다. 그리고 조금 알아보기도 힘든 것같다. 익숙하지 않아서 그런가? 그래도 사용하고 싶다면 사용해도 된다. JPA 표준이니까. 이것의 장점은 타입세이프해서 좋은 것 같다. 물론 root.get("id") 이 부분도 타입세이프하게 만들 수 있다.

참고로 spring data jpa 에서도 Criteria를 사용한다. 당연히 그래야 했을 거고 그럴꺼라고 생각했다.

Proxy

JPA에서는 바로 쿼리를 날려서 가져오는 find 메서드와 나중에, 해당 객체를 사용할 떄 가져오는(Lazy) getReference 메서드가 존재한다.

Account account = entityManager.find(Account.class, 1L);
Account account1 = entityManager.getReference(Account.class, 2L);

위와 같이 딱 두줄만 코드를 작성했을 때 쿼리는 몇번 날아갈까?
정답은 한번이다. getReference 메서드를 사용할 때는 실제 쿼리를 날리지 않고 프록시 객체만 전달 된다. 왜 그럼 굳이 proxy 객체만 전달하는 getReference 를 만들었을까?
예를들어 연관관계를 맺을 경우 account의 엔티티가 필요한 상황이라고 가정해보자. 물론 account의 엔티티들을 사용한다면 프록시 객체가 필요 없겠지만 단지 연관관계를 맺을 목적이라면 굳이 데이터베이스에 쿼리를 날릴 필요가 없다. 그럴 경우 이 프록시 객체를 사용하면 된다.

Account account = entityManager.getReference(Account.class, 2L);
Address address = new Address();
address.setAddress("seoul");
address.setAccount(account);
entityManager.persist(address);

위와 같이 작성했을 경우 insert 쿼리 한번만 데이터베이스에 날라가게 된다. 좀 더 성능적으로 최적화를 할 수 있다. 이런 경우에라면 정말 좋은 기능이지 않나 싶다.

참고로 다른 JPA의 구현체들은 어떤 프록시를 사용하는지 모르겠지만 hibernate 경우에는 javassist를 사용했다. 하지만 이 글을 쓰는 기준으로 최신버전인 5.3.7 버전은 bytebuddy로 proxy를 변경하였다. 아마도 5.3 이후부터 bytebuddy로 변경한 듯 싶다. 요즘에 code generation 으로 bytebuddy 를 많이 이용하는 것 같다.

또한 Spring data jpa 프로젝트에서도 해당 메서드를 사용가능하다. 현재 버전은 findById 이전버전은 findOne 메서드로 데이터베이스에서 바로 조회했다면 getOne 메서드로 해당 프록시 엔티티를 가져올 수 있다.

FlushMode

hibernate 경우에는 FlushMode 6가지 정도 지원하는데 JPA 스펙에는 2가지가 있다. 하이버네이트 API를 사용한다면 6개 모두 사용할 수 있겠지만 JPA API만 사용한다면 2가지 타입만이 존재한다. COMMITAUTO 가 JPA에서 지원해주는 FlushMode 타입이다.
일반적으로 JPA는 트랜잭션이 commit 되기 직전에 flush도 자동으로 된다. 또 한가지 자동으로 flush 가 될 때가 있는데 그때는 jpql이나 쿼리를 날릴 때 자동으로 flush가 호출 된다.

jpql이나 쿼리를 작성해서 날릴 때 flush 되는 설정이 FlushMode.AUTO 설정이다. 이것은 JPA의 기본값이다. 굳이 아래처럼 명시해주지 않아도 된다.

Account account = new Account();
account.setName("wonwoo");
entityManager.setFlushMode(FlushModeType.AUTO);
entityManager.persist(account);
entityManager.createQuery("select a from Account a", Account.class).getResultList();

위와 같이 코드를 작성할 경우 insert 쿼리는 jpql 쿼리를 날리 전에 먼저 데이터베이스에 날라간다.

insert
 into
account
 (content, name, id)
values
 (?, ?, ?)

select
 account0_.id as id1_0_,
 account0_.content as content2_0_,
 account0_.name as name3_0_
from
 account account0_

만약 AUTO가 아닌 COMMIT으로 했을 경우에는 insert 는 commit 되고 날라가고 그전에 jpql이나 쿼리가 먼저 날라간다. 그래서 원하는 데이터가 나오지 않을 수도 있다.

entityManager.setFlushMode(FlushModeType.COMMIT);

아래는 FlushMode를 COMMIT으로 했을 떄 쿼리이다. select 쿼리가 먼저 데이터베이스에 날라갔다.

select
 account0_.id as id1_0_,
 account0_.content as content2_0_,
 account0_.name as name3_0_
from
 account account0_

insert
 into
account
 (content, name, id)
values
 (?, ?, ?)

쿼리를 날릴때 마다 매번 flush를 하지 않아 성능은 더 좋겠지만 원하는 데이터가 나오지 않을 수도 있으니 상황에 맞게 고민을 해서 사용하면 되겠다.

@NamedQuery

필자는 잘은 사용하지 않지만 사용하면 나쁘지 않은 기능이다. 정적쿼리를 사용할 때 매우 유용한 어노테이션이다. 매번 동일한 쿼리를 작성하지 않고 해당 Name만 지정해줘서 쿼리를 날리면 된다.

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

위와 같이 @NamedQuery 어노테이션을 이용해서 정적 쿼리를 만들 수 있다. 아주 심플하다. 근데 많으면 보기 불편할거 같다.

List<account> accounts = entityManager.createNamedQuery("findByname", Account.class)
  .setParameter("name", "wonwoo")
  .getResultList();

매번 같은 쿼리를 작성하지 않고 해당 name만 작성해서 쿼리를 날릴 수 있으니 재사용성도 있다. 만약 여러개를 작성하고 싶다면 @NamedQueries 어노테이션을 이용하면 된다.

@Entity
@NamedQueries({
  @NamedQuery(name = "findByname", query = "select a from Account a where name = :name"),
  @NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
})
public class Account {
  //...
}

이것은 JPA2.1 기준이며 JPA 2.2부터는 좀 더 간편하게 @NamedQuery 어노테이션만 이용해도 된다.

@Entity
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name")
@NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
public class Account {
}

@NamedQueries 어노테이션을 사용하는 것보다 @NamedQuery를 사용하는 것이 더 보기에 좋아 보인다. 하지만 JPA 2.2 부터 지원하니 그 전 사용자라면 @NamedQueries를 사용해야 한다.

오늘은 이렇게 JPA에 관련해서 두번째 시간을 가져봤다. 물론 필자도 JPA를 잘 사용하지 못한다. (나도 잘사용하고 싶다고오오..)

계속계속 사용해야 하는데 말이다.

Spring Controller 파라미터 타입 (2)

오늘은 예전에 작성했던 Spring Controller 파라미터 타입 에 이어서 두번째 시간을 가져보도록 하자. Spring webmvc 뿐만아니라 다른 프로젝트에서도 다양한 파라미터 타입을 지원주고 있으니 web 뿐아니라 다른 프로젝트에서도 어떤 파라미터를 지원하고 있는지 알아보도록 하자.

webmvc

일단 먼저 Spring mvc 부터 시작하자. 저번에 대부분 spring webmvc에 관련해서 이야기했으나 그래도 많은 부분은 이야기 하지 못했다. 많지는 않지만 몇가지 추가적으로 sprig mvc 파라미터 타입을 살펴보자.

@Value

@Value 어노테이션을 파라미터 타입에 작성할 수 있다. 흠 글쎄다.  Value 어노테이션을 굳이 파라미터에 작성할 이유가 있나 싶기도 하지만 지원은 해주고 있으니..  사용은 하겠지? 

@GetMapping("/value")
public String value(@Value("${name}") String name) {
    //
}

위와 같이 작성후에 properties 에 name이라는 프로퍼티를 작성해주면 된다.

name=wonwoo

아주 간단하다.  이렇게 하면 value를 호출 할때 name이라는 파라미터에 wonwoo  가 자동으로 들어가게 된다.

RedirectAttributes

리다이랙션 할 때 유용한 인터페이스이다. 해당 인터페이스를 사용하면 손쉽게 파라미터등을 전달 할 수 있다. 

@GetMapping("/hi")
public String redirectAttributes(RedirectAttributes redirectAttributes) {
    redirectAttributes.addAttribute("id", 1);
    redirectAttributes.addAttribute("foo", "bar");
    return "redirect:redirectAttributes/{id}";
}

@GetMapping("/redirectAttributes/{id}")
public String redirectAttributesHi(@PathVariable Long id, String foo) {
   //

}

위와 같이 리다이랙션하는 부분에 RedirectAttributes  인터페이스를 파라미터 타입으로 받은 후 파라미터 등을 해당 attribute에  작성하면 된다. 그럼 실제 리다이랙트 부분의 uri는 아래와 같이 만들어진다.

/redirectAttributes/1?foo=bar

리다이랙트 할 때 유용한 파라미터 타입이다.

@RequestAttribute

RequestAttribute 어노테이션은 request attribute 를 가져올 수 있는 어노테이션이다. 미리 정의된 속성을 가져올 수도 있고 지정한 속성을 가져올 수 도 있다. 코드를 보면 더 이해하기 쉬울 것이다.

@ModelAttribute
public void foo(HttpServletRequest request) {
    request.setAttribute("foo", "bar");
}

@GetMapping("/requestAttribute")
@ResponseBody
public String requestAttribute(@RequestAttribute String foo) {
    return foo;
}

위와 같이 필자가 정의한 foo라는 속성을 @RequestAttribute 어노테이션을 이용해서 foo라는 속성을 가져왔다. 굳이 controller가 아닌 interceptor에서 해당 속성을 넣어서 컨트롤러에 파라미터로 넣을 수도 있다. 다양한 방법으로 사용할 수 있으니 참고하면 되겠다.

@MatrixVariable

@MatrixVariable 어노테이션은 조금 생소한 기능을 가지고 있다. 매트릭 형태의 파라미터라고 할까? 잘 쓰면 유연한 url이 되겠지만 필자는 아직 잘 모르겠다. 잘 쓰지 않아서 그런건지도.. 


@GetMapping("/matrix/{id}")
public String matrix(@PathVariable String id, @MatrixVariable int q) {
    
}

만약 위와 같은 코드가 있다면 우리는 아래와 같이 호출하면 된다.

http://localhost:8080/matrix/1;q=11

위와 같이 ; 세미콜론으로 구분이 된다. 조금 어색한 url이 된거 같다.  문서에서 다른 예제도 가져왔다. 어떤지 한번 보고 넘어가자.

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}

/owners/42;q=11/pets/21;q=22

흠.. 좋은건가? 괜찮은 건가? 관심이 있다면 한번 살펴보는 것도 나쁘지 않다. 

참고로 @MatrixVariable 어노테이션을 사용하려면 UrlPathHelper 의 removeSemicolonContent 속성을 false로 작성해야 된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

UriComponentsBuilder

사용할 일이 있긴 하나? 아무튼 지원은 해주고 있으니 한번 살펴보자. 아니 뭐 살펴볼 것도 없다. 코드를 보자.

@GetMapping("/uriComponentsBuilder")
@ResponseBody
public String uri(UriComponentsBuilder uriComponentsBuilder) {

}

글쎄다. 딱히 유용한 정보들이 들어가는 것도 아니고. 언제 어디에 쓰일지는 잘..

Spring data

spring webmvc를 살펴봤는데 spring data 에서도 몇가지 파라미터를 제공해주고 있다. 한번 살펴보도록 하자.

Pageable 와 Sort

Spring data에는 Pageable 인터페이스와 Sort 클래스가 존재한다. Pageable은 페이지 사이즈, 페이지 번호 Sort등을 넣을 수 있다.  Sort만 사용할 경우에는 Sort 클래스만 파라미터 타입으로 작성하면 된다.

@GetMapping("/page")
public String page(Pageable pageable, Sort sort) {
    
}

위와 같이 작성하였을 경우에 우리는 다음과 같이 호출 할 수 있다.

/page?size=10&page=1&sort=id,desc

그럼 Spring data 에서 적절하게 파싱해 Pageable과 Sort에 값을 넣어 준다. 그럼 손쉽게 spring data api를 사용할 수 있다. 

@PageableDefault 어노테이션을 사용해서 해당 디폴트 값도 변경 가능하다.

@GetMapping("/page")
public String page(@PageableDefault(size = 100, page = 1, sort = "id", direction = Direction.ASC) Pageable pageable) {
 
}

Predicate

Querydsl에서 지원하는 Predicate도 사용할 수 있다. Spring data 와 querydsl을 사용하면 좀 더 손쉽게 사용할 수 있다. 

@GetMapping("/dsl")
public Iterable<Bar> dsl(@QuerydslPredicate(root = Bar.class) Predicate predicate) {
    return barRepository.findAll(predicate);
}

public interface BarRepository extends QuerydslBinderCustomizer<QBar>, JpaRepository<Bar, Long>, QuerydslPredicateExecutor<Bar> {

    @Override
    default void customize(QuerydslBindings bindings, QBar user) {
        bindings.bind(String.class)
            .first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
    }
}

사용하고 싶다면 다음과 같이 호출하면 된다.

/dsl?name=foo

그럼 name이 foo인것을 like검색 한다. 좀 더 자세한 내용은 문서를 통해 확인하면 되겠다.

@ProjectedPayload

Spring data 에서 지원해주는 @ProjectedPayload 어노테이션이다. 인터페이스로 작성하면 된다. 

@GetMapping("/payload")
public String payload(PayLoadExample payLoad) {

}

@ProjectedPayload
public interface PayLoadExample {
    String getName();
}

위와 같이 인터페이스에 @ProjectedPayload 어노테이션을 작성하면 된다. 간단하다. 그러고 나서 아래와 같이 호출하면 된다.

/payload?name=wonwoo

그럼 PayLoadExample 에 getName() 메서드를 호출해보면 wonwoo라는 값을 꺼낼 수 있다.

또한 파라미터 말고도 body도 가능하다. 

@PostMapping("/payload")
public String payload(@RequestBody PayLoadExample payLoad) {
    return payLoad.getName();
}

위와 같이 @RequestBody 어노테이션을 작성하고 호출하면 body로도 받을 수 있다. 

또한 만약 json으로 body를 받는다면 @JsonPath 어노테이션을 이용해서 해당 필드도 변경할 수 있다. 

@ProjectedPayload
public interface PayLoadExample {
    @JsonPath("$..firstname")
    String getName();
}

그럼 firstname 으로 body를 작성해서 보내면 된다. xml을 이용한다면 @XBRead 을 이용해서 변경할 수 있다.

@ProjectedPayload
public interface PayLoadExample {

    @XBRead("//firstname")
    String getName();
}

@JsonPath, @XBRead 어노테이션을 사용하려면 적절한 디펜더시가 필요하다. 

<!-- JsonPath -->
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>${version}</version>
</dependency>
<!-- XBRead -->
<dependency>
    <groupId>org.xmlbeam</groupId>
    <artifactId>xmlprojector</artifactId>
    <version>${version}</version>
</dependency>

Spring cloud web gateway

spring cloud gateway mvc 에서도 ProxyExchange 클래스를 파라미터 타입으로 지정할 수 있다.

프록시 역할을 하는 클래스이다. 아주 간편하게 프록시 역할을 할 수 있다.

@GetMapping("/proxy")
public String proxy(ProxyExchange<String> exchange) {
    ResponseEntity<String> response =
        exchange.uri("https://start.spring.io")
            .get();
    return response.getBody();
}

적절하게 헤더 정보도 변경할 수 있고 파라미터, body도 정보도 변경 할 수도 있다. 

spring webflux

webflux에도 파라미터 타입이 몇가지 추가 되었다. 예를들어 ServerWebExchange 인터페이스나 ServerHttpRequest , ServerHttpResponse 인터페이스를 추가로 파라미터 타입으로 받을 수 있다. 

@GetMapping("/server")
public Mono<?> webflux(ServerWebExchange serverWebExchange) {
    ServerHttpRequest request = serverWebExchange.getRequest();
    ServerHttpResponse response = serverWebExchange.getResponse();
    return Mono.empty();
}

ServerWebExchange 파라미터를 받아서 getRequest 와 getRepsonse 를 꺼내서 사용할 수도 있고 ServerHttpRequest, ServerHttpResponse 를 각각 파라미터로 받아서 사용해도 된다.

@GetMapping("/server")
public Mono<?> webflux(ServerHttpRequest request, ServerHttpResponse response) {
    return Mono.empty();
}

또 한 Mono를 파라미터로 받을 수 도 있다.

@GetMapping("/webflux")
public Mono<?> webflux(Mono<Foo> foo) {
    return foo;
}

위와 같이 Mono를 사용해서 파라미터로 받을 수도 있고 @RequestBody 어노테이션을 이용해서  body로도  받을 수 있다. 여기서 주의할 점은 wrapper Type 은 지원하지 않는다.  model object만 지원하고 있으니 그것만 주의해서 사용하면 되겠다.

오늘은 이렇게 Spring Controller 파라미터 타입에 대해서 좀 더 알아봤다. 유용한 파라미터 타입이 있다면 적절하게 사용하면 좋을 것 같다. 

기존에 있던 web 과 새로운 webflux는 호환되지 않는다. 하지만 이미 기존의 webmvc에 존재 했던 예를들어 @RequestParam, @RequestBody , … 기타 등등 들 파라미터 타입들이  webflux 에서 다시 구현되어 있다. (왜냐면 인터페이스가 완전히 다르기 때문이다. ) 하지만 우리는 그것을 신경쓰지 않고 마치 webmvc 처럼 동일하게 사용할 수 있었던 것이다. 그래서 webflux에서도 @RequestParam, @RequestBody … 기타 등등 webmvc에서 사용했던 어노테이션, 클래스들을 사용할 수 있다. 물론 모두는 아니겠지만 대부분은 사용가능하다. 

Spring Controller 파라미터 타입