<개요>

  • 테스트의 크기는 되도록이면 작게
  • 각 테스트 단위는 독립적으로 주입할 수 있도록
  • 필요한 것만 주입받고 테스트 해야하며
  • 최대한 빠르게 테스트 실행이 가능해야 함

<내용>

  • Layer
    • Controller
      • Web을 통한 호출시 PathVariable, RequestBody, Header, 인증등 을 담당하는 역할
    • Service
      • Biz Logic 구현
      • Biz Transaction 이 필요할 경우
    • Repository
      • Storage 와 Application 의 다리역할
      • 객체변환, 타입검증
    • POJO
      • 독립된 구현
      • 주로 Input / Output 을 통합 로직 수행
  • 공통사항
    • Given / When / Then 으로 작성하면 편함 (준비-실행-검증)
  • 각 Layer 별 테스트 코드는 다른 레이어가 정상동작한다는 가정으로 테스트하는 것이 원칙
    • Controller Layer
      • WebMvc에 관련된 Context만 로딩 (WebMvcTest)
      • 사용할 Bean들만 TestConfiguration 으로 정의하여 Context의 가동범위를 최소한 으로 한다.  
@RunWith(SpringRunner.class)
@WebMvcTest(BizController.class
)
@Import(SecurityConfig.class)
@ContextConfiguration(classes = SpringSecurityWebAuthTestConfig.class)
public class BizControllerTest {

    @Autowired
    private MockMvc mvc;
    @MockBean
    private BizService bizService;
   
    @Test
    @WithUserDetails("admin")
    public void getAllData() throws Exception{
        List<DataDto> datas = new ArrayList<>();

        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        String dataName = "ttt";
        dataDto.setDataName(dataName);
        dataDto.setUserId(1);
        datas.add(dataDto);

        Page<DataDto> pages=new PageImpl<>(datas, Pageable.unpaged(), 1);

        //given
        given(bizService.findAllDatas(PageRequest.of(0,1))).willReturn(pages);
        //when , then
        this.mvc.perform(get("/datas/all?page=0&size=1"))
                .andExpect(jsonPath("content").isNotEmpty())
                .andExpect(jsonPath("content[0].dataId").value(1))
                .andExpect(status().isOk());
    }
    • 테스트 메소드 작성
      • MockBean : Mockup 대상
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : mvc.perform : 수행
      • then : andExpect : 기대값
  • Service Layer
    • JUnit으로만 테스트 (Spring Mvc 필요없음)
    • 테스트 대상인 Service 만 Inject, 나머지는 Mock
@RunWith(MockitoJUnitRunner.class)
public class BizServiceTest {
    @Mock
    private BizRepository dataRepository;
    @Mock
    private ModelMapper modelMapper;
    @InjectMocks
    private BizService bizService;

    @Test
    public void createService() throws Exception {
        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        dataDto.setUserId(1);
        dataDto.setDataName("text");
        
        DataEntity dataEntity = new DataEntity();
        dataEntity.setDataId(1);
        dataEntity.setUserId(1);
        dataEntity.setDataName("text");
				
        //given
        given(modelMapper.map(dataDto, DataEntity.class)).willReturn(dataEntity);
        given(modelMapper.map(dataEntity, DataDto.class)).willReturn(dataDto);
        //when
        DataDto result = dataService.createData(dataDto);
        //then
        Assert.assertEquals(dataDto, result);

    }
      • 테스트 메소드 작성
        • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
        • when : 테스트 대상
        • then : assertEquals ( expected, actual)
  • Repository Layer
    • DataJpaTest 관련된 Context만 로딩
    • 사용할 Bean들만 정의하여 가동범위 최소한으로
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @Import({EnableEncryptablePropertiesConfiguration.class, JasyptConfig.class, SpringSecurityWebAuthTestConfig.class, TestJpaAuditingConfig.class})
    public class NewsRepositoryTest {
    
        @Autowired
        private NewsRepository newsRepository;
    
        @Test
        public void findAllByEnabled(){
            //given
            Pageable pageable = PageRequest.of(0,10);
    	//when
            Page<NewsEntity> newsEntityPage = newsRepository.findAllByEnabled(true, pageable);
    	//then
            Assert.assertEquals(1, newsEntityPage.getTotalElements());
        }
    • 테스트 메소드 작성
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : 테스트 대상
      • then : assertEquals ( expected, actual )
    • Local Test의 경우 H2 나 기타 메모리DB로 기동될때마다 테스트 데이터를 넣어놓으면 독립적인 테스트가 가능하기 때문에 편리하다.
  • 독립 Module
    • Context 기동없이 가능
    public class EmailValidationTest {
    
        @Test
        public void validation(){
            Pattern codePattern = PatternValidator.ValidationType.EMAIL.getMyPattern();
    
            Matcher matcher = codePattern.matcher("terst@gmail.com");
            Assert.assertTrue(matcher.matches());
    
            matcher = codePattern.matcher("test-1@naver.com");
            Assert.assertTrue(matcher.matches());
    • Bean주입없이 그냥 Java new로 POJO 테스트
public class JasyptTest {

	@Test
	public void encryptDecrypt() throws Exception {
		PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
		SimpleStringPBEConfig config = new SimpleStringPBEConfig();
		config.setPassword("");
		config.setAlgorithm("PBEWithMD5AndDES");
		config.setKeyObtentionIterations("1000");
		config.setPoolSize("1");
		config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
		config.setStringOutputType("base64");
		encryptor.setConfig(config);

		String raw = "abcdefg";
		String encrypted = encryptor.encrypt(raw);

		String decrypted = encryptor.decrypt(encrypted);
		Assert.assertEquals(raw , decrypted);
	}

 

Git Remote Repository에 Push하기전에 Local Test Case를 모두 통과하는지 반드시 확인해야 한다.

<개요>


우리가 많이 들어본 콘웨이 법칙이다.

- 조직/조직구조의 커뮤니케이션 구조가 소프트웨어의 구조를 결정한다.

우리가 일반적으로 사용하고 있는 MVC구조도 사실 여기에서 영향을 받았다. (UI - 벡엔드 - 데이터)  그래서 비지니스 적으로는 응집력이 낮기 때문에 다른 여러가지 시도가 이루어지고 있는 것이다.


 
한편 역 콘웨이 법칙도 존재한다.


- 소프트웨어 아키텍처 구조가 회사 조직구조를 결정한다.
그래서 만들고 싶은 소프트웨어의 방향에 따라서 조직구성을 인위적으로 하는 것이다. 그리고 그 구성을 매우 유연하게 자주 바꿀 수 있도록 한다.
 


콘웨이법칙은 워낙 개발자들 사이에서 진리를 통하는 법칙인지라 다양한 인용과 해석이 존재하는데 재미있는 표현 몇가지를 찾아봤다.

- 하나의 컴파일러를 만들기 위해서 4개의 팀이 조직된다면, 4단계로 빌드하는 컴파일러가 나오게 된다.

- N명의 그룹이 코볼컴파일러를 구현한다면 N-1단계가 될것이다. (왜냐하면 한명은 관리자가 되어야 하기 때문에)

- 영웅개발자가 만든 소프트웨어는 기발할지 모르지만 에러도 무지 많다.

- 시스템 설계를 자유롭게 하고 싶다면 조직역시 변화에 대비해야 한다.

조직의 구조때문에 만들 수 없다고 여기고 있는 더 나은 설계가 존재하는가

 

<Self 적용>

요즘은 이런 생각을 한다.
MSA라는 구조가 매우 일반적이되었는데 이것은 특정 소프트웨어 조직의 커뮤니케이션 구조라기보다는 이 세상의 일반적인 동작방식과 유사하다는 생각이 들었다. 

- 아주 예전에 작성했던 글
https://icthuman.tistory.com/entry/IT%EC%8B%9C%EC%8A%A4%ED%85%9C%EA%B3%BC-%ED%98%84%EC%8B%A4%EC%84%B8%EA%B3%84%EC%9D%98-%EA%B4%80%EA%B3%84

 

IT시스템과 현실세계의 관계

IT시스템의 구현은 현실세계와 밀접한 관련이 있다고 생각합니다.짧지않은 기간동안 공부하고 경험한바를 바탕으로 마구 써내려가봅니다. =======================================================================

icthuman.tistory.com

- Rebecca Wirfs-Brock 선생님 께서 Nature of Order 를 SW에 비교하여 설명하신 내용
(Nature of Order 라는 자연적 질서에 대해 서술한 책이 있는데 매우 재미있다.)
https://www.youtube.com/watch?v=NZ5mI6-tNUc

Design Matters - Rebecca wirffs-brock

 

1. Message, Event

개인을 각각의 서비스로 상상해보면 일반적인 의사소통을 하면 전달하는방식 (언어, 글쓰기 등)은 유사하지만 그 안에 담기는 내용은 다르며, 같은 메시지도 해석하여 동작하는 방식이 다르다.
 유사한 내용을 다루어 본 사람끼리 더 잘 통하고 이해하며, 내가 잘 모르는 내용에 대해서는 반만 듣고 반은 버린다.
때로는 상대의 공격적인 언행에 대해서는 그냥 내가 필터링을 하거나 한귀로 듣고 한귀로 흘려버리기도 한다.
 
또  어느날 어떤 사람이 컨디션이 평상시와 다르면 말을 더 많이 하거나, 혹은 말을 하지 않기도 하며
가끔은 누군가 괜찮은지 안부인사를 묻기도 한다.
 
2. Service
개인을 각 서비스로 또 상상해보자.
서비스가 동작하는 것은 누군가에게 어떠한 기능을 제공해주기 위해서 존재하기도 하지만 스스로의 목적을 추구하기도 한다.
누군가를 위해서 어떤 일을 해주기도 하고 혹은 누군가가 해주는 어떤 일을 받기도 해서 내가 원하는 바를 달성한다.
혼자서 모든일 을 처리할 수 없기 때문에 다양한 타인/조직, 물건,생물등 소통하는 것은 당연한 것이다.
 
3. Role
맡고 있는 역할과 책임이 존재한다. 
누군가의 스승, 누군가의 동료, 누군가의 배우자. 
각각의 역할에는 기대하는 바가 있고 그 기대하는 바가 적절히 충족되어야 상대방이 만족한다.
만약 그 기대하는 바가 완벽하게 충족되지 않는다면 스스로 방법을 찾아보거나 부족한 부분을 대신할 수 있는 다른 것을 찾게 된다.
 
4. Error / Fault / Failure
Error는 언제나 발생한다. 늦잠을 잘 수도 있고, 과식을 할 수도 있고, 물건을 놓칠 수도 있다.
Error가 가끔은 Fault로 연결된다. 지각을 하기도 하고, 소화가 안되고, 물건이 땅에 떨어진다. (놓쳐다가 잡으면 안떨어진거니까..)
그리고 이것들이 누적되면 Failure로 연결되며 우리는 타격을 입게 된다.
성적을 망친다던지, 앓아눕던지, 소중한 물건을 망가뜨리거나..
 
실수를 막기 위해서 시계알람을 울리거나, 미끄럼 방지스티커를 부착하는 것 같이 작은 안전장치가 큰 불이익을 막을 수 있다.
 
5. Waiting
전자제품이 고장났다. ->  AS센터에 전화를 한다. -> 전화를 안받는다. 받을 때까지 전화를 건다. ->
전화를 받았다. -> 담당AS기사를 확인해보고 10분뒤에 전화를 준다고 한다. -> 10분동안 전화를 기다린다. -> 전화가 다시 온다.
글로만 읽어도 답답하고 에너지소모가 많다.
 
요즘은
전자제품이 고장났다 -> App을 통해서 접수한다.  -> 내 할일 한다.
잠시 후 담당AS기사 방문일정이 알람으로 온다. -> 확인하고 다시 내 할일 한다.
 
물론 매우 중요한 일은 여전히 기다려야 한다. e.g) 인증, 결제 ARS
 

<정리>

우리의 이러한 생활을 관찰하다보면 Software Architecture에서의 해답점과 연결점을 많이 찾아낼 수 있다.

 

1. Protocol

- 주고 받는 메시지의 규약을 정하고, 그에 해당하는 메시지를 받았을 때 수행하는 동작을 서로 약속한다. (e.g HTTP, TCP/IP, Json 등)

- Health Check를 통해서 각 노드/서비스의 상태를 주기적으로 확인한다.


 
2. Component / Sequence

- 최종 얻기 위한 결과물을 위해서 필요한 구성요소를 정의하고 (Component)

- 어떠한 순서로 요청하여 받은 결과물을 활용할 것인지 그려본다. (Sequence)

 

3. Exception Catch / Retry / Side-car

- 명시적으로 일어날 수 있는 오류에 대해서는 사전에 처리를 하고 예방한다.

- 일시적인 오류에 대해서는 다시 시도해보고

- 보다 큰 오류로 전파되는 것을 미리 막아둔다. (나만 죽는게 낫다)

 
4. Async / Non-blocking / Timeout

- 반드시 모든 작업이 동시에 끝나야 할 필요는 없다. (Async)

- 오래 기다려야 하는 작업(내가 제어할 수 없는 일) 은 맡겨놓고 다른 작업을 한다. (Non-blocking)

- 완료되었다는 것을 인지하면 그 때 후속작업을 수행한다. (callback, future)

- 동시에 끝나야 하는 작업이 있을 수도 있고, 응답을 반드시 확인해야 하는 경우도 있다. (Sync)

- 언제까지 끝난다고 장담할 수 있는 일은 없다. 적절한 시점에는 포기해야 한다. (unbounded delay, timeout)
 

<참조>

https://johngrib.github.io/wiki/Conway-s-law/ 
https://wiki.wooridle.net/NatureOfOrder

 

NatureOfOrder - Wooridle Wiki

ChristopherAlexander가 질서의 본질적인 특성 (Nature of Order)에 대해서 저술한 책. PatternLanguage에 대해서 정리한 그는, 패턴을 마구잡이로 적용하는 경우가 많은 것을 발견하고, 어떤 원리에 따라 구성

wiki.wooridle.net

 

1. Authorization Code

Authorization Code

- 권한 부여 승인을 위해서 자체생성한 Authorization Code를 전달하는 방식

- 기본이 되는 방식

- Refresh Token 사용이 가능

 

2. Client Credentials

- 클라이언트의 자격증명만으로 Access Token을 획득하는 방식

- 가장 간단한 방식

- 자격증명을 안전하게 보관할 수 있는 클라이언트에서만 사용되어야 함

- Refresh Token 사용 불가능

 

3. Implicit Grant

- 자격증명을 안전하게 저장하기 힘든 클라이언트에게 최적화된 방식

- Access Token이 바로 발급되기 때문에 만료기간을 짧게 설정할 필요가 있음

- Refresh Token 사용 불가능

 

4. Resource Owner Password Credentials Grant

- username, password로 Access Token을 받는 방식

- 클라이언트가 외부 프로그램일 경우 사용하면 안됨

- 권한서버, 리소스서버, 클라이언트가 모두 같은 시스템에 속해 있을때 사용해야 함 (대부분 비권장)

- 요청이 성공한 클라이언트는 메모리에서 자격증명을 폐기해야 함

- Refresh Token 사용 가능

 

<정리>

  Authorization Code Client Credentials Implicit Grant ROPC Grant
App 유형 SPA, Web App, Mobile Web Server Browser User / Client
권장사항 일반적     비권장
Refresh Token O X X O
Redirect Uri 필수 필수 권장 X

 

 

<참조>

- https://learn.microsoft.com/ko-kr/azure/active-directory/develop/v2-oauth2-auth-code-flow

- https://www.rfc-editor.org/rfc/rfc6749

<개요>

- Designing Data-Intensive Applications 를 읽고 그 중 분산시스템의 오류처리에 대한 부분 정리

https://icthuman.tistory.com/entry/The-Trouble-with-Distributed-Systems-1

 

<내용>

6. Timeouts and Unbounded Delays

- Timeout이 Fault를 감지하는 확실한 방법이라면 얼마로 설정해야 할까?

- Long timeout : Node가 죽었다는 것을 인지하기 위해서는 오래 기다려한다. (사용자는 기다리거나 에러메시지를 확인한다.)

- Short timeout : fault를 빠르게 감시할 수 있지만 잘못 인식할 있는 위험이 있다. (spike같이 일시적인 현상도 있기 때문에)

 

* 문제점

- 작업이 살아있고 수행하는 중이었는데 Node를 죽은 것으로 간주한다면, 작업이 종료되기 전에 다시 수행해서 중복 수행될 수 있다.

- 만약에 노드가 죽었다면 다른 노드에 이 사실을 전달해야하고 이것은 다른 노드나 네트워크에 추가적인 부하상황으로 이어질 수 있다.

  이미 시스템이 고부하상황이었고 노드가 죽었다고 잘못 판단할 경우 상황은 더 악화될 수 있다.

  특히 죽은 것이 아니라 overload로 인해서 응답이 지연되고 있었다면 (죽은게 아니었다면) 에러가 계속 전파되어서 모든 노드가 죽었다고 판단하면서 모든 작업이 멈춰버릴 수도 있는 극단의 상황도..

 

*아름다운 상상으로 접근 (fictitious system)

- 모든 패킷이 d 시간내에 전달된다고 하고, 살아있는 노드는 해당 request를 처리할때 r 시간내에 가능하다면 

- 모든 성공적인 request는 response time이 2d + r내로 들어올 것이고

- 해당 시간동안 응답을 받지 못한다면 network 나 node 가 동작하지 않는 것으로 간주할 수 있다.

- 그렇다면 2d + r 은 reasonable timeout 으로 사용할 수 있다.

 

*현실

- 불행하게도 대부분의 시스템은 이를 보장할 수가 없다.

- Asynchronous network 는 unbounded dealy를 가지고 있다.( 최대한 빨리 도착하도록 노력은 하지만.. upper limit이 존재하지 않는다는 점)

- 대부분의 서버 구현에서는 maximum time을 보장할 수가 없다. (Response time guarantees)

- Failure Detection을 위해서는 시스템이 빠르다는 것만으로는 충분하지 않다. Timeout이 너무 짧으면 위에서 살펴본것처럼 spike등이 발생하였을 때 system off-balance

 

* Network congestion and queueing

- 네트워크의 패킷 지연현상은 대부분 queueing 때문이다.

a. 여러 노드에서 동시에 한 곳으로 패킷을 보내면, 네트워크 스위치는 Queue에 채우고 Destination network link에 하나씩 넣어줘야 하는데, 패킷을 얻기 위해서 잠시 기다려야할 수도 있고 만약 Queue가 가득 차게되면 packet 이 drop되어서 다시 보내야한다.

b. 패킷이 Desination 머신에 도착했을 때 Cpu core가 모두 사용중이면 request처리준비를 할때까지 OS에서 queued된다.

c. 가상환경을 사용중이라면 OS가 종종 중지된다. 이 시간동안 VM은 network로부터 데이터를 소비할 수 없기 때문에 VM monitor에 의해서 queued (buffered) 된다.

d. TCP는 flow control을 수행하여 과부하를 방지하도록 속도를 제어하기도 한다. 또 TCP는 손실되는 패킷에 대해서 재전송을 해야하기 때문에 Delay를 두면서 timeout to expired 나 retransmitted packet을 기다린다.

(그래서 우리에게 이러한 기능이 필요없다면, 즉 유실방지,유량제어가 필요없고 지연된 데이터는 가치가 없는 상황이라면 UDP를 사용하는 것이 더 좋은 선택이 된다. 예를 들어서 VoIP call)

 

* 환경적인 문제

 Public Cloud 같이 여러 고객들이 같이 사용하는 네트워크 자원 (link, switch) ,각 NIC, CPU 등은 공유가 된다. 또 MapReduce같은 작업들은 병렬처리를 진행하면서 네트워크를 사용하기도 한다.

* 네트워크의 round trip시간의 분포를 적절하게 측정하여 예상되는 Delay 변동성을 결정하고,  Application의 특성을 고려하여 Failure detection delay  과  Risk of premature timeouts 간의 적절한 Trade Off를 결정할 수 있습니다.

* 더 좋은 방법은 상수값의 timeout보다는 Response time 과 Jitter를 지속적으로 측정하고 관찰된 응답시간의 분포에 따라서 Timeouts을 자동으로 조정하는 것입니다.

-  Phi Accrual failure detector (for example, Akka and Cassandra)

- TCP retransmission timeouts

<개요>

- Designing Data-Intensive Applications 를 읽고 그 중 분산시스템의 오류처리에 대한 부분 정리

- 언제나 늘 그렇듯이 새로운 개념이라기 보다는 얼마나 체계적으로 잘 정리해서 핵심을 간직하는가에 집중

 

<내용>

1. Faults and Partial Failures

- Single Computer에서 작업을 할 경우 same operation은 same result를 만들어낸다. (deterministic)

- 우리가 수 대의 컴퓨터에서 동작하는 소프트웨어를 개발할 때 (즉, 네트워크로 연결되어 있는 상태)는 이와는 다르다.

- 분산처리 시스템에서는 예측할 수 없니 특정 부분에서 문제가 발생하는 경우가 있는데 이를 partial failure 라고 부른다.

- partial failure의 어려운 점은 non-deterministic이라는 점이다. 여러 노드와 네트워크에 걸쳐서 무언가를 했을때 예상과는 다르게 성공하기도 하고 실패하기도 한다는 점인데 이러한 부분이 분산처리 시스템을 어렵게 만든다.

 

2. Building a Reliable System from Unreliable Components

- unreliable한 구성요소를 가지고 어떻게 하면 reliable한 시스템을 만들수 있을까? 몇 가지 아이디어를 소개한다.

a. error-correcting code를 가지고 어느부분이 오류가 발생했는지 체크하고 정확하게 전송할 수 있다.

b. TCP/IP : IP는 unreliable하다. 사라지거나 늦게 도착하거나 혹은 중복되거나.. 순서보장도 안된다. 여기에 TCP가 Transpory layer의 역할을 상위에서 해줌으로 사라진것은 다시 보내주고, 중복된것은 재거해주며, 발송순서에 맞게 재조립을 해준다.

물론 언제나 한계점은 존재한다. 데이터의 양이나, 혹은 네트워크 자체의 지연현상등은 커버하기 어렵다. 하지만 까다로운 일반적인 문제를 제거하고 나면 훨씬 쉬워지는것도 사실이다.

 

3. Unreliable Networks

- 분산처리에서는 shared-nothing구조를 유지하는 것이 일반적이다. 왜냐하면 다른 machine끼리는 네트워크로 연결이 되며 각자의 disk나 메모리등에는 접근할 수 없기 때문이다.

- 인터넷 환경에서 각 데이터 센터는 멀리 떨어져 있기 때문에 더욱 이러한 구조인데 대부분 비동기처리로 진행이 된다.(asynchronous packet networks)

- 메시지를 보내기는 하지만 언제 도착할지, 도착이 하기는 할지.. 보장할 수가 없다. 

 a. request가 사라질 수 있다.
 b. request가 늦게 도착할 수 있다.
 c. 원격 node가 죽을 수도 있고
 d. 원격 node가 잠깐 멈출수도 있고. (gc)
 e. 원격 node가 request처리를 했지만 response가 사라질수 있다.
 f. 원격 node가 request처리를 하고 reponse를 보냈지만 늦게 도착할수도 있다.
- 종합해 보면 메시지를 보낸쪽에서는 무엇이 문제인지 알수 있는 방법이 없다.

* 이러한 문제를 처리하는 방법이 일반적으로 "Timeout" 이다. 기다리는 것을 포기하는 것이다.

수신 node가 받았는지, 메시지가 사라졌는지는 여전히 알 수 없지만...

 

4. Network Faults in Practice

- 위에서 살펴본것처럼 reliable한 시스템을 만드는 법은 완벽한것은 없다. (왜? 네트워크는 여전히 불안하기 때문에..)

- 결국 소프트웨어에서 이를 처리할 수 있도록 해야한다. (이것을 지속적으로 테스트하도록 만든 프레임워크가 바로 Chaos Monkey)

 

5. Detecting Faults

- Faults를 감지해야 이후의 처리를 할수 있으니 감지하는 법을 살펴보자.

- 불확실성을 통해서 작동여부를 판단하는 것은 어려우니, 거꾸로 특정상황에서 작동하지 않는다는 것을 명시적으로 알려주는 FeedBack !

 a. 대상 포트에서 수신 프로세스가 없는 경우 OS에서 RST 또는 FIN을 전송한다.

 b. 노드 프로세스가 죽었지만 OS가 여전히 실행중이면 script로 다른 노드에 알릴 수 있다. (e.g Hbase)

 c. 데이터 센터에서 NIC 관리 기능을 사용중이면 하드웨어 수준으로 감지 할 수 있다.

 d. 라우터가 해당 IP에 연결할 수 없다고 확신하면 ICMP destination unreachable 패킷으로 응답할 수 있다. 

 

- 이렇게 빠르게 feedback처리를 하면 매우 유용함을 알 수 있다. 하지만 이것도 역시 신뢰할 수는 없다! (네트워크이니까)

- 결국 request가 성공적으로 처리되었는지는 application레벨에서의 positive response를 받는 것이 필요하다.

 

 

<개요>

- 최근 S3를 File,정적데이터 제공등의 목적으로 사용중인데 max-age 헤더에 대한 내용이 궁금하여 상세한 내용을 파악해 보았다. (HTTP 완벽 가이드 중 일부 내용 정리)

- HTTP 프로토콜은 통신의 많은 부분을 차지하고 있으며 OSI 7 Layer상 최상단에 위치한다.

 

- 즉, 해당 계층을 잘 활용하면 실제 사용자에게 전달되는 데이터를 컨트롤 할 수 있으며

 특히 캐시를 잘 활용하면 응답시간을 상당히 개선할 수 있다.

(다만 브라우저나 클라이언트등에서 일으키는 강제 Refresh에 대해서도 고려할 필요가 있다.)

 

<내용>

1. Cache-Control 헤어

- 클라이언트는 Cache-Control 요청헤더를 사용하여 만료제약을 조정할 수 있다.

Cache-Control: max-stale
Cache-Control: max-stale=<s>
캐시는 신선하지 않은 문서라도 자유롭게 제공할 수 있다.
<s>가 지정되면, 클라이언트는 만료시간이 <s>만큼 지난 문서도 받아들인다.
완화
Cache-Control: min-fresh=<s> 클라이언트는 지금으로부터 적어도 <s>초 후까지 신선한 문서만을 받아들인다. 엄격
Cache-Control: max-age=<s> 캐시는 <s>초보다 오랫동안 캐시된 문서를 반환할 수 없다.
나이가 유효기간을 넘어서게 되는 max-stale지시어가 함께 설정되지 않는 이상 더엄격하게 만든다.
엄격
Cache-Control: no-cache-Pragma:no-cache 이 클라이언트는 캐시된 리소스는 재검사하기 전에는 받아들이지 않을 것이다. 엄격
Cache-Control: no-store 이 캐시는 저장소에서 문서의 흔적을 최대한 빨리 삭제해야 한다.
그 문서에는 민감한 정보가 포함되어 있기 때문이다.
엄격
Cache-Control: only-if-cached 클라이언트는 캐시에 들어있는 사본만을 원한다.  

* 이는 완벽한 시스템이 아니다.

* 유효기간을 먼 미래로 설정한다면, 어떤 변경도 캐시에 반영되지 않을 것이다. 

* 유효기간을 사용조차 하지 않아서 문서가 얼마나 오래 신선할 것인지 캐시가 알기 어려운 경우도 많다.

* 이는 DNS와 같은 많은 인터넷 프로토콜에서 사용되는 "ttl"의 기법의 한 형식이다.

다행히 HTTP에는 DNS와 달리 클라이언트가 만료일을 덮어쓰고 강제로 재로딩할 수 있는 메커니즘이 있다.

 

2. 나이와 신선도 계산

- 캐시된 문서가 제공되기에 충분히 신선한지 알려면 두 가지값을 계산할 필요가 있다.

- 바로 캐시된 사본의 나이와 신선도의 수명이다.

- 충분히 신선한가?

 $나이 < $신선도 수명

 

다음 사항이 주 고려사항이다.

- 캐시는 문서응답이 어디에서 왔는지 알 수 없기 때문에 헤더를 통해서 계산해야 한다.

- 신선도 수명은 해당 문서의 나이가 신선도 수명을 넘었다면 제공하기에 충분하지 않다고 판단하는 것으로 문서의 유효기간 뿐만 아니라 영향을 주는 클라이언트의 모든 요청을 고려해야 한다. (e.g 네트워크 지연) 

 

A. 겉보기 나이는 Date헤더에 기반한다.

$겉보기_나이 = max(0, $응답을 받은 시각 - $Date_헤더값)

$문서가_캐시에_도착했을때의_나이 = $겉보기 나이

 

- 모든 컴퓨터가 똑같이 정확한 시간을 갖고 있다면 단순히 현재시간 - 문서를 보낸 시간으로 계산할 수 있다.

- 하지만 모든 시계는 동기화되지 않으며 심지어 오차가 심할 경우에는 음수가 되기도 한다. max(0, )처리가 필요한 이유

- 이러한 문제를 클럭 스큐라고 한다. 

 

B. 점층적 나이

- 그래서 우리는 이에 대한 대응방법으로 프락시나 캐시를 통과할때마다 Age헤더에 상대적인 나이를 누적해서 더하도록 한다.

- 이 방법은 서버간의 시간비교나 종단 시간비교가 필요없기 때문에 유용하다. (내부시계를 사용하여 체류시간 계산)

- 문서가 각 어플리케이션에 머무른시간과 네트워크 사이를 이동한 시간만큼 Age헤더의값을 늘려야 한다.

- 비 HTTP/1.1 장치의 경우 헤더를 고치거나 삭제하기 때문에 유의해야 하며, 따라서 Age 헤더는 상대나이에 대한 모자란 추정값의 상태로 본다.

 

$보정된_겉보기_나이 = max($겉보기_나이, $Age헤더값)

$문서가_캐시에_도착했을때의_나이 = $보정된_겉보기_나이

 

*신선한 컨텐츠를 얻는 것이 목적이기 때문에 max를 이용해서 보수적으로 계산한다.

 

C. 네트워크 지연에 대한 보상

- 트랜잭션은 느려질 수 있다. (캐시의 주된 동기)

- 매우 느린 네트워크, 과부하 서버, 트리팩등의 발생은 문서의 나이 추정에 대한 추가 계산이 필요하다.

- Date헤더는 언제 문서가 원 서버를 떠났는지 나타내주고 ( *프락시/캐시는 절대 이 헤더를 수정해서는 안된다), 캐시로 옮겨가는 중 얼만큼 시간이 걸렸는지 말해주지 않는다.

- 서버 <> 캐시 왕복지연 시간을 계산하는 것은 상대적으로 쉽다. (왜나하면 요청시각과 도착시간을 알고 있으니까)

 

$겉보기_나이 = max(0, $응답을 받은 시각 - $Date_헤더값)

$보정된_겉보기_나이 = max($겉보기_나이, $Age헤더값)

$응답_지연_추정값 = ($응답을_받은_시각 - $요청을_보낸_시각)

$문서가_캐시에_도착했을때의_나이 = $보정된_겉보기_나이 + $응답_지연_추정값

 

D. 최종 나이계산

- 이 응답이 캐시에 한번 저장되면, 나이를 더 먹게 된다.

- 그 문서의 현재 나이를 계산하기 위해서 그 문서가 캐시에 얼마나 오랫동안 머물렀는지 알아야 한다.

 

$나이 = $문서가_캐시에_도착했을때의_나이 + $사본이_얼마나_오래_우리의_캐시에_있었는지

  캐시된 문서의 나이
서버   요청네트워크지연 서버가 처리하는 시간 응답네트워크지연      
캐시 요청한_시각       응답을_받은_시각 캐시에 체류한 시간 현재_시각
클라이언트             클라이언트가_요청한_시각

 

<정리>

- HTTP에서는 문서의 나이와 신선도를 계산하여 캐시를 제공한다.

- 신선도 수명은 서버와 클라이언트의 제약조건에 의존한다.

- 인터넷의 특성상 클럭스큐와 네트워크 지연이 발생하며 문서의 나이를 계산할때 이를 고려한 방법들이 존재한다.

- 다음 글에서는 신선도를 계산하는 알고리즘에 대해서 정리한다.

<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 이전글에서 Weak Isolation Levels 중 Read Commited, Snapshot Isolation(Repeatable Read)에 관련된 내용을 살펴보았고

 이번 글에서는 Preventing Lost Updates, Write Skew (Phantoms) 에 대해서 살펴본다.

- 이전글 https://icthuman.tistory.com/entry/Transactions-2

 

Transactions #2 (Atomicity, Isolation)

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리. - 이전글 https://icthuman.tistory.com/entry/Transactions-개념정리-1 Transactions #1 (Basic) - Designing Data Intensive Applications 를 읽고 그

icthuman.tistory.com

 

<내용>

Weak Isolation Levels

 

Preventing Lost Updates

- read commited 와 snapshot isolation은 주로 동시 쓰기가 있는 경우, 읽기전용 트랜잭션의 볼수 있는 내용을 보장하는 것이다.

- 이전 글에서는 두 트랜잭션이 동시에 쓰는것에 대해서는 다루지 않았고, 특정 유형의 쓰기-쓰기 에 대해서만 살펴봤다.

- 몇 가지 다른유형의 충돌을 살펴 볼텐데 그 중 가장 유명한 것이 Lost Updates 이다.

- 아래 그림을 살펴보자.

TimeLine 1 2 3 4
User #1 getCounter 42 + 1 setCounter 43  
Data 42 42 43 43
User #2   getCounter 42 + 1 setCounter 43
 

- 두 Client간의 race condition이 발생하였다. 주로 App이 read - modify - write의 사이클을 가지고 있을때 발생한다.

- 즉 write가 발생하기 전에 그 사이에 일어난 modification을 포함하지 않기 때문이다.

 

Solutions

Atomic write Operations

- 많은 DB가 atomic update operations를 제공하기 때문에 App에서 해당 로직을 구현하지 않는 것이 좋다. 일반적인 Best Solution

 예) UPDATE counters SET val = val + 1 WHERE key = 'foo';

- 일반적으로 DB내부에서 execlusive lock을 통해서 구현하기 때문에 (update할때 읽을 수 없다!) "Cursor Stability 유지" 

- 다른 방법으로 all atomic operations를 single thread 에서 수행하는 방법도 있다. (성능 고려)

 

Explicit Locking

- DB에서 해당 기능을 제공하지 않는다면 App에서 명시적으로 update 될 object를 잠그는 방법이다. 

- 잠금을 수행후 read - modify - write 를 수행할 수 있으며, 다른 트랜잭션이 같은 object 에 접근할 경우에 첫 번째 read - modify - write 가 완료될 때 까지 강제로 대기한다.

예 )

BEGIN TRANSACTION

SELECT * FROM ... WHERE ... FOR UPDATE;  (해당쿼리로 반환된 all rows lock !)
UPDATE ... 

COMMIT;

 

Automatically detecting lost updates

- atomic operations & locks : read - modify - write 를 순차적으로 하여 lost updates 를 막는다.

- 대안 : 병렬 수행을 허락하고, Transaction Manager 가 lost update를 감지하면, 트랜잭션을 중단하고 강제로 read - modify - write 를 retry 한다!

- 장점 : DB가 이 검사를 효율적으로 수행할 수 있다는 것. with Snapshot Isolation

PostgreSQL repeatable read automatically detect
and abort the offending transaction
Orale serializable
SQL snapshot isolation
MySQL, InnoDB repetable read X

 

Compare-and-Set

- 우리가 CAS연산이라고 부르는 방법이다. DB가 Transcations를 제공하지 않는 atomic compare-and-set을 찾는 것이다. (Single-object writes)

- 마지막으로 값을 읽은 후 값이 변경되지 않았을때에만 업데이트가 발생할 수 있도록 허용하는 것이다.

- 만약 변경이 일어났다면? read-modify-write연산을 재시도한다. 반드시!

 

주의! Conflict replication

- Locks and Compared and Set은 Single up-to-date, copy of the data를 가정한다.

- replicated DB에서는 여러 노드에 복사본이 존재하고, 데이터 수정이 다른 노드에서 발생할 수 있기 때문에 다른차원의 접근이 필요하다.  즉, 다시 말하면 multi leader 또는 leaderless replication에서는 write가 동시에 발생하고, 비동기 연산이 있다면 보장할 수 없다. (Linearizability)

- 대신 "Detecting Concurrent Writes" 챕터에서 살펴본 내용처럼 concurrent writes 가 충돌된 값의 버전들을 생성하고 (App 또는 별도의 자료구조활용), 이러한 충돌을 versions를 통해서 reslove , merge하는 방법이 가능하다.

- Atomic Operations는 영향을 받지 않는다. (특히 Commutative한 Actions이라면 !)

- 슬프게도.. 많은 replicated DB에서는 기본값으로 Last Write Wins 이다.

 

<정리>

- 개인적으로 매우 유익했던 챕터이다. 결국 두 개 이상의 동시쓰기가 발생한다면 해결방법은 아래와 같이 정리할 수 있다.

 1) 해당 사이클을 통째로 묶는다.

 2) 동시수행을 제한한다. ( Lock or Single Thread )

 3) 일단 진행시켜! 에러나면 다시 시도

 

- 멀티 노드를 가지는 Database라면 여러 곳에서 동시다발적으로 데이터에 대한 복제 / 연산이 일어나기 때문에

 1) Single Leader를 통해서 제어하던지(Hbase 같은)

 2) 마지막에 Write한 값으로 저장

 3) 별도의 Application이나 자료구조를 활용하여 충돌버전을 관리하고 resolve / merge 

- 그래서 대부분의 분산병렬처리 오픈소스 진영에서 zookeeper를 사용하고 있는 듯 하다.

 

- 다음 글에서는 이 글에서 다루지 못한 Isolation Level ( Write Skew, Phantoms read )을 좀 더 자세히 살펴보고 분산환경의 Consistency 에 대해서 정리하도록 해야겠다.

<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 이전글 https://icthuman.tistory.com/entry/Transactions-개념정리-1

 

Transactions #1 (Basic)

<개요> - Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리. - 기존에 일반적인 내용들 (e.g RDB기준), 새로 추가되는 개념들 (e.g NoSQL)을 포함하여 정리 <내용> 1. Trans..

icthuman.tistory.com

 

Single-Object and Multi-Object Operation

 

Atomicity

- error 가 발생했을때 transaction은 중단이 되어야하고, 변경되었던 내용은 삭제가 되어야 한다.

- 부분적인 실패가 발생하면 안되는 것으로 이해하면 된다.

- 키워드는 all or nothing !

 

Isolation

- 동시에 수행되고 있는 transactions는 서로에 간섭하지 않아야 한다.

- 가장 많이 나오는 예제로 다음을 살펴본다.

- emails에는 우리가 받은 이메일이 저장되고, mailboxes에서는 읽지않은 메일의 수가 저장된다고 가정할때.

TimeLine 1 2 3 4
User#1 Req INSERT INTO emails
VALUES(2,'Hello', true)
    UPDATE mailboxes
SET unread=unread+1
WHERE recipient_id=2
User#1 Res OK     OK
User#2 Req   SELECT FROM emails
WHERE recipient_id='2'
SELECT * FROM mailboxes
WHERE recipient_id='2'
 
User#2 Res   'Hello',true 0  

 - emails, mailboxes 데이터간의 불일치가 발생하게 된다.

- 이러한 현상을 Dirty Read라고 하며 커밋되지 않은 데이터를 읽기 때문에 발생하는 현상이다.

 (흔히 말하는 가장 낮은 isolation level 0 : UNCOMMITED_READ)

 

Weak Isolation Levels

 

Read Committed

No dirty reads

- commit된 데이터만 읽어야 한다.

No dirty writes

- commit된 데이터만 overwrite한다.

 

COMMIT이 수행된 데이터만 읽도록 하여 Dirty Read를 막는다. 

 

Implementing read committed

- READ_COMMITED를 실제로 구현하는 방식은?

- 수정하려고 할때 lock을 얻는다. (row, document, table level )

 - 오직 한 트랜잭션만이 lock을 보유한다. 앞 트랜잭션이 끝날때까디 대기하는 것이 원칙

- Dirty Read를 방지하기 위해서 읽을 때 lock을 얻고, 읽고난 뒤 lock을 해제하는 방식은 응답시간에 악영향을 미친다.

 -> 객체들의 전/후 값을 기억하고, 트랜잭션이 진행되는 동안에 다른 트랜잭션에서 해당 객체를 읽을때에는 이전값을 제공하는 방식으로 개선할 수 있다.

 

Snapshot Isolation and Repeatable Read

Implementing snapshot isolation

- READ COMMITED의 방식대로 하면 Dirty Read의 문제를 해결할 수 있으나 아래와 같은 문제가 여전히 존재한다.

TimeLine 1 2 3 4
User#1 SELECT * FROM accounts
where id=1
    SELECT * FROM accounts
where id=2
Account#1 Account #1 = 500 Account #1 = 600    
Account#2 Account #2 = 500   Account #2 = 400 Account #2 = 400
Transfer   UPDATE accounts
SET bal = bal+100
WHERE id=1
UPDATE accounts
SET bal=bal-100
WHERE id=2;
COMMIT;
 

- User#1이 계좌를 조회했을때 각각 500, 400으로 조회가 되어 총 잔액이 900인 상태가 된다. ( 100원이 사라짐 )

- 이러한 상태를 Read Skew, Non Repeatable Read 라고 한다.

- 다시 재조회를 하면 문제가 없지만 다음과 같은 상황에서 주의해야한다.

 A) Backup : 대부분의 작업이 오래 걸리며, 일부 백업은 이전버전 데이터, 일부 백업은 최신버전 데이터를 가져가게 되면 불일치가 영구적으로 발생하게 된다.

 B) Analytics : 전체적인 통계작업등의 정합성이 맞지 않는 상황이 발생하게 된다.

 

이러한 현상을 해결하기 위해서 일반적으로 Snapshot Isolation이 사용된다.

- 아이디어는 해당 트랜잭션 시작시 Database에서 커밋된 모든 데이터를 보고 , 이후 다른 트랜잭션에 의해서 데이터가 변경되더라도 각 트랜잭션에서는 이전 데이터만 표시되는 형태이다.

- Read-Only Long query (백업,분석) 에서 유용하게 사용될 수 있다.

 

Implementing snapshot isolation

- Read Commited 처럼, Dirty Write 를 방지하기 위해서는 write lock이 필요하다.

- 하지만 읽기에서는 lock이 필요하지 않음으로 성능을 향상시킬 수 있는 포인트가 된다. 

 (Readers never block writers, Writers never block Readers) -> Lock경합없이 쓰기를 처리하면서 장시간 실행되는 Read Query를  처리할 수 있다.

 

MVCC

- Dirty Read를 방지하기 위해 사용되는 유형을 일반화 시킨 것

- 다양하게 진행되고 있는 트랜잭션들이 서로 다른 시점에서 상태를 확인하기 위해서는 결국 Object의 여러 커밋된 버전을 유지해야 한다.

- Database가 Read Comiited만 제공하고 Snapshot Isolation을 제공하지 않는 다면 단순하게 두 가지 버전으로 접근하면 된다.

 the commited version / the overwritten-but-not-yet-commited version

- 일반적으로 Snapshot Isolation을 지원한다면 Read Commited에서도 MVCC를 사용한다.

- 전체 트랜잭션에 대해서 동일한 Snapshot을 사용함으로 Snapshot Isolation을 적용하게 된다.

- 이 때 내부적으로는 어느 transaction에 의해서 데이터가 created,deleted되었는지 기록하는 방식으로 접근하며

 update 역시 create / delete의 형태로 관리하게 된다. 

 

Repeatable read and naming confusion

- Snapshot Isolation의 경우 매우 유용한 Isolation Level임에도 불구하고 많은 Database에서 각자의 이름과 방식으로 구현하고 있다.

- 예를 들어서 Oracle : serializable, PostgerSQL/MySql : Repetable Read

- 그 이유는 SQL 표준이 만들어 질때에는 snapshot isolation의 개념이 포함되지도 않았으며 만들어 지지도 않은 상태였기 때문..

- 결국 이 책에서는 Isolation Level의 표준 정의자체에 결함이 있다고 이야기한다. (모호함, 부정확함)

 특히 우리가 표준이라고 부르는 것들에는 (e.g RFC문서) 구현에 독립적이어야 하는데 그렇지 못하며, 각 Database들이 표면적으로는 표준화 되어있지만 실제로 guarantee 하는 부분에도 많은 차이가 있다.

실제로 과거에 어플리케이션 개발을 하면서 JDBC Driver의 각 구현체의 차이에 따라서 의도치 않은 결과나 버그를 얻은 적이 있었는데 아마도 DB의 영역만큼 각 벤더사에 특화된 부분이 있을까 싶다.

 

<정리>

- Database가 READ_COMMITED, SNAPSHOT ISOLATION (REPEATABLE_READ) 을 통해서

- 동시에 Write연산들이 수행되는 상황 속에서, Read only Transaction에게 어떤 방식으로 데이터를 제공하여

- 그것들을 guarantee 할 수 있는지에 대해서 정리해봤다.

- 다음 글에서는 동시에 Write연산들이 수행되면서 발생하는 문제인 Lost Updates 에 대해서 정리하도록 한다.

+ Recent posts