OAuth 2.0 Resource Server에서 JWT 토큰을 검증하고 권한을 처리하는 방법에 대해 알아보겠습니다. Spring Security를 사용하여 Resource Server를 구성하고, JWT 토큰의 scope와 roles 클레임을 권한으로 변환하는 과정을 살펴볼 것입니다. 또한, 자주 발생하는 SSL 관련 오류와 그 해결 방법에 대해서도 다룰 예정입니다.

 

Authorization_code vs Client_credentials

OAuth 2.0의 Authorization Code 방식과 Client Credentials 방식은 서로 다른 사용 사례와 흐름을 가지고 있습니다. 두 방식의 주요 차이점은 다음과 같습니다:
  • Authorization Code 방식:
    • 사용자 개입이 필요한 인증 흐름
    • 웹 애플리케이션이나 모바일 앱에서 주로 사용
    • 리소스 소유자(사용자)의 동의를 얻어 액세스 토큰을 발급
    • 보안성이 높고 refresh token을 사용할 수 있음
  • Client Credentials 방식:
    • 클라이언트 애플리케이션이 직접 자신의 자격 증명으로 인증
    • 서버 간 통신이나 백그라운드 작업에 주로 사용
    • 사용자 컨텍스트 없이 클라이언트 자체의 권한으로 액세스 토큰을 얻음
    • 간단하지만 사용자 특정 데이터에 접근할 수 없음

 

OAuth2 Authorization Server Setup

Spring Boot와 Spring Security를 사용하여 OAuth 2.0 Authorization Server를 구현하기 위해서는 다음과 같은 핵심 설정이 필요합니다:
  • spring-boot-starter-oauth2-authorization-server 의존성을 추가합니다
  • @EnableAuthorizationServer 어노테이션을 사용하여 OAuth 2.0 권한 부여 서버 구성을 활성화합니다
  • AuthorizationServerConfigurerAdapter를 상속받는 구성 클래스를 생성하고, 클라이언트 세부 정보, 토큰 저장소, 보안 제약 조건 등을 설정합니다
  • ClientDetailsServiceConfigurer를 사용하여 클라이언트 ID, 시크릿, 권한 부여 유형, 스코프 등을 정의합니다
  • JWT 토큰을 사용하는 경우, JwtAccessTokenConverter TokenStore를 구성하여 토큰 생성 및 검증 로직을 커스터마이즈할 수 있습니다
  • 사용자 인증을 위해 UserDetailsService를 구현하고, 비밀번호 인코딩을 위한 PasswordEncoder를 설정합니다
이러한 설정을 통해 기본적인 OAuth 2.0 Authorization Server를 구현할 수 있으며, 필요에 따라 추가적인 커스터마이징이 가능합니다.

 

Configuring Resource Server with JWT

 
JWT를 사용하는 리소스 서버 구성은 Spring Security의 OAuth 2.0 지원을 통해 간단히 설정할 수 있습니다. 주요 단계는 다음과 같습니다:
  • application.yml 파일에 spring.security.oauth2.resourceserver.jwt.issuer-uri 속성을 설정하여 JWT 발급자 URI를 지정합니다
  • @EnableWebSecurity 어노테이션과 함께 SecurityFilterChain 빈을 구성하여 JWT 인증을 활성화합니다
  • JwtDecoder 빈을 커스터마이즈하여 토큰 유효성 검사 로직을 추가할 수 있습니다
  • 필요한 경우 JwtAuthenticationConverter를 구현하여 JWT 클레임을 Spring Security의 권한으로 매핑합니다
이러한 설정을 통해 리소스 서버는 Authorization Server에서 발급한 JWT를 검증하고, 토큰에 포함된 스코프나 역할에 따라 접근 제어를 수행할 수 있습니다
 
 
What is Scope and Role
OAuth 2.0에서 scope와 role은 접근 제어를 위한 중요한 개념이지만, 그 용도와 적용 방식에 차이가 있습니다:
  • Scope: 클라이언트 애플리케이션이 사용자 리소스에 접근할 수 있는 범위를 정의합니다. 예를 들어, 'read:profile', 'write:email' 등으로 세분화된 권한을 나타냅니다.
    • Scope는 OAuth 2.0 프로토콜의 표준 부분으로, 인증 서버에서 관리됩니다.
  • Role: 사용자의 조직 내 역할이나 권한 수준을 나타냅니다. 예를 들어, 'admin', 'user', 'manager' 등이 있을 수 있습니다
    • Role은 주로 애플리케이션 내부에서 정의되고 관리됩니다.
적절한 사용:
  • Scope는 클라이언트 애플리케이션의 권한을 제한하는 데 사용합니다. 예: 'read:books'
  • Role은 사용자의 전반적인 권한 수준을 정의하는 데 사용합니다. 예: 'librarian'
두 개념을 조합하여 더 세밀한 접근 제어를 구현할 수 있습니다. 예를 들어, 'librarian' 역할을 가진 사용자에게만 'write:books' 스코프를 허용하는 방식으로 사용할 수 있습니다
 
 

Handling Roles and Scopes in Tokens

OAuth 2.0 토큰에서 역할(roles)과 범위(scopes)를 처리하는 것은 리소스 서버의 중요한 기능입니다. Spring Security에서는 JwtAuthenticationConverter를 사용하여 이를 구현할 수 있습니다:
  • JWT 토큰의 'scope' 클레임을 Spring Security의 권한으로 자동 변환합니다.
  • 커스텀 'roles' 클레임을 처리하려면 JwtAuthenticationConverter를 확장하여 구현합니다.
  • GrantedAuthoritiesMapper를 사용하여 클레임을 세분화된 권한으로 매핑할 수 있습니다.
  • 보안 구성에서 @PreAuthorize 또는 hasRole() 메소드를 사용하여 엔드포인트별 권한을 설정합니다.
이러한 방식으로 토큰의 역할과 범위를 효과적으로 처리하여 세밀한 접근 제어를 구현할 수 있습니다.

Customizing Token Claims

JWT 토큰의 클레임을 커스터마이즈하는 것은 OAuth 2.0 인증 서버에서 중요한 기능입니다. Spring Security에서는 OAuth2TokenCustomizer 인터페이스를 구현하여 이를 수행할 수 있습니다
 
- @Bean 메서드를 통해 OAuth2TokenCustomizer를 구현합니다.
- customize 메서드 내에서 context.getClaims()를 사용하여 JWT 클레임에 접근합니다.
- claims.put() 메서드로 커스텀 클레임을 추가하거나 기존 클레임을 수정합니다.
- 사용자의 권한이나 역할을 클레임으로 추가하려면 context.getPrincipal()에서 정보를 추출합니다

 

이 방법을 통해 리소스 소유자의 추가 정보나 애플리케이션 특정 데이터를 토큰에 포함시킬 수 있어, 리소스 서버에서 더 세밀한 접근 제어가 가능해집니다.
 

Resolving Errors

OAuth 2.0 구현 시 JwtDecoder와 SSL 관련 오류를 해결하는 방법은 다음과 같습니다:
  • JwtDecoder 오류: NimbusJwtDecoder.withJwkSetUri()를 사용하여 JWK Set URI를 명시적으로 설정합니다.
    • 이때 URI가 올바른지 확인하고, 필요한 경우 커스텀 RestTemplate을 구성하여 추가적인 헤더나 인증을 처리할 수 있습니다.
  • SSL 인증서 오류: 개발 환경에서는 server.ssl.key-store-type=PKCS12 server.ssl.key-store=classpath:keystore.p12를 설정하여 자체 서명된 인증서를 사용할 수 있습니다.
  • 프로덕션 환경에서는 신뢰할 수 있는 인증 기관에서 발급한 유효한 SSL 인증서를 사용해야 합니다.
이러한 설정을 통해 대부분의 JwtDecoder 및 SSL 관련 오류를 해결할 수 있으며, 안전하고 신뢰할 수 있는 OAuth 2.0 인증 흐름을 구현할 수 있습니다.
 
 

Setting Up JWK URI for Validation

  • application.yml 파일에 spring.security.oauth2.resourceserver.jwt.jwk-set-uri 속성을 추가합니다.  이 URI는 일반적으로 https:///.well-known/jwks.json 형식을 따릅니다.
  • 보안 구성 클래스에서 JwtDecoder 빈을 커스터마이즈하여 JWK URI를 명시적으로 설정할 수 있습니다
    • JWT 토큰 검증을 위한 JWK (JSON Web Key) URI 설정은 OAuth 2.0 리소스 서버 구현의 중요한 부분입니다.
이러한 설정을 통해 리소스 서버는 Authorization Server에서 제공하는 공개 키를 사용하여 JWT 토큰의 서명을 검증할 수 있으며, 토큰의 무결성과 신뢰성을 보장할 수 있습니다.

 

개요


AWS RDS와 DynamoDB의 비용 및 성능 비교는 데이터베이스 선택 시 중요한 고려사항입니다. 이 분석에서는 두 서비스의 비용 구조, 성능 특성, 그리고 대규모 쓰기 작업 시나리오에서의 비용 효율성을 살펴보았습니다. 또한 고가용성을 위한 중복 구성 시의 비용과 Spring Boot와의 연동 방법에 대해서도 논의하였습니다.

 

AWS RDS vs DynamoDB 비용비교

 
AWS RDS와 DynamoDB의 비용 비교에서, 25백만 건의 500바이트 레코드(총 12.5GB)를 기준으로 분석한 결과, DynamoDB가 RDS보다 상당히 높은 비용을 보였습니다. DynamoDB의 월간 비용은 $937.5로 추정되며, 이는 주로 쓰기 작업에 따른 비용입니다. 반면 RDS의 월간 비용은 $198.24로, 인스턴스 비용과 쓰기 작업 비용을 포함합니다. 이러한 차이는 DynamoDB의 쓰기 중심 요금 체계와 RDS의 인스턴스 기반 요금 구조의 차이에서 비롯됩니다.
      • DynamoDB: 쓰기 작업당 $1.25/백만 건, 스토리지 비용 $0.25/GB/월
      • RDS: 인스턴스 비용(예: db.t3.medium) + 쓰기 작업 비용 $0.20/백만 건

고가용성 구성 시, DynamoDB의 비용은 $1,406.25로 증가하며, RDS Multi-AZ는 $297.36로 증가합니다

선택은 애플리케이션의 요구사항, 확장성 필요, 그리고 운영 팀의 역량을 고려하여 이루어져야 합니다. DynamoDB는 글로벌 확장성과 관리 용이성에서 우위를 보이며, RDS는 복잡한 쿼리와 트랜잭션 지원에 더 적합합니다.
 
 

추가적으로 비용에서 고려할 수 있는 부분

AWS 데이터베이스 서비스의 비용을 고려할 때, 다음과 같은 추가적인 요소들을 염두에 두어야 합니다:
  • 데이터 전송 비용: AWS 리전 간 또는 인터넷으로의 데이터 전송에 따른 추가 비용이 발생할 수 있습니다.
  • 백업 및 복구: RDS의 경우 자동 백업과 수동 스냅샷에 대한 추가 스토리지 비용이 발생할 수 있으며, DynamoDB는 온디맨드 백업과 특정 시점으로의 복구(PITR) 기능에 대한 비용이 추가될 수 있습니다.
  • 성능 최적화: RDS의 경우 쿼리 최적화를 통해 성능을 향상시키고 비용을 절감할 수 있습니다. 예를 들어, AI 기반 최적화 도구를 사용하여 쿼리 성능을 23배까지 향상시킨 사례가 있습니다.
  • 서버리스 옵션: Amazon Athena와 같은 서버리스 쿼리 서비스를 사용하면 데이터 스캔량에 따라 비용이 청구되며, S3 Express One Zone 스토리지 클래스를 활용하여 쿼리 성능을 최대 2.1배 향상시킬 수 있습니다.
이러한 요소들을 고려하여 총소유비용(TCO)을 산정하고, 애플리케이션의 요구사항에 맞는 최적의 데이터베이스 솔루션을 선택해야 합니다.
 
 

Spring Boot 환경에서 DynamoDB 를 사용할때 설정해야 하는 부분

Spring Boot 환경에서 DynamoDB를 사용할 때는 다음과 같은 주요 설정을 고려해야 합니다:
  • 의존성 추가: spring-boot-starter-data-dynamodb와 AWS SDK 의존성을 pom.xml에 추가합니다.
  • DynamoDB 클라이언트 구성: AmazonDynamoDB 빈을 생성하고, 리전, 엔드포인트, 인증 정보를 설정합니다.
  • 리포지토리 인터페이스 정의: @EnableDynamoDBRepositories 어노테이션을 사용하여 DynamoDB 리포지토리를 활성화하고, CrudRepository를 확장한 인터페이스를 생성합니다.
  • 엔티티 매핑: @DynamoDBTable, @DynamoDBHashKey, @DynamoDBRangeKey 등의 어노테이션을 사용하여 Java 객체를 DynamoDB 테이블에 매핑합니다.
  • 트랜잭션 관리: DynamoDB의 제한된 트랜잭션 지원을 고려하여, 필요한 경우 @Transactional 어노테이션을 사용하되 주의가 필요합니다.
이러한 설정을 통해 Spring Boot 애플리케이션에서 DynamoDB를 효과적으로 사용할 수 있으며, 개발 생산성을 높일 수 있습니다.
 
 

DynamoDB 사용과 Application Integration에서 고려사항

DynamoDB를 애플리케이션에 통합할 때 성능과 비용 최적화를 위해 고려해야 할 주요 사항들은 다음과 같습니다:
  • 지연 시간 관리: DynamoDB는 일반적으로 10-20ms의 낮은 지연 시간을 제공하지만, 반복적인 다중 항목 작업의 경우 일관된 평균 성공 요청 지연 시간을 보장합니다.
  • 데이터 모델링: 애플리케이션의 액세스 패턴에 맞춰 효율적인 파티션 키와 정렬 키를 설계하여 읽기/쓰기 성능을 최적화합니다.
  • 배치 작업 활용: 여러 개의 개별 요청 대신 BatchGetItem 또는 BatchWriteItem 작업을 사용하여 처리량을 향상시키고 비용을 절감합니다.
  • 글로벌 테이블 고려: 다중 지역 배포가 필요한 경우, 글로벌 테이블을 사용하여 지연 시간을 줄이고 데이터 일관성을 유지할 수 있습니다.
  • 캐싱 전략: Amazon DynamoDB Accelerator(DAX)를 활용하여 읽기 성능을 향상시키고 DynamoDB 요청 비용을 절감합니다.
  • 비용 모니터링: AWS Cost Explorer를 사용하여 DynamoDB 사용량을 지속적으로 모니터링하고 필요에 따라 용량을 조정합니다.

이러한 고려사항들을 적절히 적용하면 DynamoDB를 효율적으로 활용하여 애플리케이션의 성능을 최적화하고 운영 비용을 절감할 수 있습니다.


 

DynamoDB 비용 최적화 전략

DynamoDB 비용 최적화를 위해서는 다음과 같은 전략을 고려할 수 있습니다:
  • 적절한 용량 모드 선택: 온디맨드 용량 모드는 트래픽 예측이 어려운 경우에 유용하며, 프로비저닝된 용량 모드는 일정한 트래픽 패턴에 더 경제적입니다
  • 스토리지 클래스 최적화: 자주 액세스하지 않는 데이터의 경우 Standard-Infrequent Access(IA) 스토리지 클래스로 전환하여 스토리지 비용을 절감할 수 있습니다.
  • 효율적인 쿼리 설계: Scan 작업 대신 Query 작업을 사용하고, 적절한 인덱스를 설정하여 읽기 용량 단위(RCU) 사용을 최소화합니다.
  • 데이터 수명 주기 관리: TTL(Time to Live) 기능을 활용하여 불필요한 데이터를 자동으로 삭제하고 스토리지 비용을 절감합니다.
  • 모니터링 및 최적화: AWS Cost Explorer를 활용하여 비용 구조를 분석하고, 필요에 따라 용량을 조정합니다.

이러한 전략을 적용하면 DynamoDB 사용 비용을 크게 절감할 수 있으며, 특히 대규모 데이터를 다루는 경우 더욱 효과적입니다.


<개요>

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

<내용>

  • 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에서는 문서의 나이와 신선도를 계산하여 캐시를 제공한다.

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

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

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

+ Recent posts