프로젝트/팀 프로젝트

[팀 INXJ] 마지막 리팩토링

go_getter 2025. 4. 11. 21:59

도메인에서 공통적으로 사용하던 orElseThrow문 공통 클래스에서 처리

@Component
@RequiredArgsConstructor
public class EntityFetcher {

    private final UserRepository userRepository;
    private final PostRepository postRepository;
    private final PostLikeRepository postLikeRepository;
    private final CommentRepository commentRepository;
    private final CommentLikeRepository commentLikeRepository;
    private final FriendRepository friendRepository;

    public User getUserOrThrow(Long userId) {

        return userRepository.findById(userId)
                .orElseThrow(NotFoundUserException::new);
    }

    public Post getPostOrThrow(Long postId) {
        return postRepository.findById(postId)
                .orElseThrow(() -> new BaseException(ErrorCode.NOT_FOUND_POST_ID));
    }

    public PostLike getPostLikeOrThrow(PostLikeId postLikeId) {
        return postLikeRepository.findById(postLikeId)
                .orElseThrow(NotFoundPostException::new);
    }

    public Comment getCommentOrThrow(Long commentId) {
        return commentRepository.findById(commentId)
                .orElseThrow(() -> new BaseException(ErrorCode.NOT_FOUND_COMMENT_ID));
    }

    public CommentLike getCommentLikeOrThrow(CommentLikeId commentLikeId) {
        return commentLikeRepository.findById(commentLikeId)
                .orElseThrow(() -> new BaseException(ErrorCode.NOT_FOUND_LIKE_ID));
    }

    public FriendRequest getInteractiveFriendRequestOrThrow(User user1, User user2){
        return friendRepository.findInteractiveRequest(user1, user2)
                .orElseThrow(FriendRequestAlreadyHandledException::new);
    }

    public FriendRequest getFriendRequestOrThrow(User user1, User user2){
        return friendRepository.findByReceiverAndRequester(user1, user2)
                .orElseThrow(FriendRequestAlreadyHandledException::new);
    }

}

 

기존의 각 도메인별 서비스 레이어에서 공통적으로 사용중이던 orElseThrow문을 EntityFetcher 클래스로 묶어서 공통적으로 처리했다.

이렇게 해서 각 서비스 레이어의 가독성을 향상시킬 수 있고, user 정보를 뽑아오는 메서드의 경우는 대부분의 도메인에서 모두 사용했었기 때문에 이 방법이 관리에 수월했다.

 


엔티티 필드에 @Column(columnDefinition = "longtext") 사용

JPA는 기본적으로 자바 타입을 기반으로 적절한 DB 타입을 생성하지만, columnDefinition을 통해 직접 DB 타입을 지정할 수 있다.

위 어노테이션은 MySQL의 TEXT 계열 중 가장 큰 용량을 지원한다.

어노테이션 없이 String 타입 필드로만 정의하면 JPA는 기본적으로 VARCHAR(255)로 생성하기 때문에 최대 255자까지 저장이 가능하다.

물론 @Column(length = 1000) 어노테이션을 사용해서 최대 1000자까지 저장도 가능하다.

따라서 긴 게시글, 댓글, HTML 콘텐츠, 블로그 포스트를 필드로 저장해야 하거나 JSON과 같은 비정형 데이터를 그대로 문자열로 저장해야 할 때 @Column(columnDefinition = "longtext")를 사용하면 좋다.

 

사용 시 주의사항

- 인덱스 제한

인덱싱이 제한적이기 때문에 전체 문자열을 대상으로 LIKE 키워드를 사용해서 검색을 하면 성능 이슈가 발생할 수 있다.

 

- DBMS 종속성

MySQL 전용 문법이기 때문에 PostgreSQL, Oracle 등의 다른 DB와 호환되지 않을 수 있어서 추후 DB 변경 시 주의해야 한다.

 

- 메모리 사용량

너무 긴 데이터를 자주 불러오면 JPA Entity 조회 성능 저하 및 메모리 사용 증가 위험이 있다.

 


JPQL에 CONCAT('%', ?1, '%') 사용

LIKE 검색 시 검색어를 동적으로 생성하기 위해 사용한다.

만약 임의의 문자열이 아니라, 정확히 1개의 임의의 문자를 조건으로 검색하고 싶다면 `%` 대신 `_`을 사용하면 된다.

SELECT * FROM user WHERE username LIKE CONCAT('%', ?1, '%')

 

`?1`은 첫 번째 파라미터를 바인딩하기 위해 사용된다.

만약 ?1 = "john"이라면 이 sql문은 아래처럼 해석된다.

username LIKE '%john%'

 

 

nativeQuery 속성값을 true로 설정한다고 해도, 위와 같이 직접 문자열을 쿼리에 넣어서 작성하게 되면 SQL Injection 문제가 발생할 수 있고, 무엇보다 코드 재사용이 불가능하기 때문에 동적으로 문자열을 주입해주는 게 좋다.

 

동적 쿼리를 생성하는 방법은 위 예시처럼 concat을 사용하거나, 애초에 파라미터로 %가 포함된 문자열을 전달하는 방법이 있다.

@Query(value = "SELECT * FROM user WHERE username LIKE CONCAT('%', :keyword, '%')", nativeQuery = true)
List<User> findByUsernameContaining(@Param("keyword") String keyword);
@Query(value = "SELECT * FROM user WHERE username LIKE :keyword", nativeQuery = true)
List<User> findByUsernameLike(@Param("keyword") String keyword);

// 호출 시:
repository.findByUsernameLike("%john%");

 


SQL 쿼리에서 DTO 사용

"SELECT new inxj.newsfeed.domain.user.dto.response.SearchUsersResponseDto(u.username,u.profileImageUrl)" +
            " FROM User u WHERE u.username LIKE CONCAT('%', :username, '%') AND u.deletedAt IS NULL"

 

SQL 쿼리에서 new DTO로 코드를 작성하면, 반환에 필요한 데이터만 select해서 서비스 레이어로 반환할 수 있다.

주의할 점은 JPA가 해당 DTO 클래스의 생성자를 리플렉션으로 찾아서 실행하기 때문에, 패키지 전체 경로를 지정해줘야 정확히 어떤 클래스인지 알 수 있다.

그리고 이 DTO 내부에 반드시 해당 필드들을 받는 생성자가 필요하다.

반대로 DTO 생성자 파라미터가 바뀌면 해당 쿼리도 수정해야 한다는 단점이 있다.

 

 

 


Object.equals(a, b)

두 데이터가 같은지 null-safe하게 비교하는 코드이다.

즉, a.equals(b)를 NPE 없이 비교할 수 있도록 도와주는 메서드이다.

Long id = null;
Long loginUserId = 1L;

// id.equals(loginUserId); <- NPE 발생

Objects.equals(id, loginUserId); // false

 

내부적으로는 아래와 같이 동작한다.

return (a == b) || (a != null && a.equals(b));