[팀 INXJ] 마지막 리팩토링
도메인에서 공통적으로 사용하던 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));