<기존>

- Redis를 default config로 사용하면서 해당 Cache의 경우 다음과 같이 key를 정의하도록 keyGenerator를 구현하였다.

1번유형 ) String customKey = target.getClass().getSimpleName() + "_"+ method.getName();

              customKey += buildKey(params[0]);

2번유형 ) 별도의 Key지정 없음

 

- Redis내에 생성된 key값을 확인하면 다음과 같다.

1번유형 ) {methodName}::{customKey}

2번유형 ) {methodName}::SimpleKey [{argements}]

 

<Try>

- prefixKeysWith("Service Name")를 사용하여 공통 Prefix를 추가하는 것

- 의도한 결과는 기존의 Cache Name앞에 prefix를 추가하는 것이었다

- 기대값

1번유형 ) Service Name + methodName::{customKey}

2번유형 ) Service Name{methodName}::SimpleKey [{argements}]

 

- 그러나 실제 결과는 다음과 같았다.

1번유형 ) ServiceName{customKey}

2번유형 ) ServiceNameSimpleKey [{arguments}]

기존의 cache name이 사라지고 prefix부분만 남게되는것

 

<원인>

- 비슷한 이슈를 제기하신분들이 기존에 있습니다.

https://github.com/spring-projects/spring-data-redis/issues/1614

 

RedisCacheConfiguration.prefixKeysWith(…) does not consider cache name [DATAREDIS-1041] · Issue #1614 · spring-projects/spr

Mykyta Bezverkhyi opened DATAREDIS-1041 and commented Assuming I have spring-data-redis application with the following method: @Cacheable( cacheNames = "cacheName", key = "T(java.lan...

github.com

https://github.com/spring-projects/spring-data-redis/issues/1548

 

RedisCacheConfiguration prefixKeysWith override the cacheName [DATAREDIS-975] · Issue #1548 · spring-projects/spring-data-redi

Markfred Chen opened DATAREDIS-975 and commented With spring boot version 2.1.4, I found below code which doesn't serve as prefix, instead it overrides the cacheName passed in. Is it a bug? or ...

github.com

 

return this.computePrefixWith((cacheName) -> {
            return prefix;
        });

소스를 살펴보면 다음과 같습니다. cacheName을 날려버립니다...

 

<결론>

그냥 자체적으로 cache name을 생성하는 로직을 만들어서 사용하도록 하였습니다..

<현상>

- Async API Call 후 응답을 제대로 처리하지 못하는 현상이 있습니다.

- 그 여파로 내부적으로 AtomicInteger를 이용하여 호출Count를 처리하는 로직이 있는데 해당 로직이 수행되지 않아서 버그가 발생하고 있었습니다.

 

<원인>

contents = webClient
                .post()
                .uri(multiCountApiPath)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .header("Authorization","Bearer " + token)
                .body(BodyInserters.fromObject(inputs))
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofMillis(200000))
                .onErrorReturn(null)
                .flux()
                .toStream()
                .findFirst()
                .orElse(null);

        try {
            ...
        } catch (IOException e) {
            ...
        }
        callCount.decrementAndGet();
    
        return CompletableFuture.completedFuture(ret);

- 원래의 의도는 API Call 오류가 발생하였을 경우 null 로 처리하여 빈값을 가져가도록 하는 것이었습니다.

- 그러나 실제로 테스트해보면 try 이후 구문이 수행되지 않고 있습니다.

- API Call에서 오류가 발생했을 경우 null 처리를 잘못하여 flux() 이하 로직이 수행되지 않고 있습니다.

 

<수정사항>

contents = webClient
                .post()
                .uri(multiCountApiPath)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .header("Authorization","Bearer " + token)
                .body(BodyInserters.fromObject(inputs))
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofMillis(200000))
                .onErrorReturn("")
                .flux()
                .toStream()
                .findFirst()
                .orElse("");

        try {
            ...
        } catch (IOException e) {
            ...
        }
        callCount.decrementAndGet();
    
        return CompletableFuture.completedFuture(ret);

     

- 위와 같이 bodyToMono에서 우리가 사용하고자 했던 타입에 맞는 값(e.g String "" )으로 처리해주면 onErrorReturn 이후 로직이 정상적으로 수행되는 것을 확인할 수 있습니다.

 

<추가로 확인해야 할 사항>

- timeout 이 발생했을 때 특정 로직을 수행하도록 handler 가 가능하다면 decrement 로직을 그쪽으로 옮길 수 있을지 검토가 필요합니다.

- onErrorReturn 이후 로직이 Spring내부에서 어떻게 동작하는지 상세한 확인이 필요합니다.

 

<현상>

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: 발생

<코드>

RangeCount rangeCount = new RangeCount(map);
filterDto.setRangeCount(rangeCount);

 

@AllArgsConstructor
@NoArgsConstructor
public class RangeCount {
    public static final int RANGE_SIZE = 10;
    private java.util.Map<Integer, Integer> rangeCount;

    public boolean validateRangeCount(){

        Iterator<Integer> iterator = rangeCount.keySet().iterator();

        while(iterator.hasNext()){
            if( rangeCount.get(iterator.next()) < 0 ){
                return false;
            }
        }

        return true;
    }
}

 

<원인>

- getter 가 없을 경우 해당 멤버변수에 접근할 수 없어서 없는 것으로 판단한다.

<현상>

- 우연히 내부 개발서버 테스트중 발견함

- "ParkUser1" 이라는 사용자가 DB에 있는데 로그인API를 "parkuser1"로 해도 로그인이 됨!

 

<이유>

- 기존에 많은 블로그에서 포스팅된 것처럼 MySQL이 대소문자가 다음과 같이 구분한다.

 VARCHAR : 대소문자 구분 안함

 VARBINARY : 대소문자 구분 함

 BINARY() 함수 : 대소문자를 구분하여 WHERE 이하 절 연산

parkuser1로 검색했는데 ParkUser1 조회완료

 

<해결방법>

1. WHERE 절에 BINARY() 함수를 사용하여 조건을 검색하면 대소문자 구분이 됩니다.

정상적으로 대소문자 구분

2. VARBINARY타입으로 테이블 생성

 

<아키텍처 보완을 위한 추가사항>

- 이번 개발계는 Spring JPA로 진행하면서 테이블이 자동생성 되었고 이때 타입이 VARCHAR로 기본생성되어 발생된 문제였다.

- 클라우드를  사용하면서 대부분의 환경을 자동화하려고 한다.

- 또한 JPA가 지향하는 바 Database 디펜던시를 낮추고 대부분의 로직은 어플리케이션으로 처리한다.

- 확장에 열려있고 변경에는 닫혀있는 디자인 원칙을 적용하는 것은, 설계레벨 뿐만 아니라 아키텍처 레벨에서도 적용되어야 한다. (OCP)

 

UserDetailsService에 다음과 같은 로직을 추가하였다.

대소문자 구분을 하지 않아서 로그인에 성공하더라도 Real값과 일치하는지 한번더 비교한다.

 

테스트 결과 MySQL을 사용할 때 Column Type을 VARCHAR로 사용해도 문제가 없었다.

@Test
    public void updateDeviceTelemetryInterval() throws Exception {

        int deviceId = 1;
        String serviceName = "AAAService";

        //given service info
        ServiceDto serviceDto = new ServiceDto();
        serviceDto.setServiceId(1);
        AzureIotHubEntity azureIotHubEntity = new AzureIotHubEntity();
        azureIotHubEntity.setAzureIotHubConnectionString("");
        serviceDto.setAzureIotHub(azureIotHubEntity);
        //given device info
        DeviceDto deviceDto = new DeviceDto();
        deviceDto.setDeviceId(1);
        //given interval info
        Gson gson = new Gson();
        DeviceDto device = new DeviceDto();
        device.setTelemetryInterval(1);
        String str = gson.toJson(device);

        given(serviceService.findServiceByName(serviceName)).willReturn(serviceDto);
        given(deviceService.findDeviceById(deviceId)).willReturn(deviceDto);

        //when then
        MockHttpServletRequestBuilder createUserRequest = post("/ServiceName/setTelemetryInterval/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(str);

        this.mvc.perform(createUserRequest)
                .andExpect(status().is2xxSuccessful());
    }

이와 같은 테스트코드는 정상적 상황을 테스트하는 상황입니다.

 

만약 해당하는 서비스가 없는 상황에서는 다음과 같은 Exception이 발생하도록 내부적으로 정의했습니다.

@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="No such Service")
public class ServiceNotFoundException extends Exception {

    public ServiceNotFoundException(String message){
        super(message);
    }

    public ServiceNotFoundException(String message, Throwable cause) {
        super(message,cause);
    }
}

이와 같은 Exception이 의도한 것처럼 발생하는지 우리는 일반적으로 TestCode에서 아래와 같은 annotation을 통해서 처리합니다.

@Test(expected = ServiceNotFoundException.class)

그런데 여기서 주의해야 할점이 있습니다!!! 

 

우리가 일반적으로 Spring mvc를 사용할때 위와 같은 형태로 Exception을 정의할 경우, 내부적으로 Exception을 catch하여 밖으로 나가지 않도록 하고 HTTP STATUS코드를 변경하게 됩니다.

따라서 위와 같이 @Test(expected) 를 추가하더라도 Exception은 잡히지 않습니다.

어떻게 하면 내가 의도한 Exception을 잡아낼 수 있을지 Spring 내부소스를 살펴보았습니다.

final class TestDispatcherServlet extends DispatcherServlet {

@Override
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		ModelAndView mav = super.processHandlerException(request, response, handler, ex);

		// We got this far, exception was processed..
		DefaultMvcResult mvcResult = getMvcResult(request);
		mvcResult.setResolvedException(ex);
		mvcResult.setModelAndView(mav);

		return mav;
	}
 

위와 같이 result에 resolvedException에 발생한 Exception정보를 저장하고 있습니다.

그렇다면 아래와 같이 테스트 코드를 작성하면 될것이라고 추측됩니다.

@Test
    public void updateDeviceTelemetryIntervalNonExistService() throws Exception {

        //given
        ServiceDto serviceDto = new ServiceDto();

        Gson gson = new Gson();
        DeviceDto device = new DeviceDto();
        device.setTelemetryInterval(1);
        String str = gson.toJson(device);

        given(serviceService.findServiceByName("TestService")).willReturn(serviceDto);

        //when then
        MockHttpServletRequestBuilder createUserRequest = post("/AAAService/setTelemetryInterval/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(str);

        this.mvc.perform(createUserRequest)
                .andExpect((result)->assertTrue(result.getResolvedException().getClass().isAssignableFrom(ServiceNotFoundException.class)))
                .andExpect(status().is4xxClientError());
    }

테스트 결과 404 NotFound  와 exception의 유형을 모두 확인할 수  있습니다.

 가장 많이 사용하고 있는 Spring Boot + JPA 로  Azure Cosmos DB를 연결하는 작업을 진행해보았다.

https://docs.microsoft.com/ko-kr/java/azure/spring-framework/configure-spring-boot-starter-java-app-with-cosmos-db?view=azure-java-stable


1. maven pom.xml에 아래와 같이 추가하고.

<dependency>

   <groupId>com.microsoft.azure</groupId>

   <artifactId>azure-documentdb-spring-boot-starter</artifactId>

   <version>2.0.4</version>

</dependency>


2. JPA에서 제공하는 CRUD Repository를 이용해서 다음과 같이 작업했다.

public interface DeviceTelemetryRepository extends DocumentDbRepository<DeviceTelemetry, String>{

    List<DeviceTelemetry> findAll();

    @Query(value="SELECT * FROM DeviceTelemetry where deviceId= ?1 and date >= ?2 and date <= ?3", nativeQuery = true)
    List<DeviceTelemetry> findDeviceTelemetryByDeviceIdAndDateGreaterThanAndDateLessThan(@Param("deviceId") int deviceId,
                                                               @Param("from") long from,
                                                               @Param("to") long to    );

}


3. Entity클래스의 경우 테이블에 따라서 잦은 변경이 일어나기 때문에 Controller, Service Layer에서는 DTO클래스를 별도 로 사용하는 것을 권장한다. (디자인 패턴 참고)

@Service
public class DeviceTelemetryService {
@Autowired
private DeviceTelemetryRepository deviceTelemetryRepository;

@Autowired
private ModelMapper modelMapper;

public List<DeviceTelemetryDto> getDeviceTelemetry(int deviceId, long from, long to){
return deviceTelemetryRepository.findDeviceTelemetryByDeviceIdAndDateBetween(deviceId, from, to).stream()
.map(deviceTelemetry -> modelMapper.map(deviceTelemetry, DeviceTelemetryDto.class))
.collect(Collectors.toList());
}

}

4. Controller 까지 완성하여. 테스트하였는데 아래와 같은 오류가 발생한다.

@RequestMapping(path="/api/")
public class DeviceTelemetryController {

@Autowired
private DeviceTelemetryService deviceTelemetryService;


@RequestMapping(value="/{serviceId}/{deviceId}/telemetry", method= RequestMethod.GET)
public @ResponseBody
List<DeviceTelemetryDto> getAllDeviceTelemetry(@PathVariable("deviceId") int deviceId,
@RequestParam("from") long from,
@RequestParam("to") long to) {

return deviceTelemetryService.getDeviceTelemetry(deviceId,from,to);
}
}

java.lang.IllegalArgumentException: unsupported keyword: GREATER_THAN (1): [IsGreaterThan, GreaterThan]
	at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.from(DocumentDbQueryCreator.java:82) ~[spring-data-cosmosdb-2.0.3.jar:na]
	at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.and(DocumentDbQueryCreator.java:56) ~[spring-data-cosmosdb-2.0.3.jar:na]
	at com.microsoft.azure.spring.data.documentdb.repository.query.DocumentDbQueryCreator.and(DocumentDbQueryCreator.java:25) ~[spring-data-cosmosdb-2.0.3.jar:na]
	at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:122) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
	at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
	at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81) ~[spring-data-commons-2.0.10.RELEASE.jar:2.0.10.RELEASE]
	at com.microsoft.azure.spring.data.documentdb.repository.query.PartTreeDocumentDbQuery.createQuery(PartTreeDocumentDbQuery.java:38) ~[spring-data-cosmosdb-2.0.3.jar:na]
	at com.microsoft.azure.spring.data.documentdb.repository.query.AbstractDocumentDbQuery.execute(AbstractDocumentDbQuery.java:25) ~[spring-data-cosmosdb-2.0.3.jar:na]


5. Azure Cosmos DB에 문의하니 다음과 같은 답변을 받았다.

spring-data-cosmosdb 2.0.6을 사용해야 하는데 아무리 찾아도 azure-documentdb-spring-boot-starter에 포함된 최신버전은 2.0.4가 최신버전이다!

다시 문의를 했다. 수동으로 업데이트해야하는가?

결론은 "azure-documentdb는 구 네이빙 버전입니다. 신버전을 사용하세요."


Thanks for filing issue, according to your stack trace, seems you are using version 2.0.3, if you are using Spring Boot 2.0.x, try upgrading to 2.0.6.


You can use the azure-cosmosdb-spring-boot-starter(not azure-documentdb-spring-boot-starter) version 2.0.13, which uses spring-data-cosmosdb 2.0.6. Between should have been supported in 2.0.6, check this issue. Also you can reference the integration tests in this repo for the usage.


6. pom.xml에서 azure-cosmosdb-spring-boot-starter 최신버전으로 변경하였다.

<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-cosmosdb-spring-boot-starter</artifactId>
<version>2.0.13</version>
</dependency>


7. 하지만 Spring JPA의 naming rule에 같은컬럼을 중복해서 사용하면 에러가 발생한다.

BETWEEN 쿼리로 변경!

(GREATER_THAN, BETWEEN등은 위에서 언급한 2.0.6 버전에서 정상지원한다.)



8. Repository 소스 변경(@Query는 없어도 쿼리를 자동으로 생성하여 동작한다)

public interface DeviceTelemetryRepository extends DocumentDbRepository<DeviceTelemetry, String> {

List<DeviceTelemetry> findAll();

// @Query(value="SELECT * FROM DeviceTelemetry where deviceId= ?1 and date >= ?2 and date <= ?3", nativeQuery = true)
List<DeviceTelemetry> findDeviceTelemetryByDeviceIdAndDateBetween(@Param("deviceId") int deviceId,
@Param("from") long from,
@Param("to") long to );
}

9. Azure Cosmos DB에서 정상적으로 데이터를 조회하는 것을 확인할 수 있다.

[

{ "id": "3ebd07c0-0740-466f-acb4-1e04a58cdf1a", "serviceId": 1, "deviceId": 1, "contents": "{\"temperature\":34.797642257199705,\"humidity\":79.18982439419167,\"illuminance\":100}", "date": 1552376519931 }, { "id": "9424f15a-e452-4dcc-8ff6-bc3707b7ec3a", "serviceId": 1, "deviceId": 1, "contents": "{\"temperature\":25.964463142640522,\"humidity\":73.64654000868197,\"illuminance\":100}", "date": 1552376579937 }, { "id": "c72aed1b-b4d0-4338-a21f-ae8b7e5b4eba", "serviceId": 1, "deviceId": 1, "contents": "{\"temperature\":28.32913263660401,\"humidity\":73.40374660917695,\"illuminance\":100}", "date": 1552376639938 }, { "id": "c1571a80-7eb6-49dc-be9f-32457e41f69a", "serviceId": 1, "deviceId": 1, "contents": "{\"temperature\":30.035071643495087,\"humidity\":70.52682127516005,\"illuminance\":100}", "date": 1552376699940 } ]

참고사이트)

1. 위 내용 관련하여 MS Github에서 주고 받은 내용들

https://github.com/Microsoft/spring-data-cosmosdb/issues/347


2. 현재 google 에서 조회하면 가장 상단에 Azure SDK for Java Stable 나오는 페이지

https://docs.microsoft.com/ko-kr/java/azure/spring-framework/configure-spring-boot-starter-java-app-with-cosmos-db?view=azure-java-stable




<개요>

동시에 다수의 Java Process를 기동하여 JDBC Connection을 맺을때 시간이 매우 많이 소요되는 경우가 있습니다.

(timeout이 발생하기도 함)

 

이럴때는 난수발생을 체크해보시기 바랍니다.

 

<내용>

JDBC Driver에서 로그인을 하기 위해서 난수발생을 이용하는데 기본적으로

random을 사용하게 되어있습니다.

 

random에서 waiting이 발생하면서 connection을 맺지 못하는 경우가 있습니다.

이 부분을 urandom을 사용하도록 변경해보시기 바랍니다.

(random에 비해서 urandom은 속도가 빠르고 대신 보안에 취약하다고 보고가 되어있습니다.)

 

 

<방법>

JVM옵션을 아래와 같이 추가합니다.

 

-Djava.security.egd=file:///dev/urandom

 

 

 ps>리눅스 커널버전에 따라서 발생하는 경우도 있으니 같이 확인하시면 좋을 것 같습니다.

Hadoop분산환경에서 FTP연결관련 공부를 하면서 몇가지 테스트 프로그램을 작성했는데..

갑자기 Connection Reset이라는 오류가 발생한다. 구글링을 하면 대부분 Firewall, Network Setting, IPV4, 좀 더나아가서는 Data Connection에 대한 설명들(Active/Passive)이 있다. 

(Data Connection방식에 대한 상세한 설명은 다른 글에서 포스팅할 예정)

그러나.. 며칠동안 나오는 모든 내용을 살펴봤지만 해당하는 부분이 없었다.



<현상>

- Windows IIS FTP 접속시 Data전송요청이나 Command수행 후 재요청하면 Connection Reset이 발생함


원시적인 방법으로 돌아가서 프로그램상의 값을 하나씩 값을 변경해보다가 마침내 원인을 찾았다.



<원인>

- Hadoop에서 사용하고 있는 FTPFileSystem이라는 클래스를 살펴보면 다음과 같이 코딩되어 있다.

client.setFileTransferMode(FTP.BLOCK_TRANSFER_MODE);
client.setFileType(FTP.BINARY_FILE_TYPE);
client.setBufferSize(DEFAULT_BUFFER_SIZE);


이에 관련하여 FTP에 연관되어있는 많은 RFC문서를 볼 수 있는데, 기초가 되는 것이 RFC959 문서이다. 해당 문서를 읽어보면 Transmission Mode에 크게 3종류가 있으며, Hadoop FTPFileSystem은 Block transer mode를 기본으로 사용하고 있다. (https://tools.ietf.org/html/rfc959)


1. Stream Mode : The data is transmitted as a stream of bytes. There is no restriction on the representation type used; record structures are allowed. (가장 기본적인 방식이다.)


2. Block Mode : The file is transmitted as a series of data blocks preceded by one or more header bytes. The header bytes contain a count field, and descriptor code. The count field indicates the total length of the data block in bytes, thus marking the beginning of the next data block (there are no filler bits).


3. Compressed Mode There are three kinds of information to be sent: regular data, sent in a byte string; compressed data, consisting of replications or filler; and control information, sent in a two-byte escape sequence. If n>0 bytes (up to 127) of regular data are sent, these n bytes are preceded by a byte with the left-most bit set to 0 and the right-most 7 bits containing the number n.

compressed data와 그외 정보들로 전송하는 방법이다.


 문제가 되었던 것은 바로 2번 Block Mode!!

리눅스/유닉스 계열의 FTP서버에서는 별 문제가 없지만 IIS 의 FTP에서는 해당 모드를 제대로 지원하지 않아서 계속 EOF 신호를 기다리다가 timeout에 걸려서 server에 socket이 끊어지고 결국 connection reset이 발생하게 된다.

https://technet.microsoft.com/ko-kr/library/cc771040(v=ws.10).aspx

IIS 기반 FTP의 기본 데이터 전송 모드는 스트림입니다. IIS는 현재 블록 또는 압축 데이터 전송 모드를 지원하지 않습니다.

IIS 8.0버전에서 확인해봐도 여전히 Stream Mode만 지원하고 있음.


 현재까지 google이나 naver를 찾아봐도 FTP프로그램 작성시 connection reset현상의 원인에 대해서 FTP Transfer mode에서 해결책을 찾은 경우는 전무하다.

즉 동일한 오류가 발생하더라도 그 원인은 각각 다를 수 있다.



<해결방안>

- 소스를 수정하여 FTPClient 를 Stream Mode로 사용하면 Windows IIS 상에서도 문제없이 전송이 된다.
- 혹은 Block Mode를 사용하고 싶다면 이를 지원하는 FTP Server로 변경하면 된다.

- 왜 Hadoop의 FTPFileSystem에서는 apache net의 FileType이나 FileTransferMode Default값(STREAM_MODE)이 있음에도 불구하고 BLOCK_MODE로 하드코딩을 했을까?
 아마도 Hadoop은 Linux기반의 대용량 처리를 기본으로 하기 때문에 멀티 파일 전송에 유리한 BLOCK_MODE를 택한 것으로 보인다.



<Lesson & Learned>

- 오픈소스들은 결국 어떤 이론과 개념을 코드로 실체화 한 것이기 때문에 사용하기에 앞서서 이해가 필요하다. 원천에 대한 해당 문서를 꼼꼼히 읽어볼 필요가 있다.

- Connection Reset의 경우 매우 다양한 원인에 의해서 발생되지만 로그메시지는 대부분 Socket Read/Write 부분으로 동일하다. 

 따라서 무조건 Google에서 해당 로그를 검색하는 것보다는 Step별로 상세 테스트 케이스를 작성하여 하나씩 검증해나가는 방식을 추천한다. 

 또한 서버환경에 따라서도 다르게 동작하기 때문에 각 구성요소는 동일하게 한다.

아무리 google에 정보가 많아도 일반적이지 않은 오류에 대해서는 무용지물이다.


+ Recent posts