-
Notifications
You must be signed in to change notification settings - Fork 0
Board Service 구현 및 단위 테스트 #19
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
Changes from all commits
bca5627
2419069
cba79ca
96756de
bf819d6
6b14a72
9675332
2c70e28
7e1f509
7e2f07a
665d31b
68ae6d0
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,14 @@ | ||
package me.nettee.board.application.model; | ||
|
||
import java.time.Instant; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
|
||
public record BoardReadDetailModel( | ||
Long id, | ||
String title, | ||
String content, | ||
BoardStatus status, | ||
Instant createdAt, | ||
Instant updatedAt, | ||
Instant deletedAt | ||
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. ❗
|
||
) {} |
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. ❗
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package me.nettee.board.application.model; | ||
|
||
import java.time.Instant; | ||
|
||
public record BoardReadSummaryModel( | ||
Long id, | ||
String title, | ||
String content, | ||
Instant createdAt, | ||
Instant updatedAt | ||
){} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,15 @@ | ||
package me.nettee.board.application.port; | ||
|
||
import java.util.Optional; | ||
import me.nettee.board.application.domain.Board; | ||
|
||
public interface BoardCommandPort { | ||
|
||
Optional<Board> findById(Long id); | ||
|
||
Board create(Board board); | ||
|
||
Board update(Board board); | ||
|
||
void delete(Long id); | ||
|
||
void delete(Board id); | ||
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. 💬 아이디 타입 대신 도메인 모델 객체를 넘기는 이유가 설명되면 좋겠습니다.기존 라이브 코드리뷰 코멘트를 참고하여 배경이 되는 다음 내용을 숙지하시고, 혹시나 이 외에 추가적인 이유로
관련 논의같은 이슈의 이 코멘트에 담겨 있습니다. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,17 @@ | ||
package me.nettee.board.application.port; | ||
|
||
import me.nettee.board.application.domain.Board; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
import me.nettee.board.application.model.BoardReadSummaryModel; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
|
||
import java.util.Optional; | ||
|
||
public interface BoardQueryPort { | ||
|
||
Page<Board> findAll(Pageable pageable); | ||
Optional<BoardReadDetailModel> findById(Long id); | ||
|
||
Optional<Board> findById(Long id); | ||
Page<BoardReadSummaryModel> findByStatusesList(Pageable pageable, Set<BoardStatus> statuses); | ||
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. 💊 일반적인 파라미터 입력 순서에 맞게 statuses, pageable 순서가 어떨까 합니다.Port는 반드시 Spring Data JPA의 스펙을 준수할 필요가 없지만, 일반적으로 사람들에게 익숙한 관례에 맞게 순서를 바꾸는 것이 좋다고 생각합니다. 그 외 메서드 선언 순서, read model 등의 적용은 잘되어 있어서 잘 신경써 주신 것 같습니다! 👍 |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package me.nettee.board.application.service; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import me.nettee.board.application.domain.Board; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
import me.nettee.board.application.port.BoardCommandPort; | ||
import me.nettee.board.application.port.BoardQueryPort; | ||
import me.nettee.board.application.usecase.BoardCreateUseCase; | ||
import me.nettee.board.application.usecase.BoardDeleteUseCase; | ||
import me.nettee.board.application.usecase.BoardUpdateUseCase; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class BoardCommandService implements BoardCreateUseCase, BoardUpdateUseCase, BoardDeleteUseCase { | ||
|
||
private final BoardCommandPort boardCommandPort; | ||
|
||
public Board createBoard(Board board) { | ||
return boardCommandPort.create(board); | ||
} | ||
|
||
public Board updateBoard(Board board) { | ||
return boardCommandPort.update(board); | ||
} | ||
|
||
public void deleteBoard(Long id) { | ||
Board board = boardCommandPort.findById(id).orElseThrow( | ||
() -> new IllegalArgumentException("게시글을 찾을 수 없습니다.")); | ||
|
||
board.softDelete(); | ||
|
||
boardCommandPort.delete(board); | ||
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. ❗ Soft Delete로 변경하는 것이 좋습니다.포트 및 어댑터의 '기획상' 또는 '서버의 정책상' 소프트 딜리트를 수행할지 여부는 애플리케이션 계층에서 결정하는 것이 좋다는 논의가 있었습니다. 라이브 코드리뷰 내용을 참고하세요! |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package me.nettee.board.application.service; | ||
|
||
import java.util.Set; | ||
import lombok.RequiredArgsConstructor; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
import me.nettee.board.application.model.BoardReadSummaryModel; | ||
import me.nettee.board.application.port.BoardQueryPort; | ||
import me.nettee.board.application.usecase.BoardReadByStatusesUseCase; | ||
import me.nettee.board.application.usecase.BoardReadUseCase; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class BoardQueryService implements BoardReadUseCase, BoardReadByStatusesUseCase { | ||
|
||
private final BoardQueryPort boardQueryPort; | ||
|
||
@Override | ||
public BoardReadDetailModel getBoard(Long id) { | ||
return boardQueryPort.findById(id).orElseThrow( | ||
() -> new IllegalArgumentException("게시글을 찾을 수 없습니다.")); | ||
} | ||
|
||
@Override | ||
public Page<BoardReadSummaryModel> findByStatuses(Set<BoardStatus> statuses, Pageable pageable) { | ||
return boardQueryPort.findByStatusesList(pageable, statuses); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,5 @@ | |
public interface BoardCreateUseCase { | ||
|
||
Board createBoard(Board board); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ | |
public interface BoardDeleteUseCase { | ||
|
||
void deleteBoard(Long id); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package me.nettee.board.application.usecase; | ||
|
||
import java.util.Set; | ||
import me.nettee.board.application.domain.type.BoardStatus; | ||
import me.nettee.board.application.model.BoardReadSummaryModel; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
|
||
public interface BoardReadByStatusesUseCase { | ||
|
||
Page<BoardReadSummaryModel> findByStatuses(Set<BoardStatus> statuses, Pageable pageable); | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,9 @@ | ||
package me.nettee.board.application.usecase; | ||
|
||
import me.nettee.board.application.domain.Board; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
import me.nettee.board.application.model.BoardReadDetailModel; | ||
|
||
public interface BoardReadUseCase { | ||
|
||
Board getBoard(Long id); | ||
BoardReadDetailModel getBoard(Long id); | ||
|
||
Page<Board> findGeneralBy(Pageable pageable); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ | |
public interface BoardUpdateUseCase { | ||
|
||
Board updateBoard(Board board); | ||
|
||
} |
merge-simpson marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package me.nettee.board.application.service | ||
|
||
import io.kotest.core.spec.style.FreeSpec | ||
import io.kotest.matchers.should | ||
import io.kotest.matchers.shouldBe | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
import me.nettee.board.application.domain.Board | ||
import me.nettee.board.application.port.BoardCommandPort | ||
|
||
class BoardCommandServiceTest : FreeSpec({ | ||
|
||
val boardCommandPort = mockk<BoardCommandPort>() | ||
val boardCommandService = BoardCommandService(boardCommandPort) | ||
|
||
"BoardCommandService" - { | ||
"create" { | ||
// given | ||
var board = Board() | ||
every { | ||
boardCommandPort.create(board) | ||
} returns board | ||
|
||
// when | ||
val result = boardCommandService.createBoard(board) | ||
|
||
// then | ||
result shouldBe board | ||
verify { boardCommandPort.create(board) } | ||
} | ||
|
||
"update" { | ||
// given | ||
val board = Board() | ||
every { | ||
boardCommandPort.update(board) | ||
} returns board | ||
|
||
// when | ||
val result = boardCommandService.updateBoard(board) | ||
|
||
// then | ||
result shouldBe board | ||
verify { boardCommandPort.update(board) } | ||
} | ||
|
||
"delete" { | ||
// given | ||
val boardId = 1L | ||
every { | ||
boardCommandPort.delete(boardId) | ||
} returns Unit | ||
|
||
// when | ||
boardCommandService.deleteBoard(boardId) | ||
|
||
// then | ||
verify { boardCommandPort.delete(boardId) } | ||
} | ||
} | ||
}) |
merge-simpson marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package me.nettee.board.application.service | ||
|
||
import io.kotest.core.spec.style.FreeSpec | ||
import io.kotest.matchers.shouldBe | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
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.PageImpl | ||
import org.springframework.data.domain.PageRequest | ||
import java.util.* | ||
|
||
class BoardQueryServiceTest : FreeSpec({ | ||
|
||
val boardQueryPort = mockk<BoardQueryPort>() // mocking | ||
val boardQueryService = BoardQueryService(boardQueryPort) // 주입 | ||
|
||
"BoardQueryService" - { | ||
"findById" - { | ||
"존재하는 ID로 조회한다." { | ||
// given | ||
val boardId = 1L | ||
val expectedBoard = Board.builder().id(boardId).title("test title").build() | ||
every { | ||
boardQueryPort.findById(boardId) | ||
} returns Optional.of(expectedBoard) | ||
|
||
// when | ||
val result = boardQueryService.getBoard(boardId) | ||
|
||
// then | ||
result shouldBe expectedBoard | ||
verify { boardQueryPort.findById(boardId) } | ||
} | ||
|
||
"존재하지 않는 ID로 예외를 발생시킨다." { | ||
// given | ||
val boardId = 1L | ||
every { | ||
boardQueryPort.findById(boardId) | ||
} returns Optional.empty() | ||
|
||
// when | ||
val result = runCatching { | ||
boardQueryService.getBoard(boardId) // Optional이 비어있을 때, orElseThrow()에서 예외가 발생한다. | ||
} | ||
|
||
// then | ||
result.isFailure shouldBe true | ||
verify { boardQueryPort.findById(boardId) } | ||
} | ||
} | ||
|
||
"findByStatuses" - { | ||
"상태 목록으로 조회 시, 페이징 조회한다." { | ||
// given | ||
val statuses = listOf(BoardStatus.ACTIVE, BoardStatus.SUSPENDED) | ||
val pageable = PageRequest.of(0, 10) | ||
val boards = listOf( | ||
Board.builder().id(1L).title("Active Board").status(BoardStatus.ACTIVE).build(), | ||
Board.builder().id(2L).title("Suspended Board").status(BoardStatus.SUSPENDED).build() | ||
) | ||
val expectedPage = PageImpl(boards, pageable, boards.size.toLong()) | ||
every { | ||
boardQueryPort.findByStatusesList(pageable, statuses) | ||
} returns expectedPage | ||
|
||
// when | ||
val result = boardQueryService.findByStatuses(pageable, statuses) | ||
|
||
// then | ||
result shouldBe expectedPage | ||
verify { boardQueryPort.findByStatusesList(pageable, statuses) } | ||
} | ||
|
||
"빈 상태 목록으로 조회 시, 빈 페이지를 조회한다." { | ||
// given | ||
val statuses = emptyList<BoardStatus>() | ||
val pageable = PageRequest.of(0, 10) | ||
val boards: List<Board> = emptyList() | ||
val expectedPage = PageImpl(boards, pageable, 0) | ||
every { | ||
boardQueryPort.findByStatusesList(pageable, statuses) | ||
} returns expectedPage | ||
|
||
// when | ||
val result = boardQueryService.findByStatuses(pageable, statuses) | ||
|
||
// then | ||
result shouldBe expectedPage | ||
verify { boardQueryPort.findByStatusesList(pageable, statuses) } | ||
} | ||
} | ||
} | ||
}) |
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.
💊 클래스 이름을
BoardDetailReadModel
또는BoardDetail
로 제안드립니다.클래스 이름에서 변하는 부분과 반복되는 부분의 배치를 다음 패턴으로 권하고 있습니다.
지금 클래스 이름은 {바뀌는 부분} + {바뀌지 않는 부분} + {바뀌는 부분} + {바뀌지 않는 부분}으로, 클래스 이름을 인식할 때 다소간 혼란이 될 수도 있다고 생각합니다.
또는
Summary
,Detail
등 자주 사용되는 조회 모델에서-ReadModel
접두사 제거를 고려할 수 있습니다.Summary
와Detail
은 자주 사용되는 관례로 인식하여 이름에서 조회 모델임을 알아볼 수도 있다고 생각합니다.따라서 Application 레이어에서는 매번 이름에
-ReadModel
접두사를 붙이지 않는 것을 고려할 수 있습니다.단, 다른 도메인 모델이
-Summary
,-Detail
로 명명되는 케이스와 네이밍에서 중복될 여지를 고려해야 합니다.