[우아한테크코스] 10월 19일 TIL

3 minute read

[Spring] lombok 주의할점

lombok은 개발자에게 극강의 편리함을 제공해주지만 주의해야할 점이 존재한다.

lombok의 toString() 메서드는 java Reflection을 통해 이루어진다.

1. AllArgsConstructor, RequiredArgsConstructor 사용 시

private int hi;
private int hello;

리팩터링 도중 hi와 hello의 순서를 바꾸면 lombok이 변수 순서를 바꾼다.

그렇게 되면 해당 생성자가 사용된 모든 곳에 순서를 바꿔줘야 하는데, 놓치기 매우 쉽다.

생성자는 builder를 사용할 것

2. @EqualsAndHashCode 사용 시

불변 객체가 아닌 경우에 사용하는 것이 문제가 된다. 필드 값이 변경되면 hashCode가 변경되면서 찾지 못하기 때문이다.

불변객체에만 사용하거나, @EqualsAndHashCode(of={"name"}) 과 같이 동등성을 비교할 필드를 명시해야 한다.

따라서 @EqualsAndHashCode@RequiredArgsConstructor 를 포함하는 @Data, @EqualsAndHashCode@AllArgsConstructor를 포함하는 @Value를 사용할 때도 주의해야만 한다.

3. @Builder를 생성자나 static 객체 생성 메서드에 사용하기

Builder를 사용하면 내부적으로 @AllArgsConstructor 를 포함하고 있다. package-private으로 생성하기 때문에 크게 문제가 되지 않지만 내부적으로 호출할 때 1번과 같은 문제가 발생할 수 있으니 어노테이션 자체를 생성자에 붙이거나 정적팩토리메서드에 붙이는 것이 좋다.

4. @NonNull 사용 시

테스트 커버리지 측정 시 브랜치 커버리지가 커버되지 않는다.

5. @toString, @EqualsAndHashCode 필드명 지정 시

of나 exclude 메서드를 이용해 파라미터로 특정 필드를 지정해 처리 대상에 포함하거나 제외할 수 있다.

하지만 이게 내부적으로 Reflection을 이용하기 때문에 필드명에 오타가 있어도 알기 어렵다.

뿐만 아니라, of나 exclude를 적절히 사용하지 않으면 상호 참조 중인 두 객체가 toString() 메서드를 호출하면 서로의 toString을 계속 호출하며 무한 루프에 빠질 수 있다.

결론

@Getter, @Setter, @ToString 정도만 사용하고 보수적으로 사용하도록 하자!

큰 프로젝트를 할 때 lombok.config 를 이용해 어노테이션을 금지할 수 있다.

참고

[Spring] transaction

propagation이 REQUIRES_NEW인 경우에 서비스에서 트랜잭션(readonly=false) 메서드를 호출하고, 그 내부에서 또 다른 트랜잭션(readonly=true)을 호출하면 readonly=true의 옵션이 먹지 않는다.

그 이유는 트랜잭션은 우선 AOP로 동작하는데, AOP는 프록시로 한번 감싸진 채로 메서드를 동작시킨다. 하지만 그 내부에서 또 다른 트랜잭션이 발생하면 새로운 트랜잭션을 생성하기 위해 프록시를 갈아끼워야 한다. 따라서 requires_new 를 주었음에도 불구하고 새로운 트랜잭션이 생성되지 않고 기존의 트랜잭션을 사용한다.

참고

[JPA] @DynamicInsert, @DynamicUpdate

DynamicInsert 어노테이션이 달려있는 엔티티의 경우 null이 들어오게 되면 쿼리에 해당 컬럼이 제외된다.

또한 update의 경우 변경된 필드만 수정하는 쿼리가 날라간다.

nullable=false인 경우는 null로 들어간다.

//전
Hibernate:
	insert into member (name, age, college) values (?, ?, ?)
	
//후
Hibernate:
	insert into member (name, age) values (?, ?)

Entity Listener

JPA의 리스너 기능은 엔티티의 생명주기에 따른 이벤트를 처리할 수 있게 한다.

  • PrePersist: 엔티티를 영속성 컨텍트스에 관리하기 직전
  • PostPersist: 엔티티를 데이터베이스에 저장한 직후
  • PreUpdate: 엔티티를 데이터베이스에 수정하기 직전
  • PostUpdate: 엔티티를 데티터베이스에 수정한 직후
  • PreRemove: 엔티티를 영속성 컨텍스트에서 삭제하기 직전
  • PostRemove: 엔티티를 데이터베이스에 삭제한 직후
  • PostLoad: 엔티티가 영속성 선텍스트에 조회된 직후 또는 refresh를 호출한 후

[infra] 로드밸런싱 세션 문제

우리는 스케일 아웃을 통해 하나의 서버에 가는 부하를 줄이고자 한다. 이렇게 서버가 다수가 되어버리면, 요청이 들어왔을 때 어떠한 서버에 요청을 전달할지 결정해야하고, 이를 위해 사용하는 것이 로드밸런싱이다. 로드밸런싱은 병목현상이 생기지 않도록 적절한 서버에 요청을 전달하는 것이 목표이다.

그렇다면 서버의 데이터 정합성 문제는 어떻게 해결할 수 있을까? 제일 눈에 띄는 예시로 세션 관리 문제가 있다. 서버는 세션을 저장해 관리한다. 하지만 예를 들어 사용자는 로그인을 했는데 그에 대한 세션이 서버1에만 저장이 되어 있으면 어떻게 될까? 다른 어떠한 요청을 보냈을 때 서버2로 요청이 전달된다면 서버는 사용자가 로그인을 제대로 했는지 알 수 없을 것이다.

이 문제는 아래 방법들로 해결할 수 있다.

  1. sticky session

    사용자가 로그인을 해 세션이 저장되었다면, 그 사용자의 모든 요청은 저장된 서버로만 전송되는 방식이다.

    로드밸런서는 요청을 받으면 요청에 쿠키가 존재하는지 확인하고, 있으면 쿠키에 지정된 서버로 전송하고 없으면 기존 알고리즘으로 서버를 선정해 요청을 전송한다.

    이 경우 유저는 동일한 서버를 사용하기 때문에 데이터 정합성에서 자유롭다.

    하지만 이렇게 되면 특정 서버에 트래픽이 집중될 가능성이 존재한다. 스케일 아웃의 장점을 누릴 수 없고 그렇게 하나의 서버가 장애가 발생하면 그 서버를 이용했던 사용자들은 세션 정보를 잃어버려 가용성이 떨어진다.

  2. session clustering

    여러 서버를 연결해 하나의 시스템처럼 동작하도록 하는것이 클러스터링

    tomcat의 세션 클러스터링은?

    All-to-all session replication을 이용해 세션을 복제한다. 세션 저장소에 요소가 변경되면 이 사항을 다른 모든 세션에 복제한다는 것이다. 하지만 이렇게 되면 모든 서버가 세션 객체를 가져야 하기 때문에 메모리가 많이 필요하고 서버가 늘어날수록 비효율적이다.

  3. 그외

    spring에서는 bean을 globalSession scope으로 등록하면 session이 전역 portlet의 생명주기의 scope를 가진다.

    Primary-secondary 세션 복제를 이용해 primary 서버는 secondary 서버에 세션 객체의 key-value 전체를 복제하고 그 외 서버에는 key만 복제하도록 한다. 그러면 복제에 대한 시간은 줄일 수 있지만 다시 value를 복제하는 단점이 여전히 존재한다.

    session 저장소를 이용하는 세션 스토리지 방식, 여기서 in-memory db(ex. redis)를 쓰면 i/o에 대한 부담을 줄일 수 있고 없어지는 것도 세션이라 괜찮으니 disk 보다 낫다.