์ ๋ชฉ, ๋ด์ฉ๊ณผ ํจ๊ป ์ด๋ฏธ์ง ํ๋๋ฅผ ์
๋ ฅ๋ฐ์ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ ์ ํ๋ค.
UI ์์ด Junit์ ์ด์ฉํ ํ
์คํธ๋ง ์งํํ๋ค.
ํ์ผ ์
๋ก๋์ ์ง์คํ๊ธฐ ์ํด ๊ท์ฐฎ์ ์ผ์ด ๋ฐ์ํ ์ ์๋ ์๋ฐฉํฅ ๊ด๊ณ๋ ๋์ง ์์๋ค.
BoardImage
์์ Board
๋ฅผ ๋ณด๋ ๋จ๋ฐฉํฅ ๊ด๊ณ๋ง ์ค์ ํ๋ค.
@Entity
class Board(
title: String,
content: String,
member: Member
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
@Column(nullable = false)
var title: String = title
protected set
@Column(nullable = false)
var content: String = content
protected set
@ManyToOne(fetch = FetchType.LAZY)
var member: Member = member
protected set
fun addMember(member: Member){
this.member = member
member.boards.add(this)
}
fun updateBoard(updateDto: BoardUpdateDto) {
this.title = updateDto.title
this.content = updateDto.content
}
fun modifyMember(member: Member) {
this.member = member
}
}
saveFileName
์ ์๋ฒ ์ธก์์ ๊ด๋ฆฌํ๋ ํ์ผ์ ์ด๋ฆ์ด๋ค.
UUID
๋ฅผ ์ด์ฉํด์ ๋์ํ ๋ ์ด๋ฆ์ด ์ค์ ๋๋๋ก ํ ๊ฒ์ด๋ค. ์
๋ก๋ ๋๋ ํ์ผ์ ์ด๋ฆ์ด ์ค๋ณต๋๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํจ์ด๋ค.
originalFileName
์ ์ฌ์ฉ์๊ฐ ์
๋ก๋ ํ ํ์ผ์ด๋ฆ์ ์ ์ฅํ๋ค.
@Entity
class BoardImage(
originalFileName: String,
saveFileName: String,
board: Board) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
@Column(nullable = false)
val saveFileName: String = saveFileName
@Column(nullable = false)
val originalFileName: String = originalFileName
@ManyToOne(fetch = FetchType.LAZY)
val board: Board = board
}
์ด๋ฏธ์ง๋ ์ ํ์ ์ผ๋ก ๋์ด์ค๋๋ก (required = false) ํ๋ค.
(์ด์ ํ๋ก์ ํธ์ ์ฐ๊ฒฐ๋์ด Security๊ฐ ์ ์ฉ๋ ์ํ์ด๋ค.)
ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ Service
๊ณ์ธต์ผ๋ก ๋๊ฒจ์ ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๋ค.
MultipartFile
์ ์ด์ฉํด์ ๋์ด์ค๋ ๋ฐ์ด๋๋ฆฌ ํ์ผ์ ๋ฐ์์ค๋ค.
Multipart ์ฐธ๊ณ
@RequestMapping("/api/boards")
@RestController
class BoardController (private val boardService: BoardService) {
@PostMapping
fun postBoard(
@AuthenticationPrincipal userDetails: UserDetailsImpl,
@RequestParam title: String,
@RequestParam content: String,
@RequestPart(required = false) file: MultipartFile?): ResponseEntity<String> {
val member = userDetails.member
boardService.saveBoard(member, title, content, file)
return ResponseEntity.ok().body("์ ์ฅ์๋ฃ")
}
}
MultipartFile
์ originalFilename
๋ฉ์๋๋ก ์
๋ก๋ ๋ ํ์ผ์ ์ด๋ฆ์ ์ป์ด์ค๊ณ ์ด ์ด๋ฆ์ ์ด์ฉํด์ ์๋ฒ์์ ๊ด๋ฆฌํ ํ์ผ์ด๋ฆ๊น์ง ๋ง๋ค์ด์ค๋ค.
์๋ณธ์ด๋ฆ๊ณผ ์๋ฒ์์ ๊ด๋ฆฌํ ์ด๋ฆ์ด ์์ฑ๋๋ค๋ฉด transferTo
๋ฉ์๋๋ฅผ ์ด์ฉํด์ ํ์ผ์ ์ง์ ํ ๊ฒฝ๋ก์ ์ ์ฅํ๊ณ BoardImage
์ํฐํฐ๋ฅผ ์์ฑํด์ DB์ ์ ์ฅํ๋ค.
์ค์ ํ์ผ์ ์ ์ฅํ๋ ๋ถ๋ถ์ ์ดํ S3์ ์
๋ก๋ํ๋ ๊ฒ์ผ๋ก ๋ณ๊ฒฝํ ์์ ์ด๋ค.
@Service
class BoardService(
private val boardRepository: BoardRepository,
private val boardImageRepository: BoardImageRepository,
) {
val uploadeDir = "์
๋ก๋_๋ _๊ฒฝ๋ก"
@Transactional
fun saveBoard(member: Member, title: String, content: String, file: MultipartFile?) {
val board = Board(title, content, member)
boardRepository.save(board)
// ์๋ณธํ์ผ์ด๋ฆ, ์ ์ฅ๋ ํ์ผ์ด๋ฆ
val originalFilename: String? = file?.originalFilename
val saveFileName = getSaveFileName(originalFilename)
// ํ์ผ ์ ์ฅ(transferTo), ํ์ผ์ ๋ณด DB์ ์ ์ฅ
if (file != null && originalFilename != null) {
file.transferTo(File(uploadeDir + saveFileName))
val boardImage = BoardImage(originalFilename, saveFileName, board)
boardImageRepository.save(boardImage)
}
}
// ์ ์ฅ๋ ํ์ผ์ด๋ฆ ์์ฑ
private fun getSaveFileName(originalFilename: String?): String {
val extPosIndex: Int? = originalFilename?.lastIndexOf(".")
val ext = originalFilename?.substring(extPosIndex?.plus(1) as Int)
return UUID.randomUUID().toString() + "." + ext
}
}
ํ
์คํธ๋ MockMultipartFile
๋ฅผ ์ด์ฉํด์ ๊ฐ๋จํ๊ฒ ์งํํ๋ค.
MockMultipartFile
์ ํ์ผ์ API๋ก ๋๊ธธ ๋ ๋งคํ๋ ์ด๋ฆ๊ณผ ํ์ผ์ด๋ฆ, contentType, ์ค์ ๋ฐ์ดํฐ๊น์ง ๋ง๋ค์ด์ ํ
์คํธํ ์ ์๋ ์ข~~์ ๋๊ตฌ์ด๋ค.
ํ ์คํธ๋ฅผ ์ํด์ ํน์ ๊ฒฝ๋ก์ ํ์ผ์ ์์น์ํค๋ ๋ฑ์ ์์ ์ด ํ์ ์์ด์ ๋ณด๋ค ๋ ๋ฆฝ์ ์ธ ํ ์คํธ๊ฐ ๊ฐ๋ฅํ๋ค.
MockMultipartFile(๋งคํ๋ _์ด๋ฆ, ์๋ณธํ์ผ์ด๋ฆ, ContentType, ๋ฐ์ดํฐ(byte))
๋จผ์ MockMultipartFile
๋ฅผ ์ด์ฉํด์ text ํ์ผ ํ๋๋ฅผ ์์ฑํ๋ค.
ํ์ผ๋ถ๋ถ์ file
๋ก, String ๋ฐ์ดํฐ๋ part
๋ก ์ง์ ํด์ ์๋ฒ๋ก ๋๊ธด๋ค.
(headers ๋ถ๋ถ์ security๊ฐ ์ ์ฉ๋ ์ํ๋ผ ํ
์คํธ๋ฅผ ์ํด ์ค์ ํ๋ค.)
์๋ฒ๋ก ์์ฒญ์ด ๋๋๋ฉด DB ์ ์ฅ์ฌ๋ถ๊น์ง ํ์ธํ๋ ๊ฒ์ผ๋ก ํ ์คํธ๋ฅผ ๋ง์น๋ค.
@Test
@DisplayName("์
๋ก๋ ํ
์คํธ")
fun `Board ์ ์ฅ - ํ์ผ์
๋ก๋ ํฌํจ`() {
val imagePart = MockMultipartFile("file","test.txt" , "text/plain" , "hello file".byteInputStream(StandardCharsets.UTF_8))
mockMvc.multipart("/api/boards")
{
file(imagePart)
.part(MockPart("title", "title1".toByteArray(StandardCharsets.UTF_8)))
.part(MockPart("content", "content1".toByteArray(StandardCharsets.UTF_8)))
headers {
header("Authorization", "bearer ".plus(token))
}
}
.andDo {
print()
}
.andExpect {
status { isOk() }
}
val boardImages = boardImageRepository.findAll()
Assertions.assertEquals(1, boardImages.size)
Assertions.assertEquals("test.txt", boardImages.get(0).originalFileName)
}
์ง์ ํ ๊ฒฝ๋ก์ ํ์ผ์ด ์ ์ฅ๋ ๊ฒ ๊น์ง ํ์ธํ ์ ์๋ค.