-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 이미지 도메인 생성 #14
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
Merged
feat: 이미지 도메인 생성 #14
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1ef392d
chore: 테스트 종속성 추가 (mockk, RestAssured)
sosow0212 a70b06f
feat: 통합테스트 격리 헬퍼 추가
sosow0212 5f01be4
chore: kotest 종속성 변경
sosow0212 bf4545e
refactor: 불필요한 Import 제거
sosow0212 4ddb53b
feat: 이미지용 스레드풀 설정
sosow0212 3682774
feat: 이미지 객체 생성
sosow0212 30d2be9
feat: 이미지 모듈 개발
sosow0212 7fb37ea
fix: conflict 해소
sosow0212 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/main/kotlin/com/study/core/global/config/AsyncConfig.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.study.core.global.config | ||
|
|
||
| import org.springframework.context.annotation.Bean | ||
| import org.springframework.context.annotation.Configuration | ||
| import org.springframework.scheduling.annotation.EnableAsync | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor | ||
| import java.util.concurrent.Executor | ||
|
|
||
| @Configuration | ||
| @EnableAsync | ||
| class AsyncConfig { | ||
|
|
||
| @Bean(name = ["imageStorageExecutor"]) | ||
| fun imageStorageExecutor(): Executor { | ||
| val executor = ThreadPoolTaskExecutor() | ||
| executor.corePoolSize = 2 | ||
| executor.maxPoolSize = 4 | ||
| executor.queueCapacity = 50 | ||
| executor.setThreadNamePrefix("image-storage-") | ||
| executor.initialize() | ||
| return executor | ||
| } | ||
| } |
32 changes: 32 additions & 0 deletions
32
src/main/kotlin/com/study/core/image/application/ImageEventListener.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.study.core.image.application | ||
|
|
||
| import com.study.core.image.application.event.ImageCreateEvent | ||
| import com.study.core.image.domain.domainservice.ImageStorage | ||
| import com.study.core.image.domain.Image | ||
| import com.study.core.image.domain.ImageRepository | ||
| import org.springframework.context.event.EventListener | ||
| import org.springframework.scheduling.annotation.Async | ||
| import org.springframework.stereotype.Component | ||
|
|
||
| @Component | ||
| class ImageEventListener( | ||
| private val imageRepository: ImageRepository, | ||
| private val imageStorage: ImageStorage | ||
| ) { | ||
|
|
||
| @Async("imageStorageExecutor") | ||
| @EventListener | ||
| fun handleImageCreate(event: ImageCreateEvent) { | ||
| require(event.content.isNotEmpty()) { "content must not be empty." } | ||
|
|
||
| val image = Image.createAndStore( | ||
| aggregateType = event.aggregateType, | ||
| aggregateId = event.aggregateId, | ||
| originalFilename = event.originalFilename, | ||
| content = event.content, | ||
| sortOrder = event.sortOrder, | ||
| imageStorage = imageStorage, | ||
| ) | ||
| imageRepository.save(image) | ||
| } | ||
| } |
34 changes: 34 additions & 0 deletions
34
src/main/kotlin/com/study/core/image/application/ImageEventPublisher.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.study.core.image.application | ||
|
|
||
| import com.study.core.image.application.event.ImageCreateEvent | ||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
| import org.springframework.context.ApplicationEventPublisher | ||
| import org.springframework.stereotype.Component | ||
|
|
||
| @Component | ||
| class ImageEventPublisher( | ||
| private val applicationEventPublisher: ApplicationEventPublisher | ||
| ) { | ||
|
|
||
| fun publishCreate(event: ImageCreateEvent) { | ||
| applicationEventPublisher.publishEvent(event) | ||
| } | ||
|
|
||
| fun publishCreate( | ||
| aggregateType: ImageAggregateType, | ||
| aggregateId: Long, | ||
| originalFilename: String, | ||
| content: ByteArray, | ||
| sortOrder: Int = 0 | ||
| ) { | ||
| publishCreate( | ||
| ImageCreateEvent( | ||
| aggregateType = aggregateType, | ||
| aggregateId = aggregateId, | ||
| originalFilename = originalFilename, | ||
| content = content, | ||
| sortOrder = sortOrder | ||
| ) | ||
| ) | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
src/main/kotlin/com/study/core/image/application/event/ImageCreateEvent.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.study.core.image.application.event | ||
|
|
||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
|
|
||
| data class ImageCreateEvent( | ||
| val aggregateType: ImageAggregateType, | ||
| val aggregateId: Long, | ||
| val originalFilename: String, | ||
| val content: ByteArray, | ||
| val sortOrder: Int = 0, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package com.study.core.image.domain | ||
|
|
||
| import com.study.core.image.domain.domainservice.ImageStorage | ||
| import com.study.core.global.BaseEntity | ||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
| import jakarta.persistence.Column | ||
| import jakarta.persistence.Entity | ||
| import jakarta.persistence.EnumType | ||
| import jakarta.persistence.Enumerated | ||
| import jakarta.persistence.GeneratedValue | ||
| import jakarta.persistence.GenerationType | ||
| import jakarta.persistence.Id | ||
| import jakarta.persistence.Table | ||
| import java.util.UUID | ||
|
|
||
| @Entity | ||
| @Table(name = "images") | ||
| class Image( | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| val id: Long = 0L, | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "aggregate_type", nullable = false, length = 30) | ||
| val aggregateType: ImageAggregateType, | ||
|
|
||
| @Column(name = "aggregate_id", nullable = false) | ||
| val aggregateId: Long, | ||
|
|
||
| @Column(name = "original_filename", nullable = false, length = 255) | ||
| val originalFilename: String, | ||
|
|
||
| @Column(name = "unique_filename", nullable = false, length = 255) | ||
| val uniqueFilename: String, | ||
|
|
||
| @Column(name = "extension", nullable = false, length = 20) | ||
| val extension: String, | ||
|
|
||
| @Column(name = "file_size", nullable = false) | ||
| val fileSize: Long, | ||
|
|
||
| @Column(name = "url", nullable = false, length = 500) | ||
| val url: String, | ||
|
|
||
| @Column(name = "sort_order", nullable = false) | ||
| var sortOrder: Int = 0, | ||
| ) : BaseEntity() { | ||
|
|
||
| fun changeSortOrder(targetSortOrder: Int): Image { | ||
| require(targetSortOrder >= 0) { "sortOrder must be zero or positive." } | ||
| sortOrder = targetSortOrder | ||
| return this | ||
| } | ||
|
|
||
| companion object { | ||
| fun createAndStore( | ||
| aggregateType: ImageAggregateType, | ||
| aggregateId: Long, | ||
| originalFilename: String, | ||
| content: ByteArray, | ||
| sortOrder: Int = 0, | ||
| imageStorage: ImageStorage, | ||
| ): Image { | ||
| require(aggregateId > 0) { "aggregateId must be positive." } | ||
| require(content.isNotEmpty()) { "content must not be empty." } | ||
| require(sortOrder >= 0) { "sortOrder must be zero or positive." } | ||
|
|
||
| val normalizedOriginal = originalFilename.trim() | ||
| require(normalizedOriginal.isNotEmpty()) { "originalFilename must not be blank." } | ||
|
|
||
| val generatedFilename = generateUniqueFilename(normalizedOriginal) | ||
| val storedImage = imageStorage.store(generatedFilename.uniqueFilename, content) | ||
|
|
||
| require(storedImage.url.isNotBlank()) { "url must not be blank." } | ||
| require(storedImage.fileSize > 0) { "fileSize must be greater than zero." } | ||
|
|
||
| return Image( | ||
| aggregateType = aggregateType, | ||
| aggregateId = aggregateId, | ||
| originalFilename = normalizedOriginal, | ||
| uniqueFilename = generatedFilename.uniqueFilename, | ||
| extension = generatedFilename.extension, | ||
| fileSize = storedImage.fileSize, | ||
| url = storedImage.url, | ||
| sortOrder = sortOrder, | ||
| ) | ||
| } | ||
|
|
||
| fun generateUniqueFilename(originalFilename: String): GeneratedFilename { | ||
| val normalizedOriginal = originalFilename.trim() | ||
| require(normalizedOriginal.isNotEmpty()) { "originalFilename must not be blank." } | ||
| val extension = extractExtension(normalizedOriginal) | ||
| return GeneratedFilename( | ||
| uniqueFilename = "${UUID.randomUUID()}.$extension", | ||
| extension = extension | ||
| ) | ||
| } | ||
|
|
||
| private fun extractExtension(filename: String): String { | ||
| val extension = filename.substringAfterLast('.', "").lowercase() | ||
| require(extension.isNotEmpty()) { "filename must contain an extension." } | ||
| return extension | ||
| } | ||
| } | ||
| } | ||
|
|
||
| data class GeneratedFilename( | ||
| val uniqueFilename: String, | ||
| val extension: String | ||
| ) | ||
14 changes: 14 additions & 0 deletions
14
src/main/kotlin/com/study/core/image/domain/ImageRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.study.core.image.domain | ||
|
|
||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
|
|
||
| interface ImageRepository { | ||
|
|
||
| fun save(image: Image): Image | ||
|
|
||
| fun saveAll(images: Collection<Image>): List<Image> | ||
|
|
||
| fun findByAggregate(aggregateType: ImageAggregateType, aggregateId: Long): List<Image> | ||
|
|
||
| fun deleteAllByAggregate(aggregateType: ImageAggregateType, aggregateId: Long) | ||
| } |
10 changes: 10 additions & 0 deletions
10
src/main/kotlin/com/study/core/image/domain/domainservice/ImageStorage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.study.core.image.domain.domainservice | ||
|
|
||
| interface ImageStorage { | ||
| fun store(uniqueFilename: String, content: ByteArray): StoredImage | ||
| } | ||
|
|
||
| data class StoredImage( | ||
| val url: String, | ||
| val fileSize: Long | ||
| ) |
7 changes: 7 additions & 0 deletions
7
src/main/kotlin/com/study/core/image/domain/vo/ImageAggregateType.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.study.core.image.domain.vo | ||
|
|
||
| enum class ImageAggregateType { | ||
| PACKAGE, | ||
| PRODUCT, | ||
| USER, | ||
| } |
15 changes: 15 additions & 0 deletions
15
src/main/kotlin/com/study/core/image/infrastructure/persistence/ImageJpaRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.study.core.image.infrastructure.persistence | ||
|
|
||
| import com.study.core.image.domain.Image | ||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
| import org.springframework.data.jpa.repository.JpaRepository | ||
|
|
||
| interface ImageJpaRepository : JpaRepository<Image, Long> { | ||
|
|
||
| fun findByAggregateTypeAndAggregateIdOrderBySortOrderAsc( | ||
| aggregateType: ImageAggregateType, | ||
| aggregateId: Long | ||
| ): List<Image> | ||
|
|
||
| fun deleteByAggregateTypeAndAggregateId(aggregateType: ImageAggregateType, aggregateId: Long) | ||
| } |
25 changes: 25 additions & 0 deletions
25
src/main/kotlin/com/study/core/image/infrastructure/persistence/ImageRepositoryImpl.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.study.core.image.infrastructure.persistence | ||
|
|
||
| import com.study.core.image.domain.Image | ||
| import com.study.core.image.domain.vo.ImageAggregateType | ||
| import com.study.core.image.domain.ImageRepository | ||
| import org.springframework.stereotype.Repository | ||
|
|
||
| @Repository | ||
| class ImageRepositoryImpl( | ||
| private val imageJpaRepository: ImageJpaRepository | ||
| ) : ImageRepository { | ||
|
|
||
| override fun save(image: Image): Image = | ||
| imageJpaRepository.save(image) | ||
|
|
||
| override fun saveAll(images: Collection<Image>): List<Image> = | ||
| imageJpaRepository.saveAll(images) | ||
|
|
||
| override fun findByAggregate(aggregateType: ImageAggregateType, aggregateId: Long): List<Image> = | ||
| imageJpaRepository.findByAggregateTypeAndAggregateIdOrderBySortOrderAsc(aggregateType, aggregateId) | ||
|
|
||
| override fun deleteAllByAggregate(aggregateType: ImageAggregateType, aggregateId: Long) { | ||
| imageJpaRepository.deleteByAggregateTypeAndAggregateId(aggregateType, aggregateId) | ||
| } | ||
| } |
45 changes: 45 additions & 0 deletions
45
src/main/kotlin/com/study/core/image/infrastructure/storage/LocalImageStorage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.study.core.image.infrastructure.storage | ||
|
|
||
| import com.study.core.image.domain.domainservice.ImageStorage | ||
| import com.study.core.image.domain.domainservice.StoredImage | ||
| import org.springframework.beans.factory.annotation.Value | ||
| import org.springframework.context.annotation.Primary | ||
| import org.springframework.context.annotation.Profile | ||
| import org.springframework.stereotype.Component | ||
| import java.nio.file.Files | ||
| import java.nio.file.Path | ||
| import java.nio.file.Paths | ||
| import java.nio.file.StandardOpenOption | ||
|
|
||
| @Primary | ||
| @Profile("local", "test") | ||
| @Component | ||
| class LocalImageStorage( | ||
| @Value("\${app.storage.local.base-path:}") basePath: String? = null | ||
| ) : ImageStorage { | ||
|
|
||
| // 자신 로컬 환경에 맞게 이미지 저장 경로 설정 | ||
| private val rootPath: Path = basePath | ||
| ?.takeIf { it.isNotBlank() } | ||
| ?.let { Paths.get(it) } | ||
| ?: Paths.get(System.getProperty("user.home"), "Desktop", "image") | ||
|
|
||
| override fun store(uniqueFilename: String, content: ByteArray): StoredImage { | ||
| require(uniqueFilename.isNotBlank()) { "uniqueFilename must not be blank." } | ||
| require(content.isNotEmpty()) { "content must not be empty." } | ||
|
|
||
| Files.createDirectories(rootPath) | ||
| val targetPath = rootPath.resolve(uniqueFilename) | ||
| Files.write( | ||
| targetPath, | ||
| content, | ||
| StandardOpenOption.CREATE, | ||
| StandardOpenOption.TRUNCATE_EXISTING, | ||
| StandardOpenOption.WRITE | ||
| ) | ||
| return StoredImage( | ||
| url = targetPath.toUri().toString(), | ||
| fileSize = content.size.toLong() | ||
| ) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
이미지를 식별하기 위한 파일명인가요?? Id, Uniquefilename 둘 다 필요한 건지 궁금합니다!
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.
ex. originalFileName이 이미지.jpg인데, 중복된 파일명이 또 저장되는 경우를 예방하기 위해서 uuid기반의 uniqueName을 추가해습니다! db에서 식별은 id만 있어도 되지만 물리 환경에선 클라우드냐 로컬이냐에 따라서 uniqueName도 필요할 수 있어서요
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.
스토리지 식별자로 사용되는 부분 이해했어요 감사합니다! 그럼 혹시 url은 가변적이어서 식별자로 사용하지 않는 걸까요?
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.
넵 스토리지가 구글클라우드, aws, 클라우드플레어 등등 변경될 수 있고 가변적이라 생각해서 식별자로 사용하지 않았습니다~