반응형

Zookeeper

Zookeeper는 Broker들을 관리하는 역할을 한다

  • Broker 등록 / 상태 관리
  • 브로커의 상태를 감시
  • Controller 선출
  • 파티션 리더 선출 및 정보 관리
  • 클러스터 메타데이터 관리

주키퍼는 카프카에 종속되지 않은 별개의 시스템으로 브로커 관리를 위해 존재한다

Kafka 2.8 부터 이에 대한 의존성을 제거하고자 KRaft를 통해 이를 대처하려고 하고 있다

Broker

Broker는 메시지 저장 및 메시지 제공 등의 역할 수행하며 Producer/Consumer와 직접 통신하는 주체이다

  • 데이터 저장소
  • 메시지 수신 / 전달
  • 파티션 및 레플리카 관리
  • 파티션 리더 역할 수행
  • Controller 역할 수행

 

Kafka의 Broker들의 모음을 Kafka Cluster라고 한다

Zookeeper는 Kafka Cluster 내 Controller를 선출하는 역할, Broker들을 관리하는 역할을 하며 Broker들의 상태를 감시하고 브로커의 상태를 Controller에게 전달하여 Controller가 Failover를 할 수 있도록 한다

반응형

'공부 > Kafka' 카테고리의 다른 글

[Kafka] Kafka의 특징  (0) 2021.06.13
[Kafka: 카프카] Kafka란?  (0) 2021.06.07
반응형

getParameter() 그리고 getReader() 사용

HttpServletRequest 객체의 getParameter()를 통해 요청으로부터 파라미터 값을 가져와 작업을 하는 경우 주의가 필요하다.

만약 getParameter()를 통해 요청 파라미터 값을 받게 되는 경우 후속 작업에서 getReader를 통해 요청 값을 처리하는 경우가 생길 때 문제가 될 수 있다.

그 이유는 Servlet Spec에 있는데, Servlet Spec에 명시된 get Parameter에 대한 설명은 다음과 같다.

 

Parameter를 이용할 수 있는 상황에 대해 정의한 부분이다.

  1. 요청이 HTTP, HTTPS 인 경우
  2. HTTP POST 메소드 인 경우
  3. Content-Type 이 application/x-www-form-urlencoded 인 경우
  4. Servlet이 getPrarmeter 유형의 메소드를 최초 호출한 경우

위 조건이 만족하지 않아 사용할 수 없는 경우에는 요청 객체의 input stream을 통해 post data를 이용할 수 있다.

 

다만, 위 조건이 충족한 경우에는 요청 객체에 대한 input stream을 직접 이용할 수 없다.

 

getParameter함수와 getReader 함수를 같이 써야하는 경우 주의가 필요하다.

일반적으로 함수를 사용하거나 할 때 해당 함수의 스펙이나 자세한 문서를 찾아보지 않고 그냥 쓰는 경우가 많은데 그럴 경우 문제가 될 수 있다.

이러한 문제를 극복하기 위해 Wapper 클래스를 상속하여 Reusable하게 만든 HttpServletRequest 클래스를 사용하고 있다.

하지만 was 마다 해당 부분에 대한 처리가 다를 수 있기 때문에 관련 부분을 확인해본 후 사용할 필요가 있다.

 

JEUS의 겨우 : jeus.servlet.engine.WebtoBServletRequest

Tomcat의 경우 : org.apache.catalina.connector.RequestFacade

반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

JPA를 사용하면서 자칫하면 순환참조가 발생하는 경우가 있다

그렇다면 왜 JPA를 사용할 때 이러한 일이 발생할까

1. 원인

일반적으로 JPA에서 순환참조가 발생하는 이유는 양방향 연관관계가 존재하는 경우 이다

/* 양방향 연관관계 */
@Entity
data class Customer(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,

    @OneToMany(mappedBy = "customer")
    val orders: List<Order> = mutableListOf()
)

@Entity
data class Order(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val orderDate: String,

    @ManyToOne
    @JoinColumn(name = "customer_id")
    val customer: Customer
)

이 처럼 Customer(고객)가 여러 Order(주문)을 가지는 경우에 대해 필요 시 양방향 연관 관계를 설정할 수 있다

 

그렇다면 이러한 상황에서 Customer의 정보를 조회하여 조회된 결과를 그냥 반환한다고 하면 순환 참조가 발생할 수 있게 된다

@Service
class CustomerService(
    private val customerRepository: CustomerRepository
) {
	fun getCustomerById(id: Long): Customer? = 
		customerRepository.findById(id).orElseThrow(EntityNotFoundException("Customer not found"))
}

@Repository
interface CustomerRepository : JpaRepository<Customer, Long>

이렇게 조회된 Customer를 별도의 DTO 객체로 변환하거나 하지 않고 Service를 지나 Controller에서 반환된다면 JSON으로 변환될 때 순환 참조를 맛볼 수 있다

 

그렇다면 왜 JSON으로 변환하는 과정에서 순환 참조가 발생하게 되는 걸까?

 

2. 이유

그 이유를 알아보기 위해서는 객체를 JSON으로 변환해주는 라이브러리를 살펴볼 필요가 있다

Spring에서는 @RestController가 있는 Controller에서 객체를 반환하면 JSON 형식으로 변환되어 응답이 나가게 된다

이러한 것은 Spring 내부에 있는 Message Converter(HttpMessageConverter)가 우리의 객체를 Http 메시지로 변환하는 과정을 통해 이루어 진다

Controller에서 반환된 객체는 ReturnValue Handler에 의해 처리가 되고,

반환된 객체는 Jackson Message Coverter에 의해 처리가 되고 내부에 ObjectMapper에 의해 객체가 json으로 serialize 되는 과정에서 객체 내부의 필드에 대해 재귀 호출이 발생하고 이 과정에서 양방향 연관관계가 있는 객체에 대해 순환 참조가 발생하게 되고 StackOverFlow Exception이 발생하게된다

3. 해결 방법

1) @JsonIgnore

@Entity
class Child(
	@Id
	@GenerateValue
	val id: Long? = null,
	
	@ManyToOne
	@JoinColumn(name = "parent_id")
	@JsonIgnore
	val parent: Parent
)

JsonIgnore 어노테이션을 사용하면 직렬화를 할 때 해당 변수에 대한 직렬화를 제외하게 되고 순환 참조가 일어나지 않는다

단, 이러한 경우 해당 변수에 대한 값은 직렬화 되지 못하여 반환된 값에서 제외 된다

 

2) DTO활용 (가장 적절한 방법)

데이터를 반환할 때는 DTO를 통해 의미 있는 값만 전달 반환하는 것이 바람직하다

data class ParentDto(
    val id: Long?,
    val children: List<ChildDto>
)

data class ChildDto(
    val id: Long?,
    val parentId: Long?
)

fun Parent.toDto(): ParentDto = ParentDto(
    id = this.id,
    children = this.children.map {it.toDto()}
)

fun Child.toDto(): ChildDto = ChildDto(
    id = this.id,
    parentId = this.parent.id
)

이 처럼 원하는 데이터만 DTO에 포함하여 반환하면 순환참조를 제어할 수 있고, 진짜 필요한 값만 반환할 수 있기 때문에 가장 추천되는 방법이다

 

위에서 설명한 것 외에도 순환 참조가 발생하는 케이스들이 존재하기 때문에 JPA를 사용하는 경우 이를 유의하며 코드를 작성해야 한다

반응형

'공부 > JPA' 카테고리의 다른 글

JPA 시작  (0) 2021.04.25
반응형

1. S(Shared) Lock, 공유 락

  • 데이터를 읽는 동안 쓰기에 대한 제한을 두기 위한 락
  • 여러 트랜잭션에서 동시에 공유락 획득 가능
  • X락이 설정된 경우는 공유락 획득 불가
/* 오라클 */
SELECT * FROM TEST FOR SHARE;

일반적인 SELECT SQL 수행 시 마지막에 FOR SHARE 구문을 붙이게 되면 S락을 설정할 수 있다

! 중요

공유락에 대해서 SELECT 조회를 하면 공유락이 걸린다는 오해를 하는 경우가 있다

일반적으로 SELECT를 하게 되면 공유락이 걸린다고 생각하는데 FOR SHARE을 붙여서 SELECT를 하지 않으면 공유락이 획득되지 않는다

이는 오라클이 MVCC(Multi Version Concurrency Control)로 동작하기 때문이다

오라클 기준으로 일반적으로 조회를 하는 경우에는 실제 데이터를 읽는 것이 아닌 스냅샷을 통해서 데이터를 데이터를 읽기 때문에 락의 영향을 받지 않고 데이터를 읽을 수 있다

다만 이런 경우 조회하는 시간이 오래걸리거나 하는 경우 스냅샷으로 부터 읽은 데이터가 변경되면 Ora-01555: Snapshot Too Old ****가 발생할 수 있다

 

2. X(Exclusive) Lock, 베타 락

  • 데이터 쓰기를 하는 트랜잭션에서 설정하는 락
  • 데이터를 쓰는 동안 다른 락을 허용하지 않음
  • X 락이 설정 되어있으면 S 락 또는 X 락을 설정할 수 없음
  • 베타락이 걸려있으면 데이터를 읽는 것 또한 차단
  • 동시성은 감소하나 데이터 무결성 보장
/* 오라클 */
SELECT * FROM TEST FOR UPDATE;

/* DML 시에도 X락 설정됨 */
UPDATE, DELETE

베타락은 데이터 또는 테이블에 대한 변경이 발생할 때, 그리고 SELECT 시 FOR UPDATE 구문을 붙인 경우 설정된다

반응형

'공부' 카테고리의 다른 글

레거시 시스템에서 발생하는 DeadLock에 대한 고찰  (0) 2025.06.18
반응형

3개 이상의 주문 채널이 존재하고 각 주문 채널별 모놀리식 시스템과 단일 DB로 구성된 환경에서 특정 시간 동시 주문 발생 간 데드락이 발생하였고 이에 대한 해결 방법에 대한 사고 실험을 해보고자 한다

 

문제 상황

한개 이상의 상품을 포함하는 주문이 여러 판매 채널로 부터 발생하는 과정에서 상품의 재고 차감 중 데드락이 발생

 

발생 예시

주문1(상품 A, C) 주문2 (상품 B, C, A)
상품 A Lock 상품 B Lock
상품 C Lock 대기 상품 C Lock
  상품 A Lock 대기

 

이처럼 여러 상품을 포함하고 있는 주문 건에 대해 재고 차감 시 데드락이 발생할 수 있다

 

 

락의 종류

1. Optimistic Lock(낙관적 락)

낙관적 락은 version 컬럼을 추가로 두고 업데이트 시 버전을 조건으로 넣어 데이터를 변경하는 방식

조건절에 있는 버전 정보가 맞지 않으면 업데이트가 실패하게 됨으로써 정합성을 보장하는 방법이다

UPDATE ..... WHERE PRODUCT_ID = ? AND VERSION = ?;

 

락을 점유하지 않아 데드락 발생에서 자유롭다

버전 정보가 맞지 않아 업데이트가 실패하는 경우에 대한 재수행 로직이 추가로 구현될 필요가 있다

또한, 충돌이 빈번하게 발생하는 상황(주문이 대량으로 들어오는 상황)에서 반복적인 재수행에 따라 성능 저하가 발생할 수 있다

 

 

2. Pessimistic Lock(비관적 락)

비관적 락은 충돌이 발생할 수 있는 데이터에 접근할 때 해당 데이터에 대한 락을 설정하여 다른 트랜잭션에서 해당 데이터에 대한 변경을 차단하는 방식(행에 대한 락 설정)

SELECT * FROM INVENTORY WHERE PRODUCT_ID = ? FOR UPDATE;
SELECT * FROM INVENTORY WHERE PRODUCT_ID IN (?, ?, ?...) ORDER BY PRODUCT_ID FOR UPDATE;

* 여러 행에 대해 락을 설정하는 경우 ORDER  BY 설정이 중요하다. ORDER BY 설정을 하지 않은 경우 락을 설정하는 행의 순서에 따라 데드락이 발생할 수 있기 때문이다

 

출돌이 발생하기 전에 락을 걸어 차단하기 때문에 충돌 상황에서 안전하게 처리가 가능하다

하지만 트랜잭션 시간이 길거나 상품 수가 많은 경우 성능 저하가 발생할 수 있다

 

3. Distributed Lock(분산 락)

분산 락은 여러 서버들 간의 리소스에 대한 잠금이 필요한 경우 사용하는 것으로 Redis나 Zookeeper 등의 미들웨어를 통해 잠금을 수행한다

 

분산 시스템에 대한 락을 설정할 수 있는 장점이 있으나 미들웨어에 의존하기 때문에 미들웨어의 장애 시 락의 안정성을 보장하기 어렵다(미들웨어의 FailOver 설정 중요)

네트워크 지연이나 TTL 만료 등 고려하지 않으면 중복 실행될 수 있다

 

 

고찰

위와 같은 문제가 발생한 상황과 현재 운영 중인 시스템의 관점에서 보면 낙관적 락을 선택하는게 바람직해 보인다

 

1. 단일 DB이므로 분산 환경 X

2. 동시 발생하는 주문이 많지 않다

3. 데드락 회피 필요

위와 같은 이유로 현재 주어진 상황 속에서 적용할 수 있는 락을 선택하였다

 

!다만, 낙관적 락을 위해서는 변경 실패에 대한 재시도 로직에 대한 추가적인 구현과 기존 코드나 쿼리에 대한 변경이 필요하고, 재시도 횟수에 대한 정책 또한 수립이 필요하다

 

 

사고 실험을 하면서 서비스의 현재 상황과 시스템의 구성 등을 고려하여 가장 적합한 방법을 고민해볼 수 있었다

반응형

'공부' 카테고리의 다른 글

[DB] S/X Lock  (0) 2025.06.24
반응형

 

외부와 통신하는 기능에 대해 외부 서비스의 SSL이 2세대 인증서로 변경되면서 이에 대해 확인하는 과정을 기록한 것이다

 

어플리케이션에서 외부 서비스와의 통신을 하는 경우 RestTemplate를 통해 통신을 하는 등 java 단에서 통신을 하는 경우가 있다

이러한 경우 통신의 과정에서 SSL 인증서의 검증은 어떻게 이루어 지는지에 대해 알아보았다

 

일반적으로 웹사이트에 접속을 하게 되면 SSL 인증서에 대한 검증은 브라우저에서 수행하게 되어있는데

Java 어플리케이션 내에서 https 프로토콜로 통신 하는 경우 SSL 인증서에 대한 검증은 JVM 내에서 이루어지게 되어있다

 

Java의 SSL 인증서 검증 흐름

Java 어플리케이션 내에서 SSL 인증서의 검증 흐름은 다음과 같다

  HttpsURLConnection
         ↓
     SSLContext
         ↓
 TrustManagerFactory
         ↓
  X509TrustManager (실제 검증 수행)
         ↓
 checkServerTrusted

 

HttpsURLConnection은 네트워크 통신 레벨의 API로 SSL 핸드셰이크를 수행

SSLContext는 SSL 세션의 구성요소에 대한 초기화 및 관리르 수행하고 해당 객체에서 암호화 방식이나 TrustManager를 결정

TrustManagerFactory SSLContext에서 사용할 TrustManager 리스트를 생성하는 역할 수행한다 이 때, JVM "cacerts" 또는 사용자 keyStore를 통해 TrustManager 생성

X509TrustManager는 실제 서버 인증서 체인을 검증하는 역할을 수행하며 "checkServerTrusted" 메소드를 통해 서버가 제공한 인증서 체인을 검증

 

(RestTemplate도 내부적으로 HttpURLConnection을 사용하고 있어 인증 흐름은 동일)

 

 

Java 내 cacerts 확인 방법

cd $JAVA_HOME (JAVA가 설치된 디렉토리로 이동)

cd jre/lib/security (cacerts가 있는 위치로 이동)

# 아래 명령을 수행하면 해당 되는 alias에 대한 CA 인증서 정보를 볼 수 있다
keytool -list -v -keystore ./cacerts -storepass [password, 기본 "changeit"] -alias '[alias 명]'

 

해당 CA인증서를 확인함으로써 인증서의 세대 교체 등이 발생했을 때 신규 인증서를 어플리케이션에서 검증할 수 있는지 확인할 수 있다

 

추가

어플리케이션 실행할 때 jvm 옵션으로 아래 옵션을 추가하면 HTTPS 요청이 발생할 때 인증서, TLS 버전, 루트 인증서에 대한 정보를 로그로 확인할 수 있다

-Djavax.net.debug=ssl,handshake,certpath

 

반응형

'공부 > Java' 카테고리의 다른 글

getParameter, getReader 값 못 읽는 문제  (0) 2025.06.25
Out Of Memory 문제 분석  (0) 2025.05.31
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

선택도 : 전체 레코드 중에서 조건절에 의해 선택되는 레코드 비율

선택도 = 1 / NDV(Number of Distinct Value)

 

카디널리티 : 전체 레코드 중에서 조건절에 의해 선택되는 레코드 개수

카디널리티 = 총 로우 수 * 선택도 = 총 로우 수 / NDV

 

예) 상품분류 컬럼이 ‘가전’, ‘의류’, ‘식음료’, ‘생활용품’ 네 개의 값이 있을 때,

where 상품분류 = ‘가전’ 해당 조건에 대한 선택 도는 25%(1/4)

만약 전체 레코드가 10만건이면 카디널리티는 2만 5천

 

 

통계정보

 

오브젝트 통계

테이블 통계

테이블 통계 정보 수집 방법

 

테이블 통계 정보 조회

또는 all_tab_statistics 뷰를 통해 조회 가능

 

테이블 통계항목

 

인덱스 통계

인덱스 통계 수집

 

인덱스 조회

 

인덱스 통계정보는 all_ind_statistics 뷰에서 조회가 가능하다

 

 

컬럼 통계(히스토그램 포함)

컬럼통계는 테이블 통계 수집할 때 함께 수집된다

 

컬럼 통계 조회

 

컬럼 히스토그램

컬럼 값별로 데이터 비중 또는 빈도를 미리 계산해 놓은 통계정보

실제 값을 읽어서 계산해둔 값으로 데이터 분포가 많이 변하지 않는 한 거의 정확하다

 

해스토그램 수집 방법

히스토그램을 수집하려면, 테이블 통계 수집 시 method_opt 파라미터를 지정하면 된다

 

수집된 컬럼 히스토그램 조회

all_tab_histograms 뷰에서 같은 저보 조회 가능

 

 

시스템 통계

애플리케이션 및 하드웨어 성능 특성을 측정한 것으로 아래 항목을 포함 한다

  • CPU 속도
  • 평균적인 Single Block I/O 속도
  • 평균적인 Multiblock I/O 속도
  • 평균적인 Multiblock I/O 개수
  • I/O 서브시스템의 최대 처리량
  • 병렬 Slave의 평균적인 처리량

sys.aux_stats$ 뷰에서 조회할 수 있다

 

 

비용 계산 원리

인덱스 키값을 모두 = 조건으로 검색할 때의 비용 계산

 

인덱스 키값이 모두 = 조건이 아닐 때는 컬럼 통계까지 활용하여 비용 계산

 

 

비용(Cost)

I/O 비용 모델 : I/O 비용 모델을 사용할 때 실행게획에 나타나는 Cost는 ‘예상 I/O Call 횟수’를 의미

CPU 비용 모델 : Single Block I/O 를 기준으로 한 상대적 시간을 표현

 

 

옵티마이저

  • 비용기반(Cost-Based) 옵티마이저(CBO) : 사용자 쿼리를 위해 후보군이 될만한 실행계획들을 도출하고, 데이터 딕셔너리에 미리 수집해 둔 통계정보를 이용해 각 실행계획의 예상비용을 산정하고, 그 중 가장 낮은 비용의 실행계획 하나를 선택하는 옵티마이저
  • 규칙기반(Rule-Based) 옵티마이저(RBO) : 통계정보를 활용하지 않고 단순한 규칙에만 의존하여 대량 데이터를 처리하는데 부적합

 

옵티마이저 모드

  • ALL_ROWS : 전체 처리속도 최적화 : 전체를 읽는 것을 전제로 시스템 리소스를 가장 적게 사용하는 실행계획을 선택
  • FIRST_ROWS : 최초 응답속도 최적화 : 앞쪽 일부만 읽다가 멈추는 것을 전제로 응답 속도가 가장 빠른 실행계획을 선택
  • FIRST_ROWS_N : 최초 N건 응답속도 최적화 : 앞쪽 N개 로우만 읽고 멈추는 것을 전제로 응답 속도가 가장 빠른 실행계획을 선택

옵티마이저 모드 설정

힌트 적용

 

 

옵티마이저에 영향을 미치는 요소

  • SQL과 연산자 형태 : SQL 형태, 연산자(=, IN, LIKE, BETWEEN, 부등호 등)
  • 인덱스, IOT, 클러스터, 파티션, MV 등 옵티마이징 팩터
  • 제약 설정 : PK, FK, Check, Not Null
  • 통계정보
  • 옵티마이저 힌트
  • 옵티마이저 관련 파라미터 : 옵티마이저 파라미터 목록 확인

 

옵티아미저 한계

  • 옵티마이저 행동에 가장 큰 영향을 미치는 통계정보를 필요한 만큼 충분히 확보하는 것은 불가능한 일, 통계정보를 수집하고 관리하는 데 어마어마한 시간과 비용이 들기 때문
  • 바인드 변수를 사용한 SQL에 컬럼 히스토그램을 활용할 수 없다는 치명적인 단점
  • 최적화에 허용되는 시간이 매우 짧다

 

개발자의 역할

- 필요한 최소 블록만 읽도록 쿼리 작성

1) 출력 대상이 아닌 게시물에 대해서도 GET_ICON 함수 적용

2) 회원, 게시판유형, 질문유형 테이블과 조인 → 출력 대상 집합 확정 후 조인 필요

 

 

- 최적의 옵티마이징 팩터 제공

최적화는 옵티마이저가 수행하지만, 잘 할 수 있도록 적절한 수단을 제공하는 것은 사용자의 몫

 

  • 전략적인 인덱스 구성
  • DBMS가 제공하는 다양한 기능 활용
  • 옵티마이저 모드 설정
  • 정확하고 안정적인 통계정보

옵티마이저 모드를 포함해 각종 파라미터를 적절한 값으로 설정하고, 통계정보를 잘 수집해 주는 것이 중요

그리고 난 후에 전략적인 인덱스 구성이 필수적으로 뒷받침되어야한다

또한 DBMS가 제공하는 기능을 적극적으로 활용해 옵티마이저가 최적의 선택을 할 수 있도록 다양한 수단을 제공해 주어야한다

 

- 필요시, 옵티마이저 힌트를 사용해 최적의 액세스 경로로 유도

 

 

데이터베이스 튜닝

SQL이 병목이나 지연 없이 빠르고 안정적으로 수행되도록 조치하는 모든 활동

  • SQL 튜닝 : I/O 효율화, DB Call 최소화, SQL 파싱 최소화 등
  • DB 설계 : 논리적 데이터 구조 설계, 물리적 저장 구조 설계 등
  • 인스턴스 튜닝 : Lock/Latch 모니터링 및 해소, 메모리 설정, 프로세스 설정 등
반응형
반응형

모의 커머스 서비스를 개발하는 과정에서 API에 대한 동작을 확인하기 위해 Testcontainer를 활용하여 테스트를 작성하였는데,

그 과정에서 발생한 내용에 대한 기록이다

 

회원 모듈을 개발하였고 "회원가입, 회원수정, 회원탈퇴, 로그인, 로그아웃" 이렇게 단순한 기능을 개발하였다

로그인과 로그아웃을 위해 JWT와 Redis를 활용하여 세션을 관리하였다

 

개발을 마친 후 단위적인 기능에 대한 테스트가 아닌 API를 통해 실제적으로 원하는 결과가 도출되는지 확인하기 위해 통합테스트가 필요하다

 

개발한 기능에는 Redis라는 서드파티 서비스가 필요하였고, 테스트를 위해 Redis 컨테이너를 그 때마다 띄울 수 없었다

그래서 Testcontainer를 활용하였다

 

Testcontainer를 활용하면 테스트가 수행되는 동한 테스트용 컨테이너를 자동으로 띄울 수 있기 때문에 통합테스트가 편리해진다

 

첫번째 회원에 대한 API 테스트를 Testcontainer를 활용하여 작성하고 수행하였다

@Testcontainers
abstract class RedisTestConfig {
    companion object {
        @JvmStatic
        @Container
        private val redis = GenericContainer("redis:6.2.7-alpine")
            .withExposedPorts(6379)
            .withReuse(true)
            .apply { start() }

        @JvmStatic
        @DynamicPropertySource
        fun registerRedisProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.data.redis.host") { redis.host }
            registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
        }
    }
}

Testconatiner를 각 테스트 별로 사용할 수 있도록 abstract 클래스로 작성해둔 것이다

이것을 아래와 같이 상속 받아 테스트를 만들게 되면 테스트가 수행될 때 redis에 대한 테스트 컨테이너가 생성되고 테스트가 수행되게 된다

 

그러나 이런 방법으로 여러 테스트를 작성하고 테스트를 일괄로 수행하게 되면 문제가 발생하게 된다

 

이처럼 첫번째 테스트는 잘 수행이 되지만 두번째 테스트에서는 redis container를 찾지 못하여 테스트가 정상적으로 수행되지 않는 모습을 볼 수 있다

 

해결 방법

이를 해결하는 방법으로는 두가지가 있다

 

1. redis를 static으로 선언하지 않는다

@Testcontainers
abstract class RedisTestConfig {
    companion object {
        private val redis = GenericContainer("redis:6.2.7-alpine")
            .withExposedPorts(6379)
            .withReuse(true)
            .apply { start() }

        @JvmStatic
        @DynamicPropertySource
        fun registerRedisProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.data.redis.host") { redis.host }
            registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
        }
    }
}

 

2. @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) 를 테스트 클래스 상단에 작성해준다

@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class AuthControllerV1Test : RedisTestConfig() {

    @Autowired
    lateinit var memberRepository: MemberJpaRepository

    @Autowired
    lateinit var redisTemplate: RedisTemplate<String, Any>

.....
}

 

위 두 방법을 사용하게 되면 앞서 발생한 문제는 해소가 된다

 

그렇다면 문제가 발생할까?

 

문제 원인

문제의 원인은 Spring 테스트의 특성 때문에 발생한다

통합테스트를 할 때는 Spring 컨테이너를 통해 필요한 객체를 생성하고 의존성을 주입해야한다

그리고 이를 위해 @SpringBootTest 어노테이션을 붙인다

이를 통해 테스트가 수행될 때 SpringBootTestContextBootstrapper가 동작하면서 ContextLoader를 통해 context(ApplicationContext)를 로드하는 과정에서 캐싱된 context가 있으면 해당 컨텍스트를 재사용하게 된다

- ApplicationContext를 생성하는 비용이 크기 때문에 재사용

 

@SpringBootTest 동작 흐름

@SpringBootTest
   ↓
SpringBootTestContextBootstrapper
   ↓
SpringBootContextLoader.loadContext()
   ↓
ApplicationContext 생성
   ↓
의존성 주입
   ↓
테스트 수행
------------------
@BeforeEach 실행
   ↓
@Test 실행
   ↓
@AfterEach 실행
------------------
   ↓
컨텍스트 캐시 유지 또는 제거

 

 

컨텍스트를 재사용하면서 컨텍스트 로드 시점에 동작해야하는 아래 설정 함수가 동작하지 못하고 이로 인해 두번째 테스트에서 redis에 연결을 시도하였으나 실패하면서 테스트가 정상 수행되지 못한 것이다

@JvmStatic
@DynamicPropertySource
fun registerRedisProperties(registry: DynamicPropertyRegistry) {
    registry.add("spring.data.redis.host") { redis.host }
    registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
}

 

회고

이번 문제를 해결하면서 Testcontainer는 물론 통합 테스트를 작성함에 있어서 Spring의 테스트 동작 구조에 대해서 조금 들여다 볼 수 있었다

기본적으로 Spring의 동작 구조에 대해서는 계속적으로 접할 기회가 많고 학습의 기회가 많았는데, Spring의 테스트 메커니즘에 대해서는 다소 학습의 기회가 적지 않았나 하는 생각이 든다

반응형

+ Recent posts