본문 바로가기

프로젝트 기록

코드 리팩토링 기록 - JPA Embedded 타입 활용

전체적으로 코드를 깔끔하게 짜고, 컨벤션을 맞추기 위해 여러번 리팩토링을 진행하긴 했지만 기록에 남기고 싶은 리팩토링을 포스팅으로 쓰고자 한다.

 

간단하게 말하면, JPA의 Embedded 타입을 활용해서 Entity class의 코드를 깔끔하게 유지시켰다.

 

Board와 BoardComment를 예시 코드로 이게 무슨 말인지 보자.

 

@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate
@Table(name = "tbl_all_board")
public class Board extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

//   @OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
//    private List<BoardComment> commentList = new ArrayList<>();

//   @OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
//    private List<BoardLike> likeList = new ArrayList<>();

    @Embedded
    private final BoardComments boardComments = new BoardComments();

    @Embedded
    private final BoardLikes boardLikes = new BoardLikes();

    private Integer boardType; //1:자유 2:익명 3:취업정보 4:질문

    private String title;

    private String content;

    private Integer views;

    public void addView() {
        this.views++;
    }

    public void addComment(BoardComment comment){
        this.boardComments.getBoardComments().add(comment);
        comment.setBoard(this);
    }

    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }

    @Builder
    public Board(User user, Integer boardType, String title, String content){
        this.user = user;
        this.boardType = boardType;
        this.title = title;
        this.content = content;
        this.views=0;
    }

    public boolean toggleBoardLike(BoardLike boardLike) {
        return boardLikes.toggleBoardLike(boardLike);
    }

    public int getTotalComments(){
        return boardComments.size();
    }

    public int getTotalLikes(){
        return boardLikes.size();
    }
}

 

코드를 보면 처음에는 주석처리 된 곳처럼 일반적인 방법으로 commentList와 likeList를 entity class에 선언했으나, 후에 이 둘을 Embedded화 해서 boardComments, boardLikes로 설정하였다.

또한, boardLike에 대한 작업은 toggleBoardLike 메서드 하나로만 캡슐화되어 나타나 있는 걸 확인할 수 있다.

 

 

이 둘은 거의 유사한 구조이므로 boardLikes만 살펴보자. boardLikes 타입 클래스는 다음과 같다.

 

@Getter
@NoArgsConstructor
@Embeddable
public class BoardLikes {

    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<BoardLike> boardLikes = new ArrayList<>();

    public int size(){
        return boardLikes.size();
    }

    public boolean toggleBoardLike(BoardLike boardLike){
        if(contains(boardLike.getUserId())){
            removeBoardLike(boardLike);
            return false;
        }
        boardLikes.add(boardLike);
        return true;
    }

    public boolean contains(Long userId){
        return boardLikes.parallelStream()
                .anyMatch(l -> l.ownedBy(userId));
    }

    private void removeBoardLike(BoardLike boardLike) {
        Long userId = boardLike.getUserId();
        BoardLike removalTarget = boardLikes.parallelStream()
                .filter(l -> l.ownedBy(userId))
                .findAny()
                .orElseThrow(BoardLikeRemovalFailure::new);
        boardLikes.remove(removalTarget);
    }
}

 

결국 Board class에서 주석처리 된 부분이 위 코드에서 그대로 쓰인 걸 확인할 수 있다.

이렇게 따로 분리해서 Board에서는 캡슐화 된 BoardLikes 타입으로 board의 좋아요 리스트를 선언한 것이다.

그런 후, toggle 방식의 게시물 좋아요 로직은 다 여기에 담았다.

 

만약 이미 좋아요가 눌러진 상태에서 PUT 요청이 들어오면 좋아요를 취소하고 리턴값으로 false를 보내주고,

좋아요가 눌러지지 않은 상태에서 PUT 요청이 들어오면 like 리스트에 이를 추가하고 true를 리턴하는 원리이다.

 

또한, parallelStream()을 사용해서 병렬 처리를 하게끔 해서, 처리 속도를 높였다.

 

 


 

이러한 리팩토링 방식은 우테코 깃허브를 참고했다.

 

처음에는 이게 쿼리 최적화에 도움을 주는 줄 알고 클론 코딩했는데, 실행시켜 보니 나가는 쿼리 갯수는 똑같았다.

그렇다면 Board Like, Study Scrap을 embedded로 해서 굳이 한 겹을 더 싼 이유는 무엇일까? 결국 양방향 관계인건 마찬가지인데.

 

위에서도 설명했지만, 다시 한번 정리하자면-

Like나 Scrap을 PUT 토글 방식으로 구현할 건데 그 로직 부분을 따로 분리해서 구현하기 위해서였다.

Board, Study entity 내에서 그 작업을 하면 클래스가 무거워지고, db entity만의 정보를 담지 않게 되므로 코드가 더러워지니까 이렇게 따로 빼고, 캡슐화를 시킨 것이다.

이렇게 또 한 수 배웠다 :)