Jpa 까먹지 말자.

오늘은 필자가 자주 까먹거나 기억을 하지 못하는 부분들을 좀 정리좀 해보려고 한다. JPA는 자주 기억이 안난다. 이상하게 사용하고 있을 때는 기억이 나지만 또 사용하지 않으면 기억이 안난다. 뭐 원래 그런건가? 아무튼 오늘 한번 자주 기억이 나지 않는 부분을 정리해보자.

LAZY, EAGER

Jpa 에서는 LAZY 로딩과 EAGER 로딩이 존재한다. LAZY 로딩일 경우에는 쿼리를 날리지 않고 해당 객체를 사용하는 시점에 쿼리가 나간다. EAGER 로딩일 경우에는 처음 부터 쿼리를 모두 날린다.

public class Account {
  @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
  private List orders;
}

Account의 모든 리스트를 가져올 경우를 생각해보자.

List<account> accounts = accountRepository.findAll()

이 경우에는 n + 1 의 쿼리가 나간다. 하지만 accountRepository.findAll()을 호출 했을 때가 아니라 account.orders 를 호출 했을 때 각 쿼리가 작성된다.

그렇다면 EAGER로 했을 때는 어떨까? 마찬가지다. 마찬가지긴 하지만 Lazy와 달리 accountRepository.findAll() 메서드를 호출할 때 모든 쿼리가 날라간다. (n + 1)

하지만 이때는 좀 다르다. 모든 Account를 가져오는게 아니라 특정한 아이디로 Account를 하나를 가져와보자.

Account account = accountRepository.findById(1L).get();

만약 위와 같이 단일 Account를 가져온다면 Lazy 경우에는 동일하게 account.orders를 호출 할때 쿼리가 한번 더 나간다. 하지만 Eager일 경우에는 쿼리가 두번 날라가지 않고 한번만 날라간다. 그 이유는 JPA가 최적화를 해서 조인을 한다.

from
account account0_
left outer join
orders orders1_
on account0_.id=orders1_.account_id

여기서는 OneToMany만 했지만 ManyToOne 도 동일하다.
참고로 xxxToMany의 기본전략은 Lazy이고 xxxToOne의 기본전략은 Eager 로딩 전략이다.

cascade

casCade 속성은 많지만 필자가 자주 사용할 만한 몇가지만 설명하고 나머지는 생략하겠다. 또한 굳이 설명하지 않아도 대충 어떤 의미 인지 알 수 있을 듯하다.

casCade는 영속성 전이를 의미한다. 쉽게 설명하면 부모객체와 함께 하겠다는 의미이다.

Account account = new Account();
Order order = new Order();
order.setName("mac");
order.setAccount(account);
account.setOrders(Arrays.asList(order));
account.setName("wonwoo");
entityManager.persist(account);

예로 위와 같이 엔티티를 저장할 경우에 부모 엔티티만 저장했지만 자식 Order 엔티티까지 저장된다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<order> orders;

위와 같이 cascade 를 CascadeType.PERSIST 로 설정하면 된다.

remove도 동일하다. 해당 엔티티가 삭제될 때 자식의 엔티티도 함께 삭제 하는 것이다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private List<order> orders;</order>

entityManager.remove(account);

그러면 먼저 자식의 엔티티부터 삭제하고 다음의 부모의 엔티티인 account를 삭제한다.
이외에도 MERGE, REFRESH, DETACH가 있다. 모두 해당 메서드를 호출할 때 영속성 전이가 된다.

entityManager.merge(account);
entityManager.refresh(account);
entityManager.detach(account);

만약 어떤 것이든 모두 하고 싶다면 ALL로 설정하면 된다.

OneToOne

기본적으로 OneToOne은 Lazy 로딩이 먹지 않는다. JPA OneToOne?
일반적으로 부모의 키가 자식의 테이블에 외래키로 되길 마련이다. 만약 그런다면 문제가 된다. 예를들어 우리는 Account를 가져오는 시점에 orders의 정보를 알고 있어야 한다. 그래야 jpa에서는 null을 넣을지 아니면 프록시 객체를 넣을지 결정해야 되기 때문이다. 이미 프록시 객체를 넣는다면 그건 이미 null이 아니다. 그래서 Lazy 로딩이 먹히지 않는다.
그렇다면 만약 자식의 키를 부모가 들고 있으면 어떨까? 그럼 가능하지 않을까? 왜냐하면 부모의 엔티티를 가져올 때 해당 자식의 id가 있으면 그건 null이 아니라는 뜻이다. 그럼 그때는 프록시 객체를 넣어주면 되고 id가 null일 경우에는 그냥 null을 넣어 주면 되니까 말이다. 맞다. 그렇게 하면 OneToOne 이라도 Lazy 로딩을 할 수 있다.

참고로 필자가 이것 저것 테스트를 해봤는데 다음과 같다.
– optional 여부와는 상관없다.
– 양방향과 단방향의 여부도 상관없다. 물론 단방향으로 했을 때는 부모 테이블에 지식 주키가 저장된다.
– 부모 테이블에 자식 주키가 저장된다면 그건 Lazy 로딩이 된다.

고아 객체

Account account = entityManager.find(Account.class, 2L);
account.getOrders().remove(0);

CascadeType.ALL 이고 orphanRemoval = true 일 경우에만 컬렉션 고아 객체가 삭제가 된다. orphanRemoval = true 만 있을 경우에는 삭제가 안되는데?
CascadeType PERSIST REMOVE 는 바로 전이가 되지 않고 플러시를 호출 할때 전이가 된다.

CascadeType.REMOVEorphanRemoval = true 동일하게 부모 엔티티를 삭제하면 자식 엔티티 까지 삭제 된다.

persist, merge

persist() 와 merge()는 모두 엔티티를 저장할 수 있다. 하지만 조금 다르다. persist 경우에는 key의 값이 존재 하면 안된다. 하지만 merge 경우에는 key의 값이 존재 해도 상관없다. 만약 db에 있다면 업데이트를 진행하고 없으면 저장을 한다.

또한 영속성을 관리하는 부분이 조금 다르다.

Account account = new Account();
account.setName("wonwoo");
entityManager.persist(account);
account.setName("wonwoo1");

위 경우에는 저장 후에 업데이트를 하지만 merge를 사용할 경우에는 업데이트를 하지 않는다.

Account account = new Account();
account.setName("wonwoo");
Account merge = entityManager.merge(account);
merge.setName("wonwoo1");

이 처럼 merge 에서 나온 엔티티로 상태를 변경해야 한다. 그래야 업데이트를 한다. account 필드는 영속성 컨텍스트에서 관리하지 않고 merge 에서 리턴한 엔티티를 영속성 컨텍스에서 관리하고 있다.

isolation

JPA 이야기는 아니지만 그래도 자주 기억이 나지 않기에..

READ_UNCOMMITTED

READ_UNCOMMITTED 커밋되지 않는 읽기 가능. 예를들어 A라는 트랜잭션이 데이터를 쓰고 (where id = 1) 커밋 하지 않았지만 B라는 트랜잭션이 (select where id = 1) 을 하면 데이터를 가지고 온다.

@Transactional
public void insertAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
  try {
   TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {
  }
}

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUnCommit() {
  accountRepository.findAll().forEach(System.out::println); //위의 foobar를 커밋도 하지 않았지만 select 된다.
}

READ_COMMITTED

READ_COMMITTED 커밋된 읽기 가능. 예를들어 A라는 트랜잭션이 데이터를 읽는 도중 B라는 트랜잭션이 데이터를 쓰고 커밋을 한 후 다시 A트랜잭션이 해당 데이터를 읽으면 B에서 넣은 데이터가 읽어진다. A라는 트랜잭션은 일관성 떨어진다.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommit() {
  accountRepository.findAll().forEach(System.out::println);

  try {
    TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {
  }
  accountRepository.findAll().forEach(System.out::println); // 아래의 foobar 가 나온다.
}

@Transactional
public void insertCommitAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
}

REPEATABLE_READ

READ_COMMITTED 반대로 데이터가 일관성이 있다. A라는 트랜잭션은 일관성있게 동일한 데이터를 읽고 온다.

@Transactional
public void insertRepeatableAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void readRepeatableCommit() {
  accountRepository.findAll().forEach(System.out::println);

  try {
    TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {

 }
  accountRepository.findAll().forEach(System.out::println);  //foobar 가 나오지 않는다.
}

SERIALIZABLE

A라는 트랜잭션이 데이터를 쓰고 있을때 B라는 때 트랜잭션은 select 를 할 lock 이 걸린다. 가장 비용도 높고 성능은 떨어지지만 가장 신뢰도가 높다.

@Transactional
public void insertSerializableAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
  try {
   TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {

  }
}

@Transactional(isolation = Isolation.SERIALIZABLE)
public void readSerializableCommit() {
  System.out.println("lock");
  //lock
  accountRepository.findAll().forEach(System.out::println);

}

참고로 DB 밴더들마다 기능과 기본값이 조금씩 다를 수 있다.

오늘은 이렇게 필자가 자주 기억이 안나는 것을 정리해봤다. 헷갈린다. 자주자주 해야 되는데 사용하다 말다 그러다 보니까 맨날 잊어버린다.

필자가 테스트 해보면서 쓴 내용이니 정확하게 맞지 않을 수 도 있다.

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 파라미터 타입

lombok을 잘써보자! (3)

오늘은 예전에 lombok을 잘써보자! 시리즈에서 조금 추가된 내용을 써보려고 한다. lombok 버전도 올라가면서 새로 추가된 어노테이션도 있고 놓쳤던 부분도 있을 수 있어 좀 더 추가하여 내용을 이어가보자. 참고로 지금 필자가 lombok 을 사용하는 버전은 1.18.2 버전이다. 지금 현재까지 나온 버전은 1.18.4 버전으로 알고 있다.

lombok을 잘써보자! (1)

lombok을 잘써보자! (2)

@Value

Value 어노테이션이다. 이것은 불변을 의미한다.  아주 간단하게 클래스 레벨에 @Value 어노테이션만 선언하면 사용할 수 있다.  코드를 보면서 살펴보도록 하자.

@Value
public class ValueExample {
    String name;
    String email;
}

기본적으로 위와 같이 선언했을 경우 필드는 기본적으로 private 접근제어자와 final 이 붙은 상수가 된다. final이 붙어 setter는 존재하지 않고 getter만 존재한다. 클래스 자체도 final class로 상속을 받을 수 없다.  @Data 어노테이션과 비슷하게 equals, hashCode, toString을 함께 만들어 준다. @Data 어노테이션이 비슷하지만 불변인 정도? 그 정도로만 생각해도 문제없을 듯 하다.  기본 생성자는 private 생성자이다.  기본생성자는 만들어 주지만 private 생성자로 만들어 준다.  위의 클래스를 바닐라 자바로 본다면 다음과 같을 것이다.

public final class ValueExample {
    private final String name;
    private final String email;

    public ValueExample(String name, String email) {
        this.name = name;
        this.email = email;
    }

    private ValueExample() {
        this.name = null;
        this.email = null;
    }

    public String getName() {
        return this.name;
    }

    public String getEmail() {
        return this.email;
    }
    // equals, hashCode, toString
}

위와 비슷한 모양으로 코드가 생성될 것으로 판단된다.  @Value어노테이션의 속성으로는 staticConstructor 가 존재하는데 static한 생성자를 생성해주는 속성이다. 이 속성을 사용할 경우에는 모든 생성자가 private 으로 되고 정의해둔 해당 static 메서드만 사용할 수 있다.

@Value(staticConstructor = "of")
public class ValueExample {
    String name;
    String email;
}

ValueExample ve = ValueExample.of("wonwoo", "wonwoo@test.com");

주로 DTO로 사용할 때 사용하면 될 듯 하다.

@Wither

이번에는 Wither 어노테이션이다. 음.. 이 어노테이션도 불변?과 관련이 있다. 해당 프로퍼티를 다시 어사인할때 해당 Object를 변경하는게 아니라 새로운 Object를 리턴해준다. 이 어노테이션은 필드 혹은 클래스에 붙일 수 있다.

public class WitherExample {
    @Wither
    private final String name;

    @Wither
    private final String email;

    public WitherExample(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

위와 같이 필드를 정의 했을때 다음과 같이 사용할 수 있다.

WitherExample we = new WitherExample("wonwoo", "wonwoo@test.com");
WitherExample we1 = we.withName("woo");

위에서 말했다시피 해당 Object를 새로 만들어 return 해 주고 있다. 만약 위와 같이 모든 필드에 적용하고 싶다면 클래스 레벨에 @Wither 어노테이션을 붙어도 된다.

@Wither
public class WitherExample {

    private final String name;

    private final String email;

    public WitherExample(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

위 아래 모두 동일한 코드가 생성된다. 위 코드를 바닐라 자바로 바꾸어 본다면 아래와 같을 것이다.

public class WitherExample {
    private final String name;
    private final String email;

    public WitherExample(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public WitherExample withName(String name) {
        return this.name == name ? this : new WitherExample(name, this.email);
    }

    public WitherExample withEmail(String email) {
        return this.email == email ? this : new WitherExample(this.name, email);
    }
}

어렵지 않다.  @Wither 속성중에 해당 메서드의 접근제어자를 설정해 줄 수도 있다. 

public class WitherExample {

    @Wither(AccessLevel.PROTECTED)
    private final String name;
    //...
}

@Singular와 @Builder.Default

이 어노테이션은 @Builder 어노테이션을 사용할 때 유용하다.  @Builder 어노테이션은 다들 아시다시피 builder 패턴으로 해당 Object을 만들어주는 그런 어노테이션이다. 생성자의 파라미터가 많을 경우 유용한 어노테이션이다. 그 때 사용할 수 있는 어노테이션이 @Singular 어노테이션과 @Builder.Default 어노테이션이다. @Singular 어노테이션은 컬렉션에 사용할 수 있는데 하나의 어떤 Object을 컬렉션에 추가 할 수도 있고  컬렉션 모두를 추가할 수 도 있다.

@Builder.Default 어노테이션은 @Builder 어노테이션을 사용할 경우 미리 선언된 프로퍼티의 값을 사용할 수 없다. 

아래의 예제를 보면서 살펴보도록 하자.

@Builder
public class SingularExample {

    @Builder.Default
    private String name = "wonwoo";

    @Singular
    private List<String> phones;
}

위 처럼 사용할 경우에는 name에 기본적으로 wonwoo 라는 값을 넣어두었다. 만약 @Builder.Default 어노테이션이 존재 하지 않는다면 해당 값을 초기화 되지 않는다.

SingularExample singularExample  = SingularExample
    .builder()
    .build();

SingularExample(name=wonwoo, phones=[])

위와 같이 아무 값을 넣지 않았지만 name에는 wonwoo라는 값이 존재한다.  만약 @Builder.Default 를 제거한다면 아래와 같은 값이 출력 될 것이다.

@Builder
public class SingularExample {
    private String name = "wonwoo";

    @Singular
    private List<String> phones;
}

SingularExample(name=null, phones=[])

@Singular 어노테이션은 단일 Object를 컬렉션에 추가할 수도 있고 컬렉션 자체를 추가할 수 도 있는 어노테이션이다. 사용하는 코드를 살펴보자.

SingularExample singularExample = SingularExample
    .builder()
    .phone("010-0000-1111")
    .phone("010-0000-1112")
    .phones(Arrays.asList("010-1111-2222", "010-1111-2222"))
    .build();

위와 같이  phones 라는 컬렉션에 phone을 하나씩 하나씩 추가할 수 있다. 컬렉션을 사용할 때 유용한 어노테이션인 듯 싶다. 

빌더 어노테이션을 유용하게 사용한다면 한번씩 살펴보는 것도 나쁘지 않다.

@FieldNameConstants

이 어노테이션은 어노테이션명 그대로 필드명을 상수로 사용하는 어노테이션이다. 근데 조금 문서와 다르다. 필자는 intellij를 쓰는데 해당 플러그인이 지원을 제대로 해주지 않는 건지.. 모르겠지만 필자가 테스트 해본 걸로 글을 작성할 예정이니 참고하면 되겠다. 보니까 1.18.4 부터 디자인이 조금 바뀌었다고 한다. 일단 이런게 있다고만 알자!

@FieldNameConstants
public class FieldNameConstantsExample {
    private String abcd;
}

위와 같이 상수를 담고 있는 클래스에 작성하면 된다.  그럼 해당 필드명 그대로 상수 값이 된다. 

String fieldAbcd = FieldNameConstantsExample.FIELD_ABCD;
System.out.println(fieldAbcd);
// abcd

위와 같이 작성하고 출력할 경우에 abcd 라는 문자열이 출력 된다. 

@FieldNameConstants
public class FieldNameConstantsExample {

private String ABCD;
}

만약 위와 같이 상수를 ABCD 대문자로 하면 필드명에 _ 언더바가 많이 생긴다.

String fieldAbcd = FieldNameConstantsExample.FIELD_A_B_C_D;

또 한  해당 필드에 접근제어를 할 수 도 있다.

@FieldNameConstants(level = AccessLevel.PRIVATE)
private String abcd;

글쎄다. 사용할지는 모르겠다. 아마도 당분간은 사용하지 않을 것 같다.

@Accessors

해당 어노테이션은 클래스 레벨에 사용할 경우 setter를 체이닝하게 만들 수 있는 어노테이션이다.  참고로 필드에서 사용할 수 있는 prefix는 언급하지 않겠다. 딱히 사용할 일도 없을 것 같아서.. 그냥 필드명 그대로 사용할 것 같다.

어쨌든 제외 하고도 두개의 속성이 있는데 다른점은 메서드명이 달라진다는 것뿐이지 하는 역할을 같다.

@Accessors(chain = true)
@Data
public class AccessorsExample {

    private String name;
    private String email;
}

위와 같이 chain 옵션은 사용할 경우에는 setter가 만들어 질 때 해당 클래스를 다시 리턴하는 체이닝방식으로 만들어 진다.

AccessorsExample accessorsExample = new AccessorsExample();
AccessorsExample emailAccessors = accessorsExample.setEmail("wonwoo@test.com");

그렇다고 해서 불변은 아니다.  상태를 변경시킨후에 해당 Object을 리턴할 뿐이다. 

public AccessorsExample name(String name) {
    this.name = name;
    return this;
}

public AccessorsExample email(String email) {
   this.email = email;
   return this;
}

대략 위와 같이 setter가 만들어 진다고 생각하면 된다.
이번엔 fluent 속성을 사용해보자.

@Accessors(fluent = true)
@Data
public class AccessorsExample {

    private String name;
    private String email;
}

달라진 것 메서드명 뿐이다. getter, setter 모두 달라진다.

AccessorsExample accessorsExample = new AccessorsExample();
AccessorsExample emailAccessors = accessorsExample.email("wonwoo@test.com");
AccessorsExample nameAccessors = accessorsExample.name("wonwoo");
String name = accessorsExample.name();

get과 set이 라는 prefix가 사라지고 email, name 으로 상태를 변경 시키고 값을 가져올 수 있다.  이 또한 상태만 변경시키지 새로운 Object을 만드는 것은 아니다. 

오늘은 이렇게 lombok을 잘써보자! 3탄을 가져나왔다. 예전에 썼던 1탄, 2탄 모두 참고하면 좋겠다.

부디 유용한 글이 되었기를..