본문 바로가기
스프링/만들면서 배우는 실무 백엔드 개발

8. JPA 적용하기

by kdohyeon (김대니) 2023. 3. 30.
반응형

이번 시간에는 데이터베이스에서 CRUD 작업을 하기 위한 JPA를 적용해 보도록 하겠습니다. 클린 아키텍처인 만큼 도메인을 만드는 것부터 시작해 보며 이를 DB에 저장하고 조회하는 것까지 해보도록 합니다.


Project issue: https://github.com/kdohyeon/crypto-labs/issues/12
Pull request: https://github.com/kdohyeon/crypto-labs/pull/13

Gradle 설정

JPA를 사용하기 위해서는 의존성을 먼저 추가해주어야 한다. 멀티 모듈 환경에서는 필요한 의존성을 주로 각 모듈에 위치한 build.gradle 에서 관리하게 되는데 Entity는 보통 Domain 모듈에서 관리된다. 따라서 Domain 모듈의 build.gradle 에 아래 의존성을 추가해준다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("jakarta.persistence:jakarta.persistence-api")
}

Entity 설정

데이터베이스에 저장할 엔티티는 다음과 같이 작성할 수 있다. Upbit에서 거래 가능한 마켓에 대한 정보이다.

@Entity
class Market(
    id: Long = 0,
    marketPair: String,
    koreanName: String,
    englishName: String,
    marketWarning: Boolean,
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = id
        protected set

    var marketPair: String = marketPair
        protected set

    var koreanName: String = koreanName
        protected set

    var englishName: String = englishName
        protected set

    var marketWarning: Boolean = marketWarning
        protected set
}

H2 DB

엔티티까지 만들고 H2 인메모리 데이터베이스가 연결된 스프링부트 어플리케이션을 시작하면 알아서 테이블까지 만들어준다. 

localhost:8080/h2-console

1) h2-console 로 접근을 하면 MARKET 이라는 테이블이 이미 생성되어 있는 것을 확인할 수 있다. 컬럼도 엔티티에서 정의한대로 잘 생성되어 있다. 2) SELECT문을 활용하여 데이터를 조회해보면 바로 아래에 해당 조회에 대한 결과가 노출된다. 3) 아직 데이터를 넣지 않았기 때문에 아무런 데이터가 없다.

자동으로 생성된 MARKET 테이블에 아직 데이터가 없는 상태

데이터 넣기

이제 해보아야 할 것은 데이터를 넣고, 데이터를 JPA와 QueryDSL으로 조회하는 것이다. 수작업으로 하나씩 넣긴 귀찮으니 한번에 다 넣을 수 코드를 작성해보자.

Upbit에서 데이터를 호출하여 DB에 Insert하기

MarketController 에서 요청을 받는다. 해당 요청은 CreateMarketUseCase 로 전달이 되며 이를 구현하는 MarketCreateService 가 수행된다. MarketCreateService 에서는 Upbit API 를 호출하여 거래 가능한 마켓 정보를 모두 조회하고 이를 H2 DB 에 저장한다. 가져온 각 마켓 정보를 순회하며 Market 엔티티로 변환하고 MarketRepository 의 save(...) 를 통해 저장한다. 데이터를 저장하는 방법은 JPA 를 활용한다 (MarketJpaRepository)

Controller 부터 Repository 까지의 흐름

흐름을 알아보았으니 각 코드에 대해 알아보자. 코드는 반대로 뒤에서부터 알아보는 것이 편하기 때문에 MarketJpaRepository 부터 알아보도록 하자.

MarketJpaRepository

JpaRepository 를 implement 하고 있다. 대상 엔티티는 Market 이며 해당 엔티티의 ID 의 데이터 타입은 Long 이다. marketPair 로 조회를 하고 싶기 때문에 별도 메소드로 만들었다.

interface MarketJpaRepository : JpaRepository<Market, Long> {
    fun findByMarketPair(marketPair: String): Optional<Market>
}

MarketRepository & MarketRepositoryImpl

MarketRepository 는 마켓 정보에 대한 DB 컨트롤을 하기 위한 output port 이다. 이를 구현한 것이 MarketRepositoryImpl 인데 DB 컨트롤을 하기 위한 문지기 역할 정도로 보면 될 것 같다.

interface MarketRepository {
    fun save(market: Market)
    fun findByMarketPair(marketPair: String): Optional<Market>
}

@Repository
class MarketRepositoryImpl(
    private val marketJpaRepository: MarketJpaRepository,
) : MarketRepository {
    override fun save(market: Market) {
        marketJpaRepository.save(market)
    }

    override fun findByMarketPair(marketPair: String): Optional<Market> {
        return marketJpaRepository.findByMarketPair(marketPair)
    }
}

CreateMarketUseCase & MarketCreateService

CreateMarketUseCase 는 마켓 데이터 생성을 위한 유즈케이스를 정의한다. CreateMarketUseCase 를 구현하는 구현체이다. Upbit에서 거래 가능한 마켓 정보를 조회하는 OPEN API를 호출하여 데이터를 받아온 다음 MarketRepository를 통해 데이터를 저장한다.

interface CreateMarketUseCase {
    fun marketAll()
}

@Service
class MarketCreateService(
    private val marketSearchService: MarketSearchService,
    private val marketRepository: MarketRepository,
) : CreateMarketUseCase {
    override fun marketAll() {
        marketSearchService.marketAll()
            .forEach {
                marketRepository.save(
                    Market.create(
                        CreateMarket(it.marketPair, it.koreanName, it.englishName)
                    )
                )
            }
    }
}

 

MarketController

작업한 코드를 호출하기 위한 API 를 정의한다.

@RestController
class MarketController(
    private val createMarketUseCase: CreateMarketUseCase,
) {
    @PostMapping("/api/v1/market/all")
    fun saveAllMarkets() {
        createMarketUseCase.marketAll()
    }
}

API 호출하기

위와 작업을 하고 이제 호출을 해보자.

### Upbit 에서 마켓 코드 전체 조회 후 전체 저장
POST http://localhost:8080/api/v1/market/all

성공적으로 데이터가 저장되었는지 확인해보자.

잘 저장된 데이터

잘 저장된 것을 확인해보았으니 이제 저장된 데이터를 조회해보자.

데이터 조회

JpaRepository 를 구현하는 MarketJpaRepository 를 만들었고, 기본적인 CRUD 기능은 제공된다. MarketPair 로 조회하는 메소드는 없어서 별도로 선언해주었다 (findByMarketPair). 

SearchMarketUseCase & MarketSearchService

조회를 위한 유즈케이스와 구현체이다.

interface SearchMarketUseCase {
    fun marketAll(): List<MarketDto>
    fun marketPair(marketPair: String): MarketDto
}

@Service
class MarketSearchService(
    private val upbitMarketClient: UpbitMarketClient,
    private val marketRepository: MarketRepository,
    private val marketDtoConverter: MarketDtoConverter,
) : SearchMarketUseCase {
    override fun marketAll(): List<MarketDto> {
        return upbitMarketClient.marketAll().map { marketDtoConverter.convert(it) }
    }

    override fun marketPair(marketPair: String): MarketDto {
        return marketRepository.findByMarketPair(marketPair)
            .orElseThrow { throw IllegalArgumentException("MarketPair does not exist") }
            .let { marketDtoConverter.convert(it) }
    }
}

API 호출하기

이를 호출하는 API 를 만들고 호출을 해보자.

### 마켓 코드 단건 조회 (DB)
GET http://localhost:8080/api/v1/market/KRW-BTC

데이터를 잘 가져온 것을 확인할 수 있다.

성공적으로 조회


다음 시간에는 QueryDSL을 적용해보도록 하겠습니다.

반응형

댓글