Home mbti 지연로딩 문제, 변경감지 with setter
Post
Cancel

mbti 지연로딩 문제, 변경감지 with setter

평가 로직을 구현하는데 발생한 문제를 정리해보고자 한다.

평가 로직

웹툰에 대한 사용자의 평가를 Evaluation Entity에 저장하는 로직이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
public class Evaluation extends BaseEntity {  
  
    @Id @GeneratedValue  
    private Long id;  
  
    @ManyToOne  
    @JoinColumn(name = "account_id")  
    private Account account;  
  
    @ManyToOne  
    @JoinColumn(name = "webtoon_id")  
    private Webtoon webtoon;  
  
    private Double rating;  
  
    @Builder  
    public Evaluation(Account account, Webtoon webtoon, Double rating) {  
	    this.account = account;  
        this.webtoon = webtoon;  
        this.rating = rating;  
    }  
  
    public void updateRating(Double rating) {  
	    this.rating = rating;  
    }  
}

문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping  
public ResponseEntity<Evaluation> evaluate(@RequestBody EvaluationRequestDto evaluationRequest) {  
	Account account = accountService.findByUsername(evaluationRequest.getUsername());  
	Webtoon webtoon = webtoonService.findById(evaluationRequest.getWebtoonId());  
  
    Evaluation evaluation = Evaluation.builder()  
	    .account(account)  
	    .webtoon(webtoon)  
	    .rating(evaluationRequest.getRating())  
	    .build();  
  
    Evaluation savedEvaluation = evaluationService.saveEvaluation(evaluation);  
  
    return ResponseEntity.ok(savedEvaluation);  
}

Evaluation 엔티티에는 Account (평가 주체) 와 Webtoon (평가 대상)이 필요하다.

따라서 각각의 Service 계층을 이용해 해당 객체들을 가져왔다.

여기서 Account 엔티티를 보자.

1
2
3
@ManyToOne(fetch = FetchType.LAZY)  
@JoinColumn(name = "mbti_id")  
private Mbti mbti;

Mbti 엔티티와의 관계가 ManyToOne이고, 지연로딩 설정이 되어 있다.

이 와중 아래와 같은 에러를 만나게 된다.

1
2
3
4
InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: 
com.recommtoon.recommtoonapi.evaluation.entity.Evaluation["account"]-
>com.recommtoon.recommtoonapi.account.entity.Account["mbti"]-
>com.recommtoon.recommtoonapi.mbti.entity.Mbti$HibernateProxy$dYXn3N0k["hibernateLazyInitializer"])

요약하자면 엔티티를 json으로 직렬화하는데, 이 중 mbti가 지연로딩으로 설정되어 프록시 객체를 직렬화하게 되어 발생하는 예외이다.

이 문제의 해결 방법은 3가지이다.

  • 지연로딩을 즉시로딩으로 바꾸기.
    • DB 쿼리를 추가로 실행하므로 성능에 좋지 않아 사용하지 않음.
  • DTO를 사용해 엔티티 대신 DTO를 직렬화 하는 방법.
    • Evaluation 엔티티에 데이터를 저장하는 로직이 필요했으므로 X.
  • 페치조인

해결

이 중 위와 같은 이유들로 페치조인으로 해결하는 것을 선택했다.

복습할 겸 정리하자면 페치조인은 연관된 엔티티나 컬렉션을 SQL 쿼리 한 번으로 모두 함께 조회하도록 해준다.

사실 이 부분만 봐서는 한 명의 Account, 하나의 Mbti만을 필요로 하기 때문에 N+1 문제나, 즉시로딩을 사용함에 의한 성능 문제는 발생하지 않을 것이다.

하지만 추후 지연로딩 설정이 필요하게 될 것이고, 따라서 지연 로딩을 유지할 수 있는 페치조인을 선택하게 된 것이다.

1
2
@Query("select a from Account a join fetch a.mbti where a.username = :username")  
Account findByUsernameWithMbti(@Param("username") String username);

변경 감지에 관한 문제

내 웹툰 평가는 웹툰 카드의 별을 누르면 post 요청을 보내 별에 해당하는 수를 rating으로 DB에 저장하게 된다.

이 때 별을 바꾸면서 누를 수 있다.

한 번 별을 누르면 웹툰 카드가 사라지거나, 제약을 걸어 다시 평가할 수 없도록 한 것이 아니다. 따라서 이 부분을 고려한다면 DB에 계속 컬럼이 추가되는 것이 아닌 기존 값이 update 되어야 한다.

이 때 문제가 있다.

1
evaluationRepository.save(evaluation);

스프링 데이터 JPA 리포지토리를 사용하고 있는데, 위의 save() 메서드는 값이 없을 때 persist()를, 값이 존재한다면 merge()를 실행한다.

  • merge()의 문제
    • merge는 일부 값의 업데이트가 아닌 해당 데이터를 통째로 교체한다.
    • 따라서 경우에 따라 null 값이 들어갈 수 있다.
    • 거기에 더해 DB에 쿼리를 실행하므로 추후 성능에 문제가 있을 수 있다.

따라서 나는 단순하게 save()를 호출하는 대신 merge()를 사용하지 않을 로직이 필요했다.

변경 감지

트랜잭션이 적용된 상태에서 영속상태의 엔티티를 조회해 그 엔티티의 값을 바꾸면 변경 감지를 통한 값 변경이 가능하다.

  • 트랜잭션 커밋 시점에 변경 감지가 실행된다.
  • 원하는 속성만 변경이 가능하다.

이러한 변경 감지를 통한 값 업데이트를 구현하려 했는데 고민이 생겼다.

setter에 대한 문제

클린코드에서 setter의 사용을 주의해야 한다.

  • 객체의 일관성을 유지하기 어렵다.
    • setter로 어디서든 접근이 가능해 의도치 않게 값이 변경될 수 있다.
  • 사용의도를 파악하기 어렵다.
    • 값의 생성인지, update인 지 setter 네이밍으로는 파악하기 어렵다.
  • 캡슐화가 깨진다.
  • 불변 객체를 만들 수 없다.
    • 객체의 상태가 변하지 않음을 보장해야 예측 가능하고 안전하다.

위와 같은 이유로 setter를 사용하지 않으려 했다.

하지만 변경감지를 하려면 결국 값을 변경해야 하는데 setter 없이 할 수 있을까에 대한 고민이 시작되었다.

해결

여러가지 해결방법을 찾아보았다.

  • 단순한 setter 대신 의미 있는 메서드를 만든다.
  • 생성자를 통한 값 변경
  • 빌더패턴을 이용한 값 변경

이 중 나는 setter 대신 의미있는 메서드를 만들기로 했다.

생성자나 빌더패턴을 이용하는 것은 결국 객체를 새로 생성하는 것이고 이는 엔티티의 상태를 변경하는 것이 아니기 때문에 변경 감지가 일어나지 않는다.

즉 변경감지가 아닌 새로운 객체를 생성해 저장하는 것일 뿐이다. 따라서 해결책이 되지 않는다.

1
2
3
public void updateRating(Double rating) {  
   this.rating = rating;  
}

Evaluation 엔티티의 값이 변동되는 것은 rating 뿐이다. 따라서 로직 자체는 setter와 같다.

하지만 의미있는 네이밍을 통해 의도를 명확히 할 수 있다.

그리고 이렇게 단순하게 하나의 속성이 아닌 여러 속성을 바꾸게 된다면 setter와는 다르다고 볼 수 있을 것이다.

결론적으로는 setter를 사용한 것은 맞다. 하지만 변경감지를 위해서는 결국 setter 로직은 반드시 필요하다.

내가 모르는 것일 수 있기 때문에 다른 방법이 있는지는 더 찾아볼 예정이다.

This post is licensed under CC BY 4.0 by the author.

Refresh 토큰을 이용한 Access 토큰 갱신에서의 문제

페이징에 shuffle 적용하기, 무한스크롤