분석 요청 API 동작 시
비동기로 처리를 할건데
처리를 위한 job 을 관리하는 DB를 만들것이다.
1. analysis_jobs 테이블 설계
- schema.sql
-- 분석 job 테이블
CREATE TABLE IF NOT EXISTS analysis_jobs (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
job_id VARCHAR(255) NOT NULL COMMENT '작업 ID(UUID)',
file_name VARCHAR(255) NOT NULL COMMENT '파일명',
file_size BIGINT NOT NULL COMMENT '파일 크기',
status VARCHAR(50) NOT NULL COMMENT '상태(예: PENDING, IN_PROGRESS, COMPLETED, FAILED)',
result TEXT NULL COMMENT '분석 결과 JSON',
description TEXT NULL COMMENT '설명 (에러메시지 등)',
is_deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '삭제 여부',
created_id BIGINT NOT NULL COMMENT '생성자 ID',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
updated_id BIGINT NULL COMMENT '수정자 ID',
updated_at TIMESTAMP NULL DEFAULT NULL COMMENT '수정 시간'
);
CREATE INDEX IF NOT EXISTS idx_analysis_jobs_job_id ON analysis_jobs(job_id);
CREATE INDEX IF NOT EXISTS idx_analysis_jobs_status ON analysis_jobs(status);
2. 패키지 구성
deed
├── adapter
│ ├── inbound.web
│ │ ├── dto
│ │ │ ├── DeedRequest
│ │ │ └── DeedResponse
│ │ └── DeedInboundWebAdapter
│ └── outbound.persistence
│ ├── jpa
│ │ ├── AnalysisJobEntity
│ │ └── AnalysisJobRepository
│ ├── AnalysisJobentityMapper
│ └── AnalysisJobPersistenceAdapter
├── application
│ ├── port
│ │ ├── inbound
│ │ │ └── DeedUseCase
│ │ └── outbound
│ │ └── AnalysisJobPersistencePort
│ └── usecase
│ └── DeedUseCaseImpl
└── model
└── AnalysisJob
3. 엔티티 생성
- AnalysisJobEntity
package com.woopi.safehome.domain.deed.adapter.outbound.persistence.jpa
import com.woopi.safehome.global.enums.AnalysisJobStatus
import com.woopi.safehome.global.`object`.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table
@Entity
@Table(name = "analysis_jobs")
class AnalysisJobEntity(
@Column(name = "job_id" )
var jobId: String,
@Column(name = "file_name" )
var fileName: String,
@Column(name = "file_size" )
var fileSize: Long,
@Column(name = "status" )
@Enumerated(EnumType.STRING)
var status: AnalysisJobStatus,
@Column(name = "result" )
var result: String? = null,
@Column(name = "description" )
var description: String? = null,
) : BaseEntity() {
}
4. repository 생성
- AnalysisJobRepository
package com.woopi.safehome.domain.deed.adapter.outbound.persistence.jpa
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AnalysisJobRepository : JpaRepository<AnalysisJobEntity, Long> {
fun findByJobId(jobId: String): AnalysisJobEntity?
}
5. model 생성
- AnalysisJob
- 클래스 수를 줄이기 위해서 object 선언 후 안에 data class를 추가
- Query, Create, Update 모두 조회와 관련된 모델
- Data는 응답시의 모델(Info, Response 랑 고민했는데, Data가 나아보임...)
package com.woopi.safehome.domain.deed.model
import com.woopi.safehome.global.enums.AnalysisJobStatus
object AnalysisJob {
data class Query(
val id: Long,
val status: AnalysisJobStatus
)
data class Create(
val jobId: Long,
val fileName: String,
val fileSize: Long,
val status: AnalysisJobStatus,
val result: String? = null,
val description: String? = null
)
data class Update(
val id: Long,
val status: AnalysisJobStatus,
val result: String? = null,
val description: String? = null
)
data class Data(
val id: Long,
val jobId: Long,
val fileName: String,
val fileSize: Long,
val status: AnalysisJobStatus,
val result: String? = null,
val description: String? = null
)
}
6. model - entity Mapper 생성
package com.woopi.safehome.domain.deed.adapter.outbound.persistence
import com.woopi.safehome.domain.deed.adapter.outbound.persistence.jpa.AnalysisJobEntity
import com.woopi.safehome.domain.deed.model.AnalysisJob
object AnalysisJobEntityMapper {
fun toModel(entity: AnalysisJobEntity): AnalysisJob.Data {
return AnalysisJob.Data(
id = entity.id!!,
jobId = entity.jobId,
fileName = entity.fileName,
fileSize = entity.fileSize,
status = entity.status,
result = entity.result,
description = entity.description,
)
}
fun toEntity(createModel: AnalysisJob.Create): AnalysisJobEntity {
return AnalysisJobEntity(
jobId = createModel.jobId,
fileName = createModel.fileName,
fileSize = createModel.fileSize,
status = createModel.status,
result = createModel.result,
description = createModel.description,
)
}
}
7. persistencePort, persistenceAdapter 생성
- PersistencePort
package com.woopi.safehome.domain.deed.application.port.outbound
import com.woopi.safehome.domain.deed.model.AnalysisJob
interface AnalysisJobPersistencePort {
/**
* 분석 작업 저장
*/
fun save(analysisJobCreate: AnalysisJob.Create): AnalysisJob.Data
}
- PersistenceAdapter
package com.woopi.safehome.domain.deed.adapter.outbound.persistence
import com.woopi.safehome.domain.deed.adapter.outbound.persistence.jpa.AnalysisJobRepository
import com.woopi.safehome.domain.deed.application.port.outbound.AnalysisJobPersistencePort
import com.woopi.safehome.domain.deed.model.AnalysisJob
import org.springframework.stereotype.Component
@Component
class AnalysisJobPersistenceAdapter(
private val analysisJobRepository: AnalysisJobRepository
) : AnalysisJobPersistencePort {
override fun save(analysisJobCreate: AnalysisJob.Create): AnalysisJob.Data {
return analysisJobRepository.save(
AnalysisJobEntityMapper.toEntity(analysisJobCreate)
).let { AnalysisJobEntityMapper.toModel(it) }
}
}
8. UseCase, UseCaseImple 생성
package com.woopi.safehome.domain.deed.application.port.inbound
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedRequest
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedResponse
interface DeedUseCase {
/**
* 분석 작업
*/
fun analyzeDeed(request: DeedRequest.Analyze): DeedResponse
}
package com.woopi.safehome.domain.deed.application.usecase
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedDtoMapper
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedRequest
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedResponse
import com.woopi.safehome.domain.deed.application.port.inbound.DeedUseCase
import com.woopi.safehome.domain.deed.application.port.outbound.AnalysisJobPersistencePort
import com.woopi.safehome.domain.deed.model.AnalysisJob
import com.woopi.safehome.global.enums.AnalysisJobStatus
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.UUID
@Transactional(readOnly = true)
@Service
class DeedUseCaseImpl (
private val analysisJobPersistencePort: AnalysisJobPersistencePort
): DeedUseCase {
private val logger = LoggerFactory.getLogger(javaClass)
override fun analyzeDeed(request: DeedRequest.Analyze): DeedResponse {
val file = request.file
logger.info("📤 분석 요청 - fileName: ${file.originalFilename}, size: ${file.size}")
// 파일 검증
require(!file.isEmpty) { "파일이 비어있습니다" }
require(file.contentType == "application/pdf") { "PDF 파일만 가능합니다" }
require(file.size <= 50 * 1024 * 1024) { "파일은 50MB 이하여야 합니다" }
val jobId = UUID.randomUUID().toString()
val job = AnalysisJob.Create(
jobId = jobId,
fileName = file.originalFilename ?: "unknown.pdf",
fileSize = file.size,
status = AnalysisJobStatus.PENDING,
)
val savedJob = analysisJobPersistencePort.save(job)
logger.info("✅ 분석 완료 - jobId: $jobId")
return DeedDtoMapper.toResponse(savedJob)
}
}
9. Dto Mapper생성
package com.woopi.safehome.domain.deed.adapter.inbound.web.dto
import com.woopi.safehome.domain.deed.model.AnalysisJob
object DeedDtoMapper {
fun toResponse(model: AnalysisJob.Data): DeedResponse {
return DeedResponse(
id = 0L,
jobId = model.jobId,
description = model.description,
)
}
}
10. WebAdapter 생성
package com.woopi.safehome.domain.deed.adapter.inbound.web
import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleRequest
import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleResponse
import com.woopi.safehome.domain._sample.application.port.inbound.SampleUseCase
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedRequest
import com.woopi.safehome.domain.deed.adapter.inbound.web.dto.DeedResponse
import com.woopi.safehome.domain.deed.application.port.inbound.DeedUseCase
import com.woopi.safehome.global.response.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@Tag(name = "등기부등본 API", description = "등기부등본 API")
@RestController
@RequestMapping("/api/deed")
class DeedInboundWebAdapter (
private val deedUseCase: DeedUseCase
) {
@Operation(summary = "등기부등본 분석", description = "등기부등본 분석")
@PostMapping
fun analyzeDeed (request: DeedRequest.Analyze): ApiResponse<DeedResponse> {
return ApiResponse.success(deedUseCase.analyzeDeed(request))
}
}
'개발 고민' 카테고리의 다른 글
| [개발 고민] webSocket, SSE(Server Sent Event) 중 뭐가 좋을까? (0) | 2026.01.18 |
|---|---|
| [개발 고민]MySQL 조인 순서 최적화로 쿼리 속도 개선하기 (0) | 2026.01.16 |
| [Java, Spring] Lombok @Data 쓰지 말아야 하는 이유 (0) | 2025.11.01 |
| 멀티테넌시 아키텍처 (0) | 2025.04.02 |
| SaaS와 멀티테넌시 (0) | 2025.04.02 |