
WWDC 2025 " What s New in Passkey" 요약
비밀번호는 그간 수많은 보안 사고의 원인이 되어 왔습니다.전세계 데이터 유출사고의 약 80% 이상 취약한 패스워드로 인해 발생했다는 결과가 있을만큼이죠. 취약한 패스워드의 70%는 단 1초만에 해킹이 가능하다고도 해요.이런 문제로 비밀번호를 없애는 새로운 기술 패스키(Passkey)가 등장합니다.패스키는 비밀번호를 대체하는 새로운 기술로 사용자의 기기와 이용하는 서비스 양 쪽에 생성된 암호화 키 쌍으로 동작하는 차세대 인증 기술이에요.패스키의 등장과 그 역사를 논하자면 더 많은 설명이 필요하지만 이번 포스트는 Apple의 WWDC25 세션 중 가장 인상깊었던 “What’s New in Passkeys” 를 요약해 설명드리고자 해요.이 세션을 통해 Apple은 생태계 전반 (iOS, iPad OS, macOS, visionOS)에 걸쳐 회원가입 단계부터 패스키 관리 이전까지의 전체 흐름을 아우르는 한층 진화된 패스키 인증 프레임워크를 보여주었는데요,패스키 기술의 진화 발전은 궁극적으로 피싱(phishing)을 방지하는 보안 수준으로의 성숙도를 향해 가고 있다는 점과 실제 시장에서 의미있는 적용 결과들이 속속들이 발표되었습니다.특히 이번 세션에서는 서비스 기획자 및 개발자 관점에서 적용할 수 있는 사용자 시나리오 그리고 개선된 API를 통해 서비스 혁신에 효과적인 핵심 포인트를 총 5개의 Chapter로 구성하였어요.그럼 각 항목의 핵심 내용 위주로 살펴보도록 하겠습니다.가장 먼저 아이디와 패스워드 설정없이 패스키 기반의 회원가입이 가능해졌음을 알렸습니다.기존의 비밀번호 기반의 회원가입도 자동 완성 (Auto-fill) 그리고 강력한 비밀번호 제안 등의 기능 지원으로 사용자 경험이 개선되었지만, 여전히 모든 설정 화면을 거쳐 정보를 입력해야 합니다.또한, 비밀번호라는 피싱 요소 (phishable factor)가 존재해요.하지만 Account Creation API를 통해 회원가입부터 Credential Manager (iOS Keychain 등 암호 관리자)에서 이미 검증된 이메일과 회원정보(예. 이름)을 그대로 사용해패스키 기반의 회원가입(Sign-up with a passkey)을 적용하면 사용자 경험과 보안 수준 모두 향상할 수 있게 됩니다.사용자는 한 번의 클릭으로 회원가입이 가능하게 됩니다.서비스의 계정 정보(이메일, 이름 등)의 최신 변경 사항이 사용자의 Credential Manager에 자동으로 반영되어 정보 갱신을 위한 별도의 사용자 행위가 불필요해 집니다.이를 위해 개발자는 Account 정보 변경 시, Signal API를 호출하는 기능을 구현하면 됩니다.패스키 지원 이전의 서비스에서 비밀번호를 사용하고 있던 기존 사용자들에게 백그라운드에서 자동으로 패스키를 생성해 제공할 수 있습니다.사용자는 기존 방식의 로그인 이후 패스키 업그레이드 알림을 받게되고, 그 다음 인증시부터 별도의 등록절차 없이 패스키 로그인이 가능해집니다.이를 통해 사용자의 패스키 인증방식 전환과 사용자 경험의 개선이 보다 자연스럽게 이루어질 수 있습니다./credential-management 와 같은 REST 기반 Endpoint를 통해 서비스별 패스키를 Credential Manager에서 조회 · 폐기 · 업데이트 할 수 있게 했습니다.즉, Credential Manager 화면에서 서비스별 패스키를 직접 추가할 수 있는 경로를 제공하여 서비스 밖에서도 사용자 패스키 경험을 확대할 수 있습니다.크로스 플랫폼 사용자 편의성 향상을 위해 서로 다른 Credential Manager 또는 다른 기기로 더 안전하고 쉽게 이전할 수 있도록 지원합니다.사용자는 기존 관리자 혹은 기기에서 Face ID 등 인증 방식을 통해 권한을 검증하고 간편하게 패스키를 이전하여 연속적으로 관리할 수 있습니다.이제 패스키는 단순한 로그인 수단을 넘어, 계정 생성 > 상태 동기화 > 자동 전환(생성) > 마이그레이션까지 커버하는 통합 인증 기술을 의미하는 개념이 되었습니다.서비스 제공자는 기존 가입 및 로그인에서 작은 수정만으로도 보안을 강화하고 서비스의 진입의 허들을 대폭 낮추어 서비스 본질에 집중할 수 있습니다.• None 패스키: 인증 기술의 미래, 비밀번호 없는 세상으로의 발걸음
6/18/2025

WWDC 2025 " What s New in Passkey" 요약
비밀번호는 그간 수많은 보안 사고의 원인이 되어 왔습니다.전세계 데이터 유출사고의 약 80% 이상 취약한 패스워드로 인해 발생했다는 결과가 있을만큼이죠. 취약한 패스워드의 70%는 단 1초만에 해킹이 가능하다고도 해요.이런 문제로 비밀번호를 없애는 새로운 기술 패스키(Passkey)가 등장합니다.패스키는 비밀번호를 대체하는 새로운 기술로 사용자의 기기와 이용하는 서비스 양 쪽에 생성된 암호화 키 쌍으로 동작하는 차세대 인증 기술이에요.패스키의 등장과 그 역사를 논하자면 더 많은 설명이 필요하지만 이번 포스트는 Apple의 WWDC25 세션 중 가장 인상깊었던 “What’s New in Passkeys” 를 요약해 설명드리고자 해요.이 세션을 통해 Apple은 생태계 전반 (iOS, iPad OS, macOS, visionOS)에 걸쳐 회원가입 단계부터 패스키 관리 이전까지의 전체 흐름을 아우르는 한층 진화된 패스키 인증 프레임워크를 보여주었는데요,패스키 기술의 진화 발전은 궁극적으로 피싱(phishing)을 방지하는 보안 수준으로의 성숙도를 향해 가고 있다는 점과 실제 시장에서 의미있는 적용 결과들이 속속들이 발표되었습니다.특히 이번 세션에서는 서비스 기획자 및 개발자 관점에서 적용할 수 있는 사용자 시나리오 그리고 개선된 API를 통해 서비스 혁신에 효과적인 핵심 포인트를 총 5개의 Chapter로 구성하였어요.그럼 각 항목의 핵심 내용 위주로 살펴보도록 하겠습니다.가장 먼저 아이디와 패스워드 설정없이 패스키 기반의 회원가입이 가능해졌음을 알렸습니다.기존의 비밀번호 기반의 회원가입도 자동 완성 (Auto-fill) 그리고 강력한 비밀번호 제안 등의 기능 지원으로 사용자 경험이 개선되었지만, 여전히 모든 설정 화면을 거쳐 정보를 입력해야 합니다.또한, 비밀번호라는 피싱 요소 (phishable factor)가 존재해요.하지만 Account Creation API를 통해 회원가입부터 Credential Manager (iOS Keychain 등 암호 관리자)에서 이미 검증된 이메일과 회원정보(예. 이름)을 그대로 사용해패스키 기반의 회원가입(Sign-up with a passkey)을 적용하면 사용자 경험과 보안 수준 모두 향상할 수 있게 됩니다.사용자는 한 번의 클릭으로 회원가입이 가능하게 됩니다.서비스의 계정 정보(이메일, 이름 등)의 최신 변경 사항이 사용자의 Credential Manager에 자동으로 반영되어 정보 갱신을 위한 별도의 사용자 행위가 불필요해 집니다.이를 위해 개발자는 Account 정보 변경 시, Signal API를 호출하는 기능을 구현하면 됩니다.패스키 지원 이전의 서비스에서 비밀번호를 사용하고 있던 기존 사용자들에게 백그라운드에서 자동으로 패스키를 생성해 제공할 수 있습니다.사용자는 기존 방식의 로그인 이후 패스키 업그레이드 알림을 받게되고, 그 다음 인증시부터 별도의 등록절차 없이 패스키 로그인이 가능해집니다.이를 통해 사용자의 패스키 인증방식 전환과 사용자 경험의 개선이 보다 자연스럽게 이루어질 수 있습니다./credential-management 와 같은 REST 기반 Endpoint를 통해 서비스별 패스키를 Credential Manager에서 조회 · 폐기 · 업데이트 할 수 있게 했습니다.즉, Credential Manager 화면에서 서비스별 패스키를 직접 추가할 수 있는 경로를 제공하여 서비스 밖에서도 사용자 패스키 경험을 확대할 수 있습니다.크로스 플랫폼 사용자 편의성 향상을 위해 서로 다른 Credential Manager 또는 다른 기기로 더 안전하고 쉽게 이전할 수 있도록 지원합니다.사용자는 기존 관리자 혹은 기기에서 Face ID 등 인증 방식을 통해 권한을 검증하고 간편하게 패스키를 이전하여 연속적으로 관리할 수 있습니다.이제 패스키는 단순한 로그인 수단을 넘어, 계정 생성 > 상태 동기화 > 자동 전환(생성) > 마이그레이션까지 커버하는 통합 인증 기술을 의미하는 개념이 되었습니다.서비스 제공자는 기존 가입 및 로그인에서 작은 수정만으로도 보안을 강화하고 서비스의 진입의 허들을 대폭 낮추어 서비스 본질에 집중할 수 있습니다.• None 패스키: 인증 기술의 미래, 비밀번호 없는 세상으로의 발걸음
2025.06.18

좋아요

별로에요

한 줄로 끝내는 iOS 화면 생성: Scaffold + Makefile
안녕하세요, 여기어때 iOS개발팀 두부입니다.여기어때 iOS 앱은 1년 전인 2024년 5월 Tuist 도입을 통한 모듈화를 진행했고, 현재는 Micro Feature Architecture 기반의 유연한 Feature 개발을 통해 빠르게 성장해 나가고 있습니다.최근에는 SwiftUI와 MVI Pattern을 결합한 UI 로직과 상태 관리가 명확히 분리되는 아키텍처를 채택하며 팀 내부 기술 스택은 한층 견고해졌습니다. 이 덕분에 공통 컴포넌트 재사용은 물론, 기능 개발 시 일관된 규칙 적용이 자연스럽게 뒤따르고 있죠.Photo by Jack Sloop on Unsplash여기어때 iOS 앱에서는 SwiftUI 화면을 개발할 때 아래 5가지 파일로 책임을 분리하고 있습니다.CoordinatorHostingViewControllerReducerView (View)ObservableReducerReducer이렇게 분리된 레이어 덕분에 명확하게 상태를 관리할 수 있지만, 매번 수작업으로 5개의 파일을 생성하고 기본 코드를 채우는 과정은 매우 번거롭습니다. 하지만 이러한 ‘Boilerplate Code’를 자동으로 생성할 수 있다면, 앞에서 언급한 기본 코드를 반복적으로 채우는 업무 패턴을 역으로 이용해 생산성을 높이는 구조를 만들 수 있지 않을까요?바로 이 지점에서 Tuist Scaffold와 Makefile을 활용한 자동화가 빛을 발합니다.Scaffold?Scaffold는 건설, 건축 등 산업현장에서 쓰이는 임시로 설치한 가시설물 등을 뜻하는데요. 프로그래밍에서의 Scaffolding은 기본 구조를 빠르게 생성하는 자동화된 코드 생성 기술을 의미합니다.소프트웨어를 개발하다 보면, 기존 아키텍처가 있는 프로젝트에서 개발자가 프로젝트와 일관성 있는 새로운 컴포넌트나 기능을 부트스트랩하고 싶을 때가 생깁니다. Tuist는 이러한 Scaffolding 기술을 Template으로 관리할 수 있게 제공하고 있습니다. Template은 Tuist에서 기본으로 제공해 주는 템플릿을 사용할 수도 있고, 직접 만들어 사용할 수도 있습니다.이번 글에서는 화면 개발 시 흔히 겪는 불편을 해소하기 위해, 생성된 모듈에 Boilerplate Code를 자동으로 생성하는 Tuist Template을 만드는 방법을 다뤄보겠습니다.Template을 톺아봅시다Template 생성먼저, Template을 생성하는 법에 대해서 알아보겠습니다.디렉터리 이름과 동일한 .swift파일을 생성합니다. (Manifest file)이때 주의할 점은, 파일 및 폴더명을 아래 형식에 맞게 설정해주어야 한다는 것 입니다....└─ Tuist └── Templates └── SwiftUIScreen ✅ └── SwiftUIScreen.swift ✅✅ Templates 하위 폴더 이름과 그 폴더 안에 있는 .swift 파일 이름이 동일해야 합니다.Template 정의Tuist Template의 핵심은 Manifest라 불리는 .swift 파일입니다. 이 파일에
echo
swift
tuist
6/18/2025

한 줄로 끝내는 iOS 화면 생성: Scaffold + Makefile
안녕하세요, 여기어때 iOS개발팀 두부입니다.여기어때 iOS 앱은 1년 전인 2024년 5월 Tuist 도입을 통한 모듈화를 진행했고, 현재는 Micro Feature Architecture 기반의 유연한 Feature 개발을 통해 빠르게 성장해 나가고 있습니다.최근에는 SwiftUI와 MVI Pattern을 결합한 UI 로직과 상태 관리가 명확히 분리되는 아키텍처를 채택하며 팀 내부 기술 스택은 한층 견고해졌습니다. 이 덕분에 공통 컴포넌트 재사용은 물론, 기능 개발 시 일관된 규칙 적용이 자연스럽게 뒤따르고 있죠.Photo by Jack Sloop on Unsplash여기어때 iOS 앱에서는 SwiftUI 화면을 개발할 때 아래 5가지 파일로 책임을 분리하고 있습니다.CoordinatorHostingViewControllerReducerView (View)ObservableReducerReducer이렇게 분리된 레이어 덕분에 명확하게 상태를 관리할 수 있지만, 매번 수작업으로 5개의 파일을 생성하고 기본 코드를 채우는 과정은 매우 번거롭습니다. 하지만 이러한 ‘Boilerplate Code’를 자동으로 생성할 수 있다면, 앞에서 언급한 기본 코드를 반복적으로 채우는 업무 패턴을 역으로 이용해 생산성을 높이는 구조를 만들 수 있지 않을까요?바로 이 지점에서 Tuist Scaffold와 Makefile을 활용한 자동화가 빛을 발합니다.Scaffold?Scaffold는 건설, 건축 등 산업현장에서 쓰이는 임시로 설치한 가시설물 등을 뜻하는데요. 프로그래밍에서의 Scaffolding은 기본 구조를 빠르게 생성하는 자동화된 코드 생성 기술을 의미합니다.소프트웨어를 개발하다 보면, 기존 아키텍처가 있는 프로젝트에서 개발자가 프로젝트와 일관성 있는 새로운 컴포넌트나 기능을 부트스트랩하고 싶을 때가 생깁니다. Tuist는 이러한 Scaffolding 기술을 Template으로 관리할 수 있게 제공하고 있습니다. Template은 Tuist에서 기본으로 제공해 주는 템플릿을 사용할 수도 있고, 직접 만들어 사용할 수도 있습니다.이번 글에서는 화면 개발 시 흔히 겪는 불편을 해소하기 위해, 생성된 모듈에 Boilerplate Code를 자동으로 생성하는 Tuist Template을 만드는 방법을 다뤄보겠습니다.Template을 톺아봅시다Template 생성먼저, Template을 생성하는 법에 대해서 알아보겠습니다.디렉터리 이름과 동일한 .swift파일을 생성합니다. (Manifest file)이때 주의할 점은, 파일 및 폴더명을 아래 형식에 맞게 설정해주어야 한다는 것 입니다....└─ Tuist └── Templates └── SwiftUIScreen ✅ └── SwiftUIScreen.swift ✅✅ Templates 하위 폴더 이름과 그 폴더 안에 있는 .swift 파일 이름이 동일해야 합니다.Template 정의Tuist Template의 핵심은 Manifest라 불리는 .swift 파일입니다. 이 파일에
2025.06.18
echo
swift
tuist

좋아요

별로에요

네트워크 기반 Compose Preview가 안보인다면?
안녕하세요, 여기어때컴퍼니(이하 여기어때) Android개발팀 테드입니다.YDS 컴포넌트 중 YDSImage의 Compose Preview가 렌더링되지 않는 문제를 발견했고, 이를 어떻게 개선했는지 공유드리고자 합니다.본격적인 내용에 들어가기 전에, YDS와 YDSImage가 무엇인지 간단히 짚고 넘어가겠습니다.※ YDS가 낯선 분들은 아래 설명을 읽어보시면 전체 내용을 이해하는데 도움이 됩니다.YDS(Yeogi Design System)?여기어때는 UI 일관성을 매우 중요하게 생각합니다. 그래서 사내 디자인 시스템인 YDS(Yeogi Design System)를 활용하여 텍스트, 색상, 여백, 컴포넌트 규격은 물론, 상태 처리 방식까지 체계적으로 정의하고 있습니다. 이를 바탕으로 Android개발팀과 iOS개발팀이 모두 동일한 기준으로 화면을 구성하고 있습니다. 제가 속한 Android개발팀 역시 YDS의 가이드라인을 기반으로 컴포넌트를 작성하고 있고, 실제 앱 화면에서도 이를 적극 활용하고 있습니다. 덕분에 화면 간 일관성을 자연스럽게 유지할 수 있고, 코드 재사용성이 높아 유지보수와 기능 확장이 쉬운 구조를 갖추고 있습니다.저는 아직 인턴으로 합류한 지 3개월밖에 되지 않았지만, YDS의 장점을 크게 체감하고 있습니다. 디자이너 분들께서 Figma 내에서 YDS에 정의된 규격을 기반으로 화면을 일관성 있게 설계해 주신 덕분에, 제가 디자인 의도를 파악하는 데 혼란이 없었습니다. 코드 역시 컴포넌트 단위로 잘 추상화되어 있어, UI 구현 과정이 매우 수월했습니다.그 결과 UI 구현 과정에서 드는 시간과 시행착오가 눈에 띄게 줄었고, 동일한 피쳐를 개발하는 iOS개발팀과의 싱크를 맞추기 위한 별도의 시간이나 비용이 거의 들지 않아서 개발 효율 면에서도 크게 만족하고 있습니다 :)YDSImage 컴포넌트?YDSImage는 YDS의 여러 컴포넌트에서 공통적으로 사용하는 네트워크 기반 이미지 렌더링 컴포넌트입니다.@Composablefun YDSImage( modifier: Modifier = Modifier, url: String?, customDefaultImage: @Composable (() -> Unit)? = null, ...) { var imageLoadState: State by remember { mutableStateOf(State.Empty) } val imageRequest = remember(url) { defaultImageRequest(context = LocalContext.current, data = url) } Box(modifier = modifier) { when (imageLoadState) { is State.Loading -> Skeleton() is State.Error -> customDefaultImage?.invoke() ?: DefaultImage() else ->
6/18/2025

네트워크 기반 Compose Preview가 안보인다면?
안녕하세요, 여기어때컴퍼니(이하 여기어때) Android개발팀 테드입니다.YDS 컴포넌트 중 YDSImage의 Compose Preview가 렌더링되지 않는 문제를 발견했고, 이를 어떻게 개선했는지 공유드리고자 합니다.본격적인 내용에 들어가기 전에, YDS와 YDSImage가 무엇인지 간단히 짚고 넘어가겠습니다.※ YDS가 낯선 분들은 아래 설명을 읽어보시면 전체 내용을 이해하는데 도움이 됩니다.YDS(Yeogi Design System)?여기어때는 UI 일관성을 매우 중요하게 생각합니다. 그래서 사내 디자인 시스템인 YDS(Yeogi Design System)를 활용하여 텍스트, 색상, 여백, 컴포넌트 규격은 물론, 상태 처리 방식까지 체계적으로 정의하고 있습니다. 이를 바탕으로 Android개발팀과 iOS개발팀이 모두 동일한 기준으로 화면을 구성하고 있습니다. 제가 속한 Android개발팀 역시 YDS의 가이드라인을 기반으로 컴포넌트를 작성하고 있고, 실제 앱 화면에서도 이를 적극 활용하고 있습니다. 덕분에 화면 간 일관성을 자연스럽게 유지할 수 있고, 코드 재사용성이 높아 유지보수와 기능 확장이 쉬운 구조를 갖추고 있습니다.저는 아직 인턴으로 합류한 지 3개월밖에 되지 않았지만, YDS의 장점을 크게 체감하고 있습니다. 디자이너 분들께서 Figma 내에서 YDS에 정의된 규격을 기반으로 화면을 일관성 있게 설계해 주신 덕분에, 제가 디자인 의도를 파악하는 데 혼란이 없었습니다. 코드 역시 컴포넌트 단위로 잘 추상화되어 있어, UI 구현 과정이 매우 수월했습니다.그 결과 UI 구현 과정에서 드는 시간과 시행착오가 눈에 띄게 줄었고, 동일한 피쳐를 개발하는 iOS개발팀과의 싱크를 맞추기 위한 별도의 시간이나 비용이 거의 들지 않아서 개발 효율 면에서도 크게 만족하고 있습니다 :)YDSImage 컴포넌트?YDSImage는 YDS의 여러 컴포넌트에서 공통적으로 사용하는 네트워크 기반 이미지 렌더링 컴포넌트입니다.@Composablefun YDSImage( modifier: Modifier = Modifier, url: String?, customDefaultImage: @Composable (() -> Unit)? = null, ...) { var imageLoadState: State by remember { mutableStateOf(State.Empty) } val imageRequest = remember(url) { defaultImageRequest(context = LocalContext.current, data = url) } Box(modifier = modifier) { when (imageLoadState) { is State.Loading -> Skeleton() is State.Error -> customDefaultImage?.invoke() ?: DefaultImage() else ->
2025.06.18

좋아요

별로에요

BFF(Backend for Frontend) 가 여기어때에서 하는 일
안녕하세요! 여기어때 BFF개발팀 미카엘입니다. 오늘은 여기어때에서 BFF가 어떤 역할을 하는지, 왜 필요한지, 그리고 실제로 어떻게 활용되고 있는지에 대해 이야기해보려고 해요. 기술적인 개념이지만 최대한 쉽게 풀어볼 테니 편하게 읽어주세요!BFF(Backend for Frontend)란?BFF(Backend for Frontend)는 각각의 프론트엔드(Web, iOS, Android 등)에 맞춰진 최적화된 백엔드 계층(Layer)을 의미해요. 말 그대로 “프론트엔드를 위한 백엔드”라는 뜻이죠.예를 들어, iOS와 Android 앱에서 같은 데이터를 받아야 하지만, 각 클라이언트의 특성이 다르다면 어떻게 해야 할까요? BFF 없이 백엔드에서 직접 데이터를 주면, 프론트엔드에서 불필요한 데이터를 걸러내거나 가공해야 해서 비효율적일 수 있어요. 반면, BFF가 있으면 클라이언트의 특성에 맞춰 데이터 가공을 한 후 전달하므로, 프론트엔드 개발이 훨씬 쉬워지고 성능도 최적화될 수 있는 것이죠.여기어때에서는 왜 BFF가 필요할까요?여기어때는 숙박뿐만 아니라 항공권, 레저 티켓, 여행 패키지 등 다양한 서비스를 운영하고 있어요. 게다가 이 서비스는 B2B 웹페이지, B2C 웹페이지, Android 및 iOS 앱 등 다양한 클라이언트를 통해 제공됩니다.모든 클라이언트가 같은 백엔드를 호출하면 어떻게 될까요?불필요한 데이터까지 모두 받아야 해서 네트워크 비용 증가하고, 프론트엔드에서 데이터를 변환해야 해서 처리 속도가 저하하고, 새로운 기능 추가할땐 각 클라이언트의 코드 변경 부담이 증가하겠죠.이런 문제를 해결하기 위해서는 BFF를 활용하면 됩니다!여기어때에서는 각 클라이언트별 맞춤형 API를 제공하는 BFF 구조를 통해서 성능을 최적화하고 있습니다.BFF가 여기어때에서 하는 일1. 클라이언트별 맞춤형 API 제공각 클라이언트(Web, Android, iOS)는 사용 방식과 요구사항이 다릅니다.모바일 앱(Android, iOS)은 네트워크 트래픽을 최소화하고 배터리 사용량을 절감하기 위해 가벼운 데이터를 요청해야 하고 B2B 웹페이지는 대량의 데이터를 조회하고 분석하는 기능이 필요하고 B2C 웹페이지는 사용자 경험을 극대화하기 위해 풍부한 데이터를 제공해야 할 수 있지요.BFF는 이러한 차이를 반영하여 각 클라이언트에 적합한 데이터 형태로 응답을 줍니다.덕분에 불필요한 데이터 전송을 줄이고, 최적화된 성능을 제공할 수 있죠.2. 성능 최적화여기어때의 서비스에서는 실시간 예약, 빠른 검색, 대량 데이터 조회가 아주 중요한 요소입니다.특히 모바일 환경에서는 네트워크 비용과 응답 시간이 중요한데, BFF를 통해 이를 최적화할 수 있습니다.네트워크 트래픽 감소: 모바일에서는 꼭 필요한 데이터만 전송하여 트래픽 비용을 줄일 수 있습니다.캐싱 활용: 자주 요청되는 데이터는 캐싱하여 백엔드 부하를 줄이고 응답 속도를 빠르게 만들어줍니다.데이터 가공: 프론트엔드에서 데이터를 가공할 필요 없이, BFF에서 필요한 형태로 변환하여 전송합니다.3. 보안 및 인증 처리B2
6/18/2025

BFF(Backend for Frontend) 가 여기어때에서 하는 일
안녕하세요! 여기어때 BFF개발팀 미카엘입니다. 오늘은 여기어때에서 BFF가 어떤 역할을 하는지, 왜 필요한지, 그리고 실제로 어떻게 활용되고 있는지에 대해 이야기해보려고 해요. 기술적인 개념이지만 최대한 쉽게 풀어볼 테니 편하게 읽어주세요!BFF(Backend for Frontend)란?BFF(Backend for Frontend)는 각각의 프론트엔드(Web, iOS, Android 등)에 맞춰진 최적화된 백엔드 계층(Layer)을 의미해요. 말 그대로 “프론트엔드를 위한 백엔드”라는 뜻이죠.예를 들어, iOS와 Android 앱에서 같은 데이터를 받아야 하지만, 각 클라이언트의 특성이 다르다면 어떻게 해야 할까요? BFF 없이 백엔드에서 직접 데이터를 주면, 프론트엔드에서 불필요한 데이터를 걸러내거나 가공해야 해서 비효율적일 수 있어요. 반면, BFF가 있으면 클라이언트의 특성에 맞춰 데이터 가공을 한 후 전달하므로, 프론트엔드 개발이 훨씬 쉬워지고 성능도 최적화될 수 있는 것이죠.여기어때에서는 왜 BFF가 필요할까요?여기어때는 숙박뿐만 아니라 항공권, 레저 티켓, 여행 패키지 등 다양한 서비스를 운영하고 있어요. 게다가 이 서비스는 B2B 웹페이지, B2C 웹페이지, Android 및 iOS 앱 등 다양한 클라이언트를 통해 제공됩니다.모든 클라이언트가 같은 백엔드를 호출하면 어떻게 될까요?불필요한 데이터까지 모두 받아야 해서 네트워크 비용 증가하고, 프론트엔드에서 데이터를 변환해야 해서 처리 속도가 저하하고, 새로운 기능 추가할땐 각 클라이언트의 코드 변경 부담이 증가하겠죠.이런 문제를 해결하기 위해서는 BFF를 활용하면 됩니다!여기어때에서는 각 클라이언트별 맞춤형 API를 제공하는 BFF 구조를 통해서 성능을 최적화하고 있습니다.BFF가 여기어때에서 하는 일1. 클라이언트별 맞춤형 API 제공각 클라이언트(Web, Android, iOS)는 사용 방식과 요구사항이 다릅니다.모바일 앱(Android, iOS)은 네트워크 트래픽을 최소화하고 배터리 사용량을 절감하기 위해 가벼운 데이터를 요청해야 하고 B2B 웹페이지는 대량의 데이터를 조회하고 분석하는 기능이 필요하고 B2C 웹페이지는 사용자 경험을 극대화하기 위해 풍부한 데이터를 제공해야 할 수 있지요.BFF는 이러한 차이를 반영하여 각 클라이언트에 적합한 데이터 형태로 응답을 줍니다.덕분에 불필요한 데이터 전송을 줄이고, 최적화된 성능을 제공할 수 있죠.2. 성능 최적화여기어때의 서비스에서는 실시간 예약, 빠른 검색, 대량 데이터 조회가 아주 중요한 요소입니다.특히 모바일 환경에서는 네트워크 비용과 응답 시간이 중요한데, BFF를 통해 이를 최적화할 수 있습니다.네트워크 트래픽 감소: 모바일에서는 꼭 필요한 데이터만 전송하여 트래픽 비용을 줄일 수 있습니다.캐싱 활용: 자주 요청되는 데이터는 캐싱하여 백엔드 부하를 줄이고 응답 속도를 빠르게 만들어줍니다.데이터 가공: 프론트엔드에서 데이터를 가공할 필요 없이, BFF에서 필요한 형태로 변환하여 전송합니다.3. 보안 및 인증 처리B2
2025.06.18

좋아요

별로에요

에이닷 및 D9을 위한 웹뷰 인터페이스 소개
에이닷 및 D9을 위한 웹뷰 인터페이스 소개• None 네이티브 앱(Android, iOS)과 웹뷰(WebView)에 로드된 웹 콘텐츠(HTML, JavaScript)가 서로 소통할 수 있도록 연결하는 다리 역할의 기술입니다.• None• None 네이티브 코드에서 JavaScript 코드를 직접 실행하여 웹 콘텐츠를 제어합니다.• None• None Web의 JavaScript에서 특정 인터페이스(함수)를 호출하면, 네이티브 앱에 약속된 기능이 실행됩니다.2. 왜 웹뷰 인터페이스가 필요할까요? 🚀• None• None 웹에서는 직접 접근할 수 없는 스마트폰의 고유 기능(카메라, GPS, 진동, 푸시 알림 등)을 사용해야 할 때 필요합니다.• None• None 웹뷰 내에서 화면 전환, 정보 요청 등을 네이티브 앱처럼 부드럽고 빠르게 처리하여 이질감을 줄여줍니다.• None• None 중요한 사용자 정보나 앱의 핵심 데이터를 웹이 아닌 네이티브 영역에서 안전하게 처리하고 관리할 수 있습니다.• None• None 이미 만들어진 웹 서비스를 앱의 일부 기능으로 통합하여 개발 효율성을 높일 수 있습니다.3. 효율적인 웹뷰 인터페이스 설계 및 사용 가이드 ✍️• None• None• None 함수의 역할을 명확하게 알 수 있도록 이름을 정합니다. (예: getUserInfo, openCamera, closeWebView)• None• None 안드로이드와 iOS에서 동일한 기능은 동일한 이름의 인터페이스를 사용하여 웹 개발의 편의성을 높입니다.• None• None• None 복잡한 데이터를 주고받을 때는 JSON 형식을 사용하여 구조화하고 파싱하기 용이하게 만듭니다.• None• None 네이티브 기능 처리 후 결과를 웹으로 전달해야 할 경우, 콜백(Callback) 함수를 파라미터로 받아 비동기 처리를 명확하게 합니다.• None• None• None 신뢰할 수 있는 도메인의 웹 페이지만 인터페이스를 호출할 수 있도록 제한합니다.• None• None 웹에서 전달받은 데이터는 항상 네이티브단에서 유효성을 검증한 후 사용합니다.• None• None• None 빈번하고 반복적인 호출보다는 여러 데이터를 한 번에 묶어 요청하는 것이 효율적입니다.• None• None 시간이 오래 걸리는 작업은 비동기적으로 처리하여 웹뷰의 렌더링을 방해하지 않도록 합니다.• None 웹페이지에서 특정 동작이 필요할 때, 아래 함수를 호출하여 스마트폰의 고유 기능이나 앱의 기능을 실행할 수 있습니다.현재 열려있는 웹뷰 화면을 닫고 이전 화면으로 돌아갑니다. 웹뷰 내의 입력 필드 등을 위해 시스템 키보드를 강제로 노출시킵니다. 새로운 웹뷰(인앱 브라우저)를 열어 전달된 url을 로드합니다. 앱을 벗어나지 않고 다른 웹페이지를 보여줄 때 사용합니다. 디바이스의 기본 브라우저(Chrome, Safari 등)를 열어 url을 로드합니다. 앱 내의 특정 화면(쿠폰함, 이벤트 페이지 등)을 직접 엽니다. json 데이터로 필요한 정보를 전달합니다. 사용자의 현재 위치 정보를 요청합니다. 권한이 없으면 권한 요청 팝업을 먼저 띄웁니다. (결과는 setLocationResult로 전달) 위치, 카메라, 앨범(사진첩) 접근 권한을 명시적으로 요청합니다. (결과는 setPermissionResults로 전달) 안드로이드의 '뒤로가기' 버튼 이벤트를 웹에서 직접 제어할지(true), 앱에서 처리할지(false) 결정합니다. (이벤트는 onBackPress로 전달) OS의 기본 공유 기능을 호출하여 url을 다른 앱(메신저, SNS 등)으로 공유합니다. Base64로 인코딩된 이미지를 스마트폰 앨범에 저장합니다. (결과는 setSaveImageResult로 전달) 앱이 실행 중인 동안 사라지지 않는 데이터를 네이티브 메모리에 임시로 저장합니다. (예: 로그인 상태 유지) setSessionData로 저장했던 데이터를 key 값으로 조회합니다. (결과는 responseSessionData로 전달) 민감한 데이터를 안전하게 사용하기 위해 암호화된 정보를 앱에 요청합니다. (결과는 setEncryptedPayload로 전달)Native → Web (앱에서 웹으로 정보 전달)• None 네이티브 앱에서 특정 작업이 완료되거나 이벤트가 발생했을 때, 웹페이지의 함수를 호출하여 결과나 데이터를 전달합니다.requestLocation() 호출에 대한 응답으로, 획득한 위도(latitude)와 경도(longitude) 정보를 웹으로 전달합니다. requestPermission() 또는 checkPermissions() 호출 후, 각 권한의 승인 여부(granted)를 JSON 형태로 전달합니다. requestSessionData(key) 호출에 대한 응답으로, key에 해당하는 value를 전달합니다. requestEncryptedPayload(id) 요청에 대한 응답으로, 암호화된 데이터(value)를 전달합니다. id는 어떤 요청에 대한 응답인지 구분하는 값입니다. saveImage() 호출 후, 이미지 저장 성공 여부를 true 또는 false로 전달합니다. setBackPressHandlingEnabled(true) 상태에서 사용자가 안드로이드 '뒤로가기' 버튼을 눌렀을 때, 이 함수가 웹에서 실행됩니다.5. 마무리하며: 네이티브와 웹의 완벽한 시너지를 위하여지금까지 안드로이드와 iOS 앱 환경에서 네이티브와 웹뷰가 서로 소통하는 창구인 웹뷰 인터페이스(JavaScript Bridge)에 대해 알아보았습니다.웹뷰 인터페이스는 단순히 웹 콘텐츠를 앱 안에 표시하는 것을 넘어, 웹 기술의 유연성과 네이티브 앱의 강력한 기능을 결합하여 사용자 경험을 극대화하는 핵심적인 역할을 합니다.카메라, 위치 정보, 공유 기능처럼 스마트폰 고유의 기능을 웹에서 자연스럽게 사용하고, 앱의 특정 화면으로 이동하거나 데이터를 안전하게 관리하는 모든 과정이 바로 이 인터페이스를 통해 이루어집니다.우리가 살펴본 closeWindow, requestLocation, openNativeScreen 등의 다양한 인터페이스들은 이러한 소통이 실제로 어떻게
javascript
6/18/2025

에이닷 및 D9을 위한 웹뷰 인터페이스 소개
에이닷 및 D9을 위한 웹뷰 인터페이스 소개• None 네이티브 앱(Android, iOS)과 웹뷰(WebView)에 로드된 웹 콘텐츠(HTML, JavaScript)가 서로 소통할 수 있도록 연결하는 다리 역할의 기술입니다.• None• None 네이티브 코드에서 JavaScript 코드를 직접 실행하여 웹 콘텐츠를 제어합니다.• None• None Web의 JavaScript에서 특정 인터페이스(함수)를 호출하면, 네이티브 앱에 약속된 기능이 실행됩니다.2. 왜 웹뷰 인터페이스가 필요할까요? 🚀• None• None 웹에서는 직접 접근할 수 없는 스마트폰의 고유 기능(카메라, GPS, 진동, 푸시 알림 등)을 사용해야 할 때 필요합니다.• None• None 웹뷰 내에서 화면 전환, 정보 요청 등을 네이티브 앱처럼 부드럽고 빠르게 처리하여 이질감을 줄여줍니다.• None• None 중요한 사용자 정보나 앱의 핵심 데이터를 웹이 아닌 네이티브 영역에서 안전하게 처리하고 관리할 수 있습니다.• None• None 이미 만들어진 웹 서비스를 앱의 일부 기능으로 통합하여 개발 효율성을 높일 수 있습니다.3. 효율적인 웹뷰 인터페이스 설계 및 사용 가이드 ✍️• None• None• None 함수의 역할을 명확하게 알 수 있도록 이름을 정합니다. (예: getUserInfo, openCamera, closeWebView)• None• None 안드로이드와 iOS에서 동일한 기능은 동일한 이름의 인터페이스를 사용하여 웹 개발의 편의성을 높입니다.• None• None• None 복잡한 데이터를 주고받을 때는 JSON 형식을 사용하여 구조화하고 파싱하기 용이하게 만듭니다.• None• None 네이티브 기능 처리 후 결과를 웹으로 전달해야 할 경우, 콜백(Callback) 함수를 파라미터로 받아 비동기 처리를 명확하게 합니다.• None• None• None 신뢰할 수 있는 도메인의 웹 페이지만 인터페이스를 호출할 수 있도록 제한합니다.• None• None 웹에서 전달받은 데이터는 항상 네이티브단에서 유효성을 검증한 후 사용합니다.• None• None• None 빈번하고 반복적인 호출보다는 여러 데이터를 한 번에 묶어 요청하는 것이 효율적입니다.• None• None 시간이 오래 걸리는 작업은 비동기적으로 처리하여 웹뷰의 렌더링을 방해하지 않도록 합니다.• None 웹페이지에서 특정 동작이 필요할 때, 아래 함수를 호출하여 스마트폰의 고유 기능이나 앱의 기능을 실행할 수 있습니다.현재 열려있는 웹뷰 화면을 닫고 이전 화면으로 돌아갑니다. 웹뷰 내의 입력 필드 등을 위해 시스템 키보드를 강제로 노출시킵니다. 새로운 웹뷰(인앱 브라우저)를 열어 전달된 url을 로드합니다. 앱을 벗어나지 않고 다른 웹페이지를 보여줄 때 사용합니다. 디바이스의 기본 브라우저(Chrome, Safari 등)를 열어 url을 로드합니다. 앱 내의 특정 화면(쿠폰함, 이벤트 페이지 등)을 직접 엽니다. json 데이터로 필요한 정보를 전달합니다. 사용자의 현재 위치 정보를 요청합니다. 권한이 없으면 권한 요청 팝업을 먼저 띄웁니다. (결과는 setLocationResult로 전달) 위치, 카메라, 앨범(사진첩) 접근 권한을 명시적으로 요청합니다. (결과는 setPermissionResults로 전달) 안드로이드의 '뒤로가기' 버튼 이벤트를 웹에서 직접 제어할지(true), 앱에서 처리할지(false) 결정합니다. (이벤트는 onBackPress로 전달) OS의 기본 공유 기능을 호출하여 url을 다른 앱(메신저, SNS 등)으로 공유합니다. Base64로 인코딩된 이미지를 스마트폰 앨범에 저장합니다. (결과는 setSaveImageResult로 전달) 앱이 실행 중인 동안 사라지지 않는 데이터를 네이티브 메모리에 임시로 저장합니다. (예: 로그인 상태 유지) setSessionData로 저장했던 데이터를 key 값으로 조회합니다. (결과는 responseSessionData로 전달) 민감한 데이터를 안전하게 사용하기 위해 암호화된 정보를 앱에 요청합니다. (결과는 setEncryptedPayload로 전달)Native → Web (앱에서 웹으로 정보 전달)• None 네이티브 앱에서 특정 작업이 완료되거나 이벤트가 발생했을 때, 웹페이지의 함수를 호출하여 결과나 데이터를 전달합니다.requestLocation() 호출에 대한 응답으로, 획득한 위도(latitude)와 경도(longitude) 정보를 웹으로 전달합니다. requestPermission() 또는 checkPermissions() 호출 후, 각 권한의 승인 여부(granted)를 JSON 형태로 전달합니다. requestSessionData(key) 호출에 대한 응답으로, key에 해당하는 value를 전달합니다. requestEncryptedPayload(id) 요청에 대한 응답으로, 암호화된 데이터(value)를 전달합니다. id는 어떤 요청에 대한 응답인지 구분하는 값입니다. saveImage() 호출 후, 이미지 저장 성공 여부를 true 또는 false로 전달합니다. setBackPressHandlingEnabled(true) 상태에서 사용자가 안드로이드 '뒤로가기' 버튼을 눌렀을 때, 이 함수가 웹에서 실행됩니다.5. 마무리하며: 네이티브와 웹의 완벽한 시너지를 위하여지금까지 안드로이드와 iOS 앱 환경에서 네이티브와 웹뷰가 서로 소통하는 창구인 웹뷰 인터페이스(JavaScript Bridge)에 대해 알아보았습니다.웹뷰 인터페이스는 단순히 웹 콘텐츠를 앱 안에 표시하는 것을 넘어, 웹 기술의 유연성과 네이티브 앱의 강력한 기능을 결합하여 사용자 경험을 극대화하는 핵심적인 역할을 합니다.카메라, 위치 정보, 공유 기능처럼 스마트폰 고유의 기능을 웹에서 자연스럽게 사용하고, 앱의 특정 화면으로 이동하거나 데이터를 안전하게 관리하는 모든 과정이 바로 이 인터페이스를 통해 이루어집니다.우리가 살펴본 closeWindow, requestLocation, openNativeScreen 등의 다양한 인터페이스들은 이러한 소통이 실제로 어떻게
2025.06.18
javascript

좋아요

별로에요

KMP 기반 UI 컴포넌트 통합 전략
네이버 사내 기술 교류 행사인 NAVER ENGINEERING DAY 2025(5월)에서 발표되었던 세션을 공개합니다.멀티플랫폼에 대응 가능한 밴드 디자인 시스템의 설계 및 구현 방식에 대해 소개하고, 밴드 안드로이드 팀에서 아이콘 리소스를 자동화하여 관리하는 방법에 대해서 공유합니다.• 안드로이드 개발자 및 멀티플랫폼에 대한 관심이 있는 분• 디자인 시스템에 관심이 있는 분Episode 1 : 디자인 시스템, 개념에서 실전까지 Episode 2 : KMP, 진짜 실무에 쓸 수 있을까? Episode 3 : BDS 모듈에 KMP 적용하기 Episode 4 : 디자인 시스템 UI 컴포넌트, Maven으로 배포하기 Episode 5 : Figicon으로 아이콘 자동 동기화하기NAVER에서는 사내 개발 경험과 기술 트렌드를 교류를 할 수 있는 프로그램이 많이 있습니다. 그중 매회 평균 70개 이상의 발표가 이루어지는 NAVER ENGINEERING DAY를 빼놓을 수 없는데요. 2016년부터 시작된 ENGINEERING DAY는 실무에서의 기술 개발 경험과 새로운 기술과 플랫폼 도입 시 유용하게 활용될 수 있는 팁 등을 공유하며 서로 배우고 성장하는 네이버의 대표적인 사내 개발자 행사입니다. 올해 진행된 NAVER ENGINEERING DAY 2025(5월)의 일부 세션을 공개합니다.
6/18/2025

KMP 기반 UI 컴포넌트 통합 전략
네이버 사내 기술 교류 행사인 NAVER ENGINEERING DAY 2025(5월)에서 발표되었던 세션을 공개합니다.멀티플랫폼에 대응 가능한 밴드 디자인 시스템의 설계 및 구현 방식에 대해 소개하고, 밴드 안드로이드 팀에서 아이콘 리소스를 자동화하여 관리하는 방법에 대해서 공유합니다.• 안드로이드 개발자 및 멀티플랫폼에 대한 관심이 있는 분• 디자인 시스템에 관심이 있는 분Episode 1 : 디자인 시스템, 개념에서 실전까지 Episode 2 : KMP, 진짜 실무에 쓸 수 있을까? Episode 3 : BDS 모듈에 KMP 적용하기 Episode 4 : 디자인 시스템 UI 컴포넌트, Maven으로 배포하기 Episode 5 : Figicon으로 아이콘 자동 동기화하기NAVER에서는 사내 개발 경험과 기술 트렌드를 교류를 할 수 있는 프로그램이 많이 있습니다. 그중 매회 평균 70개 이상의 발표가 이루어지는 NAVER ENGINEERING DAY를 빼놓을 수 없는데요. 2016년부터 시작된 ENGINEERING DAY는 실무에서의 기술 개발 경험과 새로운 기술과 플랫폼 도입 시 유용하게 활용될 수 있는 팁 등을 공유하며 서로 배우고 성장하는 네이버의 대표적인 사내 개발자 행사입니다. 올해 진행된 NAVER ENGINEERING DAY 2025(5월)의 일부 세션을 공개합니다.
2025.06.18

좋아요

별로에요

에러 0%, MSA에서의 Enum 관리 전략
안녕하세요, 토스뱅크 Server Developer 김인회 · 이준영입니다.토스뱅크는 다른 은행과 달리 시스템을 MSA(Microservice Architecture)로 구축해 빠른 개발과 배포 속도를 누리고 있습니다. 하지만 "은탄환은 없다"는 말처럼, MSA는 다양한 복잡함을 수반했는데요. 그중 하나가 바로 Enum입니다.이번 글에서는 MSA 환경에서 Enum이 왜 문제가 될 수 있는지, 그리고 토스뱅크는 이를 어떻게 해결했는지에 대해 소개해 드리려고 합니다.MSA에서는 독이 될 수 있는 EnumEnum은 타입 안정성, 컴파일 타임 체크, IDE의 자동완성 등의 장점이 있어서 Java 혹은 Kotlin을 다뤄본 개발자라면 자주 사용하는 도구일 것 입니다.하지만 MSA환경에서는 이러한 Enum이 자칫 독이 될 수도 있는데요.MSA에서는 하나의 Enum을 각 서버끼리 공유할 때 정의되지 않은 Enum 값이 넘어오면 Deserialize 에러가 발생합니다.많게는 1년에 한두번씩 꾸준히 발생하는 이 문제는, 단순히 휴먼 에러라고 치부하기에는 너무 빈번하고 치명적이에요. 담당자가 바뀌거나, 서비스가 확장되거나, 새로운 Enum 값이 추가되는 순간 다시 문제가 발생하기 시작했고 토스뱅크 또한 예외는 아니었습니다.발전하는 MSA 속에서 Enum 관리가 점점 어려워지기 시작했고 결국 사람의 주의가 아닌 시스템적으로 이 문제를 예방하고 관리할 수 있는 방법이 필요해지기 시작했습니다.MSA에서 Enum 문제가 복잡해지는 이유 중 하나는 통신 주체에 따라 기대하는 동작이 다르기 때문입니다.그래서 저희는 요청 주체 기준으로 Enum 사용을 '제공자'와 '소비자'로 나누었고 각각의 Use Case에 맞는 전략을 수립했습니다.1. 클라이언트(소비자) → 서버(제공자): 요청을 수신하는 입장에서는 오류가 명확해야 한다.클라이언트가 Enum 값을 요청으로 전달하고, 서버가 이를 파싱해 처리하는 구조에서 서버는 Enum 값의 의미를 정확히 이해하고 처리해야 하므로, 정의되지 않은 값을 받을 경우 즉시 에러를 반환하는 것이 타당합니다.예를 들어 으로 처리해 흐름을 이어가면, 의도하지 않은 상태로 비즈니스 로직이 흘러갈 수 있고, 데이터 무결성이 훼손될 우려가 있습니다.따라서 이 경우에는 Enum 값을 엄격하게 검증하고, 알 수 없는 값은 등의 클라이언트 오류로 응답하는 것이 바람직합니다.2. 서버(제공자) → 클라이언트(소비자) : 처리는 소비자의 선택에 맡긴다반대로 서버가 Enum 값을 응답으로 보내고, 클라이언트가 이를 파싱하는 구조에서는 상황이 달라집니다. 서버가 Enum을 확장하여 새로운 값을 보내더라도, 클라이언트는 그 값을 아직 모를 수 있어요. 이때는 클라이언트 쪽에서 어떻게 대응할지를 선택할 수 있어야 합니다.• None 알 수 없는 값이면 경고를 띄우고 무시한다• None 오류를 발생시켜 사용자에게 명시적으로 알린다이처럼 소비자가 상황에 맞게 처리 전략을 선택할 수 있도록 열려 있는 구조가 필요합니다.Kafka 소비자나 이벤트 핸들러 등 외부로부터
6/18/2025

에러 0%, MSA에서의 Enum 관리 전략
안녕하세요, 토스뱅크 Server Developer 김인회 · 이준영입니다.토스뱅크는 다른 은행과 달리 시스템을 MSA(Microservice Architecture)로 구축해 빠른 개발과 배포 속도를 누리고 있습니다. 하지만 "은탄환은 없다"는 말처럼, MSA는 다양한 복잡함을 수반했는데요. 그중 하나가 바로 Enum입니다.이번 글에서는 MSA 환경에서 Enum이 왜 문제가 될 수 있는지, 그리고 토스뱅크는 이를 어떻게 해결했는지에 대해 소개해 드리려고 합니다.MSA에서는 독이 될 수 있는 EnumEnum은 타입 안정성, 컴파일 타임 체크, IDE의 자동완성 등의 장점이 있어서 Java 혹은 Kotlin을 다뤄본 개발자라면 자주 사용하는 도구일 것 입니다.하지만 MSA환경에서는 이러한 Enum이 자칫 독이 될 수도 있는데요.MSA에서는 하나의 Enum을 각 서버끼리 공유할 때 정의되지 않은 Enum 값이 넘어오면 Deserialize 에러가 발생합니다.많게는 1년에 한두번씩 꾸준히 발생하는 이 문제는, 단순히 휴먼 에러라고 치부하기에는 너무 빈번하고 치명적이에요. 담당자가 바뀌거나, 서비스가 확장되거나, 새로운 Enum 값이 추가되는 순간 다시 문제가 발생하기 시작했고 토스뱅크 또한 예외는 아니었습니다.발전하는 MSA 속에서 Enum 관리가 점점 어려워지기 시작했고 결국 사람의 주의가 아닌 시스템적으로 이 문제를 예방하고 관리할 수 있는 방법이 필요해지기 시작했습니다.MSA에서 Enum 문제가 복잡해지는 이유 중 하나는 통신 주체에 따라 기대하는 동작이 다르기 때문입니다.그래서 저희는 요청 주체 기준으로 Enum 사용을 '제공자'와 '소비자'로 나누었고 각각의 Use Case에 맞는 전략을 수립했습니다.1. 클라이언트(소비자) → 서버(제공자): 요청을 수신하는 입장에서는 오류가 명확해야 한다.클라이언트가 Enum 값을 요청으로 전달하고, 서버가 이를 파싱해 처리하는 구조에서 서버는 Enum 값의 의미를 정확히 이해하고 처리해야 하므로, 정의되지 않은 값을 받을 경우 즉시 에러를 반환하는 것이 타당합니다.예를 들어 으로 처리해 흐름을 이어가면, 의도하지 않은 상태로 비즈니스 로직이 흘러갈 수 있고, 데이터 무결성이 훼손될 우려가 있습니다.따라서 이 경우에는 Enum 값을 엄격하게 검증하고, 알 수 없는 값은 등의 클라이언트 오류로 응답하는 것이 바람직합니다.2. 서버(제공자) → 클라이언트(소비자) : 처리는 소비자의 선택에 맡긴다반대로 서버가 Enum 값을 응답으로 보내고, 클라이언트가 이를 파싱하는 구조에서는 상황이 달라집니다. 서버가 Enum을 확장하여 새로운 값을 보내더라도, 클라이언트는 그 값을 아직 모를 수 있어요. 이때는 클라이언트 쪽에서 어떻게 대응할지를 선택할 수 있어야 합니다.• None 알 수 없는 값이면 경고를 띄우고 무시한다• None 오류를 발생시켜 사용자에게 명시적으로 알린다이처럼 소비자가 상황에 맞게 처리 전략을 선택할 수 있도록 열려 있는 구조가 필요합니다.Kafka 소비자나 이벤트 핸들러 등 외부로부터
2025.06.18

좋아요

별로에요

일 평균 30억 건을 처리하는 결제 시스템의 DB를 Vitess로 교체하기 - 1. 솔루션 선정기
안녕하세요. LINE Billing Platform 개발 팀의 김영재, 이정재입니다. LINE Billing Platform 개발 팀에서는 LINE 앱 내 여러 서비스에서 사용하는 결제 및 포인트 플랫폼을 이용한 결제 기능을 제공하고 있습니다.저희 팀에서는 최근 가장 핵심 시스템이자 오랫동안 운영해 온 결제 시스템의 DB를 Nbase-T에서 Vitess로 마이그레이션했습니다. 마이그레이션하게 된 가장 큰 이유는 기존에는 비용이 들지 않았지만 최근 계약 관계에 변화 가 발생해 라이선스 비용이 추가되는 상황이 되었기 때문입니다.저희는 마이그레이션할 솔루션 선정 과정과 마이그레이션 후 실제 개발 및 운영 단계에서 Vitess를 어떻게 활용하고 있는지 두 편으로 나눠 소개하려고 합니다. 먼저 이번 글에서는 마이그레이션할 솔루션으로 Vitess를 선정한 과정과 이유를 말씀드리겠습니다.마이그레이션 대상이 결제 코어 시스템이었기 때문에 솔루션을 선정할 때 성능 대비 운용 비용이 낮은 기준으로 어떤 솔루션이 있는지 조사했습니다. 그중 PoC까지 진행해 비교 분석했던 샤딩(sharding) 솔루션인 Apache ShardingSphere와 TiDB, Vitess의 특징을 소개하고, 진행 과정에서 발생한 이슈 및 이슈 해결 방법을 공유하겠습니다.Apache ShardingSphere의 경우 프락시와 JDBC 방식을 모두 지원하며, 각 방식의 특징은 다음과 같습니다.Apache ShardingSphere은 프락시와 JDBC 방식을 모두 지원하기 때문에 서비스 상황에 맞게 유연하게 애플리케이션을 구성할 수 있는 샤딩 솔루션입니다. 다양한 샤딩 전략을 제공하며, 분산 트랜잭션도 지원하는 등 여러 기본적인 기능에 충실합니다.다만 프락시 레이어 방식은 SQL 통합 처리 정도의 매우 단순한 기능만 처리할 수 있다는 한계가 있습니다. JDBC 레이어 방식 또한 여러 컴포넌트에서 사용할 경우 개별 구현 포인트가 많아지고 관리 비용도 무시할 수 없다는 단점이 있습니다. 특히, 두 방식 모두 데이터가 각 샤드에 고르게 분배될 경우 데이터 리샤딩(리밸런싱)을 직접 구현해야 한다는 단점이 있습니다. 이러한 추가적인 개발 비용을 제외한다면 괜찮은 샤딩 솔루션이지만, 저희가 구현해야 할 추가적인 개발 포인트가 많아 PoC 대상에서는 제외되었습니다.TiDB의 경우 사내 MySQL DB 팀의 추천으로 알게 됐으며, 사내 VOOM 팀의 ESPA 조직에서 사용한 선례가 있어 PoC 진행시 MySQL DB 팀과 VOOM 팀의 많은 도움을 받았습니다.TiDB는 MySQL 호환 분산형 SQL 데이터베이스입니다. 대규모 데이터 처리와 고가용성, 확장성을 목표로 OLTP(Online Transactional Processing) 및 OLAP(Online Analytical Processing) 워크로드 모두에 적합하도록 설계돼 있습니다. 기존 샤딩 솔루션과는 메커니즘이 완전히 다른데요. TiDB는 아래 그림과 같이 크게 세 개(TiDB, PD, 스토리지)로 나뉜 클러스터 영역으로 구성됩니다.각 영역에서 수행하는 역할은 다음과 같습니다.• 스토리지 클러스터: 스토리지 클러스터는 OLTP 워크로드를 위한 행 기반(row-based) 스토리지인 TiKV와, OLAP 워크로드를 위한 열 기반(column-based) 스토리지인 TFlash로 구성됩니다.• 여기서 TiKV는 분산 키-값 저장소로, TiDB의 데이터 저장 계층을 구성 데이터를 분산해 저장하고, ACID 트랜잭션을 지원합니다. 이 영역에서 실제 데이터가 분산 저장됩니다.• TiDB 클러스터: TiDB는 SQL 계층으로 MySQL 프로토콜을 지원하며, 사용자의 SQL 요청을 받아 이를 실행 계획으로 변환해 TiKV에 데이터를 읽고 쓰는 작업을 수행합니다.• PD 클러스터: PD는 클러스터의 메타 데이터 관리 및 조정 작업을 담당합니다. TiKV 클러스터의 데이터 분할 및 배치를 관리하며, 클러스터의 전체적인 균형을 유지합니다.TiDB는 실제로 MySQL 영역인 TiDB 클러스터에 데이터를 저장하는 것이 아니라 스토리지 클러스터 내의 TiKV에 데이터를 저장합니다. 따라서 별도의 샤딩 전략이 필요하지 않고, 데이터 관리할 때 샤딩키값도 필요 없습니다. 메커니즘 자체가 전체 TiKV에 자동으로 리밸런싱되는 형태이기 때문에 부하와 데이터를 균등하게 분산할 수 있는데요. 이런 점을 고려했을 때 현재 저희 상황에서 개발과 DBA의 운용 비용을 상당히 줄일 수 있는 이점이 있다고 판단해 PoC를 진행했습니다.Vitess는 대규모 MySQL 데이터베이스 클러스터를 관리하기 위한 오픈 소스 데이터베이스 클러스터링 시스템입니다. YouTube에서 MySQL 데이터베이스의 확장 문제를 해결하기 위해 개발했으며, 현재는 CNCF(Cloud Native Computing Foundation)의 프로젝트로 관리되고 있습니다(참고).Vitess의 특징은 다음과 같습니다.• 확장성: 대량의 트래픽을 처리하기 위해 데이터베이스를 수평으로 확장할 수 있습니다. 이를 위해 샤 딩 기술을 사용합니다.• 고가용성: 자동 장애 조치 및 복제를 통해 고가용성(high availability, 이하 HA)을 보장합니다.• 데이터베이스 추상화: 애플리케이션이 데이터베이스의 물리적 레이아웃을 알 필요 없도록 Vitess가 이를 추상화해 처리합니다.• 쿼리 라우팅 및 최적화: 쿼리를 적절한 샤드로 라우팅하고 최적화해 성능을 향상시킵니다.• 클라우드 네이티브: 컨테이너 환경에서 쉽게 배포하고 관리할 수 있도록 설계됐습니다.Vitess는 서비스의 성격이나 상황에 따라 유연하게 선택할 수 있도록 클라우드와 베어 메탈(bare metal), 하이브리드 환경을 모두 제공합니다. 저희 서비스는 결제 시스템이라는 특성상 잠시라도 페일오버(failover)가 발생하면 회사에 손실이 발생하기 때문에 베어 메탈 방식, 즉 물리적인 서버에 직접 설치하는 방식을 선택했습니다. Vitess 베어 메탈 환경은 GitHub이나 Slack 등의 서비스에서도 사용하고 있는데요. 이와 같은 유수의 기업들이 사용하고 있다는 것도 큰 장점으로 작용했습니다
mysql
6/18/2025

일 평균 30억 건을 처리하는 결제 시스템의 DB를 Vitess로 교체하기 - 1. 솔루션 선정기
안녕하세요. LINE Billing Platform 개발 팀의 김영재, 이정재입니다. LINE Billing Platform 개발 팀에서는 LINE 앱 내 여러 서비스에서 사용하는 결제 및 포인트 플랫폼을 이용한 결제 기능을 제공하고 있습니다.저희 팀에서는 최근 가장 핵심 시스템이자 오랫동안 운영해 온 결제 시스템의 DB를 Nbase-T에서 Vitess로 마이그레이션했습니다. 마이그레이션하게 된 가장 큰 이유는 기존에는 비용이 들지 않았지만 최근 계약 관계에 변화 가 발생해 라이선스 비용이 추가되는 상황이 되었기 때문입니다.저희는 마이그레이션할 솔루션 선정 과정과 마이그레이션 후 실제 개발 및 운영 단계에서 Vitess를 어떻게 활용하고 있는지 두 편으로 나눠 소개하려고 합니다. 먼저 이번 글에서는 마이그레이션할 솔루션으로 Vitess를 선정한 과정과 이유를 말씀드리겠습니다.마이그레이션 대상이 결제 코어 시스템이었기 때문에 솔루션을 선정할 때 성능 대비 운용 비용이 낮은 기준으로 어떤 솔루션이 있는지 조사했습니다. 그중 PoC까지 진행해 비교 분석했던 샤딩(sharding) 솔루션인 Apache ShardingSphere와 TiDB, Vitess의 특징을 소개하고, 진행 과정에서 발생한 이슈 및 이슈 해결 방법을 공유하겠습니다.Apache ShardingSphere의 경우 프락시와 JDBC 방식을 모두 지원하며, 각 방식의 특징은 다음과 같습니다.Apache ShardingSphere은 프락시와 JDBC 방식을 모두 지원하기 때문에 서비스 상황에 맞게 유연하게 애플리케이션을 구성할 수 있는 샤딩 솔루션입니다. 다양한 샤딩 전략을 제공하며, 분산 트랜잭션도 지원하는 등 여러 기본적인 기능에 충실합니다.다만 프락시 레이어 방식은 SQL 통합 처리 정도의 매우 단순한 기능만 처리할 수 있다는 한계가 있습니다. JDBC 레이어 방식 또한 여러 컴포넌트에서 사용할 경우 개별 구현 포인트가 많아지고 관리 비용도 무시할 수 없다는 단점이 있습니다. 특히, 두 방식 모두 데이터가 각 샤드에 고르게 분배될 경우 데이터 리샤딩(리밸런싱)을 직접 구현해야 한다는 단점이 있습니다. 이러한 추가적인 개발 비용을 제외한다면 괜찮은 샤딩 솔루션이지만, 저희가 구현해야 할 추가적인 개발 포인트가 많아 PoC 대상에서는 제외되었습니다.TiDB의 경우 사내 MySQL DB 팀의 추천으로 알게 됐으며, 사내 VOOM 팀의 ESPA 조직에서 사용한 선례가 있어 PoC 진행시 MySQL DB 팀과 VOOM 팀의 많은 도움을 받았습니다.TiDB는 MySQL 호환 분산형 SQL 데이터베이스입니다. 대규모 데이터 처리와 고가용성, 확장성을 목표로 OLTP(Online Transactional Processing) 및 OLAP(Online Analytical Processing) 워크로드 모두에 적합하도록 설계돼 있습니다. 기존 샤딩 솔루션과는 메커니즘이 완전히 다른데요. TiDB는 아래 그림과 같이 크게 세 개(TiDB, PD, 스토리지)로 나뉜 클러스터 영역으로 구성됩니다.각 영역에서 수행하는 역할은 다음과 같습니다.• 스토리지 클러스터: 스토리지 클러스터는 OLTP 워크로드를 위한 행 기반(row-based) 스토리지인 TiKV와, OLAP 워크로드를 위한 열 기반(column-based) 스토리지인 TFlash로 구성됩니다.• 여기서 TiKV는 분산 키-값 저장소로, TiDB의 데이터 저장 계층을 구성 데이터를 분산해 저장하고, ACID 트랜잭션을 지원합니다. 이 영역에서 실제 데이터가 분산 저장됩니다.• TiDB 클러스터: TiDB는 SQL 계층으로 MySQL 프로토콜을 지원하며, 사용자의 SQL 요청을 받아 이를 실행 계획으로 변환해 TiKV에 데이터를 읽고 쓰는 작업을 수행합니다.• PD 클러스터: PD는 클러스터의 메타 데이터 관리 및 조정 작업을 담당합니다. TiKV 클러스터의 데이터 분할 및 배치를 관리하며, 클러스터의 전체적인 균형을 유지합니다.TiDB는 실제로 MySQL 영역인 TiDB 클러스터에 데이터를 저장하는 것이 아니라 스토리지 클러스터 내의 TiKV에 데이터를 저장합니다. 따라서 별도의 샤딩 전략이 필요하지 않고, 데이터 관리할 때 샤딩키값도 필요 없습니다. 메커니즘 자체가 전체 TiKV에 자동으로 리밸런싱되는 형태이기 때문에 부하와 데이터를 균등하게 분산할 수 있는데요. 이런 점을 고려했을 때 현재 저희 상황에서 개발과 DBA의 운용 비용을 상당히 줄일 수 있는 이점이 있다고 판단해 PoC를 진행했습니다.Vitess는 대규모 MySQL 데이터베이스 클러스터를 관리하기 위한 오픈 소스 데이터베이스 클러스터링 시스템입니다. YouTube에서 MySQL 데이터베이스의 확장 문제를 해결하기 위해 개발했으며, 현재는 CNCF(Cloud Native Computing Foundation)의 프로젝트로 관리되고 있습니다(참고).Vitess의 특징은 다음과 같습니다.• 확장성: 대량의 트래픽을 처리하기 위해 데이터베이스를 수평으로 확장할 수 있습니다. 이를 위해 샤 딩 기술을 사용합니다.• 고가용성: 자동 장애 조치 및 복제를 통해 고가용성(high availability, 이하 HA)을 보장합니다.• 데이터베이스 추상화: 애플리케이션이 데이터베이스의 물리적 레이아웃을 알 필요 없도록 Vitess가 이를 추상화해 처리합니다.• 쿼리 라우팅 및 최적화: 쿼리를 적절한 샤드로 라우팅하고 최적화해 성능을 향상시킵니다.• 클라우드 네이티브: 컨테이너 환경에서 쉽게 배포하고 관리할 수 있도록 설계됐습니다.Vitess는 서비스의 성격이나 상황에 따라 유연하게 선택할 수 있도록 클라우드와 베어 메탈(bare metal), 하이브리드 환경을 모두 제공합니다. 저희 서비스는 결제 시스템이라는 특성상 잠시라도 페일오버(failover)가 발생하면 회사에 손실이 발생하기 때문에 베어 메탈 방식, 즉 물리적인 서버에 직접 설치하는 방식을 선택했습니다. Vitess 베어 메탈 환경은 GitHub이나 Slack 등의 서비스에서도 사용하고 있는데요. 이와 같은 유수의 기업들이 사용하고 있다는 것도 큰 장점으로 작용했습니다
2025.06.18
mysql

좋아요

별로에요

한/글 문서 파일 형식: Python을 통한 HWPX 포맷 파싱하기 (1)
“한/글 문서 파일 형식 : HWPX 포맷 구조 살펴보기”를 통해 HWPX 문서의 내부 구조를 이해했다면, 이제 실제 문서에서 데이터를 추출할 수 있습니다.이번 글에서는 HWPX 표준 문서(KS X 6101)를 포함해 공개된 HWPX 포맷 정보를 바탕으로, 문서 데이터를 Python 코드 예제를 통해 추출하는 방법을 알아보겠습니다.* 표준 문서는 e-나라표준인증에서 확인 가능합니다.* 수록된 예제 코드는 Python 내장 라이브러리만을 사용하여 작성되었으며, 데이터 추출 과정을 설명하기 위해 간단하게 작성된 점 참고 부탁드립니다.이전 글에서 설명했듯이, HWPX는 ZIP 파일 구조를 가진 XML 기반 포맷입니다.따라서 파일의 내용을 확인하려면 (1) 압축을 해제하고, (2) 내부 XML 문서를 분석하는 과정이 필요합니다.다행히 이 모든 기능은 Python에 기본적으로 내장되어 있어 별도의 라이브러리를 설치하지 않아도 됩니다.예제 코드에서는 아래의 두 가지 표준 라이브러리를 사용합니다.HWP의 ‘DocInfo’에 해당하는 정보들이 HWPX에서는 metadata.xml이나 settings.xml 등 여러 파일에 분산 저장되어 있습니다.이렇게 분산된 정보를 매번 파일에서 직접 읽어오는 것은 비효율적이므로, 필요한 정보를 미리 추출해 하나로 모아둘 데이터 모델(Data Model)을 먼저 정의하겠습니다.이제 각 파일에서 정보를 읽어와 위 Document 모델의 필드를 채우는 과정을 살펴보겠습니다.우선, zipfile 라이브러리를 사용해 HWPX 파일을 ZIP 파일 형태로 읽어옵니다.표준 문서에 설명되어 있듯이, 문서 버전에 따라 XML Namespace가 달라질 수 있습니다.따라서 별도로 구현한 extract_namespaces 함수를 통해 현재 문서에 저장된 Namespace를 추출하여 파싱 과정에 사용하도록 코드를 작성했습니다.표준 문서나 실제 파일을 참고해 ZIP 파일 내에서 데이터를 꺼내올 XML 파일 위치를 파악합니다.패키징 정보가 담겨있는 Contents/content.hpf 파일에서 spine에 저장된 순서대로 파일을 읽어야하지만, 이번 예시에서는 각 파일에 접근하여 정보를 추출하는 과정 위주로 설명하려고 합니다.HWPX의 여러 파일 중에서도 Contents/header.xml은 문서의 전반적인 정보와 모양 속성을 담고 있는 파일입니다.이번 장에서는 이 파일을 열어 구조를 살펴보고, 앞서 정의한 Document 데이터 모델을 채우기 위한 정보를 어떻게 추출하는지 알아보겠습니다.먼저,(1) zipf.namelist()를 통해 ZIP 파일에 저장된 파일 목록에 Contents/header.xml이 존재하는지 확인하고,(2) Namespace를 추출합니다.그 후,(3) ZipFile에서 해당 파일을 꺼내오고,(4) ElementTree로 파싱하면 header 변수에는 최상위 요소가 저장됩니다.이제 이 header 요소에서 직접 정보를 추출할 수 있습니다.가장 먼저 문서의 전체 구역 수를 나타내는 secCnt 속성부터 가져와 보겠습니다.최상위 요소의 속성을 먼저 가져왔으니, 이제 하위 요소들을 하나씩 탐색해 보겠습니다.실제 파일에서 보면 Contents/header.xml은 다음과 같은 형태로 저장되어 있습니다.각 요소와 속성의 의미를 알기 위해서는 표준 문서를 확인해야 합니다.이런 구조를 파악하면, 필요한 정보의 위치를 알고 해당 데이터에 접근할 수 있습니다.우리는 Document 모델을 채워야 하기 때문에 beginNum과 refList가 필요합니다.저장된 beginNum과 각 속성의 의미를 먼저 확인해 보면, beginNum 요소에는 문서 내에서 사용되는 각 객체들의 시작 번호가 담겨있다는 것을 알 수 있습니다.확인된 정보를 바탕으로 코드를 작성하여 데이터를 채워줍니다.저장된 refList에는 문서 내에서 사용되는 모양 정보 등이 담겨있습니다.refList의 fontface에 폰트 정보가 있고, 이를 실제 파일에서 다시 확인해보면 아래와 같은 내용을 확인할 수 있습니다.현재 코드 상으로는 저장된 폰트 개수만을 읽어왔지만, 하위 요소에서는 언어별로 어떤 폰트가 저장되어 있는지도 확인할 수 있습니다. 이 폰트 목록은 charProperties와 연결되어 본문 텍스트에 글자 모양으로 적용됩니다.위에 설명한 것과 동일한 방법으로, settings.xml 파일의 최상위 요소를 확인합니다.문서 내용을 바탕으로 CaretPosition 요소에서 저장된 커서 위치 정보를 추출할 수 있습니다.Contents/content.hpf 파일은 OPF 표준을 따르며, 패키징 주요 파일 목록을 담고 있습니다.특히 manifest 요소에서 문서에 포함된 이미지, OLE 객체와 같은 바이너리 데이터의 목록을 얻을 수 있습니다.문서에 포함된 바이너리 데이터 목록을 확인하고, 해당 데이터에 접근할 수 있도록 binary_data_list를 채우는 예제입니다.현재 코드에서는 binary_data_list를 채우고 Document 모델을 채우기 위해 이 목록의 전체 개수를 세어 binaryDataCount에 저장하는 역할만 수행했지만, 해당 경로에 접근하여 실제 이미지를 별도의 이미지 파일로 저장하는 것도 가능합니다.예를 들어, 리스트에 ‘BinData/image1.png’라는 경로가 있다면, zipfile 객체에서 이 경로로 실제 이미지의 바이너리 데이터를 읽어와 파일로 쓸 수 있습니다.바이너리 데이터들은 본문 텍스트 내의 특정 태그와 BinData 폴더 안의 실제 파일로 연결되어, 문서 내용을 구성하게 됩니다.지금까지 “한/글 문서 파일 형식 : HWPX 포맷 구조 살펴보기”와 KS X 6101 표준 문서에서 확인한 HWPX 파일의 내부 구조를 통해, 문서의 일부 데이터를 추출하고 Python 객체(Document)로 구조화하는 과정을 실제 문서와 예시 코드로 살펴보았습니다.다음 글에서는 문서에서 가장 중요한 본문 내용이 담긴 Section 파일을 분석하여 단락과 텍스트, 표의 내용을 추출하는 방법을 알아보겠습니다.HWPX 파일의 내부를 이해하고 다루는 데 도움이 되었길 바랍니다. 감사합니다.
python
6/18/2025

한/글 문서 파일 형식: Python을 통한 HWPX 포맷 파싱하기 (1)
“한/글 문서 파일 형식 : HWPX 포맷 구조 살펴보기”를 통해 HWPX 문서의 내부 구조를 이해했다면, 이제 실제 문서에서 데이터를 추출할 수 있습니다.이번 글에서는 HWPX 표준 문서(KS X 6101)를 포함해 공개된 HWPX 포맷 정보를 바탕으로, 문서 데이터를 Python 코드 예제를 통해 추출하는 방법을 알아보겠습니다.* 표준 문서는 e-나라표준인증에서 확인 가능합니다.* 수록된 예제 코드는 Python 내장 라이브러리만을 사용하여 작성되었으며, 데이터 추출 과정을 설명하기 위해 간단하게 작성된 점 참고 부탁드립니다.이전 글에서 설명했듯이, HWPX는 ZIP 파일 구조를 가진 XML 기반 포맷입니다.따라서 파일의 내용을 확인하려면 (1) 압축을 해제하고, (2) 내부 XML 문서를 분석하는 과정이 필요합니다.다행히 이 모든 기능은 Python에 기본적으로 내장되어 있어 별도의 라이브러리를 설치하지 않아도 됩니다.예제 코드에서는 아래의 두 가지 표준 라이브러리를 사용합니다.HWP의 ‘DocInfo’에 해당하는 정보들이 HWPX에서는 metadata.xml이나 settings.xml 등 여러 파일에 분산 저장되어 있습니다.이렇게 분산된 정보를 매번 파일에서 직접 읽어오는 것은 비효율적이므로, 필요한 정보를 미리 추출해 하나로 모아둘 데이터 모델(Data Model)을 먼저 정의하겠습니다.이제 각 파일에서 정보를 읽어와 위 Document 모델의 필드를 채우는 과정을 살펴보겠습니다.우선, zipfile 라이브러리를 사용해 HWPX 파일을 ZIP 파일 형태로 읽어옵니다.표준 문서에 설명되어 있듯이, 문서 버전에 따라 XML Namespace가 달라질 수 있습니다.따라서 별도로 구현한 extract_namespaces 함수를 통해 현재 문서에 저장된 Namespace를 추출하여 파싱 과정에 사용하도록 코드를 작성했습니다.표준 문서나 실제 파일을 참고해 ZIP 파일 내에서 데이터를 꺼내올 XML 파일 위치를 파악합니다.패키징 정보가 담겨있는 Contents/content.hpf 파일에서 spine에 저장된 순서대로 파일을 읽어야하지만, 이번 예시에서는 각 파일에 접근하여 정보를 추출하는 과정 위주로 설명하려고 합니다.HWPX의 여러 파일 중에서도 Contents/header.xml은 문서의 전반적인 정보와 모양 속성을 담고 있는 파일입니다.이번 장에서는 이 파일을 열어 구조를 살펴보고, 앞서 정의한 Document 데이터 모델을 채우기 위한 정보를 어떻게 추출하는지 알아보겠습니다.먼저,(1) zipf.namelist()를 통해 ZIP 파일에 저장된 파일 목록에 Contents/header.xml이 존재하는지 확인하고,(2) Namespace를 추출합니다.그 후,(3) ZipFile에서 해당 파일을 꺼내오고,(4) ElementTree로 파싱하면 header 변수에는 최상위 요소가 저장됩니다.이제 이 header 요소에서 직접 정보를 추출할 수 있습니다.가장 먼저 문서의 전체 구역 수를 나타내는 secCnt 속성부터 가져와 보겠습니다.최상위 요소의 속성을 먼저 가져왔으니, 이제 하위 요소들을 하나씩 탐색해 보겠습니다.실제 파일에서 보면 Contents/header.xml은 다음과 같은 형태로 저장되어 있습니다.각 요소와 속성의 의미를 알기 위해서는 표준 문서를 확인해야 합니다.이런 구조를 파악하면, 필요한 정보의 위치를 알고 해당 데이터에 접근할 수 있습니다.우리는 Document 모델을 채워야 하기 때문에 beginNum과 refList가 필요합니다.저장된 beginNum과 각 속성의 의미를 먼저 확인해 보면, beginNum 요소에는 문서 내에서 사용되는 각 객체들의 시작 번호가 담겨있다는 것을 알 수 있습니다.확인된 정보를 바탕으로 코드를 작성하여 데이터를 채워줍니다.저장된 refList에는 문서 내에서 사용되는 모양 정보 등이 담겨있습니다.refList의 fontface에 폰트 정보가 있고, 이를 실제 파일에서 다시 확인해보면 아래와 같은 내용을 확인할 수 있습니다.현재 코드 상으로는 저장된 폰트 개수만을 읽어왔지만, 하위 요소에서는 언어별로 어떤 폰트가 저장되어 있는지도 확인할 수 있습니다. 이 폰트 목록은 charProperties와 연결되어 본문 텍스트에 글자 모양으로 적용됩니다.위에 설명한 것과 동일한 방법으로, settings.xml 파일의 최상위 요소를 확인합니다.문서 내용을 바탕으로 CaretPosition 요소에서 저장된 커서 위치 정보를 추출할 수 있습니다.Contents/content.hpf 파일은 OPF 표준을 따르며, 패키징 주요 파일 목록을 담고 있습니다.특히 manifest 요소에서 문서에 포함된 이미지, OLE 객체와 같은 바이너리 데이터의 목록을 얻을 수 있습니다.문서에 포함된 바이너리 데이터 목록을 확인하고, 해당 데이터에 접근할 수 있도록 binary_data_list를 채우는 예제입니다.현재 코드에서는 binary_data_list를 채우고 Document 모델을 채우기 위해 이 목록의 전체 개수를 세어 binaryDataCount에 저장하는 역할만 수행했지만, 해당 경로에 접근하여 실제 이미지를 별도의 이미지 파일로 저장하는 것도 가능합니다.예를 들어, 리스트에 ‘BinData/image1.png’라는 경로가 있다면, zipfile 객체에서 이 경로로 실제 이미지의 바이너리 데이터를 읽어와 파일로 쓸 수 있습니다.바이너리 데이터들은 본문 텍스트 내의 특정 태그와 BinData 폴더 안의 실제 파일로 연결되어, 문서 내용을 구성하게 됩니다.지금까지 “한/글 문서 파일 형식 : HWPX 포맷 구조 살펴보기”와 KS X 6101 표준 문서에서 확인한 HWPX 파일의 내부 구조를 통해, 문서의 일부 데이터를 추출하고 Python 객체(Document)로 구조화하는 과정을 실제 문서와 예시 코드로 살펴보았습니다.다음 글에서는 문서에서 가장 중요한 본문 내용이 담긴 Section 파일을 분석하여 단락과 텍스트, 표의 내용을 추출하는 방법을 알아보겠습니다.HWPX 파일의 내부를 이해하고 다루는 데 도움이 되었길 바랍니다. 감사합니다.
2025.06.18
python

좋아요

별로에요

한/글 문서 파일 형식: Python을 통한 HWP 포맷 파싱하기 (1)
지난 시간에는 HWP 문서 파일 구조에 대한 공식 문서를 바탕으로, HWP 포맷을 분석하여 문서 내부 정보의 구성과 읽는 방법을 살펴보았습니다. 이번 글에서는 Python 코드 예제를 통해 HWP 파일을 파싱하는 방법을 알아보겠습니다.HWP 파일은 복잡한 구조를 가지고 있어, 특히 본문(BodyText) 같은 정보는 다양한 요소들이 결합되어 있어 파싱이 어려울 수 있습니다. 그래서 이번 글에서는 비교적 간단한 DocInfo 영역을 읽어보는 것으로 HWP 파일 파싱을 시작해 보겠습니다.HWP 파일은 Compound File Structure라는 이진(Binary) 형식으로 이루어져 있습니다. 이 구조는 저장소 (Storage)와 스트림(Stream)으로 이루어져 있어, 파일 내부에 포함된 다양한 데이터들을 효율적으로 관리할 수 있도록 합니다.HWP 파일의 데이터를 파싱하고 접근하기 위해서는 이러한 바이너리 구조, 특히 Storage와 Stream을 읽을 수 있는 라이브러리가 필요합니다. 또한 HWP 파일은 용량을 최소화하기 위해 압축 기능을 사용하고 있는데, 이때 zlib 압축 알고리즘이 사용됩니다.이번 글에서는 Python을 이용해 HWP 포맷을 파싱하기 위해 두 가지 라이브러리를 사용합니다.이 두 라이브러리를 사용하면 HWP 파일의 이진(Binary) 구조를 해석하고, 압축된 데이터를 복호화하여 원본 데이터를 편리하게 확인할 수 있습니다.HWP 파일의 전체적인 구조는 파일 인식 정보, 문서 정보, 본문 등 여러 부분으로 나뉩니다. 이 중 HWP 파일을 파싱하는 데 있어 주목해야 할 부분은 레코드 구조와 압축/암호화입니다.FileHeader는 파일이 HWP 문서임을 알리는 단순한 인식 정보가 고정된 길이로 나열되어 있습니다. 반면, 실제 문서의 내용을 담고 있는 DocInfo(문서정보)와 BodyText/Section0(본문)은 다릅니다. 이들은 레코드 구조로 데이터가 저장되며, 필요에 따라 압축 또는 암호화가 적용될 수 있습니다. 즉, DocInfo와 BodyText 같은 핵심 데이터는 단순한 정보 나열을 넘어 체계적인 구조와 보안 기능을 갖추고 있습니다.특히 아래 그림에서 보시는 것과 같은 DocInfo 데이터를 온전히 얻으려면, 압축된 DocInfo 데이터를 복호화 하는 과정이 필수적입니다.위에서 언급한 라이브러리를 활용해 HWP파일에서 DocInfo 스트림을 읽어오는 예제를 함께 살펴보겠습니다.HWP 파일에서 DocInfo를 읽기 위해선 크게 3가지 단계를 거쳐야 합니다. 위 코드 예제를 통해 각 단계를 자세히 살펴보겠습니다.• olefile 라이브러리를 이용해 HWP 파일의 내부 구조에 접근할 준비를 합니다.• olefile 라이브러리를 이용해 DocInfo 스트림을 열어서 읽어들입니다.• 마지막으로 읽어 들인 DocInfo 데이터를 zlib 라이브러리를 이용해서 압축을 해제하고 문자열로 변환한다.이런 과정을 통해 DocInfo의 데이터를 최종적으로 확인할 수 있게 됩니다.이제 읽어들인 HWP 파일의 DocInfo 데이터 영역을 아래의 레코드 구조 스펙에 따라 파싱하는 방법을 예제를 통해 살펴보겠습니다.• Tag ID : 레코드가 나타내는 데이터의 종류로 10비트가 사용된다.• Level : 연관된 레코드의 논리적인 묶음을 표현하기 위한 정보로 10비트가 사용된다.• Size : 데이터 영역의 길이를 바이트 단위로 나타낸다. 4095 바이트 이상의 데이터 일 때 레코드 헤더에 DWORD가 추가된다.레코드 헤더를 읽는 단계는 크게 3단계로 나뉩니다.• 레코드 헤더에 해당하는 32비트를 읽는다. ([그림 1] 참고)• 읽어온 헤더는 리틀 엔디안(Little-Endian) 방식으로 해석해야 하며 다음과 같이 3가지 필드로 분리한다.• TagID (10비트) : 레코드의 종류를 나타내는 식별자• Size (12비트) : 레코드 데이터의 길이• 만약 위에서 읽어온 Size의 값이 4095(이진수로 1111 1111 1111) 라면 추가로 4바이트(DWORD)를 더 읽어와 실제 레코드의 크기로 사용한다.이제 레코드를 읽는 방법을 알았으니 DocInfo에 저장된 레코드들을 실제로 읽어보겠습니다.DocInfo 스트림을 읽기 시작하면, 가장 먼저 마주하게 되는 레코드 헤더는 다음과 같습니다.이 정보를 바탕으로 [표 2] 문서 정보의 데이터 레코드를 확인해 보면 Tag ID는 HWPTAG_DOCUMENT_PROPERTIES에 해당함을 알 수 있습니다. 이는 현재 읽고 있는 레코드가 문서 속성에 대한 정보를 담고 있다는 의미입니다.2. 레코드 헤더 정보를 이용하여 데이터 레코드 읽기이전에 DocInfo 스트림에서 Tag ID가 16인 HWPTAG_DOCUMENT_PROPERTIES를 읽어왔으므로 [표 2] 문서 정보의 데이터 레코드를 참고해서 이 레코드에 저장된 데이터를 읽어보겠습니다.문서 속성에 해당하는 HWPTAG_DOCUMENT_PROPERTIES에 어떤 데이터들이 저장되는지 확인할 수 있습니다. 이 정보를 기반으로 샘플 예제를 통해 데이터를 추출해 보겠습니다.제공된 샘플 코드를 통해 HWPTAG_DOCUMENT_PROPERTIES 레코드를 파싱 한 결과, 다음과 같은 문서 속성들을 확인할 수 있습니다.이 결과를 통해 HWP 파일이 논리적으로 연관된 데이터를 레코드 형태로 저장하며, 각 레코드에는 헤더 정보가 포함되어 데이터의 종류와 길이를 명확하게 식별할 수 있도록 설계되었음을 보여줍니다.DocInfo 스트림에서 HWPTAG_DOCUMENT_PROPERTIES 레코드를 파싱 했으니, 이제 다음 레코드로 저장되는 HWPTAG_ID_MAPPINGS를 읽어볼 차례입니다. [표 2] 문서 정보의 데이터 레코드에 명시된 대로 이 레코드는 BinData나 FaceName과 같이 ID로 매핑되는 다양한 데이터들의 개수 정보를 담고 있습니다.여기서 주목해야 할 부분은 HWPTAG_ID_MAPPINGS의 전체 길이가 72바이트이며 “doc version에 따라 가변적”이라는 설명입니다. 이는 HWP 포맷이 확장됨에 따라 파일 버전에 따라 이 레코드의 실제 길이가 달라질 수 있음을 시사합니
6/18/2025

한/글 문서 파일 형식: Python을 통한 HWP 포맷 파싱하기 (1)
지난 시간에는 HWP 문서 파일 구조에 대한 공식 문서를 바탕으로, HWP 포맷을 분석하여 문서 내부 정보의 구성과 읽는 방법을 살펴보았습니다. 이번 글에서는 Python 코드 예제를 통해 HWP 파일을 파싱하는 방법을 알아보겠습니다.HWP 파일은 복잡한 구조를 가지고 있어, 특히 본문(BodyText) 같은 정보는 다양한 요소들이 결합되어 있어 파싱이 어려울 수 있습니다. 그래서 이번 글에서는 비교적 간단한 DocInfo 영역을 읽어보는 것으로 HWP 파일 파싱을 시작해 보겠습니다.HWP 파일은 Compound File Structure라는 이진(Binary) 형식으로 이루어져 있습니다. 이 구조는 저장소 (Storage)와 스트림(Stream)으로 이루어져 있어, 파일 내부에 포함된 다양한 데이터들을 효율적으로 관리할 수 있도록 합니다.HWP 파일의 데이터를 파싱하고 접근하기 위해서는 이러한 바이너리 구조, 특히 Storage와 Stream을 읽을 수 있는 라이브러리가 필요합니다. 또한 HWP 파일은 용량을 최소화하기 위해 압축 기능을 사용하고 있는데, 이때 zlib 압축 알고리즘이 사용됩니다.이번 글에서는 Python을 이용해 HWP 포맷을 파싱하기 위해 두 가지 라이브러리를 사용합니다.이 두 라이브러리를 사용하면 HWP 파일의 이진(Binary) 구조를 해석하고, 압축된 데이터를 복호화하여 원본 데이터를 편리하게 확인할 수 있습니다.HWP 파일의 전체적인 구조는 파일 인식 정보, 문서 정보, 본문 등 여러 부분으로 나뉩니다. 이 중 HWP 파일을 파싱하는 데 있어 주목해야 할 부분은 레코드 구조와 압축/암호화입니다.FileHeader는 파일이 HWP 문서임을 알리는 단순한 인식 정보가 고정된 길이로 나열되어 있습니다. 반면, 실제 문서의 내용을 담고 있는 DocInfo(문서정보)와 BodyText/Section0(본문)은 다릅니다. 이들은 레코드 구조로 데이터가 저장되며, 필요에 따라 압축 또는 암호화가 적용될 수 있습니다. 즉, DocInfo와 BodyText 같은 핵심 데이터는 단순한 정보 나열을 넘어 체계적인 구조와 보안 기능을 갖추고 있습니다.특히 아래 그림에서 보시는 것과 같은 DocInfo 데이터를 온전히 얻으려면, 압축된 DocInfo 데이터를 복호화 하는 과정이 필수적입니다.위에서 언급한 라이브러리를 활용해 HWP파일에서 DocInfo 스트림을 읽어오는 예제를 함께 살펴보겠습니다.HWP 파일에서 DocInfo를 읽기 위해선 크게 3가지 단계를 거쳐야 합니다. 위 코드 예제를 통해 각 단계를 자세히 살펴보겠습니다.• olefile 라이브러리를 이용해 HWP 파일의 내부 구조에 접근할 준비를 합니다.• olefile 라이브러리를 이용해 DocInfo 스트림을 열어서 읽어들입니다.• 마지막으로 읽어 들인 DocInfo 데이터를 zlib 라이브러리를 이용해서 압축을 해제하고 문자열로 변환한다.이런 과정을 통해 DocInfo의 데이터를 최종적으로 확인할 수 있게 됩니다.이제 읽어들인 HWP 파일의 DocInfo 데이터 영역을 아래의 레코드 구조 스펙에 따라 파싱하는 방법을 예제를 통해 살펴보겠습니다.• Tag ID : 레코드가 나타내는 데이터의 종류로 10비트가 사용된다.• Level : 연관된 레코드의 논리적인 묶음을 표현하기 위한 정보로 10비트가 사용된다.• Size : 데이터 영역의 길이를 바이트 단위로 나타낸다. 4095 바이트 이상의 데이터 일 때 레코드 헤더에 DWORD가 추가된다.레코드 헤더를 읽는 단계는 크게 3단계로 나뉩니다.• 레코드 헤더에 해당하는 32비트를 읽는다. ([그림 1] 참고)• 읽어온 헤더는 리틀 엔디안(Little-Endian) 방식으로 해석해야 하며 다음과 같이 3가지 필드로 분리한다.• TagID (10비트) : 레코드의 종류를 나타내는 식별자• Size (12비트) : 레코드 데이터의 길이• 만약 위에서 읽어온 Size의 값이 4095(이진수로 1111 1111 1111) 라면 추가로 4바이트(DWORD)를 더 읽어와 실제 레코드의 크기로 사용한다.이제 레코드를 읽는 방법을 알았으니 DocInfo에 저장된 레코드들을 실제로 읽어보겠습니다.DocInfo 스트림을 읽기 시작하면, 가장 먼저 마주하게 되는 레코드 헤더는 다음과 같습니다.이 정보를 바탕으로 [표 2] 문서 정보의 데이터 레코드를 확인해 보면 Tag ID는 HWPTAG_DOCUMENT_PROPERTIES에 해당함을 알 수 있습니다. 이는 현재 읽고 있는 레코드가 문서 속성에 대한 정보를 담고 있다는 의미입니다.2. 레코드 헤더 정보를 이용하여 데이터 레코드 읽기이전에 DocInfo 스트림에서 Tag ID가 16인 HWPTAG_DOCUMENT_PROPERTIES를 읽어왔으므로 [표 2] 문서 정보의 데이터 레코드를 참고해서 이 레코드에 저장된 데이터를 읽어보겠습니다.문서 속성에 해당하는 HWPTAG_DOCUMENT_PROPERTIES에 어떤 데이터들이 저장되는지 확인할 수 있습니다. 이 정보를 기반으로 샘플 예제를 통해 데이터를 추출해 보겠습니다.제공된 샘플 코드를 통해 HWPTAG_DOCUMENT_PROPERTIES 레코드를 파싱 한 결과, 다음과 같은 문서 속성들을 확인할 수 있습니다.이 결과를 통해 HWP 파일이 논리적으로 연관된 데이터를 레코드 형태로 저장하며, 각 레코드에는 헤더 정보가 포함되어 데이터의 종류와 길이를 명확하게 식별할 수 있도록 설계되었음을 보여줍니다.DocInfo 스트림에서 HWPTAG_DOCUMENT_PROPERTIES 레코드를 파싱 했으니, 이제 다음 레코드로 저장되는 HWPTAG_ID_MAPPINGS를 읽어볼 차례입니다. [표 2] 문서 정보의 데이터 레코드에 명시된 대로 이 레코드는 BinData나 FaceName과 같이 ID로 매핑되는 다양한 데이터들의 개수 정보를 담고 있습니다.여기서 주목해야 할 부분은 HWPTAG_ID_MAPPINGS의 전체 길이가 72바이트이며 “doc version에 따라 가변적”이라는 설명입니다. 이는 HWP 포맷이 확장됨에 따라 파일 버전에 따라 이 레코드의 실제 길이가 달라질 수 있음을 시사합니
2025.06.18

좋아요

별로에요