-
Notifications
You must be signed in to change notification settings - Fork 0
Driven(query) 구현 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Driven(query) 구현 #25
Changes from all commits
394c3f2
35ef464
d2a4b18
c28527b
79a7a7f
de50be5
961c2fc
f695a0e
45c0c06
505eb0e
9e08025
ef18ae3
5004dff
6e23033
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package me.nettee.board.adapter.driven.mapper; | ||
|
||
import me.nettee.board.adapter.driven.persistence.entity.BoardEntity; | ||
import me.nettee.board.application.domain.Board; | ||
import org.mapstruct.Mapper; | ||
|
||
import java.util.Optional; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
import me.nettee.board.application.model.BoardReadSummaryModel; | ||
|
||
// Entity <-> Domain 매핑 클래스 | ||
@Mapper(componentModel = "spring") | ||
public interface BoardEntityMapper { | ||
Board toDomain(BoardEntity boardEntity); | ||
BoardReadDetailModel toBoardReadDetailModel(BoardEntity boardEntity); | ||
BoardReadSummaryModel toBoardReadSummaryModel(BoardEntity boardEntity); | ||
BoardEntity toEntity(Board board); | ||
|
||
default Optional<BoardReadDetailModel> toOptionalBoardReadDetailModel(BoardEntity boardEntity) { | ||
return Optional.ofNullable(toBoardReadDetailModel(boardEntity)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package me.nettee.board.adapter.driven.persistence; | ||
|
||
import me.nettee.board.adapter.driven.persistence.entity.BoardEntity; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.PageRequest; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.Set; | ||
|
||
public interface BoardJpaRepository extends JpaRepository<BoardEntity, Long> { | ||
Page<BoardEntity> findByStatusIn(Set<BoardStatus> statuses, Pageable pageable); | ||
Page<BoardEntity> findByStatus(BoardStatus status, PageRequest pageRequest); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package me.nettee.board.adapter.driven.persistence; | ||
|
||
import me.nettee.board.adapter.driven.mapper.BoardEntityMapper; | ||
import me.nettee.board.adapter.driven.persistence.entity.BoardEntity; | ||
import me.nettee.board.adapter.driven.persistence.entity.QBoardEntity; | ||
import me.nettee.board.application.domain.Board; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import me.nettee.board.application.port.BoardQueryPort; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; | ||
import org.springframework.data.support.PageableExecutionUtils; | ||
import org.springframework.stereotype.Repository; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
import me.nettee.board.application.model.BoardReadSummaryModel; | ||
|
||
import java.util.Optional; | ||
|
||
merge-simpson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Repository | ||
public class BoardQueryAdapter extends QuerydslRepositorySupport implements BoardQueryPort { | ||
private final BoardEntityMapper boardEntityMapper; | ||
QBoardEntity boardEntity = QBoardEntity.boardEntity; | ||
|
||
public BoardQueryAdapter(BoardEntityMapper boardEntityMapper) { | ||
super(BoardEntity.class); | ||
this.boardEntityMapper = boardEntityMapper; | ||
} | ||
|
||
@Override | ||
public Optional<BoardReadDetailModel> findById(Long id) { | ||
return boardEntityMapper.toOptionalBoardReadDetailModel( | ||
getQuerydsl().createQuery() | ||
.select(boardEntity) | ||
.from(boardEntity) | ||
.where( | ||
boardEntity.id.eq(id) | ||
).fetchOne() | ||
); | ||
} | ||
|
||
@Override | ||
public Page<BoardReadDetailModel> findAll(Pageable pageable) { | ||
// 기본 쿼리 생성 | ||
var query = getQuerydsl().createQuery() | ||
.select(boardEntity) | ||
.from(boardEntity) | ||
.where(); | ||
|
||
// pageable 정렬 조건 적용 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. private method로 작성하는 방식도 생각해볼 수 있을 것 같습니다. |
||
pageable.getSort().forEach(order -> { | ||
if (order.isAscending()) { | ||
query.orderBy(boardEntity.createdAt.asc()); // createdAt 필드를 기준으로 오름차순 정렬 | ||
} else { | ||
query.orderBy(boardEntity.createdAt.desc()); // createdAt 필드를 기준으로 내림차순 정렬 | ||
} | ||
}); | ||
|
||
var result = query | ||
.offset(pageable.getOffset()) // 현재 페이지의 오프셋 설정 | ||
.limit(pageable.getPageSize()) // 페이지 크기 설정 | ||
.fetch(); // 쿼리 실행 | ||
|
||
var totalCount = getQuerydsl().createQuery() | ||
.select(boardEntity.count()) | ||
.from(boardEntity) | ||
.where(); | ||
|
||
return PageableExecutionUtils.getPage( | ||
result.stream().map(boardEntityMapper::toBoardReadDetailModel).toList(), | ||
pageable, | ||
totalCount::fetchOne | ||
); | ||
} | ||
|
||
@Override | ||
public Page<BoardReadSummaryModel> findByStatusesList(Pageable pageable, java.util.Set<me.nettee.board.application.domain.type.BoardStatus> statuses) { | ||
// 기본 쿼리 생성 | ||
var query = getQuerydsl().createQuery() | ||
.select(boardEntity) | ||
.from(boardEntity) | ||
.where(boardEntity.status.in(statuses)); | ||
|
||
// pageable 정렬 조건 적용 | ||
pageable.getSort().forEach(order -> { | ||
if (order.isAscending()) { | ||
query.orderBy(boardEntity.createdAt.asc()); // createdAt 필드를 기준으로 오름차순 정렬 | ||
} else { | ||
query.orderBy(boardEntity.createdAt.desc()); // createdAt 필드를 기준으로 내림차순 정렬 | ||
} | ||
}); | ||
|
||
var result = query | ||
.offset(pageable.getOffset()) // 현재 페이지의 오프셋 설정 | ||
.limit(pageable.getPageSize()) // 페이지 크기 설정 | ||
.fetch(); // 쿼리 실행 | ||
|
||
var totalCount = getQuerydsl().createQuery() | ||
.select(boardEntity.count()) | ||
.from(boardEntity) | ||
.where(boardEntity.status.in(statuses)); | ||
|
||
return PageableExecutionUtils.getPage( | ||
result.stream().map(boardEntityMapper::toBoardReadSummaryModel).toList(), | ||
pageable, | ||
totalCount::fetchOne | ||
); | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 컨플릭트 해결을 응원하겠습니다.원래 동일 driven 어댑터 내에서 공통으로 작업되는 파일들은 별도 브랜치 생성 또는 각 브랜치 간 병합으로 서로 다른 브랜치가 동일한 이름으로 새 파일을 만들어서 비슷한 코드를 작성하면 많은 컨플릭트를 해결해야 할 수 있습니다. 이번에는 그 파일의 개수와 규모가 조금 클 수 있어서 도움이 필요하다면 @silberbullet 님께서 도와주시기로 하셨지만, 컨디션이 좋지 않으셔서 다른 팀원들이 함께 참여하는 게 좋아 보입니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package me.nettee.board.adapter.driven.persistence.entity; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.*; | ||
Comment on lines
+3
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎨 와일드카드(*) 임포트를 사용하지 않습니다.구글의 Java 코드 스타일 가이드에서 다음 임포트 종류는 사용하지 않거나 제한적으로만 사용합니다.
예를 들어, 유틸리티 함수 등 자주 사용되거나 팀에서 수용할 수 있는 정도에서는 정적 임포트를 사용합니다. 테스트 코드에서는 정적 임포트, 와일드카드 임포트, 정적 와일드카드 임포트를 모두 사용할 수 있습니다. |
||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import org.hibernate.annotations.DynamicUpdate; | ||
|
||
import java.time.Instant; | ||
|
||
@Getter | ||
@Builder(toBuilder = true) | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@AllArgsConstructor | ||
@DynamicUpdate | ||
@Entity(name = "board") | ||
public class BoardEntity { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "id", nullable = false) | ||
private Long id; | ||
|
||
@Column(name = "title", length = 20, nullable = false) | ||
private String title; | ||
|
||
@Column(name = "content", length = 300, nullable = false) | ||
private String content; | ||
|
||
@Enumerated(EnumType.STRING) | ||
@Column(name = "status") | ||
private BoardStatus status; | ||
|
||
@Column(name = "created_at") | ||
private Instant createdAt; | ||
|
||
@Column(name = "updated_at") | ||
private Instant updatedAt; | ||
|
||
// 삭제 | ||
public void softDelete() { | ||
this.updatedAt = Instant.now(); | ||
this.status = BoardStatus.REMOVED; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package me.nettee.board.application.exception; | ||
|
||
import me.nettee.common.exeption.ErrorCode; | ||
import org.springframework.http.HttpStatus; | ||
|
||
public enum BoardCommandErrorCode implements ErrorCode { | ||
// ⬇️ 인증 쪽 에러코드 제공받아 쓸 것이냐 | ||
// UNAUTHORIZED("로그인이 필요한 기능입니다.", HttpStatus.UNAUTHORIZED), | ||
// ⬇️ 조회 쪽 에러코드 제공받아 쓸 것이냐, 아니면 Master DB 핸들 시 사용할 용도로 여기 따로 둘 거냐. | ||
BOARD_NOT_FOUND("게시물을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), | ||
BOARD_GONE("더 이상 존재하지 않는 게시물입니다.", HttpStatus.GONE), | ||
BOARD_FORBIDDEN("권한이 없습니다.", HttpStatus.FORBIDDEN), | ||
DEFAULT("게시물 조작 오류", HttpStatus.INTERNAL_SERVER_ERROR); | ||
|
||
private final String message; | ||
private final HttpStatus status; | ||
|
||
BoardCommandErrorCode(String message, HttpStatus status) { | ||
this.message = message; | ||
this.status = status; | ||
} | ||
|
||
@Override | ||
public String defaultMessage() { | ||
return message; | ||
} | ||
|
||
@Override | ||
public HttpStatus defaultHttpStatus() { | ||
return status; | ||
} | ||
|
||
@Override | ||
public BoardCommandException defaultException() { | ||
return new BoardCommandException(this); | ||
} | ||
|
||
@Override | ||
public BoardCommandException defaultException(Throwable cause) { | ||
return new BoardCommandException(this, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package me.nettee.board.application.exception; | ||
|
||
import me.nettee.common.exeption.CustomException; | ||
import me.nettee.common.exeption.ErrorCode; | ||
|
||
public class BoardCommandException extends CustomException { | ||
// (intellij) Ctrl + O | ||
// (eclipse or sts) Alt Shift S => generate constructors | ||
|
||
public BoardCommandException(ErrorCode errorCode) { | ||
super(errorCode); | ||
} | ||
|
||
public BoardCommandException(ErrorCode errorCode, Throwable cause) { | ||
super(errorCode, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package me.nettee.board.application.exception; | ||
|
||
import me.nettee.common.exeption.ErrorCode; | ||
import org.springframework.http.HttpStatus; | ||
|
||
public enum BoardQueryErrorCode implements ErrorCode { | ||
// ⬇️ 인증 쪽 에러코드 제공받아 쓸 것이냐 | ||
// UNAUTHORIZED("로그인이 필요한 기능입니다.", HttpStatus.UNAUTHORIZED), | ||
// ⬇️ 조회 쪽 에러코드 제공받아 쓸 것이냐, 아니면 Master DB 핸들 시 사용할 용도로 여기 따로 둘 거냐. | ||
BOARD_NOT_FOUND("게시물을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), | ||
BOARD_GONE("더 이상 존재하지 않는 게시물입니다.", HttpStatus.GONE), | ||
BOARD_FORBIDDEN("권한이 없습니다.", HttpStatus.FORBIDDEN), | ||
DEFAULT("게시물 조작 오류", HttpStatus.INTERNAL_SERVER_ERROR); | ||
|
||
private final String message; | ||
private final HttpStatus status; | ||
|
||
BoardQueryErrorCode(String message, HttpStatus status) { | ||
this.message = message; | ||
this.status = status; | ||
} | ||
|
||
@Override | ||
public String defaultMessage() { | ||
return message; | ||
} | ||
|
||
@Override | ||
public HttpStatus defaultHttpStatus() { | ||
return status; | ||
} | ||
|
||
@Override | ||
public BoardCommandException defaultException() { | ||
return new BoardCommandException(this); | ||
} | ||
|
||
@Override | ||
public BoardCommandException defaultException(Throwable cause) { | ||
return new BoardCommandException(this, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package me.nettee.board.application.exception; | ||
|
||
import me.nettee.common.exeption.CustomException; | ||
import me.nettee.common.exeption.ErrorCode; | ||
|
||
public class BoardQueryException extends CustomException { | ||
public BoardQueryException(ErrorCode errorCode) { | ||
super(errorCode); | ||
} | ||
|
||
public BoardQueryException(ErrorCode errorCode, Throwable cause) { | ||
super(errorCode, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package me.nettee.common.exeption; | ||
|
||
public class CustomException extends RuntimeException { | ||
protected ErrorCode errorCode; | ||
|
||
public CustomException(ErrorCode errorCode) { | ||
// ErrorCode의 기본 메시지를 RuntimeException에 전달하여 예외 메시지를 설정 | ||
super(errorCode.defaultMessage()); | ||
this.errorCode = errorCode; | ||
} | ||
|
||
public CustomException(ErrorCode errorCode, Throwable cause) { | ||
super(errorCode.defaultMessage(), cause); | ||
this.errorCode = errorCode; | ||
} | ||
|
||
public ErrorCode getErrorCode() { | ||
return errorCode; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package me.nettee.common.exeption; | ||
|
||
import org.springframework.http.HttpStatus; | ||
|
||
public interface ErrorCode { | ||
String name(); // enum default method | ||
String defaultMessage(); | ||
HttpStatus defaultHttpStatus(); | ||
RuntimeException defaultException(); | ||
RuntimeException defaultException(Throwable cause); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 전체적으로 가독성이 매우 좋고, 잘 정리된 코드입니다.
앞으로 추가적인 논의가 생긴다면 반영하게 될 수도 있지만,
더 구체적인 다음 단계 작업은 멀티모듈 프로젝트 때 담길 것 같습니다. 👍
예를 들면, 페이지를 생성할 때 테이블의 크기를 이대로 조회해서 만드는 것 대신,
더 빠른 조회를 지원하는 여러 단계의 아이디어 중 무엇을 할지 논의하며 리서치할 수 있습니다.