๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿค” ๊ณ ๋ฏผ์ 

[Spring] Repository์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ๋Š” ์–ด๋–ค ๊ฒƒ์„ ์จ์•ผ ํ• ๊นŒ? (Entity vs void)

by dev_writer 2024. 2. 6.

๊ฐœ์š”

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋ฅผ ์“ฐ๋‹ค ๋ณด๋ฉด, ๊ฑฐ์˜ ๋ฌด์กฐ๊ฑด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•˜๋Š” save ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์‹  ์ ์ด ์žˆ์œผ์‹ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.
 
๊ทธ๋Ÿฐ๋ฐ save์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ๋Š” ์–ด๋–ค ๊ฒƒ์„ ์จ์•ผ ํ• ์ง€ ๊ณ ๋ฏผ์ด ๋“ค์—ˆ๋˜ ์ ์ด ์žˆ์œผ์‹ ๊ฐ€์š”? ์˜ค๋Š˜์€ ์–ด์ฉŒ๋ฉด ๋ฌด์˜์‹์ ์œผ๋กœ ๋„˜๊ฒจ๋ฒ„๋ฆฌ๊ธฐ๋งŒ ํ–ˆ๋˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์— ๋Œ€ํ•ด ๊ธ€์„ ์จ ๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.
 

save ๋ฉ”์„œ๋“œ ์†Œ๊ฐœ

์šฐ์„ , ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA์— ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š” save ๋ฉ”์„œ๋“œ๋Š” ์•„๋ž˜ ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    ...
}

์ฒซ ๋ฒˆ์งธ๋กœ, JpaRepository<T, ID> (์ธํ„ฐํŽ˜์ด์Šค)๋Š” ๋”ฐ๋ผ๊ฐ€๋‹ค ๋ณด๋ฉด CrudRepository<T, ID> (์ธํ„ฐํŽ˜์ด์Šค)๋ฅผ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค.
 
๊ทธ๋ฆฌ๊ณ  ์ด JpaRepository<T, ID> ์ธํ„ฐํŽ˜์ด์Šค๋Š” JpaRepositoryImplementation<T, ID> ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ƒ์†๋ฐ›์œผ๋ฉฐ, ์ด๊ฒƒ์˜ ๊ตฌํ˜„์ฒด๋กœ SimpleJpaRepository<T, ID>๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    ...
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }
    ...
 }

๊ตฌํ˜„์ฒด์™€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ณด๋ฉด, ๋ฉ”์„œ๋“œ์˜ ์ธ์ž๋กœ ๋„˜๊ธด S ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 

Repository์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ ๊ฐ€๋Šฅํ•œ ๊ฒƒ

์ด ๊ธ€์—์„œ ์‚ฌ์šฉํ•  Repository๋Š” ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์•„๋ž˜์ฒ˜๋Ÿผ ๋„๋ฉ”์ธ์—์„œ ์‚ฌ์šฉํ•  Repository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ  ๊ตฌํ˜„์ฒด๋ฅผ ์ •์˜ํ•  ๋•Œ ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋ฅผ ํ•„๋“œ๋กœ ๊ฐ€์ง„ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

๊ฒŒ์‹œ๊ธ€ (Board)์— ๋Œ€ํ•œ C/R ์ฝ”๋“œ๋ฅผ ์˜ˆ๋กœ ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค!

 
BoardRepository ์ฝ”๋“œ

public interface BoardRepository {

    [Board | void] save(final Board board);
    Optional<Board> findById(final Long id);
    List<Board> findAll();
}

 
์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA ์ฝ”๋“œ

public interface BoardJpaRepository extends JpaRepository<Board, Long> {
    ...
}

 
BoardJpaRepositoryImpl ์ฝ”๋“œ (๊ตฌํ˜„์ฒด)

@RequiredArgsConstructor
@Repository
public class JpaBoardRepositoryImpl implements BoardRepository {

    private final BoardJpaRepository boardJpaRepository;

    @Override
    public [Board | void] save(final Board board) {
        ...
    }
    
    @Override
    public Optional<Board> findById(final Long id) {
        return boardJpaRepository.findById(id);
    }
    
    @Override
    public List<Board> findAll() {
        return boardJpaRepository.findAll();
    }
}

 
์œ„์˜ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด, ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ ๊ฐ€๋Šฅํ•œ ๊ฒƒ์€ ํฌ๊ฒŒ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š๋ƒ, void๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š๋ƒ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 
๊ฐ ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์„ ๋•Œ ์–ด๋– ํ•œ ํŠน์ง•์ด ์žˆ๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!
 

1. ๋ฉ”์„œ๋“œ์˜ ์ธ์ž๋กœ ๋ฐ›์€ ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… ์ž์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ• (์˜์†ํ™”๋œ ์—”ํ‹ฐํ‹ฐ ๋ฐ˜ํ™˜)

์˜๋„

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ๊ทธ๋Œ€๋กœ ๊ณ„์Šนํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.

์žฅ์ 

  • ์˜์†ํ™”๋œ Entity๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ์— Service์—์„œ๋„ Entity, Long (= id) ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ id๊ฐ€ null์ด ์•„๋‹™๋‹ˆ๋‹ค.
  • Service์—์„œ ๋ฐ˜ํ™˜ํ•  ํƒ€์ž…์— ๋Œ€ํ•œ ์ œ์•ฝ์ด ์—†์Šต๋‹ˆ๋‹ค. (Service์—์„œ Controller์—๊ฒŒ ์–ด๋–ค ๊ฐ’์„ ์ œ๊ณตํ•˜๋“ ์ง€ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.)
  • ํ…Œ์ŠคํŠธ ์‹œ id ๊ฐ’์„ ๊บผ๋‚ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ 

  • void๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ ๋ณด๋‹ค ์ฑ…์ž„ (์˜์†ํ™”)์—๋งŒ ์ง‘์ค‘ํ•œ๋‹ค๋Š” ์˜๋„๊ฐ€ ํ๋ ค์ง‘๋‹ˆ๋‹ค.

์˜ˆ์‹œ - ๊ตฌํ˜„ ์ฝ”๋“œ

๋„๋ฉ”์ธ ์ฝ”๋“œ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. (์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ - ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ void ํƒ€์ž…์ผ ๋•Œ์—๋„ ๋™์ผ)

// import ํ‘œํ˜„์€ ์ƒ๋žต

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    public Board(final String title, final String content) {
        this.title = title;
        this.content = content;
    }
}

 
์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

// import ํ‘œํ˜„์€ ์ƒ๋žต

@RequiredArgsConstructor
@RequestMapping("/boards")
@RestController
public class BoardController {

    private final BoardService boardService;

    @PostMapping
    public ResponseEntity<BoardWriteResponse> write(@RequestBody @Valid final BoardWriteRequest request) {
        Board writeBoard = boardService.write(request);
        BoardWriteResponse response = BoardWriteResponse.from(writeBoard);
        return ResponseEntity.ok(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<BoardFindResponse> findBoardById(@PathVariable final Long id) {
        Board findBoard = boardService.findById(id);
        BoardFindResponse response = BoardFindResponse.from(findBoard);
        return ResponseEntity.ok(response);
    }
    
    @GetMapping
    public ResponseEntity<List<BoardFindResponse>> findAllBoards() {
        List<BoardFindResponse> responses = boardService.findAllBoards()
                .stream()
                .map(BoardFindResponse::from)
                .toList();
        return ResponseEntity.ok(responses);
    }
}

 
์„œ๋น„์Šค ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค์—์„œ๋Š” ๊ธ€ ์ €์žฅ ์‹œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ€์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์„œ๋น„์Šค์—์„œ ์˜์†ํ™”๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์˜์†ํ™”๋œ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ ์ œ์•ฝ์ด ์—†์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋งŒ์•ฝ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ void ํƒ€์ž…์ด๋ผ๋ฉด ์„œ๋น„์Šค์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ ์ž ํ•  ๋•Œ์—๋Š” id๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด ์ปจํŠธ๋กค๋Ÿฌ์— ์ „๋‹ฌ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

// import ํ‘œํ˜„์€ ์ƒ๋žต

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class BoardService {

    private final BoardRepository boardRepository;

    @Transactional
    public Board write(final BoardWriteRequest request) {
        Board newBoard = new Board(request.title(), request.content());
        return boardRepository.save(newBoard); // ์„œ๋น„์Šค์—์„œ ์˜์†ํ™”๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์˜์†ํ™”๋œ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ ์ œ์•ฝ์ด ์—†์Šต๋‹ˆ๋‹ค!
    }

    public Board findById(final Long id) {
        return boardRepository.findById(id)
                .orElseThrow(BoardNotFoundException::new);
    }
    
    public List<Board> findAllBoards() {
        return boardRepository.findAll();
    }
}

 

์˜ˆ์‹œ - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

์„œ๋น„์Šค ์ฝ”๋“œ์—์„œ ์ œ์•ฝ์ด ์—†์Œ์„ ์•Œ์•„๋ดค์œผ๋‹ˆ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ• ์ง€ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์„ ์ž‘์„ฑํ•œ ๊ฒŒ ์žˆ๋Š”๋ฐ, ์ด๊ฒƒ์— ๋Œ€ํ•ด์„œ๋Š” ์ถ”ํ›„ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!)
 
์•„๋ž˜๋Š” Repository์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

// import ํ‘œํ˜„์€ ์ƒ๋žต

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@DataJpaTest
@CleanDatabase // ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ์–ด๋…ธํ…Œ์ด์…˜
public class JpaBoardRepositoryTest {

    @Autowired
    private BoardJpaRepository boardJpaRepository;

    @Test
    void ๊ฒŒ์‹œ๊ธ€์„_์ €์žฅํ•œ๋‹ค() {
        // given
        Board writeBoard = BoardFixtures.๊ฒŒ์‹œ๊ธ€_id_์—†์Œ(); // ์„œ๋น„์Šค์—์„œ ์ƒ์„ฑํ•œ Board๋Š” id๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
        Long expectedId = 1L;

        // when
        Board saveBoard = boardJpaRepository.save(writeBoard);

        // then
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(saveBoard.getId()).isEqualTo(expectedId);
            softly.assertThat(saveBoard).usingRecursiveComparison()
                    .ignoringFields("id")
                    .isEqualTo(writeBoard);
        });
    }
    ...
}

 
๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ ๋ฐ˜ํ™˜ํ•œ Board๋Š” ์˜์†ํ™”๋œ ๊ฒŒ์‹œ๊ธ€์ด๊ธฐ ๋•Œ๋ฌธ์—, ๊ธฐ์กด writeBoard์—์„œ id๊ฐ€ ์ž‘์„ฑ๋˜์–ด ์žˆ์ง€ ์•Š๋”๋ผ๋„ saveBoard์˜ id ๊ฐ’์ด expectedId์™€ ๊ฐ™์Œ์„ ๊ฒ€์ฆํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

JPA ํ™˜๊ฒฝ์—์„œ์˜ ํ…Œ์ŠคํŠธ์ด๊ธฐ์—, Repository์—์„œ void๋กœ ๋ฐ˜ํ™˜ํ•˜๋”๋ผ๋„ ์œ„ ํ…Œ์ŠคํŠธ๋Š” ํ†ต๊ณผํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. EntityManager๊ฐ€ persist ํ•จ์— ๋”ฐ๋ผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์ €์žฅ๋˜์–ด id๋ฅผ ํ• ๋‹น๋ฐ›์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 
writeBoard์™€๋Š” id๊ฐ€ null/1L๋กœ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— id๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ์†์„ฑ์— ๋Œ€ํ•ด์„œ ๊ฐ™์€์ง€ ๋น„๊ตํ•จ์œผ๋กœ์จ ๊ฒ€์ฆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
 
Service์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// import ํ‘œํ˜„์€ ์ƒ๋žต

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
public class BoardServiceTest {

    private BoardService boardService;
    private BoardRepository boardRepository;

    @BeforeEach
    void init() {
        boardRepository = new BoardFakeRepository(); // HashMap<Long, Board>๋ฅผ ๊ฐ€์ง„ ํ…Œ์ŠคํŠธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ
        boardService = new BoardService(boardRepository);
    }

    @Test
    void ๊ฒŒ์‹œ๊ธ€์„_์ €์žฅํ•œ๋‹ค() {
        // given
        String title = "default title";
        String content = "default content";
        BoardWriteRequest request = new BoardWriteRequest(title, content);
        Long expectedId = 1L;

        // when
        Board writeBoard = boardService.write(request);

        // then
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(writeBoard.getId()).isEqualTo(expectedId);
            softly.assertThat(writeBoard.getTitle()).isEqualTo(title);
            softly.assertThat(writeBoard.getContent()).isEqualTo(content);
        });
    }
    ...
}

 
๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ id์— ๋Œ€ํ•œ ์‹ค์ œ ๊ฐ’์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 

2. void๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•

์˜๋„

Repository๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์˜์†ํ™”ํ•˜๋Š” ๊ฒƒ์—๋งŒ ์ง‘์ค‘์‹œํ‚ค๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.

์žฅ์ 

Repository๊ฐ€ ๊ฐ€์ง„ ์ฑ…์ž„ (์˜์†ํ™”)์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • Service์—์„œ Entity, Long (= id) ํƒ€์ž…์„ ๋ฐ˜ํ™˜์‹œํ‚ฌ ๊ฒฝ์šฐ id๊ฐ€ null๋กœ ๋ฉ๋‹ˆ๋‹ค. (์„œ๋น„์Šค ํ…Œ์ŠคํŠธ ํ•œ์ •, ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” JPA๋ฅผ ํ™œ์šฉํ•  ๊ฒฝ์šฐ id๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)
  • Service์—์„œ void๋กœ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋Š” ์žˆ์œผ๋‚˜, ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด Controller์—์„œ ์ €์žฅ๋œ Entity์— ๋Œ€ํ•œ ์•„๋ฌด ์ •๋ณด๋„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. (์ด๋Š” Service์—์„œ void๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ์˜ ๋‹จ์ ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.)
  • ํ…Œ์ŠคํŠธ ์‹œ ๋ถˆํŽธํ•จ์ด ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. id๊ฐ’์„ ๊บผ๋‚ด๋ฉด null์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ findAll(), verify() ๋“ฑ์œผ๋กœ ๊ฒ€์ฆ์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ - ๊ตฌํ˜„ ์ฝ”๋“œ

๋„๋ฉ”์ธ, ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ๋Š” ๋ณ€ํ•จ์ด ์—†์Šต๋‹ˆ๋‹ค.
 
์„œ๋น„์Šค ์ฝ”๋“œ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค.

// import ํ‘œํ˜„์€ ์ƒ๋žต

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class BoardService {

    private final BoardRepository boardRepository;

    @Transactional
    public Board write(final BoardWriteRequest request) {
        Board newBoard = new Board(request.title(), request.content());
        boardRepository.save(newBoard);
        return newBoard; // JPA์—์„œ๋Š” save ์‹œ persist๋ฅผ ํ•œ ๋’ค์— id๊ฐ€ ์ €์žฅ๋˜๊ธฐ ๋•Œ๋ฌธ์— save ์ดํ›„ newBoard๋ฅผ ์ „๋‹ฌํ•˜๋ฉด id๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
    }
    ...
}

 

์˜ˆ์‹œ - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

์•ž์„  ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ์˜ ํ…Œ์ŠคํŠธ (JpaBoardRepositoryTest)์—์„œ ์ž‘์„ฑํ•˜์˜€๋“ฏ, Repository์—์„œ save์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์—”ํ‹ฐํ‹ฐ๋กœ ํ•˜๋“ ์ง€ void๋กœ ํ•˜๋“ ์ง€์— ์ƒ๊ด€์—†์ด Repository์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋Š” ๋ฌธ์ œ์—†์ด ํ†ต๊ณผํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” JPA๊ฐ€ ๊ฐ€์ง„ ํŠน์„ฑ (save ์‹œ persist/merge๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ์ดํ›„ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— id๋ฅผ ๊ฐ€์ง„ ์ฑ„๋กœ ๋ณด๊ด€) ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
 
๋ฌธ์ œ๋Š” ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” JPA๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”, HashMap์„ ์‚ฌ์šฉํ•˜๋Š” BoardFakeRepository๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ๋„์›€์„ ๋ฐ›์ง€ ๋ชปํ•˜๋ฉฐ, ์•„๋ž˜์˜ BoardFakeRepository๋ฅผ ๋ณด๋ฉด ์ธ์ž๋กœ ๋ฐ›์€ Board์˜ id์— ๋Œ€ํ•ด์„œ๋Š” id ๊ฐ’์ด ์ €์žฅ๋˜์ง€ ์•Š๋Š” ์ ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (๊ทธ๋ ‡๋‹ค๊ณ  id๋ฅผ set ํ•˜๊ธฐ ์œ„ํ•œ Setter ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด, ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด๋‹ˆ ์ด๋Š” ์ง€์–‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.)

// import ํ‘œํ˜„์€ ์ƒ๋žต
// ์„œ๋น„์Šค์˜ ๊ธฐ๋Šฅ ๊ฒ€์ฆ์—๋งŒ ์ง‘์ค‘ํ•˜๊ธฐ ์œ„ํ•ด JPA๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ž„์˜์˜ ํ…Œ์ŠคํŠธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

public class BoardFakeRepository implements BoardRepository {

    private final static HashMap<Long, Board> store = new HashMap<>();
    private Long id = 1L;

    @Override
    // ๋งŒ์•ฝ Board๋ฅผ ๋ฆฌํ„ดํ–ˆ๋‹ค๋ฉด id๊ฐ€ ์ €์žฅ๋œ newBoard๋ฅผ ๋ฆฌํ„ดํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    public void save(final Board board) {
        Board newBoard = Board.builder()
                .id(id)
                .title(board.getTitle())
                .content(board.getContent())
                .build();
        store.put(id, newBoard);
        id++;
    }
    ...
}

 
๊ทธ๋ž˜์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

id๊ฐ€ null์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด ์ฝ”๋“œ (์—”ํ‹ฐํ‹ฐ ๋ฐ˜ํ™˜ ๋ฐฉ์‹)๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

 
findAll๋กœ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•จ์œผ๋กœ์จ ์šฐํšŒ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Test
    void ๊ฒŒ์‹œ๊ธ€์„_์ €์žฅํ•œ๋‹ค() {
        // given
        String title = "default title";
        String content = "default content";
        BoardWriteRequest request = new BoardWriteRequest(title, content);
        // Long expectedId = 1L;
        int expectedSize = 1;

        // when
        Board writeBoard = boardService.write(request);

        // then
        assertSoftly(softly -> {
            // softly.assertThat(writeBoard.getId()).isEqualTo(expectedId);
            softly.assertThat(boardService.findAllBoards()).hasSize(expectedSize);
            softly.assertThat(writeBoard.getTitle()).isEqualTo(title);
            softly.assertThat(writeBoard.getContent()).isEqualTo(content);
        });
    }
    ...
}

์ถ”๊ฐ€: Service์—์„œ ์ €์žฅ ์‹œ ๋ฐ˜ํ™˜ ํƒ€์ž…

Service์—์„œ๋„ ๋ฐ˜ํ™˜ ํƒ€์ž…์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์ €์žฅ๋œ (์˜์†ํ™”๋œ) ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ• ๊นŒ์š”, ์•„๋‹ˆ๋ฉด id (Long) ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒŒ ์ข‹์„๊นŒ์š”? ์ด ๋˜ํ•œ ๊ฐ๊ธฐ ์žฅ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. (์–ด๋–ค ๋ถ„๋“ค์€ DTO๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•˜์‹œ๊ธฐ๋„ ํ•˜๋Š”๋ฐ, DTO๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์€ ๊ฒฐ๊ตญ ํ‘œํ˜„ ๊ณ„์ธต์— ๋Œ€ํ•ด ์˜์กด๋˜๋ฏ€๋กœ ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค๋Š” ์ƒ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์—”ํ‹ฐํ‹ฐ/Long ๋ฐ˜ํ™˜ ๋ฐฉ์‹์˜ ์ฐจ์ด์— ๋Œ€ํ•ด์„œ๋งŒ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)

์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ

์žฅ์ 

  • ํ‘œํ˜„ ์˜์—ญ์—์„œ id ๋ง๊ณ ๋„ ๋‹ค๋ฅธ ๊ฐ’ (์ œ๋ชฉ, ๊ธ€ ๋“ฑ)์„ ํ•„์š”๋กœ ํ•  ๊ฒฝ์šฐ ์—”ํ‹ฐํ‹ฐ์—์„œ ์ด๋Œ์–ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • ๋ช…๋ น๊ณผ ์กฐํšŒ์˜ ๋ถ„๋ฆฌ (์•„๋ž˜ ์›์น™)์˜ ์˜๋„์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

id (Long)๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ

์žฅ์ 

  • ๋ช…๋ น๊ณผ ์กฐํšŒ์˜ ๋ถ„๋ฆฌ (CQRS: Command and Query Responsibility Segregation) ์›์น™์„ ์ง€ํ‚ค๋Š” ๋ฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์กฐํšŒ๋ฅผ ์œ„ํ•œ ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • ์„œ๋น„์Šค์—์„œ id๋งŒ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋ฉด, ํ‘œํ˜„ ์˜์—ญ (์ปจํŠธ๋กค๋Ÿฌ) ๋˜ํ•œ id๋งŒ์„ ์ œ๊ณตํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

๋‘ ๋ฐฉ์‹ ๋ชจ๋‘ ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์ด๊ณ , ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ •ํ™•ํžˆ ์–ด๋–ค ๊ฒƒ์ด ์ •๋‹ต์ด๋‹ค!๋ผ๋Š” ๊ฒƒ์€ ๋‚ด๋ฆฌ๊ธฐ ํž˜๋“  ๋ฌธ์ œ ๊ฐ™์Šต๋‹ˆ๋‹ค. (์›๋ž˜ ๊ฐœ๋ฐœ์€ ๋ช…ํ™•ํžˆ ์ •ํ•ด์ง„ ์ •๋‹ต์ด ์ž˜ ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ๋„ ํ•˜์ฃ ..)
 
๊ฐœ์ธ์ ์œผ๋กœ๋Š” ์ฑ…์ž„์˜ ์˜์—ญ์„ ์กฐ๊ธˆ ํ๋ฆด ์ˆ˜๋Š” ์žˆ์–ด๋„ ํ…Œ์ŠคํŠธ์—๋„ ์ ํ•ฉํ•œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋” ๋งˆ์Œ์— ๋“œ๋Š”๋ฐ, ์ด๋Ÿฌํ•œ ๋‘ ๋ฐฉ์‹์˜ ์ฐจ์ด์ ์„ ๊ทผ๊ฑฐ๋กœ ํŒ€์› ๋ถ„๋“ค๊ณผ ๋…ผ์˜ํ•ด ๊ฐ€๋ฉฐ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ํ• ์ง€ ๊ฒฐ์ •ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
 
์„œ๋น„์Šค์—์„œ์˜ ๋ฐ˜ํ™˜ ๋ฐฉ์‹ ๋˜ํ•œ ๋…ผ์˜๊ฐ€ ํ•„์š”ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ €์žฅ๋œ ํ›„ API๋กœ ์‘๋‹ตํ•  ๋•Œ id๋งŒ ์žˆ์–ด๋„ ๊ดœ์ฐฎ์„์ง€, ์•„๋‹ˆ๋ฉด ๋ถ€๊ฐ€์ ์ธ ์†์„ฑ (๊ฒŒ์‹œ๊ธ€์˜ ๊ฒฝ์šฐ ์ œ๋ชฉ, ๊ธ€ ๋“ฑ)์ด ๋” ํ•„์š”ํ•œ์ง€์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

'๐Ÿค” ๊ณ ๋ฏผ์ ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

getter์™€ setter๋Š” ์–ด๋””์— ๋‘๋Š” ๊ฒŒ ์ข‹์„๊นŒ?  (1) 2023.11.15