diff --git a/content/HMAC(Hash-based Message Authentication Code).md b/content/HMAC(Hash-based Message Authentication Code).md new file mode 100644 index 00000000..3456323d --- /dev/null +++ b/content/HMAC(Hash-based Message Authentication Code).md @@ -0,0 +1,159 @@ +--- +draft: false +tags: + - 암호화 + - 보안 + - 해시 + - 인증 +description: HMAC(Hash-based Message Authentication Code)에 대한 심층적인 이해와 구현 방법을 설명합니다. +--- +현대 소프트웨어 개발에서 데이터의 무결성과 인증은 필수적인 요소입니다. 특히 네트워크를 통해 전송되는 데이터나 저장된 데이터가 변조되지 않았음을 확인하는 것은 보안의 기본입니다. HMAC(Hash-based Message Authentication Code)는 이러한 요구를 충족시키기 위한 암호학적 기법으로, 메시지의 무결성과 함께 발신자의 인증을 제공합니다. 이 글에서는 HMAC의 개념, 작동 원리, 구현 방법, 그리고 실제 사용 사례에 대해 상세히 알아보겠습니다. + +## HMAC이란? + +HMAC은 Hash-based Message Authentication Code의 약자로, 메시지의 무결성을 검증하고 인증을 제공하는 특수한 형태의 [[메시지 인증 코드]]입니다. 일반적인 해시 함수와 달리, HMAC은 비밀 키를 사용하여 해시 값을 생성합니다. 이를 통해 메시지가 전송 중에 변경되지 않았음을 확인할 수 있을 뿐만 아니라, 메시지가 실제로 키를 알고 있는 발신자로부터 왔다는 것을 인증할 수 있습니다. + +HMAC은 RFC 2104에서 정의되었으며, 다양한 [[해시 함수]]와 함께 사용할 수 있습니다. 가장 일반적으로 사용되는 조합은 HMAC-SHA256, HMAC-SHA1, HMAC-MD5 등이 있습니다. + +## HMAC의 보안 강도 + +HMAC의 보안 강도는 다음 요소에 의해 결정됩니다: + +1. **사용되는 해시 함수의 강도**: HMAC은 기본 해시 함수의 강도에 의존합니다. 예를 들어, HMAC-SHA256은 SHA-256 해시 함수를 사용하므로 그 보안 강도는 SHA-256의 강도와 관련이 있습니다. + +2. **키의 길이와 무작위성**: 키가 길고 무작위적일수록 HMAC은 더 안전합니다. 키의 길이는 최소한 해시 함수의 출력 길이와 같거나 그 이상이어야 합니다. + +3. **키의 기밀성**: HMAC의 보안은 키의 기밀성에 의존합니다. 키가 노출되면 공격자가 유효한 HMAC을 생성할 수 있으므로 키를 안전하게 관리하는 것이 중요합니다. + + +HMAC은 현재까지 알려진 공격에 대해 강력한 보안을 제공합니다. 특히, 이중 해싱 구조는 [[길이 확장 공격]]을 효과적으로 방지합니다. + +## Java에서의 HMAC 구현 + +Java에서 HMAC을 구현하는 것은 `javax.crypto` 패키지를 사용하여 비교적 간단하게 할 수 있습니다. 다음은 HMAC-SHA256을 구현하는 예시 코드입니다: + +```java +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class HMACExample { + public static String calculateHMAC(String message, String key) + throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec( + key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hmacBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hmacBytes); + } + + public static void main(String[] args) { + try { + String message = "안녕하세요, 이것은 HMAC 테스트 메시지입니다."; + String key = "비밀키_12345"; + String hmacResult = calculateHMAC(message, key); + System.out.println("메시지: " + message); + System.out.println("HMAC 결과: " + hmacResult); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +이 코드는 다음과 같은 단계로 작동합니다: + +1. "HmacSHA256" 알고리즘을 사용하는 Mac 인스턴스를 생성합니다. +2. 비밀 키로부터 SecretKeySpec 객체를 생성합니다. +3. Mac 인스턴스를 초기화합니다. +4. 메시지에 대해 HMAC을 계산합니다. +5. 결과를 Base64로 인코딩하여 반환합니다. + +다른 해시 알고리즘(예: HmacSHA1, HmacMD5)을 사용하려면 "HmacSHA256" 대신 해당 알고리즘 이름을 지정하면 됩니다. + +## Spring 프레임워크에서의 HMAC 활용 + +Spring Security는 HMAC을 활용한 보안 기능을 제공합니다. 특히 RESTful API 인증에서 HMAC을 사용하는 방법을 살펴보겠습니다: + +```java +import org.springframework.security.crypto.codec.Hex; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +@Component +public class HMACAuthenticationService { + private final String secretKey = "애플리케이션_비밀키"; + + public String generateHMAC(String data) { + try { + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec( + secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + hmac.init(secretKeySpec); + byte[] hmacBytes = hmac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return new String(Hex.encode(hmacBytes)); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("HMAC 생성 중 오류 발생", e); + } + } + + public boolean validateHMAC(String data, String providedHMAC) { + String calculatedHMAC = generateHMAC(data); + return calculatedHMAC.equals(providedHMAC); + } +} +``` + +이 서비스는 Spring 애플리케이션에서 다음과 같이 사용할 수 있습니다: + +1. API 요청에 대한 HMAC을 생성합니다. +2. 클라이언트가 제공한 HMAC과 서버에서 계산한 HMAC을 비교하여 요청의 무결성과 인증을 확인합니다. + +실제 구현에서는 비밀 키를 안전하게 관리하기 위해 환경 변수나 Spring의 프로퍼티 관리 시스템을 사용하는 것이 좋습니다. + +## HMAC의 실제 사용 사례 + +HMAC은 다양한 보안 애플리케이션에서 사용됩니다: + +1. **API 인증**: 많은 웹 API가 HMAC을 사용하여 요청의 무결성과 인증을 확인합니다. 예를 들어, AWS API는 HMAC을 사용하여 요청에 서명합니다. + +2. **웹 토큰**: [[JWT]](JSON Web Token)와 같은 웹 토큰은 HMAC을 사용하여 토큰의 무결성을 보장합니다. + +3. **비밀번호 저장**: 비밀번호를 안전하게 저장하기 위해 HMAC을 사용할 수 있습니다. 이 경우 HMAC은 [[솔트]]와 함께 사용되어 [[레인보우 테이블 공격]]을 방지합니다. + +4. **메시지 인증**: 안전한 통신 채널에서 메시지의 무결성과 인증을 확인하기 위해 HMAC을 사용합니다. + +5. **파일 무결성 검사**: 파일이 변조되지 않았는지 확인하기 위해 HMAC을 사용할 수 있습니다. + + +## HMAC과 다른 인증 기법의 비교 + +HMAC은 다른 인증 기법과 비교하여 몇 가지 장단점이 있습니다: + +### HMAC vs 일반 해시 함수 + +- **장점**: HMAC은 비밀 키를 사용하므로 인증 기능을 제공합니다. 일반 해시 함수는 무결성만 제공합니다. +- **단점**: HMAC은 키 관리가 필요하므로 추가적인 복잡성이 있습니다. + +### HMAC vs 디지털 서명 + +- **장점**: HMAC은 대칭 키를 사용하므로 계산이 빠릅니다. +- **단점**: HMAC은 부인 방지(non-repudiation) 기능을 제공하지 않습니다. 발신자와 수신자 모두 동일한 키를 가지고 있기 때문입니다. + +### HMAC vs CBC-MAC + +- **장점**: HMAC은 특별히 설계된 MAC 알고리즘으로, 블록 암호의 취약점에 영향을 받지 않습니다. +- **단점**: CBC-MAC은 블록 암호를 이미 사용하는 시스템에서 구현이 더 간단할 수 있습니다. + +## 결론 + +HMAC은 메시지의 무결성과 인증을 보장하는 강력한 암호학적 기법입니다. 다양한 해시 함수와 함께 사용할 수 있으며, 특히 HMAC-SHA256은 현재 가장 널리 사용되는 조합 중 하나 \ No newline at end of file diff --git a/content/OAuth 2.0.md b/content/OAuth 2.0.md new file mode 100644 index 00000000..4738efb9 --- /dev/null +++ b/content/OAuth 2.0.md @@ -0,0 +1,417 @@ +# OAuth 2.0: 완벽 가이드 +OAuth 2.0은 사용자의 비밀번호를 공유하지 않고도 제3자 애플리케이션이 사용자의 보호된 리소스에 접근할 수 있게 해주는 [[인증(Authentication)]]과 [[인가(Authorization)]] 프레임워크입니다. 2012년 IETF(Internet Engineering Task Force)에 의해 RFC 6749로 표준화되었으며, 현재 웹과 모바일 애플리케이션에서 가장 널리 사용되는 인증 프로토콜 중 하나입니다. + +기존 인증 방식에서는 사용자가 제3자 애플리케이션에 자신의 계정 정보(아이디/비밀번호)를 직접 제공해야 했지만, OAuth 2.0을 사용하면 사용자는 ID 제공자(Identity Provider)를 통해 인증한 후, 특정 리소스에 대한 접근 권한만 제3자 애플리케이션에 위임할 수 있습니다. 이러한 방식으로 사용자는 보안을 유지하면서도 다양한 서비스를 연동하여 사용할 수 있습니다. + +## OAuth 2.0의 주요 구성요소 + +OAuth 2.0에는 네 가지 주요 역할이 있습니다: + +1. **리소스 소유자(Resource Owner)**: 보호된 리소스에 접근 권한을 부여할 수 있는 개체로, 일반적으로 최종 사용자입니다. + +2. **리소스 서버(Resource Server)**: 보호된 리소스를 호스팅하고 액세스 토큰을 사용하여 요청을 수락하고 응답하는 서버입니다. + +3. **클라이언트(Client)**: 리소스 소유자의 보호된 리소스에 접근하려는 애플리케이션입니다. 클라이언트는 웹 애플리케이션, 모바일 앱, 데스크톱 애플리케이션 등 다양한 형태가 될 수 있습니다. + +4. **인증 서버(Authorization Server)**: 리소스 소유자를 인증하고 권한 부여를 받은 후 클라이언트에게 액세스 토큰을 발급하는 서버입니다. + + +이러한 역할들의 상호작용을 통해 OAuth 2.0은 사용자의 인증 정보를 직접 공유하지 않고도 안전하게 리소스에 접근할 수 있는 방법을 제공합니다. + +## 인증 흐름(Grant Type) + +OAuth 2.0은 다양한 유형의 클라이언트와 사용 사례를 지원하기 위해 여러 가지 인증 흐름(Grant Type)을 정의합니다. 각 흐름은 특정 시나리오와 보안 요구사항에 맞게 설계되었습니다. + +### 권한 부여 코드 흐름(Authorization Code Flow) + +가장 일반적으로 사용되는 흐름으로, 웹 애플리케이션에 적합합니다. 이 흐름은 백엔드 서버에서 클라이언트 비밀을 안전하게 보관할 수 있는 환경에 최적화되어 있습니다. + +```mermaid +sequenceDiagram + participant 사용자 as 사용자(리소스 소유자) + participant 클라이언트 as 클라이언트 + participant 인증서버 as 인증 서버 + participant 리소스서버 as 리소스 서버 + + 사용자->>클라이언트: 1. 서비스 접근 + 클라이언트->>사용자: 2. 인증 서버로 리다이렉트 + 사용자->>인증서버: 3. 인증 요청 + 인증서버->>사용자: 4. 로그인 및 권한 동의 화면 + 사용자->>인증서버: 5. 로그인 및 권한 동의 + 인증서버->>사용자: 6. 권한 부여 코드와 함께 리다이렉트 + 사용자->>클라이언트: 7. 권한 부여 코드 전달 + 클라이언트->>인증서버: 8. 권한 부여 코드로 토큰 요청 + Note right of 클라이언트: client_id, client_secret, code, redirect_uri 포함 + 인증서버->>클라이언트: 9. 액세스 토큰 및 리프레시 토큰 발급 + 클라이언트->>리소스서버: 10. 액세스 토큰으로 리소스 요청 + 리소스서버->>클라이언트: 11. 요청한 리소스 반환 +``` + +### 암묵적 흐름(Implicit Flow) + +단일 페이지 애플리케이션(SPA)과 같이 클라이언트 측 JavaScript 애플리케이션에 적합한 간소화된 흐름입니다. 권한 부여 코드 없이 바로 액세스 토큰이 발급되지만, 보안상 권한 부여 코드 흐름에 비해 취약점이 있습니다. + +```mermaid +sequenceDiagram + participant 사용자 as 사용자(리소스 소유자) + participant 클라이언트 as 클라이언트(브라우저) + participant 인증서버 as 인증 서버 + participant 리소스서버 as 리소스 서버 + + 사용자->>클라이언트: 1. 서비스 접근 + 클라이언트->>사용자: 2. 인증 서버로 리다이렉트 + 사용자->>인증서버: 3. 인증 요청 + 인증서버->>사용자: 4. 로그인 및 권한 동의 화면 + 사용자->>인증서버: 5. 로그인 및 권한 동의 + 인증서버->>사용자: 6. 액세스 토큰과 함께 리다이렉트(URI Fragment) + 사용자->>클라이언트: 7. 액세스 토큰 전달 + 클라이언트->>리소스서버: 8. 액세스 토큰으로 리소스 요청 + 리소스서버->>클라이언트: 9. 요청한 리소스 반환 +``` + +> **참고**: 보안상의 이유로 OAuth 2.0 보안 최적화(OAuth 2.0 Security Best Current Practice)에서는 새로운 애플리케이션에서 암묵적 흐름 대신 Authorization Code Flow with PKCE를 사용할 것을 권장하고 있습니다. + +### 리소스 소유자 비밀번호 자격 증명 흐름(Resource Owner Password Credentials Flow) + +사용자의 이름과 비밀번호를 직접 사용하여 액세스 토큰을 얻는 흐름입니다. 높은 신뢰도를 가진 애플리케이션(자사 애플리케이션)에서만 제한적으로 사용해야 합니다. + +```mermaid +sequenceDiagram + participant 사용자 as 사용자(리소스 소유자) + participant 클라이언트 as 클라이언트 + participant 인증서버 as 인증 서버 + participant 리소스서버 as 리소스 서버 + + 사용자->>클라이언트: 1. 사용자명/비밀번호 제공 + 클라이언트->>인증서버: 2. 사용자 자격 증명으로 토큰 요청 + 인증서버->>클라이언트: 3. 액세스 토큰 및 리프레시 토큰 발급 + 클라이언트->>리소스서버: 4. 액세스 토큰으로 리소스 요청 + 리소스서버->>클라이언트: 5. 요청한 리소스 반환 +``` + +### 클라이언트 자격 증명 흐름(Client Credentials Flow) + +사용자 컨텍스트가 없는 서버 간 통신에 사용됩니다. 클라이언트가 자신의 자격 증명을 사용하여 직접 액세스 토큰을 요청합니다. + +```mermaid +sequenceDiagram + participant 클라이언트 as 클라이언트(서버) + participant 인증서버 as 인증 서버 + participant 리소스서버 as 리소스 서버 + + 클라이언트->>인증서버: 1. 클라이언트 자격 증명으로 토큰 요청 + 인증서버->>클라이언트: 2. 액세스 토큰 발급 + 클라이언트->>리소스서버: 3. 액세스 토큰으로 리소스 요청 + 리소스서버->>클라이언트: 4. 요청한 리소스 반환 +``` + +## 토큰 유형 + +OAuth 2.0에서 사용되는 주요 토큰 유형에 대해 알아보겠습니다. + +### 액세스 토큰(Access Token) + +보호된 리소스에 접근하기 위한 자격 증명으로 사용되는 토큰입니다. 액세스 토큰은 일반적으로 짧은 수명(보통 몇 시간 또는 그 이하)을 가지며, 다양한 형식(JWT, 불투명 토큰 등)으로 구현될 수 있습니다. + +액세스 토큰은 다음과 같은 방식으로 리소스 서버에 전달됩니다: + +``` +GET /api/user/profile HTTP/1.1 +Host: example.com +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +### 리프레시 토큰(Refresh Token) + +액세스 토큰이 만료된 후 새로운 액세스 토큰을 발급받기 위해 사용하는 토큰입니다. 리프레시 토큰은 액세스 토큰보다 긴 수명을 가지며, 사용자가 매번 재인증하지 않아도 되게 합니다. + +```java +// 리프레시 토큰을 사용하여 새로운 액세스 토큰 요청 예시 +@Service +public class TokenRefreshService { + + @Autowired + private RestTemplate restTemplate; + + @Value("${oauth2.client.id}") + private String clientId; + + @Value("${oauth2.client.secret}") + private String clientSecret; + + @Value("${oauth2.token.endpoint}") + private String tokenEndpoint; + + public TokenResponse refreshAccessToken(String refreshToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "refresh_token"); + map.add("refresh_token", refreshToken); + map.add("client_id", clientId); + map.add("client_secret", clientSecret); + + HttpEntity> request = new HttpEntity<>(map, headers); + + ResponseEntity response = restTemplate.postForEntity( + tokenEndpoint, request, TokenResponse.class); + + return response.getBody(); + } + + // 토큰 응답을 위한 DTO 클래스 +} +``` + +### ID 토큰 + +[[OpenID Connect]]에서 도입된 개념으로, 사용자의 신원 정보를 포함하는 JWT(JSON Web Token) 형식의 토큰입니다. ID 토큰은 인증의 결과로, 사용자의 프로필 정보를 포함합니다. + +## 스프링 부트에서 OAuth 2.0 구현하기 + +스프링 부트는 OAuth 2.0을 쉽게 구현할 수 있는 다양한 라이브러리와 기능을 제공합니다. 여기서는 OAuth 2.0의 각 역할(클라이언트, 리소스 서버, 인증 서버)을 스프링 부트에서 구현하는 방법을 살펴보겠습니다. + +### OAuth 2.0 클라이언트 구현 + +Spring Security OAuth2 Client를 사용하면 OAuth 2.0 클라이언트를 쉽게 구현할 수 있습니다. + +```java +// build.gradle +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-web' +} +``` + +```java +// application.yml +spring: + security: + oauth2: + client: + registration: + github: + client-id: your-client-id + client-secret: your-client-secret + scope: read:user + google: + client-id: your-google-client-id + client-secret: your-google-client-secret + scope: profile,email +``` + +```java +// SecurityConfig.java +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .antMatchers("/", "/login**", "/error**").permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .loginPage("/login") + .defaultSuccessUrl("/home", true) + ); + } +} +``` + +### OAuth 2.0 리소스 서버 구현 + +Spring Security OAuth2 Resource Server를 사용하여 리소스 서버를 구현할 수 있습니다. + +```java +// build.gradle +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-web' +} +``` + +```java +// application.yml +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://your-auth-server.com + jwk-set-uri: https://your-auth-server.com/.well-known/jwks.json +``` + +```java +// ResourceServerConfig.java +@Configuration +@EnableWebSecurity +public class ResourceServerConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .antMatchers("/public/**").permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverter()) + ) + ); + } + + private JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } +} +``` + +### OAuth 2.0 인증 서버 구현 + +Spring Boot 2.0 이후부터 Spring Security OAuth 프로젝트는 더 이상 적극적으로 유지보수되지 않고, Spring Authorization Server 프로젝트가 개발되고 있습니다. 여기서는 Spring Authorization Server를 사용한 구현 예시를 살펴보겠습니다. + +```java +// build.gradle +dependencies { + implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.3.1' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' +} +``` + +```java +// AuthorizationServerConfig.java +@Configuration +@EnableWebSecurity +public class AuthorizationServerConfig { + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + return http + .exceptionHandling(exceptions -> + exceptions.authenticationEntryPoint( + new LoginUrlAuthenticationEntryPoint("/login")) + ) + .build(); + } + + @Bean + @Order(2) + public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception { + return http + .formLogin(withDefaults()) + .authorizeHttpRequests(authorize -> + authorize.anyRequest().authenticated() + ) + .build(); + } + + @Bean + public RegisteredClientRepository registeredClientRepository() { + RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("client") + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client") + .scope("read") + .scope("write") + .build(); + + return new InMemoryRegisteredClientRepository(client); + } + + @Bean + public JWKSource jwkSource() { + RSAKey rsaKey = generateRsa(); + JWKSet jwkSet = new JWKSet(rsaKey); + return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + } + + private static RSAKey generateRsa() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + + private static KeyPair generateRsaKey() { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Bean + public ProviderSettings providerSettings() { + return ProviderSettings.builder() + .issuer("http://auth-server:9000") + .build(); + } +} +``` + +## 보안 고려사항 + +OAuth 2.0을 구현할 때 고려해야 할 몇 가지 중요한 보안 사항이 있습니다: + +1. **HTTPS 사용**: 모든 OAuth 2.0 엔드포인트는 HTTPS(TLS)를 통해 보호되어야 합니다. + +2. **상태 매개변수(state parameter) 사용**: CSRF(Cross-Site Request Forgery) 공격을 방지하기 위해 권한 부여 요청에 상태 매개변수를 포함하고 콜백 시 검증해야 합니다. + +3. **PKCE(Proof Key for Code Exchange) 사용**: 모바일 애플리케이션이나 단일 페이지 애플리케이션에서는 PKCE를 사용하여 인증 코드 가로채기 공격을 방지해야 합니다. + +4. **토큰 저장**: 액세스 토큰과 리프레시 토큰은 안전하게 저장해야 합니다. 브라우저 환경에서는 HttpOnly 쿠키를 사용하는 것이 좋습니다. + +5. **클라이언트 비밀 보호**: 클라이언트 비밀은 노출되지 않도록 서버 측 코드에만 저장해야 합니다. + +6. **스코프 제한**: 애플리케이션이 요청하는 권한(스코프)은 필요한 최소한으로 제한해야 합니다. + + +## 모범 사례 + +OAuth 2.0을 구현할 때 다음과 같은 모범 사례를 따르는 것이 좋습니다: + +1. **최신 흐름 사용**: 새로운 애플리케이션에서는 가능한 한 권한 부여 코드 흐름(PKCE와 함께)을 사용하세요. + +2. **짧은 수명의 액세스 토큰**: 액세스 토큰의 수명은 짧게(1시간 이하) 설정하고, 필요한 경우 리프레시 토큰을 사용하여 갱신하세요. + +3. **토큰 검증**: 리소스 서버는 액세스 토큰의 서명, 발급자, 대상, 만료 시간 등을 철저히 검증해야 합니다. + +4. **에러 처리**: 자세한 오류 정보는 노출하지 않고, 표준 OAuth 2.0 오류 코드를 사용하세요. + +5. **로깅과 모니터링**: 인증 및 권한 부여 관련 이벤트를 로깅하고 모니터링하여 이상 징후를 탐지하세요. + + +## 마치며 + +OAuth 2.0은 강력하고 유연한 인증 및 권한 부여 프레임워크이지만, 올바르게 구현하기 위해서는 여러 보안 고려사항과 모범 사례를 이해하고 따라야 합니다. 이 글에서는 OAuth 2.0의 기본 개념, 주요 구성요소, 다양한 인증 흐름, 그리고 스프링 부트에서의 구현 방법에 대해 살펴보았습니다. + +실제 애플리케이션에서 OAuth 2.0을 구현할 때는 사용 사례와 보안 요구사항에 맞는 적절한 흐름을 선택하고, 최신 보안 권장 사항을 따르는 것이 중요합니다. 또한 OAuth 2.0은 계속 발전하고 있으므로, 최신 표준과 모범 사례를 주기적으로 확인하는 것이 좋습니다. + +## 참고 자료 + +- [OAuth 2.0 공식 문서 (RFC 6749)](https://tools.ietf.org/html/rfc6749) +- [OAuth 2.0 위협 모델 및 보안 고려사항 (RFC 6819)](https://tools.ietf.org/html/rfc6819) +- [OAuth 2.0 보안 최적화 (BCP)](https://tools.ietf.org/html/draft-ietf-oauth-security-topics) \ No newline at end of file diff --git a/content/SOLID.md b/content/SOLID.md new file mode 100644 index 00000000..fea0a086 --- /dev/null +++ b/content/SOLID.md @@ -0,0 +1,598 @@ +SOLID는 로버트 C. 마틴(Robert C. Martin, 일명 "Uncle Bob")이 2000년대 초반에 제안한 객체지향 프로그래밍 및 설계의 5가지 기본 원칙의 앞글자를 따서 만든 약어입니다. 이 원칙들은 개발자가 유지보수가 쉽고 확장 가능한 시스템을 만들 수 있도록 도와줍니다. + +SOLID의 각 글자는 다음을 의미합니다: + +- **S**: 단일 책임 원칙 (Single Responsibility Principle) +- **O**: 개방-폐쇄 원칙 (Open-Closed Principle) +- **L**: 리스코프 치환 원칙 (Liskov Substitution Principle) +- **I**: 인터페이스 분리 원칙 (Interface Segregation Principle) +- **D**: 의존성 역전 원칙 (Dependency Inversion Principle) + +이제 각 원칙에 대해 자세히 살펴보겠습니다. + +## S - 단일 책임 원칙 (Single Responsibility Principle) + +> "클래스는 단 하나의 책임만 가져야 한다." + +단일 책임 원칙은 모든 클래스가 단 하나의 책임만을 가져야 한다는 개념입니다. 다르게 표현하면, 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 합니다. + +### 위반 사례: + +```java +public class User { + private String name; + private String email; + + // 사용자 데이터 관련 메서드 + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + // 데이터베이스 관련 메서드 + public void saveToDatabase() { + // 데이터베이스에 사용자 저장 로직 + System.out.println("Saving user to database"); + } + + // 보고서 관련 메서드 + public void generateReport() { + // 사용자 보고서 생성 로직 + System.out.println("Generating user report"); + } +} +``` + +이 클래스는 다음과 같은 여러 책임을 가지고 있습니다: + +1. 사용자 데이터 관리 +2. 데이터베이스 작업 +3. 보고서 생성 + +### 개선된 버전: + +```java +// 사용자 데이터만 담당 +public class User { + private String name; + private String email; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } +} + +// 데이터베이스 작업 담당 +public class UserRepository { + public void save(User user) { + // 데이터베이스에 사용자 저장 로직 + System.out.println("Saving user to database"); + } +} + +// 보고서 생성 담당 +public class UserReportGenerator { + public void generateReport(User user) { + // 사용자 보고서 생성 로직 + System.out.println("Generating user report"); + } +} +``` + +이렇게 분리함으로써: + +- 각 클래스는 하나의 책임만 가집니다. +- 코드가 더 모듈화되어 유지보수가 용이해집니다. +- 클래스 간의 결합도가 감소합니다. + +## O - 개방-폐쇄 원칙 (Open-Closed Principle) + +> "소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다." + +이 원칙은 기존 코드를 변경하지 않고도 시스템의 기능을 확장할 수 있어야 한다는 것을 의미합니다. + +### 위반 사례: + +```java +public class Rectangle { + private double width; + private double height; + + // 생성자 및 getter/setter 생략 + public double getWidth() { return width; } + public void setWidth(double width) { this.width = width; } + public double getHeight() { return height; } + public void setHeight(double height) { this.height = height; } +} + +public class Circle { + private double radius; + + // 생성자 및 getter/setter 생략 + public double getRadius() { return radius; } + public void setRadius(double radius) { this.radius = radius; } +} + +public class AreaCalculator { + public double calculateArea(Object shape) { + if (shape instanceof Rectangle) { + Rectangle rectangle = (Rectangle) shape; + return rectangle.getWidth() * rectangle.getHeight(); + } + else if (shape instanceof Circle) { + Circle circle = (Circle) shape; + return Math.PI * circle.getRadius() * circle.getRadius(); + } + return 0; + } +} +``` + +이 설계의 문제점: + +- 새로운 도형(예: 삼각형)을 추가하려면 `AreaCalculator` 클래스를 수정해야 합니다. +- 조건문이 늘어나면서 코드가 복잡해집니다. + +### 개선된 버전: + +```java +public interface Shape { + double calculateArea(); +} + +public class Rectangle implements Shape { + private double width; + private double height; + + // 생성자 및 getter/setter 생략 + + @Override + public double calculateArea() { + return width * height; + } +} + +public class Circle implements Shape { + private double radius; + + // 생성자 및 getter/setter 생략 + + @Override + public double calculateArea() { + return Math.PI * radius * radius; + } +} + +public class AreaCalculator { + public double calculateArea(Shape shape) { + return shape.calculateArea(); + } +} +``` + +개선된 점: + +- 새로운 도형을 추가할 때 `Shape` 인터페이스를 구현하는 새 클래스만 만들면 됩니다. +- `AreaCalculator` 클래스는 수정할 필요가 없습니다. +- [[다형성]]을 통해 설계가 더 유연해졌습니다. + +## L - 리스코프 치환 원칙 (Liskov Substitution Principle) + +> "프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다." + +바바라 리스코프(Barbara Liskov)가 1987년에 소개한 이 원칙은 상속 관계에서 중요한 개념입니다. 쉽게 말해, 자식 클래스는 부모 클래스의 행동을 완벽하게 대체할 수 있어야 합니다. + +### 위반 사례: + +```java +public class Rectangle { + protected double width; + protected double height; + + public void setWidth(double width) { + this.width = width; + } + + public void setHeight(double height) { + this.height = height; + } + + public double getArea() { + return width * height; + } +} + +public class Square extends Rectangle { + @Override + public void setWidth(double width) { + this.width = width; + this.height = width; // 정사각형이므로 너비와 높이가 같아야 함 + } + + @Override + public void setHeight(double height) { + this.height = height; + this.width = height; // 정사각형이므로 너비와 높이가 같아야 함 + } +} +``` + +문제점: + +```java +void testRectangle(Rectangle r) { + r.setWidth(5); + r.setHeight(4); + // 직사각형이면 면적은 20이어야 함 + assert r.getArea() == 20; // 직사각형이면 통과, 정사각형이면 실패 +} +``` + +이 테스트는 `Rectangle` 객체로는 통과하지만 `Square` 객체로는 실패합니다. 이는 리스코프 치환 원칙을 위반합니다. + +### 개선된 버전: + +```java +public interface Shape { + double getArea(); +} + +public class Rectangle implements Shape { + private double width; + private double height; + + public Rectangle(double width, double height) { + this.width = width; + this.height = height; + } + + public double getWidth() { + return width; + } + + public double getHeight() { + return height; + } + + @Override + public double getArea() { + return width * height; + } +} + +public class Square implements Shape { + private double side; + + public Square(double side) { + this.side = side; + } + + public double getSide() { + return side; + } + + @Override + public double getArea() { + return side * side; + } +} +``` + +개선된 점: + +- `Square`가 `Rectangle`을 상속하지 않고, 둘 다 `Shape` 인터페이스를 구현합니다. +- 각 클래스는 자신의 속성에 맞게 동작합니다. +- 어떤 `Shape` 객체를 사용하든 예측 가능한 방식으로 작동합니다. + +## I - 인터페이스 분리 원칙 (Interface Segregation Principle) + +> "클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요받지 않아야 한다." + +이 원칙은 큰 인터페이스를 여러 개의 작은 인터페이스로 분리하는 것이 좋다고 말합니다. 클라이언트는 필요한 메서드만 있는 인터페이스만 알고 있으면 됩니다. + +### 위반 사례: + +```java +public interface Worker { + void work(); + void eat(); + void sleep(); +} + +public class Human implements Worker { + @Override + public void work() { + System.out.println("Human is working"); + } + + @Override + public void eat() { + System.out.println("Human is eating"); + } + + @Override + public void sleep() { + System.out.println("Human is sleeping"); + } +} + +public class Robot implements Worker { + @Override + public void work() { + System.out.println("Robot is working"); + } + + @Override + public void eat() { + // 로봇은 먹지 않음 + throw new UnsupportedOperationException("Robots don't eat"); + } + + @Override + public void sleep() { + // 로봇은 자지 않음 + throw new UnsupportedOperationException("Robots don't sleep"); + } +} +``` + +문제점: + +- `Robot` 클래스는 `eat()`와 `sleep()` 메서드를 구현해야 하지만, 실제로는 이러한 동작을 수행할 수 없습니다. +- 클라이언트는 사용하지 않는 메서드에 의존하게 됩니다. + +### 개선된 버전: + +```java +public interface Workable { + void work(); +} + +public interface Eatable { + void eat(); +} + +public interface Sleepable { + void sleep(); +} + +public class Human implements Workable, Eatable, Sleepable { + @Override + public void work() { + System.out.println("Human is working"); + } + + @Override + public void eat() { + System.out.println("Human is eating"); + } + + @Override + public void sleep() { + System.out.println("Human is sleeping"); + } +} + +public class Robot implements Workable { + @Override + public void work() { + System.out.println("Robot is working"); + } +} +``` + +개선된 점: + +- 인터페이스가 더 작고 집중된 책임을 가집니다. +- `Robot` 클래스는 필요한 `Workable` 인터페이스만 구현합니다. +- 클라이언트는 필요한 기능만 사용할 수 있습니다. + +## D - 의존성 역전 원칙 (Dependency Inversion Principle) + +> "고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다." "추상화는 세부사항에 의존해서는 안 된다. 세부사항은 추상화에 의존해야 한다." + +이 원칙은 소프트웨어 모듈 간의 의존성 방향에 관한 것입니다. 전통적인 의존성 방향을 뒤집어 유연성을 증가시키는 것이 목표입니다. + +### 위반 사례: + +```java +public class LightBulb { + public void turnOn() { + System.out.println("LightBulb turned on"); + } + + public void turnOff() { + System.out.println("LightBulb turned off"); + } +} + +public class Switch { + private LightBulb bulb; + + public Switch() { + this.bulb = new LightBulb(); + } + + public void operate() { + // ... 스위치 상태 로직 + bulb.turnOn(); + } +} +``` + +문제점: + +- `Switch` 클래스가 `LightBulb` 클래스에 직접 의존합니다. +- 다른 종류의 장치(예: 팬, TV)를 제어하려면 `Switch` 클래스를 수정해야 합니다. + +### 개선된 버전: + +```java +public interface Switchable { + void turnOn(); + void turnOff(); +} + +public class LightBulb implements Switchable { + @Override + public void turnOn() { + System.out.println("LightBulb turned on"); + } + + @Override + public void turnOff() { + System.out.println("LightBulb turned off"); + } +} + +public class Fan implements Switchable { + @Override + public void turnOn() { + System.out.println("Fan turned on"); + } + + @Override + public void turnOff() { + System.out.println("Fan turned off"); + } +} + +public class Switch { + private Switchable device; + + public Switch(Switchable device) { + this.device = device; + } + + public void operate() { + // ... 스위치 상태 로직 + device.turnOn(); + } +} +``` + +개선된 점: + +- 고수준 모듈(`Switch`)과 저수준 모듈(`LightBulb`, `Fan`)이 모두 추상화(`Switchable` 인터페이스)에 의존합니다. +- `Switch` 클래스는 구체적인 구현이 아닌 추상화에 의존하므로 다양한 장치와 함께 사용할 수 있습니다. +- 새로운 장치를 추가할 때 기존 코드를 수정할 필요가 없습니다. + +## SOLID 원칙의 실제 적용: 스프링 프레임워크 예시 + +스프링 프레임워크는 SOLID 원칙을 잘 구현한 예입니다. 특히 의존성 주입(DI)과 관련하여 의존성 역전 원칙(DIP)을 핵심으로 사용합니다. + +### 예시: 간단한 사용자 관리 시스템 + +```java +// 인터페이스 정의 (추상화) +public interface UserRepository { + User findById(Long id); + void save(User user); +} + +// 구현체 (세부사항) +@Repository +public class JpaUserRepository implements UserRepository { + @PersistenceContext + private EntityManager entityManager; + + @Override + public User findById(Long id) { + return entityManager.find(User.class, id); + } + + @Override + public void save(User user) { + entityManager.persist(user); + } +} + +// 서비스 계층 (고수준 모듈) +@Service +public class UserService { + private final UserRepository userRepository; + + // 의존성 주입을 통한 의존성 역전 구현 + @Autowired + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User getUserById(Long id) { + return userRepository.findById(id); + } + + public void createUser(User user) { + // 비즈니스 로직 + userRepository.save(user); + } +} + +// 컨트롤러 (클라이언트) +@RestController +@RequestMapping("/users") +public class UserController { + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable Long id) { + User user = userService.getUserById(id); + return ResponseEntity.ok(user); + } + + @PostMapping + public ResponseEntity createUser(@RequestBody User user) { + userService.createUser(user); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} +``` + +이 예시에서: + +- **단일 책임 원칙(SRP)**: 각 클래스는 하나의 책임만 가집니다. `UserRepository`는 데이터 접근, `UserService`는 비즈니스 로직, `UserController`는 HTTP 요청 처리만 담당합니다. +- **개방-폐쇄 원칙(OCP)**: 새로운 저장소 구현(예: `MongoUserRepository`)을 추가할 때 기존 코드를 수정할 필요가 없습니다. +- **리스코프 치환 원칙(LSP)**: `JpaUserRepository`는 `UserRepository` 인터페이스를 완벽하게 구현하므로 언제든지 대체 가능합니다. +- **인터페이스 분리 원칙(ISP)**: `UserRepository` 인터페이스는 필요한 메서드만 정의합니다. +- **의존성 역전 원칙(DIP)**: 고수준 모듈(`UserService`)은 저수준 모듈(`JpaUserRepository`)에 직접 의존하지 않고, 추상화(`UserRepository` 인터페이스)에 의존합니다. + +## SOLID 원칙의 이점 + +SOLID 원칙을 따르면 다음과 같은 이점을 얻을 수 있습니다: + +1. **유지보수성 향상**: 코드가 모듈화되어 변경 사항이 격리됩니다. +2. **확장성 개선**: 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다. +3. **테스트 용이성**: 각 컴포넌트를 독립적으로 테스트하기 쉽습니다. +4. **코드 재사용성 증가**: 느슨하게 결합된 컴포넌트는 여러 곳에서 재사용하기 쉽습니다. +5. **더 명확한 설계**: 책임이 명확하게 분리되어 코드를 이해하기 쉽습니다. + +## SOLID 원칙 적용 시 주의사항 + +SOLID 원칙을 맹목적으로 따르는 것은 위험할 수 있습니다. 다음 사항을 고려해야 합니다: + +1. **과잉 엔지니어링 방지**: 작은 문제에 복잡한 솔루션을 적용하지 마세요. +2. **실용성 유지**: 완벽한 설계보다 실용적인 설계가 더 중요할 수 있습니다. +3. **점진적 적용**: 기존 코드베이스에 점진적으로 SOLID 원칙을 적용하세요. +4. **상황에 맞는 판단**: 모든 상황에 모든 원칙이 적합하지는 않습니다. + +## 결론 + +SOLID 원칙은 객체지향 설계의 핵심 기둥이며, 유지보수가 용이하고 확장 가능한 소프트웨어를 만드는 데 중요한 지침을 제공합니다. 이러한 원칙을 이해하고 적절하게 적용하면 더 나은 코드 구조를 만들고 장기적으로 개발 비용을 절감할 수 있습니다. + +그러나 SOLID 원칙은 도그마가 아니라 지침으로 생각해야 합니다. 항상 문제의 복잡성과 프로젝트의 요구 사항을 고려하여 적절한 수준의 추상화와 모듈화를 적용해야 합니다. + +여러분의 다음 프로젝트에서 SOLID 원칙을 적용해 보세요. 처음에는 어려울 수 있지만, 시간이 지남에 따라 이러한 원칙이 자연스럽게 여러분의 설계 사고방식에 녹아들 것입니다. + +## 관련 노트 + +- [[다형성]] +- [[객체지향 설계]] +- [[디자인 패턴]] +- [[테스트 주도 개발]] +- [[스프링 프레임워크]] \ No newline at end of file diff --git "a/content/\353\251\224\354\213\234\354\247\200 \354\235\270\354\246\235 \354\275\224\353\223\234.md" "b/content/\353\251\224\354\213\234\354\247\200 \354\235\270\354\246\235 \354\275\224\353\223\234.md" new file mode 100644 index 00000000..f767aeb4 --- /dev/null +++ "b/content/\353\251\224\354\213\234\354\247\200 \354\235\270\354\246\235 \354\275\224\353\223\234.md" @@ -0,0 +1,164 @@ +--- +draft: true +tags: +- 보안 +- 암호학 +- 인증 +description: 메시지 인증 코드(MAC)의 개념, 작동 원리, 활용 사례에 대한 설명 +--- +메시지 인증 코드(Message Authentication Code, MAC)는 메시지의 무결성을 검증하고 송신자를 인증하기 위한 암호학적 기술입니다. 비밀 키를 사용하여 메시지에 대한 인증 태그를 생성하며, 수신자는 동일한 키로 메시지의 변조 여부를 확인할 수 있습니다. MAC은 데이터 전송 과정에서 발생할 수 있는 우발적 또는 의도적인 변경을 탐지하는 데 중요한 역할을 합니다. + +## MAC의 기본 개념 + +메시지 인증 코드는 다음과 같은 세 가지 알고리즘으로 구성됩니다: + +1. **키 생성 알고리즘**: 송신자와 수신자가 공유하는 비밀 키를 생성합니다. +2. **서명 알고리즘**: 메시지와 비밀 키를 입력으로 받아 MAC 태그를 생성합니다. +3. **검증 알고리즘**: 메시지, MAC 태그, 비밀 키를 입력으로 받아 메시지의 무결성을 검증합니다. + +MAC은 [[해시 함수]]와 유사하지만, 비밀 키를 사용한다는 점에서 차이가 있습니다. 일반적인 해시 함수는 누구나 계산할 수 있지만, MAC은 비밀 키를 알고 있는 당사자만 생성하고 검증할 수 있습니다. + +## MAC의 작동 원리 + +```mermaid +sequenceDiagram + participant A as 송신자 + participant B as 수신자 + + Note over A,B: 사전에 공유된 비밀 키 K + + A->>A: 메시지 M 준비 + A->>A: MAC(K, M) 계산 + A->>B: 메시지 M과 MAC 태그 전송 + + B->>B: 수신된 메시지 M'에 대해 MAC(K, M') 계산 + B->>B: 수신된 MAC 태그와 계산된 MAC 태그 비교 + + Note over B: 두 태그가 일치하면 메시지 무결성 확인
일치하지 않으면 메시지 변조 감지 + +``` + +1. 송신자와 수신자는 사전에 안전한 방법으로 비밀 키(K)를 공유합니다. +2. 송신자는 메시지(M)와 비밀 키(K)를 MAC 알고리즘에 입력하여 MAC 태그를 생성합니다. +3. 송신자는 원본 메시지와 생성된 MAC 태그를 수신자에게 전송합니다. +4. 수신자는 받은 메시지와 공유된 비밀 키를 사용하여 자신의 MAC 태그를 계산합니다. +5. 수신자는 자신이 계산한 MAC 태그와 송신자로부터 받은 MAC 태그를 비교합니다. +6. 두 태그가 일치한다면, 메시지는 변조되지 않았고 인증된 송신자로부터 온 것임을 확인할 수 있습니다. + +## MAC의 주요 특성 + +### 1. 무결성 보장 + +MAC은 메시지가 전송 과정에서 변경되지 않았음을 보장합니다. 공격자가 메시지를 수정하면 MAC 태그가 일치하지 않게 됩니다. + +### 2. 인증 제공 + +MAC은 메시지가 인증된 송신자로부터 왔음을 보장합니다. 비밀 키를 알고 있는 사람만이 유효한 MAC 태그를 생성할 수 있기 때문입니다. + +### 3. 부인 방지 기능 부재 + +MAC은 송신자와 수신자 모두 같은 키를 공유하기 때문에, 제3자에게 메시지의 출처를 증명할 수 없습니다. 이는 [[디지털 서명]]과의 주요 차이점입니다. + +### 4. 효율성 + +MAC은 [[공개 키 암호화]] 기반의 인증 방식에 비해 계산이 빠르고 효율적입니다. + +## 주요 MAC 알고리즘 + +### 1. HMAC (Hash-based Message Authentication Code) + +가장 널리 사용되는 MAC 알고리즘으로, [[SHA-256]]과 같은 표준 암호화 해시 함수와 비밀 키를 결합하여 사용합니다. HMAC은 해시 함수의 내부 동작을 변경하지 않고도 안전한 MAC을 구현할 수 있습니다. + +```java +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class HMACExample { + public static String calculateHMAC(String data, String key) throws Exception { + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + hmac.init(secretKey); + byte[] hmacBytes = hmac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hmacBytes); + } +} +``` + +### 2. CMAC (Cipher-based Message Authentication Code) + +블록 암호를 기반으로 하는 MAC 알고리즘으로, [[AES]]와 같은 블록 암호를 사용하여 MAC을 생성합니다. + +### 3. GMAC (Galois Message Authentication Code) + +[[GCM(Galois/Counter Mode)]]의 인증 부분만을 사용하는 MAC 알고리즘입니다. 암호화 없이 인증만 필요한 경우에 효율적입니다. + +## MAC의 활용 사례 + +### 1. 통신 보안 + +안전한 통신 프로토콜(TLS, SSH 등)에서 데이터 무결성을 보장하기 위해 사용됩니다. + +### 2. API 인증 + +웹 API에서 요청의 출처를 인증하고 내용의 무결성을 검증하기 위해 사용됩니다. [[JWT(JSON Web Token)]]의 일부로도 활용됩니다. + +```java +// Spring Security에서 HMAC을 사용한 JWT 설정 예시 +@Configuration +public class SecurityConfig { + @Bean + public JwtEncoder jwtEncoder() { + SecretKey key = Keys.hmacShaKeyFor(secretKeyBytes); + JWKSource jwkSource = new ImmutableJWKSet<>( + new JWKSet(new OctetSequenceKey.Builder(key).build()) + ); + return new NimbusJwtEncoder(jwkSource); + } +} +``` + +### 3. 데이터 무결성 검증 + +저장된 데이터의 무결성을 검증하기 위해 사용됩니다. 예를 들어, 데이터베이스에 저장된 중요 정보가 변경되지 않았는지 확인할 수 있습니다. + +### 4. 암호 저장 + +사용자 비밀번호를 안전하게 저장하기 위한 [[PBKDF2(Password-Based Key Derivation Function 2)]]와 같은 키 유도 함수의 기초로 활용됩니다. + +## MAC과 관련 개념의 비교 + +### MAC vs [[해시 함수]] + +- 해시 함수는 키가 없어 누구나 계산할 수 있지만, MAC은 비밀 키가 필요합니다. +- 해시 함수는 데이터 무결성만 제공하지만, MAC은 무결성과 인증을 모두 제공합니다. + +### MAC vs [[디지털 서명]] + +- MAC은 대칭 키를 사용하지만, 디지털 서명은 비대칭 키를 사용합니다. +- MAC은 송신자와 수신자만 검증할 수 있지만, 디지털 서명은 공개 키로 누구나 검증할 수 있습니다. +- MAC은 부인 방지 기능이 없지만, 디지털 서명은 부인 방지 기능을 제공합니다. + +### MAC vs [[AEAD(Authenticated Encryption with Associated Data)]] + +- MAC은 데이터 인증만 제공하지만, AEAD는 암호화와 인증을 동시에 제공합니다. +- AEAD는 일반적으로 MAC을 별도로 계산하지 않고 암호화 과정에서 인증 태그를 생성합니다. + +## 보안 고려사항 + +### 1. 키 관리 + +MAC의 보안은 비밀 키의 안전한 관리에 달려있습니다. 키가 노출되면 공격자가 유효한 MAC 태그를 생성할 수 있게 됩니다. + +### 2. 길이 확장 공격 + +일부 MAC 알고리즘은 길이 확장 공격에 취약할 수 있습니다. HMAC과 같이 이러한 공격에 저항력이 있는 알고리즘을 선택하는 것이 중요합니다. + +### 3. 재전송 공격 + +MAC은 메시지의 신선도(freshness)를 보장하지 않습니다. [[타임스탬프]]나 [[난스(Nonce)]]를 메시지에 포함시켜 재전송 공격을 방지해야 합니다. + +## 결론 + +메시지 인증 코드(MAC)는 데이터 무결성과 송신자 인증을 제공하는 필수적인 암호화 도구입니다. 대칭 키를 사용하는 MAC은 계산 효율성이 높고 구현이 간단하여 다양한 보안 응용 프로그램에서 널리 사용됩니다. 그러나 부인 방지 기능이 필요한 경우에는 디지털 서명과 같은 비대칭 암호화 기술과 함께 사용해야 합니다. 적절한 키 관리와 현대적인 MAC 알고리즘을 선택함으로써 안전한 메시지 인증 시스템을 구축할 수 있습니다. \ No newline at end of file diff --git "a/content/\354\225\204\355\214\214\354\271\230 \354\271\264\355\224\204\354\271\264(Apache Kafka).md" "b/content/\354\225\204\355\214\214\354\271\230 \354\271\264\355\224\204\354\271\264(Apache Kafka).md" new file mode 100644 index 00000000..7ab02ea2 --- /dev/null +++ "b/content/\354\225\204\355\214\214\354\271\230 \354\271\264\355\224\204\354\271\264(Apache Kafka).md" @@ -0,0 +1,393 @@ +아파치 카프카(Apache Kafka)는 LinkedIn에서 개발되어 2011년 오픈소스로 공개된 분산 [[이벤트 스트리밍(Event Streaming)]] 플랫폼입니다. 이벤트 스트리밍이란 데이터를 실시간으로 지속적으로 생성하고 처리하는 방식을 의미합니다. 카프카는 대용량 데이터를 높은 처리량(throughput)과 낮은 지연시간(latency)으로 안정적으로 처리할 수 있도록 설계되었습니다. + +카프카는 세 가지 핵심 기능을 제공합니다: + +1. **데이터 스트림 발행(publish)과 구독(subscribe)**: 다양한 시스템과 애플리케이션 간의 데이터 스트림을 안정적으로 전송합니다. +2. **데이터 스트림 저장**: 내구성을 가진 분산 저장소에 데이터 스트림을 지속적으로 저장합니다. +3. **데이터 스트림 처리**: 데이터가 발생할 때 실시간으로 처리합니다. + +## 2. 카프카의 주요 개념 + +### 2.1 카프카 아키텍처 + +카프카는 다음과 같은 핵심 컴포넌트로 구성됩니다: + +- **브로커(Broker)**: 카프카 서버로, 메시지를 저장하고 전달하는 역할을 합니다. +- **주키퍼(ZooKeeper)**: 카프카 클러스터의 메타데이터를 관리하고 브로커의 상태를 모니터링합니다. (최신 버전에서는 KRaft 모드로 주키퍼 의존성 제거 가능) +- **프로듀서(Producer)**: 메시지를 생성하여 브로커에 전송합니다. +- **컨슈머(Consumer)**: 브로커로부터 메시지를 읽어들입니다. +- **토픽(Topic)**: 메시지가 저장되는 카테고리입니다. +- **파티션(Partition)**: 토픽을 여러 부분으로 나누어 병렬 처리를 가능하게 합니다. + +아래 다이어그램은 카프카의 기본 아키텍처를 보여줍니다: + +```mermaid +graph TD + P[프로듀서] -->|메시지 발행| B1[브로커 1] + P -->|메시지 발행| B2[브로커 2] + P -->|메시지 발행| B3[브로커 3] + B1 -->|메시지 소비| C[컨슈머] + B2 -->|메시지 소비| C + B3 -->|메시지 소비| C + Z[ZooKeeper/KRaft] -.->|클러스터 관리| B1 + Z -.->|클러스터 관리| B2 + Z -.->|클러스터 관리| B3 +``` + +### 2.2 토픽과 파티션 + +**[[카프카 토픽(Topic)|토픽]]** 은 카프카에서 메시지가 저장되는 논리적인 채널입니다. 각 토픽은 여러 **[[카프카 파티션(Partition)|파티션]]** 으로 분할될 수 있으며, 이를 통해 병렬 처리가 가능해집니다. + +파티션의 특징: + +- 각 파티션은 순서가 보장된 메시지 시퀀스입니다. +- 파티션 내부의 각 메시지는 '오프셋(offset)'이라는 고유 식별자를 가집니다. +- 새로운 메시지는 항상 파티션의 끝에 추가됩니다(append-only). +- 파티션은 여러 브로커에 분산 저장되어 고가용성과 확장성을 제공합니다. + +```mermaid +graph LR + subgraph "Topic A" + direction TB + subgraph "Partition 0" + P0M0[메시지 0] --> P0M1[메시지 1] --> P0M2[메시지 2] + end + subgraph "Partition 1" + P1M0[메시지 0] --> P1M1[메시지 1] + end + subgraph "Partition 2" + P2M0[메시지 0] --> P2M1[메시지 1] --> P2M2[메시지 2] --> P2M3[메시지 3] + end + end +``` + +### 2.3 프로듀서와 컨슈머 + +**프로듀서(Producer)**는 특정 토픽에 메시지를 발행합니다. 프로듀서는 메시지 키와 파티셔닝 전략을 사용하여 메시지가 어떤 파티션으로 전송될지 결정할 수 있습니다. 동일한 키를 가진 메시지는 항상 같은 파티션으로 전송됩니다. + +**컨슈머(Consumer)**는 하나 이상의 토픽을 구독하고 메시지를 읽어들입니다. 각 컨슈머는 메시지를 읽은 후 현재 오프셋을 기록하여 중복 처리를 방지합니다. + +컨슈머 그룹(Consumer Group)을 통해 여러 컨슈머가 토픽의 파티션을 나누어 처리할 수 있습니다. 이를 통해 병렬 처리와 고가용성을 확보할 수 있습니다. + +```mermaid +graph TD + subgraph "토픽 A" + P0[파티션 0] + P1[파티션 1] + P2[파티션 2] + end + + subgraph "컨슈머 그룹 X" + C0[컨슈머 0] + C1[컨슈머 1] + end + + P0 -->|읽기| C0 + P1 -->|읽기| C0 + P2 -->|읽기| C1 +``` + +## 3. 카프카의 핵심 특징 + +### 3.1 고가용성과 내구성 + +카프카는 다음과 같은 방식으로 고가용성과 내구성을 보장합니다: + +- **복제(Replication)**: 각 파티션은 여러 브로커에 복제하여 저장됩니다. 복제 계수(replication factor)를 통해 복제본 수를 지정할 수 있습니다. +- **리더와 팔로워(Leader and Follower)**: 각 파티션은 한 개의 리더와 여러 개의 팔로워를 가집니다. 리더는 읽기와 쓰기를 담당하고, 팔로워는 리더의 데이터를 복제합니다. +- **자동 복구(Automatic Recovery)**: 브로커가 실패하면 카프카는 자동으로 새로운 리더를 선출하고 데이터 복제를 재조정합니다. + +### 3.2 확장성 + +카프카는 수평적 확장이 쉽습니다: + +- **브로커 추가**: 새로운 브로커를 클러스터에 추가하여 처리 용량을 늘릴 수 있습니다. +- **파티션 확장**: 토픽의 파티션 수를 증가시켜 병렬 처리 능력을 향상시킬 수 있습니다. + +### 3.3 성능 + +카프카의 높은 성능은 다음과 같은 설계에서 비롯됩니다: + +- **배치 처리(Batch Processing)**: 메시지를 개별적으로 처리하지 않고 배치로 처리하여 네트워크 왕복 시간을 줄입니다. +- **제로 카피(Zero Copy)**: 커널 수준에서 디스크에서 네트워크로 데이터를 직접 전송하여 CPU 오버헤드를 최소화합니다. +- **페이지 캐시(Page Cache)**: 운영체제의 페이지 캐시를 활용하여 디스크 I/O를 최적화합니다. +- **순차적 I/O**: 메시지를 순차적으로 디스크에 쓰고 읽어 랜덤 액세스보다 높은 성능을 제공합니다. + +## 4. 카프카 사용 사례 + +### 4.1 메시징 시스템 + +카프카는 기존의 메시징 시스템보다 높은 처리량과 내구성을 제공합니다. 여러 생산자와 소비자 간의 비동기 통신에 적합합니다. + +### 4.2 로그 집계 + +다양한 서비스에서 생성되는 로그를 중앙 집중화하고 처리할 수 있습니다. 로그를 수집하여 Elasticsearch, Hadoop 또는 다른 데이터 스토리지 시스템으로 전송할 수 있습니다. + +### 4.3 스트림 처리 + +카프카 스트림즈(Kafka Streams) API를 사용하여 실시간으로 데이터를 변환하고 처리할 수 있습니다. 복잡한 이벤트 처리, 실시간 분석, 데이터 변환 등에 사용됩니다. + +### 4.4 이벤트 소싱 + +시스템의 상태 변화를 이벤트로 저장하는 [[이벤트 소싱(Event Sourcing)]] 아키텍처에 적합합니다. 카프카의 지속적인 로그 저장 능력은 이벤트 소싱에 이상적입니다. + +### 4.5 데이터 파이프라인 + +Kafka Connect를 사용하여 다양한 소스에서 데이터를 수집하고 다양한 싱크로 데이터를 전송하는 ETL(Extract, Transform, Load) 파이프라인을 구축할 수 있습니다. + +## 5. 카프카 자바 프로그래밍 예제 + +### 5.1 프로듀서 예제 + +```java +import org.apache.kafka.clients.producer.*; +import java.util.Properties; + +public class SimpleProducer { + public static void main(String[] args) { + // 프로듀서 설정 + Properties props = new Properties(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + + Producer producer = new KafkaProducer<>(props); + + // 메시지 생성 및 전송 + for (int i = 0; i < 10; i++) { + String key = "key-" + i; + String value = "value-" + i; + + ProducerRecord record = new ProducerRecord<>("my-topic", key, value); + + // 비동기 전송 + producer.send(record, new Callback() { + @Override + public void onCompletion(RecordMetadata metadata, Exception exception) { + if (exception == null) { + System.out.println("메시지 전송 성공: " + + "토픽=" + metadata.topic() + + ", 파티션=" + metadata.partition() + + ", 오프셋=" + metadata.offset()); + } else { + System.err.println("메시지 전송 실패: " + exception.getMessage()); + } + } + }); + } + + // 모든 요청 완료 대기 및 자원 해제 + producer.flush(); + producer.close(); + } +} +``` + +### 5.2 컨슈머 예제 + +```java +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.serialization.StringDeserializer; +import java.time.Duration; +import java.util.Arrays; +import java.util.Properties; + +public class SimpleConsumer { + public static void main(String[] args) { + // 컨슈머 설정 + Properties props = new Properties(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("group.id", "my-consumer-group"); + props.put("key.deserializer", StringDeserializer.class.getName()); + props.put("value.deserializer", StringDeserializer.class.getName()); + props.put("auto.offset.reset", "earliest"); + props.put("enable.auto.commit", "false"); + + Consumer consumer = new KafkaConsumer<>(props); + + // 토픽 구독 + consumer.subscribe(Arrays.asList("my-topic")); + + try { + while (true) { + // 메시지 폴링 + ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); + + for (ConsumerRecord record : records) { + System.out.println("메시지 수신: " + + "토픽=" + record.topic() + + ", 파티션=" + record.partition() + + ", 오프셋=" + record.offset() + + ", 키=" + record.key() + + ", 값=" + record.value()); + } + + // 오프셋 수동 커밋 + consumer.commitAsync(new OffsetCommitCallback() { + @Override + public void onComplete(Map offsets, Exception exception) { + if (exception != null) { + System.err.println("커밋 실패: " + exception.getMessage()); + } + } + }); + } + } finally { + consumer.close(); + } + } +} +``` + +## 6. 스프링 부트와 카프카 통합 + +스프링 부트는 카프카와의 통합을 위한 편리한 추상화를 제공합니다. + +### 6.1 의존성 추가 + +```xml + + org.springframework.kafka + spring-kafka + +``` + +### 6.2 설정 + +```properties +# application.properties +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.consumer.group-id=my-consumer-group +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer +``` + +### 6.3 스프링 부트 프로듀서 예제 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Service; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@Service +public class KafkaProducerService { + + private final KafkaTemplate kafkaTemplate; + + @Autowired + public KafkaProducerService(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void sendMessage(String topic, String key, String message) { + ListenableFuture> future = + kafkaTemplate.send(topic, key, message); + + future.addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + System.out.println("메시지 전송 성공: " + + "토픽=" + result.getRecordMetadata().topic() + + ", 파티션=" + result.getRecordMetadata().partition() + + ", 오프셋=" + result.getRecordMetadata().offset()); + } + + @Override + public void onFailure(Throwable ex) { + System.err.println("메시지 전송 실패: " + ex.getMessage()); + } + }); + } +} +``` + +### 6.4 스프링 부트 컨슈머 예제 + +```java +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Service; + +@Service +public class KafkaConsumerService { + + @KafkaListener(topics = "my-topic", groupId = "my-consumer-group") + public void listen(@Payload String message, + @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, + @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition, + @Header(KafkaHeaders.OFFSET) long offset, + @Header(KafkaHeaders.RECEIVED_KEY) String key) { + + System.out.println("메시지 수신: " + + "토픽=" + topic + + ", 파티션=" + partition + + ", 오프셋=" + offset + + ", 키=" + key + + ", 값=" + message); + + // 비즈니스 로직 처리 + } +} +``` + +## 7. 카프카 운영 및 모니터링 + +### 7.1 중요 설정 파라미터 + +카프카의 성능과 신뢰성에 영향을 미치는 주요 설정 파라미터입니다: + +1. **브로커 설정**: + + - `log.retention.hours`: 로그(데이터)를 보관하는 시간 (기본값: 168시간/7일) + - `num.partitions`: 토픽 생성 시 기본 파티션 수 (기본값: 1) + - `default.replication.factor`: 기본 복제 계수 (기본값: 1) +2. **프로듀서 설정**: + + - `acks`: 메시지 전송 확인 수준 (0, 1, all) + - `batch.size`: 배치 처리 크기 + - `linger.ms`: 배치 처리 대기 시간 +3. **컨슈머 설정**: + + - `fetch.min.bytes`: 최소 페치 바이트 수 + - `fetch.max.wait.ms`: 최대 페치 대기 시간 + - `max.poll.records`: 한 번의 폴링에서 가져올 최대 레코드 수 + +### 7.2 모니터링 및 관리 도구 + +카프카 클러스터의 상태와 성능을 모니터링하기 위한 도구들: + +1. **카프카 관리자 도구(Kafka Manager)**: LinkedIn에서 개발한 오픈소스 웹 인터페이스로, 카프카 클러스터를 관리하고 모니터링할 수 있습니다. +2. **Confluent Control Center**: Confluent에서 제공하는 상용 모니터링 및 관리 도구입니다. +3. **Prometheus & Grafana**: 오픈소스 모니터링 스택으로, JMX 메트릭을 수집하고 시각화할 수 있습니다. +4. **카프카 CLI 도구**: 토픽 생성, 소비자 그룹 관리 등 다양한 작업을 수행할 수 있는 명령줄 도구입니다. + +### 7.3 성능 튜닝 팁 + +카프카의 성능을 최적화하기 위한 팁: + +1. **적절한 파티션 수 설정**: 너무 많은 파티션은 오버헤드를 증가시키고, 너무 적은 파티션은 병렬 처리 능력을 제한합니다. +2. **하드웨어 최적화**: SSD 디스크, 충분한 메모리, 고속 네트워크를 사용합니다. +3. **배치 설정 최적화**: 프로듀서와 컨슈머의 배치 크기와 대기 시간을 조정합니다. +4. **압축 사용**: 네트워크 대역폭을 줄이기 위해 메시지 압축을 활성화합니다. +5. **적절한 복제 계수 설정**: 데이터 내구성과 가용성을 확보하기 위해 적절한 복제 계수를 설정합니다. + +## 8. 결론 + +아파치 카프카는 분산 이벤트 스트리밍 플랫폼으로, 실시간 데이터 처리, 마이크로서비스 간 통신, 로그 집계, 이벤트 소싱 등 다양한 사용 사례에 적합합니다. 높은 처리량, 낮은 지연 시간, 내구성, 확장성을 갖춘 카프카는 현대적인 데이터 중심 애플리케이션에 필수적인 구성 요소가 되었습니다. + +카프카를 효과적으로 활용하려면 아키텍처의 기본 개념을 이해하고, 적절한 설계 및 운영 방법을 학습하는 것이 중요합니다. 이 가이드가 카프카에 대한 이해를 돕고, 실제 프로젝트에 적용할 수 있는 지식을 제공했기를 바랍니다. + +## 9. 관련 노트 + +- [[이벤트 스트리밍(Event Streaming)]] +- [[이벤트 소싱(Event Sourcing)]] +- [[분산 시스템 설계]] +- [[마이크로서비스 아키텍처]] \ No newline at end of file diff --git "a/content/\354\227\220\353\237\254 \355\225\270\353\223\244\353\247\201(Error Handling).md" "b/content/\354\227\220\353\237\254 \355\225\270\353\223\244\353\247\201(Error Handling).md" new file mode 100644 index 00000000..a19eb78f --- /dev/null +++ "b/content/\354\227\220\353\237\254 \355\225\270\353\223\244\353\247\201(Error Handling).md" @@ -0,0 +1,458 @@ +소프트웨어 개발에서 에러 핸들링은 안정적이고 견고한 애플리케이션을 만드는 데 필수적인 요소입니다. 적절한 에러 처리는 프로그램이 예상치 못한 상황에서도 우아하게 대응하고, 디버깅을 용이하게 하며, 사용자 경험을 향상시킵니다. 이 글에서는 자바 개발자를 위한 에러와 예외 처리의 기본 개념부터 고급 기법까지 체계적으로 살펴보겠습니다. +## 에러와 예외의 기본 개념 + +자바에서는 프로그램 실행 중 발생할 수 있는 문제를 크게 에러(Error)와 예외(Exception) 두 가지로 분류합니다. + +### 에러(Error) + +에러는 일반적으로 시스템 레벨의 심각한 문제를 나타내며, 애플리케이션 코드에서 처리하기 어렵거나 불가능한 상황을 의미합니다. + +주요 특징: + +- JVM이나 하드웨어 관련 문제에서 발생 +- 대부분 복구 불가능한 상황 +- 애플리케이션에서 잡아서 처리하지 않음 +- `java.lang.Error` 클래스의 하위 클래스 + +예시: + +- `OutOfMemoryError`: 메모리 부족 +- `StackOverflowError`: 스택 메모리 초과 +- `NoClassDefFoundError`: 클래스 정의를 찾을 수 없음 + +### 예외(Exception) + +예외는 프로그램 실행 중 발생하는 예상 가능한(또는 예상치 못한) 경우를 나타내며, 애플리케이션 코드에서 처리할 수 있습니다. + +주요 특징: + +- 프로그램 로직 실행 중 발생하는 문제 +- 적절한 처리를 통해 복구 가능한 경우가 많음 +- `try-catch` 구문으로 잡아서 처리할 수 있음 +- `java.lang.Exception` 클래스의 하위 클래스 + +## 자바의 예외 계층 구조 + +자바의 모든 예외와 에러는 `Throwable` 클래스를 상속합니다. 이 계층 구조는 예외 처리 방식을 결정하는 데 중요한 역할을 합니다. + +```mermaid +classDiagram + Throwable <|-- Error + Throwable <|-- Exception + Exception <|-- RuntimeException + Exception <|-- IOException + Exception <|-- SQLException + RuntimeException <|-- NullPointerException + RuntimeException <|-- ArrayIndexOutOfBoundsException + RuntimeException <|-- IllegalArgumentException + + class Throwable { + +String message + +Throwable cause + +printStackTrace() + +getMessage() + +getCause() + } + + class Error { + +OutOfMemoryError + +StackOverflowError + +NoClassDefFoundError + } + + class Exception { + +checked exceptions + } + + class RuntimeException { + +unchecked exceptions + } +``` + +### 체크 예외(Checked Exception) + +체크 예외는 컴파일 시점에 처리 여부를 검사하는 예외입니다. 개발자는 이러한 예외를 명시적으로 처리하거나 메서드 시그니처에 선언해야 합니다. + +주요 특징: + +- `Exception` 클래스를 직접 상속하는 하위 클래스들 +- 컴파일러가 예외 처리 여부를 강제함 +- 메서드에서 발생 가능한 체크 예외는 반드시 `throws` 절에 선언되어야 함 + +예시: + +- `IOException`: 입출력 작업 중 발생하는 예외 +- `SQLException`: 데이터베이스 액세스 관련 예외 +- `ClassNotFoundException`: 클래스를 찾을 수 없을 때 발생하는 예외 + +```java +public void readFile(String path) throws IOException { + BufferedReader reader = new BufferedReader(new FileReader(path)); + // 파일 읽기 로직 + reader.close(); +} +``` + +### 언체크 예외(Unchecked Exception) + +언체크 예외는 컴파일 시점에 처리 여부를 검사하지 않는 예외입니다. `RuntimeException`과 그 하위 클래스들이 여기에 해당합니다. + +주요 특징: + +- `RuntimeException` 클래스의 하위 클래스들 +- 컴파일러가 예외 처리를 강제하지 않음 +- 명시적인 처리나 선언이 필요 없음 +- 주로 프로그래밍 오류를 나타냄 + +예시: + +- `NullPointerException`: 널 참조를 역참조할 때 발생 +- `ArrayIndexOutOfBoundsException`: 배열 인덱스가 범위를 벗어날 때 발생 +- `IllegalArgumentException`: 메서드에 부적절한 인수를 전달했을 때 발생 + +```java +public int divide(int a, int b) { + // ArithmeticException은 언체크 예외이므로 명시적 선언 불필요 + return a / b; // b가 0이면 ArithmeticException 발생 +} +``` + +## 예외 처리 메커니즘 + +자바는 예외를 처리하기 위한 다양한 메커니즘을 제공합니다. + +### try-catch-finally + +가장 기본적인 예외 처리 방법은 `try-catch-finally` 블록을 사용하는 것입니다. + +```java +try { + // 예외가 발생할 수 있는 코드 + FileReader file = new FileReader("file.txt"); + // 파일 처리 로직 +} catch (FileNotFoundException e) { + // FileNotFoundException 처리 + System.err.println("파일을 찾을 수 없습니다: " + e.getMessage()); +} catch (IOException e) { + // IOException 처리 + System.err.println("파일 읽기 중 오류 발생: " + e.getMessage()); +} finally { + // 예외 발생 여부와 관계없이 항상 실행되는 코드 + // 주로 리소스 정리에 사용 + if (file != null) { + try { + file.close(); + } catch (IOException e) { + System.err.println("파일 닫기 실패: " + e.getMessage()); + } + } +} +``` + +### try-with-resources + +Java 7부터 도입된 `try-with-resources` 구문은 `AutoCloseable` 인터페이스를 구현한 리소스를 자동으로 닫아주는 기능을 제공합니다. + +```java +try (FileReader file = new FileReader("file.txt"); + BufferedReader reader = new BufferedReader(file)) { + // 파일 처리 로직 + String line = reader.readLine(); + // 추가 로직 +} catch (IOException e) { + System.err.println("파일 처리 중 오류: " + e.getMessage()); +} +// 리소스는 자동으로 닫힘 +``` + +이 방식의 장점: + +- 코드가 간결해짐 +- 리소스 누수(resource leak) 방지 +- 예외가 발생해도 리소스가 안전하게 닫힘 + +### 멀티 catch + +Java 7부터는 여러 예외를 하나의 catch 블록에서 처리할 수 있는 멀티 catch 구문을 지원합니다. + +```java +try { + // 예외 발생 가능 코드 +} catch (FileNotFoundException | SQLException e) { + // 두 예외를 동일한 방식으로 처리 + System.err.println("파일 또는 DB 오류: " + e.getMessage()); +} +``` + +### 예외 전파(Exception Propagation) + +예외가 발생하면 해당 예외는 콜 스택을 따라 상위 메서드로 전파됩니다. 적절한 catch 블록을 만나기 전까지 이 과정은 계속됩니다. + +```java +public void method3() { + int[] arr = new int[5]; + arr[10] = 50; // ArrayIndexOutOfBoundsException 발생 +} + +public void method2() { + method3(); // 예외가 method2로 전파됨 +} + +public void method1() { + try { + method2(); // 예외가 method1으로 전파됨 + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("배열 인덱스 오류 처리"); + } +} +``` + +## 효과적인 예외 처리 전략 + +효과적인 예외 처리는 애플리케이션의 안정성과 유지보수성을 크게 향상시킵니다. + +### 예외의 적절한 계층 설계 + +애플리케이션의 도메인에 맞는 예외 계층을 설계하는 것이 중요합니다. + +```java +// 기본 애플리케이션 예외 +public class ApplicationException extends Exception { + public ApplicationException(String message) { + super(message); + } + + public ApplicationException(String message, Throwable cause) { + super(message, cause); + } +} + +// 비즈니스 로직 예외 +public class BusinessException extends ApplicationException { + public BusinessException(String message) { + super(message); + } +} + +// 데이터 액세스 예외 +public class DataAccessException extends ApplicationException { + public DataAccessException(String message, Throwable cause) { + super(message, cause); + } +} +``` + +이러한 계층적 접근 방식의 장점: + +- 예외의 분류가 명확해짐 +- 특정 유형의 예외만 선택적으로 처리 가능 +- 일관된 예외 처리 전략 구현 가능 + +### 예외 변환(Exception Translation) + +하위 레벨의 예외를 상위 레벨의 추상화된 예외로 변환하는 것이 유용할 수 있습니다. + +```java +public User findUserById(Long id) throws UserNotFoundException { + try { + return userRepository.findById(id); + } catch (SQLException e) { + // 하위 레벨 예외를 의미 있는 비즈니스 예외로 변환 + throw new UserNotFoundException("ID가 " + id + "인 사용자를 찾을 수 없습니다", e); + } +} +``` + +예외 변환의 이점: + +- 추상화 계층 유지: 상위 계층은 하위 계층의 구현 세부 사항을 알 필요가 없음 +- 의미 있는 컨텍스트 제공: 비즈니스 로직에 맞는 예외 메시지와 정보 제공 +- 예외 처리의 일관성 유지 + +### 원인 체인(Cause Chain) + +예외를 변환할 때는 원래 예외를 원인(cause)으로 포함하는 것이 중요합니다. + +```java +try { + // 코드 +} catch (SQLException e) { + throw new DataAccessException("데이터베이스 접근 중 오류 발생", e); +} +``` + +이를 통해: + +- 원래 발생한 예외의 정보를 보존할 수 있음 +- 디버깅 시 전체 예외 체인을 추적할 수 있음 +- 상세한 오류 정보를 로깅할 수 있음 + +### 실패 원자성(Failure Atomicity) + +메서드가 예외를 던지는 경우, 객체의 상태를 호출 전과 동일하게 유지해야 합니다. 이것을 '실패 원자성'이라고 합니다. + +```java +public void transferMoney(Account from, Account to, BigDecimal amount) + throws InsufficientFundsException { + + BigDecimal originalFromBalance = from.getBalance(); + + try { + // 출금 계좌에서 금액 차감 + from.withdraw(amount); + + // 입금 계좌에 금액 추가 (예외 발생 가능) + to.deposit(amount); + } catch (Exception e) { + // 예외 발생 시 출금 계좌 상태 복원 + from.setBalance(originalFromBalance); + throw e; // 예외 다시 던지기 + } +} +``` + +실패 원자성을 보장하는 방법: + +- 연산 전에 객체 상태 저장 +- 실패 시 원래 상태로 복원 +- 트랜잭션 사용 +- 불변 객체 활용 + +## 커스텀 예외 설계하기 + +애플리케이션에 특화된 커스텀 예외를 설계하는 것은 명확한 오류 처리와 비즈니스 로직 표현에 도움이 됩니다. + +### 커스텀 예외 생성 지침 + +1. **의미 있는 이름 사용**: 예외 이름이 문제를 명확하게 설명해야 함 +2. **적절한 상위 클래스 선택**: 체크 예외나 언체크 예외 중 적절한 것 선택 +3. **충분한 컨텍스트 제공**: 문제 해결에 도움이 되는 정보 포함 +4. **직렬화 가능성 고려**: 분산 환경에서 사용할 경우 `Serializable` 구현 + +### 커스텀 예외 예시 + +```java +public class OrderNotFoundException extends RuntimeException { + private final Long orderId; + + public OrderNotFoundException(Long orderId) { + super("주문 ID: " + orderId + "를 찾을 수 없습니다"); + this.orderId = orderId; + } + + public Long getOrderId() { + return orderId; + } +} +``` + +### 체크 예외 vs 언체크 예외 선택 기준 + +**체크 예외가 적합한 경우**: + +- 호출자가 예외를 복구할 수 있을 때 +- 호출자에게 예외 처리를 강제하고 싶을 때 +- 비즈니스 로직의 일부로서 예외적 상황을 표현할 때 + +**언체크 예외가 적합한 경우**: + +- 프로그래밍 오류를 나타낼 때 +- 복구가 불가능하거나 불필요할 때 +- 예외 선언이 메서드 시그니처를 지나치게 복잡하게 만들 때 +- 대부분의 클라이언트가 예외를 처리할 필요가 없을 때 + +## 스프링 프레임워크의 예외 처리 + +스프링 프레임워크는 예외 처리를 위한 다양한 메커니즘을 제공합니다. + +### @ExceptionHandler + +컨트롤러 내에서 발생하는 특정 예외를 처리하기 위한 메서드를 지정할 수 있습니다. + +```java +@Controller +public class UserController { + + @GetMapping("/users/{id}") + public User getUser(@PathVariable Long id) { + // 사용자 조회 로직 - 사용자가 없으면 예외 발생 + if (userNotFound) { + throw new UserNotFoundException(id); + } + return user; + } + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity handleUserNotFound(UserNotFoundException ex) { + ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage()); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } +} +``` + +### @ControllerAdvice와 @RestControllerAdvice + +여러 컨트롤러에 걸쳐 전역적으로 예외를 처리하려면 `@ControllerAdvice`나 `@RestControllerAdvice`를 사용합니다. + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity handleUserNotFound(UserNotFoundException ex) { + ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage()); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(DataIntegrityException.class) + public ResponseEntity handleDataIntegrity(DataIntegrityException ex) { + ErrorResponse error = new ErrorResponse("DATA_INTEGRITY_ERROR", ex.getMessage()); + return new ResponseEntity<>(error, HttpStatus.CONFLICT); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex) { + ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "서버 내부 오류가 발생했습니다"); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } +} +``` + +### 스프링의 예외 변환 + +스프링은 기술 특화적인 예외를 추상화된 예외로 자동 변환하는 메커니즘을 제공합니다. + +```java +// 스프링 데이터 JPA 예외 변환 설정 +@Configuration +public class PersistenceConfig { + + @Bean + public PersistenceExceptionTranslationPostProcessor exceptionTranslation() { + return new PersistenceExceptionTranslationPostProcessor(); + } +} +``` + +이 설정으로 JPA나 JDBC의 저수준 예외가 스프링의 `DataAccessException` 계층으로 변환됩니다. + +### ResponseStatusException + +Spring 5부터는 `ResponseStatusException`을 사용하여 HTTP 상태 코드를 직접 지정할 수 있습니다. + +```java +@GetMapping("/users/{id}") +public User getUser(@PathVariable Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, "ID가 " + id + "인 사용자를 찾을 수 없습니다")); +} +``` + +이 방식의 장점: + +- 간단한 예외 처리를 위한 보일러플레이트 코드 감소 +- 특정 엔드포인트에 대한 맞춤형 예외 처리 가능 +- 커스텀 예외 클래스 생성 필요성 감소 +## 에러 코드 +에러 코드에 대한 상세 설명은 [[에러코드]]를 참고해주세요 \ No newline at end of file diff --git "a/content/\354\227\220\353\237\254\354\275\224\353\223\234.md" "b/content/\354\227\220\353\237\254\354\275\224\353\223\234.md" new file mode 100644 index 00000000..56fd800e --- /dev/null +++ "b/content/\354\227\220\353\237\254\354\275\224\353\223\234.md" @@ -0,0 +1,228 @@ +에러코드는 소프트웨어 개발에서 발생하는 문제를 식별하고 분류하기 위한 표준화된 방법입니다. 잘 정의된 에러코드 시스템은 개발, 디버깅, 유지보수 과정에서 시간을 절약하고 문제 해결을 더 효율적으로 만들어 줍니다. 이 글에서는 에러코드의 기본 개념부터 실전 활용법까지 깊이 있게 다루겠습니다. + +## 에러코드의 중요성 + +에러코드가 왜 중요한지 생각해 보신 적이 있으신가요? 에러코드는 단순히 문제가 발생했음을 알리는 것 이상의 역할을 합니다. + +1. **명확한 문제 식별**: 구체적인 에러코드는 발생한 문제의 정확한 원인을 빠르게 파악할 수 있게 합니다. +2. **효율적인 디버깅**: 개발자가 로그를 검토할 때 에러코드는 빠른 문제 진단을 가능하게 합니다. +3. **사용자 경험 향상**: 최종 사용자에게 적절한 에러코드와 메시지를 제공하면 문제 해결을 위한 명확한 지침을 제공할 수 있습니다. +4. **문서화와 지식 공유**: 표준화된 에러코드는 팀 내 지식 공유와 문서화를 용이하게 합니다. +5. **시스템 모니터링**: 에러코드 패턴을 분석하여 시스템의 건강 상태를 모니터링할 수 있습니다. + +## 표준 HTTP 에러코드 + +웹 개발에서 가장 널리 사용되는 에러코드 시스템은 HTTP 상태 코드입니다. 이 코드들은 클라이언트와 서버 간의 통신 상태를 나타냅니다. + +### 주요 HTTP 에러코드 범주 + +- **1xx (정보)**: 요청이 수신되었으며 처리가 진행 중임을 나타냅니다. +- **2xx (성공)**: 요청이 성공적으로 처리되었음을 나타냅니다. +- **3xx (리다이렉션)**: 요청 완료를 위해 추가 작업이 필요함을 나타냅니다. +- **4xx (클라이언트 오류)**: 클라이언트 측의 오류로 인해 요청을 처리할 수 없음을 나타냅니다. +- **5xx (서버 오류)**: 서버 측의 오류로 인해 유효한 요청을 처리할 수 없음을 나타냅니다. +## Java 예외 처리와 에러코드 + +Java 프로그래밍에서는 예외(Exception)를 통해 오류 상황을 처리합니다. 여기에 에러코드 시스템을 결합하면 더 강력한 오류 처리 메커니즘을 구축할 수 있습니다. + +### 사용자 정의 예외 클래스 생성 + +```java +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode, String detail) { + super(errorCode.getMessage() + " : " + detail); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} +``` + +### 에러코드 열거형(Enum) 정의 + +```java +public enum ErrorCode { + + // 공통 에러코드 (1000번대) + INVALID_INPUT_VALUE(1001, "입력 값이 올바르지 않습니다"), + RESOURCE_NOT_FOUND(1002, "요청한 리소스를 찾을 수 없습니다"), + INTERNAL_SERVER_ERROR(1003, "서버 내부 오류가 발생했습니다"), + + // 사용자 관련 에러코드 (2000번대) + USER_NOT_FOUND(2001, "사용자를 찾을 수 없습니다"), + DUPLICATE_USER_ID(2002, "이미 사용 중인 아이디입니다"), + INVALID_PASSWORD(2003, "비밀번호가 올바르지 않습니다"), + + // 주문 관련 에러코드 (3000번대) + ORDER_NOT_FOUND(3001, "주문을 찾을 수 없습니다"), + INSUFFICIENT_STOCK(3002, "재고가 부족합니다"), + PAYMENT_FAILED(3003, "결제에 실패했습니다"); + + private final int code; + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} +``` + +### 예외 처리 활용 예시 + +```java +public class UserService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User findById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + } + + public void register(UserRegistrationDto dto) { + if (userRepository.existsByUsername(dto.getUsername())) { + throw new BusinessException(ErrorCode.DUPLICATE_USER_ID); + } + + if (!isValidPassword(dto.getPassword())) { + throw new BusinessException( + ErrorCode.INVALID_INPUT_VALUE, + "비밀번호는 최소 8자 이상, 특수문자를 포함해야 합니다" + ); + } + + // 사용자 등록 로직 + } + + private boolean isValidPassword(String password) { + // 비밀번호 유효성 검증 로직 + return password != null && password.length() >= 8 && containsSpecialChar(password); + } + + private boolean containsSpecialChar(String str) { + return str.matches(".*[!@#$%^&*(),.?\":{}|<>].*"); + } +} +``` + +## Spring에서의 에러코드 활용 + +Spring 프레임워크에서는 [[ControllerAdvice]]와 [[ExceptionHandler]]를 사용하여 전역적인 예외 처리를 구현할 수 있습니다. + +### 전역 예외 처리기 구현 + +```java +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException e) { + log.error("Business exception occurred: {}", e.getMessage()); + + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + + return new ResponseEntity<>(response, getHttpStatus(errorCode)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Unexpected exception occurred:", e); + + ErrorResponse response = new ErrorResponse( + ErrorCode.INTERNAL_SERVER_ERROR.getCode(), + ErrorCode.INTERNAL_SERVER_ERROR.getMessage() + ); + + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private HttpStatus getHttpStatus(ErrorCode errorCode) { + // 에러코드에 따라 적절한 HTTP 상태 코드 매핑 + int code = errorCode.getCode(); + if (code >= 1000 && code < 2000) { + return HttpStatus.BAD_REQUEST; + } else if (code >= 2000 && code < 3000) { + return code == 2001 ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; + } else if (code >= 3000 && code < 4000) { + return HttpStatus.BAD_REQUEST; + } + + return HttpStatus.INTERNAL_SERVER_ERROR; + } +} +``` + +### 에러 응답 클래스 + +```java +public class ErrorResponse { + + private final int code; + private final String message; + private final LocalDateTime timestamp; + + public ErrorResponse(int code, String message) { + this.code = code; + this.message = message; + this.timestamp = LocalDateTime.now(); + } + + // Getters... +} +``` + +## 에러코드 설계 전략 + +효과적인 에러코드 시스템을 설계하기 위한 전략을 살펴보겠습니다. + +### 1. 체계적인 분류 체계 수립 + +에러코드는 범주별로 구분하여 관리하는 것이 좋습니다. + +``` +1000-1999: 일반/공통 에러 +2000-2999: 사용자/인증 관련 에러 +3000-3999: 비즈니스 로직 에러 +4000-4999: 외부 시스템 연동 에러 +5000-5999: 데이터베이스 관련 에러 +9000-9999: 시스템 레벨 에러 +``` + +### 2. 에러코드 문서화 + +에러코드는 팀 내에서 공유되는 문서로 관리해야 합니다. Wiki나 공유 문서를 통해 모든 에러코드, 설명, 해결 방법을 명시하세요. + +### 3. 에러 메시지 설계 원칙 + +좋은 에러 메시지는 다음 특성을 갖추어야 합니다: + +- **명확성**: 문제가 무엇인지 정확히 설명 +- **행동 지향적**: 사용자가 취해야 할 다음 단계 제시 +- **기술적 세부사항 최소화**: 일반 사용자에게는 기술적 세부사항 제한 +- **일관성**: 애플리케이션 전체에서 일관된 형식과 톤 유지 diff --git "a/content/\354\233\271\355\233\205(Webhook).md" "b/content/\354\233\271\355\233\205(Webhook).md" new file mode 100644 index 00000000..43debbae --- /dev/null +++ "b/content/\354\233\271\355\233\205(Webhook).md" @@ -0,0 +1,258 @@ +--- +draft: false +tags: + - 웹훅 + - API + - 이벤트기반 + - 통합 +description: 웹훅(Webhook)의 개념부터 구현, 모범 사례까지 단계별로 설명하는 개발자 가이드입니다. +--- +--- +웹훅(Webhook)은 현대 웹 애플리케이션 개발에서 필수적인 요소로 자리잡았습니다. 이 글에서는 웹훅의 개념, 작동 원리, 구현 방법 및 보안 고려사항에 대해 자세히 살펴보겠습니다. 웹훅을 통해 실시간 데이터 통합이 얼마나 효율적으로 이루어질 수 있는지, 그리고 이를 어떻게 자신의 애플리케이션에 적용할 수 있는지 알아보겠습니다. + +## 웹훅이란? + +웹훅은 한 시스템에서 특정 이벤트가 발생했을 때 다른 시스템에 자동으로 알림을 보내는 방법입니다. 일반적인 API와 달리, 웹훅은 '역방향 API' 또는 '[[콜백 URL]]'이라고도 불립니다. 전통적인 API에서는 클라이언트가 서버에 요청을 보내고 응답을 기다리지만, 웹훅에서는 이벤트가 발생했을 때 서버가 클라이언트에게 데이터를 푸시합니다. + +### 웹훅 vs 전통적인 API 요청 + +전통적인 API 요청 방식은 클라이언트가 서버에 주기적으로 데이터를 요청하는 [[폴링(Polling)]] 방식을 사용합니다. 이 방식은 실시간 데이터가 필요하지 않은 경우에는 효과적이지만, 다음과 같은 단점이 있습니다: + +1. 불필요한 요청 증가: 변경사항이 없어도 계속해서 요청을 보냅니다. +2. 리소스 낭비: 서버와 클라이언트 모두 불필요한 요청 처리로 리소스를 소모합니다. +3. 지연 시간: 폴링 간격에 따라 실시간성이 제한됩니다. + +반면, 웹훅은 이벤트 기반 방식으로 작동합니다. 이벤트가 발생했을 때만 데이터를 전송하기 때문에 더 효율적이고 실시간성이 높습니다. + +## 웹훅의 작동 원리 + +웹훅의 기본 작동 원리는 비교적 단순합니다: + +1. 수신자(Receiver)가 이벤트 발신자(Sender)에게 콜백 URL을 등록합니다. +2. 이벤트 발신자에서 특정 이벤트가 발생하면 등록된 URL로 HTTP POST 요청을 보냅니다. +3. 수신자는 이 요청을 처리하고 적절한 응답을 반환합니다. + +```mermaid + sequenceDiagram participant 클라이언트 as 클라이언트(수신자) participant 서비스 as 서비스(발신자) participant 이벤트 as 이벤트 시스템 + +클라이언트->>서비스: 웹훅 URL 등록 (https://example.com/webhook) +서비스->>클라이언트: 등록 확인 (webhook_id) + +Note over 서비스,이벤트: 시간이 지남 + +이벤트->>서비스: 이벤트 발생 (예: 결제 완료) +서비스->>클라이언트: HTTP POST 요청 (이벤트 데이터 포함) +클라이언트->>서비스: 200 OK 응답 + +Note over 클라이언트: 이벤트 처리 +``` + +## 웹훅 구현하기 + +웹훅을 구현하는 과정은 크게 두 가지 측면으로 나눌 수 있습니다: + +1. 웹훅 제공자(Provider) 구현: 이벤트 발생 시 등록된 URL로 알림을 보내는 시스템 +2. 웹훅 소비자(Consumer) 구현: 웹훅 이벤트를 수신하고 처리하는 시스템 + +### 웹훅 소비자(Consumer) 구현 + +웹훅을 수신하는 엔드포인트를 구현하는 방법을 살펴보겠습니다. 이 예제에서는 스프링 부트를 사용하여 간단한 웹훅 수신기를 만들겠습니다: + +```java +@RestController +public class WebhookController { + + private static final Logger logger = LoggerFactory.getLogger(WebhookController.class); + + @PostMapping("/webhook") + public ResponseEntity receiveWebhook(@RequestBody String payload, + @RequestHeader HttpHeaders headers) { + // 웹훅 페이로드 로깅 + logger.info("웹훅 수신: {}", payload); + + // 시그니처 검증 (선택 사항) + if (!verifySignature(payload, headers)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature"); + } + + try { + // 페이로드 처리 로직 + processWebhookPayload(payload); + + // 성공 응답 + return ResponseEntity.ok("Webhook received successfully"); + } catch (Exception e) { + logger.error("웹훅 처리 오류", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error processing webhook"); + } + } + + private boolean verifySignature(String payload, HttpHeaders headers) { + // 서명 검증 로직 (보안 섹션에서 자세히 설명) + return true; // 예시 구현 + } + + private void processWebhookPayload(String payload) { + // 실제 비즈니스 로직 처리 + // 예: 결제 확인, 데이터베이스 업데이트, 알림 전송 등 + } +} +``` + +### 웹훅 제공자(Provider) 구현 + +웹훅을 제공하는 시스템을 구현할 때는 다음과 같은 요소들을 고려해야 합니다: + +1. 웹훅 등록 API +2. 웹훅 저장 시스템 +3. 이벤트 감지 및 처리 +4. 웹훅 전송 메커니즘 + +아래는 스프링 부트를 사용한 간단한 웹훅 제공자 구현의 예시입니다: + +```java +@Service +public class WebhookService { + + private final WebhookRepository webhookRepository; + private final RestTemplate restTemplate; + + public WebhookService(WebhookRepository webhookRepository, RestTemplate restTemplate) { + this.webhookRepository = webhookRepository; + this.restTemplate = restTemplate; + } + + public void registerWebhook(String url, String event) { + Webhook webhook = new Webhook(url, event); + webhookRepository.save(webhook); + } + + public void triggerWebhook(String event, Object data) { + List webhooks = webhookRepository.findByEvent(event); + + for (Webhook webhook : webhooks) { + try { + // 웹훅 페이로드 생성 + WebhookPayload payload = createPayload(event, data); + + // 서명 생성 (보안 섹션에서 자세히 설명) + String signature = generateSignature(payload); + + // HTTP 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + headers.set("X-Webhook-Signature", signature); + + // 웹훅 전송 + HttpEntity request = new HttpEntity<>(payload, headers); + ResponseEntity response = restTemplate.postForEntity(webhook.getUrl(), request, String.class); + + // 응답 처리 (재시도 로직 등) + if (response.getStatusCode().is2xxSuccessful()) { + // 성공 로깅 + } else { + // 실패 처리 + } + } catch (Exception e) { + // 예외 처리 및 재시도 로직 + } + } + } + + private WebhookPayload createPayload(String event, Object data) { + return new WebhookPayload(event, data, System.currentTimeMillis()); + } + + private String generateSignature(WebhookPayload payload) { + // 서명 생성 로직 + return "signature"; // 예시 구현 + } +} +``` + +## 웹훅 보안 + +웹훅을 사용할 때는 보안에 특히 주의해야 합니다. 웹훅 엔드포인트는 외부에 노출되어 있어 공격의 대상이 될 수 있습니다. 다음은 웹훅 보안을 위한 핵심 사항들입니다: + +### 서명 검증 + +웹훅 요청이 실제로 신뢰할 수 있는 소스에서 온 것인지 확인하기 위해 서명 검증을 구현합니다. 일반적인 방법은 [[HMAC(Hash-based Message Authentication Code)]]를 사용하는 것입니다: + +```java +private boolean verifySignature(String payload, HttpHeaders headers) { + String receivedSignature = headers.getFirst("X-Webhook-Signature"); + if (receivedSignature == null) { + return false; + } + + String secretKey = "your_secret_key"; // 안전하게 저장된 비밀키 + + try { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hmacBytes = mac.doFinal(payload.getBytes()); + String calculatedSignature = Base64.getEncoder().encodeToString(hmacBytes); + + return MessageDigest.isEqual(calculatedSignature.getBytes(), receivedSignature.getBytes()); + } catch (Exception e) { + return false; + } +} +``` + +### 추가 보안 조치 + +1. **HTTPS 사용**: 모든 웹훅 통신은 반드시 HTTPS를 통해 이루어져야 합니다. +2. **IP 필터링**: 알려진 IP 주소에서만 웹훅을 수신하도록 구성할 수 있습니다. +3. **요청 제한(Rate Limiting)**: 짧은 시간 동안 과도한 요청을 방지합니다. +4. **타임스탬프 검증**: 오래된 요청을 차단하여 재생 공격을 방지합니다. +5. **비밀 토큰**: URL에 비밀 토큰을 포함시켜 추가 보안 계층을 제공합니다. + +## 웹훅 모범 사례 + +웹훅을 효과적으로 사용하기 위한 몇 가지 모범 사례를 소개합니다: + +### 1. 멱등성 보장 + +웹훅 요청은 동일한 이벤트에 대해 여러 번 전송될 수 있습니다(재시도 등의 이유로). 이러한 경우에도 시스템이 올바르게 동작하도록 [[멱등성(Idempotency)]]을 보장해야 합니다. + +### 2. 재시도 메커니즘 구현 + +웹훅 전송이 실패할 경우 적절한 재시도 메커니즘을 구현하는 것이 중요합니다. 지수 백오프(exponential backoff) 전략을 사용하면 효과적입니다. + +```java +private void sendWithRetry(String url, WebhookPayload payload, int maxRetries) { + int retries = 0; + boolean success = false; + + while (!success && retries < maxRetries) { + try { + // 웹훅 전송 로직 + ResponseEntity response = restTemplate.postForEntity(url, payload, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + success = true; + } else { + // 재시도 전 대기 (지수 백오프) + long waitTime = (long) Math.pow(2, retries) * 1000; + Thread.sleep(waitTime); + retries++; + } + } catch (Exception e) { + // 재시도 전 대기 (지수 백오프) + long waitTime = (long) Math.pow(2, retries) * 1000; + try { + Thread.sleep(waitTime); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + retries++; + } + } + + if (!success) { + // 모든 재시도 실패 처리 + // 예: 실패 로그 기록, 알림 전송, 데드 레터 큐에 추가 등 + } +} +``` diff --git "a/content/\354\235\264\353\262\244\355\212\270 \354\212\244\355\212\270\353\246\254\353\260\215(Event Streaming).md" "b/content/\354\235\264\353\262\244\355\212\270 \354\212\244\355\212\270\353\246\254\353\260\215(Event Streaming).md" new file mode 100644 index 00000000..1807e0bf --- /dev/null +++ "b/content/\354\235\264\353\262\244\355\212\270 \354\212\244\355\212\270\353\246\254\353\260\215(Event Streaming).md" @@ -0,0 +1,182 @@ +이벤트 스트리밍은 실시간으로 발생하는 데이터를 지속적으로 생성, 수집, 처리, 저장 및 분석하는 데이터 관리 패러다임입니다. 이벤트 스트리밍에서 '이벤트'란 비즈니스, 시스템, 디바이스 등에서 발생하는 모든 형태의 데이터 변경이나 상태 업데이트를 의미합니다. + +이벤트 스트리밍은 데이터를 일괄 처리(batch processing)하는 기존 방식과 달리, 데이터가 발생하는 즉시 [[실시간 데이터 처리]]하는 것이 특징입니다. 이는 빠른 의사 결정과 반응이 필요한 현대 비즈니스 환경에 적합합니다. + +## 2. 이벤트 스트리밍의 핵심 개념 + +### 2.1 이벤트(Event) + +이벤트는 시스템에서 발생한 사건이나 상태 변화를 나타내는 데이터 레코드입니다. 일반적으로 다음과 같은 속성을 포함합니다: + +- **이벤트 ID**: 이벤트를 고유하게 식별하는 식별자 +- **이벤트 타입**: 이벤트의 종류(예: 구매, 클릭, 로그인) +- **타임스탬프**: 이벤트가 발생한 시간 +- **데이터 페이로드**: 이벤트와 관련된 실제 데이터 +- **메타데이터**: 이벤트에 대한 추가 정보 + +### 2.2 스트림(Stream) + +스트림은 시간에 따라 순차적으로 정렬된 이벤트의 연속적인 흐름입니다. 이벤트 스트림은 무한대로 계속될 수 있으며, 각 이벤트는 스트림에 추가만 가능하고 변경은 불가능한 특성(append-only, immutable)을 가집니다. + +### 2.3 프로듀서(Producer)와 컨슈머(Consumer) + +- **프로듀서**: 이벤트를 생성하여 스트림에 게시(publish)하는 애플리케이션이나 서비스입니다. +- **컨슈머**: 스트림에서 이벤트를 구독(subscribe)하고 처리하는 애플리케이션이나 서비스입니다. + +### 2.4 프로세서(Processor) + +스트림 프로세서는 하나 이상의 스트림에서 이벤트를 소비하고, 이를 처리한 후 결과를 다른 스트림에 게시하는 컴포넌트입니다. 이를 통해 이벤트 데이터를 변환, 필터링, 집계, 조인 등의 작업을 수행할 수 있습니다. + +## 3. 이벤트 스트리밍 플랫폼 아키텍처 + +이벤트 스트리밍 플랫폼은 일반적으로 다음과 같은 구성 요소를 포함합니다: + +```mermaid +graph TD + P[프로듀서] -->|이벤트 게시| B[브로커/메시징 시스템] + B -->|이벤트 소비| C[컨슈머] + B -->|이벤트 소비| SP[스트림 프로세서] + SP -->|처리된 이벤트 게시| B + B -->|저장| S[스토리지] + S -->|조회| Q[쿼리 인터페이스] +``` + +1. **브로커/메시징 시스템**: 이벤트를 수신하고 저장하며 구독자에게 전달하는 중앙 컴포넌트입니다. 대표적인 예로는 Apache Kafka, Amazon Kinesis, RabbitMQ 등이 있습니다. + +2. **스토리지**: 이벤트 데이터를 지속적으로 저장하는 시스템입니다. 이벤트 스트리밍 플랫폼은 종종 이벤트 로그(event log)라는 특수한 형태의 스토리지를 사용합니다. + +3. **스트림 처리 엔진**: 이벤트 스트림을 실시간으로 처리하기 위한 컴퓨팅 엔진입니다. Apache Flink, Apache Spark Streaming, Kafka Streams 등이 여기에 해당합니다. + +4. **쿼리 인터페이스**: 저장된 이벤트 데이터에 대한 조회 기능을 제공합니다. + + +## 4. 이벤트 스트리밍의 주요 특징 + +### 4.1 실시간 처리 + +이벤트가 발생하는 즉시 처리하여 실시간 인사이트와 반응을 가능하게 합니다. + +### 4.2 분산 아키텍처 + +대규모 이벤트 처리를 위해 수평적으로 확장 가능한 분산 아키텍처를 채택합니다. + +### 4.3 내구성과 신뢰성 + +이벤트는 영구적으로 저장되며, 시스템 장애 시에도 데이터 손실을 방지합니다. + +### 4.4 순서 보장 + +동일한 파티션 내에서는 이벤트의 순서가 보장됩니다. + +### 4.5 재생 가능성(Replayability) + +과거에 발생한 이벤트를 다시 재생하여 처리할 수 있습니다. 이는 시스템 복구, 새로운 분석 모델 적용, 버그 수정 등에 유용합니다. + +## 5. 이벤트 스트리밍의 사용 사례 + +### 5.1 실시간 분석 + +사용자 행동, 시스템 성능, 비즈니스 메트릭 등을 실시간으로 분석하여 즉각적인 인사이트를 제공합니다. + +### 5.2 데이터 통합(Data Integration) + +다양한 소스에서 생성되는 데이터를 통합하고 일관된 형태로 변환하여 저장합니다. + +### 5.3 마이크로서비스 통신 + +마이크로서비스 아키텍처에서 서비스 간 비동기 통신을 위한 메시징 백본으로 활용됩니다. + +### 5.4 IoT 데이터 처리 + +수많은 IoT 디바이스에서 생성되는 센서 데이터를 수집하고 처리합니다. + +### 5.5 실시간 모니터링 및 알림 + +시스템 상태, 비즈니스 지표, 보안 위협 등을 모니터링하고 이상 징후 발생 시 즉시 알림을 제공합니다. + +### 5.6 사기 탐지(Fraud Detection) + +금융 거래, 사용자 행동 등을 실시간으로 분석하여 사기 패턴을 탐지합니다. + +## 6. 대표적인 이벤트 스트리밍 기술 + +### 6.1 Apache Kafka + +LinkedIn에서 개발된 분산 이벤트 스트리밍 플랫폼으로, 높은 처리량, 내구성, 확장성을 제공합니다. 카프카는 현재 이벤트 스트리밍 분야에서 사실상의 표준으로 자리 잡았습니다. + +### 6.2 Amazon Kinesis + +AWS에서 제공하는 관리형 스트리밍 데이터 서비스로, 실시간 데이터 스트리밍 수집 및 처리를 지원합니다. + +### 6.3 Apache Pulsar + +Yahoo에서 개발된 분산 메시징 및 스트리밍 플랫폼으로, 멀티 테넌시, 지역 간 복제, 계층형 스토리지 등의 기능을 제공합니다. + +### 6.4 RabbitMQ + +AMQP(Advanced Message Queuing Protocol) 기반의 오픈소스 메시지 브로커로, 다양한 메시징 패턴을 지원합니다. + +### 6.5 Google Pub/Sub + +Google Cloud Platform에서 제공하는 완전 관리형 메시징 서비스입니다. + +## 7. 이벤트 스트리밍 구현 시 고려사항 + +### 7.1 확장성 + +시스템이 증가하는 이벤트 볼륨과 프로듀서/컨슈머 수를 처리할 수 있는지 확인해야 합니다. + +### 7.2 데이터 일관성 + +분산 환경에서 이벤트의 순서와 일관성을 보장하는 메커니즘이 필요합니다. + +### 7.3 내결함성 + +시스템 장애 시에도 데이터 손실을 방지하고 신속하게 복구할 수 있는 능력이 중요합니다. + +### 7.4 지연 시간(Latency) + +실시간 처리를 위해 낮은 지연 시간을 유지해야 합니다. + +### 7.5 데이터 스키마 관리 + +이벤트 데이터의 스키마 변화를 효과적으로 관리하는 전략이 필요합니다. + +### 7.6 보안 + +이벤트 데이터의 보안과 개인정보 보호를 위한 암호화, 인증, 권한 관리 등이 구현되어야 합니다. + +## 8. 이벤트 스트리밍과 관련 개념의 비교 + +### 8.1 이벤트 스트리밍 vs 배치 처리 + +|이벤트 스트리밍|배치 처리| +|---|---| +|실시간 처리|주기적 처리| +|지속적인 데이터 흐름|고정된 데이터 집합| +|낮은 지연 시간|높은 처리량에 최적화| +|실시간 의사 결정에 적합|복잡한 분석에 적합| + +### 8.2 이벤트 스트리밍 vs 메시지 큐 + +|이벤트 스트리밍|메시지 큐| +|---|---| +|이벤트 보존 및 재생|메시지 소비 후 삭제| +|다수의 컨슈머 그룹 지원|일반적으로 단일 컨슈머| +|높은 처리량에 최적화|신뢰성 있는 전달에 초점| +|이벤트 기록으로 활용|작업 큐로 활용| + +## 9. 결론 + +이벤트 스트리밍은 실시간 데이터 처리의 핵심 패러다임으로, 현대 데이터 중심 애플리케이션과 비즈니스에 필수적인 기술이 되었습니다. 분산 아키텍처, 실시간 처리 능력, 내구성, 확장성 등의 특징을 바탕으로 다양한 산업과 사용 사례에 적용되고 있습니다. + +효과적인 이벤트 스트리밍 시스템을 구현하기 위해서는 적절한 기술 선택과 함께 확장성, 데이터 일관성, 내결함성, 지연 시간, 스키마 관리, 보안 등 다양한 측면을 고려해야 합니다. + +## 10. 관련 노트 + +- [[분산 시스템 설계]] +- [[아파치 카프카]] +- [[이벤트 소싱(Event Sourcing)]] +- [[마이크로서비스 아키텍처]] +- [[실시간 데이터 처리]] +- [[비동기 메시징 패턴]] \ No newline at end of file diff --git "a/content/\354\271\264\355\224\204\354\271\264 \355\206\240\355\224\275(Topic).md" "b/content/\354\271\264\355\224\204\354\271\264 \355\206\240\355\224\275(Topic).md" new file mode 100644 index 00000000..c84bcc69 --- /dev/null +++ "b/content/\354\271\264\355\224\204\354\271\264 \355\206\240\355\224\275(Topic).md" @@ -0,0 +1,318 @@ +카프카 토픽(Topic)은 아파치 카프카에서 데이터 스트림을 관리하는 핵심 개념입니다. 토픽은 메시지가 저장되고 관리되는 논리적인 채널로, 특정 주제나 카테고리에 관련된 데이터를 구분하는 데 사용됩니다. 쉽게 말하자면, 토픽은 메시지를 발행하고 구독하기 위한 이름이 지정된 목적지라고 볼 수 있습니다. + +## 토픽의 구조와 특징 + +### 파티션 기반 구조 + +토픽은 하나 이상의 파티션(Partition)으로 구성됩니다. 각 파티션은 순서가 보장된 불변의 메시지 시퀀스입니다. + +```mermaid +graph TD + subgraph "토픽 A" + P0[파티션 0] + P1[파티션 1] + P2[파티션 2] + end + + subgraph "파티션 0 구조" + direction LR + M0[메시지 0: 오프셋 0] --> M1[메시지 1: 오프셋 1] --> M2[메시지 2: 오프셋 2] + end +``` + +### 주요 특징 + +1. **분산 저장**: 토픽의 파티션은 여러 브로커에 분산 저장될 수 있어 고가용성과 확장성을 제공합니다. +2. **순서 보장**: 하나의 파티션 내에서는 메시지의 순서가 보장됩니다. 단, 파티션 간의 순서는 보장되지 않습니다. +3. **내구성**: 토픽에 저장된 데이터는 설정된 보존 기간 동안 유지됩니다. +4. **불변성**: 토픽에 한번 저장된 메시지는 수정할 수 없습니다(append-only). +5. **식별자**: 각 메시지는 파티션 내에서 고유한 오프셋(offset)을 갖습니다. + +## 토픽 관리 + +### 토픽 생성 + +토픽은 명시적으로 생성하거나, `auto.create.topics.enable` 설정이 활성화된 경우 프로듀서가 존재하지 않는 토픽에 메시지를 발행할 때 자동으로 생성됩니다. + +```bash +# 명령행에서 토픽 생성 +bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 6 --topic my-topic +``` + +### 주요 설정 파라미터 + +1. **파티션 수(partitions)**: 토픽을 얼마나 많은 파티션으로 나눌지 결정합니다. 파티션 수가 많을수록 처리량과 병렬성이 향상되지만, 관리 오버헤드가 증가합니다. + +2. **복제 팩터(replication factor)**: 각 파티션의 복제본 수를 지정합니다. 높은 복제 팩터는 내구성과 가용성을 향상시키지만 더 많은 디스크 공간이 필요합니다. + +3. **보존 정책(retention policy)**: + + - `retention.ms`: 메시지 보존 기간 (밀리초) + - `retention.bytes`: 파티션당 최대 크기 + + ```bash + # 토픽의 보존 정책 설정 + bin/kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --alter --add-config retention.ms=86400000 + ``` + + +### 토픽의 파티션 확장 + +토픽의 파티션 수는 증가시킬 수는 있지만, 감소시킬 수는 없습니다. + +```bash +# 파티션 수 증가 +bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic my-topic --partitions 8 +``` + +## 토픽 디자인 모범 사례 + +### 토픽 이름 지정 규칙 + +일관된 토픽 이름 규칙을 사용하면 관리가 용이해집니다: + +``` +<환경>.<서비스>.<데이터타입> +예: prod.order-service.orders + dev.user-service.events +``` + +### 파티션 수 결정 요소 + +파티션 수를 결정할 때 고려해야 할 요소: + +1. **처리량 요구사항**: 예상되는 토픽의 처리량이 높을수록 더 많은 파티션이 필요합니다. +2. **컨슈머 병렬성**: 최대 컨슈머 병렬 처리 수는 파티션 수에 제한됩니다. +3. **메시지 순서**: 순서가 중요한 경우, 관련 메시지가 같은 파티션에 들어가도록 계획해야 합니다. +4. **브로커 리소스**: 각 파티션은 브로커의 리소스를 소비합니다. + +경험적으로, 시작 파티션 수는 다음과 같이 계산할 수 있습니다: + +``` +파티션 수 = max(예상 처리량 ÷ 단일 파티션 처리량, 컨슈머 수) +``` + +### 토픽 컴팩션 + +카프카는 로그 컴팩션(log compaction)이라는 특별한 보존 정책을 제공합니다. 컴팩션을 사용하면 동일한 키를 가진 메시지 중 가장 최신 값만 유지됩니다. 이 기능은 변경 로그나 상태 저장소로 토픽을 사용할 때 유용합니다. + +```bash +# 토픽 생성 시 컴팩션 설정 +bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 6 --topic compacted-topic --config cleanup.policy=compact +``` + +## 자바에서 토픽 관리 + +다음은 자바 코드에서 AdminClient를 사용하여 토픽을 관리하는 예제입니다: + +```java +import org.apache.kafka.clients.admin.*; +import org.apache.kafka.common.config.ConfigResource; +import org.apache.kafka.common.config.ConfigResource.Type; + +import java.util.*; +import java.util.concurrent.ExecutionException; + +public class KafkaTopicManager { + private final AdminClient adminClient; + + public KafkaTopicManager(String bootstrapServers) { + Properties props = new Properties(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + this.adminClient = AdminClient.create(props); + } + + public void createTopic(String topicName, int partitions, short replicationFactor) + throws ExecutionException, InterruptedException { + NewTopic newTopic = new NewTopic(topicName, partitions, replicationFactor); + CreateTopicsResult result = adminClient.createTopics(Collections.singleton(newTopic)); + result.all().get(); // 작업 완료 대기 + System.out.println("토픽 " + topicName + "이(가) 생성되었습니다."); + } + + public void createTopicWithConfig(String topicName, int partitions, short replicationFactor, + Map configs) + throws ExecutionException, InterruptedException { + NewTopic newTopic = new NewTopic(topicName, partitions, replicationFactor); + newTopic.configs(configs); + CreateTopicsResult result = adminClient.createTopics(Collections.singleton(newTopic)); + result.all().get(); // 작업 완료 대기 + System.out.println("토픽 " + topicName + "이(가) 설정과 함께 생성되었습니다."); + } + + public void listTopics() throws ExecutionException, InterruptedException { + ListTopicsResult result = adminClient.listTopics(); + Set topicNames = result.names().get(); + System.out.println("토픽 목록: " + topicNames); + } + + public void describeTopic(String topicName) throws ExecutionException, InterruptedException { + DescribeTopicsResult result = adminClient.describeTopics(Collections.singleton(topicName)); + Map topicDescriptionMap = result.all().get(); + + TopicDescription topicDescription = topicDescriptionMap.get(topicName); + System.out.println("토픽 이름: " + topicDescription.name()); + System.out.println("토픽 ID: " + topicDescription.topicId()); + System.out.println("파티션 수: " + topicDescription.partitions().size()); + + for (TopicPartitionInfo partition : topicDescription.partitions()) { + System.out.println("파티션 " + partition.partition() + + ", 리더: " + partition.leader().id() + + ", 복제본: " + partition.replicas().size()); + } + } + + public void updateTopicConfig(String topicName, Map updateConfigs) + throws ExecutionException, InterruptedException { + ConfigResource resource = new ConfigResource(Type.TOPIC, topicName); + + List configEntries = new ArrayList<>(); + for (Map.Entry entry : updateConfigs.entrySet()) { + configEntries.add(new ConfigEntry(entry.getKey(), entry.getValue())); + } + + Config config = new Config(configEntries); + Map configs = Collections.singletonMap(resource, config); + + AlterConfigsResult result = adminClient.alterConfigs(configs); + result.all().get(); // 작업 완료 대기 + System.out.println("토픽 " + topicName + "의 설정이 업데이트되었습니다."); + } + + public void deleteTopic(String topicName) throws ExecutionException, InterruptedException { + DeleteTopicsResult result = adminClient.deleteTopics(Collections.singleton(topicName)); + result.all().get(); // 작업 완료 대기 + System.out.println("토픽 " + topicName + "이(가) 삭제되었습니다."); + } + + public void close() { + if (adminClient != null) { + adminClient.close(); + } + } + + public static void main(String[] args) { + KafkaTopicManager manager = new KafkaTopicManager("localhost:9092"); + + try { + // 토픽 생성 + manager.createTopic("example-topic", 3, (short) 1); + + // 설정과 함께 토픽 생성 + Map configs = new HashMap<>(); + configs.put("retention.ms", "86400000"); // 1일 + configs.put("segment.bytes", "1073741824"); // 1GB + manager.createTopicWithConfig("example-topic-with-config", 6, (short) 3, configs); + + // 토픽 목록 조회 + manager.listTopics(); + + // 토픽 상세 정보 조회 + manager.describeTopic("example-topic"); + + // 토픽 설정 업데이트 + Map updateConfigs = new HashMap<>(); + updateConfigs.put("retention.ms", "172800000"); // 2일로 변경 + manager.updateTopicConfig("example-topic", updateConfigs); + + // 토픽 삭제 + manager.deleteTopic("example-topic"); + + } catch (Exception e) { + e.printStackTrace(); + } finally { + manager.close(); + } + } +} +``` + +## 스프링 부트에서 토픽 관리 + +스프링 부트에서는 `KafkaAdmin` 클래스를 사용하여 토픽을 관리할 수 있습니다: + +```java +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.KafkaAdmin; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaTopicConfig { + + @Bean + public KafkaAdmin kafkaAdmin() { + Map configs = new HashMap<>(); + configs.put("bootstrap.servers", "localhost:9092"); + return new KafkaAdmin(configs); + } + + @Bean + public NewTopic topic1() { + return TopicBuilder.name("topic1") + .partitions(6) + .replicas(3) + .build(); + } + + @Bean + public NewTopic topic2() { + return TopicBuilder.name("topic2") + .partitions(3) + .replicas(1) + .config("retention.ms", "86400000") // 1일 + .build(); + } + + @Bean + public NewTopic compactedTopic() { + return TopicBuilder.name("compacted-topic") + .partitions(1) + .replicas(1) + .config("cleanup.policy", "compact") + .build(); + } +} +``` + +스프링 부트 애플리케이션이 시작되면, `KafkaAdmin`은 정의된 뉴토픽 빈들을 자동으로 생성합니다. + +## 토픽 모니터링 + +카프카 토픽의 상태와 성능을 모니터링하는 데 유용한 몇 가지 명령과 도구가 있습니다: + +### 1. 토픽 상태 확인 + +```bash +# 토픽 상세 정보 조회 +bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic my-topic + +# 토픽 파티션 상태 확인 +bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic my-topic --under-replicated-partitions +``` + +### 2. 토픽 메시지 소비 + +```bash +# 토픽의 메시지 확인 +bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --from-beginning +``` + +### 3. JMX 메트릭 모니터링 + +카프카는 다양한 JMX 메트릭을 제공합니다. 토픽과 관련된 주요 메트릭: + +- `kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec`: 초당 수신 메시지 수 +- `kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec`: 초당 수신 바이트 수 +- `kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec`: 초당 송신 바이트 수 + +이러한 메트릭을 Prometheus와 Grafana 같은 도구로 시각화할 수 있습니다. + +## 결론 + +카프카 토픽은 분산 이벤트 스트리밍의 기본 구성 요소로, 메시지를 논리적으로 구성하고 관리하는 역할을 합니다. 토픽을 효과적으로 설계하고 구성하면 카프카 시스템의 성능, 확장성, 안정성을 크게 향상시킬 수 있습니다. 각 애플리케이션의 요구사항에 맞게 파티션 수, 복제 팩터, 보존 정책 등의 설정을 최적화하는 것이 중요합니다. \ No newline at end of file diff --git "a/content/\354\271\264\355\224\204\354\271\264 \355\214\214\355\213\260\354\205\230(Partition).md" "b/content/\354\271\264\355\224\204\354\271\264 \355\214\214\355\213\260\354\205\230(Partition).md" new file mode 100644 index 00000000..2e2f76c3 --- /dev/null +++ "b/content/\354\271\264\355\224\204\354\271\264 \355\214\214\355\213\260\354\205\230(Partition).md" @@ -0,0 +1,78 @@ +파티션은 카프카 토픽을 물리적으로 분할한 단위입니다. 토픽은 논리적인 개념이며, 실제 데이터는 파티션이라는 물리적 단위에 저장됩니다. 각 파티션은 순서가 보장된 불변의 메시지 시퀀스이며, 카프카 클러스터의 여러 브로커에 분산되어 저장됩니다. + +## 파티션의 주요 특징 + +1. **순차적 데이터 구조**: 각 파티션은 순차적으로 추가되는(append-only) 로그 구조입니다. 메시지는 항상 파티션의 끝에 추가되며, 각 메시지는 파티션 내에서 고유한 오프셋(offset)을 부여받습니다. + +2. **분산 저장**: 파티션은 카프카 클러스터의 여러 브로커에 분산되어 저장될 수 있으며, 이를 통해 수평적 확장이 가능합니다. + +3. **병렬 처리**: 토픽의 파티션 수는 컨슈머의 병렬 처리 능력을 결정합니다. 컨슈머 그룹 내의 각 컨슈머는 하나 이상의 파티션을 독점적으로 처리할 수 있습니다. + +4. **오프셋(Offset)**: 파티션 내의 각 메시지는 0부터 시작하는 연속적인 오프셋을 가집니다. 오프셋은 파티션 내에서 메시지의 위치를 나타냅니다. + +5. **복제(Replication)**: 고가용성을 위해 각 파티션은 여러 브로커에 복제될 수 있습니다. 복제 계수(replication factor)는 각 파티션이 몇 개의 복제본을 가질지 결정합니다. + + +## 파티션 할당 및 분배 + +### 프로듀서의 파티션 할당 + +프로듀서가 메시지를 토픽에 발행할 때, 어떤 파티션으로 메시지를 보낼지 결정해야 합니다. 파티션 할당 방식은 다음과 같습니다: + +1. **명시적 파티션 지정**: 프로듀서가 메시지를 보낼 파티션을 직접 지정할 수 있습니다. + +2. **키 기반 파티션 할당**: 메시지에 키가 있는 경우, 키의 해시 값을 기반으로 파티션이 결정됩니다. 동일한 키를 가진 메시지는 항상 같은 파티션으로 전송됩니다. + +3. **라운드 로빈**: 메시지에 키가 없고 파티션을 명시적으로 지정하지 않은 경우, 기본적으로 라운드 로빈 방식으로 파티션이 선택됩니다. + + +### 컨슈머의 파티션 할당 + +컨슈머 그룹 내에서 파티션 할당은 다음과 같이 이루어집니다: + +1. **그룹 코디네이터**: 카프카는 그룹 코디네이터를 통해 컨슈머 그룹의 멤버십과 파티션 할당을 관리합니다. + +2. **리밸런싱(Rebalancing)**: 컨슈머 그룹에 컨슈머가 추가되거나 제거될 때, 파티션 할당이 재조정됩니다. + +3. **할당 전략**: 기본적으로 Range, RoundRobin, Sticky 등의 할당 전략을 사용하여 파티션을 컨슈머에게 분배합니다. + + +## 파티션 수 결정 시 고려사항 + +토픽의 파티션 수를 결정할 때 고려해야 할 요소들: + +1. **처리량(Throughput)**: 높은 처리량이 필요한 경우, 더 많은 파티션을 사용하여 병렬 처리 능력을 높일 수 있습니다. + +2. **메시지 순서**: 메시지 순서가 중요한 경우, 관련 메시지가 동일한 파티션에 할당되도록 키를 설정해야 합니다. + +3. **컨슈머 수**: 컨슈머 그룹의 최대 병렬 처리 능력은 파티션 수를 초과할 수 없습니다. 즉, 파티션 수보다 많은 컨슈머가 있다면 일부 컨슈머는 유휴 상태가 됩니다. + +4. **브로커 자원**: 각 파티션은 브로커의 리소스(디스크, 메모리, CPU)를 소비합니다. 너무 많은 파티션은 브로커에 부담을 줄 수 있습니다. + +5. **리밸런싱 비용**: 파티션 수가 많을수록 컨슈머 그룹의 리밸런싱 비용이 증가합니다. + + +## 파티션 관리 + +1. **토픽 생성 시 파티션 수 지정**: + + ```bash + kafka-topics.sh --create --topic my-topic --partitions 3 --replication-factor 2 --bootstrap-server localhost:9092 + ``` + +2. **기존 토픽의 파티션 수 증가**: + + ```bash + kafka-topics.sh --alter --topic my-topic --partitions 6 --bootstrap-server localhost:9092 + ``` + +3. **파티션 정보 확인**: + + ```bash + kafka-topics.sh --describe --topic my-topic --bootstrap-server localhost:9092 + ``` + + +주의할 점은 파티션 수는 증가만 가능하고 감소는 불가능하다는 것입니다. 또한 파티션 수를 증가시키면 메시지 키에 따른 파티션 매핑이 변경될 수 있으므로, 키 순서가 중요한 애플리케이션에서는 신중히 고려해야 합니다. + +파티션은 카프카의 확장성과 고성능의 핵심 요소이며, 애플리케이션의 요구사항에 맞게 적절히 설계하는 것이 중요합니다. \ No newline at end of file diff --git "a/content/\355\225\264\354\213\234 \355\225\250\354\210\230.md" "b/content/\355\225\264\354\213\234 \355\225\250\354\210\230.md" new file mode 100644 index 00000000..54ea3638 --- /dev/null +++ "b/content/\355\225\264\354\213\234 \355\225\250\354\210\230.md" @@ -0,0 +1,64 @@ +--- +draft: true +tags: [] +description: 해시 함수에 대한 정확하고 자세한 소개입니다. +--- + +# 해시 함수 개요 + +해시 함수는 임의의 크기의 데이터를 고정된 크기의 값으로 매핑하는 함수입니다. 주로 데이터의 무결성 확인, 데이터 검색, 암호화 등 다양한 분야에서 활용됩니다. 해시 함수는 입력값이 조금만 변경되어도 출력값이 크게 달라지는 특징을 가지고 있습니다. + +# 본론 + +## 해시 함수의 특징 + +해시 함수의 주요 특징은 다음과 같습니다. + +- **결정적 함수**: 동일한 입력값에 대해 항상 동일한 출력값을 제공합니다. +- **빠른 계산 속도**: 입력값에 대한 해시 값을 빠르게 계산할 수 있습니다. +- **단방향성**: 출력값으로부터 입력값을 역으로 추출하는 것이 현실적으로 불가능합니다. +- **충돌 저항성**: 서로 다른 두 입력값이 동일한 출력값을 가질 확률이 매우 낮습니다. + +## 해시 함수의 활용 분야 + +해시 함수는 다양한 분야에서 활용되고 있습니다. + +- **데이터 무결성 검사**: 파일의 해시 값을 생성하여 파일이 변조되었는지 확인합니다. +- **패스워드 저장**: 사용자 패스워드를 해시하여 데이터베이스에 저장함으로써 보안을 강화합니다. +- **데이터 검색 및 인덱싱**: 해시 테이블을 활용하여 데이터 검색 속도를 향상시킵니다. +- **디지털 서명 및 인증**: 암호학적 해시 함수를 활용하여 데이터의 신뢰성을 보장합니다. + +# 설명 + +## 암호학적 해시 함수 + +암호학적 해시 함수는 보안성이 강화된 해시 함수로, 주로 보안 분야에서 사용됩니다. 이러한 함수는 다음과 같은 추가적인 특성을 가지고 있습니다. + +- **역상 저항성**: 주어진 해시 값에 대한 원본 입력값을 찾는 것이 현실적으로 불가능합니다. +- **제2 역상 저항성**: 특정 입력값과 동일한 해시 값을 가지는 다른 입력값을 찾는 것이 현실적으로 불가능합니다. +- **충돌 저항성**: 서로 다른 두 입력값이 동일한 해시 값을 가지는 경우를 찾는 것이 현실적으로 불가능합니다. + +대표적인 암호학적 해시 함수에는 [[SHA-256]], [[SHA-3]], [[MD5]] 등이 있습니다. 다만, MD5와 같은 일부 해시 함수는 보안성이 취약해 현재는 사용이 권장되지 않습니다. + +## 해시 테이블과 해시 함수 + +해시 함수는 [[해시 테이블]]에서 중요한 역할을 합니다. 해시 테이블은 키-값 쌍을 저장하는 데이터 구조로, 해시 함수를 통해 키를 해시하여 인덱스로 변환함으로써 빠른 데이터 접근이 가능합니다. 이때 충돌이 발생할 수 있으므로 [[충돌 해결 방법]]이 필요합니다. + +## 해시 함수의 한계 + +해시 함수는 다양한 장점을 가지고 있지만, 다음과 같은 한계도 존재합니다. + +- **충돌 가능성**: 해시 함수는 유한한 출력 공간을 가지므로, 무한한 입력 공간에서 충돌은 필연적입니다. +- **암호학적 취약점**: 일부 해시 함수는 시간이 지남에 따라 보안 취약점이 발견될 수 있습니다. +- **역상 공격**: 충분한 계산 능력이 있는 공격자는 원본 입력값을 추출하려고 시도할 수 있습니다. + +## 관련 개념 + +- [[해시 테이블]] +- [[충돌 해결 방법]] +- [[암호학]] +- [[데이터 구조]] + +--- + +이상으로 해시 함수에 대한 개요와 주요 특징, 활용 분야, 그리고 한계점에 대해 살펴보았습니다. 해시 함수는 컴퓨터 과학 및 보안 분야에서 매우 중요한 개념이며, 그 원리와 특성을 이해하는 것이 중요합니다. \ No newline at end of file diff --git a/public/CSRF(Cross-Site-Request-Forgery).html b/public/CSRF(Cross-Site-Request-Forgery).html index 5a2927a2..5c418b4b 100644 --- a/public/CSRF(Cross-Site-Request-Forgery).html +++ b/public/CSRF(Cross-Site-Request-Forgery).html @@ -1,5 +1,5 @@ -CSRF(Cross-Site Request Forgery) | Beoks Blog

소개

+CSRF(Cross-Site Request Forgery) | Beoks Blog

소개

CSRF(Cross-Site Request Forgery) 는 사용자와 서버 간의 신뢰 관계를 악용하여 발생하는 대표적인 웹 공격입니다. 이 글에서는 CSRF의 개념과 동작 방식, 그리고 이를 방어하기 위한 다양한 기법에 대해 자세히 알아보겠습니다.


CSRF란 무엇인가?

diff --git a/public/Cache-Aside.html b/public/Cache-Aside.html index 86e13d4e..b78bf754 100644 --- a/public/Cache-Aside.html +++ b/public/Cache-Aside.html @@ -141,7 +141,7 @@ padding: 0 12px; font-size: 14px; } -/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjoiL1VzZXJzL2xlZWpzL1Byb2plY3QvYmVva3MuZ2l0aHViLmlvL3F1YXJ0ei9jb21wb25lbnRzL3N0eWxlcyIsInNvdXJjZXMiOlsibWVybWFpZC5pbmxpbmUuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTs7O0FBS0Y7RUFDRTtFQUNBOzs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQSxZQUNFO0VBRUY7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFOztBQUdGO0VBQ0U7O0FBSUY7RUFDRTtFQUNBO0VBQ0EiLCJzb3VyY2VzQ29udGVudCI6WyIuZXhwYW5kLWJ1dHRvbiB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogZmxleDtcbiAgZmxvYXQ6IHJpZ2h0O1xuICBwYWRkaW5nOiAwLjRyZW07XG4gIG1hcmdpbjogMC4zcmVtO1xuICByaWdodDogMDsgLy8gTk9URTogcmlnaHQgd2lsbCBiZSBzZXQgaW4gbWVybWFpZC5pbmxpbmUudHNcbiAgY29sb3I6IHZhcigtLWdyYXkpO1xuICBib3JkZXItY29sb3I6IHZhcigtLWRhcmspO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1saWdodCk7XG4gIGJvcmRlcjogMXB4IHNvbGlkO1xuICBib3JkZXItcmFkaXVzOiA1cHg7XG4gIG9wYWNpdHk6IDA7XG4gIHRyYW5zaXRpb246IDAuMnM7XG5cbiAgJiA+IHN2ZyB7XG4gICAgZmlsbDogdmFyKC0tbGlnaHQpO1xuICAgIGZpbHRlcjogY29udHJhc3QoMC4zKTtcbiAgfVxuXG4gICY6aG92ZXIge1xuICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICBib3JkZXItY29sb3I6IHZhcigtLXNlY29uZGFyeSk7XG4gIH1cblxuICAmOmZvY3VzIHtcbiAgICBvdXRsaW5lOiAwO1xuICB9XG59XG5cbnByZSB7XG4gICY6aG92ZXIgPiAuZXhwYW5kLWJ1dHRvbiB7XG4gICAgb3BhY2l0eTogMTtcbiAgICB0cmFuc2l0aW9uOiAwLjJzO1xuICB9XG59XG5cbiNtZXJtYWlkLWNvbnRhaW5lciB7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgY29udGFpbjogbGF5b3V0O1xuICB6LWluZGV4OiA5OTk7XG4gIGxlZnQ6IDA7XG4gIHRvcDogMDtcbiAgd2lkdGg6IDEwMHZ3O1xuICBoZWlnaHQ6IDEwMHZoO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICBkaXNwbGF5OiBub25lO1xuICBiYWNrZHJvcC1maWx0ZXI6IGJsdXIoNHB4KTtcbiAgYmFja2dyb3VuZDogcmdiYSgwLCAwLCAwLCAwLjUpO1xuXG4gICYuYWN0aXZlIHtcbiAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gIH1cblxuICAmID4gI21lcm1haWQtc3BhY2Uge1xuICAgIGRpc3BsYXk6IGdyaWQ7XG4gICAgd2lkdGg6IDkwJTtcbiAgICBoZWlnaHQ6IDkwdmg7XG4gICAgbWFyZ2luOiA1dmggYXV0bztcbiAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodCk7XG4gICAgYm94LXNoYWRvdzpcbiAgICAgIDAgMTRweCA1MHB4IHJnYmEoMjcsIDMzLCA0OCwgMC4xMiksXG4gICAgICAwIDEwcHggMzBweCByZ2JhKDI3LCAzMywgNDgsIDAuMTYpO1xuICAgIG92ZXJmbG93OiBoaWRkZW47XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuXG4gICAgJiA+IC5tZXJtYWlkLWhlYWRlciB7XG4gICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAganVzdGlmeS1jb250ZW50OiBmbGV4LWVuZDtcbiAgICAgIHBhZGRpbmc6IDFyZW07XG4gICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgIGJhY2tncm91bmQ6IHZhcigtLWxpZ2h0KTtcbiAgICAgIHotaW5kZXg6IDI7XG4gICAgICBtYXgtaGVpZ2h0OiBmaXQtY29udGVudDtcblxuICAgICAgJiA+IC5jbG9zZS1idXR0b24ge1xuICAgICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgICBhbGlnbi1pdGVtczogY2VudGVyO1xuICAgICAgICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgICAgICAgd2lkdGg6IDMycHg7XG4gICAgICAgIGhlaWdodDogMzJweDtcbiAgICAgICAgcGFkZGluZzogMDtcbiAgICAgICAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNHB4O1xuICAgICAgICBjb2xvcjogdmFyKC0tZGFya2dyYXkpO1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIHRyYW5zaXRpb246IGFsbCAwLjJzIGVhc2U7XG5cbiAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgYmFja2dyb3VuZDogdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgICAgICBjb2xvcjogdmFyKC0tZGFyayk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAmID4gLm1lcm1haWQtY29udGVudCB7XG4gICAgICBwYWRkaW5nOiAycmVtO1xuICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgdHJhbnNmb3JtLW9yaWdpbjogMCAwO1xuICAgICAgdHJhbnNpdGlvbjogdHJhbnNmb3JtIDAuMXMgZWFzZTtcbiAgICAgIG92ZXJmbG93OiB2aXNpYmxlO1xuICAgICAgbWluLWhlaWdodDogMjAwcHg7XG4gICAgICBtaW4td2lkdGg6IDIwMHB4O1xuXG4gICAgICBwcmUge1xuICAgICAgICBtYXJnaW46IDA7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgIH1cblxuICAgICAgc3ZnIHtcbiAgICAgICAgbWF4LXdpZHRoOiBub25lO1xuICAgICAgICBoZWlnaHQ6IGF1dG87XG4gICAgICB9XG4gICAgfVxuXG4gICAgJiA+IC5tZXJtYWlkLWNvbnRyb2xzIHtcbiAgICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICAgIGJvdHRvbTogMjBweDtcbiAgICAgIHJpZ2h0OiAyMHB4O1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGdhcDogOHB4O1xuICAgICAgcGFkZGluZzogOHB4O1xuICAgICAgYmFja2dyb3VuZDogdmFyKC0tbGlnaHQpO1xuICAgICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgIGJvcmRlci1yYWRpdXM6IDZweDtcbiAgICAgIGJveC1zaGFkb3c6IDAgMnB4IDRweCByZ2JhKDAsIDAsIDAsIDAuMSk7XG4gICAgICB6LWluZGV4OiAyO1xuXG4gICAgICAubWVybWFpZC1jb250cm9sLWJ1dHRvbiB7XG4gICAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gICAgICAgIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICAgICAgICB3aWR0aDogMzJweDtcbiAgICAgICAgaGVpZ2h0OiAzMnB4O1xuICAgICAgICBwYWRkaW5nOiAwO1xuICAgICAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1saWdodGdyYXkpO1xuICAgICAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodCk7XG4gICAgICAgIGNvbG9yOiB2YXIoLS1kYXJrKTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNHB4O1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIGZvbnQtc2l6ZTogMTZweDtcbiAgICAgICAgZm9udC1mYW1pbHk6IHZhcigtLWJvZHlGb250KTtcbiAgICAgICAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZTtcblxuICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodGdyYXkpO1xuICAgICAgICB9XG5cbiAgICAgICAgJjphY3RpdmUge1xuICAgICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgxcHgpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU3R5bGUgdGhlIHJlc2V0IGJ1dHRvbiBkaWZmZXJlbnRseVxuICAgICAgICAmOm50aC1jaGlsZCgyKSB7XG4gICAgICAgICAgd2lkdGg6IGF1dG87XG4gICAgICAgICAgcGFkZGluZzogMCAxMnB4O1xuICAgICAgICAgIGZvbnQtc2l6ZTogMTRweDtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIl19 */

이 글에서는 데이터 캐싱(Caching)을 구현하는 전략 중 하나인 Cache Aside에 대해 자세히 알아보고, 이를 구현할 때 고려해야 할 사항들을 소개하겠습니다.

+/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjoiL1VzZXJzL2xlZWpzL1Byb2plY3QvYmVva3MuZ2l0aHViLmlvL3F1YXJ0ei9jb21wb25lbnRzL3N0eWxlcyIsInNvdXJjZXMiOlsibWVybWFpZC5pbmxpbmUuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTs7O0FBS0Y7RUFDRTtFQUNBOzs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQSxZQUNFO0VBRUY7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFOztBQUdGO0VBQ0U7O0FBSUY7RUFDRTtFQUNBO0VBQ0EiLCJzb3VyY2VzQ29udGVudCI6WyIuZXhwYW5kLWJ1dHRvbiB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogZmxleDtcbiAgZmxvYXQ6IHJpZ2h0O1xuICBwYWRkaW5nOiAwLjRyZW07XG4gIG1hcmdpbjogMC4zcmVtO1xuICByaWdodDogMDsgLy8gTk9URTogcmlnaHQgd2lsbCBiZSBzZXQgaW4gbWVybWFpZC5pbmxpbmUudHNcbiAgY29sb3I6IHZhcigtLWdyYXkpO1xuICBib3JkZXItY29sb3I6IHZhcigtLWRhcmspO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1saWdodCk7XG4gIGJvcmRlcjogMXB4IHNvbGlkO1xuICBib3JkZXItcmFkaXVzOiA1cHg7XG4gIG9wYWNpdHk6IDA7XG4gIHRyYW5zaXRpb246IDAuMnM7XG5cbiAgJiA+IHN2ZyB7XG4gICAgZmlsbDogdmFyKC0tbGlnaHQpO1xuICAgIGZpbHRlcjogY29udHJhc3QoMC4zKTtcbiAgfVxuXG4gICY6aG92ZXIge1xuICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICBib3JkZXItY29sb3I6IHZhcigtLXNlY29uZGFyeSk7XG4gIH1cblxuICAmOmZvY3VzIHtcbiAgICBvdXRsaW5lOiAwO1xuICB9XG59XG5cbnByZSB7XG4gICY6aG92ZXIgPiAuZXhwYW5kLWJ1dHRvbiB7XG4gICAgb3BhY2l0eTogMTtcbiAgICB0cmFuc2l0aW9uOiAwLjJzO1xuICB9XG59XG5cbiNtZXJtYWlkLWNvbnRhaW5lciB7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgY29udGFpbjogbGF5b3V0O1xuICB6LWluZGV4OiA5OTk7XG4gIGxlZnQ6IDA7XG4gIHRvcDogMDtcbiAgd2lkdGg6IDEwMHZ3O1xuICBoZWlnaHQ6IDEwMHZoO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICBkaXNwbGF5OiBub25lO1xuICBiYWNrZHJvcC1maWx0ZXI6IGJsdXIoNHB4KTtcbiAgYmFja2dyb3VuZDogcmdiYSgwLCAwLCAwLCAwLjUpO1xuXG4gICYuYWN0aXZlIHtcbiAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gIH1cblxuICAmID4gI21lcm1haWQtc3BhY2Uge1xuICAgIGRpc3BsYXk6IGdyaWQ7XG4gICAgd2lkdGg6IDkwJTtcbiAgICBoZWlnaHQ6IDkwdmg7XG4gICAgbWFyZ2luOiA1dmggYXV0bztcbiAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodCk7XG4gICAgYm94LXNoYWRvdzpcbiAgICAgIDAgMTRweCA1MHB4IHJnYmEoMjcsIDMzLCA0OCwgMC4xMiksXG4gICAgICAwIDEwcHggMzBweCByZ2JhKDI3LCAzMywgNDgsIDAuMTYpO1xuICAgIG92ZXJmbG93OiBoaWRkZW47XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuXG4gICAgJiA+IC5tZXJtYWlkLWhlYWRlciB7XG4gICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAganVzdGlmeS1jb250ZW50OiBmbGV4LWVuZDtcbiAgICAgIHBhZGRpbmc6IDFyZW07XG4gICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgIGJhY2tncm91bmQ6IHZhcigtLWxpZ2h0KTtcbiAgICAgIHotaW5kZXg6IDI7XG4gICAgICBtYXgtaGVpZ2h0OiBmaXQtY29udGVudDtcblxuICAgICAgJiA+IC5jbG9zZS1idXR0b24ge1xuICAgICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgICBhbGlnbi1pdGVtczogY2VudGVyO1xuICAgICAgICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgICAgICAgd2lkdGg6IDMycHg7XG4gICAgICAgIGhlaWdodDogMzJweDtcbiAgICAgICAgcGFkZGluZzogMDtcbiAgICAgICAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNHB4O1xuICAgICAgICBjb2xvcjogdmFyKC0tZGFya2dyYXkpO1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIHRyYW5zaXRpb246IGFsbCAwLjJzIGVhc2U7XG5cbiAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgYmFja2dyb3VuZDogdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgICAgICBjb2xvcjogdmFyKC0tZGFyayk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAmID4gLm1lcm1haWQtY29udGVudCB7XG4gICAgICBwYWRkaW5nOiAycmVtO1xuICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgdHJhbnNmb3JtLW9yaWdpbjogMCAwO1xuICAgICAgdHJhbnNpdGlvbjogdHJhbnNmb3JtIDAuMXMgZWFzZTtcbiAgICAgIG92ZXJmbG93OiB2aXNpYmxlO1xuICAgICAgbWluLWhlaWdodDogMjAwcHg7XG4gICAgICBtaW4td2lkdGg6IDIwMHB4O1xuXG4gICAgICBwcmUge1xuICAgICAgICBtYXJnaW46IDA7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgIH1cblxuICAgICAgc3ZnIHtcbiAgICAgICAgbWF4LXdpZHRoOiBub25lO1xuICAgICAgICBoZWlnaHQ6IGF1dG87XG4gICAgICB9XG4gICAgfVxuXG4gICAgJiA+IC5tZXJtYWlkLWNvbnRyb2xzIHtcbiAgICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICAgIGJvdHRvbTogMjBweDtcbiAgICAgIHJpZ2h0OiAyMHB4O1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGdhcDogOHB4O1xuICAgICAgcGFkZGluZzogOHB4O1xuICAgICAgYmFja2dyb3VuZDogdmFyKC0tbGlnaHQpO1xuICAgICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tbGlnaHRncmF5KTtcbiAgICAgIGJvcmRlci1yYWRpdXM6IDZweDtcbiAgICAgIGJveC1zaGFkb3c6IDAgMnB4IDRweCByZ2JhKDAsIDAsIDAsIDAuMSk7XG4gICAgICB6LWluZGV4OiAyO1xuXG4gICAgICAubWVybWFpZC1jb250cm9sLWJ1dHRvbiB7XG4gICAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gICAgICAgIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICAgICAgICB3aWR0aDogMzJweDtcbiAgICAgICAgaGVpZ2h0OiAzMnB4O1xuICAgICAgICBwYWRkaW5nOiAwO1xuICAgICAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1saWdodGdyYXkpO1xuICAgICAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodCk7XG4gICAgICAgIGNvbG9yOiB2YXIoLS1kYXJrKTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNHB4O1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIGZvbnQtc2l6ZTogMTZweDtcbiAgICAgICAgZm9udC1mYW1pbHk6IHZhcigtLWJvZHlGb250KTtcbiAgICAgICAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZTtcblxuICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICBiYWNrZ3JvdW5kOiB2YXIoLS1saWdodGdyYXkpO1xuICAgICAgICB9XG5cbiAgICAgICAgJjphY3RpdmUge1xuICAgICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgxcHgpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU3R5bGUgdGhlIHJlc2V0IGJ1dHRvbiBkaWZmZXJlbnRseVxuICAgICAgICAmOm50aC1jaGlsZCgyKSB7XG4gICAgICAgICAgd2lkdGg6IGF1dG87XG4gICAgICAgICAgcGFkZGluZzogMCAxMnB4O1xuICAgICAgICAgIGZvbnQtc2l6ZTogMTRweDtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIl19 */

이 글에서는 데이터 캐싱(Caching)을 구현하는 전략 중 하나인 Cache Aside에 대해 자세히 알아보고, 이를 구현할 때 고려해야 할 사항들을 소개하겠습니다.


Cache Aside 전략이란?

Cache Aside 전략은 애플리케이션이 데이터베이스와 캐시 사이에서 데이터를 관리하는 방식입니다. 이 전략에서는 애플리케이션이 직접 캐시를 제어하며, 필요한 데이터를 가져오거나 업데이트할 때 캐시와 데이터베이스를 적절히 활용합니다.

diff --git "a/public/Docker-Compose\353\241\234-Redis-\354\204\244\354\240\225\355\225\230\352\270\260.html" "b/public/Docker-Compose\353\241\234-Redis-\354\204\244\354\240\225\355\225\230\352\270\260.html" index 30491a4d..e65066a8 100644 --- "a/public/Docker-Compose\353\241\234-Redis-\354\204\244\354\240\225\355\225\230\352\270\260.html" +++ "b/public/Docker-Compose\353\241\234-Redis-\354\204\244\354\240\225\355\225\230\352\270\260.html" @@ -1,5 +1,5 @@ -Docker Compose로 Redis 설정하기 | Beoks Blog

앞선 포스팅에서는 Docker를 이용하여 Redis를 설치하고 실행하는 방법에 대해 알아보았습니다. 이번에는 docker-compose를 활용하여 Redis를 설정하고, 인증 정보 등을 포함한 다양한 설정을 적용하는 방법을 알아보겠습니다.

+Docker Compose로 Redis 설정하기 | Beoks Blog

앞선 포스팅에서는 Docker를 이용하여 Redis를 설치하고 실행하는 방법에 대해 알아보았습니다. 이번에는 docker-compose를 활용하여 Redis를 설정하고, 인증 정보 등을 포함한 다양한 설정을 적용하는 방법을 알아보겠습니다.


Redis용 docker-compose.yml 작성

docker-compose.yml 파일을 작성하여 Redis 컨테이너를 설정할 수 있습니다. 해당 파일에서 환경 변수나 볼륨, 포트 매핑 등을 지정하여 원하는 설정을 적용할 수 있습니다.

diff --git "a/public/Docker-\353\241\234-Redis-\354\204\244\354\271\230\355\225\230\352\270\260.html" "b/public/Docker-\353\241\234-Redis-\354\204\244\354\271\230\355\225\230\352\270\260.html" index 6f804b0f..16c4095d 100644 --- "a/public/Docker-\353\241\234-Redis-\354\204\244\354\271\230\355\225\230\352\270\260.html" +++ "b/public/Docker-\353\241\234-Redis-\354\204\244\354\271\230\355\225\230\352\270\260.html" @@ -1,5 +1,5 @@ -Docker 로 Redis 설치하기 | Beoks Blog

Docker로 Redis 설치하기

+Docker 로 Redis 설치하기 | Beoks Blog

Docker로 Redis 설치하기

개발을 진행하다 보면 캐시나 메시지 브로커로 Redis를 사용할 일이 많습니다. 이번 포스팅에서는 Docker를 이용하여 Redis를 설치하고 실행하는 방법에 대해 알아보겠습니다.


Docker 설치 여부 확인

diff --git a/public/HMAC(Hash-based-Message-Authentication-Code).html b/public/HMAC(Hash-based-Message-Authentication-Code).html new file mode 100644 index 00000000..a80d004b --- /dev/null +++ b/public/HMAC(Hash-based-Message-Authentication-Code).html @@ -0,0 +1,152 @@ + +HMAC(Hash-based Message Authentication Code) | Beoks Blog

현대 소프트웨어 개발에서 데이터의 무결성과 인증은 필수적인 요소입니다. 특히 네트워크를 통해 전송되는 데이터나 저장된 데이터가 변조되지 않았음을 확인하는 것은 보안의 기본입니다. HMAC(Hash-based Message Authentication Code)는 이러한 요구를 충족시키기 위한 암호학적 기법으로, 메시지의 무결성과 함께 발신자의 인증을 제공합니다. 이 글에서는 HMAC의 개념, 작동 원리, 구현 방법, 그리고 실제 사용 사례에 대해 상세히 알아보겠습니다.

+

HMAC이란?

+

HMAC은 Hash-based Message Authentication Code의 약자로, 메시지의 무결성을 검증하고 인증을 제공하는 특수한 형태의 메시지 인증 코드입니다. 일반적인 해시 함수와 달리, HMAC은 비밀 키를 사용하여 해시 값을 생성합니다. 이를 통해 메시지가 전송 중에 변경되지 않았음을 확인할 수 있을 뿐만 아니라, 메시지가 실제로 키를 알고 있는 발신자로부터 왔다는 것을 인증할 수 있습니다.

+

HMAC은 RFC 2104에서 정의되었으며, 다양한 해시 함수와 함께 사용할 수 있습니다. 가장 일반적으로 사용되는 조합은 HMAC-SHA256, HMAC-SHA1, HMAC-MD5 등이 있습니다.

+

HMAC의 보안 강도

+

HMAC의 보안 강도는 다음 요소에 의해 결정됩니다:

+
    +
  1. +

    사용되는 해시 함수의 강도: HMAC은 기본 해시 함수의 강도에 의존합니다. 예를 들어, HMAC-SHA256은 SHA-256 해시 함수를 사용하므로 그 보안 강도는 SHA-256의 강도와 관련이 있습니다.

    +
  2. +
  3. +

    키의 길이와 무작위성: 키가 길고 무작위적일수록 HMAC은 더 안전합니다. 키의 길이는 최소한 해시 함수의 출력 길이와 같거나 그 이상이어야 합니다.

    +
  4. +
  5. +

    키의 기밀성: HMAC의 보안은 키의 기밀성에 의존합니다. 키가 노출되면 공격자가 유효한 HMAC을 생성할 수 있으므로 키를 안전하게 관리하는 것이 중요합니다.

    +
  6. +
+

HMAC은 현재까지 알려진 공격에 대해 강력한 보안을 제공합니다. 특히, 이중 해싱 구조는 길이 확장 공격을 효과적으로 방지합니다.

+

Java에서의 HMAC 구현

+

Java에서 HMAC을 구현하는 것은 javax.crypto 패키지를 사용하여 비교적 간단하게 할 수 있습니다. 다음은 HMAC-SHA256을 구현하는 예시 코드입니다:

+
import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+ 
+public class HMACExample {
+    public static String calculateHMAC(String message, String key) 
+            throws NoSuchAlgorithmException, InvalidKeyException {
+        Mac mac = Mac.getInstance("HmacSHA256");
+        SecretKeySpec secretKeySpec = new SecretKeySpec(
+            key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+        mac.init(secretKeySpec);
+        byte[] hmacBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
+        return Base64.getEncoder().encodeToString(hmacBytes);
+    }
+    
+    public static void main(String[] args) {
+        try {
+            String message = "안녕하세요, 이것은 HMAC 테스트 메시지입니다.";
+            String key = "비밀키_12345";
+            String hmacResult = calculateHMAC(message, key);
+            System.out.println("메시지: " + message);
+            System.out.println("HMAC 결과: " + hmacResult);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
+

이 코드는 다음과 같은 단계로 작동합니다:

+
    +
  1. “HmacSHA256” 알고리즘을 사용하는 Mac 인스턴스를 생성합니다.
  2. +
  3. 비밀 키로부터 SecretKeySpec 객체를 생성합니다.
  4. +
  5. Mac 인스턴스를 초기화합니다.
  6. +
  7. 메시지에 대해 HMAC을 계산합니다.
  8. +
  9. 결과를 Base64로 인코딩하여 반환합니다.
  10. +
+

다른 해시 알고리즘(예: HmacSHA1, HmacMD5)을 사용하려면 “HmacSHA256” 대신 해당 알고리즘 이름을 지정하면 됩니다.

+

Spring 프레임워크에서의 HMAC 활용

+

Spring Security는 HMAC을 활용한 보안 기능을 제공합니다. 특히 RESTful API 인증에서 HMAC을 사용하는 방법을 살펴보겠습니다:

+
import org.springframework.security.crypto.codec.Hex;
+import org.springframework.stereotype.Component;
+ 
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+ 
+@Component
+public class HMACAuthenticationService {
+    private final String secretKey = "애플리케이션_비밀키";
+    
+    public String generateHMAC(String data) {
+        try {
+            Mac hmac = Mac.getInstance("HmacSHA256");
+            SecretKeySpec secretKeySpec = new SecretKeySpec(
+                secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+            hmac.init(secretKeySpec);
+            byte[] hmacBytes = hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+            return new String(Hex.encode(hmacBytes));
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            throw new RuntimeException("HMAC 생성 중 오류 발생", e);
+        }
+    }
+    
+    public boolean validateHMAC(String data, String providedHMAC) {
+        String calculatedHMAC = generateHMAC(data);
+        return calculatedHMAC.equals(providedHMAC);
+    }
+}
+

이 서비스는 Spring 애플리케이션에서 다음과 같이 사용할 수 있습니다:

+
    +
  1. API 요청에 대한 HMAC을 생성합니다.
  2. +
  3. 클라이언트가 제공한 HMAC과 서버에서 계산한 HMAC을 비교하여 요청의 무결성과 인증을 확인합니다.
  4. +
+

실제 구현에서는 비밀 키를 안전하게 관리하기 위해 환경 변수나 Spring의 프로퍼티 관리 시스템을 사용하는 것이 좋습니다.

+

HMAC의 실제 사용 사례

+

HMAC은 다양한 보안 애플리케이션에서 사용됩니다:

+
    +
  1. +

    API 인증: 많은 웹 API가 HMAC을 사용하여 요청의 무결성과 인증을 확인합니다. 예를 들어, AWS API는 HMAC을 사용하여 요청에 서명합니다.

    +
  2. +
  3. +

    웹 토큰: JWT(JSON Web Token)와 같은 웹 토큰은 HMAC을 사용하여 토큰의 무결성을 보장합니다.

    +
  4. +
  5. +

    비밀번호 저장: 비밀번호를 안전하게 저장하기 위해 HMAC을 사용할 수 있습니다. 이 경우 HMAC은 솔트와 함께 사용되어 레인보우 테이블 공격을 방지합니다.

    +
  6. +
  7. +

    메시지 인증: 안전한 통신 채널에서 메시지의 무결성과 인증을 확인하기 위해 HMAC을 사용합니다.

    +
  8. +
  9. +

    파일 무결성 검사: 파일이 변조되지 않았는지 확인하기 위해 HMAC을 사용할 수 있습니다.

    +
  10. +
+

HMAC과 다른 인증 기법의 비교

+

HMAC은 다른 인증 기법과 비교하여 몇 가지 장단점이 있습니다:

+

HMAC vs 일반 해시 함수

+
    +
  • 장점: HMAC은 비밀 키를 사용하므로 인증 기능을 제공합니다. 일반 해시 함수는 무결성만 제공합니다.
  • +
  • 단점: HMAC은 키 관리가 필요하므로 추가적인 복잡성이 있습니다.
  • +
+

HMAC vs 디지털 서명

+
    +
  • 장점: HMAC은 대칭 키를 사용하므로 계산이 빠릅니다.
  • +
  • 단점: HMAC은 부인 방지(non-repudiation) 기능을 제공하지 않습니다. 발신자와 수신자 모두 동일한 키를 가지고 있기 때문입니다.
  • +
+

HMAC vs CBC-MAC

+
    +
  • 장점: HMAC은 특별히 설계된 MAC 알고리즘으로, 블록 암호의 취약점에 영향을 받지 않습니다.
  • +
  • 단점: CBC-MAC은 블록 암호를 이미 사용하는 시스템에서 구현이 더 간단할 수 있습니다.
  • +
+

결론

+

HMAC은 메시지의 무결성과 인증을 보장하는 강력한 암호학적 기법입니다. 다양한 해시 함수와 함께 사용할 수 있으며, 특히 HMAC-SHA256은 현재 가장 널리 사용되는 조합 중 하나


\ No newline at end of file diff --git a/public/Hands-On-Modelers.html b/public/Hands-On-Modelers.html index 2d82147e..11d4fc8d 100644 --- a/public/Hands-On-Modelers.html +++ b/public/Hands-On-Modelers.html @@ -1,5 +1,5 @@ -Hands-On Modelers | Beoks Blog

소프트웨어 개발 프로젝트에서 Hands-On 모델러모델링과 구현을 동시에 수행하는 역할을 맡은 전문가를 의미합니다. 이들은 도메인 모델을 설계하고, 그 모델을 실제 코드로 구현함으로써 개발 팀이 효과적인 소프트웨어를 만들도록 이끕니다.

+Hands-On Modelers | Beoks Blog

소프트웨어 개발 프로젝트에서 Hands-On 모델러모델링과 구현을 동시에 수행하는 역할을 맡은 전문가를 의미합니다. 이들은 도메인 모델을 설계하고, 그 모델을 실제 코드로 구현함으로써 개발 팀이 효과적인 소프트웨어를 만들도록 이끕니다.

Hands-On 모델러의 역할과 중요성

모델링과 구현의 통합

Hands-On 모델러는 모델링 작업과 코딩 작업을 분리하지 않습니다. 모델링 과정에서 얻은 통찰력과 아이디어를 직접 코드로 구현하여 모델의 의도가 정확하게 반영되도록 합니다. 이를 통해 모델에서 구현으로의 의도 전달 손실을 최소화할 수 있습니다.

diff --git "a/public/Hibernate-\353\245\274-\354\235\264\354\232\251\355\225\234-Soft-Delete-\352\265\254\355\230\204.html" "b/public/Hibernate-\353\245\274-\354\235\264\354\232\251\355\225\234-Soft-Delete-\352\265\254\355\230\204.html" index c6d863a7..a307d2b2 100644 --- "a/public/Hibernate-\353\245\274-\354\235\264\354\232\251\355\225\234-Soft-Delete-\352\265\254\355\230\204.html" +++ "b/public/Hibernate-\353\245\274-\354\235\264\354\232\251\355\225\234-Soft-Delete-\352\265\254\355\230\204.html" @@ -1,5 +1,5 @@ -Hibernate 를 이용한 Soft Delete 구현 | Beoks Blog

소프트 딜리트(Soft Delete)는 데이터베이스에서 레코드를 실제로 삭제하지 않고, “삭제됨”을 나타내는 플래그를 설정하여 관련 데이터가 유지되도록 하는 기법입니다. 이렇게 하면 데이터 복구나 감사(audit)가 필요한 경우에도 데이터를 보존할 수 있습니다.

+Hibernate 를 이용한 Soft Delete 구현 | Beoks Blog

소프트 딜리트(Soft Delete)는 데이터베이스에서 레코드를 실제로 삭제하지 않고, “삭제됨”을 나타내는 플래그를 설정하여 관련 데이터가 유지되도록 하는 기법입니다. 이렇게 하면 데이터 복구나 감사(audit)가 필요한 경우에도 데이터를 보존할 수 있습니다.

Hibernate에서는 소프트 딜리트를 구현하기 위한 다양한 방법을 제공합니다. 아래에서는 Hibernate를 사용하여 소프트 딜리트를 구현하는 방법을 설명합니다.


1. 엔티티에 삭제 플래그 필드 추가

diff --git "a/public/HttpOnly-\354\277\240\355\202\244.html" "b/public/HttpOnly-\354\277\240\355\202\244.html" index a47207ef..dbd757dd 100644 --- "a/public/HttpOnly-\354\277\240\355\202\244.html" +++ "b/public/HttpOnly-\354\277\240\355\202\244.html" @@ -1,5 +1,5 @@ -HttpOnly 쿠키 | Beoks Blog

HttpOnly 쿠키란 무엇이고 왜 중요한가?

+HttpOnly 쿠키 | Beoks Blog

HttpOnly 쿠키란 무엇이고 왜 중요한가?

웹 개발을 하다 보면 쿠키를 사용하여 세션 정보를 저장하거나 사용자 상태를 유지하는 일이 빈번합니다. 그러나 쿠키는 보안 취약점에 노출될 수 있으며, 특히 XSS(Cross-Site Scripting)에 취약합니다. 이러한 위험을 줄이기 위해 HttpOnly 쿠키를 사용합니다. 이번 글에서는 HttpOnly 쿠키가 무엇이며, 어떻게 보안을 강화하는지에 대해 알아보겠습니다.


HttpOnly 쿠키란?

diff --git a/public/JPA-Criteria-API.html b/public/JPA-Criteria-API.html index 0b2b786d..816544d9 100644 --- a/public/JPA-Criteria-API.html +++ b/public/JPA-Criteria-API.html @@ -1,5 +1,5 @@ -JPA Criteria API | Beoks Blog

JPA(Java Persistence API)는 자바 애플리케이션에서 관계형 데이터를 관리하기 위한 표준 기술입니다. 그 중에서도 Criteria API는 타입 안전한(type-safe) 방식으로 쿼리를 구성할 수 있게 해주는 강력한 도구입니다. 이 글에서는 JPA Criteria API의 개념부터 실전 활용법까지 자세히 알아보겠습니다.

+JPA Criteria API | Beoks Blog

JPA(Java Persistence API)는 자바 애플리케이션에서 관계형 데이터를 관리하기 위한 표준 기술입니다. 그 중에서도 Criteria API는 타입 안전한(type-safe) 방식으로 쿼리를 구성할 수 있게 해주는 강력한 도구입니다. 이 글에서는 JPA Criteria API의 개념부터 실전 활용법까지 자세히 알아보겠습니다.

Criteria API란 무엇인가?

Criteria API는 JPA 2.0부터 도입된 프로그래밍 방식의 쿼리 작성 방법입니다. 이는 JPQL(Java Persistence Query Language)의 대안으로, 문자열 기반의 쿼리 대신 자바 코드로 쿼리를 작성할 수 있게 해줍니다. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:

    diff --git a/public/JPA-Specification-vs-QueryDSL.html b/public/JPA-Specification-vs-QueryDSL.html new file mode 100644 index 00000000..1df23c3b --- /dev/null +++ b/public/JPA-Specification-vs-QueryDSL.html @@ -0,0 +1,17 @@ + +JPA Specification vs QueryDSL | Beoks Blog

    \ No newline at end of file diff --git a/public/JPA-Specification.html b/public/JPA-Specification.html index fce8de3a..9f6ae25d 100644 --- a/public/JPA-Specification.html +++ b/public/JPA-Specification.html @@ -1,5 +1,5 @@ -JPA Specification | Beoks Blog

    Spring Data JPA에서 복잡한 동적 쿼리를 우아하게 처리할 수 있는 Specification 패턴에 대해 자세히 알아보겠습니다. 동적 쿼리를 작성할 때 발생하는 문제점들과 이를 해결하기 위한 Specification의 역할, 그리고 실제 사용 방법까지 단계별로 설명해드리겠습니다.

    +JPA Specification | Beoks Blog

    Spring Data JPA에서 복잡한 동적 쿼리를 우아하게 처리할 수 있는 Specification 패턴에 대해 자세히 알아보겠습니다. 동적 쿼리를 작성할 때 발생하는 문제점들과 이를 해결하기 위한 Specification의 역할, 그리고 실제 사용 방법까지 단계별로 설명해드리겠습니다.

    동적 쿼리의 문제점

    웹 애플리케이션에서 검색 기능을 구현할 때, 사용자는 다양한 조건을 조합하여 검색할 수 있어야 합니다. 예를 들어, 상품 목록에서 카테고리, 가격 범위, 브랜드 등의 조건을 선택적으로 적용하여 검색하는 기능이 필요할 수 있습니다.

    이러한 동적 쿼리를 구현하는 방법으로는 다음과 같은 접근법이 있습니다:

    diff --git "a/public/JPA\354\227\220\354\204\234-Soft-Delete\354\231\200-\354\234\240\353\213\210\355\201\254-\354\240\234\354\225\275\354\241\260\352\261\264-\354\262\230\353\246\254\355\225\230\352\270\260.html" "b/public/JPA\354\227\220\354\204\234-Soft-Delete\354\231\200-\354\234\240\353\213\210\355\201\254-\354\240\234\354\225\275\354\241\260\352\261\264-\354\262\230\353\246\254\355\225\230\352\270\260.html" index 759c0b41..d2d68767 100644 --- "a/public/JPA\354\227\220\354\204\234-Soft-Delete\354\231\200-\354\234\240\353\213\210\355\201\254-\354\240\234\354\225\275\354\241\260\352\261\264-\354\262\230\353\246\254\355\225\230\352\270\260.html" +++ "b/public/JPA\354\227\220\354\204\234-Soft-Delete\354\231\200-\354\234\240\353\213\210\355\201\254-\354\240\234\354\225\275\354\241\260\352\261\264-\354\262\230\353\246\254\355\225\230\352\270\260.html" @@ -1,5 +1,5 @@ -JPA에서 Soft Delete와 유니크 제약조건 처리하기 | Beoks Blog

    JPA를 사용하면서 Soft Delete 를 구현할 때, 유니크 제약조건을 가진 필드 때문에 새로운 데이터를 삽입할 때 문제가 발생할 수 있습니다. 특히, 이미 Soft Delete된 엔티티가 동일한 유니크 키를 가지고 있을 경우, 새로운 데이터를 삽입하려고 하면 데이터베이스는 여전히 유니크 키 제약조건 위반을 발생시킵니다.

    +JPA에서 Soft Delete와 유니크 제약조건 처리하기 | Beoks Blog

    JPA를 사용하면서 Soft Delete 를 구현할 때, 유니크 제약조건을 가진 필드 때문에 새로운 데이터를 삽입할 때 문제가 발생할 수 있습니다. 특히, 이미 Soft Delete된 엔티티가 동일한 유니크 키를 가지고 있을 경우, 새로운 데이터를 삽입하려고 하면 데이터베이스는 여전히 유니크 키 제약조건 위반을 발생시킵니다.

    이번 글에서는 이러한 문제를 해결하기 위한 간단한 방법을 소개하겠습니다. 기존의 Soft Delete된 엔티티를 다시 활성화하면서 유니크 제약조건 오류를 우회하는 방법입니다.

    문제 상황