개발일지

[개발일지_safeHome] 7. sample 도메인 개발 (adapter, usecase, repository, 등등.....)

woopii 2025. 12. 23. 02:05

 

1. Sample Repository 생성

Sample, SampleDetail 엔티티에 맞춰서 repository를 생성
sample도메인에서는 만들지 않을거지만,추후 복잡한 쿼리는 QueryDsl을 활용하고자 한다

 

  • SampleRepository
package com.woopi.safehome.domain._sample.adapter.outbound.persistence.jpa

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface SampleRepository: JpaRepository<SampleEntity, Long> {

    fun findByIsDeletedFalse(): List<SampleEntity>

    fun findByIdAndIsDeletedFalse(id: Long): SampleEntity?

}

 

 

  • SampleDetailRepository
package com.woopi.safehome.domain._sample.adapter.outbound.persistence.jpa

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface SampleDetailRepository: JpaRepository<SampleDetailEntity, Long> {
}

 

 

2. PersistencePort, PersistenceAdapter 생성

usecase에서 호출하는 port와 구현체인 adapter를 정의함

 

  • SamplePersistencePort
package com.woopi.safehome.domain._sample.application.port.outbound

import com.woopi.safehome.domain._sample.model.Sample

interface SamplePersistencePort {

    /**
     * 샘플 리스트 조회
     */
    fun findAllSample(): List<Sample>

    /**
     * 샘플 상세 조회
     */
    fun findSampleById(id: Long): Sample?

    /**
     * 샘플 저장
     */
    fun saveSample(sample: Sample): Sample

}

 

 

  • SamplePersistenceAdapter

Model 쪽을 좀 수정했다, 비지니스를 명확하게 하기위해서 하나의 모델을 CreateModel, UpdateModel 등으로 세분화 하고

그에 따라서 Mapper도 좀 더 명확하게 만들었다.

package com.woopi.safehome.domain._sample.adapter.outbound.persistence

import com.woopi.safehome.domain._sample.adapter.outbound.persistence.jpa.SampleEntity
import com.woopi.safehome.domain._sample.model.Sample
import com.woopi.safehome.domain._sample.model.SampleCreate
import com.woopi.safehome.domain._sample.model.SampleUpdate

object SampleEntityMapper {

    fun toModel(entity: SampleEntity): Sample {
        return Sample(
            id = entity.id!!,
            name = entity.name,
            code = entity.code,
            description = entity.description,
            orderNo = entity.orderNo,
        )
    }

    fun toModelList(entities: List<SampleEntity>): List<Sample> {
        return entities.map(SampleEntityMapper::toModel)
    }

    fun toEntity(model: SampleCreate): SampleEntity {
        return SampleEntity(
            name = model.name,
            code = model.code,
            description = model.description,
            orderNo = model.orderNo,
        )
    }

    fun toEntity(model: SampleUpdate): SampleEntity {
        return SampleEntity(
            name = model.name,
            code = model.code,
            description = model.description,
            orderNo = model.orderNo,
        ).apply { id = model.id }
    }

}

 

 

 

3.Mapper 생성

객체간 변환을 도와주는 Mapper 클래스를 만들었는데,
2개를 만들었다, EntityMapper, DtoMapper

 

  • SampleEntityMapper
package com.woopi.safehome.domain._sample.adapter.outbound.persistence

import com.woopi.safehome.domain._sample.adapter.outbound.persistence.jpa.SampleEntity
import com.woopi.safehome.domain._sample.model.Sample

object SampleEntityMapper {

    fun toModel(entity: SampleEntity): Sample {
        return Sample(
            id = entity.id,
            name = entity.name,
            code = entity.code,
            description = entity.description,
            orderNo = entity.orderNo,
        )
    }

    fun toModelList(entities: List<SampleEntity>): List<Sample> {
        return entities.map(SampleEntityMapper::toModel)
    }

    fun toEntity(model: Sample): SampleEntity {
        return SampleEntity(
            name = model.name,
            code = model.code,
            description = model.description,
            orderNo = model.orderNo,
        ).apply {
            model.id?.let { this.id = it }
        }
    }

}

 

 

  • SampleDtoMapper

 

4. UseCase 생성

간단하게 CRUD 정도만 메소드를 만들 예정

 

  • SampleUseCase
package com.woopi.safehome.domain._sample.application.port.inbound

import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleRequest
import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleResponse

interface SampleUseCase {

    /**
     * 샘플 리스트 조회
     */
    fun getSampleList(request: SampleRequest.Search): List<SampleResponse>

    /**
     * 샘플 상세 조회
     */
    fun getSampleDetails(id: Long): SampleResponse

    /**
     * 샘플 생성
     */
    fun createSample(request: SampleRequest.Create): SampleResponse

    /**
     * 샘플 수정
     */
    fun updateSample(request: SampleRequest.Update): SampleResponse

    /**
     * 샘플 삭제
     */
    fun deleteSample(id: Long): SampleResponse

}

 

 

 

  • SampleDetailUseCase

 

package com.woopi.safehome.domain._sample.application.port.inbound

import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleDetailRequest
import com.woopi.safehome.domain._sample.adapter.inbound.web.dto.SampleDetailResponse

interface SampleDetailUseCase {

    /**
     * 샘플 상세 리스트 조회
     */
    fun getSampleDetailList(request: SampleDetailRequest.Search): List<SampleDetailResponse>

    /**
     * 샘플 상세 상세 조회
     */
    fun getSampleDetailDetails(id: Long): SampleDetailResponse

    /**
     * 샘플 상세 생성
     */
    fun createSampleDetail(request: SampleDetailRequest.Create): SampleDetailResponse

    /**
     * 샘플 상세 수정
     */
    fun updateSampleDetail(request: SampleDetailRequest.Update): SampleDetailResponse

    /**
     * 샘플 상세 삭제
     */
    fun deleteSampleDetail(id: Long): SampleDetailResponse

}

 

 

5. Request, Response 생성

클래스가 너무 많이 생성되는 것을 방지하기 위해서,

Request는 Object클래스를 만들고, 안에 payload 역할을 하는 데이터 클래스를 넣었다.

Response는 아직 좀 고민이긴 한데.....일단 조회용 class 하나 만들고나서 나중에 고민하려한다 

 

Object 클래스

- 싱글톤 패턴을 구현하는 특수한 클래스
- 애플리케이션에서 단 하나의 인스턴스만 존재하도록 보장
- 인스턴스화 불가능 (.new 불가)

 

 

  • Sample Request
package com.woopi.safehome.domain._sample.adapter.inbound.web.dto

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

object SampleRequest {

    data class Search(
        @Schema(description = "이름", example = "샘플1")
        val name: String?,

        @Schema(description = "코드", example = "SAMPLE_001")
        val code: String?,
    )

    data class Create(
        @Schema(description = "이름", example = "샘플1")
        @field:NotBlank(message = "이름은 필수 입니다.")
        val name: String,

        @Schema(description = "코드", example = "SAMPLE_001")
        @field:NotBlank(message = "코드는 필수 입니다.")
        val code: String,

        @Schema(description = "설명", example = "샘플 설명")
        val description: String?,

        @Schema(description = "정렬 순서", example = "1")
        val orderNo: Int,
    )

    data class Update(
        @Schema(description = "샘플 ID", example = "1")
        @field:NotNull(message = "샘플 ID는 필수 입니다.")
        val id: Long,

        @Schema(description = "이름", example = "샘플1")
        @field:NotBlank(message = "이름은 필수 입니다.")
        val name: String,

        @Schema(description = "코드", example = "SAMPLE_001")
        @field:NotBlank(message = "코드는 필수 입니다.")
        val code: String,

        @Schema(description = "설명", example = "샘플 설명")
        val description: String?,

        @Schema(description = "정렬 순서", example = "1")
        val orderNo: Int,
    )

}

 

 

  • SampleResponse
package com.woopi.safehome.domain._sample.adapter.inbound.web.dto

import io.swagger.v3.oas.annotations.media.Schema

data class SampleResponse(

    @Schema(description = "ID")
    val id: Long,

    @Schema(description = "이름")
    val name: String,

    @Schema(description = "코드")
    val code: String,

    @Schema(description = "설명")
    val description: String?,

    @Schema(description = "정렬 순서")
    val orderNo: Int,
)

 

 

 

6. UseCaseImpl 생성

usecaseimpl 혹은 service 만들때, 

클래스에 @Transactional(readOnly = true)을 선언하고, 쓰기 작업을 수행해야 하는곳에서만
@Transactional( readOnly = false는 default라 생략) 을 사용했는데, 

 

 

이유로는

  1. 코드 간결 및 의미 명확화 (어노테이션 반복 최소화)
  2. 의도치 않은 데이터 수정 방지 (읽기 작업만 하는 메서드에서 실수로 데이터 변경 방지)
  3. Master-Slave 구조에서 자동으로 Slave DB로 라우팅하는 설정과 함께 사용하여 효율적인 리소스 관리
  4. Jpa 환경에서 조회 시 스냅샷을 생성하지도 않고, flush시 스냅샵과 엔티티를 비교하지도 않음, 즉 dirtyChecking을 하지 않음
    • 그로 인해 스냅샷에 할당하는 메모리를 절약하고, 비교 연산을 수행하지 않아서, CPU 절약 수행

 

 

package com.woopi.safehome.domain._sample.application.usecase

import com.woopi.safehome.domain._sample.adapter.inbound.web.SampleDtoMapper
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._sample.application.port.outbound.SamplePersistencePort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Transactional(readOnly = true)
@Service
class SampleUseCaseImpl(
    private val samplePersistencePort: SamplePersistencePort
): SampleUseCase {

    override fun getSampleList(request: SampleRequest.Search): List<SampleResponse> {
        return SampleDtoMapper.toResponse(samplePersistencePort.findAllSample());
    }

    override fun getSampleDetails(id: Long): SampleResponse {
        samplePersistencePort.findSampleById(id).let {
            return SampleDtoMapper.toResponse(it)
        }
    }

    @Transactional
    override fun createSample(request: SampleRequest.Create): SampleResponse {
        TODO("Not yet implemented")
    }

    @Transactional
    override fun updateSample(request: SampleRequest.Update): SampleResponse {
        TODO("Not yet implemented")
    }

    @Transactional
    override fun deleteSample(id: Long): SampleResponse {
        TODO("Not yet implemented")
    }


}