서킷브레이커(CircuitBreaker)란

서킷브레이커 패턴이란 외부 서비스에 의한 문제를 방지하기 위해 등장한 디자인 패턴으로 문제가 발생한 지점을 감지하고 실패하는 요청을 계속하지 않도록 방지합니다.
그리고 이를 통해 시스템의 장애 확산을 막고 장애 복구를 도와주며 유저는 불필요하게 대기하지 않게 됩니다.
가정집에 있는 누전차단기가 화재를 막는 것과 비슷하게 CircuitBreaker(직역하면 회로차단기)는 서비스의 장애 전파를 막는다고 이해하면 됩니다.

아래 그림과 같이 ServiceA가 ServiceB를 호출 할 때
ServiceB가 반복적으로 실패한다면 CircuitBreaker 를 Open 하여 ServiceB에 대한 흐름을 차단하는게 서킷브레이커의 역할입니다.
* CircuitBreaker 의 Open 은 흐름을 차단하는 것으로, 흐름을 열어둔다(opened) 라는 의미가 아닙니다.
* 
반대로 CircuitBreaker 의 Closed 상태는 흐름을 허용하는 정상상태를 의미합니다.

이미지 출처 : https://symphony.is/blog/service-resiliency-with-spring-boot-and-resilience4j


"CircuitBreaker 가 무엇인지 그리고 어떤 역할을 하는지 알았으니, 이제 CircuitBreaker의 상태인 Closed/Open/Half Open에 대해 좀 더 알아보겠습니다."

 

서킷브레이커의 3가지 상태

  Closed Open HalfOpen
상황 정상 장애 Open 상태가 되고 일정 요청 횟수/시간이 지난 상황.
Open 으로 상태를 변경할지, Closed 로 상태를 변경할지에 대한 판단이 이루어지는 상황
요청에 대한 처리 요청에 대한 처리 수행.
정해진 횟수만큼 실패할 경우 Open 상태로 변경
외부 요청을 차단하고 에러를 뱉거나 지정한 callback 메소드를 호출 요청에 대한 처리를 수행하고 실패시 CircuitBreaker 를 Open 상태로 변경.
성공시 CircuitBreaker를 Close 상태로 변경

* 서킷브레이커에서 장애판단의 기준(Closed 상태에서 Open 이 되기 위해 카운팅 되는 실패의 기준)은 아래와 같습니다.

1) slow call : 기준보다 오래 걸린 요청

2) failure call : 실패하거나 오류 응답을 받은 요청

* slow call 과 failure call 은 CircuitBreaker의 프로퍼티로 정의되어 있으며 사용자가 특정 값으로 지정할 수 있습니다.

 

"아래는 CircuitBreaker 의 3가지 상태에 대해 이해를 돕기 위한 순서도와 그림입니다."

서킷브레이커 상태 변경

이미지 출처 : https://martinfowler.com/bliki/CircuitBreaker.html

서킷브레이커의 상태는 아래와 같이 변경됩니다.

1. 정상 요청 수행(Closed)

2. 실패 임계치 도달(Closed → Open)

3. 일정시간 소요(Open → Half Open)

4. 요청 수행

     a. 수행 결과 정상 (Half Open → Closed)

     b. 수행 결과 실패 (Half Open → Open) 

 

서킷브레이커 라이브러리 종류

1) Netflix Hystrix

넷플릭스에서 개발한 라이브러리로 MSA 환경에서 분산된 서비스간 통신이 원할하지 않을 경우 각 서비스가 장애 내성과 지연 내성을 갖게하도록 하는 라이브러리
현재는 deprecated 된 상태로 Resilience4j 사용을 권장

 

2) Resilience4j

Netflix Hystrix 로부터 영감을 받아 개발된 Fault Tolerance Library 
Java 전용으로 개발된 경량화된 라이브러리

 

"Netflix Hystrix 공식 doc에서도 Resilience4j 사용을 권장하고 있으니, Hystrix 를 사용할 이유가 없습니다.

Hystrix 에 대해 알아볼 필요 없이 바로 Resilence4j 에 대해 알아보겠습니다."

 

Resilience4j 의 코어 모듈

1) CircuitBreaker : 장애 전파 방지 기능 제공

2) Retry : 요청 실패시 재시도 처리 기능 제공

3) RateLimiter : 제한치를 넘어서 요청을 거부하거나 Queue 생성하여 처리하는 기능 제공

4) TimeLimiter : 실행 시간 제한 설정 기능 제공

5) Bulkhead : 동시 실행 횟수 제한 기능 제공

6) Cache : 결과 캐싱 기능 제공

 

Resilience4j 의 코어 모듈은 위와 같으며 필요한 모듈의 의존성을 설정해 주어 필요한 모듈만 사용할 수 있습니다.

 

Gradle 예시

 
dependencies {
  implementation("io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}")
  implementation("io.github.resilience4j:resilience4j-ratelimiter:${resilience4jVersion}")
  implementation("io.github.resilience4j:resilience4j-retry:${resilience4jVersion}")
  implementation("io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}")
  implementation("io.github.resilience4j:resilience4j-cache:${resilience4jVersion}")
  implementation("io.github.resilience4j:resilience4j-timelimiter:${resilience4jVersion}")
}

Resilience4j 모듈의 우선순위

Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( BulkHead ( TargetFunction ) ) ) ) )

위와 같은 우선순위로 모듈이 적용됩니다. (Retry 모듈이 가장 마지막에 적용됨)

 

이를 알아보기 위해 resilience4j jar의 CircuitBreakerConfigurationProperties, RetryConfigurationProperties 클래스 내부를 살펴보면, 

CircuitBreaker 와 Retry 의 Order 값이 각각 -3, -4 로

별도 처리가 없을 경우 CircuitBreaker 가 Retry 보다 우선으로 적용됨을 알 수 있습니다.

 

CircuitBreakerConfigurationProperties

 
public class CircuitBreakerConfigurationProperties extends
    io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties {

    private int circuitBreakerAspectOrder = Ordered.LOWEST_PRECEDENCE - 3;
    ...
}

RetryConfigurationProperties

 
public class RetryConfigurationProperties extends
    io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties {

    private int retryAspectOrder = Ordered.LOWEST_PRECEDENCE - 4;
    ...
}

CircuitBreakerAspect 

 
@Aspect
public class CircuitBreakerAspect implements Ordered {
   ...
   @Override
    public int getOrder() {
        return circuitBreakerProperties.getCircuitBreakerAspectOrder();
    }
}

AOP 기반하에 동작하므로 우선순위를 바꿔서 적용하고자 할 경우 annotation 방식을 사용하여 layer 를 분리하거나 aspectOrder 속성값을 수정하여 적용할 수 있습니다.

 

Resilience4j Configuration

Resilience4j 의 Configuration 은 yml 파일을 사용하거나, java 코드를 통해 설정할 수 있습니다.

1) yml 파일을 사용한 Config 예시

 
resilience4j.circuitbreaker:
    configs:
        default:
            slidingWindowSize: 100
            waitDurationInOpenState: 10000
            permittedNumberOfCallsInHalfOpenState: 30
            failureRateThreshold: 60
            eventConsumerBufferSize: 10
        custom:
            slidingWindowSize: 50
            permittedNumberOfCallsInHalfOpenState: 10
            ... 생략

2) Java 코드를 통한 Config 예시

 
@Configuration
class CircuitBreakerProvider(
    val circuitBreakerRegistry: CircuitBreakerRegistry,
) {

    companion object {
        const val CIRCUIT_MEMDB: String = "CB_MEMDB"
    }

    @Bean
    fun memDBCircuitBreaker(): CircuitBreaker {
        return circuitBreakerRegistry.circuitBreaker(            
            CIRCUIT_MEMDB, CircuitBreakerConfig.custom()
                .failureRateThreshold(10F)  // 실패비율 10% 이상시 서킷 오픈
                .slowCallDurationThreshold(Duration.ofMillis(500))  // 500ms 이상 소요시 실패로 간주
                .slowCallRateThreshold(10F) // slowCallDurationThreshold 초과 비율이 10% 이상시 서킷 오픈
                .waitDurationInOpenState(Duration.ofMillis(60000))   // OPEN -> HALF-OPEN 전환 전 기다리는 시간
                .minimumNumberOfCalls(5) // 집계에 필요한 최소 호출 수
                .slidingWindowSize(5)    // 서킷 CLOSE 상태에서 5회 호출 도달시 failureRateThreshold 실패비율 계산
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)    // 호출 횟수 기준 계산 (TIME_BASED는 시간 기준)
                .ignoreExceptions(StockManageException::class.java)   // 화이트리스트로 서킷 오픈 기준 ex 관리
                .build()
        )
    }

 

"Resilience4j 모듈 중 가장 많이 사용되는 CircuitBreaker, Retry 모듈의 속성값에 대해 간단히 알아보겠습니다."

Resilience4j CircuitBreaker Property

property설명
failureRateThreshold 실패비율 임계치를 백분율로 설정 해당 값을 넘어갈 시 Circuit Breaker 는 Open상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 50)
slowCallRateThreshold 임계값을 백분율로 설정, CircuitBreaker는 호출에 걸리는 시간이 slowCallDurationThreshold보다 길면 느린 호출로 간주, 해당 값을 넘어갈 시 Circuit Breaker 는 Open상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 100)
slowCallDurationThreshold 호출에 소요되는 시간이 설정한 임계치보다 길면 느린 호출로 계산한다. -> 응답시간이 느린것으로 판단할 기준 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms])
permittedNumberOfCallsInHalfOpenState HALF_OPEN 상태일 때, OPEN/CLOSE 여부를 판단하기 위해 허용할 호출 횟수를 설정 수 (기본값: 10)
maxWaitDurationInHalfOpenState HALF_OPEN 상태로 있을 수 있는 최대 시간이다. 0일 때 허용 횟수 만큼 호출을 모두 완료할 때까지 HALF_OEPN 상태로 무한정 기다린다. (기본값: 0)
slidingWindowType sliding window 타입을 결정한다. COUNT_BASED인 경우 slidingWindowSize만큼의 마지막 call들이 기록되고 집계됩니다.
TIME_BASED인 경우 마지막 slidingWindowSize초 동안의 call들이 기록되고 집계됩니다. (기본값: COUNT_BASED)
slidingWindowSize CLOSED 상태에서 집계되는 슬라이딩 윈도우 크기를 설정한다. (기본값: 100)
minimumNumberOfCalls minimumNumberOfCalls 이상의 요청이 있을 때부터 faiure/slowCall rate를 계산한다. 예를들어, 해당값이 10이라면 최소한 호출을 10번을 기록해야 실패 비율을 계산할 수 있다.
기록한 호출 횟수가 9번뿐이라면 9번 모두 실패했더라도 circuitbreaker는 열리지 않는다. (기본값: 100)
waitDurationInOpenState OPEN에서 HALF_OPEN 상태로 전환하기 전 기다리는 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms])
recordExceptions 실패로 기록할 Exception 리스트 (기본값: empty)
ignoreExceptions 실패나 성공으로 기록하지 않을 Exception 리스트 (기본값: empty)
ignoreException 기록하지 않을 Exception을 판단하는 Predicate<Throwable>을 설정 (커스터마이징, 기본값: throwable -> true)
recordFailure 어떠한 경우에 Failure Count를 증가시킬지 Predicate를 정의해 CircuitBreaker에 대한 Exception Handler를 재정의하는 것이다. true를 return할 경우, failure count를 증가시키게 된다 (기본값: false)

 

Resilience4j Retry Property

property설명
maxRetryAttempts 최대 재시도 수(최초 호출도 포함, 기본값 3)
waitDuration 재시도 할 때마다 기다리는 고정시간 (1초[1000ms], 기본값: 0.5초[500ms])
retryOnResultPredicate 반환되는 결과에 따라서 retry를 할지 말지 결정하는 filter, true로 반환하면 retry하고 false로 반환하면 retry 하지 않습니다. (기본값: (numOfAttempts,Either<throwable, result) -> waitDuration)
retryExceptionPredicate 예외(Exception)에 따라 재시도 여부를를 결정하기 위한 filter, 만약 예외에 따라 재시도해야 한다면 true를, 그 외엔 false를 리턴해야 한다. (기본값: result -> false)
retryExceptions 실패로 기록되는 블랙리스트 예외. empty일 경우 모든 에러 클래스를 재시도 한다. (기본값: empty)
ignoreExceptions 무시되어야 하는 예외(화이트리스트) 즉, 재시도 되지 않아야 할 에러 클래스 리스트이다. (기본값: empty)
failAfterMaxRetries 설정한 maxAttempts 만틈 재시도하고 나서도 결과가 여전히 retryOnResultPredicate를 통과하지 못했을 때 MaxRetriesExceededException 발생을 활성화/비활성화하는 boolean (기본값: false)

 

* 그외 모듈에 대한 속성값이 궁금하시다면 아래의 Resilience4j 공식 document 를 참고해주세요.

 

 

 

참고 : 

https://resilience4j.readme.io/docs/getting-started

 

반응형

 

SIFT Appender 를 사용하여 로그 분기처리

 

[설정]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
 
<configuration>
 
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%-5level] : %msg%n</Pattern>
        </encoder>
    </appender>
 
    <appender name="TST" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%-5level]-%msg%n</Pattern>
        </encoder>
    </appender>
 
    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
        <discriminator>
            <key>discr</key>
            <defaultValue>type</defaultValue>
        </discriminator>
        <sift>
            <appender name="FILE-${discr}" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>/${discr}.log</file>
                <layout class="ch.qos.logback.classic.PatternLayout">
                    <Pattern>%d [%thread] %level %mdc %logger{35} SIFT- %msg%n</Pattern>
                </layout>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>/${discr}_%d{yyyy-MM-dd-HH-mm}.%i.log</fileNamePattern>
                </rollingPolicy>
            </appender>
        </sift>
    </appender>
 
 
    <logger name="jpabook.jpashop.service.OrderService" level="error" additivity="false">
        <!--<appender-ref ref="TST"/>-->
        <appender-ref ref="SIFT"/>
    </logger>
 
    <root level="debug">
        <appender-ref ref="CONSOLE"/>
    </root>
 
 
</configuration>
cs

 

[테스트]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.slf4j.MDC;
 
@Slf4j
public class Sample {
    public void testLog(){
        MDC.put("discr""TYP1");
        //log.
        log.trace("test trace");
        log.debug("test debug");
        log.info("test info");
        log.warn("test warn");
        log.error("test error");
 
        MDC.put("discr""TYP2");
        log.error("test error222");
    }
}
cs

 

[결과]

 

* ThreadPool 사용시 MDC key값을 전달받지 못함.

 : MDC.setContextMap 으로 key 값 전달가능

 참고 : https://stackoverflow.com/questions/6073019/how-to-use-mdc-with-thread-pools

 

* slf4j MDC 를 사용하지 않더라도, strategy pattern 과 같은 디자인 패턴을 사용하고, 분기처리할 구현체에 logger 를 지정하여 패키지 경로마다 별도 로그 파일 및 별도 경로의 로그파일에 로그 적재가 가능할 듯.

 

반응형

 

1. 테스트를 위한 계산기 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package Calculator;
 
public class Calculator {
    
    int add(int i, int j) {
        return i+j;
    }
    
    int subtract(int i, int j) {
        return i-j;
    }
    
    int muliply(int i, int j) {
        return i*j;
    }
    
    int devide(int i, int j) {
        return i/j;
    }
}
 
cs

 

2. junit 테스트 케이스 생성

1) 클래스 생성 버튼 우측의 화살표 버튼을 눌러 JUnit Test Case 선택

 

2) JUnit Test 를 위한 JUnit 클래스 생성

Test 대상 Class뒤에 Test 를 붙여 이름을 짓는게 관례(Calculator+Test)

 

3. junit 테스트 케이스 작성

1) 기본적으로 생성된 test() 메소드 제거 후 아래와 같이 코딩

: add() 메소드 테스트

: assertEquals 는 버전에 따라 deprecated 된 메소드가 있으니 잘 보고 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Calculator;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class CalculatorTest {
 
    @Test
    public void add() {
        Calculator cal = new Calculator();
        cal.add(12);
        assertEquals(""33);
    }
 
}
 
cs

 

2) 지역 변수 cal 을 필드로 빼서 사용하기

11번라인의 Calculator cal = new Calculator(); 를 8번 라인으로 빼서 필드로 사용이 가능하나, @Before를 사용하자.

필드 초기화를 전역에서 명시적으로 수행할 경우, 테스트간 독립성이 보장되지 않는다.

반면 @Before 사용시 add(), devide() 와 같은 테스트 메소드 실행시마다 인스턴스를 초기화 하여 

테스트간 독립성이 보장된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package Calculator;
 
import static org.junit.Assert.*;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
public class CalculatorTest {
 
    private Calculator cal;
    
    @Before
    public void setup() {
        cal = new Calculator();
        System.out.println("setup !!!");
    }
    
    @Test
    public void add() {
        int r = cal.add(23);
        assertEquals("sc"5, r);
        System.out.println("add !!!");
    }
 
    @Test
    public void devide() {
        int r = cal.devide(33);
        assertEquals("success"1, r);
        System.out.println("devide !!!");
    }
 
    @After
    public void teardown() {
        System.out.println("teardown !!!");
    }
    
}
 
cs

 

[실행 결과]

setup !!!
devide !!!
teardown !!!
setup !!!
add !!!
teardown !!!

위처럼 @Before, @After 는 테스트 메소드 실행시마다 호출됨을 확인 할 수 있다.

 

 

참고 : 박재성님의 유투브 강의 https://www.youtube.com/watch?v=tyZMdwT3rIY

반응형

 

https://velopert.com/2350

 

https://mangkyu.tistory.com/55?category=761303

 

https://mangkyu.tistory.com/56

 

https://www.sauru.so/blog/basic-of-oauth2-and-jwt/

반응형

DB연동 및 mybatis 설정이 끝났다면

junit 을 이용하여 설정이 잘 되었는지 확인을 해보자.

 

1. Test 코드 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.jpp;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import com.jpp.main.config.MybatisConfig;
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class MainServiceTest {
    
    @Autowired
    private MainDAO mainDao;
    
    @Test
    public void getUserList() throws Exception {
        
        for(UserVO vo : mainDao.getUserList()) {
            System.out.println(vo.getName());
        }
        
    }
}
 
cs

Spring Framework 에서 Bean 을 가져오기위해 @ContextConfiguration(classes = ~) 을 사용했지만 

Spring Boot 는 @*Test(@SpringBootTest 를 포함한 각종 Test 어노테이션) 사용시 Bean 을 알아서 가져온다고 한다.

참고 및 부분 발췌

※JUnit 4 사용시 @RunWith(SpringRunner.class) 를 꼭 달아주어야 한다.

※ @SpringBootTest(classes = {MainDAO.class, MybatisConfig.class}) 와 같이 테스트에 사용할 특정 Bean 을 지정해 줄 수 도 있다.

※ ApplicationContext (스프링컨텍스트) 에서 DAO bean 을 .getBean으로 가져와 테스트 하는 것 또한 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.jpp;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
import com.jpp.main.config.MybatisConfig;
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MainDAO.class, MybatisConfig.class})
public class MainServiceTest {
    
    //@Autowired
    //private MainDAO mainDao;
    
    @Autowired
    private ApplicationContext context;
    
    @Test
    public void getUserList() throws Exception {
        
        MainDAO dao = context.getBean("mainDAO", MainDAO.class);
        
        for(UserVO vo : dao.getUserList()) {
            System.out.println(vo.getName());
        }
    }
}
 
cs

 

2. Run as JUnit Test 로 실행.

결과 :

 

콘솔 결과 :

 

만약 테스트를 기존에 사용하는 DB가 아닌 별도의 DB 에서 하고자 하는 경우는 어떻게 해야할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.jpp;
 
import javax.sql.DataSource;
 
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class MainServiceTest {
    
    @Autowired
    private MainDAO mainDao;
    
//    @Autowired
//    private ApplicationContext context;
    
    @Test
    public void getUserList() throws Exception {
        
        //MainDAO dao = context.getBean("mainDAO", MainDAO.class);
        
        for(UserVO vo : mainDao.getUserList()) {
            System.out.println(vo.getName());
        }
    }
    
    @TestConfiguration
    @MapperScan(value= {"com.jpp.main.mapper"})
    @EnableTransactionManagement
    public static class TestConfig {
 
            @Bean
            public DataSource customDataSource() {
                return DataSourceBuilder.create()
                                        .url("jdbc:mysql://ip:3306/~?useSSL=false&serverTimezone=UTC")
                                        .driverClassName("com.mysql.cj.jdbc.Driver")
                                        .username("id")
                                        .password("pw")
                                        .build();
            }
            
            @Bean
            public SqlSessionFactory sqlSessionFactory(DataSource dataSource)throws Exception{
                    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
                    sessionFactory.setDataSource(dataSource);
                    
                    Resource[] res = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
                    
                    sessionFactory.setMapperLocations(res);
                    
                    return sessionFactory.getObject();
            }
 
            @Bean
            public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
                return new SqlSessionTemplate(sqlSessionFactory);
            }
    }
}
 
cs

위와 같이 @TestConfiguration 을 테스트 코드 내부에 inner Class로 작성하여 테스트만을 위한 DB 설정을 따로 해줄 수 있다.

테스트 코드 내부에 inner Class 로 @TestConfiguration 작성시 별다른 설정이 없어도 @SpringBootTest 가 해당 config Bean을 인식하여 DI 한다.

inner Class가 아닌 외부에 TestConfig.java 파일을 따로 작성해줄 경우 @Import(TestConfig.class) 어노테이션을 @SpringBootTest 와 함께 달아주어야 테스트 코드에서 주입 및 사용이 가능하다.

ex)

@RunWith(SpringRunner.class)
@SpringBootTest
@Import(TestConfig.class)

 

그럼 이제 위의 테스트 코드를 실행해 보자.

아래와 같은 에러가 발생한다.

 

The bean 'customDataSource', defined in com.jpp.MainServiceTest$TestConfig, could not be registered. A bean with that name has already been defined in class path resource [com/jpp/main/config/MybatisConfig.class] and overriding is disabled.

 

동일한 이름을 가진 Bean이 MybatisConfig.class에 이미 정의되어 있어 TestConfiguration 으로 작성한 Bean 을 등록할 수 없단다.

 

※ 동일한 Bean 의 이름이 존재할 경우 IoC 컨테이너가 Test 코드 내의 TestConfiguration 어노테이션이 달린 Bean 을 우선적으로 인식하여 주입해줄까 싶었는데 그것 까진 안되나보다.

27번라인을 아래와 같이 고쳐주자

@SpringBootTest(classes = {TestConfig.class, MainDAO.class})

: @SpringBootTest 어노테이션에 테스트 DB가 설정되어있는 Bean class와 이를 사용할 class 를 명시

 

다시 테스트 모드로 실행시켜보면 결과는 성공일 것이다.

 

 

※ Test용 Config 를 외부 클래스로 작성하고 테스트코드에서 사용하고자 할 경우

1. 앞서 언급한 @Import 를 사용하는 대신

2. @SpringBootTest(classes = {TestConfig.class}) 와 같이 테스트용 Config 를 명시하여 사용 할 수 도 있다.

   (@Import 를 사용하지 않아도 주입 받아 사용이 가능)

 

※ 단, 위처럼 프로젝트 내에 동일한 이름의 Bean 이 존재할 경우(2개의 DataSource Bean 이 동일한 이름으로 존재) @SpringBootTest(classes = {TestConfig.class, MainDAO.class})와 같이 테스트에 사용할 Bean을 명시하여 사용해야한다. (@Import만 사용시 앞서 살펴보았던 에러와 동일하게, Bean 이 중복 선언된다는 Exception이 발생한다)

 

 

* Controller를 테스트 하는 경우 아래와 같이 가능하다.

1. 테스트 대상 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.jpp.main.controller;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
 
import com.jpp.main.service.MainService;
import com.jpp.main.vo.UserVO;
 
@Controller
public class MainController {
    
    @Autowired
    private MainService mainService;
    
    @PostMapping("/api2")
    @ResponseBody
    public String api(@RequestBody String test) throws Exception {
        String rs = "good";
        
        System.out.println("api2 called !");
        
        return "{\"good\":\"res\"}";
    }
}
 
cs

 

 

2. test class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.jpp;
 
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
 
import com.jpp.main.controller.MainController;
import com.jpp.main.service.MainService;
 
@RunWith(SpringRunner.class)
@WebMvcTest(MainController.class)
public class MainServiceTest2 {
    
    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private MainService mainService;
    
    @Test
    public void api() throws Exception {
        
        
        ResultActions actions = 
                mvc.perform(post("/api2")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content("{\"test\":\"val\"}")
                        ).andDo(print());
        
        actions.andExpect(status().isOk());
    }
    
}
 
cs

 

3. junit 실행 결과

 

 

JUnit 과 관련된 트랜잭션(@Transactional) 부분, Mock 객체 등에 대한 내용은 추가 공부 및 정리 후 작성 하겠다.

 

 

Spring Boot의 Test코드에 대한 설명이 매우 잘 되어 있는 글

 

반응형

Spring Boot 게시판 만들기 2 : DB 연동 및 Mybatis 설정

Spring Boot - ver 2.1.8

Gradle

Java - ver 1.8

 

application.properties

내에 datasource 관련 설정값 선언

1
2
3
4
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://ip:3306/~?useSSL=false&serverTimezone=UTC
spring.datasource.username=id
spring.datasource.password=pw
cs

 

스프링부트 앱을 구동시키는 main 메소드 내의 @SpringBootApplication 을 들어가보면 @EnableAutoConfiguration 어노테이션이 존재.

해당 어노테이션이  프로젝트 내 Bean 들의 DI 주입을 자동으로 설정해준단다.

(Spring Framework 프로젝트의 web.xml 파일 내에 context.xml 를 매핑해주는 과정을 직접 하지 않아도 된다고 한다)

이 이상 자세히 파기엔 내공의 한계가 있어 일단 나중에 더 공부해보자..

 

위와 같이 프로퍼티에 datasource 관련 설정값만 선언해주면

아래와 같은 코드와 같다고 보면 된다.

1
2
3
4
5
6
7
8
9
10
    @Bean
    public DataSource customDataSource() {
 
        return DataSourceBuilder.create()
                                .url("jdbc:mysql://ip:3306/!?useSSL=false&serverTimezone=UTC")
                                .driverClassName("com.mysql.cj.jdbc.Driver")
                                .username("id")
                                .password("pw")
                                .build();
    }
cs

 

다음으로,

SqlSessionFactory 와 SqlSessionTemplate 설정.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.jpp.main.config;
 
import javax.sql.DataSource;
 
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(value= {"com.jpp.main.mapper"})
@EnableTransactionManagement
public class MybatisConfig {
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource)throws Exception{
            SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(dataSource);
            
            Resource[] res = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
            
            sessionFactory.setMapperLocations(res);
            
            return sessionFactory.getObject();
    }
 
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
 
cs

위와 같이 datasource 관련 설정을 application.properties 에 설정하고,

mybatis 관련 설정은 java config 로 설정할 수 있다. (mybatis 는 application.properties 에 설정 불가)

application.properties 에 설정 가능한 값들은 여기를 참고

 

 datasource도 application.properties 대신 java config 로 설정하고 싶다면 아래와 같이 datasource 를 @Bean 으로 설정해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.jpp.main.config;
 
import javax.sql.DataSource;
 
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(value= {"com.jpp.main.mapper"})
@EnableTransactionManagement
public class MybatisConfig {
    
    @Bean
    public DataSource customDataSource() {
        return DataSourceBuilder.create()
                                .url("jdbc:mysql://ip:3306/~?useSSL=false&serverTimezone=UTC")
                                .driverClassName("com.mysql.cj.jdbc.Driver")
                                .username("id")
                                .password("pw")
                                .build();
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource)throws Exception{
            SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(dataSource);
            
            Resource[] res = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
            
            sessionFactory.setMapperLocations(res);
            
            return sessionFactory.getObject();
    }
 
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
 
cs

17 Line : @MapperScan : com.jpp.main.mapper 경로 밑에 있는 mapper Interface 를 스캔하여 빈으로 등록 (mapper Interface 대신 DAO를 사용할 경우 필요 없음)

36 Line : getResources("classpath:mapper/*Mapper.xml");

~Mapper.xml 파일명 기준 모든 sql mapper 를 sqlSessionFactory에 등록

 

 

[mainMapper.xml]

간단한 select 쿼리를 작성하여 Mapper를 작성해준다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
 
<mapper namespace="com.jpp.main.mapper.MainMapper">
 
    <select id="getUserList" resultType="com.jpp.main.vo.UserVO">
        select empno as no, emp_nm as name 
        from emp_info
        limit 10
    </select>
 
</mapper>
 
 
cs

 

* Mapper Interface 사용 vs DAO 사용

1) DAO 사용시

1-1) MainDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.jpp.main.dao;
 
import java.util.List;
 
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
 
import com.jpp.main.vo.UserVO;
 
@Repository
public class MainDAO {
    
   @Resource
    private SqlSessionTemplate sqlSessionTemplate;
    
    private static final String MAPPER_NM = "com.jpp.main.mapper.MainMapper.";
    
    public List<UserVO> getUserList(){
        return sqlSessionTemplate.selectList(MAPPER_NM+"getUserList");
    }
}
 
cs

1-2) MainService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.jpp.main.service;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.jpp.main.dao.MainDAO;
import com.jpp.main.mapper.MainMapper;
import com.jpp.main.vo.UserVO;
 
@Service
public class MainService {
    @Autowired
    private MainDAO mainDao;
    
    public List<UserVO> getUserList() throws Exception {
        return mainDao.getUserList();
    }
}
 
cs

 

 

2) mapper Interface 사용시

2-1) MainMapper

1
2
3
4
5
6
7
8
9
10
11
package com.jpp.main.mapper;
 
import java.util.List;
 
import com.jpp.main.vo.UserVO;
 
public interface MainMapper {
    
    public List<UserVO> getUserList() throws Exception;
}
 
cs

 

2-2) MainService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.jpp.main.service;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.jpp.main.dao.MainDAO;
import com.jpp.main.mapper.MainMapper;
import com.jpp.main.vo.UserVO;
 
@Service
public class MainService {
 
    @Autowired
    private MainMapper mainMapper;
    
    public List<UserVO> getUserList() throws Exception {
        
        return mainMapper.getUserList();
    }
}
 
cs

 

 

 

반응형

1. 우클릭 > New > Spring Starter Project

 

2. 프로젝트명 입력

   Gradle / Maven 선택,

   배포 파일형태 Jar/War 선택

   Java 버전 선택

 

3. Boot Version 선택

   사용할 Library 선택(WEB, JPA 등)

 

4. @SpringBootApplication 어노테이션이 붙은, (main 메소드를 지닌) 클래스의 이름을

Application 으로 수정 밑 실행(Run As > Spring Boot App)

 

5. 프로젝트 우클릭 > Gradle > Refresh Gradle Project

 

 

※ Lombok 설치 (참고)

1) lombok-1.18.10.jar 가 위치한 경로로 이동 (버전은 다를 수 있음)

2) jar 가 위치한 경로에서 shift + 우클릭하여 Powershell 실행

3) java -jar lombok-1.18.10.jar 로 lombok jar 실행 

4) Specify location 버튼을 누른 후 sts.exe(혹은 사용중인 IDE 실행파일) 선택

 

5) 완료 화면

 

6) IDE 재시작

7) sts.ini 파일을 연 후

-vmargs

-javaagent:lombok.jar (약간 다를 수 있음)

위의 내용이 추가 되었는지 확인(sts.exe와 동일한 경로에 위치)

 

참고 :

https://jojoldu.tistory.com/250?category=635883

 

 

반응형

+ Recent posts